@testspectra/cli 1.1.11-rc.3 → 1.1.12

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 (90) hide show
  1. package/demo-app/assets/index-DDYS5fLp.css +1 -0
  2. package/demo-app/assets/index-euCENPMy.js +154 -0
  3. package/demo-app/index.html +2 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/commands/__tests__/run-e2e.test.js +24 -6
  6. package/dist/commands/__tests__/run.test.js +27 -0
  7. package/dist/commands/demo.d.ts +1 -0
  8. package/dist/commands/demo.js +26 -4
  9. package/dist/commands/doctor.js +23 -0
  10. package/dist/commands/run.d.ts +6 -0
  11. package/dist/commands/run.js +51 -5
  12. package/dist/commands/test.d.ts +9 -9
  13. package/dist/commands/test.js +40 -75
  14. package/dist/commands/update.d.ts +3 -0
  15. package/dist/commands/update.js +109 -0
  16. package/dist/config/loader.js +31 -4
  17. package/dist/config/schema.d.ts +60 -2
  18. package/dist/config/schema.js +4 -1
  19. package/dist/generator/__tests__/tsconfig-isolation.test.js +21 -0
  20. package/dist/generator/__tests__/type-generator.test.js +15 -1
  21. package/dist/generator/tsconfig-generator.js +1 -0
  22. package/dist/generator/type-generator.js +2 -0
  23. package/dist/index.js +11 -4
  24. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  25. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  26. package/dist/lifecycle/hook-discovery.js +60 -0
  27. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  28. package/dist/lifecycle/hook-dispatcher.js +190 -0
  29. package/dist/lifecycle/index.d.ts +3 -0
  30. package/dist/lifecycle/index.js +3 -0
  31. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  32. package/dist/lifecycle/parallel-grouping.js +151 -0
  33. package/dist/reporter/semantic-formatter.js +2 -0
  34. package/dist/reporter/types.d.ts +1 -1
  35. package/dist/runner/bridge.d.ts +2 -0
  36. package/dist/runner/bridge.js +4 -1
  37. package/dist/runner/reporter.d.ts +5 -43
  38. package/dist/runner/reporter.js +46 -179
  39. package/dist/utils/__tests__/demo-state.test.d.ts +1 -0
  40. package/dist/utils/__tests__/demo-state.test.js +96 -0
  41. package/dist/utils/api-server.d.ts +1 -1
  42. package/dist/utils/api-server.js +7 -3
  43. package/dist/utils/demo-server.d.ts +2 -0
  44. package/dist/utils/demo-server.js +18 -5
  45. package/dist/utils/demo-state.d.ts +40 -0
  46. package/dist/utils/demo-state.js +157 -0
  47. package/package.json +9 -8
  48. package/templates/default/fixtures/sampleDocument.pdf +14 -0
  49. package/templates/default/fixtures/sampleImage.png +0 -0
  50. package/templates/default/package.json +2 -2
  51. package/templates/default/page-objects/MatchersPage/common.ts +41 -0
  52. package/templates/default/page-objects/MatchersPage/mobile.ts +8 -3
  53. package/templates/default/page-objects/PlaygroundPage/mobile.ts +6 -0
  54. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
  55. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +3 -0
  56. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -0
  57. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  58. package/templates/default/specs/Playground/TC-0003-file-upload/web.test.ts +11 -0
  59. package/templates/default/spectra.config.ts +5 -1
  60. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  61. package/templates/nx/.nx/workspace-data/d/daemon.log +7063 -1012
  62. package/templates/nx/.nx/workspace-data/file-map.json +181 -169
  63. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
  64. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  65. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +81 -130
  66. package/templates/nx/.nx/workspace-data/project-graph.json +83 -138
  67. package/templates/nx/package.json +2 -2
  68. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  69. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0003-file-upload/web.test.ts +11 -0
  70. package/templates/nx/shared/testing/fixtures/sampleDocument.pdf +14 -0
  71. package/templates/nx/shared/testing/fixtures/sampleImage.png +0 -0
  72. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +68 -26
  73. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +39 -17
  74. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
  75. package/templates/nx/spectra.config.ts +3 -3
  76. package/templates/react-component/fixtures/sampleDocument.pdf +14 -0
  77. package/templates/react-component/fixtures/sampleImage.png +0 -0
  78. package/templates/react-component/package.json +3 -3
  79. package/templates/react-component/page-objects/FileUploadComponent/web.ts +15 -0
  80. package/templates/react-component/specs/FileUploadComponent/TC-0001-upload-fixtures/web.test.tsx +18 -0
  81. package/templates/react-component/specs/UserListComponent/TC-0001-intercepted-user-list/spec.md +20 -0
  82. package/templates/react-component/specs/UserListComponent/TC-0001-intercepted-user-list/web.test.tsx +15 -0
  83. package/templates/react-component/specs/UserListComponent/suite.md +12 -0
  84. package/templates/react-component/src/components/FileUpload/FileUpload.tsx +44 -0
  85. package/templates/react-component/src/components/UserList/UserList.tsx +48 -0
  86. package/bin/.testspectra-runner.hash +0 -1
  87. package/demo-app/assets/index-BhlW-eXG.css +0 -1
  88. package/demo-app/assets/index-DOtRypCa.js +0 -154
  89. package/dist/commands/__tests__/doctor-scope.test.js +0 -24
  90. /package/dist/{commands/__tests__/doctor-scope.test.d.ts → lifecycle/__tests__/lifecycle-engine.test.d.ts} +0 -0
@@ -0,0 +1,190 @@
1
+ import { pathToFileURL } from 'url';
2
+ export class HookExecutionError extends Error {
3
+ phase;
4
+ originalError;
5
+ constructor(phase, originalError) {
6
+ super(`Hook failure during [${phase}]: ${originalError?.message || String(originalError)}`);
7
+ this.phase = phase;
8
+ this.originalError = originalError;
9
+ this.name = 'HookExecutionError';
10
+ }
11
+ }
12
+ export async function runHook(hookPathOrFn, timeoutMs = 10000) {
13
+ let fn;
14
+ if (typeof hookPathOrFn === 'function') {
15
+ fn = hookPathOrFn;
16
+ }
17
+ else {
18
+ try {
19
+ const fileUrl = pathToFileURL(hookPathOrFn).href;
20
+ const mod = await import(fileUrl);
21
+ fn = mod.default || mod;
22
+ if (typeof fn !== 'function') {
23
+ throw new Error(`Hook file ${hookPathOrFn} does not export a default function`);
24
+ }
25
+ }
26
+ catch (err) {
27
+ throw new Error(`Failed to load hook from ${hookPathOrFn}: ${err.message}`);
28
+ }
29
+ }
30
+ let timeoutId = null;
31
+ const timeoutPromise = new Promise((_, reject) => {
32
+ timeoutId = setTimeout(() => {
33
+ reject(new Error(`Hook execution timed out after ${timeoutMs}ms`));
34
+ }, timeoutMs);
35
+ });
36
+ try {
37
+ await Promise.race([Promise.resolve(fn()), timeoutPromise]);
38
+ }
39
+ finally {
40
+ if (timeoutId)
41
+ clearTimeout(timeoutId);
42
+ }
43
+ }
44
+ export class LifecycleDispatcher {
45
+ mode;
46
+ globalHooks;
47
+ constructor(mode = 'single', globalHooks = { before: [], after: [] }) {
48
+ this.mode = mode;
49
+ this.globalHooks = globalHooks;
50
+ }
51
+ async runGlobalBefore() {
52
+ if (this.mode !== 'global')
53
+ return;
54
+ for (const hook of this.globalHooks.before) {
55
+ try {
56
+ await runHook(hook);
57
+ }
58
+ catch (err) {
59
+ throw new HookExecutionError('globalBefore', err);
60
+ }
61
+ }
62
+ }
63
+ async runGlobalAfter() {
64
+ if (this.mode !== 'global')
65
+ return;
66
+ let firstError = null;
67
+ for (const hook of this.globalHooks.after) {
68
+ try {
69
+ await runHook(hook);
70
+ }
71
+ catch (err) {
72
+ if (!firstError)
73
+ firstError = err;
74
+ }
75
+ }
76
+ if (firstError) {
77
+ throw new HookExecutionError('globalAfter', firstError);
78
+ }
79
+ }
80
+ async executeSuite(suiteName, suiteHooks, testCases) {
81
+ const results = [];
82
+ let suiteLevelError;
83
+ // 1. Run suite before hook
84
+ let beforeSuiteFailed = false;
85
+ for (const hook of suiteHooks.before) {
86
+ try {
87
+ await runHook(hook);
88
+ }
89
+ catch (err) {
90
+ beforeSuiteFailed = true;
91
+ suiteLevelError = new HookExecutionError('before', err);
92
+ break;
93
+ }
94
+ }
95
+ if (beforeSuiteFailed) {
96
+ // All test cases in suite are skipped / marked ERROR
97
+ for (const tc of testCases) {
98
+ results.push({
99
+ id: tc.id,
100
+ title: tc.title,
101
+ status: 'ERROR',
102
+ error: suiteLevelError,
103
+ durationMs: 0,
104
+ });
105
+ }
106
+ // Always execute suite after hook even on before failure
107
+ for (const hook of suiteHooks.after) {
108
+ try {
109
+ await runHook(hook);
110
+ }
111
+ catch { }
112
+ }
113
+ return {
114
+ suiteName,
115
+ results,
116
+ status: 'ERROR',
117
+ error: suiteLevelError,
118
+ };
119
+ }
120
+ // 2. Run each test case with beforeEach and afterEach
121
+ for (const tc of testCases) {
122
+ const startTime = Date.now();
123
+ let tcStatus = 'PASSED';
124
+ let tcError;
125
+ let beforeEachFailed = false;
126
+ // Run beforeEach hooks
127
+ for (const hook of suiteHooks.beforeEach) {
128
+ try {
129
+ await runHook(hook);
130
+ }
131
+ catch (err) {
132
+ beforeEachFailed = true;
133
+ tcStatus = 'ERROR';
134
+ tcError = new HookExecutionError('beforeEach', err);
135
+ break;
136
+ }
137
+ }
138
+ // Run test body if beforeEach passed
139
+ if (!beforeEachFailed) {
140
+ try {
141
+ await tc.fn();
142
+ tcStatus = 'PASSED';
143
+ }
144
+ catch (err) {
145
+ tcStatus = 'FAILED';
146
+ tcError = err instanceof Error ? err : new Error(String(err));
147
+ }
148
+ }
149
+ // afterEach is GUARANTEED to execute even if beforeEach or test body failed
150
+ for (const hook of suiteHooks.afterEach) {
151
+ try {
152
+ await runHook(hook);
153
+ }
154
+ catch (err) {
155
+ // If test passed or failed, an afterEach failure transitions it to ERROR (Teardown Failure)
156
+ tcStatus = 'ERROR';
157
+ if (!tcError) {
158
+ tcError = new HookExecutionError('afterEach', err);
159
+ }
160
+ }
161
+ }
162
+ results.push({
163
+ id: tc.id,
164
+ title: tc.title,
165
+ status: tcStatus,
166
+ error: tcError,
167
+ durationMs: Date.now() - startTime,
168
+ });
169
+ }
170
+ // 3. Run suite after hook
171
+ for (const hook of suiteHooks.after) {
172
+ try {
173
+ await runHook(hook);
174
+ }
175
+ catch (err) {
176
+ if (!suiteLevelError) {
177
+ suiteLevelError = new HookExecutionError('after', err);
178
+ }
179
+ }
180
+ }
181
+ const hasErrors = results.some((r) => r.status === 'ERROR') || !!suiteLevelError;
182
+ const hasFailures = results.some((r) => r.status === 'FAILED');
183
+ return {
184
+ suiteName,
185
+ results,
186
+ status: hasErrors ? 'ERROR' : hasFailures ? 'FAILED' : 'PASSED',
187
+ error: suiteLevelError,
188
+ };
189
+ }
190
+ }
@@ -0,0 +1,3 @@
1
+ export * from './hook-discovery.js';
2
+ export * from './hook-dispatcher.js';
3
+ export * from './parallel-grouping.js';
@@ -0,0 +1,3 @@
1
+ export * from './hook-discovery.js';
2
+ export * from './hook-dispatcher.js';
3
+ export * from './parallel-grouping.js';
@@ -0,0 +1,27 @@
1
+ export type ParallelizationMode = 'suite' | 'testcase';
2
+ export interface WorkerSizingOptions {
3
+ maxWorkersCap?: number;
4
+ userConcurrency?: number;
5
+ cpuQuotaOverride?: number;
6
+ memoryBytesOverride?: number;
7
+ }
8
+ /**
9
+ * Detects actual CPU quota available to the process, accounting for
10
+ * Docker and Kubernetes cgroup v1 and cgroup v2 container limits.
11
+ */
12
+ export declare function detectContainerCpuQuota(): number;
13
+ /**
14
+ * Detects available memory in bytes, respecting container cgroup memory limits
15
+ * to prevent Linux OOM (Out of Memory) kills.
16
+ */
17
+ export declare function detectAvailableMemoryBytes(): number;
18
+ /**
19
+ * Dynamically computes optimal worker concurrency respecting:
20
+ * 1. Explicit user concurrency flag (--concurrency <N> or config)
21
+ * 2. Real container cgroups CPU quota & host CPU count
22
+ * 3. Low-CPU safety ratio (clamping <=2 CPUs to 1 worker, 3-4 CPUs to 2 workers)
23
+ * 4. Memory-aware guard (~600MB RAM budget per Chromium instance to prevent OOM kills)
24
+ * 5. Target items count & maximum worker cap
25
+ */
26
+ export declare function calculateWorkerCount(targetItems: any[], options?: WorkerSizingOptions | number): number;
27
+ export declare function groupSpecs(specPaths: string[], mode?: ParallelizationMode): (string | string[])[];
@@ -0,0 +1,151 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ /**
5
+ * Detects actual CPU quota available to the process, accounting for
6
+ * Docker and Kubernetes cgroup v1 and cgroup v2 container limits.
7
+ */
8
+ export function detectContainerCpuQuota() {
9
+ try {
10
+ // 1. Check cgroup v2 (Unified Hierarchy): /sys/fs/cgroup/cpu.max
11
+ // Format: "<quota> <period>" e.g. "200000 100000" or "max 100000"
12
+ if (fs.existsSync('/sys/fs/cgroup/cpu.max')) {
13
+ const content = fs.readFileSync('/sys/fs/cgroup/cpu.max', 'utf-8').trim();
14
+ const [quotaStr, periodStr] = content.split(/\s+/);
15
+ if (quotaStr && quotaStr !== 'max' && periodStr) {
16
+ const quota = parseFloat(quotaStr);
17
+ const period = parseFloat(periodStr);
18
+ if (quota > 0 && period > 0) {
19
+ return Math.max(0.5, quota / period);
20
+ }
21
+ }
22
+ }
23
+ // 2. Check cgroup v1: /sys/fs/cgroup/cpu/cpu.cfs_quota_us and cpu.cfs_period_us
24
+ if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
25
+ const quota = parseFloat(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf-8').trim());
26
+ const period = parseFloat(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf-8').trim());
27
+ if (quota > 0 && period > 0) {
28
+ return Math.max(0.5, quota / period);
29
+ }
30
+ }
31
+ }
32
+ catch { }
33
+ // 3. Fallback to host CPU count
34
+ if (typeof os.availableParallelism === 'function') {
35
+ try {
36
+ return os.availableParallelism();
37
+ }
38
+ catch { }
39
+ }
40
+ return os.cpus()?.length || 2;
41
+ }
42
+ /**
43
+ * Detects available memory in bytes, respecting container cgroup memory limits
44
+ * to prevent Linux OOM (Out of Memory) kills.
45
+ */
46
+ export function detectAvailableMemoryBytes() {
47
+ try {
48
+ // 1. Check cgroup v2: /sys/fs/cgroup/memory.max and /sys/fs/cgroup/memory.current
49
+ if (fs.existsSync('/sys/fs/cgroup/memory.max')) {
50
+ const maxStr = fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf-8').trim();
51
+ if (maxStr !== 'max') {
52
+ const limit = parseFloat(maxStr);
53
+ let usage = 0;
54
+ if (fs.existsSync('/sys/fs/cgroup/memory.current')) {
55
+ usage = parseFloat(fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf-8').trim()) || 0;
56
+ }
57
+ if (limit > 0) {
58
+ return Math.max(0, limit - usage);
59
+ }
60
+ }
61
+ }
62
+ // 2. Check cgroup v1: /sys/fs/cgroup/memory/memory.limit_in_bytes
63
+ if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
64
+ const limit = parseFloat(fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf-8').trim());
65
+ // cgroup v1 sets a huge number (e.g. 9223372036854771712) when no limit is enforced
66
+ if (limit > 0 && limit < 1e15) {
67
+ let usage = 0;
68
+ if (fs.existsSync('/sys/fs/cgroup/memory/memory.usage_in_bytes')) {
69
+ usage = parseFloat(fs.readFileSync('/sys/fs/cgroup/memory/memory.usage_in_bytes', 'utf-8').trim()) || 0;
70
+ }
71
+ return Math.max(0, limit - usage);
72
+ }
73
+ }
74
+ }
75
+ catch { }
76
+ // 3. Fallback to host available memory (estimating reclaimable cache/buffer on modern OS)
77
+ try {
78
+ const free = os.freemem();
79
+ const total = os.totalmem();
80
+ return Math.max(free, total * 0.4);
81
+ }
82
+ catch {
83
+ return 1024 * 1024 * 1024; // 1 GB fallback
84
+ }
85
+ }
86
+ /**
87
+ * Dynamically computes optimal worker concurrency respecting:
88
+ * 1. Explicit user concurrency flag (--concurrency <N> or config)
89
+ * 2. Real container cgroups CPU quota & host CPU count
90
+ * 3. Low-CPU safety ratio (clamping <=2 CPUs to 1 worker, 3-4 CPUs to 2 workers)
91
+ * 4. Memory-aware guard (~600MB RAM budget per Chromium instance to prevent OOM kills)
92
+ * 5. Target items count & maximum worker cap
93
+ */
94
+ export function calculateWorkerCount(targetItems, options) {
95
+ const opts = typeof options === 'number' ? { maxWorkersCap: options } : options || {};
96
+ const itemCount = targetItems.length;
97
+ if (itemCount === 0)
98
+ return 1;
99
+ // 1. Explicit User Concurrency has absolute priority
100
+ if (opts.userConcurrency && opts.userConcurrency > 0) {
101
+ return Math.min(opts.userConcurrency, itemCount);
102
+ }
103
+ // 2. Determine CPU Quota (cgroups aware)
104
+ const cpuCount = opts.cpuQuotaOverride !== undefined ? opts.cpuQuotaOverride : detectContainerCpuQuota();
105
+ // 3. Low-CPU Safety Ratio:
106
+ // - CPU <= 2: Max 1 worker (prevents severe thrashing on 2-vCPU CI runners)
107
+ // - CPU 3-4: Max 2 workers
108
+ // - CPU > 4: cpuCount - 2 (leaves 2 cores for OS/IDE/DevTools)
109
+ let cpuBasedWorkers = 1;
110
+ if (cpuCount <= 2) {
111
+ cpuBasedWorkers = 1;
112
+ }
113
+ else if (cpuCount <= 4) {
114
+ cpuBasedWorkers = 2;
115
+ }
116
+ else {
117
+ cpuBasedWorkers = Math.max(1, Math.floor(cpuCount - 2));
118
+ }
119
+ // 4. Memory-Aware Guard (Anti-OOM):
120
+ // Allocate ~600MB per Chromium browser instance
121
+ const MEMORY_PER_WORKER_BYTES = 600 * 1024 * 1024;
122
+ const availableMemBytes = opts.memoryBytesOverride !== undefined ? opts.memoryBytesOverride : detectAvailableMemoryBytes();
123
+ const memBasedWorkers = Math.max(1, Math.floor(availableMemBytes / MEMORY_PER_WORKER_BYTES));
124
+ // 5. Apply Maximum Cap and Item Count Clamping
125
+ const maxCap = opts.maxWorkersCap || 6;
126
+ return Math.max(1, Math.min(cpuBasedWorkers, memBasedWorkers, maxCap, itemCount));
127
+ }
128
+ export function groupSpecs(specPaths, mode = 'suite') {
129
+ if (mode === 'testcase') {
130
+ return [...specPaths];
131
+ }
132
+ // Group by suite directory
133
+ const suiteGroups = new Map();
134
+ for (const specPath of specPaths) {
135
+ const normalized = specPath.replace(/\\/g, '/');
136
+ const parts = normalized.split('/');
137
+ let suiteKey = 'default';
138
+ const specsIdx = parts.findIndex((p) => p === 'specs' || p === 'suites');
139
+ if (specsIdx !== -1 && specsIdx + 1 < parts.length) {
140
+ suiteKey = parts.slice(0, specsIdx + 2).join('/');
141
+ }
142
+ else {
143
+ suiteKey = path.dirname(normalized);
144
+ }
145
+ if (!suiteGroups.has(suiteKey)) {
146
+ suiteGroups.set(suiteKey, []);
147
+ }
148
+ suiteGroups.get(suiteKey).push(specPath);
149
+ }
150
+ return Array.from(suiteGroups.values());
151
+ }
@@ -97,6 +97,8 @@ export function formatAction(actionKey, target, args = {}) {
97
97
  return `Scroll ${args.direction ?? '{direction}'} by ${args.pixels ?? '{pixels}'}px`;
98
98
  case 'swipe':
99
99
  return `Swipe ${args.direction ?? '{direction}'} by ${args.distance ?? '{distance}'}px`;
100
+ case 'upload':
101
+ return `Upload file "${args.files ?? args.value ?? args.file ?? '{file}'}" to ${el}`;
100
102
  case 'intercept':
101
103
  return `Intercept ${args.method ?? '{method}'} "${args.url ?? '{url}'}" mock rule registered`;
102
104
  case 'clearCookies':
@@ -1,2 +1,2 @@
1
- export type ActionKey = 'click' | 'doubleClick' | 'rightClick' | 'type' | 'clear' | 'select' | 'hover' | 'dragDrop' | 'scrollIntoView' | 'longPress' | 'waitForElement' | 'navigate' | 'back' | 'forward' | 'refresh' | 'setViewport' | 'wait' | 'pressKey' | 'scroll' | 'swipe' | 'intercept' | 'clearCookies' | 'setCookies' | 'clearLocalStorage';
1
+ export type ActionKey = 'click' | 'doubleClick' | 'rightClick' | 'type' | 'clear' | 'select' | 'hover' | 'dragDrop' | 'scrollIntoView' | 'longPress' | 'waitForElement' | 'navigate' | 'back' | 'forward' | 'refresh' | 'setViewport' | 'wait' | 'pressKey' | 'scroll' | 'swipe' | 'upload' | 'intercept' | 'clearCookies' | 'setCookies' | 'clearLocalStorage';
2
2
  export type AssertionKey = 'be.visible' | 'not.be.visible' | 'exist' | 'not.exist' | 'be.clickable' | 'not.be.clickable' | 'be.enabled' | 'be.disabled' | 'be.checked' | 'not.be.checked' | 'be.selected' | 'not.be.selected' | 'be.focused' | 'not.be.focused' | 'have.text' | 'not.have.text' | 'contain.text' | 'not.contain.text' | 'have.value' | 'not.have.value' | 'contain.value' | 'not.contain.value' | 'have.attr' | 'not.have.attr' | 'have.class' | 'not.have.class' | 'have.css' | 'not.have.css' | 'have.length' | 'not.have.length' | 'have.length.greaterThan' | 'have.length.lessThan' | 'be.empty' | 'not.be.empty' | 'shouldHaveUrl' | 'shouldContainUrl' | 'shouldHaveTitle' | 'shouldContainTitle' | 'shouldBeLoaded' | 'shouldBePageLoaded' | 'shouldHaveNoConsoleErrors' | 'have.url' | 'contain.url' | 'have.title' | 'contain.title' | 'be.loaded' | 'be.pageLoaded' | 'have.noConsoleErrors';
@@ -18,6 +18,8 @@ export interface RunOptions {
18
18
  concurrency?: number;
19
19
  failedOnly?: boolean;
20
20
  failedTarget?: string;
21
+ debug?: boolean;
22
+ inspectPort?: number;
21
23
  }
22
24
  export declare class RustCoreBridge {
23
25
  static resolveBinaryPath(): string;
@@ -121,6 +121,8 @@ export class RustCoreBridge {
121
121
  concurrency: options.concurrency,
122
122
  failedOnly: options.failedOnly,
123
123
  failedTarget: options.failedTarget,
124
+ debug: options.debug,
125
+ inspectPort: options.inspectPort,
124
126
  });
125
127
  const home = process.env.HOME || process.env.USERPROFILE || '';
126
128
  const globalDriversDir = path.join(home, '.testspectra', 'drivers');
@@ -162,7 +164,8 @@ export class RustCoreBridge {
162
164
  // a real TTY (CI, or an external tool like the VS Code extension spawning this process
163
165
  // via a pipe) — in that case this raw line is the *only* live signal a wrapping process
164
166
  // can see, so it must still reach stdout verbatim.
165
- if (!reporter.isInteractiveMode()) {
167
+ // Always forward inspector websocket URL so IDE tools can attach directly.
168
+ if (line.startsWith('[TESTSPECTRA_DEBUG_INSPECTOR]') || !reporter.isInteractiveMode()) {
166
169
  console.log(line);
167
170
  }
168
171
  continue;
@@ -1,55 +1,17 @@
1
- export interface TestLog {
2
- timestamp: string;
3
- level: 'INFO' | 'SUCCESS' | 'WARNING' | 'ERROR' | 'DEBUG' | string;
4
- message: string;
5
- }
6
- export interface NetworkResource {
7
- requestId: string;
8
- url: string;
9
- method: string;
10
- status: number;
11
- }
12
- export interface TestRunResult {
13
- status: 'passed' | 'failed' | 'error' | string;
14
- duration: string;
15
- passedCount: number;
16
- failedCount: number;
17
- logs: TestLog[];
18
- networkResources?: NetworkResource[];
19
- }
20
- export interface PendingStep {
21
- text: string;
22
- duration: number;
23
- passed: boolean;
24
- }
25
- export interface PendingTestCase {
26
- title: string;
27
- workerId?: number;
28
- steps: PendingStep[];
29
- currentStep?: string;
30
- errorMsg?: string;
31
- startTime: number;
32
- }
33
- export declare class Reporter {
34
- private logs;
35
- private networkEvents;
36
- private pendingTests;
37
- private workerToTest;
38
- private pendingErrorMsg?;
39
- private passedCount;
40
- private failedCount;
1
+ import { type ActionKey, type AssertionKey, type ExecutionSummary, formatAction, formatAssertion, type NetworkResource, parseAndFormat, type PendingStep, type PendingTestCase, type ReporterOptions, SemanticReporter, stripAnsi, type TestLog, type TestRunResult } from '@testspectra/reporter';
2
+ export { type ActionKey, type AssertionKey, type ExecutionSummary, formatAction, formatAssertion, type NetworkResource, parseAndFormat, type PendingStep, type PendingTestCase, type ReporterOptions, SemanticReporter, stripAnsi, type TestLog, type TestRunResult, };
3
+ export declare class Reporter extends SemanticReporter {
41
4
  private isInteractive;
42
5
  private lastRenderedLines;
43
6
  private spinnerFrames;
44
7
  private spinnerIndex;
45
8
  private spinnerTimer;
46
- constructor();
9
+ constructor(options?: ReporterOptions);
47
10
  /** Whether this reporter is drawing the live-updating interactive terminal UI (real TTY, not CI/VS Code/test). */
48
11
  isInteractiveMode(): boolean;
49
12
  private clearActiveSlots;
50
13
  private renderActiveSlots;
51
- private resolveTestCase;
52
- addLog(log: TestLog): void;
14
+ addLog(input: string | TestLog): void;
53
15
  addNetwork(res: NetworkResource): void;
54
16
  getLogs(): TestLog[];
55
17
  getNetworkEvents(): NetworkResource[];