@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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -2
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +171 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +2 -0
  9. package/dist/lib/config.d.ts +60 -0
  10. package/dist/lib/config.d.ts.map +1 -0
  11. package/dist/lib/config.js +152 -0
  12. package/dist/lib/context.d.ts +69 -0
  13. package/dist/lib/context.d.ts.map +1 -0
  14. package/dist/lib/context.js +49 -0
  15. package/dist/lib/e2e-server.d.ts +11 -0
  16. package/dist/lib/e2e-server.d.ts.map +1 -0
  17. package/dist/lib/e2e-server.js +15 -0
  18. package/dist/lib/executor.d.ts +27 -0
  19. package/dist/lib/executor.d.ts.map +1 -0
  20. package/dist/lib/executor.js +123 -0
  21. package/dist/lib/framework.d.ts +107 -0
  22. package/dist/lib/framework.d.ts.map +1 -0
  23. package/dist/lib/framework.js +198 -0
  24. package/dist/lib/framework.test.d.ts +2 -0
  25. package/dist/lib/framework.test.d.ts.map +1 -0
  26. package/dist/lib/framework.test.e2e.d.ts +2 -0
  27. package/dist/lib/framework.test.e2e.d.ts.map +1 -0
  28. package/dist/lib/framework.test.e2e.js +29 -0
  29. package/dist/lib/framework.test.js +283 -0
  30. package/dist/lib/mock.d.ts +52 -0
  31. package/dist/lib/mock.d.ts.map +1 -0
  32. package/dist/lib/mock.js +61 -0
  33. package/dist/lib/playwright.d.ts +15 -0
  34. package/dist/lib/playwright.d.ts.map +1 -0
  35. package/dist/lib/playwright.js +84 -0
  36. package/dist/lib/reporters/dot.d.ts +10 -0
  37. package/dist/lib/reporters/dot.d.ts.map +1 -0
  38. package/dist/lib/reporters/dot.js +55 -0
  39. package/dist/lib/reporters/files.d.ts +10 -0
  40. package/dist/lib/reporters/files.d.ts.map +1 -0
  41. package/dist/lib/reporters/files.js +70 -0
  42. package/dist/lib/reporters/index.d.ts +14 -0
  43. package/dist/lib/reporters/index.d.ts.map +1 -0
  44. package/dist/lib/reporters/index.js +18 -0
  45. package/dist/lib/reporters/spec.d.ts +10 -0
  46. package/dist/lib/reporters/spec.d.ts.map +1 -0
  47. package/dist/lib/reporters/spec.js +152 -0
  48. package/dist/lib/reporters/tap.d.ts +10 -0
  49. package/dist/lib/reporters/tap.d.ts.map +1 -0
  50. package/dist/lib/reporters/tap.js +54 -0
  51. package/dist/lib/runner.d.ts +9 -0
  52. package/dist/lib/runner.d.ts.map +1 -0
  53. package/dist/lib/runner.js +89 -0
  54. package/dist/lib/utils.d.ts +16 -0
  55. package/dist/lib/utils.d.ts.map +1 -0
  56. package/dist/lib/utils.js +27 -0
  57. package/dist/lib/watcher.d.ts +5 -0
  58. package/dist/lib/watcher.d.ts.map +1 -0
  59. package/dist/lib/watcher.js +39 -0
  60. package/dist/lib/worker-e2e.d.ts +2 -0
  61. package/dist/lib/worker-e2e.d.ts.map +1 -0
  62. package/dist/lib/worker-e2e.js +48 -0
  63. package/dist/lib/worker.d.ts +2 -0
  64. package/dist/lib/worker.d.ts.map +1 -0
  65. package/dist/lib/worker.js +29 -0
  66. package/package.json +58 -5
  67. package/src/cli.ts +210 -0
  68. package/src/index.ts +15 -0
  69. package/src/lib/config.ts +231 -0
  70. package/src/lib/context.ts +126 -0
  71. package/src/lib/e2e-server.ts +28 -0
  72. package/src/lib/executor.ts +162 -0
  73. package/src/lib/framework.ts +251 -0
  74. package/src/lib/mock.ts +89 -0
  75. package/src/lib/playwright.ts +102 -0
  76. package/src/lib/reporters/dot.ts +57 -0
  77. package/src/lib/reporters/files.ts +76 -0
  78. package/src/lib/reporters/index.ts +28 -0
  79. package/src/lib/reporters/spec.ts +173 -0
  80. package/src/lib/reporters/tap.ts +58 -0
  81. package/src/lib/runner.ts +137 -0
  82. package/src/lib/utils.ts +40 -0
  83. package/src/lib/watcher.ts +46 -0
  84. package/src/lib/worker-e2e.ts +52 -0
  85. package/src/lib/worker.ts +30 -0
  86. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=framework.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework.test.d.ts","sourceRoot":"","sources":["../../src/lib/framework.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=framework.test.e2e.d.ts.map
@@ -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"}
@@ -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"}