@remix-run/test 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +325 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +171 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/config.d.ts +60 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +152 -0
- package/dist/lib/context.d.ts +69 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/context.js +49 -0
- package/dist/lib/e2e-server.d.ts +11 -0
- package/dist/lib/e2e-server.d.ts.map +1 -0
- package/dist/lib/e2e-server.js +15 -0
- package/dist/lib/executor.d.ts +27 -0
- package/dist/lib/executor.d.ts.map +1 -0
- package/dist/lib/executor.js +123 -0
- package/dist/lib/framework.d.ts +107 -0
- package/dist/lib/framework.d.ts.map +1 -0
- package/dist/lib/framework.js +198 -0
- package/dist/lib/framework.test.d.ts +2 -0
- package/dist/lib/framework.test.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.d.ts +2 -0
- package/dist/lib/framework.test.e2e.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.js +29 -0
- package/dist/lib/framework.test.js +283 -0
- package/dist/lib/mock.d.ts +52 -0
- package/dist/lib/mock.d.ts.map +1 -0
- package/dist/lib/mock.js +61 -0
- package/dist/lib/playwright.d.ts +15 -0
- package/dist/lib/playwright.d.ts.map +1 -0
- package/dist/lib/playwright.js +84 -0
- package/dist/lib/reporters/dot.d.ts +10 -0
- package/dist/lib/reporters/dot.d.ts.map +1 -0
- package/dist/lib/reporters/dot.js +55 -0
- package/dist/lib/reporters/files.d.ts +10 -0
- package/dist/lib/reporters/files.d.ts.map +1 -0
- package/dist/lib/reporters/files.js +70 -0
- package/dist/lib/reporters/index.d.ts +14 -0
- package/dist/lib/reporters/index.d.ts.map +1 -0
- package/dist/lib/reporters/index.js +18 -0
- package/dist/lib/reporters/spec.d.ts +10 -0
- package/dist/lib/reporters/spec.d.ts.map +1 -0
- package/dist/lib/reporters/spec.js +152 -0
- package/dist/lib/reporters/tap.d.ts +10 -0
- package/dist/lib/reporters/tap.d.ts.map +1 -0
- package/dist/lib/reporters/tap.js +54 -0
- package/dist/lib/runner.d.ts +9 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +89 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +27 -0
- package/dist/lib/watcher.d.ts +5 -0
- package/dist/lib/watcher.d.ts.map +1 -0
- package/dist/lib/watcher.js +39 -0
- package/dist/lib/worker-e2e.d.ts +2 -0
- package/dist/lib/worker-e2e.d.ts.map +1 -0
- package/dist/lib/worker-e2e.js +48 -0
- package/dist/lib/worker.d.ts +2 -0
- package/dist/lib/worker.d.ts.map +1 -0
- package/dist/lib/worker.js +29 -0
- package/package.json +58 -5
- package/src/cli.ts +210 -0
- package/src/index.ts +15 -0
- package/src/lib/config.ts +231 -0
- package/src/lib/context.ts +126 -0
- package/src/lib/e2e-server.ts +28 -0
- package/src/lib/executor.ts +162 -0
- package/src/lib/framework.ts +251 -0
- package/src/lib/mock.ts +89 -0
- package/src/lib/playwright.ts +102 -0
- package/src/lib/reporters/dot.ts +57 -0
- package/src/lib/reporters/files.ts +76 -0
- package/src/lib/reporters/index.ts +28 -0
- package/src/lib/reporters/spec.ts +173 -0
- package/src/lib/reporters/tap.ts +58 -0
- package/src/lib/runner.ts +137 -0
- package/src/lib/utils.ts +40 -0
- package/src/lib/watcher.ts +46 -0
- package/src/lib/worker-e2e.ts +52 -0
- package/src/lib/worker.ts +30 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Holds lifecycle hooks registered at the top level (outside any describe).
|
|
2
|
+
// Top-level describes inherit these hooks just like nested describes inherit
|
|
3
|
+
// from their parent.
|
|
4
|
+
const rootHooks = {};
|
|
5
|
+
let currentSuite = null;
|
|
6
|
+
const rootSuites = [];
|
|
7
|
+
// Lazily-created suite for top-level it() calls outside any describe().
|
|
8
|
+
// Name '' causes the reporter to display these tests under "Global".
|
|
9
|
+
// We check rootSuites.includes() so the suite is re-created after the executor
|
|
10
|
+
// clears rootSuites between files (suites.length = 0) or after captureRegistration splices it.
|
|
11
|
+
let implicitRootSuite = null;
|
|
12
|
+
function getImplicitRootSuite() {
|
|
13
|
+
if (!implicitRootSuite || !rootSuites.includes(implicitRootSuite)) {
|
|
14
|
+
implicitRootSuite = { name: '', tests: [], ...rootHooks };
|
|
15
|
+
rootSuites.push(implicitRootSuite);
|
|
16
|
+
}
|
|
17
|
+
return implicitRootSuite;
|
|
18
|
+
}
|
|
19
|
+
// Expose for executor.ts which reads this global
|
|
20
|
+
;
|
|
21
|
+
globalThis.__testSuites = rootSuites;
|
|
22
|
+
function registerDescribe(name, fn, flags) {
|
|
23
|
+
// Nested describes are flattened: "Parent > Child"
|
|
24
|
+
let fullName = currentSuite ? `${currentSuite.name} > ${name}` : name;
|
|
25
|
+
if (rootSuites.some((s) => s.name === fullName)) {
|
|
26
|
+
throw new Error(`Duplicate suite name: "${fullName}"`);
|
|
27
|
+
}
|
|
28
|
+
let suite = { name: fullName, tests: [], ...flags };
|
|
29
|
+
// Inherit lifecycle hooks from parent suite (or root hooks if at top level)
|
|
30
|
+
let parent = currentSuite ?? rootHooks;
|
|
31
|
+
if (parent.beforeEach)
|
|
32
|
+
suite.beforeEach = parent.beforeEach;
|
|
33
|
+
if (parent.afterEach)
|
|
34
|
+
suite.afterEach = parent.afterEach;
|
|
35
|
+
if (parent.beforeAll)
|
|
36
|
+
suite.beforeAll = parent.beforeAll;
|
|
37
|
+
if (parent.afterAll)
|
|
38
|
+
suite.afterAll = parent.afterAll;
|
|
39
|
+
let insertedAt = rootSuites.length;
|
|
40
|
+
rootSuites.push(suite);
|
|
41
|
+
let prevSuite = currentSuite;
|
|
42
|
+
currentSuite = suite;
|
|
43
|
+
try {
|
|
44
|
+
fn();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Remove this suite and any suites registered during fn() so they don't
|
|
48
|
+
// end up in the executor after a failed registration call
|
|
49
|
+
rootSuites.splice(insertedAt);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
currentSuite = prevSuite;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Groups related tests into a named suite. Suites can be nested snd will be displayed
|
|
58
|
+
* as such or joined with ` > ` in reporter output. Lifecycle hooks registered inside
|
|
59
|
+
* a `describe` block apply only to tests within that block.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* describe('auth', () => {
|
|
63
|
+
* it('logs in', async () => { ... })
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // Modifiers
|
|
67
|
+
* describe.skip('skipped suite', () => { ... })
|
|
68
|
+
* describe.only('focused suite', () => { ... })
|
|
69
|
+
* describe.todo('planned suite')
|
|
70
|
+
*
|
|
71
|
+
* @param name - The suite name shown in reporter output.
|
|
72
|
+
* @param fn - A function that registers the tests and lifecycle hooks in this suite.
|
|
73
|
+
*/
|
|
74
|
+
export const describe = Object.assign((name, metaOrFn, fn) => {
|
|
75
|
+
let meta = typeof metaOrFn === 'function' ? {} : metaOrFn;
|
|
76
|
+
let suiteFn = typeof metaOrFn === 'function' ? metaOrFn : fn;
|
|
77
|
+
registerDescribe(name, suiteFn, meta);
|
|
78
|
+
}, {
|
|
79
|
+
skip: (name, fn) => registerDescribe(name, fn, { skip: true }),
|
|
80
|
+
only: (name, fn) => registerDescribe(name, fn, { only: true }),
|
|
81
|
+
todo: (name) => {
|
|
82
|
+
let fullName = currentSuite ? `${currentSuite.name} > ${name}` : name;
|
|
83
|
+
if (rootSuites.some((s) => s.name === fullName)) {
|
|
84
|
+
throw new Error(`Duplicate suite name: "${fullName}"`);
|
|
85
|
+
}
|
|
86
|
+
rootSuites.push({ name: fullName, tests: [], todo: true });
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
function registerIt(name, fn, flags) {
|
|
90
|
+
let suite = currentSuite ?? getImplicitRootSuite();
|
|
91
|
+
if (suite.tests.some((t) => t.name === name)) {
|
|
92
|
+
throw new Error(`Duplicate test name: "${name}" in suite "${suite.name || 'Global'}"`);
|
|
93
|
+
}
|
|
94
|
+
suite.tests.push({ name, fn, suite, ...flags });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Defines a single test case. The optional `TestContext` argument `t` provides
|
|
98
|
+
* mock helpers and per-test cleanup registration.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* it('returns 200 for the home route', async () => {
|
|
102
|
+
* const res = await router.fetch('/')
|
|
103
|
+
* assert.equal(res.status, 200)
|
|
104
|
+
* })
|
|
105
|
+
*
|
|
106
|
+
* // Modifiers
|
|
107
|
+
* it.skip('not ready yet', () => { ... })
|
|
108
|
+
* it.only('focused test', () => { ... })
|
|
109
|
+
* it.todo('coming soon')
|
|
110
|
+
*
|
|
111
|
+
* @param name - The test name shown in reporter output.
|
|
112
|
+
* @param fn - The test body, receiving a {@link TestContext} as its first argument.
|
|
113
|
+
*/
|
|
114
|
+
export const it = Object.assign((name, metaOrFn, fn) => {
|
|
115
|
+
let meta = typeof metaOrFn === 'function' ? {} : metaOrFn;
|
|
116
|
+
let testFn = typeof metaOrFn === 'function' ? metaOrFn : fn;
|
|
117
|
+
registerIt(name, testFn, meta);
|
|
118
|
+
}, {
|
|
119
|
+
skip: (name, fn) => registerIt(name, fn ?? (() => { }), { skip: true }),
|
|
120
|
+
only: (name, fn) => registerIt(name, fn, { only: true }),
|
|
121
|
+
todo: (name) => {
|
|
122
|
+
let suite = currentSuite ?? getImplicitRootSuite();
|
|
123
|
+
if (suite.tests.some((t) => t.name === name)) {
|
|
124
|
+
throw new Error(`Duplicate test name: "${name}" in suite "${suite.name || 'Global'}"`);
|
|
125
|
+
}
|
|
126
|
+
suite.tests.push({ name, fn: () => { }, suite, todo: true });
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
/** Alias for {@link describe}. */
|
|
130
|
+
export const suite = describe;
|
|
131
|
+
/** Alias for {@link it}. */
|
|
132
|
+
export const test = it;
|
|
133
|
+
function chainBefore(existing, fn) {
|
|
134
|
+
return existing
|
|
135
|
+
? async () => {
|
|
136
|
+
await existing();
|
|
137
|
+
await fn();
|
|
138
|
+
}
|
|
139
|
+
: fn;
|
|
140
|
+
}
|
|
141
|
+
function chainAfter(existing, fn) {
|
|
142
|
+
// Child/later runs first, then earlier (reverse order)
|
|
143
|
+
return existing
|
|
144
|
+
? async () => {
|
|
145
|
+
await fn();
|
|
146
|
+
await existing();
|
|
147
|
+
}
|
|
148
|
+
: fn;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Registers a hook that runs before **each** test in the current suite (or
|
|
152
|
+
* globally if called outside a `describe`). Multiple calls are chained in
|
|
153
|
+
* registration order.
|
|
154
|
+
*
|
|
155
|
+
* @param fn - The setup function to run before each test.
|
|
156
|
+
*/
|
|
157
|
+
export function beforeEach(fn) {
|
|
158
|
+
let target = currentSuite ?? rootHooks;
|
|
159
|
+
target.beforeEach = chainBefore(target.beforeEach, fn);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Registers a hook that runs after **each** test in the current suite (or
|
|
163
|
+
* globally if called outside a `describe`). Multiple calls are chained in
|
|
164
|
+
* reverse registration order. To run logic after a singular test, use
|
|
165
|
+
* `t.after()` from the {@link TestContext}
|
|
166
|
+
*
|
|
167
|
+
* @param fn - The teardown function to run after each test.
|
|
168
|
+
*/
|
|
169
|
+
export function afterEach(fn) {
|
|
170
|
+
let target = currentSuite ?? rootHooks;
|
|
171
|
+
target.afterEach = chainAfter(target.afterEach, fn);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Registers a hook that runs once before **all** tests in the current suite
|
|
175
|
+
* (or globally if called outside a `describe`). Multiple calls are chained in
|
|
176
|
+
* registration order.
|
|
177
|
+
*
|
|
178
|
+
* @param fn - The setup function to run once before all tests in the suite.
|
|
179
|
+
*/
|
|
180
|
+
export function beforeAll(fn) {
|
|
181
|
+
let target = currentSuite ?? rootHooks;
|
|
182
|
+
target.beforeAll = chainBefore(target.beforeAll, fn);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Registers a hook that runs once after **all** tests in the current suite (or
|
|
186
|
+
* globally if called outside a `describe`). Multiple calls are chained in
|
|
187
|
+
* reverse registration order.
|
|
188
|
+
*
|
|
189
|
+
* @param fn - The teardown function to run once after all tests in the suite.
|
|
190
|
+
*/
|
|
191
|
+
export function afterAll(fn) {
|
|
192
|
+
let target = currentSuite ?? rootHooks;
|
|
193
|
+
target.afterAll = chainAfter(target.afterAll, fn);
|
|
194
|
+
}
|
|
195
|
+
/** Alias for {@link beforeAll} — matches the `node:test` API. */
|
|
196
|
+
export const before = beforeAll;
|
|
197
|
+
/** Alias for {@link afterAll} — matches the `node:test` API. */
|
|
198
|
+
export const after = afterAll;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework.test.d.ts","sourceRoot":"","sources":["../../src/lib/framework.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework.test.e2e.d.ts","sourceRoot":"","sources":["../../src/lib/framework.test.e2e.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@remix-run/component/jsx-runtime";
|
|
2
|
+
import assert from '@remix-run/assert';
|
|
3
|
+
import { renderToString } from '@remix-run/component/server';
|
|
4
|
+
import { createRouter } from '@remix-run/fetch-router';
|
|
5
|
+
import { route } from '@remix-run/fetch-router/routes';
|
|
6
|
+
import { describe, it } from "./framework.js";
|
|
7
|
+
const html = async (n) => new Response(await renderToString(n), {
|
|
8
|
+
headers: { 'Content-Type': 'text/html' },
|
|
9
|
+
});
|
|
10
|
+
describe('e2e tests', () => {
|
|
11
|
+
it('runs playwright against a fetch-router instance', async (t) => {
|
|
12
|
+
function Doc() {
|
|
13
|
+
return ({ children }) => (_jsxs("html", { children: [
|
|
14
|
+
_jsx("head", { children: _jsx("title", { children: "Test" }) }), _jsx("body", { children: children })
|
|
15
|
+
] }));
|
|
16
|
+
}
|
|
17
|
+
let routes = route({ home: '/', about: '/about' });
|
|
18
|
+
let router = createRouter();
|
|
19
|
+
router.get(routes.home, async () => html(_jsxs(Doc, { children: [
|
|
20
|
+
_jsx("h1", { children: "Hello Remix" }), _jsx("a", { href: "/about", children: "About" })
|
|
21
|
+
] })));
|
|
22
|
+
router.get(routes.about, async () => html(_jsx(Doc, { children: _jsx("h1", { children: "About Remix" }) })));
|
|
23
|
+
let page = await t.serve(router.fetch);
|
|
24
|
+
await page.goto('/');
|
|
25
|
+
assert.equal(await page.locator('h1').textContent(), 'Hello Remix');
|
|
26
|
+
await page.click('[href="/about"]');
|
|
27
|
+
assert.equal(await page.locator('h1').textContent(), 'About Remix');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import * as assert from '@remix-run/assert';
|
|
2
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, it, suite, test, } from "./framework.js";
|
|
3
|
+
// During test execution, currentSuite is null so describe() can be called freely.
|
|
4
|
+
// captureRegistration() splices any newly-registered suites back out of __testSuites
|
|
5
|
+
// so the executor doesn't run them — we just inspect their shape.
|
|
6
|
+
function captureRegistration(fn) {
|
|
7
|
+
let suites = globalThis.__testSuites;
|
|
8
|
+
let before = suites.length;
|
|
9
|
+
fn();
|
|
10
|
+
return suites.splice(before);
|
|
11
|
+
}
|
|
12
|
+
// ── describe ──────────────────────────────────────────────────────────────────
|
|
13
|
+
describe('describe', () => {
|
|
14
|
+
it('has skip, only, and todo sub-functions', () => {
|
|
15
|
+
assert.equal(typeof describe.skip, 'function');
|
|
16
|
+
assert.equal(typeof describe.only, 'function');
|
|
17
|
+
assert.equal(typeof describe.todo, 'function');
|
|
18
|
+
});
|
|
19
|
+
it('registers a suite with the given name', () => {
|
|
20
|
+
let [s] = captureRegistration(() => describe('my suite', () => { }));
|
|
21
|
+
assert.equal(s.name, 'my suite');
|
|
22
|
+
});
|
|
23
|
+
it('calls fn to register tests', () => {
|
|
24
|
+
let called = false;
|
|
25
|
+
captureRegistration(() => describe('suite', () => {
|
|
26
|
+
called = true;
|
|
27
|
+
}));
|
|
28
|
+
assert.equal(called, true);
|
|
29
|
+
});
|
|
30
|
+
it('registers tests added inside fn', () => {
|
|
31
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
32
|
+
it('test one', () => { });
|
|
33
|
+
it('test two', () => { });
|
|
34
|
+
}));
|
|
35
|
+
assert.equal(s.tests.length, 2);
|
|
36
|
+
assert.equal(s.tests[0].name, 'test one');
|
|
37
|
+
assert.equal(s.tests[1].name, 'test two');
|
|
38
|
+
});
|
|
39
|
+
it('flattens nested describes into "Outer > Inner" names', () => {
|
|
40
|
+
let [outer, inner] = captureRegistration(() => {
|
|
41
|
+
describe('outer', () => {
|
|
42
|
+
describe('inner', () => { });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
assert.equal(outer.name, 'outer');
|
|
46
|
+
assert.equal(inner.name, 'outer > inner');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('describe.skip', () => {
|
|
50
|
+
it('marks the suite as skipped', () => {
|
|
51
|
+
let [s] = captureRegistration(() => describe.skip('suite', () => { }));
|
|
52
|
+
assert.equal(s.skip, true);
|
|
53
|
+
});
|
|
54
|
+
it('still calls fn to register tests', () => {
|
|
55
|
+
let [s] = captureRegistration(() => describe.skip('suite', () => {
|
|
56
|
+
it('test', () => { });
|
|
57
|
+
}));
|
|
58
|
+
assert.equal(s.tests.length, 1);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('describe.only', () => {
|
|
62
|
+
it('marks the suite as only', () => {
|
|
63
|
+
let [s] = captureRegistration(() => describe.only('suite', () => { }));
|
|
64
|
+
assert.equal(s.only, true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('describe.todo', () => {
|
|
68
|
+
it('marks the suite as todo', () => {
|
|
69
|
+
let [s] = captureRegistration(() => describe.todo('suite'));
|
|
70
|
+
assert.equal(s.todo, true);
|
|
71
|
+
});
|
|
72
|
+
it('registers with no tests', () => {
|
|
73
|
+
let [s] = captureRegistration(() => describe.todo('suite'));
|
|
74
|
+
assert.equal(s.tests.length, 0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// ── it ────────────────────────────────────────────────────────────────────────
|
|
78
|
+
describe('it', () => {
|
|
79
|
+
it('has skip, only, and todo sub-functions', () => {
|
|
80
|
+
assert.equal(typeof it.skip, 'function');
|
|
81
|
+
assert.equal(typeof it.only, 'function');
|
|
82
|
+
assert.equal(typeof it.todo, 'function');
|
|
83
|
+
});
|
|
84
|
+
it('can be called outside describe (registers on implicit root suite)', () => {
|
|
85
|
+
let fn = () => { };
|
|
86
|
+
let captured = captureRegistration(() => it('orphan', fn));
|
|
87
|
+
let root = captured.find((s) => s.name === '');
|
|
88
|
+
assert.equal(root?.tests.some((t) => t.fn === fn), true);
|
|
89
|
+
});
|
|
90
|
+
it('registers a test with the given name and fn', () => {
|
|
91
|
+
let fn = () => { };
|
|
92
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
93
|
+
it('my test', fn);
|
|
94
|
+
}));
|
|
95
|
+
assert.equal(s.tests[0].name, 'my test');
|
|
96
|
+
assert.equal(s.tests[0].fn, fn);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('it.skip', () => {
|
|
100
|
+
it('marks the test as skipped', () => {
|
|
101
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
102
|
+
it.skip('skipped test', () => { });
|
|
103
|
+
}));
|
|
104
|
+
assert.equal(s.tests[0].skip, true);
|
|
105
|
+
});
|
|
106
|
+
it('accepts an optional fn', () => {
|
|
107
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
108
|
+
it.skip('no fn');
|
|
109
|
+
}));
|
|
110
|
+
assert.equal(s.tests[0].skip, true);
|
|
111
|
+
assert.equal(typeof s.tests[0].fn, 'function');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('it.only', () => {
|
|
115
|
+
it('marks the test as only', () => {
|
|
116
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
117
|
+
it.only('only test', () => { });
|
|
118
|
+
}));
|
|
119
|
+
assert.equal(s.tests[0].only, true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('it.todo', () => {
|
|
123
|
+
it('marks the test as todo', () => {
|
|
124
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
125
|
+
it.todo('todo test');
|
|
126
|
+
}));
|
|
127
|
+
assert.equal(s.tests[0].todo, true);
|
|
128
|
+
assert.equal(s.tests[0].name, 'todo test');
|
|
129
|
+
});
|
|
130
|
+
it('can be called outside describe (registers on implicit root suite)', () => {
|
|
131
|
+
let captured = captureRegistration(() => it.todo('orphan'));
|
|
132
|
+
let root = captured.find((s) => s.name === '');
|
|
133
|
+
assert.equal(root?.tests[0]?.name, 'orphan');
|
|
134
|
+
assert.equal(root?.tests[0]?.todo, true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// ── Duplicate detection ───────────────────────────────────────────────────────
|
|
138
|
+
describe('duplicate detection', () => {
|
|
139
|
+
it('throws on duplicate suite name', () => {
|
|
140
|
+
let suites = globalThis.__testSuites;
|
|
141
|
+
let before = suites.length;
|
|
142
|
+
try {
|
|
143
|
+
assert.throws(() => {
|
|
144
|
+
describe('__dup-suite__', () => { });
|
|
145
|
+
describe('__dup-suite__', () => { });
|
|
146
|
+
}, /Duplicate suite name: "__dup-suite__"/);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
suites.splice(before);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
it('throws on duplicate describe.todo name', () => {
|
|
153
|
+
let suites = globalThis.__testSuites;
|
|
154
|
+
let before = suites.length;
|
|
155
|
+
try {
|
|
156
|
+
assert.throws(() => {
|
|
157
|
+
describe.todo('__dup-todo-suite__');
|
|
158
|
+
describe.todo('__dup-todo-suite__');
|
|
159
|
+
}, /Duplicate suite name: "__dup-todo-suite__"/);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
suites.splice(before);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
it('throws on duplicate test name within a suite', () => {
|
|
166
|
+
assert.throws(() => captureRegistration(() => describe('suite', () => {
|
|
167
|
+
it('same name', () => { });
|
|
168
|
+
it('same name', () => { });
|
|
169
|
+
})), /Duplicate test name: "same name" in suite "suite"/);
|
|
170
|
+
});
|
|
171
|
+
it('throws on duplicate it.todo name within a suite', () => {
|
|
172
|
+
assert.throws(() => captureRegistration(() => describe('suite', () => {
|
|
173
|
+
it.todo('same name');
|
|
174
|
+
it.todo('same name');
|
|
175
|
+
})), /Duplicate test name: "same name" in suite "suite"/);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ── Aliases ───────────────────────────────────────────────────────────────────
|
|
179
|
+
describe('suite alias', () => {
|
|
180
|
+
it('is an alias for describe', () => {
|
|
181
|
+
assert.equal(suite, describe);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('test alias', () => {
|
|
185
|
+
it('is an alias for it', () => {
|
|
186
|
+
assert.equal(test, it);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
// ── Lifecycle hooks ───────────────────────────────────────────────────────────
|
|
190
|
+
describe('lifecycle hooks', () => {
|
|
191
|
+
it('beforeEach registers on the current suite', () => {
|
|
192
|
+
let fn = () => { };
|
|
193
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
194
|
+
beforeEach(fn);
|
|
195
|
+
}));
|
|
196
|
+
assert.equal(s.beforeEach, fn);
|
|
197
|
+
});
|
|
198
|
+
it('afterEach registers on the current suite', () => {
|
|
199
|
+
let fn = () => { };
|
|
200
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
201
|
+
afterEach(fn);
|
|
202
|
+
}));
|
|
203
|
+
assert.equal(s.afterEach, fn);
|
|
204
|
+
});
|
|
205
|
+
it('beforeAll registers on the current suite', () => {
|
|
206
|
+
let fn = () => { };
|
|
207
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
208
|
+
beforeAll(fn);
|
|
209
|
+
}));
|
|
210
|
+
assert.equal(s.beforeAll, fn);
|
|
211
|
+
});
|
|
212
|
+
it('afterAll registers on the current suite', () => {
|
|
213
|
+
let fn = () => { };
|
|
214
|
+
let [s] = captureRegistration(() => describe('suite', () => {
|
|
215
|
+
afterAll(fn);
|
|
216
|
+
}));
|
|
217
|
+
assert.equal(s.afterAll, fn);
|
|
218
|
+
});
|
|
219
|
+
it('beforeEach can be called outside describe (registers on root hooks)', () => {
|
|
220
|
+
// Just verify it doesn't throw — root-level hooks are tested via inheritance
|
|
221
|
+
assert.doesNotThrow(() => {
|
|
222
|
+
let [s] = captureRegistration(() => {
|
|
223
|
+
beforeEach(() => { });
|
|
224
|
+
describe('suite', () => { });
|
|
225
|
+
});
|
|
226
|
+
assert.equal(typeof s.beforeEach, 'function');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
it('afterEach can be called outside describe (registers on root hooks)', () => {
|
|
230
|
+
assert.doesNotThrow(() => {
|
|
231
|
+
let [s] = captureRegistration(() => {
|
|
232
|
+
afterEach(() => { });
|
|
233
|
+
describe('suite', () => { });
|
|
234
|
+
});
|
|
235
|
+
assert.equal(typeof s.afterEach, 'function');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
it('beforeAll can be called outside describe (registers on root hooks)', () => {
|
|
239
|
+
assert.doesNotThrow(() => {
|
|
240
|
+
let [s] = captureRegistration(() => {
|
|
241
|
+
beforeAll(() => { });
|
|
242
|
+
describe('suite', () => { });
|
|
243
|
+
});
|
|
244
|
+
assert.equal(typeof s.beforeAll, 'function');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
it('afterAll can be called outside describe (registers on root hooks)', () => {
|
|
248
|
+
assert.doesNotThrow(() => {
|
|
249
|
+
let [s] = captureRegistration(() => {
|
|
250
|
+
afterAll(() => { });
|
|
251
|
+
describe('suite', () => { });
|
|
252
|
+
});
|
|
253
|
+
assert.equal(typeof s.afterAll, 'function');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe('Example Test Suite', () => {
|
|
258
|
+
it('passes basic equality', () => {
|
|
259
|
+
assert.equal(1 + 1, 2);
|
|
260
|
+
});
|
|
261
|
+
it('passes deep equality', () => {
|
|
262
|
+
assert.deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 });
|
|
263
|
+
});
|
|
264
|
+
it('can test async code', async () => {
|
|
265
|
+
let result = await Promise.resolve(42);
|
|
266
|
+
assert.equal(result, 42);
|
|
267
|
+
});
|
|
268
|
+
it('can assert throws', () => {
|
|
269
|
+
assert.throws(() => {
|
|
270
|
+
throw new Error('test error');
|
|
271
|
+
}, /test error/);
|
|
272
|
+
});
|
|
273
|
+
it.skip('skip: can skip tests', () => {
|
|
274
|
+
assert.equal(true, false);
|
|
275
|
+
});
|
|
276
|
+
it.todo('todo: can mark tests as todo');
|
|
277
|
+
});
|
|
278
|
+
describe.skip('skip: Skipped Test Suite', () => {
|
|
279
|
+
it('would fail', () => {
|
|
280
|
+
assert.equal(true, false);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe.todo('todo: Test Suite');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Records the arguments, return value, and any thrown error for a single call. */
|
|
2
|
+
export interface MockCall<Args extends unknown[] = unknown[], Result = unknown> {
|
|
3
|
+
arguments: Args;
|
|
4
|
+
result?: Result;
|
|
5
|
+
error?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Metadata attached to every mock/spy function via its `.mock` property.
|
|
9
|
+
* `restore` is present on spies and reverts the original method when called.
|
|
10
|
+
*/
|
|
11
|
+
export interface MockContext<Args extends unknown[] = unknown[], Result = unknown> {
|
|
12
|
+
calls: MockCall<Args, Result>[];
|
|
13
|
+
restore?: () => void;
|
|
14
|
+
}
|
|
15
|
+
/** A function augmented with a `.mock` property for inspecting recorded calls. */
|
|
16
|
+
export type MockFunction<T extends (...args: any[]) => any = (...args: any[]) => any> = T & {
|
|
17
|
+
mock: MockContext<Parameters<T>, ReturnType<T>>;
|
|
18
|
+
};
|
|
19
|
+
declare function createMockFn<T extends (...args: any[]) => any>(impl?: T): MockFunction<T>;
|
|
20
|
+
declare function createMethodMock<T extends object, K extends keyof T>(obj: T, method: K, impl?: T[K] extends (...args: any[]) => any ? (...args: Parameters<T[K]>) => any : never): MockFunction;
|
|
21
|
+
/**
|
|
22
|
+
* Utilities for creating mock functions and method spies. Mirrors the names
|
|
23
|
+
* on Node.js's built-in `MockTracker` from `node:test`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Standalone mock
|
|
27
|
+
* const fn = mock.fn((x: number) => x * 2)
|
|
28
|
+
* fn(3)
|
|
29
|
+
* assert.equal(fn.mock.calls[0].result, 6)
|
|
30
|
+
*
|
|
31
|
+
* // Mock an existing method
|
|
32
|
+
* const spy = mock.method(console, 'log')
|
|
33
|
+
* console.log('hello')
|
|
34
|
+
* assert.equal(spy.mock.calls.length, 1)
|
|
35
|
+
* spy.mock.restore?.()
|
|
36
|
+
*/
|
|
37
|
+
export declare const mock: {
|
|
38
|
+
/**
|
|
39
|
+
* Creates a mock function that records every call. If `impl` is provided it
|
|
40
|
+
* is used as the underlying implementation; otherwise the mock returns
|
|
41
|
+
* `undefined`.
|
|
42
|
+
*/
|
|
43
|
+
fn: typeof createMockFn;
|
|
44
|
+
/**
|
|
45
|
+
* Replaces `obj[methodName]` with a mock and records every call. The
|
|
46
|
+
* original method is used as the implementation unless `impl` is provided.
|
|
47
|
+
* Call `mockFn.mock.restore()` to revert.
|
|
48
|
+
*/
|
|
49
|
+
method: typeof createMethodMock;
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../src/lib/mock.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,WAAW,QAAQ,CAAC,IAAI,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,EAAE,MAAM,GAAG,OAAO;IAC5E,SAAS,EAAE,IAAI,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,EAAE,MAAM,GAAG,OAAO;IAC/E,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAA;IAC/B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,kFAAkF;AAClF,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,IAAI,CAAC,GAAG;IAC1F,IAAI,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;CAChD,CAAA;AAED,iBAAS,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAqBlF;AAED,iBAAS,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAC3D,GAAG,EAAE,CAAC,EACN,MAAM,EAAE,CAAC,EACT,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,GAAG,KAAK,GACvF,YAAY,CASd;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,IAAI;IACf;;;;OAIG;;IAEH;;;;OAIG;;CAEJ,CAAA"}
|
package/dist/lib/mock.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function createMockFn(impl) {
|
|
2
|
+
let calls = [];
|
|
3
|
+
let fn = function (...args) {
|
|
4
|
+
let call = { arguments: args };
|
|
5
|
+
calls.push(call);
|
|
6
|
+
if (impl) {
|
|
7
|
+
try {
|
|
8
|
+
let result = impl.apply(this, args);
|
|
9
|
+
call.result = result;
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
call.error = error;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
fn.mock = { calls };
|
|
20
|
+
return fn;
|
|
21
|
+
}
|
|
22
|
+
function createMethodMock(obj, method, impl) {
|
|
23
|
+
let original = obj[method];
|
|
24
|
+
let effectiveImpl = (impl ?? original);
|
|
25
|
+
let mockFn = createMockFn(effectiveImpl);
|
|
26
|
+
obj[method] = mockFn;
|
|
27
|
+
mockFn.mock.restore = () => {
|
|
28
|
+
obj[method] = original;
|
|
29
|
+
};
|
|
30
|
+
return mockFn;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Utilities for creating mock functions and method spies. Mirrors the names
|
|
34
|
+
* on Node.js's built-in `MockTracker` from `node:test`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Standalone mock
|
|
38
|
+
* const fn = mock.fn((x: number) => x * 2)
|
|
39
|
+
* fn(3)
|
|
40
|
+
* assert.equal(fn.mock.calls[0].result, 6)
|
|
41
|
+
*
|
|
42
|
+
* // Mock an existing method
|
|
43
|
+
* const spy = mock.method(console, 'log')
|
|
44
|
+
* console.log('hello')
|
|
45
|
+
* assert.equal(spy.mock.calls.length, 1)
|
|
46
|
+
* spy.mock.restore?.()
|
|
47
|
+
*/
|
|
48
|
+
export const mock = {
|
|
49
|
+
/**
|
|
50
|
+
* Creates a mock function that records every call. If `impl` is provided it
|
|
51
|
+
* is used as the underlying implementation; otherwise the mock returns
|
|
52
|
+
* `undefined`.
|
|
53
|
+
*/
|
|
54
|
+
fn: createMockFn,
|
|
55
|
+
/**
|
|
56
|
+
* Replaces `obj[methodName]` with a mock and records every call. The
|
|
57
|
+
* original method is used as the implementation unless `impl` is provided.
|
|
58
|
+
* Call `mockFn.mock.restore()` to revert.
|
|
59
|
+
*/
|
|
60
|
+
method: createMethodMock,
|
|
61
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BrowserContextOptions, LaunchOptions } from 'playwright';
|
|
2
|
+
import type { PlaywrightTestConfig } from 'playwright/test';
|
|
3
|
+
export type PlaywrightUseOpts = PlaywrightTestConfig['use'];
|
|
4
|
+
export declare function loadPlaywrightConfig(input: string | undefined): Promise<PlaywrightTestConfig | undefined>;
|
|
5
|
+
export declare function getBrowserLauncher(playwrightUseOpts?: PlaywrightUseOpts): import("playwright").BrowserType<{}>;
|
|
6
|
+
export declare function resolveProjects(config?: PlaywrightTestConfig): Array<{
|
|
7
|
+
name?: string;
|
|
8
|
+
playwrightUseOpts: PlaywrightUseOpts;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function getPlaywrightLaunchOptions(playwrightUseOpts?: PlaywrightUseOpts): LaunchOptions;
|
|
11
|
+
export declare function getPlaywrightPageOptions(playwrightUseOpts?: PlaywrightUseOpts): BrowserContextOptions & {
|
|
12
|
+
navigationTimeout?: number;
|
|
13
|
+
actionTimeout?: number;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=playwright.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../src/lib/playwright.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAG3D,MAAM,MAAM,iBAAiB,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAA;AAE3D,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAiB3C;AAQD,wBAAgB,kBAAkB,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,wCAavE;AAED,wBAAgB,eAAe,CAC7B,MAAM,CAAC,EAAE,oBAAoB,GAC5B,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAahE;AAED,wBAAgB,0BAA0B,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,aAAa,CAK/F;AAED,wBAAgB,wBAAwB,CACtC,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,qBAAqB,GAAG;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAwBhF"}
|