@netflix/x-test
Advanced tools
Comparing version 1.0.0-rc.18 to 1.0.0-rc.19
@@ -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 |
16
test.js
@@ -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'); | ||
}); | ||
}); |
1762
x-test.js
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
118946
2573
75
36
1