Автоматизация тестирования — единственный способ обеспечить качество в среде CI (непрерывной интеграции). Тестирование серверной логики и веб-интерфейсов, хотя и занимает много времени и трудно обеспечить точность, хорошо документировано, и существуют передовые методы.

Генерация PDF — функция, часто используемая в продуктах, ориентированных на потребителя (например, подтверждение бронирования, анализ данных, персонализированные документы планирования). Тем не менее, несмотря на то, что часто это критически важная функция, тестирование сгенерированных PDF-файлов обычно упускается из виду.

Мы поэкспериментировали с новым подходом к тестированию PDF, объединив Puppeteer (способ манипулирования безголовым Chrome), среду тестирования jest JavaScript и расширение jest визуальной регрессии от American Express.

Основной предпосылкой визуального регрессионного тестирования является сравнение заведомо исправных снимков интерфейса с снимками, созданными после изменения базового кода. С ошибкой сборки, если обнаруживается разница в пикселях выше некоторого порога.

Для этого нам нужно:

  1. Способ преобразования страницы PDF в снимки изображений, которые могут использоваться инструментом сравнения изображений. Формат PDF не подходит для сравнения и не работает с существующими инструментами сравнения изображений. Puppeteer сможет генерировать PNG для каждой страницы PDF.
  2. Фреймворк для запуска тестов (шутка).
  3. Инструмент для сравнения сгенерированных снимков с новыми снимками. (шутка-изображение-моментальный снимок).

Создание моментальных снимков из PDF

Puppeteer — безголовый экземпляр хрома. Его можно использовать для создания визуальных снимков веб-сайтов для сравнения изображений с помощью простой функции «скриншот».

const puppeteer = require('puppeteer');
(async () => {
   const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('https://example.com');
   await page.screenshot({path: 'example.png'});
   await browser.close();
})();

В Chrome есть встроенный просмотрщик PDF, и Puppeteer может манипулировать Chrome, чтобы открыть URL-адрес или файл. Таким образом, Puppeteer может открыть файл PDF, а затем использовать его для создания снимка экрана страницы. В приведенном выше примере мы можем изменить строку 6, чтобы использовать файловый протокол для открытия локального файла PDF.

await page.goto(“file:///Users/me/project/test.pdf”)

Одной из проблем с этим подходом является полоса прокрутки в правой части окна просмотра. Из-за того, как хром загружает свою полосу прокрутки, было бы неплохо удалить ее из моментального снимка, поскольку это может добавить ненадежность и проблемы с синхронизацией в тестах.

К счастью, Puppeteer может использовать параметр клипа в своей функции скриншота, чтобы обрезать скриншот. Например:

page.screenshot({clip: {
   x: 0,
   y: 0,
   width: 790,
   height: 600,
}});

Это удалит полосу прокрутки со снимка экрана. Вы можете редактировать значения, основанные на точке обзора, установленной для окна, чтобы удалить только полосу прокрутки. Chrome также отображает имя файла в средстве просмотра PDF, поэтому, если вновь созданные файлы имеют имена, отличные от исходных файлов, то обрезку можно расширить, чтобы исключить верхнюю часть средства просмотра PDF, где отображаются имена файлов.

В шутку

Jest — это восхитительная платформа для тестирования JavaScript с упором на простоту.

Добавить шутку в существующий JS-проект так же просто, как:

yarn add --dev jest
npm install --save-dev jest # Yes, it's cool again!

Полную информацию о начале работы с jest можно найти здесь: https://jestjs.io/docs/en/getting-started.

Тогда наш тестовый шаблон будет выглядеть следующим образом

describe('pdf suite', () => {
   it('works', async () => {
      expect(1).toBe(1)
   });
});

У American Express есть отличное расширение для jest, обеспечивающее сравнение изображений для визуального регрессионного тестирования. Его можно добавить следующим образом:

yarn add -D jest-image-snapshot
npm i --save-dev jest-image-snapshot

Затем встроенный объект ожидания Jest можно расширить следующим образом:

const { toMatchImageSnapshot } = require('jest-image-snapshot');
expect.extend({ toMatchImageSnapshot });

Полную документацию по настройке можно найти здесь: https://github.com/americanexpress/jest-image-snapshot

Настройка браузера для шутки

Jest, как и большинство инструментов тестирования, имеет функции настройки и демонтажа, которые можно добавить в набор. Они могут настраивать и отключать общие ресурсы, используемые несколькими тестами, а браузер — это ресурс, который следует использовать совместно, чтобы сократить время выполнения теста и дублирование кода в ваших тестах.

const puppeteer = require('puppeteer');

describe('pdf suite', () => {
   let browser;
   beforeAll(async () => {
      browser = await puppeteer.launch({ headless: false })
   });

   it('works', async () => {
      expect(1).toBe(1)
   });
   
   afterAll(async () => {
      await browser.close();
   });
});

Тестирование первой страницы

Код скриншота Puppeteer теперь можно комбинировать с настройкой сравнения шутливых изображений.

const puppeteer = require('puppeteer');

describe('pdf suite', () => {
   let browser;
   beforeAll(async () => {
      browser = await puppeteer.launch({ headless: false })
   });

   it('works', async () => {
      const page = await browser.newPage()
      await page.goto("file:///Users/me/project/test.pdf")
      await page.waitFor(500); // depends on the size of your file,     requires some manual configuring.
      const image = await page.screenshot({clip: {
         x: 0,
         y: 0,
         width: 790,
         height: 600,
      }});
      expect(image).toMatchImageSnapshot({
         failureThreshold: '0.10',
         failureThresholdType: 'percent',
         includeAA: true,
      });
   });
   afterAll(async () => {
      await browser.close();
   });
});

Теперь мы можем отредактировать проверяемый PDF-файл, повторно запустить его и получить процент сбоев и визуальное различие, которое можно проверить вручную.

Если мы возьмем базовый 1-страничный PDF.

Затем сгенерируйте наш первый снимок.

Если мы отредактируем оригинал, переместив заголовок, изменив текст подзаголовка и добавив кружок.

Затем после повторного запуска тестов мы получим визуальную разницу, показывающую все измененные компоненты красным цветом.

Переход на первую страницу

Очевидно, что многие PDF-файлы имеют длину более одной страницы…

К счастью, мы можем имитировать нажатия клавиш с помощью Puppeteer. Вместо того, чтобы писать сложную логику прокрутки, мы можем имитировать нажатие стрелки вправо, ярлык Chrome для перехода к следующей странице в средстве просмотра PDF.

await page.keyboard.press(‘ArrowRight’);

Предполагая, что мы заранее знаем количество страниц документа, мы можем построить базовый цикл for для страниц документа, делая снимки экрана и нажимая стрелку вправо на каждой итерации.

it('works', async () => {
   const numberOfPages = 20;
   const page = await browser.newPage()
   await page.goto("file:///Users/me/project/test.pdf")
   await page.waitFor(500);
   for(let i=0; i < numberOfPages; i++) {
      const image = await page.screenshot({clip: {
         x: 0,
         y: 0,
         width: 780,
         height: 600,
      }});
      expect(image).toMatchImageSnapshot({
         failureThreshold: '0.07',
         failureThresholdType: 'percent',
         includeAA: true,
      });
      await page.keyboard.press('ArrowRight');
   }
});

Срывать…

В заключение, мы использовали описанный выше подход для выявления проблем в больших сгенерированных отчетах в формате PDF со сложными графиками, стилями и мультимедиа. Тесты сэкономили часы ручной проверки и выявили проблемы, о которых мы даже не подозревали.