Puppeteer c ES6 в ноде и браузере, или почему Zora это лучший тест-фреймворк в своей нише

    Все хорошо, пока вам нужно прогнать 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

    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 1

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

      Only users with full accounts can post comments. Log in, please.