Перейти к основному содержимому

События testplane

Обзор

Как устроено описание событий

Ниже описаны все события testplane, на которые можно подписаться в своем плагине.

Описание каждого события начинается с тегов, которые представлены следующими вариантами:

  • sync или async обозначают, соответственно, синхронный и асинхронный режимы вызова обработчика события;
  • master обозначает, что данное событие доступно из мастер-процесса testplane;
  • worker обозначает, что данное событие доступно из воркеров (подпроцессов) testplane;
  • can be intercepted обозначает, что данное событие можно перехватить и соответственно, изменить.

Далее идут:

  • описание обстоятельств, при которых триггерится событие;
  • сниппет с кодом, показывающим как на него подписаться;
  • параметры обработчика события;
  • и опционально, примеры использования данного события в плагине или плагинах.

Схема генерации событий

Генерация testplane-событий

Описание последовательности событий

Testplane можно запускать как через CLI (командную строку), так и через её API: из скрипта с помощью команды run.

После запуска testplane загружает все плагины и переходит к парсингу CLI, если она была запущена через CLI, или сразу к стадии инициализации, если её запустили через API.

Парсинг CLI

Если testplane была запущена через CLI, то она триггерит событие CLI. Любой плагин может подписаться на это событие, чтобы добавить свои опции и команды к testplane до того, как testplane распарсит CLI.

Если testplane была запущена с помощью API, то стадия парсинга CLI будет пропущена и сразу же начнется стадия инициализации.

Инициализация

Во время инициализации testplane триггерит событие INIT. Это событие срабатывает всего 1 раз за весь запуск testplane. Подписавшись на это событие, плагины могут выполнить всю необходимую им подготовительную работу: открыть и прочитать какие-то файлы, поднять dev-сервер, инициализировать структуры данных, и т. д. и т. п.

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

Количество воркеров, которые запускает testplane, регулируется параметром workers в разделе system конфига testplane. При запуске очередного воркера testplane триггерит специальное событие NEW_WORKER_PROCESS.

к сведению

Все тесты testplane запускает в воркерах, чтобы не столкнуться с ограничениями по памяти и CPU для мастер-процесса. Как только в воркере количество выполненных тестов достигнет значения testsPerWorker, воркер завершит свою работу, и будет запущен новый воркер. Соответственно, будет снова послано событие NEW_WORKER_PROCESS.

Чтение тестов

После этого testplane читает все тесты с файловой системы в мастер-процессе. Посылая для каждого файла, перед тем как его прочитать, событие BEFORE_FILE_READ и после прочтения — событие AFTER_FILE_READ.

После того, как все тесты будут прочитаны, триггерится событие AFTER_TESTS_READ.

Запуск тестов

Затем testplane посылает события RUNNER_START и BEGIN. И стартует новую сессию (браузерную сессию), в которой будут выполняться тесты. При старте сессии testplane триггерит событие SESSION_START.

к сведению

Если количество тестов, выполненных в рамках одной сессии, достигнет значения параметра testsPerSession, то testplane завершит сессию, стриггерив событие SESSION_END, и запустит новую, послав событие SESSION_START.

Если тест при выполнении упадет с критической ошибкой, то testplane:

  • досрочно удалит сессию и браузер, который с ней связан;
  • создаст новую сессию;
  • запросит новый браузер, и привяжет его к новой сессии.

Это нужно для того, чтобы сбой в сессии при выполнении одного из тестов не начал влиять на запуски последующих тестов.

После создания новой сессии testplane переходит к выполнению тестов. Все тесты выполняются в воркерах, но сам запуск и сбор результатов прогона тестов осуществляется в рамках мастер-процесса. Мастер-процесс триггерит событие SUITE_BEGIN для describe-блоков в файле с тестами и TEST_BEGIN для it-блоков. Если тест отключен с помощью хелперов типа skip.in и тому подобных, то триггерится событие TEST_PENDING.

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

После того как соответствующие файлы с тестами будут прочитаны воркером, триггерится событие AFTER_TESTS_READ.

Перечисленные 3 события — BEFORE_FILE_READ, AFTER_FILE_READ и AFTER_TESTS_READ будут триггериться в воркерах в процессе прогона тестов каждый раз как воркеры будут получать от мастер-процесса очередные тесты, которые нужно запустить. Кроме случаев, когда соответствующий файл с тестами уже был прочитан воркером ранее. Так как после первого чтения какого-либо файла, воркер сохраняет его в кэше, чтобы в следующий раз избежать повторного чтения файла с тестами.

Почему файл может запрашиваться несколько раз?

Потому что один файл может содержать множество тестов. А запуск тестов идет по отдельным тестам, а не по файлам. Поэтому в какой-то момент времени может прогоняться тест из файла, из которого уже прогонялся другой тест. В таких случаях кэширование защищает от ненужных повторных чтений одних и тех же файлов.

Прежде чем тест будет запущен, триггерится событие NEW_BROWSER. Однако это событие будет триггериться не для всех тестов, так как один и тот же браузер может использоваться много раз для запуска тестов (см. параметр sessionsPerBrowser). Также в случае падения теста с критической ошибкой происходит пересоздание браузера, чтобы не допустить падения других тестов в этом браузере из-за системного сбоя. В этом случае снова будет послано событие NEW_BROWSER.

Завершение тестов

После того как тест завершается, может быть послано событие SESSION_END. Но это только в том случае, если суммарное количество тестов, которые были запущены в использованной сессии, превысило значение testsPerSession.

Далее всё будет зависеть от того, с каким результатом завершился прогон теста. Если тест прошел успешно, то testplane стриггерит событие TEST_PASS. Если тест упал — TEST_FAIL. Если тест упал, но его следует запустить повторно (см. настройки retry и shouldRetry в конфиге testplane), то вместо события TEST_FAIL будет отправлено событие RETRY.

Если тест не нужно запускать повторно, и результат — окончательный, то testplane триггерит события TEST_END и SUITE_END, если речь идет о завершении выполнения describe-блока.

После того как все тесты будут выполнены, а сессии завершены, testplane триггерит события END и RUNNER_END.

Обновление эталонных скриншотов

Во время запуска тестов может произойти обновление эталонных скриншотов. Это может произойти по следующим причинам:

  • разработчик запустил testplane в специальном GUI-режиме и дал команду «принять скриншоты»;
  • разработчик указал опцию --update-ref при запуске testplane;
  • у тестов не было эталонных скриншотов.

Во всех этих случаях триггерится событие UPDATE_REFERENCE.

Ошибки и аварийное завершение работы

Если во время прогона тестов, в одном из перехватчиков событий произойдет критическая ошибка, то testplane стриггерит для этого теста событие ERROR. При этом остальные тесты будут выполняться в штатном порядке.

Если во время прогона тестов, testplane получит сигнал SIGTERM (например, в результате нажатия клавиш Ctrl + C), то testplane стриггерит событие EXIT и досрочно прервет выполнение тестов.

Про перехват событий

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

События, которые могут быть перехвачены, снабжены в описании тегом can be intercepted. Всего таких событий 7:

Меняем одно событие на другое

Например, код ниже показывает, как можно перехватывать событие TEST_FAIL и изменять его на событие TEST_PENDING — то есть автоматически отключать падающие тесты, не давая им уронить общий прогон тестов:

module.exports = testplane => {
testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
const test = Object.create(data);
test.pending = true;
test.skipReason = "intercepted failure";

return { event: testplane.events.TEST_PENDING, data: test };
});

testplane.on(testplane.events.TEST_FAIL, test => {
// этот обработчик никогда не будет вызван
});

testplane.on(testplane.evenst.TEST_PENDING, test => {
// этот обработчик будет всегда вызываться вместо обработчика для TEST_FAIL
});
};

Оставляем событие as is

Если по какой-либо причине перехваченное событие нужно оставить как есть, то его обработчик должен вернуть точно такой же объект или любое falsey значение: undefined, null или false.

module.exports = testplane => {
testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
return { event, data };
// или return;
// или return null;
// или return false;
});

testplane.on(testplane.events.TEST_FAIL, test => {
// этот обработчик будет вызван как обычно,
// потому что перехват события TEST_FAIL никак не меняет его
});
};

Игнорируем событие

Чтобы проигнорировать какое-либо событие и не дать ему распространяться дальше, нужно вернуть из обработчика (в котором перехватывается событие) пустой объект:

module.exports = testplane => {
testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
return {};
});

testplane.on(testplane.events.TEST_FAIL, test => {
// этот обработчик никогда не будет вызван
});
};

Задерживаем обработку события

Приведенный выше подход с игнорированием события можно использовать для задержки появления тех или иных событий, например:

module.exports = testplane => {
const intercepted = [];

testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
// собираем все события TEST_FAIL
intercepted.push({ event, data });
// и не даем им распространяться дальше
return {};
});

testplane.on(testplane.events.END, () => {
// после окончания прогона тестов, триггерим все накопленные события TEST_FAIL
intercepted.forEach(({ event, data }) => testplane.emit(event, data));
});
};

Передача информации между обработчиками событий

События, которые триггерятся в мастер-процессе и в воркерах testplane не могут обмениваться информацией через глобальные переменные.

Например, такой подход работать не будет:

module.exports = testplane => {
let flag = false;

testplane.on(testplane.events.RUNNER_START, () => {
flag = true;
});

testplane.on(testplane.events.NEW_BROWSER, () => {
// будет выведено значение false, потому что событие NEW_BROWSER
// триггерится в воркере testplane, а RUNNER_START – в мастер-процессе
console.info(flag);
});

testplane.on(testplane.events.RUNNER_END, () => {
// будет выведено значение true
console.info(flag);
});
};

Но можно решить проблему следующим образом:

module.exports = (testplane, opts) => {
testplane.on(testplane.events.RUNNER_START, () => {
opts.flag = true;
});

testplane.on(testplane.events.NEW_BROWSER, () => {
// будет выведено значение true, потому что свойства в конфиге,
// которые имеют примитивный тип (а переменная "opts" является частью конфига),
// автоматически передаются в воркеры во время события RUNNER_START
console.info(opts.flag);
});
};

Или следующим образом: смотрите пример из описания события NEW_WORKER_PROCESS.

Параллельное выполнение кода плагина

У раннера тестов есть метод registerWorkers, который регистрирует код плагина для параллельного выполнения в воркерах testplane. Метод принимает следующие параметры:

ПараметрТипОписание
workerFilepathStringАбсолютный путь к воркеру.
exportedMethodsString[]Список экспортируемых методов.

При этом возвращает объект, который содержит асинхронные функции с именами из экспортированных методов.

Файл с путем workerFilepath должен экспортировать объект, который содержит асинхронные функции с именами из exportedMethods.

Пример параллельного выполнения кода плагина

Код плагина: plugin.js

let workers;

module.exports = testplane => {
testplane.on(testplane.events.RUNNER_START, async runner => {
const workerFilepath = require.resolve("./worker");
const exportedMethods = ["foo"];

workers = runner.registerWorkers(workerFilepath, exportedMethods);

// выводит FOO_RUNNER_START
console.info(await workers.foo("RUNNER_START"));
});

testplane.on(testplane.events.RUNNER_END, async () => {
// выводит FOO_RUNNER_END
console.info(await workers.foo("RUNNER_END"));
});
};

Код воркера: worker.js

module.exports = {
foo: async function (event) {
return "FOO_" + event;
},
};

CLI

sync | master

Событие CLI триггерится сразу при запуске, до того, как testplane распарсит CLI. Обработчик события выполняется синхронно. С помощью него можно добавить новые команды или расширить справку testplane.

Подписка на событие

testplane.on(testplane.events.CLI, cli => {
console.info("Выполняется обработка события CLI...");

cli.option(
"--some-option <some-value>",
"the full description of the option",
// см. подробнее в https://github.com/tj/commander.js#options
);
});

Параметры обработчика

В обработчик события передается объект типа Commander.

Пример использования

Рассмотрим в качестве примера реализацию плагина @testplane/test-repeater.

Используя событие CLI, плагин добавляет к testplane новую опцию --repeat. С помощью неё можно указать, сколько раз нужно прогнать тесты, независимо от результата каждого прогона.

Нажмите, чтобы посмотреть код

const parseConfig = require('./config');

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled <tr><td>testplane.isWorker()) {
// или плагин отключен, или мы находимся в контексте воркера – уходим
return;
}

testplane.on(testplane.events.CLI, (cli) => {
// добавляем опцию --repeat
cli.option(
'--repeat <number>',
'how many times tests should be repeated regardless of the result',
(value) => parseNonNegativeInteger(value, 'repeat')
);
});

// ...
};

INIT

async | master

Событие INIT триггерится перед тем, как будут выполнены задачи run или readTests. Обработчик события может быть асинхронным: в таком случае задачи начнут выполняться только после того, как будет разрезолвлен Promise, возвращаемый обработчиком события. Событие триггерится только один раз, независимо от того, сколько раз будут выполнены задачи.

Подписка на событие

testplane.on(testplane.events.INIT, async () => {
console.info("Выполняется обработка события INIT...");
});

Параметры обработчика

В обработчик события никакие данные не передаются.

Пример использования

В обработчике события INIT можно организовать, например, запуск dev-сервера для вашего проекта.

Что такое dev-сервер?

Dev-сервер — это express-like приложение, которое позволяет разрабатывать фронтенд проекта.

Ниже приведена самая короткая реализация. Более подробный пример можно посмотреть в разделе «Автоматический запуск dev-сервера».

Нажмите, чтобы посмотреть код

const http = require('http');
const parseConfig = require('./config');

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled</td></tr> testplane.isWorker()) {
// или плагин отключен, или мы находимся в контексте воркера – уходим
return;
}

// ...

testplane.on(testplane.events.INIT, () => {
// контент, который отдает dev-сервер
const content = '<h1>Hello, World!</h1>';

// создаем сервер и начинаем слушать порт 3000
http
.createServer((req, res) => res.end(content))
.listen(3000);

// по адресу http://localhost:3000/index.html будет отдаваться: <h1>Hello, World!</h1>
});
};

BEFORE_FILE_READ

sync | master | worker

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

Подписка на событие

testplane.on(testplane.events.BEFORE_FILE_READ, ({ file, testplane, testParser }) => {
testParser.setController("<some-command-name>", {
"<some-helper>": function (matcher) {
// ...
},
});
});

Параметры обработчика

В обработчик события передается объект следующего формата:

{
file, // String: путь к файлу с тестом
testplane, // Object: то же, что и global.testplane
testParser; // Object: типа TestParserAPI
}

testParser: TestParserAPI

Объект testParser типа TestParserAPI передается в обработчик события BEFORE_FILE_READ. С помощью него можно управлять процессом парсинга файлов с тестами. Объект поддерживает метод setController, с помощью которого можно создать свои хелперы для тестов.

setController(name, methods)

Метод добавляет контроллер к глобальному объекту testplane, доступному внутри тестов.

  • name — имя хелпера (или иначе — контроллера);
  • methods — объект-словарь, ключи которого задают названия методов соответствующих хелперов, а значения ключей определяют их реализацию. Каждый метод будет вызван на соответствующем тесте или наборе тестов (suite).
к сведению

Контроллер будет удален, как только закончится парсинг текущего файла.

Пример использования

Создадим в качестве примера специальный хелпер testplane.logger.log(), с помощью которого мы сможем логировать информацию о парсинге интересующего нас теста.

Нажмите, чтобы посмотреть пример использования

Код плагина

testplane.on(testplane.events.BEFORE_FILE_READ, ({ file, testParser }) => {
testParser.setController("logger", {
log: function (prefix) {
console.log(
`${prefix}: только что распарсил ${this.fullTitle()} из файла ${file} для браузера ${this.browserId}`,
);
},
});
});

Код теста

describe("foo", () => {
testplane.logger.log("some-prefix");
it("bar", function () {
// ...
});
});

Ещё один пример использования события BEFORE_FILE_READ можно посмотреть в разделе «Запуск тестов с хелперами».

AFTER_FILE_READ

sync | master | worker

Событие AFTER_FILE_READ триггерится после того, как будет прочтен файл с тестом. Обработчик события выполняется синхронно. Событие также доступно в воркерах testplane.

Подписка на событие

testplane.on(testplane.events.AFTER_FILE_READ, ({ file, testplane }) => {
console.info("Выполняется обработка события AFTER_FILE_READ...");
});

Параметры обработчика

В обработчик события передается объект следующего формата:

{
file, // String: путь к файлу с тестом
testplane; // Object: то же, что и global.testplane
}

AFTER_TESTS_READ

sync | master | worker

Событие AFTER_TESTS_READ триггерится после того, как будут вызваны методы readTests или run объекта типа TestCollection. Обработчик события выполняется синхронно. Событие также доступно в воркерах testplane.

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

Подписка на событие

testplane.on(testplane.events.AFTER_TESTS_READ, testCollection => {
console.info("Выполняется обработка события AFTER_TESTS_READ...");
});

Параметры обработчика

В обработчик события передается объект testCollection типа TestCollection.

Пример использования

Рассмотрим реализацию плагина testplane-global-hook, с помощью которого можно вынести действия, повторяющиеся перед запуском и завершением каждого теста, в отдельные beforeEach- и afterEach-обработчики.

Используя событие AFTER_TESTS_READ, плагин добавляет к каждому корневому suite обработчики beforeEach и afterEach-хуков. Последние задаются пользователем в конфиге плагина testplane-global-hook.

Нажмите, чтобы посмотреть код

const parseConfig = require("./config");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

// ...

const { beforeEach, afterEach } = pluginConfig;

testplane.on(testplane.events.AFTER_TESTS_READ, testCollection => {
testCollection.eachRootSuite(root => {
beforeEach && root.beforeEach(beforeEach);
afterEach && root.afterEach(afterEach);
});
});
};

Ещё примеры использования события AFTER_TESTS_READ можно посмотреть в разделах «Запуск тестов из заданного списка» и «Запуск тестов с хелперами».

RUNNER_START

async | master

Событие RUNNER_START триггерится после инициализации всех воркеров testplane и перед выполнением тестов. Обработчик события может быть асинхронным: в таком случае тесты начнут выполняться только после того, как будет разрезолвлен Promise, возвращаемый обработчиком события.

Подписка на событие

testplane.on(testplane.events.RUNNER_START, async runner => {
console.info("Выполняется обработка события RUNNER_START...");
});

Параметры обработчика

В обработчик события передается ссылка на инстанс раннера. Используя этот инстанс, можно триггерить различные события или подписаться на них.

Пример использования

Предположим, мы хотим автоматически поднимать ssh-туннель при запуске тестов и перенаправлять все урлы в тестах в поднятый туннель. Для этого мы можем воспользоваться событиями RUNNER_START и RUNNER_END, чтобы открывать туннель при запуске раннера и закрывать его после завершения работы раннера.

Нажмите, чтобы посмотреть код

const parseConfig = require("./config");
const Tunnel = require("./lib/tunnel");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled) {
// плагин отключен – уходим
return;
}

// конфиг плагина задает параметры туннеля:
// host, ports, localport, retries, etc.
const tunnel = Tunnel.create(testplane.config, pluginConfig);

testplane.on(testplane.events.RUNNER_START, () => tunnel.open());
testplane.on(testplane.events.RUNNER_END, () => tunnel.close());
};

Похожую реализацию можно посмотреть в плагине ssh-tunneler.

RUNNER_END

async | master

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

Подписка на событие

testplane.on(testplane.events.RUNNER_END, async result => {
console.info("Выполняется обработка события RUNNER_END...");
});

Параметры обработчика

В обработчик события передается объект со статистикой прогона тестов следующего вида:

{
passed: 0, // количество успешно выполненных тестов
failed: 0, // количество упавших тестов
retries: 0, // количество ретраев (повторных запусков) тестов
skipped: 0, // количество отключенных (заскипанных) тестов
total: 0 // общее количество тестов
};

Пример использования

Смотрите пример выше про открытие и закрытие туннеля при запуске и остановке раннера.

NEW_WORKER_PROCESS

sync | master

Событие NEW_WORKER_PROCESS триггерится после порождения нового подпроцесса (воркера) testplane. Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.NEW_WORKER_PROCESS, workerProcess => {
console.info("Выполняется обработка события NEW_WORKER_PROCESS...");
});

Параметры обработчика

В обработчик события передается объект-обертка над порожденным подпроцессом, с одним единственным методом send для обмена сообщениями.

Пример использования

В примере ниже показано, как можно использовать событие NEW_WORKER_PROCESS, чтобы организовать взаимодействие мастер-процесса со всеми воркерами testplane. Например, для того, чтобы обновить значение какого-либо параметра во всех воркерах testplane из мастер-процесса перед началом прогона всех тестов.

В примере также используется событие BEGIN.

Нажмите, чтобы посмотреть код

const masterPlugin = (testplane, opts) => {
const workers = [];

testplane.on(testplane.events.NEW_WORKER_PROCESS, (workerProcess) => {
// запоминаем ссылки на все созданные воркеры testplane
workers.push(workerProcess);
});

testplane.on(testplane.events.BEGIN, () => {
// посылаем значение параметра всем воркерам
workers.forEach((worker) => {
worker.send({
type: PARAM_VALUE_UPDATED_EVENT,
param: 'some-value'
});
});
});
};

const workerPlugin = (testplane) => {
process.on('message', ({ type, ...data }) => {
if (type === PARAM_VALUE_UPDATED_EVENT) {
const { param } = data;
console.info(`Получил значение "${param}" для "param" из мастер-процесса`);
}
});

...
};

const plugin = (testplane, opts) => {
if (testplane.isWorker()) {
workerPlugin(testplane, opts);
} else {
masterPlugin(testplane, opts);
}
};

module.exports = plugin;

SESSION_START

async | master

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

Подписка на событие

testplane.on(testplane.events.SESSION_START, async (browser, { browserId, sessionId }) => {
console.info("Выполняется обработка события SESSION_START...");
});

Параметры обработчика

В обработчик события передаются 2 аргумента:

  • первый аргумент — инстанс WebdriverIO;
  • второй аргумент — объект вида { browserId, sessionId }, где browserId — это имя браузера, а sessionId — идентификатор сессии браузера.

Пример использования

Рассмотрим пример, в котором плагин подписывается на событие SESSION_START, чтобы отключить скролл-бары в браузерах с помощью Chrome DevTools Protocol'а.

Нажмите, чтобы посмотреть код

const parseConfig = require("./config");
const DevTools = require("./dev-tools");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled) {
// плагин отключен – уходим
return;
}

testplane.on(testplane.events.SESSION_START, async (browser, { browserId, sessionId }) => {
if (!pluginConfig.browsers.includes(browserId)) {
// браузера нет в списке браузеров, для которых возможно отключить скроллбары
// посредством Chrome DevTools Protocol'а (CDP) – уходим
return;
}

const gridUrl = testplane.config.forBrowser(browserId).gridUrl;

// pluginConfig.browserWSEndpoint задает функцию, которая должна вернуть URL
// для работы с браузером через CDP. Чтобы функция могла вычислить URL,
// в функцию передаются идентификатор сессии и ссылка на грид
const browserWSEndpoint = pluginConfig.browserWSEndpoint({ sessionId, gridUrl });

const devtools = await DevTools.create({ browserWSEndpoint });

devtools.setScrollbarsHiddenOnNewPage();

await devtools.hideScrollbarsOnActivePages();
});
};

Более подробную реализацию можно посмотреть в плагине hermione-hide-scrollbars.

SESSION_END

async | master

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

Подписка на событие

testplane.on(testplane.events.SESSION_END, async (browser, { browserId, sessionId }) => {
console.info("Выполняется обработка события SESSION_END...");
});

Параметры обработчика

В обработчик события передаются 2 аргумента:

  • первый аргумент — инстанс WebdriverIO;
  • второй аргумент — объект вида { browserId, sessionId }, где browserId — это имя браузера, а sessionId — идентификатор сессии браузера.

BEGIN

sync | master

Событие BEGIN триггерится перед выполнением теста, но после того как все раннеры будут инициализированы. Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.BEGIN, () => {
console.info("Выполняется обработка события BEGIN...");
});

Параметры обработчика

В обработчик события никакие данные не передаются.

Пример использования

Смотрите пример выше про организацию взаимодействия мастер-процесса testplane со всеми воркерами.

END

sync | master

Событие END триггерится прямо перед событием RUNNER_END. Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.END, () => {
console.info("Выполняется обработка события END...");
});

Параметры обработчика

В обработчик события никакие данные не передаются.

Пример использования

В качестве примера использования события END смотрите раздел «Задерживаем обработку события».

SUITE_BEGIN

sync | master | can be intercepted

Событие SUITE_BEGIN триггерится перед выполнением набора тестов (suite). Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.SUITE_BEGIN, suite => {
console.info(`Выполняется обработка события SUITE_BEGIN для "${suite.fullTitle()}"...`);
});

Параметры обработчика

В обработчик события передается инстанс suite.

Перехват события

testplane.intercept(testplane.events.SUITE_BEGIN, ({ event, data: suite }) => {
console.info(`Выполняется перехват события SUITE_BEGIN для "${suite.fullTitle()}"...`);
});

SUITE_END

sync | master | can be intercepted

Событие SUITE_END триггерится после окончания выполнения набора тестов (suite). Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.SUITE_END, suite => {
console.info(`Выполняется обработка события SUITE_END для "${suite.fullTitle()}"...`);
});

Параметры обработчика

В обработчик события передается инстанс suite.

Перехват события

testplane.intercept(testplane.events.SUITE_END, ({ event, data: suite }) => {
console.info(`Выполняется перехват события SUITE_END для "${suite.fullTitle()}"...`);
});

TEST_BEGIN

sync | master | can be intercepted

Событие TEST_BEGIN триггерится перед выполнением теста. Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.TEST_BEGIN, test => {
if (test.pending) {
// тест отключен, ничего делать не нужно
return;
}

console.info(
`Выполняется обработка события TEST_BEGIN ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Параметры обработчика

В обработчик события передается инстанс теста.

Перехват события

testplane.intercept(testplane.events.TEST_BEGIN, ({ event, data: test }) => {
console.info(
`Выполняется перехват события TEST_BEGIN ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Пример использования

Смотрите в качестве примера «Профилирование прогона тестов».

TEST_END

sync | master | can be intercepted

Событие TEST_END триггерится после окончания выполнения теста. Обработчик события выполняется синхронно. Также событие можно перехватить и изменить в специальном обработчике.

Подписка на событие

testplane.on(testplane.events.TEST_END, test => {
if (test.pending) {
// тест отключен, ничего делать не нужно
return;
}

console.info(
`Выполняется обработка события TEST_END ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Параметры обработчика

В обработчик события передается инстанс теста.

Перехват события

testplane.intercept(testplane.events.TEST_END, ({ event, data: test }) => {
console.info(
`Выполняется перехват события TEST_END ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Пример использования

Смотрите в качестве примера «Профилирование прогона тестов».

TEST_PASS

sync | master | can be intercepted

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

Подписка на событие

testplane.on(testplane.events.TEST_PASS, test => {
console.info(
`Выполняется обработка события TEST_PASS ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Параметры обработчика

В обработчик события передается инстанс теста.

Перехват события

testplane.intercept(testplane.events.TEST_PASS, ({ event, data: test }) => {
console.info(
`Выполняется перехват события TEST_PASS ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Пример использования

Смотрите в качестве примера «Сбор статистики о прогоне тестов».

TEST_FAIL

sync | master | can be intercepted

Событие TEST_FAIL триггерится, если тест упал. Обработчик события выполняется синхронно. Также событие можно перехватить и изменить в специальном обработчике.

Подписка на событие

testplane.on(testplane.events.TEST_FAIL, test => {
console.info(
`Выполняется обработка события TEST_FAIL ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Параметры обработчика

В обработчик события передается инстанс теста.

Перехват события

testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
console.info(
`Выполняется перехват события TEST_PASS ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Пример использования

Смотрите в качестве примера «Сбор статистики о прогоне тестов».

TEST_PENDING

sync | master

Событие TEST_PENDING триггерится, если тест отключен. Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.TEST_PENDING, test => {
console.info(
`Выполняется обработка события TEST_PENDING ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Параметры обработчика

В обработчик события передается инстанс теста.

Пример использования

Смотрите в качестве примера «Сбор статистики о прогоне тестов».

RETRY

sync | master | can be intercepted

Событие RETRY триггерится, если тест упал, но ушел на повторный прогон, так называемый «ретрай». Возможности повторного прогона теста определяются настройками retry и shouldRetry в конфиге testplane. Также на это могут влиять плагины testplane, если они модифицируют «на лету» указанные выше настройки. Смотрите для примера плагины retry-limiter и testplane-retry-progressive.

Обработчик события выполняется синхронно. Также событие можно перехватить и изменить в специальном обработчике.

Подписка на событие

testplane.on(testplane.events.RETRY, test => {
console.info(
`Выполняется обработка события RETRY ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Параметры обработчика

В обработчик события передается инстанс теста.

Перехват события

testplane.intercept(testplane.events.RETRY, ({ event, data: test }) => {
console.info(
`Выполняется перехват события RETRY ` +
`для теста "${test.fullTitle()}" в браузере "${test.browserId}"...`,
);
});

Пример использования

Смотрите в качестве примера «Сбор статистики о прогоне тестов».

ERROR

sync | master

Событие ERROR триггерится только из перехватчиков событий в случае критической ошибки. Обработчик события выполняется синхронно.

Подписка на событие

testplane.on(testplane.events.ERROR, error => {
console.info("Выполняется обработка события ERROR...");
});

Параметры обработчика

В обработчик события передается объект с ошибкой.

Пример использования

Смотрите в качестве примера «Профилирование прогона тестов».

INFO

reserved

Зарезервировано.

WARNING

reserved

Зарезервировано.

EXIT

async | master

Событие EXIT триггерится при получении сигнала SIGTERM (например, после нажатия Ctrl + C). Обработчик события может быть асинхронным.

Подписка на событие

testplane.on(testplane.events.EXIT, async () => {
console.info("Выполняется обработка события EXIT...");
});

Параметры обработчика

В обработчик события никакие данные не передаются.

NEW_BROWSER

sync | worker

Событие NEW_BROWSER триггерится после того, как создан новый инстанс браузера. Обработчик события выполняется синхронно. Событие доступно только в воркерах testplane.

Подписка на событие

testplane.on(testplane.events.NEW_BROWSER, (browser, { browserId, browserVersion }) => {
console.info("Выполняется обработка события NEW_BROWSER...");
});

Параметры обработчика

В обработчик события передаются 2 аргумента:

  • первый аргумент — инстанс WebdriverIO;
  • второй аргумент — объект вида { browserId, versionId }, где browserId — это имя браузера, а browserVersion — версия браузера.

Пример реализации

Событие NEW_BROWSER часто используют для того, чтобы добавить новые команды к браузеру, или как-то дополнить уже существующие команды. Например, некоторые плагины добавляют кастомные команды к браузеру в обработчике события NEW_BROWSER.

Нажмите, чтобы посмотреть код

module.exports = (testplane, opts) => {
// ...

if (testplane.isWorker()) {
testplane.on(testplane.events.NEW_BROWSER, (browser, { browserId }) => {
// ...

browser.addCommand("vncUrl", vncUrlHandler);
browser.addCommand("openVnc", createOpenVnc(browser, ipcOptions));
browser.addCommand("waitForEnter", waitForEnter);
});
}
};

UPDATE_REFERENCE

sync | worker

Событие UPDATE_REFERENCE триггерится после обновления эталонных скриншотов. Обработчик события выполняется синхронно. Событие доступно только в воркерах testplane.

Подписка на событие

testplane.on(testplane.events.UPDATE_REFERENCE, ({ state, refImg }) => {
console.info("Выполняется обработка события UPDATE_REFERENCE...");
});

Параметры обработчика

В обработчик события передается объект следующего формата:

{
state, // String: состояние, которое отражает данный скриншот, например: plain, map-view, scroll-left и т. д.
refImg; // Object: типа { path, size: { width, height } }, описывающий эталонный скриншот
}

Параметр refImg.path хранит путь к эталонному скриншоту на файловой системе, а refImg.size.width и refImg.size.height хранят, соответственно, ширину и высоту эталонного скриншота.

Пример использования

Рассмотрим в качестве примера реализацию плагина testplane-image-minifier, в котором при сохранении эталонных скриншотов происходит их автоматическое сжатие с заданным уровнем компрессии.

Нажмите, чтобы посмотреть код

const parseConfig = require("./config");
const Minifier = require("./minifier");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled) {
// плагин отключен – уходим
return;
}

const minifier = Minifier.create(pluginConfig);

testplane.on(testplane.events.UPDATE_REFERENCE, ({ refImg }) => {
minifier.minify(refImg.path);
});
};

Примеры использования событий

Запуск тестов из заданного списка

Рассмотрим в качестве примера реализацию плагина testplane-test-filter, с помощью которого можно запускать только заданные в json-файле тесты.

В этом примере используются следующие события и методы API testplane:

Нажмите, чтобы посмотреть код

const _ = require("lodash");
const parseConfig = require("./config");
const utils = require("./utils");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled) {
// плагин отключен – уходим
return;
}

if (testplane.isWorker()) {
// в воркерах testplane нам нечего делать – уходим
return;
}

let input;

testplane.on(testplane.events.INIT, async () => {
// читаем файл со списком тестов, которые надо прогнать;
// readFile возвращает json, который содержит массив вида:
// [
// { "fullTitle": "test-1", "browserId": "bro-1" },
// { "fullTitle": "test-2", "browserId": "bro-2" }
// ]
input = await utils.readFile(pluginConfig.inputFile);
});

testplane.on(testplane.events.AFTER_TESTS_READ, testCollection => {
if (_.isEmpty(input)) {
// список тестов – пустой – будем запускать все тесты,
// то есть не трогаем исходную коллекцию (testCollection) тестов
return;
}

// отключаем все тесты
testCollection.disableAll();

// а теперь включаем только те, которые были переданы в json-файле
input.forEach(({ fullTitle, browserId }) => {
testCollection.enableTest(fullTitle, browserId);
});
});
};

Сбор статистики о прогоне тестов

Рассмотрим в качестве примера реализацию плагина json-reporter.

В этом примере используются следующие события testplane:

Нажмите, чтобы посмотреть код

const Collector = require("./lib/collector");
const testplaneToolCollector = require("./lib/collector/tool/testplane");
const parseConfig = require("./lib/config");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled) {
// плагин отключен – уходим
return;
}

// collector будет аккумулировать статистику
const collector = Collector.create(testplaneToolCollector, pluginConfig);

// подписываемся на соответствующие события,
// чтобы в итоге получить необходимую статистику:

// - сколько тестов выполнилось успешно
testplane.on(testplane.events.TEST_PASS, data => collector.addSuccess(data));

// - сколько тестов упало
testplane.on(testplane.events.TEST_FAIL, data => collector.addFail(data));

// - сколько было отключено (заскипано)
testplane.on(testplane.events.TEST_PENDING, data => collector.addSkipped(data));

// - количество ретраев
testplane.on(testplane.events.RETRY, data => collector.addRetry(data));

// после того, как прогон тестов завершен, сохраняем статистику в json-файл
testplane.on(testplane.events.RUNNER_END, () => collector.saveFile());
};

Автоматический запуск dev-севера

Реализуем схематично плагин testplane-dev-server для testplane, чтобы при каждом запуске testplane автоматически поднимать dev-сервер.

Запуск dev-сервера — опционален: для этого плагин добавляет специальную опцию --dev-server к testplane, позволяя разработчику указывать при запуске testplane, нужно ли поднимать dev-сервер.

Помимо этого, плагин позволяет задать параметр devServer в своем конфиге.

В этом примере используются следующие события testplane:

Нажмите, чтобы посмотреть код

Код плагина

const http = require('http');
const parseConfig = require('./config');

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled <tr><td>testplane.isWorker()) {
// или плагин отключен, или мы находимся в контексте воркера – уходим
return;
}

let program;

testplane.on(testplane.events.CLI, (cli) => {
// нужно сохранить ссылку на инстанс commander'а (https://github.com/tj/commander.js),
// чтобы потом проверить наличие опции
program = cli;
// добавляем к testplane опцию --dev-server,
// чтобы пользователь мог явно указывать, когда надо запустить dev-сервер
cli.option('--dev-server', 'run dev-server');
});

testplane.on(testplane.events.INIT, () => {
// dev-сервер может быть запущен как через указание опции --dev-server
// при запуске testplane, так и в настройках плагина
const devServer = program && program.devServer</td></tr> pluginConfig.devServer;

if (!devServer) {
// если dev-сервер запускать не нужно – уходим
return;
}

// контент, который отдает dev-сервер
const content = '<h1>Hello, World!</h1>';

// создаем сервер и начинаем слушать порт 3000
http
.createServer((req, res) => res.end(content))
.listen(3000);

// по адресу http://localhost:3000/index.html будет отдаваться: <h1>Hello, World!</h1>
});
};

Конфиг testplane

module.exports = {
// тесты будут запускаться в локальном браузере,
// см. про selenium-standalone в разделе «Быстрый старт»
gridUrl: "http://localhost:4444/wd/hub",
// указываем путь к dev-серверу
baseUrl: "http://localhost:3000",

browsers: {
chrome: {
desiredCapabilities: {
browserName: "chrome",
},
},
},

plugins: {
// добавляем наш плагин к списку плагинов
"testplane-dev-server": {
enabled: true,
// по умолчанию dev-сервер запускаться не будет
devServer: false,
},
},
};

Код теста

const { assert } = require("chai");

describe("example", async () => {
it("should find hello world", async ({ browser }) => {
// baseUrl, относительно которого задается index.html,
// указан в конфиге testplane выше
await browser.url("index.html");

const title = await browser.$("h1").getText();
assert.equal(title, "Hello, World!");
});
});

Запуск тестов с хелперами

Рассмотрим реализацию плагина testplane-passive-browsers.

Используя события BEFORE_FILE_READ и AFTER_TESTS_READ плагин позволяет добавить специальный хелпер, с помощью которого можно запускать указанные тесты или наборы тестов (suites) в заданных браузерах. Такая логика может пригодиться, если вам не нужно запускать большинство тестов в каких-то браузерах. Но при этом некоторые тесты вы все же хотели бы запускать в этих (пассивных) браузерах, чтобы проверять браузеро-специфичные вещи.

В примере ниже мы немного упростили код плагина, задав название хелпера also непосредственно в коде, а не беря его из конфига плагина.

В этом примере используются следующие события testplane:

Также используются testParser и testCollection.

Нажмите, чтобы посмотреть код

Код плагина

const _ = require("lodash");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled) {
// плагин отключен – уходим
return;
}

if (testplane.isWorker()) {
testplane.on(testplane.events.BEFORE_FILE_READ, ({ testParser }) => {
// в воркерах хелпер ничего делать не будет,
// задаем для него "no operation"
testParser.setController("also", { in: _.noop });
});

return;
}

const suitesToRun = {};
const testsToRun = {};

testplane.on(testplane.events.BEFORE_FILE_READ, ({ testParser }) => {
testParser.setController("also", {
// matcher – параметр, который передается в хелпер also.in();
// может быть строкой, регулярным выражением или массивом строк/regexp;
// в нашем случае matcher определяет пассивные браузеры,
// в которых нужно запустить тест (тесты)
in: function (matcher) {
const storage = this.suites ? suitesToRun : testsToRun;

if (!shouldRunInBro(this.browserId, matcher)) {
// если текущего браузера нет в том списке,
// который указан в хелпере, то ничего не делаем
return;
}

if (!storage[this.browserId]) {
storage[this.browserId] = [];
}

// иначе собираем айдишники тестов,
// которые должны быть запущены для текущего браузера
storage[this.browserId].push({ id: this.id() });
},
});
});

// используем prependListener, чтобы изначально включить тесты только
// в указанных пассивных браузерах, а затем уже будут включены все остальные тесты,
// которые должны быть включены
testplane.prependListener(testplane.events.AFTER_TESTS_READ, testCollection => {
// формируем список пассивных браузеров как пересечение браузеров для тестов,
// которые были прочитаны, и браузеров из конфига плагина
const passiveBrowserIds = getPassiveBrowserIds(testCollection, pluginConfig);

passiveBrowserIds.forEach(passiveBrowserId => {
const shouldRunTest = (runnable, storage = testsToRun) => {
const foundRunnable =
runnable.id && _.find(storage[passiveBrowserId], { id: runnable.id() });

return (
foundRunnable ||
(runnable.parent && shouldRunTest(runnable.parent, suitesToRun))
);
};

// отключаем все тесты, кроме тех, что должны быть запущены
// в указанных пассивных браузерах
testCollection.eachTest(browserId, test => {
test.disabled = !shouldRunTest(test);
});
});
});
};

Код теста

testplane.also.in("ie6");
describe("suite", () => {
it("test1", function () {
// ...
});

testplane.also.in(["ie7", /ie[89]/]);
it("test2", function () {
// ...
});
});

Профилирование прогона тестов

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

Нажмите, чтобы посмотреть код

const parseConfig = require("./lib/config");
const StreamWriter = require("./lib/stream-writer");

module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);

if (!pluginConfig.enabled) {
// плагин отключен – уходим
return;
}

let writeStream;

testplane.on(testplane.events.RUNNER_START, () => {
// создаем стрим для записи данных профилирования
writeStream = StreamWriter.create(pluginConfig.path);
});

testplane.on(testplane.events.TEST_BEGIN, test => {
if (test.pending) {
// тест отключен – ничего делать не нужно
return;
}

// засекаем время запуска теста
test.timeStart = Date.now();
});

testplane.on(testplane.events.TEST_END, test => {
if (test.pending) {
// тест отключен – ничего делать не нужно
return;
}

// засекаем время завершения теста
test.timeEnd = Date.now();
// и сохраняем информацию о тесте в стрим
writeStream.write(test);
});

// в случае ошибки закрываем стрим
testplane.on(testplane.events.ERROR, () => writeStream.end());

// после завершения раннера закрываем стрим
testplane.on(testplane.events.RUNNER_END, () => writeStream.end());
};

Более подробную реализацию можно посмотреть в плагине testplane-profiler.