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

События 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 завершает штатную очистку ресурсов, а затем триггерит событие PROFILER_RESULT с итоговым неизменяемым профилем.

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

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

  • разработчик запустил 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 || 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, worker

Событие 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 || 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 // общее количество тестов
};

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

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

PROFILER_RESULT​

async | master

Событие PROFILER_RESULT триггерится один раз после штатной очистки ресурсов, когда включён встроенный профайлер. Оно также отправляется с частичным результатом, если Testplane может завершить формирование профиля для прерванной или завершившейся ошибкой операции. На уровне 0 событие не триггерится.

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

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

testplane.on(testplane.events.PROFILER_RESULT, async result => {
console.info(`Profile ${result.run.id}: ${result.run.durationMs} ms`);
});

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

Обработчик получает readonly-объект ProfilerResultV1. Он содержит метаданные прогона, сведения о среде и возможностях, сохранённую временную шкалу, полные агрегаты, подтверждённые данными выводы, сведения о качестве данных, ошибки сбора и метаданные усечения. Тот же объект используется для сводки в консоли и необязательного JSON-файла.

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

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, если они модифицируют «на лету» указанные выше настройки. Смотрите для примера плагины @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 || 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) || 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 = {
// тесты будут запускаться в локальном браузере,
// см. https://testplane.io/ru/docs/basic-guides/browsers-overview/
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.

On this page