@unrift/core 0.0.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/bin/unrift.mjs +5 -0
  3. package/dist/cjs/index.cjs +31 -0
  4. package/dist/esm/cli.d.ts +3 -0
  5. package/dist/esm/cli.d.ts.map +1 -0
  6. package/dist/esm/cli.js +87 -0
  7. package/dist/esm/context.d.ts +15 -0
  8. package/dist/esm/context.d.ts.map +1 -0
  9. package/dist/esm/context.js +16 -0
  10. package/dist/esm/expect.d.ts +27 -0
  11. package/dist/esm/expect.d.ts.map +1 -0
  12. package/dist/esm/expect.js +43 -0
  13. package/dist/esm/hooks.d.ts +8 -0
  14. package/dist/esm/hooks.d.ts.map +1 -0
  15. package/dist/esm/hooks.js +18 -0
  16. package/dist/esm/index.d.ts +7 -0
  17. package/dist/esm/index.d.ts.map +1 -0
  18. package/dist/esm/index.js +4 -0
  19. package/dist/esm/injected/globals.d.ts +4 -0
  20. package/dist/esm/injected/globals.d.ts.map +1 -0
  21. package/dist/esm/injected/globals.js +6 -0
  22. package/dist/esm/interface.d.ts +18 -0
  23. package/dist/esm/interface.d.ts.map +1 -0
  24. package/dist/esm/interface.js +29 -0
  25. package/dist/esm/matchers.d.ts +2 -0
  26. package/dist/esm/matchers.d.ts.map +1 -0
  27. package/dist/esm/matchers.js +81 -0
  28. package/dist/esm/run.d.ts +21 -0
  29. package/dist/esm/run.d.ts.map +1 -0
  30. package/dist/esm/run.js +57 -0
  31. package/dist/esm/runner.d.ts +14 -0
  32. package/dist/esm/runner.d.ts.map +1 -0
  33. package/dist/esm/runner.js +207 -0
  34. package/dist/esm/suite.d.ts +58 -0
  35. package/dist/esm/suite.d.ts.map +1 -0
  36. package/dist/esm/suite.js +281 -0
  37. package/dist/esm/test.d.ts +18 -0
  38. package/dist/esm/test.d.ts.map +1 -0
  39. package/dist/esm/test.js +52 -0
  40. package/dist/esm/types.d.ts +24 -0
  41. package/dist/esm/types.d.ts.map +1 -0
  42. package/dist/esm/types.js +16 -0
  43. package/dist/esm/utils/colors.d.ts +7 -0
  44. package/dist/esm/utils/colors.d.ts.map +1 -0
  45. package/dist/esm/utils/colors.js +12 -0
  46. package/dist/esm/utils/deepEqual.d.ts +9 -0
  47. package/dist/esm/utils/deepEqual.d.ts.map +1 -0
  48. package/dist/esm/utils/deepEqual.js +201 -0
  49. package/dist/esm/utils/defineConfig.d.ts +12 -0
  50. package/dist/esm/utils/defineConfig.d.ts.map +1 -0
  51. package/dist/esm/utils/defineConfig.js +3 -0
  52. package/dist/esm/utils/discoverTestFiles.d.ts +3 -0
  53. package/dist/esm/utils/discoverTestFiles.d.ts.map +1 -0
  54. package/dist/esm/utils/discoverTestFiles.js +22 -0
  55. package/dist/esm/utils/helpers.d.ts +13 -0
  56. package/dist/esm/utils/helpers.d.ts.map +1 -0
  57. package/dist/esm/utils/helpers.js +29 -0
  58. package/dist/esm/utils/loadConfig.d.ts +9 -0
  59. package/dist/esm/utils/loadConfig.d.ts.map +1 -0
  60. package/dist/esm/utils/loadConfig.js +89 -0
  61. package/dist/esm/utils/looseEqual.d.ts +2 -0
  62. package/dist/esm/utils/looseEqual.d.ts.map +1 -0
  63. package/dist/esm/utils/looseEqual.js +205 -0
  64. package/dist/esm/utils/toThrow.d.ts +11 -0
  65. package/dist/esm/utils/toThrow.d.ts.map +1 -0
  66. package/dist/esm/utils/toThrow.js +93 -0
  67. package/dist/esm/utils/transform.d.ts +10 -0
  68. package/dist/esm/utils/transform.d.ts.map +1 -0
  69. package/dist/esm/utils/transform.js +213 -0
  70. package/package.json +60 -0
@@ -0,0 +1,207 @@
1
+ import { relative, resolve } from "node:path";
2
+ import { loadConfig, loadConfigFromPath, } from "./utils/loadConfig.js";
3
+ import { cleanUnriftCaches } from "./utils/transform.js";
4
+ import { colors } from "./utils/colors.js";
5
+ import { discoverTestFiles } from "./utils/discoverTestFiles.js";
6
+ import { runEngine } from "./run.js";
7
+ import { TaskStatus } from "./types.js";
8
+ function normalizePath(p) {
9
+ // stable for matching/reporting across platforms
10
+ return p.replace(/\\/g, "/");
11
+ }
12
+ function debugLog(enabled, json, ...args) {
13
+ // Keep stdout clean for --json mode; use stderr for debug.
14
+ if (!enabled)
15
+ return;
16
+ (json ? console.error : console.log)(...args);
17
+ }
18
+ function safeRegExp(source) {
19
+ try {
20
+ return new RegExp(source);
21
+ }
22
+ catch (err) {
23
+ const msg = err instanceof Error ? err.message : String(err);
24
+ throw new Error(`Invalid regex "${source}": ${msg}`);
25
+ }
26
+ }
27
+ /**
28
+ * @TODO Add glob support
29
+ */
30
+ function matchesAnyPattern(fileAbs, patterns, baseDir) {
31
+ const absolutePath = normalizePath(fileAbs);
32
+ const relativePath = normalizePath(relative(baseDir, fileAbs));
33
+ return patterns.some((pattern) => {
34
+ const re = safeRegExp(pattern);
35
+ return re.test(relativePath) || re.test(absolutePath);
36
+ });
37
+ }
38
+ function filterByIncludesExcludes(files, baseDir, includes, excludes) {
39
+ let filtered = files;
40
+ if (includes && includes.length > 0) {
41
+ filtered = filtered.filter((file) => matchesAnyPattern(file, includes, baseDir));
42
+ }
43
+ if (excludes && excludes.length > 0) {
44
+ filtered = filtered.filter((file) => !matchesAnyPattern(file, excludes, baseDir));
45
+ }
46
+ return filtered;
47
+ }
48
+ function formatDuration(ms) {
49
+ if (ms < 1000)
50
+ return `${ms.toFixed(0)}ms`;
51
+ return `${(ms / 1000).toFixed(2)}s`;
52
+ }
53
+ function padRight(str, width) {
54
+ return str.length >= width ? str : str + " ".repeat(width - str.length);
55
+ }
56
+ function getFileHeadingFromDescription(description) {
57
+ const index = description.indexOf("›");
58
+ if (index === -1)
59
+ return null;
60
+ return description.slice(0, index).trim();
61
+ }
62
+ export async function runTestsCLI(options = {}) {
63
+ const runStart = performance.now();
64
+ // Cache clean mode
65
+ if (options.cacheClean) {
66
+ const projectRoot = process.cwd();
67
+ const result = cleanUnriftCaches(projectRoot);
68
+ if (options.json) {
69
+ console.log(JSON.stringify({ ok: true, cleaned: result }, null, 2));
70
+ }
71
+ else {
72
+ const lines = [];
73
+ if (result.projectCacheDeleted)
74
+ lines.push("✔ deleted project cache");
75
+ if (result.tempCacheDeleted)
76
+ lines.push("✔ deleted temp cache");
77
+ if (lines.length === 0)
78
+ lines.push("— nothing to clean");
79
+ console.log(lines.join("\n"));
80
+ }
81
+ return;
82
+ }
83
+ // Load config and capture its directory so testDir can be relative to it
84
+ const loaded = options.configPath
85
+ ? await loadConfigFromPath(options.configPath)
86
+ : await loadConfig(process.cwd());
87
+ const config = loaded?.config ?? null;
88
+ const configDir = loaded?.configDir ?? process.cwd();
89
+ const resolvedConfigPath = loaded?.configPath ?? null;
90
+ debugLog(options.debug, options.json, "Using config:", {
91
+ configPath: resolvedConfigPath,
92
+ configDir,
93
+ config,
94
+ });
95
+ // Test dir is relative to config dir if config is loaded
96
+ const testDir = resolve(configDir, config?.testDir ?? options.testDir ?? "test");
97
+ let files = discoverTestFiles(testDir);
98
+ if (options.pattern) {
99
+ files = files.filter((f) => options.pattern.test(normalizePath(f)));
100
+ }
101
+ files = filterByIncludesExcludes(files, testDir, config?.includes, config?.excludes).sort((a, b) => normalizePath(a).localeCompare(normalizePath(b)));
102
+ debugLog(options.debug, options.json, "Discovered test files:", files);
103
+ // List mode
104
+ if (options.list) {
105
+ if (options.json) {
106
+ console.log(JSON.stringify({ files: files.map(normalizePath), count: files.length }, null, 2));
107
+ }
108
+ else {
109
+ for (const file of files)
110
+ console.log(file);
111
+ if (options.debug)
112
+ console.log(`\n${files.length} file(s)`);
113
+ }
114
+ return;
115
+ }
116
+ const timeoutMs = options.timeoutMs ?? config?.timeoutMs;
117
+ const bail = options.bail ?? config?.bail ?? false;
118
+ const engine = await runEngine({
119
+ files,
120
+ timeoutMs,
121
+ bail,
122
+ matchers: config?.matchers,
123
+ });
124
+ debugLog(options.debug, options.json, "Only mode:", engine.isOnly);
125
+ const results = engine.results;
126
+ let total = 0;
127
+ let passed = 0;
128
+ let failed = 0;
129
+ let skipped = 0;
130
+ const failures = [];
131
+ const serializedResults = [];
132
+ for (const result of results) {
133
+ total++;
134
+ if (result.status === TaskStatus.Pass)
135
+ passed++;
136
+ else if (result.status === TaskStatus.Skipped)
137
+ skipped++;
138
+ else if (result.status === TaskStatus.Fail)
139
+ failed++;
140
+ if (result.status === TaskStatus.Fail)
141
+ failures.push(result);
142
+ serializedResults.push({
143
+ description: result.description,
144
+ status: result.status,
145
+ durationMs: result.durationMs ?? 0,
146
+ error: result.error
147
+ ? { message: result.error.message, stack: result.error.stack }
148
+ : undefined,
149
+ });
150
+ }
151
+ const ok = failed === 0;
152
+ const durationMs = performance.now() - runStart;
153
+ // JSON mode: stdout must be JSON only
154
+ if (options.json) {
155
+ const report = {
156
+ ok,
157
+ failed,
158
+ passed,
159
+ skipped,
160
+ total,
161
+ bail,
162
+ timeoutMs,
163
+ testDir: normalizePath(testDir),
164
+ configPath: resolvedConfigPath,
165
+ files: files.map(normalizePath),
166
+ durationMs,
167
+ results: serializedResults,
168
+ };
169
+ console.log(JSON.stringify(report, null, 2));
170
+ if (!ok)
171
+ process.exitCode = 1;
172
+ return;
173
+ }
174
+ // Standard reporting
175
+ const durationColWidth = 8;
176
+ let lastHeading = null;
177
+ for (const result of results) {
178
+ const heading = getFileHeadingFromDescription(result.description);
179
+ if (heading && heading !== lastHeading) {
180
+ console.log(colors.bold(`\n${heading}`));
181
+ lastHeading = heading;
182
+ }
183
+ const duration = formatDuration(result.durationMs ?? 0);
184
+ const durationPadded = padRight(duration, durationColWidth);
185
+ if (result.status === TaskStatus.Pass) {
186
+ console.log(colors.green("✔"), durationPadded, result.description.replace(/^.*?›\s*/, ""));
187
+ continue;
188
+ }
189
+ if (result.status === TaskStatus.Skipped) {
190
+ console.log(colors.yellow("↷"), padRight("—", durationColWidth), result.description.replace(/^.*?›\s*/, ""));
191
+ continue;
192
+ }
193
+ // Fail
194
+ console.log(colors.red("✘"), durationPadded, result.description.replace(/^.*?›\s*/, ""));
195
+ }
196
+ if (failures.length > 0) {
197
+ console.log(colors.bold("\nFailures:"));
198
+ failures.forEach((r, i) => {
199
+ console.log(colors.red(`\n${i + 1}) ${r.description}`));
200
+ if (r.error)
201
+ console.log(colors.red(r.error.stack ?? r.error.message));
202
+ });
203
+ }
204
+ console.log(colors.bold(`\nPassed: ${passed} Failed: ${failed} Skipped: ${skipped} Total: ${total} Time: ${formatDuration(durationMs)}\n`));
205
+ if (!ok)
206
+ process.exitCode = 1;
207
+ }
@@ -0,0 +1,58 @@
1
+ import { Test } from "./test";
2
+ import { type PromisableFn, TaskMode, TaskStatus } from "./types";
3
+ interface RunOptions {
4
+ timeoutMs?: number;
5
+ bail?: boolean;
6
+ }
7
+ interface SuiteRunState {
8
+ bailed: boolean;
9
+ }
10
+ interface SuiteTask {
11
+ description: string;
12
+ parent?: Suite;
13
+ suites: Suite[];
14
+ tests: Test[];
15
+ addSuite(suite: Suite): void;
16
+ addTest(test: Test): void;
17
+ run(): Promise<void>;
18
+ }
19
+ export declare class Suite implements SuiteTask {
20
+ description: string;
21
+ mode: TaskMode;
22
+ parent?: Suite;
23
+ subtreeHasOnly: boolean;
24
+ root: boolean;
25
+ errors: Array<{
26
+ label: string;
27
+ error: Error;
28
+ }>;
29
+ suites: Suite[];
30
+ tests: Test[];
31
+ afterAllError?: Error;
32
+ beforeAllError?: Error;
33
+ beforeAllHooks: PromisableFn<void>[];
34
+ afterAllHooks: PromisableFn<void>[];
35
+ beforeEachHooks: PromisableFn<void>[];
36
+ afterEachHooks: PromisableFn<void>[];
37
+ constructor(description: string, parent?: Suite, mode?: TaskMode);
38
+ markSubtreeSkipped(): void;
39
+ addSuite(suite: Suite): void;
40
+ addTest(test: Test): void;
41
+ private collectBeforeEachHooks;
42
+ private collectAfterEachHooks;
43
+ run(options?: RunOptions, state?: SuiteRunState, isOnly?: boolean, ancestorOnly?: boolean): Promise<void>;
44
+ getFullDescription(): string;
45
+ reset(): void;
46
+ getResults(): Array<{
47
+ description: string;
48
+ status: TaskStatus;
49
+ error?: Error;
50
+ durationMs: number;
51
+ }>;
52
+ }
53
+ export declare function computeOnlyFlags(suite: Suite): boolean;
54
+ export declare const rootSuite: Suite;
55
+ export declare function getCurrentSuite(): Suite;
56
+ export declare function clearContext(): void;
57
+ export {};
58
+ //# sourceMappingURL=suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suite.d.ts","sourceRoot":"","sources":["../../src/suite.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,OAAO,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAElE,UAAU,UAAU;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,UAAU,SAAS;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,KAAK,EAAE,IAAI,EAAE,CAAC;IAEd,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAE1B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAsBD,qBAAa,KAAM,YAAW,SAAS;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,CAAC;IAEf,cAAc,EAAE,OAAO,CAAS;IAEhC,IAAI,EAAE,OAAO,CAAS;IACtB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAM;IACpD,MAAM,EAAE,KAAK,EAAE,CAAM;IACrB,KAAK,EAAE,IAAI,EAAE,CAAM;IAEnB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,cAAc,CAAC,EAAE,KAAK,CAAC;IAEvB,cAAc,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;IAC1C,aAAa,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;IACzC,eAAe,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;IAC3C,cAAc,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;gBAGxC,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,KAAK,EACd,IAAI,GAAE,QAA2B;IAO5B,kBAAkB;IASzB,QAAQ,CAAC,KAAK,EAAE,KAAK;IAIrB,OAAO,CAAC,IAAI,EAAE,IAAI;IAIlB,OAAO,CAAC,sBAAsB;IAiB9B,OAAO,CAAC,qBAAqB;IAYvB,GAAG,CACP,OAAO,CAAC,EAAE,UAAU,EACpB,KAAK,GAAE,aAAiC,EACxC,MAAM,GAAE,OAAe,EACvB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,IAAI,CAAC;IAwJhB,kBAAkB,IAAI,MAAM;IAK5B,KAAK;IAaL,UAAU,IAAI,KAAK,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,UAAU,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CAkDH;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAiBtD;AAED,eAAO,MAAM,SAAS,OAAoB,CAAC;AAG3C,wBAAgB,eAAe,UAE9B;AAED,wBAAgB,YAAY,SAI3B"}
@@ -0,0 +1,281 @@
1
+ import { unriftGlobalContext } from "./context.js";
2
+ import { TaskMode, TaskStatus } from "./types.js";
3
+ function appendHookFailure(existing, hookName, hookError) {
4
+ if (!existing)
5
+ return hookError;
6
+ const primaryMsg = existing.message || String(existing);
7
+ const hookMsg = hookError.message || String(hookError);
8
+ const combined = new Error(`${primaryMsg}\n\n[${hookName}] ${hookMsg}`);
9
+ const primaryStack = existing.stack ?? primaryMsg;
10
+ const hookStack = hookError.stack ?? hookMsg;
11
+ combined.stack = `${primaryStack}\n\n[${hookName}] ${hookStack}`;
12
+ return combined;
13
+ }
14
+ export class Suite {
15
+ description;
16
+ mode;
17
+ parent;
18
+ subtreeHasOnly = false;
19
+ root = false;
20
+ errors = [];
21
+ suites = [];
22
+ tests = [];
23
+ afterAllError;
24
+ beforeAllError;
25
+ beforeAllHooks = [];
26
+ afterAllHooks = [];
27
+ beforeEachHooks = [];
28
+ afterEachHooks = [];
29
+ constructor(description, parent, mode = TaskMode.Default) {
30
+ this.description = description;
31
+ this.parent = parent;
32
+ this.mode = mode;
33
+ }
34
+ markSubtreeSkipped() {
35
+ for (const test of this.tests) {
36
+ if (test.status === TaskStatus.Pending)
37
+ test.status = TaskStatus.Skipped;
38
+ test.durationMs = 0;
39
+ }
40
+ for (const suite of this.suites)
41
+ suite.markSubtreeSkipped();
42
+ }
43
+ addSuite(suite) {
44
+ this.suites.push(suite);
45
+ }
46
+ addTest(test) {
47
+ this.tests.push(test);
48
+ }
49
+ collectBeforeEachHooks() {
50
+ const beforeEachHooks = [];
51
+ let currentSuite = this;
52
+ const stack = [];
53
+ while (currentSuite) {
54
+ stack.push(currentSuite);
55
+ currentSuite = currentSuite.parent;
56
+ }
57
+ for (let i = stack.length - 1; i >= 0; i--) {
58
+ beforeEachHooks.push(...stack[i].beforeEachHooks);
59
+ }
60
+ return beforeEachHooks;
61
+ }
62
+ collectAfterEachHooks() {
63
+ const afterEachHooks = [];
64
+ let currentSuite = this;
65
+ while (currentSuite) {
66
+ afterEachHooks.push(...currentSuite.afterEachHooks);
67
+ currentSuite = currentSuite.parent;
68
+ }
69
+ return afterEachHooks;
70
+ }
71
+ async run(options, state = { bailed: false }, isOnly = false, ancestorOnly = false) {
72
+ // Bail should mark anything not-run as skipped (no "pending" leaks).
73
+ if (state.bailed) {
74
+ this.markSubtreeSkipped();
75
+ return;
76
+ }
77
+ if (this.mode === TaskMode.Skip) {
78
+ this.markSubtreeSkipped();
79
+ return;
80
+ }
81
+ const suiteHasOnly = this.subtreeHasOnly;
82
+ const inOnlyContext = ancestorOnly || this.mode === TaskMode.Only;
83
+ // If only is on and this suite has no runnable-only content, skip it entirely
84
+ if (isOnly && !suiteHasOnly && !inOnlyContext) {
85
+ this.markSubtreeSkipped();
86
+ return;
87
+ }
88
+ let entered = false;
89
+ try {
90
+ // beforeAll
91
+ try {
92
+ for (const hook of this.beforeAllHooks) {
93
+ await hook();
94
+ }
95
+ }
96
+ catch (error) {
97
+ this.beforeAllError =
98
+ error instanceof Error ? error : new Error(String(error));
99
+ // Prevent "pending" tests from showing up if beforeAll fails
100
+ this.markSubtreeSkipped();
101
+ return;
102
+ }
103
+ entered = true;
104
+ const beforeEachHooks = this.collectBeforeEachHooks();
105
+ const afterEachHooks = this.collectAfterEachHooks();
106
+ // Indexed so we can skip remaining tests on bail
107
+ for (let i = 0; i < this.tests.length; i++) {
108
+ const test = this.tests[i];
109
+ if (state.bailed)
110
+ break;
111
+ if (test.mode === TaskMode.Skip) {
112
+ test.status = TaskStatus.Skipped;
113
+ test.durationMs = 0;
114
+ continue;
115
+ }
116
+ const testIsOnly = inOnlyContext || test.mode === TaskMode.Only;
117
+ // If onlyMode, skip tests that are not in only context
118
+ if (isOnly && !testIsOnly) {
119
+ test.status = TaskStatus.Skipped;
120
+ test.durationMs = 0;
121
+ continue;
122
+ }
123
+ try {
124
+ for (const hook of beforeEachHooks) {
125
+ try {
126
+ await hook();
127
+ }
128
+ catch (error) {
129
+ const hookError = error instanceof Error ? error : new Error(String(error));
130
+ throw appendHookFailure(undefined, "beforeEach", hookError);
131
+ }
132
+ }
133
+ await test.run(options?.timeoutMs);
134
+ }
135
+ catch (error) {
136
+ test.status = TaskStatus.Fail;
137
+ test.error =
138
+ error instanceof Error ? error : new Error(String(error));
139
+ }
140
+ finally {
141
+ for (const hook of afterEachHooks) {
142
+ try {
143
+ await hook();
144
+ }
145
+ catch (error) {
146
+ const hookError = error instanceof Error ? error : new Error(String(error));
147
+ test.status = TaskStatus.Fail;
148
+ test.error = appendHookFailure(test.error, "afterEach", hookError);
149
+ }
150
+ }
151
+ }
152
+ // Mark everything that hasn't run as skipped on bail
153
+ if (options?.bail && test.status === TaskStatus.Fail) {
154
+ state.bailed = true;
155
+ // Remaining tests in this suite
156
+ for (let j = i + 1; j < this.tests.length; j++) {
157
+ const t = this.tests[j];
158
+ if (t.status === TaskStatus.Pending)
159
+ t.status = TaskStatus.Skipped;
160
+ t.durationMs = 0;
161
+ }
162
+ // All child suites won't run
163
+ for (const child of this.suites)
164
+ child.markSubtreeSkipped();
165
+ break;
166
+ }
167
+ }
168
+ // Indexed so we can skip remaining suites on bail
169
+ for (let i = 0; i < this.suites.length; i++) {
170
+ const childSuite = this.suites[i];
171
+ if (state.bailed) {
172
+ // Current and remaining suites won't run
173
+ for (let j = i; j < this.suites.length; j++) {
174
+ this.suites[j].markSubtreeSkipped();
175
+ }
176
+ break;
177
+ }
178
+ await childSuite.run(options, state, isOnly, inOnlyContext);
179
+ }
180
+ }
181
+ finally {
182
+ // afterAll should still run for suites we entered, even on bail,
183
+ // but it should not run for suites we haven't entered.
184
+ if (entered) {
185
+ for (const afterAllHook of this.afterAllHooks) {
186
+ try {
187
+ await afterAllHook();
188
+ }
189
+ catch (error) {
190
+ // Record once; keep running remaining afterAll hooks
191
+ if (!this.afterAllError) {
192
+ this.afterAllError =
193
+ error instanceof Error ? error : new Error(String(error));
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ getFullDescription() {
201
+ if (!this.parent || this.parent.root)
202
+ return this.description;
203
+ return `${this.parent.getFullDescription()} › ${this.description}`;
204
+ }
205
+ reset() {
206
+ this.tests = [];
207
+ this.suites = [];
208
+ this.beforeAllHooks = [];
209
+ this.afterAllHooks = [];
210
+ this.beforeEachHooks = [];
211
+ this.afterEachHooks = [];
212
+ this.afterAllError = undefined;
213
+ this.beforeAllError = undefined;
214
+ this.errors = [];
215
+ this.subtreeHasOnly = false;
216
+ }
217
+ getResults() {
218
+ let results = [];
219
+ for (const suiteError of this.errors) {
220
+ results.push({
221
+ description: `${this.getFullDescription()} > [${suiteError.label}]`,
222
+ status: TaskStatus.Fail,
223
+ error: suiteError.error,
224
+ durationMs: 0,
225
+ });
226
+ }
227
+ if (this.beforeAllError) {
228
+ results.push({
229
+ description: `${this.getFullDescription()} > [beforeAll]`,
230
+ status: TaskStatus.Fail,
231
+ error: this.beforeAllError,
232
+ durationMs: 0,
233
+ });
234
+ }
235
+ if (this.afterAllError) {
236
+ results.push({
237
+ description: `${this.getFullDescription()} > [afterAll]`,
238
+ status: TaskStatus.Fail,
239
+ error: this.afterAllError,
240
+ durationMs: 0,
241
+ });
242
+ }
243
+ for (const test of this.tests) {
244
+ results.push({
245
+ description: `${this.getFullDescription()} › ${test.description}`,
246
+ status: test.status,
247
+ error: test.error,
248
+ durationMs: test.durationMs ?? 0,
249
+ });
250
+ }
251
+ for (const child of this.suites) {
252
+ results = results.concat(child.getResults());
253
+ }
254
+ return results;
255
+ }
256
+ }
257
+ export function computeOnlyFlags(suite) {
258
+ // Clear any stale value first for multi-run in same process
259
+ suite.subtreeHasOnly = false;
260
+ let subtreeHasOnly = suite.mode === TaskMode.Only;
261
+ for (const test of suite.tests) {
262
+ if (test.mode === TaskMode.Only)
263
+ subtreeHasOnly = true;
264
+ }
265
+ for (const child of suite.suites) {
266
+ if (computeOnlyFlags(child))
267
+ subtreeHasOnly = true;
268
+ }
269
+ suite.subtreeHasOnly = subtreeHasOnly;
270
+ return subtreeHasOnly;
271
+ }
272
+ export const rootSuite = new Suite("root");
273
+ rootSuite.root = true;
274
+ export function getCurrentSuite() {
275
+ return unriftGlobalContext.currentSuite || rootSuite;
276
+ }
277
+ export function clearContext() {
278
+ rootSuite.reset();
279
+ rootSuite.subtreeHasOnly = false;
280
+ unriftGlobalContext.currentSuite = rootSuite;
281
+ }
@@ -0,0 +1,18 @@
1
+ import { TaskMode, TaskStatus, type PromisableFn } from "./types";
2
+ interface TestTask {
3
+ description: string;
4
+ fn: PromisableFn<void>;
5
+ run(): Promise<void>;
6
+ }
7
+ export declare class Test implements TestTask {
8
+ description: string;
9
+ durationMs: number;
10
+ error?: Error;
11
+ fn: PromisableFn<void>;
12
+ mode: TaskMode;
13
+ status: TaskStatus;
14
+ constructor(description: string, fn: PromisableFn<void>, mode?: TaskMode);
15
+ run(timeoutMs?: number): Promise<void>;
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAElE,UAAU,QAAQ;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACvB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,qBAAa,IAAK,YAAW,QAAQ;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAK;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IAEd,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,EAAE,QAAQ,CAAC;IAEf,MAAM,EAAE,UAAU,CAAsB;gBAGtC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B;IAO7B,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAwC7C"}
@@ -0,0 +1,52 @@
1
+ import { TaskMode, TaskStatus } from "./types.js";
2
+ export class Test {
3
+ description;
4
+ durationMs = 0;
5
+ error;
6
+ fn;
7
+ mode;
8
+ status = TaskStatus.Pending;
9
+ constructor(description, fn, mode = TaskMode.Default) {
10
+ this.description = description;
11
+ this.fn = fn;
12
+ this.mode = mode;
13
+ }
14
+ async run(timeoutMs) {
15
+ if (this.mode === TaskMode.Skip) {
16
+ this.status = TaskStatus.Skipped;
17
+ this.durationMs = 0;
18
+ return;
19
+ }
20
+ const start = performance.now();
21
+ this.status = TaskStatus.Running;
22
+ const exec = Promise.resolve().then(() => this.fn());
23
+ try {
24
+ if (timeoutMs !== undefined) {
25
+ let timeoutId = null;
26
+ const timeoutFn = new Promise((_, reject) => {
27
+ timeoutId = setTimeout(() => {
28
+ reject(new Error(`Test timed out after ${timeoutMs} ms`));
29
+ }, timeoutMs);
30
+ });
31
+ try {
32
+ await Promise.race([exec, timeoutFn]);
33
+ }
34
+ finally {
35
+ if (timeoutId)
36
+ clearTimeout(timeoutId);
37
+ }
38
+ }
39
+ else {
40
+ await exec;
41
+ }
42
+ this.status = TaskStatus.Pass;
43
+ }
44
+ catch (error) {
45
+ this.error = error instanceof Error ? error : new Error(String(error));
46
+ this.status = TaskStatus.Fail;
47
+ }
48
+ finally {
49
+ this.durationMs = performance.now() - start;
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,24 @@
1
+ type Promisable<T> = Promise<T> | T;
2
+ export type PromisableFn<T> = () => Promisable<T>;
3
+ export declare enum TaskStatus {
4
+ Pending = "pending",
5
+ Running = "running",
6
+ Pass = "pass",
7
+ Fail = "fail",
8
+ Only = "only",
9
+ Skipped = "skipped",
10
+ Todo = "todo"
11
+ }
12
+ export declare enum TaskMode {
13
+ Default = "default",
14
+ Only = "only",
15
+ Skip = "skip"
16
+ }
17
+ export interface MatcherContext {
18
+ isNot: boolean;
19
+ diff(a: unknown, b: unknown): string;
20
+ }
21
+ export type MatcherFn = (this: MatcherContext, received: unknown, ...args: readonly unknown[]) => void | Promise<void>;
22
+ export type MatcherMap = Record<string, MatcherFn>;
23
+ export {};
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAEpC,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC;AAElD,oBAAY,UAAU;IACpB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,IAAI,SAAS;CACd;AAED,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,SAAS,GAAG,CACtB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,OAAO,EACjB,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ export var TaskStatus;
2
+ (function (TaskStatus) {
3
+ TaskStatus["Pending"] = "pending";
4
+ TaskStatus["Running"] = "running";
5
+ TaskStatus["Pass"] = "pass";
6
+ TaskStatus["Fail"] = "fail";
7
+ TaskStatus["Only"] = "only";
8
+ TaskStatus["Skipped"] = "skipped";
9
+ TaskStatus["Todo"] = "todo";
10
+ })(TaskStatus || (TaskStatus = {}));
11
+ export var TaskMode;
12
+ (function (TaskMode) {
13
+ TaskMode["Default"] = "default";
14
+ TaskMode["Only"] = "only";
15
+ TaskMode["Skip"] = "skip";
16
+ })(TaskMode || (TaskMode = {}));
@@ -0,0 +1,7 @@
1
+ export declare const colors: {
2
+ green: (text: string) => string;
3
+ red: (text: string) => string;
4
+ yellow: (text: string) => string;
5
+ bold: (text: string) => string;
6
+ };
7
+ //# sourceMappingURL=colors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../../src/utils/colors.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,MAAM;kBAHH,MAAM;gBAAN,MAAM;mBAAN,MAAM;iBAAN,MAAM;CAQrB,CAAC"}