@remix-run/test 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -50
- package/dist/app/client/entry.d.ts +2 -0
- package/dist/app/client/entry.d.ts.map +1 -0
- package/dist/app/client/entry.js +328 -0
- package/dist/app/client/iframe.d.ts +2 -0
- package/dist/app/client/iframe.d.ts.map +1 -0
- package/dist/app/client/iframe.js +22 -0
- package/dist/app/server.d.ts +6 -0
- package/dist/app/server.d.ts.map +1 -0
- package/dist/app/server.js +303 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +14 -0
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +319 -140
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/colors.d.ts +2 -0
- package/dist/lib/colors.d.ts.map +1 -0
- package/dist/lib/colors.js +2 -0
- package/dist/lib/config.d.ts +59 -14
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +181 -38
- package/dist/lib/context.d.ts +37 -13
- package/dist/lib/context.d.ts.map +1 -1
- package/dist/lib/context.js +19 -3
- package/dist/lib/coverage-loader.d.ts +16 -0
- package/dist/lib/coverage-loader.d.ts.map +1 -0
- package/dist/lib/coverage-loader.js +20 -0
- package/dist/lib/coverage.d.ts +28 -0
- package/dist/lib/coverage.d.ts.map +1 -0
- package/dist/lib/coverage.js +212 -0
- package/dist/lib/executor.d.ts +3 -26
- package/dist/lib/executor.d.ts.map +1 -1
- package/dist/lib/executor.js +11 -6
- package/dist/lib/fake-timers.d.ts +13 -0
- package/dist/lib/fake-timers.d.ts.map +1 -0
- package/dist/lib/fake-timers.js +64 -0
- package/dist/lib/import-module.d.ts +2 -0
- package/dist/lib/import-module.d.ts.map +1 -0
- package/dist/lib/import-module.js +38 -0
- package/dist/lib/normalize.d.ts +2 -0
- package/dist/lib/normalize.d.ts.map +1 -0
- package/dist/lib/{utils.js → normalize.js} +0 -9
- package/dist/lib/playwright.d.ts +1 -1
- package/dist/lib/playwright.d.ts.map +1 -1
- package/dist/lib/playwright.js +5 -8
- package/dist/lib/reporters/dot.d.ts +1 -2
- package/dist/lib/reporters/dot.d.ts.map +1 -1
- package/dist/lib/reporters/dot.js +12 -1
- package/dist/lib/reporters/files.d.ts +1 -2
- package/dist/lib/reporters/files.d.ts.map +1 -1
- package/dist/lib/reporters/files.js +12 -1
- package/dist/lib/reporters/index.d.ts +4 -5
- package/dist/lib/reporters/index.d.ts.map +1 -1
- package/dist/lib/reporters/index.js +3 -3
- package/dist/lib/reporters/results.d.ts +30 -0
- package/dist/lib/reporters/results.d.ts.map +1 -0
- package/dist/lib/reporters/results.js +1 -0
- package/dist/lib/reporters/spec.d.ts +1 -2
- package/dist/lib/reporters/spec.d.ts.map +1 -1
- package/dist/lib/reporters/spec.js +12 -1
- package/dist/lib/reporters/tap.d.ts +1 -2
- package/dist/lib/reporters/tap.d.ts.map +1 -1
- package/dist/lib/reporters/tap.js +11 -1
- package/dist/lib/runner-browser.d.ts +21 -0
- package/dist/lib/runner-browser.d.ts.map +1 -0
- package/dist/lib/runner-browser.js +123 -0
- package/dist/lib/runner.d.ts +24 -2
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +216 -38
- package/dist/lib/runtime.d.ts +2 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +2 -0
- package/dist/lib/ts-transform.d.ts +4 -0
- package/dist/lib/ts-transform.d.ts.map +1 -0
- package/dist/lib/ts-transform.js +29 -0
- package/dist/lib/worker-e2e-file.d.ts +11 -0
- package/dist/lib/worker-e2e-file.d.ts.map +1 -0
- package/dist/lib/worker-e2e-file.js +69 -0
- package/dist/lib/worker-e2e.js +11 -46
- package/dist/lib/worker-process.d.ts +2 -0
- package/dist/lib/worker-process.d.ts.map +1 -0
- package/dist/lib/worker-process.js +55 -0
- package/dist/lib/worker-results.d.ts +3 -0
- package/dist/lib/worker-results.d.ts.map +1 -0
- package/dist/lib/worker-results.js +20 -0
- package/dist/lib/worker-server.d.ts +10 -0
- package/dist/lib/worker-server.d.ts.map +1 -0
- package/dist/lib/worker-server.js +113 -0
- package/dist/lib/worker.js +7 -28
- package/dist/test/coverage/fixture.d.ts +5 -0
- package/dist/test/coverage/fixture.d.ts.map +1 -0
- package/dist/test/coverage/fixture.js +32 -0
- package/dist/test/coverage/test-browser.d.ts +2 -0
- package/dist/test/coverage/test-browser.d.ts.map +1 -0
- package/dist/test/coverage/test-browser.js +24 -0
- package/dist/test/coverage/test-e2e.d.ts +2 -0
- package/dist/test/coverage/test-e2e.d.ts.map +1 -0
- package/dist/test/coverage/test-e2e.js +60 -0
- package/dist/test/coverage/test-unit.d.ts +2 -0
- package/dist/test/coverage/test-unit.d.ts.map +1 -0
- package/dist/test/coverage/test-unit.js +27 -0
- package/dist/test/framework.test.browser.d.ts +2 -0
- package/dist/test/framework.test.browser.d.ts.map +1 -0
- package/dist/test/framework.test.browser.js +107 -0
- package/dist/test/framework.test.e2e.d.ts.map +1 -0
- package/dist/test/framework.test.e2e.js +34 -0
- package/package.json +30 -9
- package/src/app/client/entry.ts +357 -0
- package/src/app/client/iframe.ts +18 -0
- package/src/app/server.ts +336 -0
- package/src/cli-entry.ts +15 -0
- package/src/cli.ts +382 -145
- package/src/index.ts +2 -1
- package/src/lib/colors.ts +3 -0
- package/src/lib/config.ts +266 -54
- package/src/lib/context.ts +59 -17
- package/src/lib/coverage-loader.ts +31 -0
- package/src/lib/coverage.ts +320 -0
- package/src/lib/executor.ts +18 -35
- package/src/lib/fake-timers.ts +89 -0
- package/src/lib/import-module.ts +39 -0
- package/src/lib/{utils.ts → normalize.ts} +0 -18
- package/src/lib/playwright.ts +5 -7
- package/src/lib/reporters/dot.ts +12 -2
- package/src/lib/reporters/files.ts +12 -2
- package/src/lib/reporters/index.ts +4 -5
- package/src/lib/reporters/results.ts +29 -0
- package/src/lib/reporters/spec.ts +12 -2
- package/src/lib/reporters/tap.ts +11 -2
- package/src/lib/runner-browser.ts +171 -0
- package/src/lib/runner.ts +308 -53
- package/src/lib/runtime.ts +2 -0
- package/src/lib/ts-transform.ts +36 -0
- package/src/lib/worker-e2e-file.ts +98 -0
- package/src/lib/worker-e2e.ts +14 -49
- package/src/lib/worker-process.ts +69 -0
- package/src/lib/worker-results.ts +22 -0
- package/src/lib/worker-server.ts +123 -0
- package/src/lib/worker.ts +8 -28
- package/src/test/coverage/fixture.ts +34 -0
- package/src/test/coverage/test-browser.ts +29 -0
- package/src/test/coverage/test-e2e.ts +70 -0
- package/src/test/coverage/test-unit.ts +32 -0
- package/tsconfig.json +3 -1
- package/dist/lib/e2e-server.d.ts +0 -11
- package/dist/lib/e2e-server.d.ts.map +0 -1
- package/dist/lib/e2e-server.js +0 -15
- package/dist/lib/framework.test.d.ts +0 -2
- package/dist/lib/framework.test.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.js +0 -29
- package/dist/lib/framework.test.js +0 -283
- package/dist/lib/utils.d.ts +0 -16
- package/dist/lib/utils.d.ts.map +0 -1
- package/src/lib/e2e-server.ts +0 -28
- /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@remix-run/ui/jsx-runtime";
|
|
2
|
+
import decamelize from 'decamelize';
|
|
3
|
+
import * as assert from '@remix-run/assert';
|
|
4
|
+
import { describe, it } from '@remix-run/test';
|
|
5
|
+
import { on } from '@remix-run/ui';
|
|
6
|
+
import { render } from '@remix-run/ui/test';
|
|
7
|
+
describe('Counter', () => {
|
|
8
|
+
function Counter(handle) {
|
|
9
|
+
let count = handle.props.count ?? 0;
|
|
10
|
+
return () => (_jsxs("div", { children: [
|
|
11
|
+
_jsx("h3", { children: "Counter" }), _jsxs("div", { children: [
|
|
12
|
+
_jsx("button", { "data-action": "decrement", mix: [
|
|
13
|
+
on('click', () => {
|
|
14
|
+
count--;
|
|
15
|
+
handle.update();
|
|
16
|
+
}),
|
|
17
|
+
], children: "-" }), _jsx("span", { "data-testid": "count", style: { fontSize: '24px', minWidth: '2ch', textAlign: 'center' }, children: count }), _jsx("button", { "data-action": "increment", mix: [
|
|
18
|
+
on('click', () => {
|
|
19
|
+
count++;
|
|
20
|
+
handle.update();
|
|
21
|
+
}),
|
|
22
|
+
], children: "+" })
|
|
23
|
+
] })
|
|
24
|
+
] }));
|
|
25
|
+
}
|
|
26
|
+
it('renders with initial count of 0 when not specified', (t) => {
|
|
27
|
+
let { $, cleanup } = render(_jsx(Counter, {}));
|
|
28
|
+
t.after(cleanup);
|
|
29
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 0);
|
|
30
|
+
});
|
|
31
|
+
it('renders with a provided initial count', (t) => {
|
|
32
|
+
let { $, cleanup } = render(_jsx(Counter, { count: 5 }));
|
|
33
|
+
t.after(cleanup);
|
|
34
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 5);
|
|
35
|
+
});
|
|
36
|
+
it('increments the count', async (t) => {
|
|
37
|
+
let { $, act, cleanup } = render(_jsx(Counter, {}));
|
|
38
|
+
t.after(cleanup);
|
|
39
|
+
await act(() => $('[data-action="increment"]')?.click());
|
|
40
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 1);
|
|
41
|
+
await act(() => $('[data-action="increment"]')?.click());
|
|
42
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 2);
|
|
43
|
+
await act(() => $('[data-action="increment"]')?.click());
|
|
44
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 3);
|
|
45
|
+
});
|
|
46
|
+
it('decrements the count', async (t) => {
|
|
47
|
+
let { $, act, cleanup } = render(_jsx(Counter, { count: 3 }));
|
|
48
|
+
t.after(cleanup);
|
|
49
|
+
await act(() => $('[data-action="decrement"]')?.click());
|
|
50
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 2);
|
|
51
|
+
await act(() => $('[data-action="decrement"]')?.click());
|
|
52
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 1);
|
|
53
|
+
await act(() => $('[data-action="decrement"]')?.click());
|
|
54
|
+
assert.equal(Number($('[data-testid="count"]').textContent), 0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('FieldLabel (using decamelize)', () => {
|
|
58
|
+
// Demonstrates that ESM third-party libraries are importable from test modules
|
|
59
|
+
function FieldLabel(_handle) {
|
|
60
|
+
return (props) => (_jsx("span", { "data-testid": "label", children: decamelize(props.name, { separator: ' ' }) }));
|
|
61
|
+
}
|
|
62
|
+
it('renders a single word unchanged', (t) => {
|
|
63
|
+
let { $, cleanup } = render(_jsx(FieldLabel, { name: "name" }));
|
|
64
|
+
t.after(cleanup);
|
|
65
|
+
assert.equal($('[data-testid="label"]')?.textContent, 'name');
|
|
66
|
+
});
|
|
67
|
+
it('converts camelCase to spaced words', (t) => {
|
|
68
|
+
let { $, cleanup } = render(_jsx(FieldLabel, { name: "firstName" }));
|
|
69
|
+
t.after(cleanup);
|
|
70
|
+
assert.equal($('[data-testid="label"]')?.textContent, 'first name');
|
|
71
|
+
});
|
|
72
|
+
it('handles multiple humps', (t) => {
|
|
73
|
+
let { $, cleanup } = render(_jsx(FieldLabel, { name: "dateOfBirth" }));
|
|
74
|
+
t.after(cleanup);
|
|
75
|
+
assert.equal($('[data-testid="label"]')?.textContent, 'date of birth');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('DOM Tests', () => {
|
|
79
|
+
it('can interact with DOM', async () => {
|
|
80
|
+
let div = document.createElement('div');
|
|
81
|
+
div.textContent = 'Hello';
|
|
82
|
+
assert.equal(div.textContent, 'Hello');
|
|
83
|
+
});
|
|
84
|
+
it('can test fetch API', async () => {
|
|
85
|
+
let response = await fetch('data:text/plain,hello');
|
|
86
|
+
let text = await response.text();
|
|
87
|
+
assert.equal(text, 'hello');
|
|
88
|
+
});
|
|
89
|
+
it.skip('skip: can skip tests', () => {
|
|
90
|
+
assert.equal(true, false);
|
|
91
|
+
});
|
|
92
|
+
it.todo('todo: can mark tests as todo');
|
|
93
|
+
});
|
|
94
|
+
describe('render/cleanup', () => {
|
|
95
|
+
it('cleanup removes the container from the DOM', () => {
|
|
96
|
+
let { container, cleanup } = render(_jsx("div", { "data-testid": "manual", children: "hello" }));
|
|
97
|
+
assert.equal(document.body.contains(container), true);
|
|
98
|
+
cleanup();
|
|
99
|
+
assert.equal(document.body.contains(container), false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe.skip('skip: Skipped Test Suite', () => {
|
|
103
|
+
it('would fail', () => {
|
|
104
|
+
assert.equal(true, false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe.todo('todo: Test Suite');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework.test.e2e.d.ts","sourceRoot":"","sources":["../../src/test/framework.test.e2e.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@remix-run/ui/jsx-runtime";
|
|
2
|
+
import * as assert from 'node:assert/strict';
|
|
3
|
+
import { renderToString } from '@remix-run/ui/server';
|
|
4
|
+
import { createTestServer } from '@remix-run/node-fetch-server/test';
|
|
5
|
+
import { describe, it } from "../lib/framework.js";
|
|
6
|
+
const html = async (n) => new Response(await renderToString(n), {
|
|
7
|
+
headers: { 'Content-Type': 'text/html' },
|
|
8
|
+
});
|
|
9
|
+
describe('e2e tests', () => {
|
|
10
|
+
it('runs playwright against a fetch handler', async (t) => {
|
|
11
|
+
function Doc(handle) {
|
|
12
|
+
return () => (_jsxs("html", { children: [
|
|
13
|
+
_jsx("head", { children: _jsx("title", { children: "Test" }) }), _jsx("body", { children: handle.props.children })
|
|
14
|
+
] }));
|
|
15
|
+
}
|
|
16
|
+
let handler = (request) => {
|
|
17
|
+
let url = new URL(request.url);
|
|
18
|
+
if (url.pathname === '/') {
|
|
19
|
+
return html(_jsxs(Doc, { children: [
|
|
20
|
+
_jsx("h1", { children: "Hello Remix" }), _jsx("a", { href: "/about", children: "About" })
|
|
21
|
+
] }));
|
|
22
|
+
}
|
|
23
|
+
if (url.pathname === '/about') {
|
|
24
|
+
return html(_jsx(Doc, { children: _jsx("h1", { children: "About Remix" }) }));
|
|
25
|
+
}
|
|
26
|
+
return new Response('Not found', { status: 404 });
|
|
27
|
+
};
|
|
28
|
+
let page = await t.serve(await createTestServer(handler));
|
|
29
|
+
await page.goto('/');
|
|
30
|
+
assert.equal(await page.locator('h1').textContent(), 'Hello Remix');
|
|
31
|
+
await page.click('[href="/about"]');
|
|
32
|
+
assert.equal(await page.locator('h1').textContent(), 'About Remix');
|
|
33
|
+
});
|
|
34
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/test",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A test framework for JavaScript and TypeScript projects",
|
|
5
5
|
"author": "Shopify Inc.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
"!src/**/*.test.*"
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24.3.0"
|
|
24
|
+
},
|
|
22
25
|
"bin": {
|
|
23
|
-
"remix-test": "./dist/cli.js"
|
|
26
|
+
"remix-test": "./dist/cli-entry.js"
|
|
24
27
|
},
|
|
25
28
|
"exports": {
|
|
26
29
|
".": {
|
|
@@ -28,13 +31,23 @@
|
|
|
28
31
|
"default": "./dist/index.js"
|
|
29
32
|
},
|
|
30
33
|
"./package.json": "./package.json",
|
|
31
|
-
"./cli":
|
|
34
|
+
"./cli": {
|
|
35
|
+
"types": "./dist/cli.d.ts",
|
|
36
|
+
"default": "./dist/cli.js"
|
|
37
|
+
}
|
|
32
38
|
},
|
|
33
39
|
"dependencies": {
|
|
40
|
+
"es-module-lexer": "^2.0.0",
|
|
41
|
+
"esbuild": "^0.27.1",
|
|
42
|
+
"get-tsconfig": "^4.13.6",
|
|
43
|
+
"istanbul-lib-coverage": "^3.2.2",
|
|
44
|
+
"istanbul-lib-report": "^3.0.1",
|
|
45
|
+
"istanbul-reports": "^3.2.0",
|
|
46
|
+
"magic-string": "^0.30.21",
|
|
47
|
+
"source-map-js": "^1.2.1",
|
|
34
48
|
"tsx": "^4.21.0",
|
|
35
|
-
"
|
|
36
|
-
"@remix-run/
|
|
37
|
-
"@remix-run/node-fetch-server": "0.13.0"
|
|
49
|
+
"v8-to-istanbul": "^9.3.0",
|
|
50
|
+
"@remix-run/terminal": "^0.1.0"
|
|
38
51
|
},
|
|
39
52
|
"peerDependencies": {
|
|
40
53
|
"playwright": "^1.59.0"
|
|
@@ -45,10 +58,17 @@
|
|
|
45
58
|
}
|
|
46
59
|
},
|
|
47
60
|
"devDependencies": {
|
|
61
|
+
"@types/dom-navigation": "1.0.6",
|
|
62
|
+
"@types/istanbul-lib-coverage": "^2.0.6",
|
|
63
|
+
"@types/istanbul-lib-report": "^3.0.3",
|
|
64
|
+
"@types/istanbul-reports": "^3.0.4",
|
|
48
65
|
"@types/node": "^24.6.0",
|
|
49
66
|
"@typescript/native-preview": "7.0.0-dev.20251125.1",
|
|
67
|
+
"decamelize": "^6.0.1",
|
|
50
68
|
"playwright": "^1.59.0",
|
|
51
|
-
"@remix-run/
|
|
69
|
+
"@remix-run/node-fetch-server": "^0.13.1",
|
|
70
|
+
"@remix-run/assert": "^0.2.0",
|
|
71
|
+
"@remix-run/ui": "^0.1.1"
|
|
52
72
|
},
|
|
53
73
|
"keywords": [
|
|
54
74
|
"testing",
|
|
@@ -61,7 +81,8 @@
|
|
|
61
81
|
"scripts": {
|
|
62
82
|
"build": "tsgo -p tsconfig.build.json",
|
|
63
83
|
"clean": "git clean -fdX",
|
|
64
|
-
"test": "node src/cli.ts",
|
|
84
|
+
"test": "node src/cli-entry.ts",
|
|
85
|
+
"test:bun": "bun --bun src/cli-entry.ts --type server",
|
|
65
86
|
"typecheck": "tsgo --noEmit"
|
|
66
87
|
}
|
|
67
88
|
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { normalizeLine } from '../../lib/normalize.ts'
|
|
2
|
+
import type { TestResult, TestResults } from '../../lib/reporters/results.ts'
|
|
3
|
+
|
|
4
|
+
interface TestsSetup {
|
|
5
|
+
testPaths: string[]
|
|
6
|
+
baseDir: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type FileResults = TestResults & { tests: Array<TestResult & { filePath: string }> }
|
|
10
|
+
|
|
11
|
+
const STYLES = `
|
|
12
|
+
.rt-container {
|
|
13
|
+
font-family: monospace;
|
|
14
|
+
padding: 16px;
|
|
15
|
+
max-width: 900px;
|
|
16
|
+
}
|
|
17
|
+
.rt-summary {
|
|
18
|
+
margin-bottom: 16px;
|
|
19
|
+
line-height: 1.6;
|
|
20
|
+
}
|
|
21
|
+
.rt-summary-row {
|
|
22
|
+
display: block;
|
|
23
|
+
}
|
|
24
|
+
.rt-info {
|
|
25
|
+
color: #0ea5e9;
|
|
26
|
+
}
|
|
27
|
+
.rt-indent {
|
|
28
|
+
margin-left: 16px;
|
|
29
|
+
margin-top: 4px;
|
|
30
|
+
}
|
|
31
|
+
.rt-suite-details {
|
|
32
|
+
margin-bottom: 8px;
|
|
33
|
+
}
|
|
34
|
+
.rt-suite-summary {
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
padding: 2px 0;
|
|
37
|
+
user-select: none;
|
|
38
|
+
}
|
|
39
|
+
.rt-suite-icon {
|
|
40
|
+
margin-left: 6px;
|
|
41
|
+
}
|
|
42
|
+
.rt-test-item {
|
|
43
|
+
padding: 3px 18px;
|
|
44
|
+
}
|
|
45
|
+
.rt-test-duration {
|
|
46
|
+
color: #999;
|
|
47
|
+
font-size: 0.85em;
|
|
48
|
+
}
|
|
49
|
+
.rt-error-pre {
|
|
50
|
+
margin: 4px 0 4px 16px;
|
|
51
|
+
padding: 8px 12px;
|
|
52
|
+
font-size: 12px;
|
|
53
|
+
color: #dc2626;
|
|
54
|
+
background: #fff5f5;
|
|
55
|
+
border-left: 3px solid #dc2626;
|
|
56
|
+
white-space: pre-wrap;
|
|
57
|
+
word-break: break-word;
|
|
58
|
+
}
|
|
59
|
+
.rt-error-stack {
|
|
60
|
+
color: #999;
|
|
61
|
+
margin-top: 6px;
|
|
62
|
+
}
|
|
63
|
+
.rt-button {
|
|
64
|
+
margin-top: 8px;
|
|
65
|
+
padding: 6px 12px;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
}
|
|
68
|
+
.rt-stack-link {
|
|
69
|
+
color: inherit;
|
|
70
|
+
text-decoration: underline;
|
|
71
|
+
text-decoration-color: #aaa;
|
|
72
|
+
}
|
|
73
|
+
.rt-passed {
|
|
74
|
+
color: #16a34a;
|
|
75
|
+
}
|
|
76
|
+
.rt-failed {
|
|
77
|
+
color: #dc2626;
|
|
78
|
+
}
|
|
79
|
+
.rt-muted {
|
|
80
|
+
color: #666;
|
|
81
|
+
}
|
|
82
|
+
.rt-todo {
|
|
83
|
+
color: #a16207;
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
const styleEl = document.createElement('style')
|
|
88
|
+
styleEl.textContent = STYLES
|
|
89
|
+
document.head.appendChild(styleEl)
|
|
90
|
+
|
|
91
|
+
const setupEl = document.getElementById('test-setup')
|
|
92
|
+
if (!setupEl?.textContent) {
|
|
93
|
+
throw new Error('Test runner: missing #test-setup payload')
|
|
94
|
+
}
|
|
95
|
+
const setup = JSON.parse(setupEl.textContent) as TestsSetup
|
|
96
|
+
const root = document.getElementById('test-root')
|
|
97
|
+
if (!root) {
|
|
98
|
+
throw new Error('Test runner: missing #test-root mount point')
|
|
99
|
+
}
|
|
100
|
+
mountTests(root, setup)
|
|
101
|
+
|
|
102
|
+
function mountTests(host: HTMLElement, setup: TestsSetup): void {
|
|
103
|
+
let startTime = performance.now()
|
|
104
|
+
let totals = { passed: 0, failed: 0, skipped: 0, todo: 0 }
|
|
105
|
+
|
|
106
|
+
let container = el('div', { id: 'test-status', className: 'rt-container' })
|
|
107
|
+
host.appendChild(container)
|
|
108
|
+
|
|
109
|
+
let summary = el('div', { className: 'rt-summary' })
|
|
110
|
+
container.appendChild(summary)
|
|
111
|
+
|
|
112
|
+
let testsRow = summaryRow()
|
|
113
|
+
let passRow = summaryRow()
|
|
114
|
+
let failRow = summaryRow()
|
|
115
|
+
let skippedRow = summaryRow()
|
|
116
|
+
let todoRow = summaryRow()
|
|
117
|
+
let durationRow = summaryRow()
|
|
118
|
+
summary.append(testsRow.el, passRow.el, failRow.el)
|
|
119
|
+
|
|
120
|
+
let suitesContainer = el('div')
|
|
121
|
+
container.appendChild(suitesContainer)
|
|
122
|
+
|
|
123
|
+
function renderSummary(done: boolean) {
|
|
124
|
+
let total = totals.passed + totals.failed + totals.skipped + totals.todo
|
|
125
|
+
testsRow.text(`tests ${total}`)
|
|
126
|
+
passRow.text(`pass ${totals.passed}`)
|
|
127
|
+
failRow.text(`fail ${totals.failed}`)
|
|
128
|
+
if (totals.skipped > 0) {
|
|
129
|
+
if (!skippedRow.el.parentNode) summary.appendChild(skippedRow.el)
|
|
130
|
+
skippedRow.text(`skipped ${totals.skipped}`)
|
|
131
|
+
}
|
|
132
|
+
if (totals.todo > 0) {
|
|
133
|
+
if (!todoRow.el.parentNode) summary.appendChild(todoRow.el)
|
|
134
|
+
todoRow.text(`todo ${totals.todo}`)
|
|
135
|
+
}
|
|
136
|
+
if (done) {
|
|
137
|
+
if (!durationRow.el.parentNode) summary.appendChild(durationRow.el)
|
|
138
|
+
durationRow.text(`duration_ms ${(performance.now() - startTime).toFixed(5)}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function appendFileSuites(fileResults: FileResults) {
|
|
143
|
+
let suiteMap = new Map<string, TestResult[]>()
|
|
144
|
+
for (let test of fileResults.tests) {
|
|
145
|
+
let suite = test.suiteName || 'Tests'
|
|
146
|
+
if (!suiteMap.has(suite)) suiteMap.set(suite, [])
|
|
147
|
+
suiteMap.get(suite)!.push(test)
|
|
148
|
+
}
|
|
149
|
+
for (let [suiteName, tests] of suiteMap) {
|
|
150
|
+
suitesContainer.appendChild(buildSuite(suiteName, tests, setup.baseDir))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function appendRerunButton() {
|
|
155
|
+
let button = el('button', { className: 'rt-button', textContent: 'Re-run' })
|
|
156
|
+
button.type = 'button'
|
|
157
|
+
button.addEventListener('click', () => window.location.reload())
|
|
158
|
+
container.appendChild(button)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
renderSummary(false)
|
|
162
|
+
|
|
163
|
+
void (async () => {
|
|
164
|
+
for (let testFile of setup.testPaths) {
|
|
165
|
+
let fileResults = await runInIframe(testFile)
|
|
166
|
+
await fetch('/file-results', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
body: JSON.stringify(fileResults),
|
|
170
|
+
})
|
|
171
|
+
totals.passed += fileResults.passed
|
|
172
|
+
totals.failed += fileResults.failed
|
|
173
|
+
totals.skipped += fileResults.skipped
|
|
174
|
+
totals.todo += fileResults.todo
|
|
175
|
+
appendFileSuites(fileResults)
|
|
176
|
+
renderSummary(false)
|
|
177
|
+
}
|
|
178
|
+
renderSummary(true)
|
|
179
|
+
appendRerunButton()
|
|
180
|
+
;(window as unknown as { __testsDone?: boolean }).__testsDone = true
|
|
181
|
+
})()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function runInIframe(testFile: string): Promise<FileResults> {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
let iframe = document.createElement('iframe')
|
|
187
|
+
iframe.src = `/iframe?file=${encodeURIComponent(testFile)}`
|
|
188
|
+
// Make the iframe as big so we don't get unintentional scrolling in test UIs
|
|
189
|
+
let parentBody = iframe.contentWindow?.document.body
|
|
190
|
+
iframe.width = Math.max(parentBody?.scrollWidth ?? 0, 800).toString()
|
|
191
|
+
iframe.height = Math.max(Math.round((parentBody?.scrollHeight ?? 0) / 2), 400).toString()
|
|
192
|
+
document.body.appendChild(iframe)
|
|
193
|
+
|
|
194
|
+
function onMessage(event: MessageEvent) {
|
|
195
|
+
if (event.source !== iframe.contentWindow) return
|
|
196
|
+
window.removeEventListener('message', onMessage)
|
|
197
|
+
// Hide instead of remove so when coverage is enabled the iframe remains attached
|
|
198
|
+
// so V8 retains its scripts and Playwright can collect coverage at run end.
|
|
199
|
+
iframe.style.display = 'none'
|
|
200
|
+
if (event.data.type === 'test-results') {
|
|
201
|
+
let { passed, failed, skipped, todo, tests } = event.data.results as TestResults
|
|
202
|
+
resolve({
|
|
203
|
+
passed,
|
|
204
|
+
failed,
|
|
205
|
+
skipped,
|
|
206
|
+
todo,
|
|
207
|
+
tests: tests.map((t) => ({ ...t, filePath: testFile })),
|
|
208
|
+
})
|
|
209
|
+
} else {
|
|
210
|
+
let { message, stack } = event.data.error
|
|
211
|
+
resolve({
|
|
212
|
+
passed: 0,
|
|
213
|
+
failed: 1,
|
|
214
|
+
skipped: 0,
|
|
215
|
+
todo: 0,
|
|
216
|
+
tests: [
|
|
217
|
+
{
|
|
218
|
+
name: '',
|
|
219
|
+
suiteName: testFile,
|
|
220
|
+
filePath: testFile,
|
|
221
|
+
status: 'failed',
|
|
222
|
+
error: { message, stack },
|
|
223
|
+
duration: 0,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
window.addEventListener('message', onMessage)
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildSuite(suiteName: string, tests: TestResult[], baseDir: string): HTMLElement {
|
|
235
|
+
let suiteFailed = tests.some((t) => t.status === 'failed')
|
|
236
|
+
let suiteAllSkipped = tests.every((t) => t.status === 'skipped')
|
|
237
|
+
let suiteAllTodo = tests.every((t) => t.status === 'todo')
|
|
238
|
+
let stateClass = suiteFailed
|
|
239
|
+
? 'rt-failed'
|
|
240
|
+
: suiteAllSkipped
|
|
241
|
+
? 'rt-muted'
|
|
242
|
+
: suiteAllTodo
|
|
243
|
+
? 'rt-todo'
|
|
244
|
+
: 'rt-passed'
|
|
245
|
+
let icon = suiteFailed ? '✗' : suiteAllSkipped ? '↓' : suiteAllTodo ? '…' : '✓'
|
|
246
|
+
let suffix = suiteAllSkipped ? ' # skipped' : suiteAllTodo ? ' # todo' : ''
|
|
247
|
+
|
|
248
|
+
let details = el('details', { className: 'rt-suite-details' })
|
|
249
|
+
if (suiteFailed) details.open = true
|
|
250
|
+
|
|
251
|
+
let summary = el('summary', { className: `rt-suite-summary ${stateClass}` })
|
|
252
|
+
summary.appendChild(
|
|
253
|
+
el('span', { className: 'rt-suite-icon', textContent: `${icon} ${suiteName}${suffix}` }),
|
|
254
|
+
)
|
|
255
|
+
details.appendChild(summary)
|
|
256
|
+
|
|
257
|
+
let body = el('div', { className: 'rt-indent' })
|
|
258
|
+
for (let test of tests) {
|
|
259
|
+
let item = buildTestItem(test, baseDir)
|
|
260
|
+
if (item) body.appendChild(el('div', { className: 'rt-test-item' }, item))
|
|
261
|
+
}
|
|
262
|
+
details.appendChild(body)
|
|
263
|
+
return details
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildTestItem(test: TestResult, baseDir: string): HTMLElement | null {
|
|
267
|
+
if (test.status === 'passed') {
|
|
268
|
+
let row = el('div', { className: 'rt-passed' })
|
|
269
|
+
row.append(`✓ ${test.name} `)
|
|
270
|
+
row.appendChild(
|
|
271
|
+
el('span', {
|
|
272
|
+
className: 'rt-test-duration',
|
|
273
|
+
textContent: `(${test.duration.toFixed(2)}ms)`,
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
return row
|
|
277
|
+
}
|
|
278
|
+
if (test.status === 'failed') {
|
|
279
|
+
let row = el('div', { className: 'rt-failed' })
|
|
280
|
+
row.append(`✗ ${test.name} `)
|
|
281
|
+
row.appendChild(
|
|
282
|
+
el('span', {
|
|
283
|
+
className: 'rt-test-duration',
|
|
284
|
+
textContent: `(${test.duration.toFixed(2)}ms)`,
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
if (test.error) {
|
|
288
|
+
let pre = el('pre', { className: 'rt-error-pre' })
|
|
289
|
+
pre.append(test.error.message)
|
|
290
|
+
if (test.error.stack) {
|
|
291
|
+
let stackDiv = el('div', { className: 'rt-error-stack' })
|
|
292
|
+
stackDiv.appendChild(buildStack(test.error.stack, baseDir))
|
|
293
|
+
pre.appendChild(stackDiv)
|
|
294
|
+
}
|
|
295
|
+
row.appendChild(pre)
|
|
296
|
+
}
|
|
297
|
+
return row
|
|
298
|
+
}
|
|
299
|
+
if (test.status === 'skipped' && test.name) {
|
|
300
|
+
return el('div', { className: 'rt-muted', textContent: `↓ ${test.name} # skipped` })
|
|
301
|
+
}
|
|
302
|
+
if (test.status === 'todo' && test.name) {
|
|
303
|
+
return el('div', { className: 'rt-todo', textContent: `… ${test.name} # todo` })
|
|
304
|
+
}
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function buildStack(stack: string, baseDir: string): DocumentFragment {
|
|
309
|
+
let frameLocRe = /([^():\s][^():]*\.[jt]sx?):(\d+):(\d+)/
|
|
310
|
+
let frag = document.createDocumentFragment()
|
|
311
|
+
for (let raw of stack.split('\n')) {
|
|
312
|
+
let isTestModule = raw.includes('/@test/')
|
|
313
|
+
let line = normalizeLine(raw)
|
|
314
|
+
let match = isTestModule ? frameLocRe.exec(line) : null
|
|
315
|
+
let div = document.createElement('div')
|
|
316
|
+
if (match) {
|
|
317
|
+
let [full, file, row, col] = match
|
|
318
|
+
let abs = `${baseDir}/${file}`
|
|
319
|
+
let href = `vscode://file/${abs}:${row}:${col}`
|
|
320
|
+
div.append(line.slice(0, match.index))
|
|
321
|
+
let a = el('a', { className: 'rt-stack-link', textContent: full })
|
|
322
|
+
a.href = href
|
|
323
|
+
div.appendChild(a)
|
|
324
|
+
div.append(line.slice(match.index + full.length))
|
|
325
|
+
} else {
|
|
326
|
+
div.textContent = line
|
|
327
|
+
}
|
|
328
|
+
frag.appendChild(div)
|
|
329
|
+
}
|
|
330
|
+
return frag
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function summaryRow() {
|
|
334
|
+
let row = el('span', { className: 'rt-summary-row' })
|
|
335
|
+
let icon = el('span', { className: 'rt-info', textContent: 'ℹ' })
|
|
336
|
+
let textNode = document.createTextNode('')
|
|
337
|
+
row.append(icon, ' ', textNode)
|
|
338
|
+
return {
|
|
339
|
+
el: row,
|
|
340
|
+
text(s: string) {
|
|
341
|
+
textNode.data = ' ' + s
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function el<K extends keyof HTMLElementTagNameMap>(
|
|
347
|
+
tag: K,
|
|
348
|
+
props?: { id?: string; className?: string; textContent?: string },
|
|
349
|
+
...children: Array<Node | string>
|
|
350
|
+
): HTMLElementTagNameMap[K] {
|
|
351
|
+
let node = document.createElement(tag)
|
|
352
|
+
if (props?.id) node.id = props.id
|
|
353
|
+
if (props?.className) node.className = props.className
|
|
354
|
+
if (props?.textContent != null) node.textContent = props.textContent
|
|
355
|
+
if (children.length) node.append(...children)
|
|
356
|
+
return node
|
|
357
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runTests } from '../../lib/executor.ts'
|
|
2
|
+
|
|
3
|
+
const params = new URLSearchParams(location.search)
|
|
4
|
+
const testFile = params.get('file')!
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
await import(testFile)
|
|
8
|
+
let results = await runTests()
|
|
9
|
+
window.parent.postMessage({ type: 'test-results', results }, '*')
|
|
10
|
+
} catch (error: any) {
|
|
11
|
+
window.parent.postMessage(
|
|
12
|
+
{
|
|
13
|
+
type: 'test-error',
|
|
14
|
+
error: { message: error?.message ?? String(error), stack: error?.stack },
|
|
15
|
+
},
|
|
16
|
+
'*',
|
|
17
|
+
)
|
|
18
|
+
}
|