@testspectra/cli 1.1.8-rc.9 → 1.1.11-rc.3

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 (93) hide show
  1. package/README.md +11 -8
  2. package/bin/.testspectra-runner.hash +1 -0
  3. package/dist/commands/__tests__/doctor-filter.test.js +37 -0
  4. package/dist/commands/__tests__/doctor-scope.test.d.ts +1 -0
  5. package/dist/commands/__tests__/doctor-scope.test.js +24 -0
  6. package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
  7. package/dist/commands/__tests__/test-error-streaming.test.js +85 -0
  8. package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
  9. package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
  10. package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
  11. package/dist/commands/__tests__/upgrade.test.js +95 -0
  12. package/dist/commands/add.js +3 -3
  13. package/dist/commands/devices.js +1 -0
  14. package/dist/commands/docs.d.ts +4 -0
  15. package/dist/commands/docs.js +17 -0
  16. package/dist/commands/doctor.d.ts +9 -1
  17. package/dist/commands/doctor.js +17 -3
  18. package/dist/commands/init.js +5 -159
  19. package/dist/commands/install.js +5 -0
  20. package/dist/commands/license.js +1 -0
  21. package/dist/commands/run.d.ts +2 -0
  22. package/dist/commands/run.js +22 -0
  23. package/dist/commands/test.d.ts +13 -0
  24. package/dist/commands/test.js +115 -21
  25. package/dist/commands/upgrade.d.ts +12 -0
  26. package/dist/commands/upgrade.js +197 -0
  27. package/dist/config/schema.d.ts +53 -0
  28. package/dist/config/schema.js +2 -0
  29. package/dist/generator/__tests__/tsconfig-isolation.test.js +2 -1
  30. package/dist/generator/architecture-doc-generator.d.ts +14 -0
  31. package/dist/generator/architecture-doc-generator.js +80 -0
  32. package/dist/generator/index.d.ts +1 -0
  33. package/dist/generator/index.js +1 -0
  34. package/dist/generator/tsconfig-generator.js +1 -1
  35. package/dist/index.js +59 -16
  36. package/dist/linter/discovery.js +2 -2
  37. package/dist/linter/schemas/spec.schema.d.ts +6 -6
  38. package/dist/runner/bridge.d.ts +1 -1
  39. package/dist/runner/bridge.js +58 -16
  40. package/dist/runner/reporter.d.ts +1 -0
  41. package/dist/runner/reporter.js +12 -6
  42. package/dist/utils/__tests__/dev-server-manager.test.d.ts +1 -0
  43. package/dist/utils/__tests__/dev-server-manager.test.js +72 -0
  44. package/dist/utils/background-update-check.d.ts +1 -0
  45. package/dist/utils/background-update-check.js +43 -0
  46. package/dist/utils/dependency-updates.d.ts +15 -0
  47. package/dist/utils/dependency-updates.js +107 -0
  48. package/dist/utils/dev-server-manager.d.ts +40 -0
  49. package/dist/utils/dev-server-manager.js +257 -0
  50. package/dist/utils/update-notifier.d.ts +2 -0
  51. package/dist/utils/update-notifier.js +67 -0
  52. package/package.json +10 -9
  53. package/templates/default/package.json +2 -2
  54. package/templates/docs/ARCHITECTURE.default.md +26 -0
  55. package/templates/docs/ARCHITECTURE.nx.md +147 -0
  56. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  57. package/templates/nx/.nx/workspace-data/d/daemon.log +1012 -5748
  58. package/templates/nx/.nx/workspace-data/file-map.json +171 -185
  59. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
  60. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  61. package/templates/nx/.nx/workspace-data/project-graph.json +3 -1
  62. package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
  63. package/templates/nx/package.json +3 -3
  64. package/templates/nx/pnpm-lock.yaml +191 -214
  65. package/templates/nx/spectra.config.ts +13 -0
  66. package/templates/react-component/package.json +3 -3
  67. package/templates/react-component/page-objects/BadgeComponent/web.ts +4 -4
  68. package/templates/react-component/page-objects/ButtonComponent/web.ts +3 -3
  69. package/templates/react-component/page-objects/InputComponent/web.ts +1 -1
  70. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +0 -3
  71. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +0 -9
  72. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +1 -1
  73. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +4 -8
  74. package/templates/react-component/spectra.config.ts +0 -10
  75. package/templates/react-component/src/components/Badge/Badge.tsx +3 -3
  76. package/templates/react-component/src/components/Button/Button.tsx +5 -3
  77. package/templates/react-component/src/components/Input/Input.tsx +2 -3
  78. package/dist/.tsbuildinfo +0 -1
  79. package/dist/commands/update.d.ts +0 -3
  80. package/dist/commands/update.js +0 -109
  81. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +0 -290
  82. package/dist/lifecycle/hook-discovery.d.ts +0 -24
  83. package/dist/lifecycle/hook-discovery.js +0 -60
  84. package/dist/lifecycle/hook-dispatcher.d.ts +0 -34
  85. package/dist/lifecycle/hook-dispatcher.js +0 -190
  86. package/dist/lifecycle/index.d.ts +0 -3
  87. package/dist/lifecycle/index.js +0 -3
  88. package/dist/lifecycle/parallel-grouping.d.ts +0 -27
  89. package/dist/lifecycle/parallel-grouping.js +0 -151
  90. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  91. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  92. package/templates/nx/.nx/workspace-data/d/server-process.json +0 -3
  93. /package/dist/{lifecycle/__tests__/lifecycle-engine.test.d.ts → commands/__tests__/doctor-filter.test.d.ts} +0 -0
@@ -1,109 +0,0 @@
1
- import { execSync, execFileSync } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = path.dirname(__filename);
7
- const PACKAGES = ['@testspectra/cli', '@testspectra/matchers'];
8
- function getLatestVersion(pkg) {
9
- try {
10
- const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
11
- stdio: ['ignore', 'pipe', 'ignore'],
12
- timeout: 10000,
13
- })
14
- .toString()
15
- .trim()
16
- .replace(/"/g, '');
17
- return out || null;
18
- }
19
- catch {
20
- return null;
21
- }
22
- }
23
- function getCurrentVersion(pkg, cwd) {
24
- // Walk up to find the installed version
25
- let cur = path.resolve(cwd);
26
- while (cur && cur !== path.dirname(cur)) {
27
- const pkgJson = path.join(cur, 'node_modules', pkg, 'package.json');
28
- if (fs.existsSync(pkgJson)) {
29
- try {
30
- const p = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'));
31
- return p.version ?? null;
32
- }
33
- catch { }
34
- }
35
- cur = path.dirname(cur);
36
- }
37
- return null;
38
- }
39
- function detectPackageManager(cwd) {
40
- if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
41
- return 'pnpm';
42
- if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock')))
43
- return 'bun';
44
- return 'npm';
45
- }
46
- function buildInstallCmd(pm, pkgSpecs) {
47
- const list = pkgSpecs.join(' ');
48
- switch (pm) {
49
- case 'pnpm':
50
- return `pnpm add -D ${list}`;
51
- case 'bun':
52
- return `bun add -d ${list}`;
53
- case 'npm':
54
- default:
55
- return `npm install -D ${list}`;
56
- }
57
- }
58
- export async function updateCommand(options = {}) {
59
- const cwd = process.cwd();
60
- const pm = detectPackageManager(cwd);
61
- console.log('\x1b[36m┌ 🔄 TestSpectra Self-Update\x1b[0m');
62
- console.log('\x1b[36m│\x1b[0m');
63
- console.log(`\x1b[36m│\x1b[0m Package Manager: \x1b[33m${pm}\x1b[0m`);
64
- console.log('\x1b[36m│\x1b[0m');
65
- console.log('\x1b[36m│\x1b[0m Fetching latest versions from npm registry...');
66
- const updates = [];
67
- for (const pkg of PACKAGES) {
68
- const current = getCurrentVersion(pkg, cwd);
69
- const latest = getLatestVersion(pkg);
70
- const needsUpdate = !!latest && latest !== current;
71
- updates.push({ pkg, current, latest, needsUpdate });
72
- const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
73
- const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[31m(unavailable)\x1b[0m';
74
- const arrow = needsUpdate ? ` → ${latestStr}` : '';
75
- const status = !latest ? '\x1b[31m✗\x1b[0m' : needsUpdate ? '\x1b[33m↑\x1b[0m' : '\x1b[32m✓\x1b[0m';
76
- console.log(`\x1b[36m│\x1b[0m ${status} ${pkg.padEnd(28)} ${currentStr}${arrow}`);
77
- }
78
- const toUpdate = updates.filter((u) => u.needsUpdate && u.latest);
79
- console.log('\x1b[36m│\x1b[0m');
80
- if (toUpdate.length === 0) {
81
- console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
82
- console.log('\x1b[32m✓ All TestSpectra packages are already up-to-date!\x1b[0m\n');
83
- return;
84
- }
85
- if (options.check) {
86
- console.log(`\x1b[36m│\x1b[0m \x1b[33m${toUpdate.length} update(s) available. Run \x1b[36mspectra update\x1b[33m to install.\x1b[0m`);
87
- console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m\n');
88
- return;
89
- }
90
- const pkgSpecs = toUpdate.map((u) => `${u.pkg}@${u.latest}`);
91
- const installCmd = buildInstallCmd(pm, pkgSpecs);
92
- console.log(`\x1b[36m│\x1b[0m Running: \x1b[90m${installCmd}\x1b[0m`);
93
- console.log('\x1b[36m│\x1b[0m');
94
- try {
95
- execSync(installCmd, { cwd, stdio: 'inherit' });
96
- console.log('\x1b[36m│\x1b[0m');
97
- console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
98
- console.log('\x1b[32m🎉 TestSpectra updated successfully!\x1b[0m');
99
- for (const u of toUpdate) {
100
- console.log(` \x1b[32m✓\x1b[0m ${u.pkg}: v${u.current ?? '?'} → v${u.latest}`);
101
- }
102
- console.log('');
103
- }
104
- catch (err) {
105
- console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
106
- console.error(`\x1b[31m✗ Update failed: ${err.message}\x1b[0m\n`);
107
- process.exit(1);
108
- }
109
- }
@@ -1,290 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
- import { discoverGlobalHooks, discoverSuiteHooks } from '../hook-discovery.js';
6
- import { LifecycleDispatcher, HookExecutionError } from '../hook-dispatcher.js';
7
- import { calculateWorkerCount, groupSpecs } from '../parallel-grouping.js';
8
- describe('Core Lifecycle Execution Engine (docs/v2/cli/lifecycle-execution-engine.md)', () => {
9
- let tmpDir;
10
- beforeEach(() => {
11
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testspectra-lifecycle-test-'));
12
- });
13
- afterEach(() => {
14
- try {
15
- fs.rmSync(tmpDir, { recursive: true, force: true });
16
- }
17
- catch { }
18
- });
19
- describe('Hook Discovery', () => {
20
- it('discovers global-hooks and filters by platform hierarchy', () => {
21
- const gBefore = path.join(tmpDir, 'global-hooks', 'before');
22
- const gAfter = path.join(tmpDir, 'global-hooks', 'after');
23
- fs.mkdirSync(gBefore, { recursive: true });
24
- fs.mkdirSync(gAfter, { recursive: true });
25
- fs.writeFileSync(path.join(gBefore, 'web.hook.ts'), 'export default async () => {}');
26
- fs.writeFileSync(path.join(gBefore, 'mobile.hook.ts'), 'export default async () => {}');
27
- fs.writeFileSync(path.join(gBefore, 'common.hook.ts'), 'export default async () => {}');
28
- fs.writeFileSync(path.join(gAfter, 'web.hook.ts'), 'export default async () => {}');
29
- const webGlobal = discoverGlobalHooks(tmpDir, 'web');
30
- expect(webGlobal.before.some((h) => h.endsWith('web.hook.ts'))).toBe(true);
31
- expect(webGlobal.before.some((h) => h.endsWith('common.hook.ts'))).toBe(true);
32
- expect(webGlobal.before.some((h) => h.endsWith('mobile.hook.ts'))).toBe(false);
33
- const mobileGlobal = discoverGlobalHooks(tmpDir, 'mobile');
34
- expect(mobileGlobal.before.some((h) => h.endsWith('mobile.hook.ts'))).toBe(true);
35
- expect(mobileGlobal.before.some((h) => h.endsWith('common.hook.ts'))).toBe(true);
36
- expect(mobileGlobal.before.some((h) => h.endsWith('web.hook.ts'))).toBe(false);
37
- });
38
- it('discovers suite hooks (before, beforeEach, afterEach, after)', () => {
39
- const suiteDir = path.join(tmpDir, 'specs', 'Auth');
40
- for (const hookType of ['before', 'beforeEach', 'afterEach', 'after']) {
41
- const hDir = path.join(suiteDir, 'hooks', hookType);
42
- fs.mkdirSync(hDir, { recursive: true });
43
- fs.writeFileSync(path.join(hDir, 'web.hook.ts'), 'export default async () => {}');
44
- }
45
- const suiteHooks = discoverSuiteHooks(suiteDir, 'web');
46
- expect(suiteHooks.before.length).toBe(1);
47
- expect(suiteHooks.beforeEach.length).toBe(1);
48
- expect(suiteHooks.afterEach.length).toBe(1);
49
- expect(suiteHooks.after.length).toBe(1);
50
- });
51
- });
52
- describe('Lifecycle Dispatcher & Execution Flow', () => {
53
- it('single run executes suite hooks without triggering global hooks', async () => {
54
- const executionOrder = [];
55
- const globalBeforeFn = vi.fn(async () => {
56
- executionOrder.push('globalBefore');
57
- });
58
- const globalAfterFn = vi.fn(async () => {
59
- executionOrder.push('globalAfter');
60
- });
61
- const suiteHooks = {
62
- before: [
63
- async () => {
64
- executionOrder.push('suiteBefore');
65
- },
66
- ],
67
- beforeEach: [
68
- async () => {
69
- executionOrder.push('beforeEach');
70
- },
71
- ],
72
- afterEach: [
73
- async () => {
74
- executionOrder.push('afterEach');
75
- },
76
- ],
77
- after: [
78
- async () => {
79
- executionOrder.push('suiteAfter');
80
- },
81
- ],
82
- };
83
- const dispatcher = new LifecycleDispatcher('single', {
84
- before: [globalBeforeFn],
85
- after: [globalAfterFn],
86
- });
87
- // Single run bypasses global before & global after
88
- await dispatcher.runGlobalBefore();
89
- expect(globalBeforeFn).not.toHaveBeenCalled();
90
- const result = await dispatcher.executeSuite('AuthSuite', suiteHooks, [
91
- {
92
- id: 'TC-01',
93
- title: 'Valid Login',
94
- fn: async () => {
95
- executionOrder.push('testBody');
96
- },
97
- },
98
- ]);
99
- await dispatcher.runGlobalAfter();
100
- expect(globalAfterFn).not.toHaveBeenCalled();
101
- expect(result.status).toBe('PASSED');
102
- expect(executionOrder).toEqual(['suiteBefore', 'beforeEach', 'testBody', 'afterEach', 'suiteAfter']);
103
- });
104
- it('global batch run executes global-hooks/before once at start and global-hooks/after once at finish', async () => {
105
- const executionOrder = [];
106
- const globalBeforeFn = vi.fn(async () => {
107
- executionOrder.push('globalBefore');
108
- });
109
- const globalAfterFn = vi.fn(async () => {
110
- executionOrder.push('globalAfter');
111
- });
112
- const dispatcher = new LifecycleDispatcher('global', {
113
- before: [globalBeforeFn],
114
- after: [globalAfterFn],
115
- });
116
- await dispatcher.runGlobalBefore();
117
- expect(globalBeforeFn).toHaveBeenCalledTimes(1);
118
- const suiteHooks = {
119
- before: [
120
- async () => {
121
- executionOrder.push('suiteBefore');
122
- },
123
- ],
124
- beforeEach: [
125
- async () => {
126
- executionOrder.push('beforeEach');
127
- },
128
- ],
129
- afterEach: [
130
- async () => {
131
- executionOrder.push('afterEach');
132
- },
133
- ],
134
- after: [
135
- async () => {
136
- executionOrder.push('suiteAfter');
137
- },
138
- ],
139
- };
140
- const result = await dispatcher.executeSuite('AuthSuite', suiteHooks, [
141
- {
142
- id: 'TC-01',
143
- title: 'Test 1',
144
- fn: async () => {
145
- executionOrder.push('tc1');
146
- },
147
- },
148
- {
149
- id: 'TC-02',
150
- title: 'Test 2',
151
- fn: async () => {
152
- executionOrder.push('tc2');
153
- },
154
- },
155
- ]);
156
- await dispatcher.runGlobalAfter();
157
- expect(globalAfterFn).toHaveBeenCalledTimes(1);
158
- expect(result.status).toBe('PASSED');
159
- expect(executionOrder).toEqual([
160
- 'globalBefore',
161
- 'suiteBefore',
162
- 'beforeEach',
163
- 'tc1',
164
- 'afterEach',
165
- 'beforeEach',
166
- 'tc2',
167
- 'afterEach',
168
- 'suiteAfter',
169
- 'globalAfter',
170
- ]);
171
- });
172
- it('when beforeEach fails, test body is skipped, afterEach is guaranteed to execute, and test is marked as ERROR', async () => {
173
- const testBodyFn = vi.fn();
174
- let afterEachRan = false;
175
- const suiteHooks = {
176
- before: [],
177
- beforeEach: [
178
- async () => {
179
- throw new Error('Database connection failed in beforeEach');
180
- },
181
- ],
182
- afterEach: [
183
- async () => {
184
- afterEachRan = true;
185
- },
186
- ],
187
- after: [],
188
- };
189
- const dispatcher = new LifecycleDispatcher('single');
190
- const result = await dispatcher.executeSuite('FailingBeforeEachSuite', suiteHooks, [
191
- {
192
- id: 'TC-FAIL-01',
193
- title: 'Should be skipped',
194
- fn: testBodyFn,
195
- },
196
- ]);
197
- expect(testBodyFn).not.toHaveBeenCalled();
198
- expect(afterEachRan).toBe(true);
199
- expect(result.results[0].status).toBe('ERROR');
200
- expect(result.results[0].error?.message).toContain('Database connection failed in beforeEach');
201
- expect(result.status).toBe('ERROR');
202
- });
203
- it('when afterEach fails, test transitions to ERROR status', async () => {
204
- const suiteHooks = {
205
- before: [],
206
- beforeEach: [],
207
- afterEach: [
208
- async () => {
209
- throw new Error('Teardown resource leak in afterEach');
210
- },
211
- ],
212
- after: [],
213
- };
214
- const dispatcher = new LifecycleDispatcher('single');
215
- const result = await dispatcher.executeSuite('TeardownFailSuite', suiteHooks, [
216
- {
217
- id: 'TC-01',
218
- title: 'Passing Body but failing afterEach',
219
- fn: async () => {
220
- // Passing body
221
- },
222
- },
223
- ]);
224
- expect(result.results[0].status).toBe('ERROR');
225
- expect(result.results[0].error?.message).toContain('Teardown resource leak in afterEach');
226
- expect(result.status).toBe('ERROR');
227
- });
228
- it('when global-hooks/before fails, throws HookExecutionError immediately', async () => {
229
- const dispatcher = new LifecycleDispatcher('global', {
230
- before: [
231
- async () => {
232
- throw new Error('Global health check failed');
233
- },
234
- ],
235
- after: [],
236
- });
237
- await expect(dispatcher.runGlobalBefore()).rejects.toThrow(HookExecutionError);
238
- await expect(dispatcher.runGlobalBefore()).rejects.toThrow('Global health check failed');
239
- });
240
- });
241
- describe('Parallel Spec Grouping & Dynamic Worker Sizing', () => {
242
- it('prioritizes explicit user concurrency flag over auto-calculation', () => {
243
- const targets = Array.from({ length: 10 }, () => 'spec.ts');
244
- expect(calculateWorkerCount(targets, { userConcurrency: 4 })).toBe(4);
245
- // Clamps to item count if items < userConcurrency
246
- expect(calculateWorkerCount(['only-one.ts'], { userConcurrency: 4 })).toBe(1);
247
- });
248
- it('enforces low-CPU safety ratio based on container/host CPU quota', () => {
249
- const targets = Array.from({ length: 10 }, () => 'spec.ts');
250
- const highRam = 16 * 1024 * 1024 * 1024; // 16 GB
251
- // CPU <= 2 (e.g. 2-vCPU standard CI runner): clamped to 1 worker
252
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: 1, memoryBytesOverride: highRam })).toBe(1);
253
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: 2, memoryBytesOverride: highRam })).toBe(1);
254
- // CPU 3 - 4: allocates 2 workers
255
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: 3, memoryBytesOverride: highRam })).toBe(2);
256
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: 4, memoryBytesOverride: highRam })).toBe(2);
257
- // CPU > 4: allocates (cpu - 2) capped at maxWorkersCap (default 6)
258
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: 6, memoryBytesOverride: highRam })).toBe(4);
259
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: 8, memoryBytesOverride: highRam })).toBe(6);
260
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: 64, memoryBytesOverride: highRam })).toBe(6);
261
- });
262
- it('enforces memory-aware guard to prevent OOM kills during browser automation', () => {
263
- const targets = Array.from({ length: 10 }, () => 'spec.ts');
264
- const highCpu = 16;
265
- // 500 MB RAM budget -> allows at most 1 worker (~600MB budget per browser)
266
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: highCpu, memoryBytesOverride: 500 * 1024 * 1024 })).toBe(1);
267
- // 1.5 GB RAM budget -> allows 2 workers
268
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: highCpu, memoryBytesOverride: 1500 * 1024 * 1024 })).toBe(2);
269
- // 2.5 GB RAM budget -> allows 4 workers
270
- expect(calculateWorkerCount(targets, { cpuQuotaOverride: highCpu, memoryBytesOverride: 2500 * 1024 * 1024 })).toBe(4);
271
- });
272
- it('groups specs by suite directory in nested array format when mode is "suite"', () => {
273
- const specs = [
274
- 'specs/Auth/TC-001/web.test.ts',
275
- 'specs/Auth/TC-002/web.test.ts',
276
- 'specs/Cart/TC-010/web.test.ts',
277
- 'specs/Cart/TC-011/web.test.ts',
278
- ];
279
- const grouped = groupSpecs(specs, 'suite');
280
- expect(grouped.length).toBe(2);
281
- expect(grouped[0]).toEqual(['specs/Auth/TC-001/web.test.ts', 'specs/Auth/TC-002/web.test.ts']);
282
- expect(grouped[1]).toEqual(['specs/Cart/TC-010/web.test.ts', 'specs/Cart/TC-011/web.test.ts']);
283
- });
284
- it('flattens specs when mode is "testcase"', () => {
285
- const specs = ['specs/Auth/TC-001/web.test.ts', 'specs/Auth/TC-002/web.test.ts', 'specs/Cart/TC-010/web.test.ts'];
286
- const flat = groupSpecs(specs, 'testcase');
287
- expect(flat).toEqual(specs);
288
- });
289
- });
290
- });
@@ -1,24 +0,0 @@
1
- import { PlatformTarget } from '../generator/type-generator.js';
2
- export type HookEntry = string | (() => Promise<void> | void);
3
- export interface DiscoveredSuiteHooks {
4
- before: string[];
5
- beforeEach: string[];
6
- afterEach: string[];
7
- after: string[];
8
- }
9
- export interface DiscoveredGlobalHooks {
10
- before: string[];
11
- after: string[];
12
- }
13
- export interface SuiteHooks {
14
- before: HookEntry[];
15
- beforeEach: HookEntry[];
16
- afterEach: HookEntry[];
17
- after: HookEntry[];
18
- }
19
- export interface GlobalHooks {
20
- before: HookEntry[];
21
- after: HookEntry[];
22
- }
23
- export declare function discoverGlobalHooks(cwd: string, platform?: PlatformTarget): DiscoveredGlobalHooks;
24
- export declare function discoverSuiteHooks(suiteDir: string, platform?: PlatformTarget): DiscoveredSuiteHooks;
@@ -1,60 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { PLATFORM_HIERARCHY } from '../generator/type-generator.js';
4
- function resolveHookFilesForType(dir, platform = 'web') {
5
- if (!fs.existsSync(dir))
6
- return [];
7
- const entries = fs.readdirSync(dir, { withFileTypes: true });
8
- const hierarchy = PLATFORM_HIERARCHY[platform] || ['common'];
9
- const matched = [];
10
- // Group files by base name if applicable or collect directly
11
- for (const entry of entries) {
12
- if (!entry.isFile() || !entry.name.endsWith('.hook.ts'))
13
- continue;
14
- const parts = entry.name.split('.');
15
- // Formats:
16
- // 1. web.hook.ts -> parts = ['web', 'hook', 'ts']
17
- // 2. auth.web.hook.ts -> parts = ['auth', 'web', 'hook', 'ts']
18
- // 3. auth.hook.ts -> parts = ['auth', 'hook', 'ts']
19
- let filePlatform = 'common';
20
- if (parts.length === 3) {
21
- filePlatform = parts[0];
22
- }
23
- else if (parts.length >= 4) {
24
- filePlatform = parts[parts.length - 3];
25
- }
26
- if (hierarchy.includes(filePlatform) || filePlatform === 'common') {
27
- matched.push(path.join(dir, entry.name));
28
- }
29
- }
30
- // Sort by hierarchy priority
31
- matched.sort((a, b) => {
32
- const getPriority = (filePath) => {
33
- const base = path.basename(filePath);
34
- for (let i = 0; i < hierarchy.length; i++) {
35
- if (base.includes(`.${hierarchy[i]}.`) || base.startsWith(`${hierarchy[i]}.`)) {
36
- return i;
37
- }
38
- }
39
- return hierarchy.length;
40
- };
41
- return getPriority(a) - getPriority(b);
42
- });
43
- return matched;
44
- }
45
- export function discoverGlobalHooks(cwd, platform = 'web') {
46
- const globalHooksDir = path.join(cwd, 'global-hooks');
47
- return {
48
- before: resolveHookFilesForType(path.join(globalHooksDir, 'before'), platform),
49
- after: resolveHookFilesForType(path.join(globalHooksDir, 'after'), platform),
50
- };
51
- }
52
- export function discoverSuiteHooks(suiteDir, platform = 'web') {
53
- const hooksDir = path.join(suiteDir, 'hooks');
54
- return {
55
- before: resolveHookFilesForType(path.join(hooksDir, 'before'), platform),
56
- beforeEach: resolveHookFilesForType(path.join(hooksDir, 'beforeEach'), platform),
57
- afterEach: resolveHookFilesForType(path.join(hooksDir, 'afterEach'), platform),
58
- after: resolveHookFilesForType(path.join(hooksDir, 'after'), platform),
59
- };
60
- }
@@ -1,34 +0,0 @@
1
- import { SuiteHooks, GlobalHooks } from './hook-discovery.js';
2
- export type TestCaseStatus = 'PASSED' | 'FAILED' | 'SKIPPED' | 'ERROR';
3
- export interface TestCaseResult {
4
- id: string;
5
- title: string;
6
- status: TestCaseStatus;
7
- error?: Error;
8
- durationMs: number;
9
- }
10
- export interface SuiteExecutionResult {
11
- suiteName: string;
12
- results: TestCaseResult[];
13
- status: 'PASSED' | 'FAILED' | 'ERROR';
14
- error?: Error;
15
- }
16
- export type HookFn = () => Promise<void> | void;
17
- export declare class HookExecutionError extends Error {
18
- phase: 'before' | 'beforeEach' | 'afterEach' | 'after' | 'globalBefore' | 'globalAfter';
19
- originalError: any;
20
- constructor(phase: 'before' | 'beforeEach' | 'afterEach' | 'after' | 'globalBefore' | 'globalAfter', originalError: any);
21
- }
22
- export declare function runHook(hookPathOrFn: string | HookFn, timeoutMs?: number): Promise<void>;
23
- export declare class LifecycleDispatcher {
24
- mode: 'single' | 'global';
25
- globalHooks: GlobalHooks;
26
- constructor(mode?: 'single' | 'global', globalHooks?: GlobalHooks);
27
- runGlobalBefore(): Promise<void>;
28
- runGlobalAfter(): Promise<void>;
29
- executeSuite(suiteName: string, suiteHooks: SuiteHooks, testCases: Array<{
30
- id: string;
31
- title: string;
32
- fn: () => Promise<void>;
33
- }>): Promise<SuiteExecutionResult>;
34
- }