Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@netflix/x-test

Package Overview
Dependencies
Maintainers
10
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@netflix/x-test - npm Package Compare versions

Comparing version 1.0.0-rc.18 to 1.0.0-rc.19

demo/fail/index.html

28

demo/index.js

@@ -1,27 +0,5 @@

import { assert, it, test, todo, cover } from '../x-test.js';
import { it, assert } from '../x-test.js';
// We import this here so we can see code coverage.
import '../x-test.js';
cover(new URL('../x-test.js', import.meta.url).href, 85);
it('truthy things pass assertion tests', async () => {
await false;
assert(1);
it('start testing!', () => {
assert(true);
});
todo(`multi
line
test`,
() => {
assert(0);
}
);
todo('demonstrate passing "todo"', () => {
assert(1);
});
test('./test-basic.html');
test('./test-sibling.html');
test('./nested/');
{
"name": "@netflix/x-test",
"version": "1.0.0-rc.18",
"version": "1.0.0-rc.19",
"description": "a simple, tap-compliant test runner for the browser",

@@ -17,3 +17,3 @@ "main": "x-test.js",

"scripts": {
"start": "element-server",
"start": "http-server -o demo -c-1",
"lint": "eslint --max-warnings=0 .",

@@ -25,5 +25,2 @@ "lint-fix": "eslint --fix .",

"LICENSE",
"/index.html",
"/index.css",
"/index.js",
"/test.js",

@@ -35,7 +32,7 @@ "/x-test.js",

"devDependencies": {
"@netflix/element-server": "^1.0.12",
"eslint": "^8.35.0",
"eslint": "^8.36.0",
"http-server": "^14.1.1",
"puppeteer": "^19.7.2",
"tap-parser": "^10.0.1"
"tap-parser": "^12.0.1"
}
}

@@ -8,5 +8,5 @@ # x-test

- importable as `type="module"`
- is interoperable with TAP Version 13
- is interoperable with TAP Version 14
- nested sub-tests run in an iframe
- has a recognizable testing interface
- has a recognizable testing interface (`it`, `describe`, `assert`)
- can be used for automated testing

@@ -22,27 +22,34 @@ - can be used to assert coverage goals

- `test`: Creates a sub-test in an `iframe` based on given `src` html page.
- `it`: The smallest testing unit--asynchronous.
- `skip`: An `it` whose callback is not run and which will pass.
- `todo`: An `it` whose callback _is_ run and is expected to fail.
- `waitFor`: Ensures the test does not exit until given promise settles.
- `assert`: Simple assertion call that expects a boolean.
- `cover`: Sets a coverage goal for the given url.
- `it`: The smallest testing unit — can be asynchronous.
- `it.skip`: An `it` whose callback is not run and which will pass.
- `it.only`: Skip all other `it` tests.
- `it.todo`: An `it` whose callback _is_ run and is expected to fail.
- `describe`: Simple grouping functionality.
- `describe.skip`: Skip all `it` tests in this group.
- `describe.only`: Skip all other `describe` groups and `it` tests.
- `describe.todo`: Mark all `it` tests within this group as _todo_.
- `waitFor`: Ensures test registration remains open until given promise settles.
- `assert`: Simple assertion call that throws if the boolean input is false-y.
- `coverage`: Sets a coverage goal for the given href.
### Events
- `x-test-ping`: root responds ('x-test-pong', { status: 'started'|'ended' })
- `x-test-ended`: all tests have completed or we bailed out
- `x-test-cover-start`: root responds ('x-test-cover-ended')
- `x-test-cover`: [internal] signal to test for coverage on a particular file
- `x-test-bail`: [internal] signal to quit test early
- `x-test-queue`: [internal] queues a new test
- `x-test-next`: [internal] destroy current test and create a new one
- `x-test-it-started`: [internal] user defined a new "it"
- `x-test-it-ended`: [internal] user-defined "it" completed
- `x-test-client-ping`: root responds (`x-test-root-pong`, { status: 'started'|'ended' waiting: true|false })
- `x-test-root-pong`: response to `x-test-client-ping`
- `x-test-root-coverage-request`: client should respond (`x-test-coverage-result`)
- `x-test-client-coverage-result`: response to `x-test-root-coverage-request`
- `x-test-root-end`: all tests have completed or we bailed out
- (internal) `x-test-root-run`: all tests have completed or we bailed out
- (internal) `x-test-suite-coverage`: signal to test for coverage on a particular file
- (internal) `x-test-suite-register`: registers a new test / describe / it
- (internal) `x-test-suite-ready`: signal that test suite is done with registration
- (internal) `x-test-suite-result`: marks end of "it" test
- (internal) `x-test-suite-bail`: signal to quit test early
### Parameters
The following parameters can be passed in via a url `search`:
The following parameters can be passed in via query params on the url:
- `x-test-no-reporter`: turns off custom reporting tool ui
- `x-test-cover`: turns on coverage reporting**
- `x-test-run-coverage`: turns on coverage reporting**

@@ -49,0 +56,0 @@ **See `test.js` for an example of how to capture coverage information in

@@ -17,3 +17,3 @@ /* eslint-env node */

// Visit our test page.
await page.goto('http://0.0.0.0:8080/node_modules/@netflix/x-test/test/?x-test-cover');
await page.goto('http://127.0.0.1:8080/test/?x-test-run-coverage');

@@ -26,3 +26,7 @@ // Wait to be signaled about the end of the test. Because the test may have

const { type, data } = evt.data;
if (type === 'x-test-ended' || (type === 'x-test-pong' && data.ended)) {
if (
type === 'x-test-root-coverage-request' ||
type === 'x-test-root-end' ||
(type === 'x-test-root-pong' && (data.waiting || data.ended))
) {
top.removeEventListener('message', onMessage);

@@ -33,7 +37,7 @@ resolve();

top.addEventListener('message', onMessage);
top.postMessage({ type: 'x-test-ping' }, '*');
top.postMessage({ type: 'x-test-client-ping' }, '*');
});
});
// Gather coverage information.
// Gather / send coverage information.
const js = await page.coverage.stopJSCoverage();

@@ -46,3 +50,3 @@

const { type } = evt.data;
if (type === 'x-test-cover-ended') {
if (type === 'x-test-root-end') {
top.removeEventListener('message', onMessage);

@@ -53,3 +57,3 @@ resolve();

top.addEventListener('message', onMessage);
top.postMessage({ type: 'x-test-cover-start', data }, '*');
top.postMessage({ type: 'x-test-client-coverage-result', data }, '*');
});

@@ -56,0 +60,0 @@ }, { js });

@@ -1,31 +0,9 @@

import { assert, it, test, todo, cover } from '../x-test.js';
import { test, coverage } from '../x-test.js';
// We import this here so we can see code coverage.
import '../x-test.js';
coverage('../x-test.js', 85);
cover(new URL('../x-test.js', import.meta.url).href, 85);
it('truthy things pass assertion tests', async () => {
await false;
assert(1);
});
todo(
`multi
line
test`,
() => {
assert(0);
}
);
todo('demonstrate passing "todo"', () => {
assert(1);
});
test('./test-basic.html');
test('./test-sibling.html');
test('./nested/');
test('./test-reporter.html');
test('./test-coverage.html');
test('./test-suite.html');
test('./test-root.html');
test('./test-tap.html');
test('./test-scratch.html');

@@ -1,127 +0,181 @@

import { it, assert, __XTestReporter__ } from '../x-test.js';
import { it, describe, assert, __XTestReporter__ } from '../x-test.js';
it('parses test', () => {
const output = '# http://example.com/test.html';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'a');
assert(Object.keys(result.properties).length === 2);
assert(result.properties.href === 'http://example.com/test.html');
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.test === '');
assert(result.done === false);
assert(result.failed === false);
});
describe('render', () => {
it('prints out basic test', () => {
const tap = [
'TAP Version 14',
'# Subtest: http://127.0.0.1:8080/test/',
' # Subtest: level 1',
' ok 1 - first test',
' ok 2 - second test',
' not ok 3 - third test',
' ---',
' message: this error is expected',
' severity: todo',
' stack: |-',
' Error: this error is expected',
' at XTestSuite.assert (http://127.0.0.1:8080/x-test.js:1320:15)',
' at assert (http://127.0.0.1:8080/x-test.js:5:48)',
' at http://127.0.0.1:8080/test/test-scratch.js:22:7',
' at XTestSuite.onRun (http://127.0.0.1:8080/x-test.js:1287:31)',
' at http://127.0.0.1:8080/x-test.js:1251:22',
' ...',
' 1..3',
' not ok 1 - level 1',
' 1..1',
'not ok 1 - http://127.0.0.1:8080/test/',
'1..1',
];
const element = document.createElement('x-test-reporter');
document.body.append(element);
element.tap(...tap);
assert(element.shadowRoot.getElementById('container').textContent === tap.join(''));
assert(element.getAttribute('ok') === null);
element.remove();
});
it('parses bail test', () => {
const output = 'Bail out! http://example.com/test.html';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'a');
assert(Object.keys(result.properties).length === 2);
assert(result.properties.href === 'http://example.com/test.html');
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 3);
assert(result.attributes.output === '');
assert(result.attributes.test === '');
assert(result.attributes.bail === '');
assert(result.done === false);
assert(result.failed === true);
it('bails', () => {
const tap = [
'TAP Version 14',
'# Subtest: http://127.0.0.1:8080/test/',
' # these',
' # are',
' # comments ',
'Bail out! http://127.0.0.1:8080/test/',
];
const element = document.createElement('x-test-reporter');
document.body.append(element);
element.tap(...tap);
assert(element.shadowRoot.getElementById('container').textContent === tap.join(''));
assert(element.getAttribute('ok') === null);
element.remove();
});
});
it('parses diagnostic', () => {
const output = '# something, anything';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.diagnostic === '');
assert(result.done === false);
assert(result.failed === false);
});
describe('parse', () => {
it('parses test', () => {
const output = '# Subtest: http://example.com/test.html';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'a');
assert(Object.keys(result.properties).length === 2);
assert(result.properties.href === 'http://example.com/test.html');
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.subtest === '');
assert(result.done === false);
assert(result.failed === false);
});
it('parses test line', () => {
const output = 'ok - 145 this cool thing i tested';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 3);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.ok === '');
assert(result.done === false);
assert(result.failed === false);
});
it('parses bail test', () => {
const output = 'Bail out! http://example.com/test.html';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'a');
assert(Object.keys(result.properties).length === 2);
assert(result.properties.href === 'http://example.com/test.html');
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 3);
assert(result.attributes.output === '');
assert(result.attributes.subtest === '');
assert(result.attributes.bail === '');
assert(result.done === false);
assert(result.failed === true);
});
it('parses "SKIP" test line', () => {
const output = 'ok - 145 this cool thing i tested # SKIP';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 4);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.ok === '');
assert(result.attributes.directive === 'skip');
assert(result.done === false);
assert(result.failed === false);
});
it('parses diagnostic', () => {
const output = '# something, anything';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.diagnostic === '');
assert(result.done === false);
assert(result.failed === false);
});
it('parses "TODO" test line', () => {
const output = 'ok - 145 this cool thing i tested # TODO';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 4);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.ok === '');
assert(result.attributes.directive === 'todo');
assert(result.done === false);
assert(result.failed === false);
});
it('parses test line', () => {
const output = 'ok 145 - this cool thing i tested';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 3);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.ok === '');
assert(result.done === false);
assert(result.failed === false);
});
it('parses "not ok" test line', () => {
const output = 'not ok - 145 this cool thing i tested';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.done === false);
assert(result.failed === true);
});
it('parses "SKIP" test line', () => {
const output = 'ok 145 - this cool thing i tested # SKIP';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 4);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.ok === '');
assert(result.attributes.directive === 'skip');
assert(result.done === false);
assert(result.failed === false);
});
it('parses "not ok", "TODO" test line', () => {
const output = 'not ok - 145 this cool thing i tested # TODO tbd...';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 3);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.directive === 'todo');
assert(result.done === false);
assert(result.failed === false);
});
it('parses "TODO" test line', () => {
const output = 'ok 145 - this cool thing i tested # TODO';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 4);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.ok === '');
assert(result.attributes.directive === 'todo');
assert(result.done === false);
assert(result.failed === false);
});
it('parses version', () => {
const output = 'TAP version 13';
const result = __XTestReporter__.parseOutput(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.version === '');
assert(result.done === false);
assert(result.failed === false);
it('parses "not ok" test line', () => {
const output = 'not ok 145 - this cool thing i tested';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.done === false);
assert(result.failed === true);
});
it('parses "not ok", "TODO" test line', () => {
const output = 'not ok 145 - this cool thing i tested # TODO tbd...';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 3);
assert(result.attributes.output === '');
assert(result.attributes.it === '');
assert(result.attributes.directive === 'todo');
assert(result.done === false);
assert(result.failed === false);
});
it('parses version', () => {
const output = 'TAP version 14';
const result = __XTestReporter__.parse(output);
assert(result.tag === 'div');
assert(Object.keys(result.properties).length === 1);
assert(result.properties.innerText === output);
assert(Object.keys(result.attributes).length === 2);
assert(result.attributes.output === '');
assert(result.attributes.version === '');
assert(result.done === false);
assert(result.failed === false);
});
});

@@ -1,19 +0,91 @@

import { it, assert, __Tap__ } from '../x-test.js';
import { it, describe, assert, __XTestTap__ } from '../x-test.js';
it('version line is correct', () => {
assert(__Tap__.version() === 'TAP Version 13');
describe('version', () => {
it('renders "TAP Version 14"', () => {
assert(__XTestTap__.version() === 'TAP Version 14');
});
});
it('diagnostic works', () => {
assert(__Tap__.diagnostic('my message') === '# my message');
describe('diagnostic', () => {
it('basic', () => {
assert(__XTestTap__.diagnostic('my message') === '# my message');
});
it('basic, indented', () => {
assert(__XTestTap__.diagnostic('my message', 1) === ' # my message');
});
it('multiline', () => {
assert(__XTestTap__.diagnostic('one\ntwo\nthree') === '# one\n# two\n# three');
});
it('multiline, indented', () => {
assert(__XTestTap__.diagnostic('one\ntwo\nthree', 1) === ' # one\n # two\n # three');
});
});
it('testLine works', () => {
assert(__Tap__.testLine(true, 1, 'first test') === 'ok - 1 first test');
assert(__Tap__.testLine(false, 1, 'first test') === 'not ok - 1 first test');
assert(__Tap__.testLine(false, 1, 'first test', 'TODO') === 'not ok - 1 first test # TODO');
describe('testLine', () => {
it('basic, passing', () => {
assert(__XTestTap__.testLine(true, 1, 'first test') === 'ok 1 - first test');
});
it('basic, passing, indented', () => {
assert(__XTestTap__.testLine(true, 1, 'first test', null, 1) === ' ok 1 - first test');
});
it('basic, failing', () => {
assert(__XTestTap__.testLine(false, 1, 'first test') === 'not ok 1 - first test');
});
it('basic, failing, indented', () => {
assert(__XTestTap__.testLine(false, 1, 'first test', null, 1) === ' not ok 1 - first test');
});
it('multiline', () => {
assert(__XTestTap__.testLine(true, 1, 'first\ntest') === 'ok 1 - first test');
});
it('multiline, indented', () => {
assert(__XTestTap__.testLine(true, 1, 'first\ntest', null, 1) === ' ok 1 - first test');
});
it('skip', () => {
assert(__XTestTap__.testLine(true, 1, 'first test', 'SKIP') === 'ok 1 - first test # SKIP');
});
it('skip, indented', () => {
assert(__XTestTap__.testLine(true, 1, 'first test', 'SKIP', 1) === ' ok 1 - first test # SKIP');
});
it('todo, passing', () => {
assert(__XTestTap__.testLine(true, 1, 'first test', 'TODO') === 'ok 1 - first test # TODO');
});
it('todo, passing, indented', () => {
assert(__XTestTap__.testLine(true, 1, 'first test', 'TODO', 1) === ' ok 1 - first test # TODO');
});
it('todo, failing', () => {
assert(__XTestTap__.testLine(false, 1, 'first test', 'TODO') === 'not ok 1 - first test # TODO');
});
it('todo, failing, indented', () => {
assert(__XTestTap__.testLine(false, 1, 'first test', 'TODO', 1) === ' not ok 1 - first test # TODO');
});
});
it('yaml works', () => {
const expected = ` ---
describe('subtest', () => {
it('basic', () => {
assert(__XTestTap__.subtest('my subtest') === '# Subtest: my subtest');
});
it('basic, indented', () => {
assert(__XTestTap__.subtest('my subtest', 1) === ' # Subtest: my subtest');
});
});
describe('yaml', () => {
it('basic', () => {
const expected = `\
---
message: my message

@@ -26,11 +98,29 @@ severity: error

...`;
assert(__Tap__.yaml('my message', 'error', { stack: 'one\ntwo\nthree'}) === expected);
assert(__XTestTap__.yaml('my message', 'error', { stack: 'one\ntwo\nthree' }) === expected);
});
it('indented', () => {
const expected = `\
---
message: my message
severity: error
stack: |-
one
two
three
...`;
assert(__XTestTap__.yaml('my message', 'error', { stack: 'one\ntwo\nthree' }, 1) === expected);
});
});
it('bailOut works', () => {
assert(__Tap__.bailOut('oh no!') === 'Bail out! oh no!');
describe('bailOut', () => {
it('basic', () => {
assert(__XTestTap__.bailOut('oh no!') === 'Bail out! oh no!');
});
});
it('plan works', () => {
assert(__Tap__.plan(999) === '1..999');
describe('plan', () => {
it('basic', () => {
assert(__XTestTap__.plan(999) === '1..999');
});
});

@@ -1,33 +0,75 @@

const TAG_LINE = 'x-test - a simple, tap-compliant test runner for the browser.';
/**
* Simple assertion which throws exception when not "ok".
* assert('foo' === 'bar', 'foo does not equal bar');
*/
export const assert = (ok, text) => XTestSuite.assert(suiteContext, ok, text);
export const assert = (assertion, message) => {
Test.assert(assertion, message);
};
/**
* Register coverage percentage goal for a given file.
* coverage('../foo.js', 87);
*/
export const coverage = (href, goal) => XTestSuite.coverage(suiteContext, href, goal);
export const test = href => {
Test.test(_test, href);
};
/**
* Force test suite registration to remain open until promise resolves.
* const barsPromise = fetch('https://foo/api/v2/bars').then(response => response.json());
* waitFor(barsPromise);
*/
export const waitFor = promise => XTestSuite.waitFor(suiteContext, promise);
export const it = (description, callback, interval) => {
Test.it(_test, null, description, callback, interval);
};
/**
* Register a test to be run as a subsequent test suite.
* test('./test-sibling.html');
*/
export const test = href => XTestSuite.test(suiteContext, href);
export const skip = (description, callback, interval) => {
Test.it(_test, 'SKIP', description, callback, interval);
};
/**
* Register a grouping. Alternatively, mark with flags.
*/
export const describe = (text, callback) => XTestSuite.describe(suiteContext, text, callback);
describe.skip = (text, callback) => XTestSuite.describeSkip(suiteContext, text, callback);
describe.only = (text, callback) => XTestSuite.describeOnly(suiteContext, text, callback);
describe.todo = (text, callback) => XTestSuite.describeTodo(suiteContext, text, callback);
export const todo = (description, callback, interval) => {
Test.it(_test, 'TODO', description, callback, interval);
};
/**
* Register an individual test lint. Alternatively, mark with flags.
*/
export const it = (text, callback, interval) => XTestSuite.it(suiteContext, text, callback, interval);
it.skip = (text, callback, interval) => XTestSuite.itSkip(suiteContext, text, callback, interval);
it.only = (text, callback, interval) => XTestSuite.itOnly(suiteContext, text, callback, interval);
it.todo = (text, callback, interval) => XTestSuite.itTodo(suiteContext, text, callback, interval);
export const waitFor = promise => {
Test.waitFor(_test, promise);
};
// Internal Interface. This is exposed for testing purposes only.
export { XTestRoot as __XTestRoot__, XTestReporter as __XTestReporter__, XTestTap as __XTestTap__, XTestSuite as __XTestSuite__ };
export const cover = (relativePath, goal) => {
Test.cover(_test, relativePath, goal);
};
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
function uuid() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
}
export { XTestReporter as __XTestReporter__, Tap as __Tap__, Test as __Test__, RootTest as __RootTest__ };
function publish(type, data) {
top.postMessage({ type, data }, '*');
}
function subscribe(callback) {
top.addEventListener('message', callback);
}
function addErrorListener(callback) {
addEventListener('error', callback);
}
function addUnhandledrejectionListener(callback) {
addEventListener('unhandledrejection', callback);
}
async function timeout(interval) {
interval = interval ?? 30000;
await new Promise((resolve, reject) => {
setTimeout(() => { reject(new Error(`timeout after ${interval}ms`)); }, interval);
});
}
class XTestReporter extends HTMLElement {

@@ -37,161 +79,299 @@ constructor() {

this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `\
<style>
:host {
display: flex;
flex-direction: column;
justify-content: flex-end;
position: fixed;
z-index: 1;
left: 0;
bottom: 0;
width: 100vw;
height: 400px;
background-color: var(--black);
max-height: 100vh;
min-height: var(--header-height);
font-family: monospace;
--header-height: 40px;
--black: #111111;
--white: white;
--subdued: #8C8C8C;
--version: #39CCCC;
--todo: #FF851B;
--todone: #FFDC00;
--skip: #FF851B;
--ok: #2ECC40;
--not-ok: #FF4136;
--subtest: #4D4D4D;
--plan: #39CCCC;
--link: #0085ff;
}
:host(:not([open])) {
max-height: var(--header-height);
}
#header {
display: flex;
justify-content: space-between;
align-items: center;
height: var(--header-height);
flex-shrink: 0;
box-shadow: inset 0 -1px 0 0 #484848, 0 1px 2px 0 #484848;
padding-right: 38px;
background-color: var(--x-test-reporter-background-color);
}
:host([open]) #header {
cursor: grab;
}
:host([open][dragging]) #header {
cursor: grabbing;
}
#toggle {
position: fixed;
bottom: 7px;
right: 12px;
font: inherit;
margin: 0;
border: none;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
height: 26px;
width: 26px;
cursor: pointer;
--color: var(--subdued);
color: var(--color);
background-color: transparent;
box-shadow: inset 0 0 0 1px var(--color);
}
#toggle:hover,
#toggle:focus-visible {
--color: var(--white);
}
#toggle:active {
--color: var(--subdued);
}
#toggle::before {
content: "↑";
}
:host([open]) #toggle::before {
content: "↓";
}
#result {
margin: auto 12px;
padding: 6px 16px;
border-radius: 4px;
line-height: 14px;
color: var(--white);
background-color: var(--todo);
user-select: none;
pointer-events: none;
white-space: nowrap;
}
#result::before {
content: "TESTING...";
}
:host(:not([ok])) #result {
background-color: var(--not-ok);
}
:host(:not([ok])) #result::before {
content: "NOT OK!";
}
:host([ok]:not([testing])) #result {
background-color: var(--ok);
}
:host([ok]:not([testing])) #result::before {
content: "OK!";
}
#tag-line {
margin: auto 12px;
color: var(--subdued);
cursor: default;
user-select: none;
pointer-events: none;
}
#body {
flex: 1;
overflow: auto;
display: flex;
/* Flip top/bottom for console-like scroll behavior. */
flex-direction: column-reverse;
box-sizing: border-box;
}
:host([dragging]) #body {
pointer-events: none;
}
#spacer {
flex: 1;
}
[output] {
white-space: pre;
color: var(--subdued);
line-height: 20px;
padding: 0 12px;
cursor: default;
}
[output]:first-child {
padding-top: 12px;
}
[output]:last-child {
padding-bottom: 12px;
}
[yaml] {
line-height: 16px;
}
a[output]:any-link {
display: block;
width: min-content;
cursor: pointer;
}
[it][ok]:not([directive]) {
color: var(--ok);
}
[it]:not([ok]):not([directive]),
[bail] {
color: var(--not-ok);
}
[it][ok][directive="skip"] {
color: var(--skip);
}
[it]:not([ok])[directive="todo"] {
color: var(--todo);
}
[it][ok][directive="todo"] {
color: var(--todone);
}
[plan][indent],
[plan] + [it][ok]:not([directive]),
[plan] + [it]:not([ok]):not([directive]),
[plan] + [it][ok][directive="skip"],
[plan] + [it]:not([ok])[directive="todo"],
[plan] + [it][ok][directive="todo"] {
color: var(--subdued);
}
[version] {
color: var(--version);
}
[plan] {
color: var(--plan);
}
a[subtest]:not([bail]) {
color: var(--link);
}
[subtest]:not([bail]) {
color: var(--subdued);
}
[indent] {
position: relative;
}
[indent]::before {
position: absolute;
content: attr(indent);
color: var(--subdued);
opacity: 0.25;
}
</style>
<div id="header"><div id="result"></div><div id="tag-line">x-test - a simple, tap-compliant test runner for the browser.</div></div>
<div id="body"><div id="spacer"></div><div id="container"></div></div>
<button id="toggle" type="button"></button>
`;
}
set outputs(value) {
this.__outputs = value;
this.constructor.update(this);
}
get outputs() {
return this.__outputs;
}
connectedCallback() {
this.constructor.initializeOnce(this);
this.setAttribute('ok', '');
this.setAttribute('testing', '');
this.style.height = localStorage.getItem('x-test-reporter-height');
if (localStorage.getItem('x-test-reporter-closed') !== 'true') {
this.setAttribute('open', '');
}
this.shadowRoot.getElementById('toggle').addEventListener('click', () => {
this.hasAttribute('open') ? this.removeAttribute('open') : this.setAttribute('open', '');
localStorage.setItem('x-test-reporter-closed', String(!this.hasAttribute('open')));
});
const resize = event => {
const nextHeaderY = event.clientY - Number(this.getAttribute('dragging'));
const currentHeaderY = this.shadowRoot.getElementById('header').getBoundingClientRect().y;
const currentHeight = this.getBoundingClientRect().height;
this.style.height = `${Math.round(currentHeight + currentHeaderY - nextHeaderY)}px`;
localStorage.setItem('x-test-reporter-height', this.style.height);
};
this.shadowRoot.getElementById('header').addEventListener('pointerdown', event => {
if (this.hasAttribute('open')) {
const headerY = this.shadowRoot.getElementById('header').getBoundingClientRect().y;
const clientY = event.clientY;
this.setAttribute('dragging', String(clientY - headerY));
addEventListener('pointermove', resize);
for (const iframe of document.querySelectorAll('iframe')) {
iframe.style.pointerEvents = 'none';
}
}
});
addEventListener('pointerup', () => {
removeEventListener('pointermove', resize);
this.removeAttribute('dragging');
for (const iframe of document.querySelectorAll('iframe')) {
iframe.style.pointerEvents = null;
}
});
}
static initializeOnce(target) {
if (!target.__initialized) {
target.__initialized = true;
target.shadowRoot.innerHTML = `
<style>
:host {
display: flex;
flex-direction: column;
position: fixed;
z-index: 1;
right: 0;
bottom: 0;
left: 0;
height: 45vh;
min-height: 260px;
background-color: #111111;
font-family: monospace;
}
#header {
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
box-shadow: inset 0 -1px 0 0 #484848, 0 1px 2px 0 #484848;
}
#result {
margin: auto 12px;
padding: 6px 16px;
border-radius: 4px;
line-height: 14px;
color: white;
background-color: #FF851B;
}
#result::before {
content: "TESTING...";
}
:host(:not([ok])) #result {
background-color: #FF4136;
}
:host(:not([ok])) #result::before {
content: "NOT OK!";
}
:host([ok]:not([testing])) #result {
background-color: #2ECC40;
}
:host([ok]:not([testing])) #result::before {
content: "OK!";
}
#tag-line {
margin: auto 12px;
color: #8C8C8C;
cursor: default;
}
#body {
flex: 1;
overflow: auto;
display: flex;
/* Flip top/bottom for console-like scroll behavior. */
flex-direction: column-reverse;
box-sizing: border-box;
}
#spacer {
flex: 1;
}
[output] {
white-space: pre;
color: #AAAAAA;
line-height: 20px;
padding: 0 12px;
cursor: default;
}
[output]:first-child {
padding-top: 12px;
}
[output]:last-child {
padding-bottom: 12px;
}
[yaml] {
line-height: 16px;
}
[test],
[bail] {
display: block;
width: min-content;
cursor: pointer;
}
[it][ok]:not([directive]) {
color: #2ECC40;
}
[it]:not([ok]):not([directive]),
[bail] {
color: #FF4136;
}
[it][ok][directive="skip"],
[it]:not([ok])[directive="todo"] {
color: #FF851B;
}
[it][ok][directive="todo"] {
color: #FFDC00;
}
[version],
[plan],
[diagnostic] {
color: #39CCCC;
}
[test]:not([bail]) {
color: #0085ff;
}
</style>
<div id="header"><div id="result"></div><div id="tag-line">${TAG_LINE}</div></div>
<div id="body"><div id="spacer"></div><div id="container"></div></div>
`;
target.setAttribute('ok', '');
target.setAttribute('testing', '');
tap(...tap) {
const items = [];
const container = this.shadowRoot.getElementById('container');
for (const text of tap) {
const { tag, properties, attributes, failed, done } = XTestReporter.parse(text);
const element = document.createElement(tag);
Object.assign(element, properties);
for (const [attribute, value] of Object.entries(attributes)) {
element.setAttribute(attribute, value);
}
if (done) {
this.removeAttribute('testing');
}
if (failed) {
this.removeAttribute('ok');
}
items.push(element);
}
container.append(...items);
}
static parseOutput(output) {
static parse(text) {
const result = { tag: '', properties: {}, attributes: {}, failed: false, done: false };
result.properties.innerText = output;
if (output.match(/^# https?:.*/)) {
result.properties.innerText = text;
const indentMatch = text.match(/^((?: {4})+)/);
if (indentMatch) {
const lines = text.split('\n').length - 1;
const indent = indentMatch[1].replace(/ {4}/g, '\u00a6 ');
result.attributes.indent = lines ? `${`${indent}\n`.repeat(lines)}${indent}` : indent;
}
if (text.match(/^(?: {4})*# Subtest: https?:.*/)) {
result.tag = 'a';
result.properties.href = output.replace('# ', '');
Object.assign(result.attributes, { output: '', test: '' });
} else if (output.match(/^Bail out! https?:.*/)) {
const href = text.replace(/^(?: {4})*# Subtest: /, '');
result.properties.href = href;
Object.assign(result.attributes, { output: '', subtest: '' });
} else if (text.match(/^Bail out! https?:.*/)) {
result.tag = 'a';
result.failed = true;
result.properties.href = output.replace('Bail out! ', '');
Object.assign(result.attributes, { output: '', test: '', bail: '' });
const href = text.replace(/Bail out! /, '');
result.properties.href = href;
Object.assign(result.attributes, { output: '', subtest: '', bail: '' });
} else {
result.tag = 'div';
result.attributes.output = '';
if (output.match(/^# /)) {
if (text.match(/^(?: {4})*# Subtest:/)) {
result.attributes.subtest = '';
} else if (text.match(/^(?: {4})*# /)) {
result.attributes.diagnostic = '';
} else if (output.match(/^ok /)) {
} else if (text.match(/^(?: {4})*ok /)) {
Object.assign(result.attributes, { it: '', ok: '' });
if (output.match(/^[^#]* # SKIP/)) {
if (text.match(/^(?: {4})*[^ #][^#]* # SKIP/)) {
result.attributes.directive = 'skip';
} else if (output.match(/^[^#]* # TODO/)) {
} else if (text.match(/^(?: {4})*[^ #][^#]* # TODO/)) {
result.attributes.directive = 'todo';
}
} else if (output.match(/^not ok /)) {
} else if (text.match(/^(?: {4})*not ok /)) {
result.attributes.it = '';
if (output.match(/^[^#]* # TODO/)) {
if (text.match(/^(?: {4})*[^ #][^#]* # TODO/)) {
result.attributes.directive = 'todo';

@@ -201,10 +381,12 @@ } else {

}
} else if (output.match(/^ {2}---/)) {
} else if (text.match(/^(?: {4})* {2}---/)) {
result.attributes.yaml = '';
} else if (output.match(/^TAP/)) {
} else if (text.match(/^TAP/)) {
result.attributes.version = '';
} else if (output.match(/^1\.\.\d*/)) {
} else if (text.match(/^(?: {4})*1\.\.\d*/)) {
result.attributes.plan = '';
result.done = true;
} else if (output.match(/Bail out!.*/)) {
if (!indentMatch) {
result.done = true;
}
} else if (text.match(/^(?: {4})*Bail out!.*/)) {
result.attributes.bail = '';

@@ -216,353 +398,758 @@ result.failed = true;

}
}
customElements.define('x-test-reporter', XTestReporter);
static update(target) {
if (target.outputs) {
const items = [];
const container = target.shadowRoot.getElementById('container');
for (const output of target.outputs.slice(container.children.length)) {
const { tag, properties, attributes, failed, done } = this.parseOutput(output);
const element = document.createElement(tag);
Object.assign(element, properties);
for (const [attribute, value] of Object.entries(attributes)) {
element.setAttribute(attribute, value);
class XTestRoot {
static initialize(context, href) {
const url = new URL(href);
if (!url.searchParams.get('x-test-no-reporter')) {
context.state.reporter = new XTestReporter();
document.body.append(context.state.reporter);
}
context.state.coverage = url.searchParams.get('x-test-run-coverage') === '';
context.state.coverageValuePromise = new Promise(resolve => {
context.state.resolveCoverageValuePromise = value => {
context.state.coverageValue = value;
resolve(context.state.coverageValue);
};
});
const versionStepId = context.uuid();
const exitStepId = context.uuid();
context.state.stepIds.push(versionStepId, exitStepId);
context.state.steps[versionStepId] = { stepId: versionStepId, type: 'version', status: 'waiting' };
context.state.steps[exitStepId] = { stepId: exitStepId, type: 'exit', status: 'waiting' };
context.subscribe(event => {
switch (event.data.type) {
case 'x-test-client-ping':
XTestRoot.onPing(context, event);
break;
case 'x-test-client-coverage-result':
XTestRoot.onCoverageResult(context, event);
break;
case 'x-test-suite-register':
XTestRoot.onRegister(context, event);
break;
case 'x-test-suite-ready':
XTestRoot.onReady(context, event);
break;
case 'x-test-suite-result':
XTestRoot.onResult(context, event);
break;
case 'x-test-suite-bail':
XTestRoot.onBail(context, event);
break;
}
XTestRoot.check(context);
});
// Run own tests in iframe.
url.searchParams.delete('x-test-no-reporter');
url.searchParams.delete('x-test-run-coverage');
context.publish('x-test-suite-register', { type: 'test', testId: context.uuid(), href: url.href });
}
static onPing(context/*, event*/) {
context.publish('x-test-root-pong', { ended: context.state.ended, waiting: context.state.waiting });
}
static onBail(context, event) {
if (!context.state.ended) {
XTestRoot.bail(context, event.data.data.error, { testId: event.data.data.testId });
}
}
static registerTest(context, data) {
if (!context.state.ended) {
const testId = data.testId;
// New "test" (to be opened in its own iframe). Queue it up.
const initiatorTestId = data.initiatorTestId;
const siblingTestEndIndex = context.state.stepIds.findLastIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'test-end' && context.state.tests[candidate.testId].initiatorTestId === initiatorTestId) {
return true;
}
if (done) {
target.removeAttribute('testing');
});
const parentTestEndIndex = context.state.stepIds.findLastIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'test-end' && context.state.tests[candidate.testId].testId === initiatorTestId) {
return true;
}
if (failed) {
target.removeAttribute('ok');
});
const coverageIndex = context.state.stepIds.findIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'coverage') {
return true;
}
items.push(element);
});
const exitIndex = context.state.stepIds.findLastIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'exit') {
return true;
}
});
const index = siblingTestEndIndex === -1
? parentTestEndIndex === -1
? coverageIndex === -1
? exitIndex
: coverageIndex
: parentTestEndIndex + 1
: siblingTestEndIndex + 1;
const lastSiblingChildrenIndex = context.state.children.findLastIndex(candidate => {
return candidate.type === 'test' && context.state.tests[candidate.testId].initiatorTestId === initiatorTestId;
});
const parentTestChildrenIndex = context.state.children.findLastIndex(candidate => {
return candidate.type === 'test' && context.state.tests[candidate.testId].testId === initiatorTestId;
});
const firstCoverageChildrenIndex = context.state.children.findIndex(candidate => {
return candidate.type === 'coverage';
});
const childrenIndex = lastSiblingChildrenIndex === -1
? parentTestChildrenIndex === -1
? firstCoverageChildrenIndex === -1
? context.state.children.length
: firstCoverageChildrenIndex
: parentTestChildrenIndex + 1
: lastSiblingChildrenIndex + 1;
const testStartStepId = context.uuid();
const testPlanStepId = context.uuid();
const testEndStepId = context.uuid();
context.state.stepIds.splice(index, 0, testStartStepId, testPlanStepId, testEndStepId);
context.state.steps[testStartStepId] = { stepId: testStartStepId, type: 'test-start', testId, status: 'waiting' };
context.state.steps[testPlanStepId] = { stepId: testPlanStepId, type: 'test-plan', testId, status: 'waiting' };
context.state.steps[testEndStepId] = { stepId: testEndStepId, type: 'test-end', testId, status: 'waiting' };
context.state.tests[testId] = { ...data, children: [] };
context.state.children.splice(childrenIndex, 0, { type: 'test', testId });
}
}
static registerDescribeStart(context, data) {
if (!context.state.ended) {
// New "describe-start" (to mark the start of a subtest). Queue it up.
const stepId = context.uuid();
const describeId = data.describeId;
const index = context.state.stepIds.findLastIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'test-plan' && candidate.testId === data.parents[0].testId) {
return true;
}
});
context.state.stepIds.splice(index, 0, stepId);
context.state.steps[stepId] = { stepId, type: 'describe-start', describeId: data.describeId, status: 'waiting' };
context.state.describes[describeId] = { ...data, children: [] };
if (data.parents.at(-1)?.type === 'describe') {
context.state.describes[data.parents.at(-1).describeId].children.push({ type: 'describe', describeId });
} else {
context.state.tests[data.parents.at(-1).testId].children.push({ type: 'describe', describeId });
}
container.append(...items);
}
}
}
class Tap {
static version() {
return 'TAP Version 13';
static registerDescribeEnd(context, data) {
if (!context.state.ended) {
// Completed "describe-end" (to mark the end of a subtest). Queue it up.
const planStepId = context.uuid();
const endStepId = context.uuid();
const describe = context.state.describes[data.describeId]; // eslint-disable-line no-shadow
const index = context.state.stepIds.findLastIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'test-plan' && candidate.testId === describe.parents[0].testId) {
return true;
}
});
context.state.stepIds.splice(index, 0, planStepId, endStepId);
context.state.steps[planStepId] = { stepId: planStepId, type: 'describe-plan', describeId: data.describeId, status: 'waiting' };
context.state.steps[endStepId] = { stepId: endStepId, type: 'describe-end', describeId: data.describeId, status: 'waiting' };
}
}
static diagnostic(message) {
return `# ${message.replace(/\n/g, `\n# `)}`;
static registerIt(context, data) {
if (!context.state.ended) {
// New "it" (to be run as part of a test suite). Queue it up.
const stepId = context.uuid();
const itId = data.itId;
const index = context.state.stepIds.findLastIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'test-plan' && candidate.testId === data.parents[0].testId) {
return true;
}
});
context.state.stepIds.splice(index, 0, stepId);
context.state.steps[stepId] = { stepId, type: 'it', itId: data.itId, status: 'waiting' };
context.state.its[itId] = data;
if (data.parents.at(-1)?.type === 'describe') {
context.state.describes[data.parents.at(-1).describeId].children.push({ type: 'it', itId });
} else {
context.state.tests[data.parents.at(-1).testId].children.push({ type: 'it', itId });
}
}
}
static testLine(ok, number, description, directive) {
description = description.replace(/\n/g, ' ');
const okText = ok ? 'ok' : 'not ok';
const directiveText = directive ? ` # ${directive}` : '';
return `${okText} - ${number} ${description}${directiveText}`;
static registerCoverage(context, data) {
if (!context.state.ended) {
// New "coverage" goal. Queue it up.
const stepId = context.uuid();
const coverageId = data.coverageId;
const index = context.state.stepIds.findLastIndex(candidateId => {
const candidate = context.state.steps[candidateId];
if (candidate.type === 'exit') {
return true;
}
});
context.state.stepIds.splice(index, 0, stepId);
context.state.steps[stepId] = { stepId, type: 'coverage', coverageId: coverageId, status: 'waiting' };
context.state.coverages[coverageId] = data;
const childrenIndex = context.state.children.length;
context.state.children.splice(childrenIndex, 0, { type: 'coverage', coverageId });
}
}
static yaml(message, severity, data) {
let text = ' ---';
text += `\n message: ${message.replace(/\n/g, ' ')}`;
text += `\n severity: ${severity}`;
if (data && data.stack) {
text += `\n stack: |-`;
text += `\n ${data.stack.replace(/\n/g, `\n `)}`;
static onRegister(context, event) {
if (!context.state.ended) {
const data = event.data.data;
switch(data.type) {
case 'test':
XTestRoot.registerTest(context, data);
break;
case 'describe-start':
XTestRoot.registerDescribeStart(context, data);
break;
case 'describe-end':
XTestRoot.registerDescribeEnd(context, data);
break;
case 'it':
XTestRoot.registerIt(context, data);
break;
case 'coverage':
XTestRoot.registerCoverage(context, data);
break;
default:
throw new Error(`Unexpected registration type "${data.type}".`);
}
}
text += `\n ...`;
return text;
}
static bailOut(message) {
return `Bail out! ${message.replace(/\n/g, ' ')}`;
static onReady(context, event) {
if (!context.state.ended) {
const data = event.data.data;
const only = (
Object.values(context.state.its).some(candidate => {
return candidate.only && candidate.parents[0].testId === data.testId;
}) ||
Object.values(context.state.describes).some(candidate => {
return candidate.only && candidate.parents[0].testId === data.testId;
})
);
if (only) {
for (const it of Object.values(context.state.its)) { // eslint-disable-line no-shadow
if (it.parents[0].testId === data.testId) {
if (!it.only) {
const describeParents = it.parents
.filter(candidate => candidate.type === 'describe')
.map(parent => context.state.describes[parent.describeId]);
const hasOnlyDescribeParent = describeParents.some(candidate => candidate.only);
if (!hasOnlyDescribeParent) {
it.directive = 'SKIP';
} else if (!it.directive) {
const lastDescribeParentWithDirective = describeParents.findLast(candidate => !!candidate.directive);
if (lastDescribeParentWithDirective) {
it.directive = lastDescribeParentWithDirective.directive;
}
}
}
}
}
} else {
for (const it of Object.values(context.state.its)) { // eslint-disable-line no-shadow
if (it.parents[0].testId === data.testId) {
if (!it.directive) {
const describeParents = it.parents
.filter(candidate => candidate.type === 'describe')
.map(parent => context.state.describes[parent.describeId]);
const lastDescribeParentWithDirective = describeParents.findLast(candidate => !!candidate.directive);
if (lastDescribeParentWithDirective) {
it.directive = lastDescribeParentWithDirective.directive;
}
}
}
}
}
const stepId = context.state.stepIds.find(candidateId => {
const candidate = context.state.steps[candidateId];
return candidate.type === 'test-start' && candidate.testId === data.testId;
});
const step = context.state.steps[stepId];
if (step.status !== 'running') {
throw new Error('test to ready is not running');
}
const href = XTestRoot.href(context, stepId);
const level = XTestRoot.level(context, stepId);
const tap = XTestTap.subtest(href, level);
XTestRoot.output(context, stepId, tap);
step.status = 'done';
}
}
static plan(number) {
return `1..${number}`;
static onResult(context, event) {
if (!context.state.ended) {
const data = event.data.data;
const it = context.state.its[data.itId]; // eslint-disable-line no-shadow
const stepId = context.state.stepIds.find(candidateId => {
const candidate = context.state.steps[candidateId];
return candidate.type === 'it' && candidate.itId === it.itId;
});
const step = context.state.steps[stepId];
if (step.status !== 'running') {
throw new Error('step to complete is not running');
}
Object.assign(it, { ok: data.ok, error: data.error });
step.status = 'done';
const ok = XTestRoot.ok(context, stepId);
const number = XTestRoot.number(context, stepId);
const text = XTestRoot.text(context, stepId);
const directive = XTestRoot.directive(context, stepId);
const level = XTestRoot.level(context, stepId);
const tap = XTestTap.testLine(ok, number, text, directive, level);
if (!data.error) {
XTestRoot.output(context, stepId, tap);
} else {
const yaml = XTestRoot.yaml(context, stepId);
const errorTap = XTestTap.yaml(yaml.message, yaml.severity, yaml.data, level);
XTestRoot.output(context, stepId, tap, errorTap);
}
}
}
}
class Test {
constructor(testId) {
this.constructor.setup(this, testId);
this.constructor.initialize(this);
static onCoverageResult(context, event) {
if (!context.state.ended) {
context.state.resolveCoverageValuePromise(event.data.data);
}
}
static setup(target, testId) {
target.testId = testId;
target.href = location.href;
target.bailed = false;
target.promises = [];
target.currentItId = null;
target.doneItIds = [];
static kickoffVersion(context, stepId) {
const tap = XTestTap.version();
XTestRoot.output(context, stepId, tap);
context.state.steps[stepId].status = 'done';
}
static initialize(target) {
this.listen(this.onMessage.bind(this, target));
// Wait at least a microtask before ending.
this.waitFor(target, Promise.resolve());
static kickoffDescribeStart(context, stepId) {
const level = XTestRoot.level(context, stepId);
const text = XTestRoot.text(context, stepId);
const tap = XTestTap.subtest(text, level);
XTestRoot.output(context, stepId, tap);
context.state.steps[stepId].status = 'done';
}
static onMessage(target, evt) {
if (evt.data.type === 'x-test-bail') {
target.bailed = true;
}
static kickoffDescribePlan(context, stepId) {
const level = XTestRoot.level(context, stepId);
const count = XTestRoot.count(context, stepId);
const tap = XTestTap.plan(count, level);
XTestRoot.output(context, stepId, tap);
context.state.steps[stepId].status = 'done';
}
static promiseIt(target, itId) {
return new Promise(resolve => {
const onMessage = evt => {
const { type, data } = evt.data;
if (type === 'x-test-it-ended' && data.itId === itId) {
this.unlisten(onMessage);
resolve();
}
};
this.listen(onMessage);
static kickoffDescribeEnd(context, stepId) {
const number = XTestRoot.number(context, stepId);
const ok = XTestRoot.ok(context, stepId);
const text = XTestRoot.text(context, stepId);
const directive = XTestRoot.directive(context, stepId);
const level = XTestRoot.level(context, stepId);
const tap = XTestTap.testLine(ok, number, text, directive, level);
XTestRoot.output(context, stepId, tap);
context.state.steps[stepId].status = 'done';
}
static kickoffTestStart(context, stepId) {
// Create the new test.
const step = context.state.steps[stepId];
const href = XTestRoot.href(context, stepId);
const iframe = document.createElement('iframe');
iframe.addEventListener('error', () => {
const error = new Error(`Failed to load ${href}`);
XTestRoot.bail(context, error);
});
Object.assign(iframe, { id: step.testId, src: href });
Object.assign(iframe.style, {
border: 'none', backgroundColor: 'white', height: '100vh',
width: '100vw', position: 'fixed', zIndex: '0', top: '0', left: '0',
});
document.body.append(iframe);
step.status = 'running';
}
static assert(assertion, message = 'assertion failed') {
if (!assertion) {
throw new Error(message);
}
static kickoffTestPlan(context, stepId) {
const count = XTestRoot.count(context, stepId);
const level = XTestRoot.level(context, stepId);
const tap = XTestTap.plan(count, level);
XTestRoot.output(context, stepId, tap);
context.state.steps[stepId].status = 'done';
}
static async waitFor(target, promise) {
if (!target.bailed) {
const waitForId = this.uuidv4();
target.waitForId = waitForId;
target.promises.push(promise);
static kickoffTestEnd(context, stepId) {
// Destroy test.
const lastIframe = document.querySelector('iframe');
lastIframe?.remove();
const number = XTestRoot.number(context, stepId);
const ok = XTestRoot.ok(context, stepId);
const text = XTestRoot.text(context, stepId);
const directive = XTestRoot.directive(context, stepId);
const level = XTestRoot.level(context, stepId);
const tap = XTestTap.testLine(ok, number, text, directive, level);
XTestRoot.output(context, stepId, tap);
context.state.steps[stepId].status = 'done';
}
static kickoffIt(context, stepId) {
const step = context.state.steps[stepId];
const { itId, directive, interval } = context.state.its[step.itId];
context.publish('x-test-root-run', { itId, directive, interval });
step.status = 'running';
}
static kickoffCoverage(context, stepId) {
const step = context.state.steps[stepId];
const coverage = context.state.coverages[step.coverageId]; // eslint-disable-line no-shadow
if (context.state.coverageValue) {
try {
await Promise.all(target.promises);
if (target.waitForId === waitForId) {
this.post('x-test-next', { testId: target.testId });
}
} catch (err) {
this.bail(target, err);
const analysis = XTestRoot.analyzeHrefCoverage(context.state.coverageValue.js, coverage.href, coverage.goal);
Object.assign(coverage, { ok: analysis.ok, percent: analysis.percent, output: analysis.output });
} catch (error) {
Object.assign(coverage, { ok: false, percent: 0, output: '' });
XTestRoot.bail(context, error);
}
} else {
Object.assign(coverage, { ok: true, percent: 0, output: '', directive: 'SKIP' });
}
const ok = XTestRoot.ok(context, stepId);
const number = XTestRoot.number(context, stepId);
const text = XTestRoot.text(context, stepId);
const directive = XTestRoot.directive(context, stepId);
const level = XTestRoot.level(context, stepId);
const tap = XTestTap.testLine(ok, number, text, directive, level);
if (!ok) {
const errorTap = XTestTap.diagnostic(coverage.output, level);
XTestRoot.output(context, stepId, tap, errorTap);
} else {
XTestRoot.output(context, stepId, tap);
}
step.status = 'done';
}
static test(target, href) {
if (!target.bailed) {
const testId = this.uuidv4();
href = new URL(href, target.href).href;
const data = { testId, parentTestId: target.testId, href };
this.post('x-test-queue', data);
}
static kickoffExit(context, stepId) {
const count = XTestRoot.count(context, stepId);
const tap = XTestTap.plan(count);
XTestRoot.output(context, stepId, tap);
context.state.steps[stepId].status = 'done';
XTestRoot.end(context);
}
static timeout(interval = 30000) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`timeout after ${interval}ms`));
}, interval);
});
static requestCoverageValue(context) {
context.state.waiting = true;
Promise.race([context.state.coverageValuePromise, context.timeout(5000)])
.then(() => { XTestRoot.check(context); })
.catch(error => { XTestRoot.bail(context, error); });
context.publish('x-test-root-coverage-request');
}
static cover(target, url, goal) {
this.post('x-test-cover', { url, goal });
static check(context) {
if (!context.state.ended) {
// Look to see if any tests are running.
const runningStepId = context.state.stepIds.find(candidateId => {
return context.state.steps[candidateId].status === 'running';
});
if (!runningStepId) {
// If nothing's running, find the first step that's waiting and run that.
const stepId = context.state.stepIds.find(candidateId => {
return context.state.steps[candidateId].status === 'waiting';
});
if (stepId) {
const waitingStep = context.state.steps[stepId];
switch (waitingStep.type) {
case 'version':
XTestRoot.kickoffVersion(context, stepId);
XTestRoot.check(context);
break;
case 'describe-start':
XTestRoot.kickoffDescribeStart(context, stepId);
XTestRoot.check(context);
break;
case 'describe-plan':
XTestRoot.kickoffDescribePlan(context, stepId);
XTestRoot.check(context);
break;
case 'describe-end':
XTestRoot.kickoffDescribeEnd(context, stepId);
XTestRoot.check(context);
break;
case 'test-start':
XTestRoot.kickoffTestStart(context, stepId);
XTestRoot.check(context);
break;
case 'test-plan':
XTestRoot.kickoffTestPlan(context, stepId);
XTestRoot.check(context);
break;
case 'test-end':
XTestRoot.kickoffTestEnd(context, stepId);
XTestRoot.check(context);
break;
case 'it':
XTestRoot.kickoffIt(context, stepId);
XTestRoot.check(context);
break;
case 'coverage':
if (!context.state.coverage || context.state.coverageValue) {
XTestRoot.kickoffCoverage(context, stepId);
XTestRoot.check(context);
} else if (!context.state.waiting) {
XTestRoot.requestCoverageValue(context);
}
break;
case 'exit':
XTestRoot.kickoffExit(context, stepId);
break;
default:
throw new Error(`Unexpected step type "${waitingStep.type}".`);
}
}
}
}
}
static async it(target, directive, description, callback, interval) {
// TODO: crude way to protect against accidental directives in description.
description.replace(/#/g, '*');
if (!(callback instanceof Function)) {
throw new Error(`Callback must be a function (got ${callback}).`);
}
if (!target.bailed) {
const itId = this.uuidv4();
const lastItId = target.currentItId;
target.currentItId = itId;
this.waitFor(target, this.promiseIt(target, itId));
if (lastItId && !target.doneItIds.includes(lastItId)) {
await this.promiseIt(target, lastItId);
static bail(context, error, options) {
if (!context.state.ended) {
if (error && error.stack) {
XTestRoot.log(context, XTestTap.diagnostic(error.stack));
}
this.post('x-test-it-started', { itId, parentTestId: target.testId });
const data = { itId, description, directive, ok: true };
try {
if (directive !== 'SKIP') {
await Promise.race([callback(), this.timeout(interval)]);
}
} catch (error) {
Object.assign(data, { ok: false, error: this.createError(error) });
} finally {
this.post('x-test-it-ended', data);
target.doneItIds.push(itId);
if (options?.testId) {
const test = context.state.tests[options.testId]; // eslint-disable-line no-shadow
test.error = error;
const href = test.href;
XTestRoot.log(context, XTestTap.bailOut(href));
} else {
XTestRoot.log(context, XTestTap.bailOut());
}
XTestRoot.end(context);
}
}
static async bail(target, err) {
if (!target.bailed) {
const error = this.createError(err);
this.post('x-test-bail', { href: target.href, error });
static log(context, ...tap) {
for (const line of tap) {
console.log(line); // eslint-disable-line no-console
}
context.state.reporter?.tap(...tap);
}
static listen(callback) {
top.addEventListener('message', callback);
static output(context, stepId, ...stepTap) {
const lastIndex = context.state.stepIds.findIndex(candidateId => {
const candidate = context.state.steps[candidateId];
return !candidate.tap;
});
context.state.steps[stepId].tap = stepTap;
const index = context.state.stepIds.findIndex(candidateId => {
const candidate = context.state.steps[candidateId];
return !candidate.tap;
});
if (lastIndex !== index) {
let tap;
if (index === -1) {
// We're done!
tap = context.state.stepIds.slice(lastIndex).map(targetId => context.state.steps[targetId].tap);
} else {
tap = context.state.stepIds.slice(lastIndex, index).map(targetId => context.state.steps[targetId].tap);
}
XTestRoot.log(context, ...tap.flat());
}
}
static unlisten(callback) {
top.removeEventListener('message', callback);
static childOk(context, child, options) {
switch (child.type) {
case 'test':
return context.state.tests[child.testId].children.every(candidate => XTestRoot.childOk(context, candidate, options));
case 'describe':
return context.state.describes[child.describeId].children.every(candidate => XTestRoot.childOk(context, candidate, options));
case 'it':
return context.state.its[child.itId].ok || options?.todoOk && context.state.its[child.itId].directive === 'TODO';
case 'coverage':
return context.state.coverages[child.coverageId].ok;
default:
throw new Error(`Unexpected type "${child.type}".`);
}
}
static post(type, data) {
// TODO: do we need to filter out untrusted origins here?
top.postMessage({ type, data }, '*');
static ok(context, stepId) {
const step = context.state.steps[stepId];
switch (step.type) {
case 'test-end':
return XTestRoot.childOk(context, { type: 'test', testId: step.testId }, { todoOk: true });
case 'describe-end':
return XTestRoot.childOk(context, { type: 'describe', describeId: step.describeId }, { todoOk: true });
case 'it':
return XTestRoot.childOk(context, { type: 'it', itId: step.itId });
case 'coverage':
return XTestRoot.childOk(context, { type: 'coverage', coverageId: step.coverageId });
default:
throw new Error(`Unexpected type "${step.type}".`);
}
}
static yamlIt(ok, directive, error) {
const yaml = { message: 'ok', severity: 'comment', data: {} };
if (ok) {
if (directive === 'SKIP') {
yaml.message = 'skip';
} else if (directive === 'TODO') {
yaml.message = 'todo';
static number(context, stepId) {
const step = context.state.steps[stepId];
switch (step.type) {
case 'it': {
const it = context.state.its[step.itId]; // eslint-disable-line no-shadow
const parentChildren = it.parents.at(-1)?.type === 'describe'
? context.state.describes[it.parents.at(-1).describeId].children
: context.state.tests[it.parents.at(-1).testId].children;
const index = parentChildren.findIndex(candidate => candidate.itId === it.itId);
return index + 1;
}
} else {
if (directive === 'TODO') {
yaml.message = error && error.message ? error.message : 'todo';
yaml.severity = 'todo';
} else {
yaml.message = error && error.message ? error.message : 'fail';
yaml.severity = 'fail';
case 'describe-end': {
const describe = context.state.describes[step.describeId]; // eslint-disable-line no-shadow
const parentChildren = describe.parents.at(-1)?.type === 'describe'
? context.state.describes[describe.parents.at(-1).describeId].children
: context.state.tests[describe.parents.at(-1).testId].children;
const index = parentChildren.findIndex(candidate => candidate.describeId === describe.describeId);
return index + 1;
}
if (error && error.stack) {
yaml.data.stack = error.stack;
case 'test-end': {
const test = context.state.tests[step.testId]; // eslint-disable-line no-shadow
const index = context.state.children.findIndex(candidate => candidate.testId === test.testId);
return index + 1;
}
case 'coverage': {
const coverage = context.state.coverages[step.coverageId]; // eslint-disable-line no-shadow
const index = context.state.children.findIndex(candidate => candidate.coverageId === coverage.coverageId);
return index + 1;
}
default:
throw new Error(`Unexpected type "${step.type}".`);
}
return yaml;
}
static createError(err) {
const error = {};
if (err instanceof Error) {
error.message = err.message;
error.stack = err.stack;
} else {
error.message = String(err);
static text(context, stepId) {
// The regex-replace prevents usage of the special `#` character which is
// meaningful in TAP. It's overly-conservative now — it could be less
// restrictive in the future.
const step = context.state.steps[stepId];
switch (step.type) {
case 'test-end':
return context.state.tests[step.testId].href;
case 'describe-start':
case 'describe-end':
return context.state.describes[step.describeId].text.replace(/#/g, '*');
case 'it':
return context.state.its[step.itId].text.replace(/#/g, '*');
case 'coverage': {
const coverage = context.state.coverages[step.coverageId]; // eslint-disable-line no-shadow
return `${coverage.goal}% coverage goal for ${coverage.href} (got ${coverage.percent.toFixed(2)}%)`;
}
default:
throw new Error(`Unexpected type "${step.type}".`);
}
return error;
}
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
static uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
}
}
class RootTest extends Test {
static setup(target, testId) {
super.setup(target, testId);
target.testIds = [testId];
target.tests = { [testId]: { testId, href: target.href } };
target.index = 0;
target.itIds = [];
target.its = {};
target.coverageGoals = [];
target.coveragePromise = new Promise(resolve => target.resolveCoveragePromise = resolve);
target.status = 'started';
target.outputs = [];
const url = new URL(location);
target.reporter =
url.searchParams.get('x-test-no-reporter') !== ''
? document.createElement('x-test-reporter')
: null;
target.cover = url.searchParams.get('x-test-cover') === '';
}
static output(target, string) {
target.outputs = [...target.outputs, string];
console.log(string); // eslint-disable-line no-console
if (target.reporter) {
target.reporter.outputs = target.outputs;
static href(context, stepId) {
const step = context.state.steps[stepId];
switch (step.type) {
case 'test-start':
case 'test-end':
return context.state.tests[step.testId].href;
default:
throw new Error(`Unexpected type "${step.type}".`);
}
}
static initialize(target) {
super.initialize(target);
if (target.reporter) {
customElements.define('x-test-reporter', XTestReporter);
document.body.appendChild(target.reporter);
static directive(context, stepId) {
const step = context.state.steps[stepId];
switch (step.type) {
case 'describe-end':
case 'test-end':
return null;
case 'it':
return context.state.its[step.itId].directive;
case 'coverage':
return context.state.coverages[step.coverageId].directive;
default:
throw new Error(`Unexpected type "${step.type}".`);
}
this.output(target, Tap.version());
}
static onMessage(target, evt) {
super.onMessage(target, evt);
const { type, data } = evt.data;
switch (type) {
case 'x-test-ping':
this.post('x-test-pong', { status: target.status });
break;
case 'x-test-bail': {
// TODO: TAP Version 13 spec does not provide a yaml block for "bail".
if (data.error && data.error.stack) {
this.output(target, Tap.diagnostic(data.error.stack));
}
this.output(target, Tap.bailOut(data.href));
this.end(target);
break;
}
case 'x-test-queue':
this.onQueue(target, data);
break;
case 'x-test-next':
this.next(target);
break;
case 'x-test-it-started':
this.onItStarted(target, data);
break;
case 'x-test-it-ended':
this.onItEnded(target, data);
break;
case 'x-test-cover':
target.coverageGoals.push(data);
break;
case 'x-test-cover-start':
target.resolveCoveragePromise(data);
break;
static level(context, stepId) {
const step = context.state.steps[stepId];
switch (step.type) {
case 'test-plan':
return 1;
case 'test-start':
case 'test-end':
case 'coverage':
return 0;
case 'describe-plan':
return context.state.describes[step.describeId].parents.length + 1;
case 'describe-start':
case 'describe-end':
return context.state.describes[step.describeId].parents.length;
case 'it':
return context.state.its[step.itId].parents.length;
default:
throw new Error(`Unexpected type "${step.type}".`);
}
}
static onItStarted(target, data) {
if (!target.bailed && target.status !== 'ended') {
target.itIds.push(data.itId);
target.its[data.itId] = data;
static count(context, stepId) {
const step = context.state.steps[stepId];
switch (step.type) {
case 'test-plan':
return context.state.tests[step.testId].children.length;
case 'describe-plan':
return context.state.describes[step.describeId].children.length;
case 'exit':
return context.state.children.length;
default:
throw new Error(`Unexpected type "${step.type}".`);
}
}
static onItEnded(target, data) {
if (!target.bailed && target.status !== 'ended') {
Object.assign(target.its[data.itId], data);
const { itId, ok, description, directive, error } = data;
const number = target.itIds.indexOf(itId) + 1;
this.output(target, Tap.testLine(ok, number, description, directive));
const yaml = this.yamlIt(ok, directive, error);
// We may choose to output these later...
if (yaml.severity !== 'comment') {
this.output(target, Tap.yaml(yaml.message, yaml.severity, yaml.data));
static yaml(context, stepId) {
const step = context.state.steps[stepId];
switch (step.type) {
case 'it': {
const it = context.state.its[step.itId]; // eslint-disable-line no-shadow
const { ok, directive, error } = it;
const yaml = { message: 'ok', severity: 'comment', data: {} };
if (ok) {
if (directive === 'SKIP') {
yaml.message = 'skip';
} else if (directive === 'TODO') {
yaml.message = 'todo';
}
} else {
if (directive === 'TODO') {
yaml.message = error && error.message ? error.message : 'todo';
yaml.severity = 'todo';
} else {
yaml.message = error && error.message ? error.message : 'fail';
yaml.severity = 'fail';
}
if (error && error.stack) {
yaml.data.stack = error.stack;
}
}
return yaml;
}
default:
throw new Error(`Unexpected type "${step.type}".`);
}
}
static onQueue(target, data) {
if (!target.bailed && target.status !== 'ended') {
const parentTestId = data.parentTestId;
const siblingTestIds = target.testIds.filter(
testId => target.tests[testId].parentTestId === parentTestId
);
const lastSiblingTestId = siblingTestIds[siblingTestIds.length - 1];
const insertIndex = lastSiblingTestId
? target.testIds.indexOf(lastSiblingTestId) + 1
: target.testIds.indexOf(parentTestId) + 1;
target.testIds.splice(insertIndex, 0, data.testId);
target.tests[data.testId] = data;
}
static end(context) {
context.state.ended = true;
context.state.waiting = false;
context.publish('x-test-root-end');
}
static analyzeUrlCoverage(coverage, url, goal) {
static analyzeHrefCoverage(coverageValue, href, goal) {
const set = new Set();
let text = '';
for (const item of coverage) {
if (item.url === url) {
for (const item of coverageValue ?? []) {
if (item.url === href) {
text = item.text;

@@ -608,30 +1195,103 @@ for (const range of item.ranges) {

}
}
static next(target) {
if (!target.bailed && target.status !== 'ended') {
// Destroy current test.
const currentTestId = target.testIds[target.index];
if (currentTestId && document.getElementById(currentTestId)) {
document.body.removeChild(document.getElementById(currentTestId));
class XTestTap {
static level(level) {
level = level ?? 0;
return ' '.repeat(level);
}
static version() {
return 'TAP Version 14';
}
static diagnostic(message, level) {
return `${XTestTap.level(level)}# ${message.replace(/\n/g, `\n${XTestTap.level(level)}# `)}`;
}
static testLine(ok, number, description, directive, level) {
description = description.replace(/\n/g, ' ');
const okText = ok ? 'ok' : 'not ok';
const directiveText = directive ? ` # ${directive}` : '';
return `${XTestTap.level(level)}${okText} ${number} - ${description}${directiveText}`;
}
static subtest(name, level) {
const text = `${XTestTap.level(level)}# Subtest: ${name}`;
return text;
}
static yaml(message, severity, data, level) {
let text = `${XTestTap.level(level)} ---`;
text += `\n${XTestTap.level(level)} message: ${message.replace(/\n/g, ' ')}`;
text += `\n${XTestTap.level(level)} severity: ${severity}`;
if (data && data.stack) {
text += `\n${XTestTap.level(level)} stack: |-`;
text += `\n${XTestTap.level(level)} ${data.stack.replace(/\n/g, `\n${XTestTap.level(level)} `)}`;
}
text += `\n${XTestTap.level(level)} ...`;
return text;
}
static bailOut(message) {
return message ? `Bail out! ${message.replace(/\n/g, ` `)}` : 'Bail out!';
}
static plan(number, level) {
return `${XTestTap.level(level)}1..${number}`;
}
}
class XTestSuite {
static initialize(context, testId, href) {
Object.assign(context.state, { testId, href });
context.state.parents.push({ type: 'test', testId });
context.subscribe(async event => {
switch (event.data.type) {
case 'x-test-suite-bail':
XTestSuite.onBail(context, event);
break;
case 'x-test-root-run':
XTestSuite.onRun(context, event);
break;
default:
// Ignore — this message isn't for us.
}
});
const testId = target.testIds[++target.index];
if (testId) {
// Create the new test.
const { href } = target.tests[testId];
this.output(target, Tap.diagnostic(`${href}`));
const el = document.createElement('iframe');
Object.assign(el, { id: testId, src: href });
Object.assign(el.style, {
border: 'none', backgroundColor: 'white', height: '100vh',
width: '100vw', position: 'fixed', zIndex: '0', top: '0', left: '0',
});
el.addEventListener('error', () => {
const error = new Error(`${target.href} failed to load ${href}`);
this.bail(target, error);
});
document.body.appendChild(el);
Object.assign(target.tests[testId], { status: 'started' });
} else {
this.end(target);
// Setup global error / rejection handlers.
context.addErrorListener(event => {
event.preventDefault();
XTestSuite.bail(context, event.error);
});
context.addUnhandledrejectionListener(event => {
event.preventDefault();
XTestSuite.bail(context, event.reason);
});
// Await a single microtask before we signal that we're ready.
XTestSuite.waitFor(context, Promise.resolve());
}
static onBail(context/*, event*/) {
if (!context.state.bailed) {
context.state.bailed = true;
}
}
static async onRun(context, event) {
if (
!context.state.bailed &&
context.state.callbacks[event.data.data.itId]
) {
const { itId, directive, interval } = event.data.data;
try {
if (directive !== 'SKIP') {
const callback = context.state.callbacks[itId];
await Promise.race([callback(), context.timeout(interval)]);
}
context.publish('x-test-suite-result', { itId, ok: true, error: null });
} catch (error) {
error = XTestSuite.createError(error); // eslint-disable-line no-ex-assign
context.publish('x-test-suite-result', { itId, ok: false, error });
}

@@ -641,34 +1301,140 @@ }

static end(target) {
target.status = 'ended';
if (!target.cover) {
for (const { url, goal } of target.coverageGoals) {
this.output(target, Tap.diagnostic(`Not checking "${url}" for ${goal}% coverage (no "x-test-cover" query param).`));
}
this.output(target, Tap.plan(target.itIds.length));
static bail(context, error) {
if (!context.state.bailed) {
context.state.bailed = true;
context.publish(
'x-test-suite-bail',
{ testId: context.state.testId, error: XTestSuite.createError(error) }
);
}
}
static createError(originalError) {
const error = {};
if (originalError instanceof Error) {
Object.assign(error, { message: originalError.message, stack: originalError.stack });
} else {
this.startCoverageReport(target);
error.message = String(originalError);
}
this.post('x-test-ended');
return error;
}
static async startCoverageReport(target) {
const coverageTimeout = new Promise((resolve, reject) =>
setTimeout(() => reject(new Error(`Timed out awaiting coverage from puppeteer.`)), 5000)
);
try {
const { js } = await Promise.race([target.coveragePromise, coverageTimeout]);
for (const { url, goal } of target.coverageGoals) {
const analysis = this.analyzeUrlCoverage(js, url, goal);
if (analysis.ok) {
this.output(target, Tap.diagnostic(`Coverage goal of ${goal}% for ${url} met (got ${analysis.percent.toFixed(2)}%).`));
} else {
this.output(target, Tap.diagnostic(analysis.output));
this.output(target, Tap.bailOut(`Coverage goal of ${goal}% for ${url} not met (got ${analysis.percent.toFixed(2)}%).`));
static assert(context, ok, text) {
if (context && !context.state.bailed) {
if (!ok) {
throw new Error(text ?? 'not ok');
}
}
}
static coverage(context, href, goal) {
if (context && !context.state.bailed) {
if (!(goal >= 0 && goal <= 100)) {
throw new Error(`Unexpected goal percentage "${goal}".`);
}
const coverageId = context.uuid();
const url = new URL(href, context.state.href);
context.publish('x-test-suite-register', { type: 'coverage', coverageId, href: url.href, goal });
}
}
static test(context, href) {
if (context && !context.state.bailed && !context.state.ready) {
const testId = context.uuid();
const testHref = new URL(href, context.state.href).href;
const initiatorTestId = context.state.testId;
context.publish('x-test-suite-register', { type: 'test', testId, initiatorTestId, href: testHref });
}
}
static #describerInner(context, text, callback, directive, only) {
if (context && !context.state.bailed && !context.state.ready) {
if (!(callback instanceof Function)) {
throw new Error(`Unexpected callback value "${callback}".`);
}
const describeId = context.uuid();
const parents = [...context.state.parents];
directive = directive ?? null;
only = only ?? false;
context.publish(
'x-test-suite-register',
{ type: 'describe-start', describeId, parents, text, directive, only }
);
try {
context.state.parents.push({ type: 'describe', describeId });
callback();
context.state.parents.pop();
context.publish('x-test-suite-register', { type: 'describe-end', describeId });
} catch (error) {
XTestSuite.bail(context, error);
}
}
}
static describe(context, text, callback) {
XTestSuite.#describerInner(context, text, callback);
}
static describeSkip(context, text, callback) {
XTestSuite.#describerInner(context, text, callback, 'SKIP');
}
static describeOnly(context, text, callback) {
XTestSuite.#describerInner(context, text, callback, null, true);
}
static describeTodo(context, text, callback) {
XTestSuite.#describerInner(context, text, callback, 'TODO');
}
static #itInner(context, text, callback, interval, directive, only) {
if (context && !context.state.bailed && !context.state.ready) {
if (!(callback instanceof Function)) {
throw new Error(`Unexpected callback value "${callback}".`);
}
const itId = context.uuid();
const parents = [...context.state.parents];
interval = interval ?? null;
directive = directive ?? null;
only = only ?? false;
context.state.callbacks[itId] = callback;
context.publish(
'x-test-suite-register',
{ type: 'it', itId, parents, text, interval, directive, only }
);
}
}
static it(context, text, callback, interval) {
XTestSuite.#itInner(context, text, callback, interval);
}
static itSkip(context, text, callback, interval) {
XTestSuite.#itInner(context, text, callback, interval, 'SKIP');
}
static itOnly(context, text, callback, interval) {
XTestSuite.#itInner(context, text, callback, interval, null, true);
}
static itTodo(context, text, callback, interval) {
XTestSuite.#itInner(context, text, callback, interval, 'TODO');
}
static async waitFor(context, promise) {
if (context && !context.state.bailed) {
if (!context.state.bailed) {
const waitForId = context.uuid();
context.state.waitForId = waitForId;
context.state.promises.push(promise);
try {
await Promise.all(context.state.promises);
if (context.state.waitForId === waitForId) {
context.state.ready = true;
context.publish('x-test-suite-ready', { testId: context.state.testId });
}
} catch (error) {
XTestSuite.bail(context, error);
}
}
this.output(target, Tap.plan(target.itIds.length));
this.post('x-test-cover-ended');
} catch (error) {
this.bail(target, error);
}

@@ -678,15 +1444,23 @@ }

// When we boot a new test, we check if we were instantiated or if we're root.
const _isRoot = frameElement === null || !frameElement.id;
const _testId = _isRoot ? Test.uuidv4() : frameElement.id;
const _test = _isRoot ? new RootTest(_testId) : new Test(_testId);
addEventListener('error', evt => {
evt.preventDefault();
Test.bail(_test, evt.error);
});
addEventListener('unhandledrejection', evt => {
evt.preventDefault();
Test.bail(_test, evt.reason);
});
// There is one-and-only-one root. Either boot as root or child test.
let suiteContext = null;
if (frameElement === null || !frameElement.id) {
const state = {
ended: false, waiting: false, children: [], stepIds: [], steps: {},
tests: {}, describes: {}, its: {}, coverage: false, coverages: {},
resolveCoverageValuePromise: null, coverageValuePromise: null,
coverageValue: null, reporter: null,
};
const rootContext = { state, uuid, publish, subscribe, timeout };
XTestRoot.initialize(rootContext, location.href);
} else {
const state = {
testId: null, href: null, callbacks: {}, bailed: false, waitForId: null,
ready: false, promises: [], parents: [],
};
suiteContext = {
state, uuid, publish, subscribe, timeout, addErrorListener,
addUnhandledrejectionListener,
};
XTestSuite.initialize(suiteContext, frameElement.id, location.href);
}

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc