@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,152 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ import * as fsp from 'node:fs/promises';
4
+ import * as util from 'node:util';
5
+ import { tsImport } from 'tsx/esm/api';
6
+ // prettier-ignore
7
+ // Note: `description` is not a field used by parseArgs(), it's an additional field
8
+ // we use for `--help`
9
+ const cliOptions = {
10
+ 'browser.echo': {
11
+ type: 'boolean',
12
+ description: 'Echo browser console output to stdout',
13
+ },
14
+ 'browser.open': {
15
+ type: 'boolean',
16
+ description: 'Open browser window and keep open after tests finish',
17
+ },
18
+ 'glob.e2e': {
19
+ type: 'string',
20
+ description: 'Glob pattern for E2E test files',
21
+ },
22
+ 'glob.test': {
23
+ type: 'string',
24
+ description: 'Glob pattern for all test files',
25
+ },
26
+ concurrency: {
27
+ type: 'string',
28
+ short: 'c',
29
+ description: 'Max number of concurrent test workers (default: os.availableParallelism())',
30
+ },
31
+ config: {
32
+ type: 'string',
33
+ description: 'Path to config file (default: remix-test.config.ts)',
34
+ },
35
+ setup: {
36
+ type: 'string',
37
+ short: 's',
38
+ description: 'Path to a setup module exporting globalSetup/globalTeardown',
39
+ },
40
+ playwrightConfig: {
41
+ type: 'string',
42
+ description: 'Path to a Playwright config file',
43
+ },
44
+ project: {
45
+ type: 'string',
46
+ short: 'p',
47
+ description: 'Filter to a specific Playwright project (comma-separated)',
48
+ },
49
+ reporter: {
50
+ type: 'string',
51
+ short: 'r',
52
+ description: 'Test reporter: spec, files, tap, dot (default: spec)',
53
+ },
54
+ type: {
55
+ type: 'string',
56
+ short: 't',
57
+ description: 'Comma-separated test types to run (default: server,e2e)',
58
+ },
59
+ watch: {
60
+ type: 'boolean',
61
+ short: 'w',
62
+ description: 'Re-run tests on file changes',
63
+ },
64
+ };
65
+ const defaultValues = {
66
+ browser: {
67
+ echo: false,
68
+ open: false,
69
+ },
70
+ concurrency: os.availableParallelism(),
71
+ glob: {
72
+ test: '**/*.test?(.e2e).{ts,tsx}',
73
+ e2e: '**/*.test.e2e.{ts,tsx}',
74
+ },
75
+ reporter: process.env.CI === 'true' ? 'dot' : 'spec',
76
+ type: 'server,e2e',
77
+ setup: undefined,
78
+ playwrightConfig: undefined,
79
+ project: undefined,
80
+ watch: false,
81
+ };
82
+ export async function loadConfig() {
83
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
84
+ console.log(generateHelp());
85
+ process.exit(0);
86
+ }
87
+ let parsed = parseCliArgs();
88
+ let fileConfig = await loadConfigFile(parsed.values.config);
89
+ let config = resolveConfig(fileConfig, parsed);
90
+ return config;
91
+ }
92
+ function generateHelp() {
93
+ let lines = [
94
+ 'Usage: remix-test [glob] [options]',
95
+ '',
96
+ 'Arguments:',
97
+ ` glob Glob pattern for test files (default: "${defaultValues.glob.test}")`,
98
+ '',
99
+ 'Options:',
100
+ ];
101
+ for (let [long, opt] of Object.entries(cliOptions)) {
102
+ let short = 'short' in opt ? `/-${opt.short}` : '';
103
+ let label = opt.type === 'string' ? `--${long}${short} <value>` : `--${long}${short}`;
104
+ lines.push(` ${label.padEnd(30)} ${opt.description}`);
105
+ }
106
+ lines.push(` ${'-h, --help'.padEnd(30)} Show this help message`);
107
+ return lines.join('\n');
108
+ }
109
+ function parseCliArgs(args = process.argv.slice(2)) {
110
+ return util.parseArgs({ args, options: cliOptions, allowPositionals: true });
111
+ }
112
+ function resolveConfig(fileConfig, { values: cliValues, positionals }) {
113
+ return {
114
+ glob: {
115
+ test: positionals[0] ??
116
+ cliValues['glob.test'] ??
117
+ fileConfig.glob?.test ??
118
+ defaultValues.glob.test,
119
+ e2e: cliValues['glob.e2e'] ?? fileConfig.glob?.e2e ?? defaultValues.glob.e2e,
120
+ },
121
+ browser: {
122
+ echo: cliValues['browser.echo'] ?? fileConfig.browser?.echo ?? defaultValues.browser.echo,
123
+ open: cliValues['browser.open'] ?? fileConfig.browser?.open ?? defaultValues.browser.open,
124
+ },
125
+ concurrency: Number(cliValues.concurrency ?? fileConfig.concurrency ?? defaultValues.concurrency),
126
+ setup: cliValues.setup ?? fileConfig.setup ?? defaultValues.setup,
127
+ playwrightConfig: cliValues.playwrightConfig ?? fileConfig.playwrightConfig ?? defaultValues.playwrightConfig,
128
+ project: cliValues.project ?? fileConfig.project ?? defaultValues.project,
129
+ reporter: cliValues.reporter ?? fileConfig.reporter ?? defaultValues.reporter,
130
+ type: cliValues.type ?? fileConfig.type ?? defaultValues.type,
131
+ watch: cliValues.watch ?? fileConfig.watch ?? defaultValues.watch,
132
+ };
133
+ }
134
+ async function loadConfigFile(configPath) {
135
+ let candidates = configPath
136
+ ? [path.resolve(process.cwd(), configPath)]
137
+ : [
138
+ path.join(process.cwd(), 'remix-test.config.ts'),
139
+ path.join(process.cwd(), 'remix-test.config.js'),
140
+ ];
141
+ for (let candidate of candidates) {
142
+ try {
143
+ await fsp.access(candidate);
144
+ let mod = await tsImport(candidate, { parentURL: import.meta.url });
145
+ return mod.default ?? mod;
146
+ }
147
+ catch {
148
+ // not found or failed to load — try next
149
+ }
150
+ }
151
+ return {};
152
+ }
@@ -0,0 +1,69 @@
1
+ import type { Browser, Page } from 'playwright';
2
+ import { type MockFunction, type MockCall, type MockContext } from './mock.ts';
3
+ import type { CreateServerFunction } from './e2e-server.ts';
4
+ import type { getPlaywrightPageOptions } from './playwright.ts';
5
+ /**
6
+ * Test Context providing utilities for testing via remix-test. The context is
7
+ * passed as the first argument to the {@link test}/{@link it} functions.
8
+ *
9
+ * @example
10
+ * describe('my test suite', () => {
11
+ * it('my test case', async (t) => {
12
+ * let mockFn = t.mock.fn(() => 'mocked value')
13
+ * // ...
14
+ * })
15
+ * })
16
+ */
17
+ export interface TestContext {
18
+ /**
19
+ * Registers a cleanup function to be called after the test completes.
20
+ *
21
+ * @param {() => void} fn - The cleanup function to execute
22
+ * @returns {void}
23
+ */
24
+ after(fn: () => void): void;
25
+ /**
26
+ * Mock tracker for the current test. Mirrors the shape of Node's
27
+ * `t.mock`. Method mocks created here are auto-restored on test completion.
28
+ */
29
+ mock: {
30
+ /**
31
+ * Creates a mock function with an optional implementation.
32
+ *
33
+ * @template T - The function type to be mocked
34
+ * @param {T} [impl] - Optional custom implementation for the mock
35
+ * @returns {MockFunction<T>} A mock function instance
36
+ */
37
+ fn<T extends (...args: any[]) => any>(impl?: T): MockFunction<T>;
38
+ /**
39
+ * Replaces `obj[methodName]` with a mock and records every call. The
40
+ * original method is restored automatically after the test completes.
41
+ *
42
+ * @template T - The object type
43
+ * @template K - The method key of the object
44
+ * @param {T} obj - The object to mock
45
+ * @param {K} methodName - The method name to mock
46
+ * @param {Function} [impl] - Optional implementation override (must be a function)
47
+ * @returns {MockFunction} A mock function instance for the mocked method
48
+ */
49
+ method<T extends object, K extends keyof T>(obj: T, methodName: K, impl?: Function): MockFunction;
50
+ };
51
+ /**
52
+ * Starts a test server with the provided request handler.
53
+ *
54
+ * @param {(req: Request) => Promise<Response>} handler - Function handling incoming requests
55
+ * @returns {Promise<Page>} A promise resolving to a page instance for the server
56
+ */
57
+ serve(handler: (req: Request) => Promise<Response>): Promise<Page>;
58
+ }
59
+ export declare function createTestContext(options: {
60
+ createServer?: CreateServerFunction;
61
+ browser?: Browser;
62
+ open?: boolean;
63
+ playwrightPageOptions?: ReturnType<typeof getPlaywrightPageOptions>;
64
+ }): {
65
+ testContext: TestContext;
66
+ cleanup(): Promise<void>;
67
+ };
68
+ export type { MockFunction, MockCall, MockContext };
69
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/lib/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAQ,KAAK,YAAY,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAEpF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAE3B;;;OAGG;IACH,IAAI,EAAE;QACJ;;;;;;WAMG;QACH,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;QAEhE;;;;;;;;;;WAUG;QACH,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EACxC,GAAG,EAAE,CAAC,EACN,UAAU,EAAE,CAAC,EACb,IAAI,CAAC,EAAE,QAAQ,GACd,YAAY,CAAA;KAChB,CAAA;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IACzC,YAAY,CAAC,EAAE,oBAAoB,CAAA;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,qBAAqB,CAAC,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAA;CACpE,GAAG;IAAE,WAAW,EAAE,WAAW,CAAC;IAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAkDzD;AAED,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAA"}
@@ -0,0 +1,49 @@
1
+ import { mock } from "./mock.js";
2
+ export function createTestContext(options) {
3
+ let cleanups = [];
4
+ let testContext = {
5
+ mock: {
6
+ fn: mock.fn,
7
+ method(obj, methodName, impl) {
8
+ let mockFn = mock.method(obj, methodName, impl);
9
+ if (mockFn.mock.restore)
10
+ cleanups.push(mockFn.mock.restore);
11
+ return mockFn;
12
+ },
13
+ },
14
+ after(fn) {
15
+ cleanups.push(fn);
16
+ },
17
+ async serve(handler) {
18
+ if (!options.createServer || !options.browser) {
19
+ throw new Error('t.serve() is only available in E2E test suites');
20
+ }
21
+ let server = await options.createServer(handler);
22
+ let page = await options.browser.newPage({
23
+ ...options.playwrightPageOptions,
24
+ baseURL: server.baseUrl,
25
+ });
26
+ if (options.playwrightPageOptions?.navigationTimeout != null) {
27
+ page.setDefaultNavigationTimeout(options.playwrightPageOptions.navigationTimeout);
28
+ }
29
+ if (options.playwrightPageOptions?.actionTimeout != null) {
30
+ page.setDefaultTimeout(options.playwrightPageOptions.actionTimeout);
31
+ }
32
+ cleanups.push(async () => {
33
+ if (!options.open) {
34
+ await page.close();
35
+ }
36
+ await server.close();
37
+ });
38
+ return page;
39
+ },
40
+ };
41
+ return {
42
+ testContext,
43
+ async cleanup() {
44
+ for (let fn of cleanups)
45
+ await fn();
46
+ cleanups.length = 0;
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,11 @@
1
+ export interface CreateServerFunction {
2
+ (handler: (req: Request) => Promise<Response>): Promise<{
3
+ baseUrl: string;
4
+ close(): Promise<void>;
5
+ }>;
6
+ }
7
+ export declare function createServer(handler: (req: Request) => Promise<Response>): Promise<{
8
+ baseUrl: string;
9
+ close(): Promise<void>;
10
+ }>;
11
+ //# sourceMappingURL=e2e-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e-server.d.ts","sourceRoot":"","sources":["../../src/lib/e2e-server.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,oBAAoB;IACnC,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC;QACtD,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KACvB,CAAC,CAAA;CACH;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC;IAClF,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAC,CAcD"}
@@ -0,0 +1,15 @@
1
+ import * as http from 'node:http';
2
+ import { createRequestListener } from '@remix-run/node-fetch-server';
3
+ export function createServer(handler) {
4
+ return new Promise((resolve, reject) => {
5
+ let server = http.createServer(createRequestListener(handler));
6
+ server.listen(0, '127.0.0.1', () => {
7
+ let addr = server.address();
8
+ resolve({
9
+ baseUrl: `http://127.0.0.1:${addr.port}`,
10
+ close: () => new Promise((r, rj) => server.close((e) => (e ? rj(e) : r()))),
11
+ });
12
+ });
13
+ server.on('error', reject);
14
+ });
15
+ }
@@ -0,0 +1,27 @@
1
+ import type { Browser, BrowserContextOptions } from 'playwright';
2
+ import type { CreateServerFunction } from './e2e-server.ts';
3
+ export interface TestResult {
4
+ name: string;
5
+ suiteName: string;
6
+ filePath?: string;
7
+ status: 'passed' | 'failed' | 'skipped' | 'todo';
8
+ error?: {
9
+ message: string;
10
+ stack?: string;
11
+ };
12
+ duration: number;
13
+ }
14
+ export interface TestResults {
15
+ passed: number;
16
+ failed: number;
17
+ skipped: number;
18
+ todo: number;
19
+ tests: TestResult[];
20
+ }
21
+ export declare function runTests(options?: {
22
+ createServer?: CreateServerFunction;
23
+ browser?: Browser;
24
+ open?: boolean;
25
+ playwrightPageOptions?: BrowserContextOptions;
26
+ }): Promise<TestResults>;
27
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/lib/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEhE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IAChD,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,UAAU,EAAE,CAAA;CACpB;AAED,wBAAsB,QAAQ,CAAC,OAAO,CAAC,EAAE;IACvC,YAAY,CAAC,EAAE,oBAAoB,CAAA;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,qBAAqB,CAAC,EAAE,qBAAqB,CAAA;CAC9C,GAAG,OAAO,CAAC,WAAW,CAAC,CAoIvB"}
@@ -0,0 +1,123 @@
1
+ import { createTestContext } from "./context.js";
2
+ export async function runTests(options) {
3
+ let suites = globalThis.__testSuites || [];
4
+ let results = {
5
+ passed: 0,
6
+ failed: 0,
7
+ skipped: 0,
8
+ todo: 0,
9
+ tests: [],
10
+ };
11
+ let hasOnlySuites = suites.some((s) => s.only);
12
+ for (let suite of suites) {
13
+ // If any suite uses .only, skip all non-only suites
14
+ if (hasOnlySuites && !suite.only) {
15
+ for (let test of suite.tests) {
16
+ results.tests.push({
17
+ name: test.name,
18
+ suiteName: suite.name,
19
+ status: 'skipped',
20
+ duration: 0,
21
+ });
22
+ results.skipped++;
23
+ }
24
+ continue;
25
+ }
26
+ if (suite.skip || suite.todo) {
27
+ let status = suite.todo ? 'todo' : 'skipped';
28
+ for (let test of suite.tests) {
29
+ results.tests.push({ name: test.name, suiteName: suite.name, status, duration: 0 });
30
+ results[status]++;
31
+ }
32
+ // describe.todo('name') with no tests — add placeholder so suite appears in output
33
+ if (suite.tests.length === 0) {
34
+ results.tests.push({ name: '', suiteName: suite.name, status, duration: 0 });
35
+ results[status]++;
36
+ }
37
+ continue;
38
+ }
39
+ if (suite.beforeAll) {
40
+ try {
41
+ await suite.beforeAll();
42
+ }
43
+ catch (error) {
44
+ console.error(`beforeAll failed in suite "${suite.name}":`, error);
45
+ continue;
46
+ }
47
+ }
48
+ let hasOnlyTests = suite.tests.some((t) => t.only);
49
+ for (let test of suite.tests) {
50
+ // If any test uses .only, skip all non-only tests in this suite
51
+ if (hasOnlyTests && !test.only) {
52
+ results.tests.push({
53
+ name: test.name,
54
+ suiteName: suite.name,
55
+ status: 'skipped',
56
+ duration: 0,
57
+ });
58
+ results.skipped++;
59
+ continue;
60
+ }
61
+ if (test.skip || test.todo) {
62
+ let status = test.todo ? 'todo' : 'skipped';
63
+ results.tests.push({ name: test.name, suiteName: suite.name, status, duration: 0 });
64
+ results[status]++;
65
+ continue;
66
+ }
67
+ let startTime = performance.now();
68
+ let result = {
69
+ name: test.name,
70
+ suiteName: suite.name,
71
+ status: 'passed',
72
+ duration: 0,
73
+ };
74
+ let { testContext, cleanup } = createTestContext({
75
+ createServer: options?.createServer,
76
+ browser: options?.browser,
77
+ open: options?.open,
78
+ playwrightPageOptions: options?.playwrightPageOptions,
79
+ });
80
+ try {
81
+ if (suite.beforeEach) {
82
+ await suite.beforeEach();
83
+ }
84
+ await test.fn(testContext);
85
+ result.status = 'passed';
86
+ results.passed++;
87
+ }
88
+ catch (error) {
89
+ result.status = 'failed';
90
+ result.error = {
91
+ message: error.message || String(error),
92
+ stack: error.stack,
93
+ };
94
+ results.failed++;
95
+ }
96
+ finally {
97
+ await cleanup();
98
+ if (suite.afterEach) {
99
+ try {
100
+ await suite.afterEach();
101
+ }
102
+ catch (error) {
103
+ console.error('afterEach failed:', error);
104
+ }
105
+ }
106
+ result.duration = performance.now() - startTime;
107
+ results.tests.push(result);
108
+ }
109
+ }
110
+ if (suite.afterAll) {
111
+ try {
112
+ await suite.afterAll();
113
+ }
114
+ catch (error) {
115
+ console.error(`afterAll failed in suite "${suite.name}":`, error);
116
+ }
117
+ }
118
+ }
119
+ // Clear suites in-place so the shared framework module is reset
120
+ // for the next test file (which reuses the same cached module instance)
121
+ suites.length = 0;
122
+ return results;
123
+ }
@@ -0,0 +1,107 @@
1
+ import type { TestContext } from './context.ts';
2
+ /**
3
+ * Groups related tests into a named suite. Suites can be nested snd will be displayed
4
+ * as such or joined with ` > ` in reporter output. Lifecycle hooks registered inside
5
+ * a `describe` block apply only to tests within that block.
6
+ *
7
+ * @example
8
+ * describe('auth', () => {
9
+ * it('logs in', async () => { ... })
10
+ * })
11
+ *
12
+ * // Modifiers
13
+ * describe.skip('skipped suite', () => { ... })
14
+ * describe.only('focused suite', () => { ... })
15
+ * describe.todo('planned suite')
16
+ *
17
+ * @param name - The suite name shown in reporter output.
18
+ * @param fn - A function that registers the tests and lifecycle hooks in this suite.
19
+ */
20
+ export declare const describe: ((name: string, metaOrFn: SuiteMeta | (() => void), fn?: (() => void) | undefined) => void) & {
21
+ skip: (name: string, fn: () => void) => void;
22
+ only: (name: string, fn: () => void) => void;
23
+ todo: (name: string) => void;
24
+ };
25
+ type SuiteMeta = {
26
+ skip?: boolean;
27
+ only?: boolean;
28
+ };
29
+ type TestMeta = {
30
+ skip?: boolean;
31
+ only?: boolean;
32
+ };
33
+ type TestFn = (t: TestContext) => void | Promise<void>;
34
+ /**
35
+ * Defines a single test case. The optional `TestContext` argument `t` provides
36
+ * mock helpers and per-test cleanup registration.
37
+ *
38
+ * @example
39
+ * it('returns 200 for the home route', async () => {
40
+ * const res = await router.fetch('/')
41
+ * assert.equal(res.status, 200)
42
+ * })
43
+ *
44
+ * // Modifiers
45
+ * it.skip('not ready yet', () => { ... })
46
+ * it.only('focused test', () => { ... })
47
+ * it.todo('coming soon')
48
+ *
49
+ * @param name - The test name shown in reporter output.
50
+ * @param fn - The test body, receiving a {@link TestContext} as its first argument.
51
+ */
52
+ export declare const it: ((name: string, metaOrFn: TestFn | TestMeta, fn?: TestFn | undefined) => void) & {
53
+ skip: (name: string, fn?: TestFn | undefined) => void;
54
+ only: (name: string, fn: TestFn) => void;
55
+ todo: (name: string) => void;
56
+ };
57
+ /** Alias for {@link describe}. */
58
+ export declare const suite: ((name: string, metaOrFn: SuiteMeta | (() => void), fn?: (() => void) | undefined) => void) & {
59
+ skip: (name: string, fn: () => void) => void;
60
+ only: (name: string, fn: () => void) => void;
61
+ todo: (name: string) => void;
62
+ };
63
+ /** Alias for {@link it}. */
64
+ export declare const test: ((name: string, metaOrFn: TestFn | TestMeta, fn?: TestFn | undefined) => void) & {
65
+ skip: (name: string, fn?: TestFn | undefined) => void;
66
+ only: (name: string, fn: TestFn) => void;
67
+ todo: (name: string) => void;
68
+ };
69
+ /**
70
+ * Registers a hook that runs before **each** test in the current suite (or
71
+ * globally if called outside a `describe`). Multiple calls are chained in
72
+ * registration order.
73
+ *
74
+ * @param fn - The setup function to run before each test.
75
+ */
76
+ export declare function beforeEach(fn: () => void | Promise<void>): void;
77
+ /**
78
+ * Registers a hook that runs after **each** test in the current suite (or
79
+ * globally if called outside a `describe`). Multiple calls are chained in
80
+ * reverse registration order. To run logic after a singular test, use
81
+ * `t.after()` from the {@link TestContext}
82
+ *
83
+ * @param fn - The teardown function to run after each test.
84
+ */
85
+ export declare function afterEach(fn: () => void | Promise<void>): void;
86
+ /**
87
+ * Registers a hook that runs once before **all** tests in the current suite
88
+ * (or globally if called outside a `describe`). Multiple calls are chained in
89
+ * registration order.
90
+ *
91
+ * @param fn - The setup function to run once before all tests in the suite.
92
+ */
93
+ export declare function beforeAll(fn: () => void | Promise<void>): void;
94
+ /**
95
+ * Registers a hook that runs once after **all** tests in the current suite (or
96
+ * globally if called outside a `describe`). Multiple calls are chained in
97
+ * reverse registration order.
98
+ *
99
+ * @param fn - The teardown function to run once after all tests in the suite.
100
+ */
101
+ export declare function afterAll(fn: () => void | Promise<void>): void;
102
+ /** Alias for {@link beforeAll} — matches the `node:test` API. */
103
+ export declare const before: typeof beforeAll;
104
+ /** Alias for {@link afterAll} — matches the `node:test` API. */
105
+ export declare const after: typeof afterAll;
106
+ export {};
107
+ //# sourceMappingURL=framework.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework.d.ts","sourceRoot":"","sources":["../../src/lib/framework.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAkF/C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ;;;;CAiBpB,CAAA;AAED,KAAK,SAAS,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AACnD,KAAK,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAClD,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAUtD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,EAAE;;;;CAiBd,CAAA;AAED,kCAAkC;AAClC,eAAO,MAAM,KAAK;;;;CAAW,CAAA;AAC7B,4BAA4B;AAC5B,eAAO,MAAM,IAAI;;;;CAAK,CAAA;AA2BtB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGxD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGtD;AAED,mEAAiE;AACjE,eAAO,MAAM,MAAM,kBAAY,CAAA;AAC/B,kEAAgE;AAChE,eAAO,MAAM,KAAK,iBAAW,CAAA"}