const { describe, it, test, beforeAll, afterAll, beforeEach, afterEach } = require('jest-circus');
const expect = require('expect');
const NodeEnvironment = require('jest-environment-node');
const {fn, spyOn} = require('jest-mock');
const run = require('jest-circus/runner');
const Runtime = require('jest-runtime');
const { delay } = require('nanodelay');
const EventEmitter = require('events');
const StackUtils = require('stack-utils');
const { ValueViewerSymbol } = require("@runkit/value-viewer");
const stackUtils = new StackUtils({cwd: 'A path that does not exist'});
global.describe = describe;
global.it = it;
global.test = test;
global.beforeAll = beforeAll;
global.afterAll = afterAll;
global.beforeEach = beforeEach;
global.afterEach = afterEach;
global.expect = expect;
global.jest = { fn, spyOn };
// most of this code is a copy/paste from jest-circus. It would be nice to have jest-circus expost the run function
// so that we don't have to copy / paste all this code
const execute = (function () {
const stateSym = Object.getOwnPropertySymbols(global).find(s => s.toString() == "Symbol(JEST_STATE_SYMBOL)");
const getState = () => global[stateSym];
const dispatcher = new EventEmitter();
async function dispatch(event) {
dispatcher.emit('event', event);
}
const getTestID = (test) => {
const titles = [];
let parent = test;
do {
titles.unshift(parent.name);
} while ((parent = parent.parent));
titles.shift(); // remove TOP_DESCRIBE_BLOCK_NAME
return titles.join(' ');
};
const hasEnabledTest = (describeBlock) => {
const {hasFocusedTests, testNamePattern} = getState();
return describeBlock.children.some(child =>
child.type === 'describeBlock'
? hasEnabledTest(child)
: !(
child.mode === 'skip' ||
(hasFocusedTests && child.mode !== 'only') ||
(testNamePattern && !testNamePattern.test(getTestID(child)))
),
);
};
const getEachHooksForTest = (test) => {
const result = {afterEach: [], beforeEach: []};
let block = test.parent;
do {
const beforeEachForCurrentBlock = [];
// TODO: inline after https://github.com/microsoft/TypeScript/pull/34840 is released
let hook;
for (hook of block.hooks) {
switch (hook.type) {
case 'beforeEach':
beforeEachForCurrentBlock.push(hook);
break;
case 'afterEach':
result.afterEach.push(hook);
break;
}
}
// 'beforeEach' hooks are executed from top to bottom, the opposite of the
// way we traversed it.
result.beforeEach = [...beforeEachForCurrentBlock, ...result.beforeEach];
} while ((block = block.parent));
return result;
};
const getAllHooksForDescribe = (describe) => {
const result = {
afterAll: [],
beforeAll: [],
};
if (hasEnabledTest(describe)) {
for (const hook of describe.hooks) {
switch (hook.type) {
case 'beforeAll':
result.beforeAll.push(hook);
break;
case 'afterAll':
result.afterAll.push(hook);
break;
}
}
}
return result;
};
async function callFnAsPromised(hook, testContext) {
const {fn} = hook;
const returnValue = fn.call(testContext);
if (returnValue instanceof Promise) {
await returnValue;
}
return returnValue;
}
const hasDoneCallback = fn => fn.length > 0;
function callFnWithCallback(hookOrTest, testContext) {
const {fn} = hookOrTest;
return new Promise((resolve, reject) => {
function finish(error) {
if (finish.done) {
return;
}
if (error) {
reject(error);
} else {
resolve();
}
finish.done = true;
}
const done = (error) => {
if (error) {
finish(error);
} else {
finish();
}
};
const returnValue = fn.call(testContext, done);
if (returnValue instanceof Promise) {
finish(new Error(`
Test functions cannot both take a 'done' callback and return something. Either use a 'done' callback, or return a promise.`));
}
});
}
function callAsyncCircusFn(hookOrTest, testContext, timeout) {
return Promise.race([
delay(timeout).then(() => {throw new Error(`timedout`)}),
hasDoneCallback(hookOrTest.fn)
? callFnWithCallback(hookOrTest, testcontext)
: callFnAsPromised(hookOrTest, testContext)
]);
}
const callCircusHook = async ({
hook,
test,
describeBlock,
testContext,
}) => {
const timeout = hook.timeout || getState().testTimeout;
try {
await callAsyncCircusFn(hook, testContext, timeout);
await dispatch({describeBlock, hook, name: 'hook_success', test});
} catch (error) {
await dispatch({describeBlock, error, hook, name: 'hook_failure', test});
}
};
const callCircusTest = async (test, testContext) => {
const timeout = test.timeout || getState().testTimeout;
if (test.errors.length) {
return; // We don't run the test if there's already an error in before hooks.
}
try {
await callAsyncCircusFn(test, testContext, timeout);
await dispatch({name: 'test_fn_success', test});
} catch (error) {
await dispatch({error, name: 'test_fn_failure', test});
}
};
const runTest = async (test, parentSkipped) => {
const testContext = Object.create(null);
const {hasFocusedTests, testNamePattern} = getState();
const isSkipped =
parentSkipped ||
test.mode === 'skip' ||
(hasFocusedTests && test.mode !== 'only') ||
(testNamePattern && !testNamePattern.test(getTestID(test)));
if (isSkipped) {
await dispatch({name: 'test_skip', test});
return;
}
if (test.mode === 'todo') {
await dispatch({name: 'test_todo', test});
return;
}
const {afterEach, beforeEach} = getEachHooksForTest(test);
for (const hook of beforeEach) {
if (test.errors.length) {
// If any of the before hooks failed already, we don't run any
// hooks after that.
break;
}
await callCircusHook({hook, test, testContext});
}
await callCircusTest(test, testContext);
for (const hook of afterEach) {
await callCircusHook({hook, test, testContext});
}
await dispatch({name: 'test_done', test});
};
async function runTestsForDescribeBlock(describeBlock) {
await dispatch({describeBlock, name: 'run_describe_start'});
const {beforeAll, afterAll} = getAllHooksForDescribe(describeBlock);
const isSkipped = describeBlock.mode === 'skip';
if (!isSkipped) {
for (const hook of beforeAll) {
await callCircusHook({describeBlock, hook});
}
}
for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
await runTestsForDescribeBlock(child);
break;
}
case 'test': {
await runTest(child, isSkipped);
break;
}
}
}
if (!isSkipped) {
for (const hook of afterAll) {
await callCircusHook({describeBlock, hook});
}
}
await dispatch({describeBlock, name: 'run_describe_finish'});
}
const makeSingleTestResult = (
test,
) => {
const {includeTestLocationInResult} = getState();
const testPath = [];
let parent = test;
const {status} = test;
// invariant(status, 'Status should be present after tests are run.');
do {
testPath.unshift(parent.name);
} while ((parent = parent.parent));
let location = null;
if (includeTestLocationInResult) {
const stackLines = test.asyncError.stack.split('\n');
const stackLine = stackLines[1];
let parsedLine = stackUtils.parseLine(stackLine);
// if (parsedLine?.file?.startsWith(jestEachBuildDir)) {
// const stackLine = stackLines[4];
// parsedLine = stackUtils.parseLine(stackLine);
// }
if (
parsedLine &&
typeof parsedLine.column === 'number' &&
typeof parsedLine.line === 'number'
) {
location = {
column: parsedLine.column,
line: parsedLine.line,
};
}
}
const errorsDetailed = test.errors.map(_getError);
return {
duration: test.duration,
errors: errorsDetailed.map(getErrorStack),
errorsDetailed,
invocations: test.invocations,
location,
status,
testPath: Array.from(testPath),
};
};
const makeTestResults = (describeBlock)=> {
const testResults = [];
for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
testResults.push(...makeTestResults(child));
break;
}
case 'test': {
testResults.push(makeSingleTestResult(child));
break;
}
}
}
return testResults;
};
const _getError = (errors) => {
let error;
let asyncError;
if (Array.isArray(errors)) {
error = errors[0];
asyncError = errors[1];
} else {
error = errors;
asyncError = new Error();
}
if (error && (typeof error.stack === 'string' || error.message)) {
return error;
}
asyncError.message = error.message;//`thrown: ${prettyFormat(error, {maxDepth: 3})}`;
return asyncError;
};
const getErrorStack = (error) =>
typeof error.stack === 'string' ? error.stack : error.message;
const makeRunResult = (describeBlock,unhandledErrors) => ({
testResults: makeTestResults(describeBlock),
unhandledErrors: unhandledErrors.map(_getError).map(getErrorStack),
});
const run = async () => {
const {rootDescribeBlock} = getState();
await dispatch({name: 'run_start'});
await runTestsForDescribeBlock(rootDescribeBlock);
await dispatch({name: 'run_finish'});
return makeRunResult(
getState().rootDescribeBlock,
getState().unhandledErrors,
);
};
class JestTestResults {
constructor(testResults) {
const title = 'Jest Test Results';
const getTestName = result => result.testPath.slice(1).join(' › ');
const HTML = `
<div>
${testResults.testResults.map(result => (`
<div>
${result.status === 'success'
? (`✔️ ${getTestName(result)}`)
: (`<div style="color:red">
<div>❌ ${getTestName(result)}</div>
<div>
${result.errors.map(error => `<pre>${error}</pre>`)}
</div>
</div>`)}
</div>
`)).join('\n')}
</div>
`;
Object.assign(this, {[ValueViewerSymbol]: {title, HTML } });
}
}
dispatcher.on('event', event => {
// console.log(event);
switch (event.name) {
case 'test_fn_success':
event.test.status = 'success';
break;
case 'test_fn_failure':
event.test.status = 'error';
event.test.errors.push(event.error);
break;
default:
break;
}
})
return () => run().then(testResults => {
const results = new JestTestResults(testResults);
console.log(results);
return results;
});
}());
execute();