Все хорошо, пока вам нужно прогнать JS исходники через бабели-шмабели для создания бандла, но начинается сущий ад, когда вы захотите написать тесты для вашего сайта или бибилиотеки. Проблема в том, что все тест-фреймворки используют специфичные функции из ноды или/и написаны в ES5. Таким образом запуск E2E тестов становятся не тривиальной задачей и предлагает танцы с бубном транспиляций и sourcemap-ов для покрытия кода. Вы же не хотите что бы ошибки указывали не туда?



В этой статье я опишу свой опыт использования Puppeteer для небольшой задачи,
и как я запустил ES6 модули в ноде и браузере, имея всего один исходник для тестов без сборщиков.


Зачем вообще Puppeteer, спросите вы, почему не WebDriver? Просто я заметил, как мучаются создатели популярных опенсорс WebGL библиотек, например, у них есть в наличии 300 страниц с примерами, каждый из которых может сломаться при любом коммите. Они проверяют их после каждого изменения, и если что-то забыли открыть — извините ?\_(?)_/?, сломалось. Коли никто до сих пор не решил данную задачу, то я решил попробовать это, пока в своей небольшой либе. Первая мысль которая была, это запустить headless-gl, но он морально устарел. Node-gles уже поддерживает WebGL2, но не редкое расширение, которое я использовал. WebDriver? Даже не пробовал. Не уверен что это возможно, python/C#/Java были мне не нужны, а требовался JS/TS с последней нодой и с новейшим браузерным API, так залетающие фичи могут быть по последней спеке.


Почему ES6 модули? Поддержка WebGL и ES6 в браузерах примерно на одном уровне. А с модулями бандл или нет, пусть решает пользователь, просто можно собрать обе версии. Но оказывается для юнит тестирования, очень удобно использовать версию именно с модулями, так как sourcemap-ы добываются очень просто, а тесты после этого без каких либо лишних телодвижений можно запускать как в ноде, так и в браузере. Запуская их в puppeteer, E2E с покрытием кода дается почти бесплатно. Typescript c таргетом в ES6 наверно был нужен, но на маленьком проекте покрытым тестами, пойдет и обычный js.


Итак, хватит введений, я поставил в проект puppeteer и puppeteer-to-istanbul и написал такую обертку


// puppeteer.js

import puppeteer from 'puppeteer';
import pup2ist from 'puppeteer-to-istanbul';

(async () => {
  const browser = await puppeteer.launch({
    headless: process.env.HEADLESS, // headless customization
    slowMo: 250                     // good fature for new configs
  });
  const page = (await browser.pages())[0];

  // enable coverage
  await page.coverage.startJSCoverage();
  await page.coverage.startCSSCoverage();

  // some additional code with console events here...

  // navigate to unit test page
  await page.goto('http://127.0.0.1:1234/');

  // disable coverage
  const jsCoverage = await page.coverage.stopJSCoverage();
  const cssCoverage = await page.coverage.stopCSSCoverage();
  pup2ist.write([...jsCoverage, ...cssCoverage])

  await new Promise(resolve => setTimeout(resolve, 6000));
  await browser.close();
})();

Которую можно запускать командой node --experimental-modules --no-warnings ./test/puppeteer.js с 11+ нодой, или даже без флагов на node 13.2+. Конечно можно использовать require, то се… Но зачем? Это же вообще бэкенд, тут поддержка у клиентов даже не нужна! Следующий код из package.json позволяет нам кастомизировать HEADLESS загрузку в консоли и в CI облаке, если требуются различные настройки для них. В travs/circle-ci наверно будет стоять linux и можно устанавливать там переменные среды в таком формате. concurrently открывает параллельно два процесса в одной консоли.


// package.json
{
  //bla-bla...

  "type": "module",      // this line indicates that we are using es6 modules
  "scripts": {
    "test": "node --experimental-modules --no-warnings ./test/puppeteer.js",
    "server": "http-server -c-1 -p 1234",
    "not-bad-cmd--dude": "concurrently -k -s first \"npm:test\" \"npm:server\"",
    "ci": "HEADLESS=true concurrently -k -s first \"npm:test\" \"npm:server\"",
  }
}

На локальной машине после ввода команды npm run server будет запускаться http сервер, а на npm run test puppeteer в отдельном окне окне хрома. Вот в принципе и все что нужно знать про puppeteer. Дальнейшие примеры по скриншотам, эмуляциям девайсов, админкам и т.д., расположены тут. Кстати, вместе с пакетом puppeteer вам установился отдельный хром в node_modules, если он вам не нужен, замените его на puppeteer-core или puppeteer-firefox. Следует заметить что в примере выше мы бесплатно получили JS/CSS покрытие кода которое пишется в папку .nyc_output, пока не будем заострять на этом внимание, на данном этапе нам от этого не холодно не жарко, но если что — оно там есть, и статистика покрытия тестов почти готова к просмотру.



Теперь перейдем к самим тестам, пытаясь выбрать в чем я буду запускать E2E в моей небольшой либе, я наткнулся на следующие графики, где сравнивали производительности фреймворков для тестирования. Наверно, время выполнения не так важно, но когда какой нибудь Jest запускает их в 10 раз медленнее, возникает вопрос "что это, и зачем это нужно". Основной критерий выбора это был запуск es6 со строчкой <script type="module" src="./test.js"></script> в html странице. Так как на момент написания моего кода, нода еще не поддерживала в полной мере ES6 (вчера вышла 12.3 в которой сняли флаги). Я решил, что если взять фреймворк с исходниками на TS или же ES6+, то оно точно должно запускаться. Вообще, наверное можно было взять какой нибудь mocha, объявить его выше на странице и обращаться к объявленному классу, но что будет если выпадет ошибка? В общем, можете назвать свой любимый тест раннер тут. Я лишь скажу что Zora поддерживает TAP формат, и это значит для нее подходит целый зоопарк TAP пожирателей. В ней есть большинство ассертов, она поддерживает async, она одна из самых быстрых, написана на чистом ES6 без зависимостей от самой ноды. Мне показалась она настоящим бриллиантом для небольших проектов.


В итоге у меня получились какие-то такие тесты, которые работают как в браузере так и в ноде. В документации по Zora есть исчерпывающая инструкция по ассертам и группировкам команд.


// test.js
import MyLibrary from '../dist/my-library.module.js';
import { test } from 'https://cdn.jsdelivr.net/npm/zora@3.0.3/dist/bundle/module.js';

test('CPU', async (t) => {
  // some stuff here
  t.ok(tfps != null, 'fps = ' + (tfps != null ? tfps.toFixed(1) : 'null'));
  t.ok(tcpu != null, 'cpu = ' + (tcpu != null ? tcpu.toFixed(1) : 'null'));
});

test('Memory', async (t) => {
  // some stuff here
  t.ok(tmem != null, 'mem = ' + (tmem != null ? tmem.toFixed(1) : 'null'));
});

// etc...

Для того что бы показать консоль без сборок, мне пришлось сделать подобный снифер. Голая консоль не очень презентабельна, можно было бы подключить TAP вывод куда-нибудь для наведения марафета. Но самое забавное — это то, что результаты тестов на вашем клиенте можно посмотреть в онлайне. Так же, совершенно этот же код запускается в CI по любому коммиту.


<!DOCTYPE html>
<html lang="en">
<head>
  <!-- some declarations in head -->
</head>
<body>
  <!-- some declarations in body -->

  <script>
    const addSniffer = (spyTarget) => function() {
      spyTarget.apply(window.console, arguments);
      sniffer([...arguments]);
    }
    window.console.log = addSniffer(window.console.log);
    window.console.error = addSniffer(window.console.error);

    let screen = document.getElementById('screen');
    function sniffer(string) {
      let screen = document.getElementById("screen");
      string.forEach(line => {
        let div = document.createElement("div");
        let text = document.createTextNode(line);
        div.appendChild(text)
        screen.appendChild(div);
      });
    }
  </script>

  <script type="module" src="./test.js"></script>

</body>
</html>

Но это еще не все, имея готовые тесты, можно подключить таких ботов как renovate/greenkeeper/dependabot, которые бы обновляли зависимости в вашей библиотеке, и делали автокоммиты, предварительно проверяя корректность обновлений. А travis/github-ci/circle-ci бы выкладывали бы новую версию npm пакетов.


Например такой конфиг от renovate, делает автокоммиты по воскресеньям, и поднимает версию


{
  "automerge": true,
  "automergeType": "branch",
  "bumpVersion": "patch",
  "schedule": ["on sunday"],
  "ignorePaths": [".circleci"]
}

А travis, когда вы сами подняли версию или же какой то бот, может автоматом выкладывать пакет в npm. Для этого нужно создать аккаунт на travis-ci.org, включить f2a как описано в данной статье, ввести два секретных ключа $NPM_EMAIL и $NPM_TOKEN, и создать подобный конфиг.


language: node_js
node_js: '12'

script:
  - npm run ci

deploy:
  provider: npm
  email: $NPM_EMAIL
  api_key: $NPM_TOKEN
  on:
    branch: master

Итого, как-то так можно, но сложно избавится от оповещений от гитхаба о том, что в какой-то зависимости появилась уязвимость :D

Комментарии (1)


  1. mrG0bliN
    23.11.2019 20:27

    сочная статья спасибо