testplane
Version:
Tests framework based on mocha and wdio
260 lines • 11.1 kB
JavaScript
/* global document */
"use strict";
const _ = require("lodash");
const HookRunner = require("./hook-runner");
const ExecutionThread = require("./execution-thread");
const { captureFailScreenshot } = require("./capture-fail-screenshot");
const { AssertViewError } = require("../../../browser/commands/assert-view/errors/assert-view-error");
const history = require("../../../browser/history");
const { SAVE_HISTORY_MODE } = require("../../../constants/config");
const { REPL_INSTRUMENTED_FN_FLAG } = require("../../../constants/repl");
const RuntimeConfig = require("../../../config/runtime-config");
const { filterExtraStackFrames } = require("../../../browser/stacktrace/utils");
const { extendWithCodeSnippet } = require("../../../error-snippets");
const { startSelectivity } = require("../../../browser/cdp/selectivity");
const SNAPSHOTS_TIMEOUT_MS = 10000;
const SNAPSHOTS_WARNING_TIMEOUT_MS = 2000;
module.exports = class TestRunner {
static create(...args) {
return new this(...args);
}
constructor({ test, file, config, browserAgent, attempt }) {
if (test) {
this._test = test.clone();
this._test.testplaneCtx = _.cloneDeep(test.testplaneCtx) || {};
}
this._file = file;
this._config = config;
this._browserAgent = browserAgent;
this._attempt = attempt;
}
assignTest(test) {
this._test = test.clone();
this._test.testplaneCtx = _.cloneDeep(test.testplaneCtx) || {};
}
async prepareBrowser({ sessionId, sessionCaps, sessionOpts, state }) {
this._browserIsSettled = new Promise(resolve => {
this._browserAgent
.getBrowser({ sessionId, sessionCaps, sessionOpts, state })
.then(browser => {
this._browser = browser;
})
.catch(err => {
this._getBrowserException = err;
})
.finally(resolve);
});
}
async run() {
await this.prepareToRun();
const stopSelectivity = await startSelectivity(this._browser);
const error = await this.runRunnables(ExecutionThread);
await stopSelectivity(this._test, Boolean(error));
return this.finishRun(error);
}
// TODO: make it protected
async prepareToRun() {
await this._browserIsSettled;
if (!this._test) {
throw new Error("Test is not assigned");
}
if (this._getBrowserException) {
const testplaneCtx = this._test.testplaneCtx;
throw Object.assign(this._getBrowserException, { testplaneCtx, hermioneCtx: testplaneCtx });
}
}
// TODO: make it protected
async finishRun(error) {
const testplaneCtx = this._test.testplaneCtx;
const { callstackHistory } = this._browser;
const assertViewResults = testplaneCtx.assertViewResults;
if (!error && assertViewResults && assertViewResults.hasFails()) {
error = new AssertViewError();
if (this._config.takeScreenshotOnFails.assertViewFail) {
const screenshot = await captureFailScreenshot(this._browser);
if (screenshot) {
error.screenshot = screenshot;
}
}
}
// we need to check session twice:
// 1. before afterEach hook to prevent working with broken sessions
// 2. after collecting all assertView errors (including afterEach section)
if (!this._browser.state.isBroken && isSessionBroken(error, this._config)) {
this._browser.markAsBroken({ stubBrowserCommands: true });
}
testplaneCtx.assertViewResults = assertViewResults ? assertViewResults.toRawObject() : [];
const { meta, tags } = this._browser;
const commandsHistory = callstackHistory ? callstackHistory.release() : [];
const results = {
testplaneCtx,
hermioneCtx: testplaneCtx,
meta,
tags,
};
switch (this._browser.config.saveHistoryMode) {
case SAVE_HISTORY_MODE.ALL:
case error && SAVE_HISTORY_MODE.ONLY_FAILED:
results.history = commandsHistory;
break;
}
if (error && this._browser.state) {
this._browser.state.isLastTestFailed = true;
}
this._browserAgent.freeBrowser(this._browser);
if (error) {
filterExtraStackFrames(error);
let current = error.cause;
let depth = 1;
// Propagate errors from the cause into the stack trace of the main error.
while (current) {
const indent = " ".repeat(depth);
error.stack += `\n\n${indent}Caused by: ${current.stack.split("\n").join(`\n${indent}`)}`;
current = current.cause;
depth++;
}
// The original cause must be removed to avoid possible duplicates later.
delete error.cause;
await extendWithCodeSnippet(error);
throw Object.assign(error, results);
}
return results;
}
// TODO: make it protected
async runRunnables(ExecutionThreadCls) {
const test = this._test;
const testplaneCtx = test.testplaneCtx || {};
const executionThread = ExecutionThreadCls.create({
test,
browser: this._browser,
testplaneCtx,
hermioneCtx: testplaneCtx,
attempt: this._attempt,
});
const hookRunner = HookRunner.create(test, executionThread);
const { callstackHistory } = this._browser;
let error;
try {
const preparePageActions = this._getPreparePageActions(this._browser, history);
const shouldRunBeforeEach = preparePageActions.length || hookRunner.hasBeforeEachHooks();
if (shouldRunBeforeEach) {
await history.runGroup({
callstack: callstackHistory,
snapshotsPromiseRef: this._browser.snapshotsPromiseRef,
config: this._config,
session: this._browser.publicAPI,
}, "beforeEach", async () => {
for (const action of preparePageActions) {
await action();
}
await hookRunner.runBeforeEachHooks();
});
}
await this._runReplBeforeTestIfNeeded(test, executionThread);
await executionThread.run(test);
}
catch (e) {
error = e;
}
if (isSessionBroken(error, this._config)) {
this._browser.markAsBroken({ stubBrowserCommands: true });
}
try {
const needsAfterEach = hookRunner.hasAfterEachHooks();
if (needsAfterEach) {
await history.runGroup({
callstack: callstackHistory,
snapshotsPromiseRef: this._browser.snapshotsPromiseRef,
config: this._config,
session: this._browser.publicAPI,
}, "afterEach", () => hookRunner.runAfterEachHooks());
}
}
catch (e) {
error = error || e;
}
finally {
history.requestDomSnapshots({
callstack: callstackHistory,
snapshotsPromiseRef: this._browser.snapshotsPromiseRef,
config: this._config,
session: this._browser.publicAPI,
currentTest: test,
attempt: this._attempt,
});
const snapshotsTimeout = new Promise((_, reject) => setTimeout(() => {
this._browser.markAsBroken({ stubBrowserCommands: true });
reject(new Error(`Collecting Time Travel snapshots timed out after ${SNAPSHOTS_TIMEOUT_MS}ms`));
}, SNAPSHOTS_TIMEOUT_MS));
// If collecting time travel snapshots takes a lot of time, make it obvious by writing a message
const collectingSnapshotsMessageTimeout = setTimeout(() => {
console.log("Collecting Time Travel snapshots takes longer than expected. Waiting...");
}, SNAPSHOTS_WARNING_TIMEOUT_MS);
try {
await Promise.race([this._browser.snapshotsPromiseRef.current, snapshotsTimeout]);
}
catch (e) {
console.error(e.message);
}
finally {
clearTimeout(collectingSnapshotsMessageTimeout);
await history.cleanupDomSnapshots({
callstack: callstackHistory,
session: this._browser.publicAPI,
});
}
}
return error;
}
async _runReplBeforeTestIfNeeded(test, executionThread) {
const { replMode } = RuntimeConfig.getInstance();
// REPL_INSTRUMENTED_FN_FLAG means that we already embedded the switchToRepl call into source via transform,
// so we don't need to run it again here. However, if typescript was not available, we need to run it here.
if (replMode?.beforeTest && !test.fn[REPL_INSTRUMENTED_FN_FLAG]) {
await executionThread.run(Object.create(test, {
timeout: {
value: 0,
},
fn: {
value: () => this._browser.publicAPI.switchToRepl(),
},
}));
}
}
_getPreparePageActions(browser, history) {
if (!browser.config.resetCursor) {
return [];
}
const fn = async () => {
// TODO: make it on browser.init when "actions" method will be implemented in all webdrivers
await history.runGroup({
callstack: browser.callstackHistory,
snapshotsPromiseRef: this._browser.snapshotsPromiseRef,
config: this._config,
session: this._browser.publicAPI,
}, "resetCursor", () => this._resetCursorPosition(browser));
};
return [fn];
}
async _resetCursorPosition({ publicAPI: session }) {
const body = await session.$("body");
if (!body) {
throw new Error('There is no "body" element on the page when resetting cursor position');
}
await body.scrollIntoView();
if (!session.isW3C) {
const { x = 0, y = 0 } = await session.execute(function () {
return document.body.getBoundingClientRect();
});
return session.moveToElement(body.elementId, -Math.floor(x), -Math.floor(y));
}
await session
.action("pointer", { parameters: { pointerType: "mouse" } })
.move({ x: 0, y: 0 })
.perform();
}
};
function isSessionBroken(error, { system: { patternsOnReject } }) {
return error && patternsOnReject.some(p => new RegExp(p).test(error.message));
}
//# sourceMappingURL=index.js.map