@voltkit/volt-test 0.1.2

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 (52) hide show
  1. package/dist/artifacts.d.ts +18 -0
  2. package/dist/artifacts.js +109 -0
  3. package/dist/artifacts.js.map +1 -0
  4. package/dist/config.d.ts +16 -0
  5. package/dist/config.js +154 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/drivers/file-dialog.d.ts +22 -0
  8. package/dist/drivers/file-dialog.js +119 -0
  9. package/dist/drivers/file-dialog.js.map +1 -0
  10. package/dist/drivers/index.d.ts +6 -0
  11. package/dist/drivers/index.js +4 -0
  12. package/dist/drivers/index.js.map +1 -0
  13. package/dist/drivers/menu.d.ts +17 -0
  14. package/dist/drivers/menu.js +34 -0
  15. package/dist/drivers/menu.js.map +1 -0
  16. package/dist/drivers/tray.d.ts +13 -0
  17. package/dist/drivers/tray.js +27 -0
  18. package/dist/drivers/tray.js.map +1 -0
  19. package/dist/fs.d.ts +9 -0
  20. package/dist/fs.js +49 -0
  21. package/dist/fs.js.map +1 -0
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.js +7 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/launcher.d.ts +42 -0
  26. package/dist/launcher.js +172 -0
  27. package/dist/launcher.js.map +1 -0
  28. package/dist/path.d.ts +1 -0
  29. package/dist/path.js +11 -0
  30. package/dist/path.js.map +1 -0
  31. package/dist/process.d.ts +10 -0
  32. package/dist/process.js +71 -0
  33. package/dist/process.js.map +1 -0
  34. package/dist/runner.d.ts +11 -0
  35. package/dist/runner.js +230 -0
  36. package/dist/runner.js.map +1 -0
  37. package/dist/suites/hello-world.d.ts +15 -0
  38. package/dist/suites/hello-world.js +139 -0
  39. package/dist/suites/hello-world.js.map +1 -0
  40. package/dist/suites/index.d.ts +2 -0
  41. package/dist/suites/index.js +3 -0
  42. package/dist/suites/index.js.map +1 -0
  43. package/dist/suites/ipc-demo.d.ts +26 -0
  44. package/dist/suites/ipc-demo.js +287 -0
  45. package/dist/suites/ipc-demo.js.map +1 -0
  46. package/dist/types.d.ts +48 -0
  47. package/dist/types.js +2 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/window.d.ts +13 -0
  50. package/dist/window.js +65 -0
  51. package/dist/window.js.map +1 -0
  52. package/package.json +36 -0
package/dist/fs.js ADDED
@@ -0,0 +1,49 @@
1
+ import { basename, join, resolve } from 'node:path';
2
+ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ const EXCLUDED_COPY_DIRS = new Set([
5
+ 'dist',
6
+ 'dist-volt',
7
+ '.turbo',
8
+ 'target',
9
+ 'coverage',
10
+ ]);
11
+ export function copyProjectToTemp(projectDir, repoRoot) {
12
+ const resolvedSource = resolve(projectDir);
13
+ if (!existsSync(resolvedSource)) {
14
+ throw new Error(`[volt:test] source project does not exist: ${resolvedSource}`);
15
+ }
16
+ const tempRoot = mkdtempSync(join(resolve(repoRoot), '.volt-test-'));
17
+ const tempProjectDir = join(tempRoot, basename(resolvedSource));
18
+ cpSync(resolvedSource, tempProjectDir, {
19
+ recursive: true,
20
+ force: true,
21
+ filter: (sourcePath) => {
22
+ const name = basename(sourcePath);
23
+ return !EXCLUDED_COPY_DIRS.has(name);
24
+ },
25
+ });
26
+ return { tempRoot, tempProjectDir };
27
+ }
28
+ export async function cleanupDirectoryBestEffort(directoryPath, logger) {
29
+ for (let attempt = 0; attempt < 8; attempt += 1) {
30
+ try {
31
+ rmSync(directoryPath, { recursive: true, force: true });
32
+ return;
33
+ }
34
+ catch (error) {
35
+ if (attempt === 7) {
36
+ logger.warn(`[volt:test] cleanup warning for ${directoryPath}: ${error instanceof Error ? error.message : String(error)}`);
37
+ return;
38
+ }
39
+ await delay(250);
40
+ }
41
+ }
42
+ }
43
+ export function readTextFile(filePath) {
44
+ return readFileSync(filePath, 'utf8');
45
+ }
46
+ export function writeTextFile(filePath, contents) {
47
+ writeFileSync(filePath, contents, 'utf8');
48
+ }
49
+ //# sourceMappingURL=fs.js.map
package/dist/fs.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/F,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAG3D,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,MAAM;IACN,WAAW;IACX,QAAQ;IACR,QAAQ;IACR,UAAU;CACX,CAAC,CAAC;AAOH,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,QAAgB;IACpE,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3C,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,8CAA8C,cAAc,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;IACrE,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC;IAEhE,MAAM,CAAC,cAAc,EAAE,cAAc,EAAE;QACrC,SAAS,EAAE,IAAI;QACf,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE;YACrB,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YAClC,OAAO,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,aAAqB,EAAE,MAAsB;IAC5F,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,CAAC;YACH,MAAM,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,OAAO;QACT,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;gBAClB,MAAM,CAAC,IAAI,CACT,mCAAmC,aAAa,KAC9C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACH,CAAC;gBACF,OAAO;YACT,CAAC;YACD,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,QAAgB;IAC9D,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { defineTestConfig, loadVoltTestConfig, validateTestConfig, } from './config.js';
2
+ export { assertWindowReady, parseWindowStatus, waitForWindowStatus, } from './window.js';
3
+ export type { LoadedVoltTestConfig, LoadVoltTestConfigOptions, RunSuitesOptions, VoltTestArtifactCaptureResult, VoltTestConfig, VoltTestLogger, VoltTestSuite, VoltTestSuiteContext, } from './types.js';
4
+ export { runSuites } from './runner.js';
5
+ export { VoltAppLauncher } from './launcher.js';
6
+ export { FileDialogAutomationDriver, MenuAutomationDriver, TrayAutomationDriver, } from './drivers/index.js';
7
+ export { createHelloWorldSmokeSuite, createIpcDemoSmokeSuite, } from './suites/index.js';
8
+ export type { AutomationEvent, FileDialogAutomationDriverOptions, FileDialogAutomationPlatform, MenuAutomationDriverOptions, MenuSetupState, OpenDialogAutomationResult, SaveDialogAutomationResult, TrayAutomationDriverOptions, TraySetupState, } from './drivers/index.js';
9
+ export type { HelloWorldSmokePayload, HelloWorldSmokeSuiteOptions, IpcDemoSmokePayload, IpcDemoSmokeSuiteOptions, } from './suites/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { defineTestConfig, loadVoltTestConfig, validateTestConfig, } from './config.js';
2
+ export { assertWindowReady, parseWindowStatus, waitForWindowStatus, } from './window.js';
3
+ export { runSuites } from './runner.js';
4
+ export { VoltAppLauncher } from './launcher.js';
5
+ export { FileDialogAutomationDriver, MenuAutomationDriver, TrayAutomationDriver, } from './drivers/index.js';
6
+ export { createHelloWorldSmokeSuite, createIpcDemoSmokeSuite, } from './suites/index.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAWrB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,42 @@
1
+ import type { VoltTestLogger } from './types.js';
2
+ export interface VoltAppLauncherOptions {
3
+ repoRoot: string;
4
+ cliEntryPath: string;
5
+ logger: VoltTestLogger;
6
+ }
7
+ export interface RunScenarioOptions<TPayload> {
8
+ sourceProjectDir: string;
9
+ resultFile: string;
10
+ timeoutMs?: number;
11
+ prepareProject?: (projectDir: string) => Promise<void> | void;
12
+ validatePayload?: (payload: unknown) => TPayload;
13
+ preserveTempDir?: boolean;
14
+ artifactsDir?: string;
15
+ logFileName?: string;
16
+ screenshotFileName?: string;
17
+ captureScreenshotOnError?: boolean;
18
+ }
19
+ /**
20
+ * Runs a Volt app scenario in an isolated temp project copy.
21
+ * The launcher builds the copied project, launches the produced runtime artifact,
22
+ * waits for a JSON result file, validates the payload, and enforces process cleanup.
23
+ */
24
+ export declare class VoltAppLauncher {
25
+ private readonly repoRoot;
26
+ private readonly cliEntryPath;
27
+ private readonly logger;
28
+ constructor(options: VoltAppLauncherOptions);
29
+ /**
30
+ * Execute one scenario and return its validated payload.
31
+ * Set `preserveTempDir` when debugging to inspect generated temp artifacts.
32
+ */
33
+ run<TPayload = unknown>(options: RunScenarioOptions<TPayload>): Promise<TPayload>;
34
+ private ensureCliEntry;
35
+ private buildProject;
36
+ private launchBinary;
37
+ }
38
+ declare function resolveRuntimeBinary(projectDir: string): string;
39
+ export declare const __testOnly: {
40
+ resolveRuntimeBinary: typeof resolveRuntimeBinary;
41
+ };
42
+ export {};
@@ -0,0 +1,172 @@
1
+ import { execFileSync, spawn } from 'node:child_process';
2
+ import { createWriteStream, existsSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { captureDesktopScreenshot, writeJsonArtifact } from './artifacts.js';
5
+ import { cleanupDirectoryBestEffort, copyProjectToTemp } from './fs.js';
6
+ import { readJsonFileWithRetry, terminateChildProcess, waitForChildExit, waitForFile } from './process.js';
7
+ const DEFAULT_LAUNCH_TIMEOUT_MS = 120_000;
8
+ const DEFAULT_PROCESS_EXIT_TIMEOUT_MS = 30_000;
9
+ /**
10
+ * Runs a Volt app scenario in an isolated temp project copy.
11
+ * The launcher builds the copied project, launches the produced runtime artifact,
12
+ * waits for a JSON result file, validates the payload, and enforces process cleanup.
13
+ */
14
+ export class VoltAppLauncher {
15
+ repoRoot;
16
+ cliEntryPath;
17
+ logger;
18
+ constructor(options) {
19
+ this.repoRoot = resolve(options.repoRoot);
20
+ this.cliEntryPath = resolve(options.cliEntryPath);
21
+ this.logger = options.logger;
22
+ }
23
+ /**
24
+ * Execute one scenario and return its validated payload.
25
+ * Set `preserveTempDir` when debugging to inspect generated temp artifacts.
26
+ */
27
+ async run(options) {
28
+ this.ensureCliEntry();
29
+ const sourceProjectDir = resolve(this.repoRoot, options.sourceProjectDir);
30
+ const timeoutMs = options.timeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS;
31
+ const captureScreenshotOnError = options.captureScreenshotOnError ?? true;
32
+ const copied = copyProjectToTemp(sourceProjectDir, this.repoRoot);
33
+ const resultPath = join(copied.tempProjectDir, options.resultFile);
34
+ const artifactsDir = options.artifactsDir ? resolve(options.artifactsDir) : null;
35
+ const processLogPath = artifactsDir ? join(artifactsDir, options.logFileName ?? 'app-process.log') : null;
36
+ const screenshotPath = artifactsDir ? join(artifactsDir, options.screenshotFileName ?? 'failure.png') : null;
37
+ let logStream = null;
38
+ let child = null;
39
+ if (artifactsDir) {
40
+ mkdirSync(artifactsDir, { recursive: true });
41
+ if (processLogPath) {
42
+ logStream = createWriteStream(processLogPath, { flags: 'a' });
43
+ }
44
+ }
45
+ try {
46
+ if (options.prepareProject) {
47
+ await options.prepareProject(copied.tempProjectDir);
48
+ }
49
+ this.logger.log(`[volt:test] building ${options.sourceProjectDir}`);
50
+ this.buildProject(copied.tempProjectDir);
51
+ const runtimeBinaryPath = resolveRuntimeBinary(copied.tempProjectDir);
52
+ this.logger.log(`[volt:test] launching ${runtimeBinaryPath}`);
53
+ child = this.launchBinary(runtimeBinaryPath, copied.tempProjectDir);
54
+ attachChildOutput(child, logStream);
55
+ const resultReady = await waitForFile(resultPath, timeoutMs);
56
+ if (!resultReady) {
57
+ if (child) {
58
+ await terminateChildProcess(child, `timeout waiting for ${options.resultFile}`, this.logger);
59
+ }
60
+ throw new Error(`[volt:test] timed out waiting for result file "${options.resultFile}" from ${options.sourceProjectDir}`);
61
+ }
62
+ const payload = await readJsonFileWithRetry(resultPath, 2_000);
63
+ const validatedPayload = options.validatePayload
64
+ ? options.validatePayload(payload)
65
+ : payload;
66
+ if (artifactsDir) {
67
+ writeJsonArtifact(join(artifactsDir, 'result-payload.json'), payload);
68
+ }
69
+ if (child) {
70
+ const exitResult = await waitForChildExit(child, DEFAULT_PROCESS_EXIT_TIMEOUT_MS);
71
+ if (!exitResult) {
72
+ await terminateChildProcess(child, 'app did not exit after result write', this.logger);
73
+ throw new Error(`[volt:test] app for ${options.sourceProjectDir} did not exit after reporting completion`);
74
+ }
75
+ if (exitResult.code !== 0) {
76
+ throw new Error(`[volt:test] app for ${options.sourceProjectDir} exited with code ${exitResult.code} (signal: ${exitResult.signal ?? 'none'})`);
77
+ }
78
+ }
79
+ return validatedPayload;
80
+ }
81
+ catch (error) {
82
+ if (captureScreenshotOnError && screenshotPath) {
83
+ await captureDesktopScreenshot(screenshotPath, this.logger);
84
+ }
85
+ throw error;
86
+ }
87
+ finally {
88
+ if (child && (child.exitCode === null && child.signalCode === null)) {
89
+ await terminateChildProcess(child, 'suite cleanup', this.logger);
90
+ }
91
+ if (logStream) {
92
+ await closeWriteStream(logStream);
93
+ }
94
+ if (!options.preserveTempDir) {
95
+ await cleanupDirectoryBestEffort(copied.tempRoot, this.logger);
96
+ }
97
+ else {
98
+ this.logger.log(`[volt:test] preserved temp directory: ${copied.tempRoot}`);
99
+ }
100
+ }
101
+ }
102
+ ensureCliEntry() {
103
+ if (!existsSync(this.cliEntryPath)) {
104
+ throw new Error(`[volt:test] volt CLI entry not found: ${this.cliEntryPath}. Build @voltkit/volt-cli first.`);
105
+ }
106
+ }
107
+ buildProject(projectDir) {
108
+ execFileSync('node', [this.cliEntryPath, 'build'], {
109
+ cwd: projectDir,
110
+ stdio: 'inherit',
111
+ env: process.env,
112
+ });
113
+ }
114
+ launchBinary(binaryPath, cwd) {
115
+ if (process.platform === 'linux') {
116
+ return spawn('xvfb-run', ['-a', binaryPath], {
117
+ cwd,
118
+ stdio: ['ignore', 'pipe', 'pipe'],
119
+ env: process.env,
120
+ });
121
+ }
122
+ return spawn(binaryPath, [], {
123
+ cwd,
124
+ stdio: ['ignore', 'pipe', 'pipe'],
125
+ env: process.env,
126
+ });
127
+ }
128
+ }
129
+ function resolveRuntimeBinary(projectDir) {
130
+ const manifestPath = join(projectDir, 'dist-volt', '.volt-runtime-artifact.json');
131
+ if (!existsSync(manifestPath)) {
132
+ throw new Error(`[volt:test] runtime manifest missing: ${manifestPath}`);
133
+ }
134
+ const manifestRaw = readFileSync(manifestPath, 'utf8');
135
+ const manifest = JSON.parse(manifestRaw);
136
+ if (!manifest || typeof manifest.artifactFileName !== 'string' || manifest.artifactFileName.length === 0) {
137
+ throw new Error(`[volt:test] invalid runtime manifest at ${manifestPath}`);
138
+ }
139
+ const binaryPath = join(projectDir, 'dist-volt', manifest.artifactFileName);
140
+ if (!existsSync(binaryPath)) {
141
+ throw new Error(`[volt:test] runtime binary missing: ${binaryPath}`);
142
+ }
143
+ return binaryPath;
144
+ }
145
+ function attachChildOutput(child, logStream) {
146
+ if (child.stdout) {
147
+ child.stdout.on('data', (chunk) => {
148
+ process.stdout.write(chunk);
149
+ if (logStream) {
150
+ logStream.write(chunk);
151
+ }
152
+ });
153
+ }
154
+ if (child.stderr) {
155
+ child.stderr.on('data', (chunk) => {
156
+ process.stderr.write(chunk);
157
+ if (logStream) {
158
+ logStream.write(chunk);
159
+ }
160
+ });
161
+ }
162
+ }
163
+ async function closeWriteStream(stream) {
164
+ await new Promise((resolveStream, reject) => {
165
+ stream.end(() => resolveStream());
166
+ stream.once('error', (error) => reject(error));
167
+ });
168
+ }
169
+ export const __testOnly = {
170
+ resolveRuntimeBinary,
171
+ };
172
+ //# sourceMappingURL=launcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launcher.js","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"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"}
package/dist/path.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function sanitizePathSegment(value: string): string;
package/dist/path.js ADDED
@@ -0,0 +1,11 @@
1
+ export function sanitizePathSegment(value) {
2
+ const normalized = value
3
+ .trim()
4
+ .replace(/[\\/:*?"<>|]/g, '-')
5
+ .replace(/\s+/g, '-')
6
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
7
+ .replace(/-+/g, '-')
8
+ .replace(/^-|-$/g, '');
9
+ return normalized.length > 0 ? normalized : 'suite';
10
+ }
11
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,MAAM,UAAU,GAAG,KAAK;SACrB,IAAI,EAAE;SACN,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC;SAChC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;AACtD,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import type { VoltTestLogger } from './types.js';
3
+ export interface ChildExitResult {
4
+ code: number | null;
5
+ signal: NodeJS.Signals | null;
6
+ }
7
+ export declare function waitForFile(filePath: string, timeoutMs: number): Promise<boolean>;
8
+ export declare function waitForChildExit(child: ChildProcess, timeoutMs: number): Promise<ChildExitResult | null>;
9
+ export declare function terminateChildProcess(child: ChildProcess, reason: string, logger: VoltTestLogger): Promise<void>;
10
+ export declare function readJsonFileWithRetry<T>(filePath: string, timeoutMs: number): Promise<T>;
@@ -0,0 +1,71 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ export async function waitForFile(filePath, timeoutMs) {
4
+ const startedAt = Date.now();
5
+ while (Date.now() - startedAt <= timeoutMs) {
6
+ try {
7
+ readFileSync(filePath, 'utf8');
8
+ return true;
9
+ }
10
+ catch {
11
+ await delay(250);
12
+ }
13
+ }
14
+ return false;
15
+ }
16
+ export async function waitForChildExit(child, timeoutMs) {
17
+ if (child.exitCode !== null || child.signalCode !== null) {
18
+ return {
19
+ code: child.exitCode,
20
+ signal: child.signalCode,
21
+ };
22
+ }
23
+ const exitPromise = new Promise((resolve) => {
24
+ child.once('exit', (code, signal) => {
25
+ resolve({ code, signal });
26
+ });
27
+ });
28
+ const timeoutPromise = delay(timeoutMs).then(() => null);
29
+ return Promise.race([exitPromise, timeoutPromise]);
30
+ }
31
+ export async function terminateChildProcess(child, reason, logger) {
32
+ if (child.exitCode !== null || child.signalCode !== null) {
33
+ return;
34
+ }
35
+ try {
36
+ child.kill('SIGTERM');
37
+ }
38
+ catch (error) {
39
+ logger.warn(`[volt:test] failed to send SIGTERM (${reason}): ${error instanceof Error ? error.message : String(error)}`);
40
+ }
41
+ const gracefulExit = await waitForChildExit(child, 5_000);
42
+ if (gracefulExit) {
43
+ return;
44
+ }
45
+ try {
46
+ child.kill('SIGKILL');
47
+ }
48
+ catch (error) {
49
+ throw new Error(`[volt:test] failed to send SIGKILL (${reason}): ${error instanceof Error ? error.message : String(error)}`, { cause: error });
50
+ }
51
+ const forcedExit = await waitForChildExit(child, 5_000);
52
+ if (!forcedExit) {
53
+ throw new Error(`[volt:test] child process did not exit after SIGKILL (${reason})`);
54
+ }
55
+ }
56
+ export async function readJsonFileWithRetry(filePath, timeoutMs) {
57
+ const startedAt = Date.now();
58
+ let lastError;
59
+ while (Date.now() - startedAt <= timeoutMs) {
60
+ try {
61
+ const content = readFileSync(filePath, 'utf8');
62
+ return JSON.parse(content);
63
+ }
64
+ catch (error) {
65
+ lastError = error;
66
+ await delay(100);
67
+ }
68
+ }
69
+ throw new Error(`[volt:test] failed to parse JSON at ${filePath} within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
70
+ }
71
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { RunSuitesOptions, VoltTestConfig, VoltTestLogger, VoltTestSuite } from './types.js';
2
+ export declare function runSuites(config: VoltTestConfig, options: RunSuitesOptions): Promise<void>;
3
+ declare function selectSuites(suites: readonly VoltTestSuite[], names?: readonly string[]): VoltTestSuite[];
4
+ declare function withTimeout(promise: Promise<void>, timeoutMs: number, suiteName: string): Promise<void>;
5
+ declare function withPrefix(logger: VoltTestLogger, prefix: string): VoltTestLogger;
6
+ export declare const __testOnly: {
7
+ selectSuites: typeof selectSuites;
8
+ withTimeout: typeof withTimeout;
9
+ withPrefix: typeof withPrefix;
10
+ };
11
+ export {};
package/dist/runner.js ADDED
@@ -0,0 +1,230 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { captureDesktopScreenshot, createRunArtifactRoot, createSuiteAttemptArtifactDir, writeJsonArtifact, } from './artifacts.js';
3
+ import { validateTestConfig } from './config.js';
4
+ import { sanitizePathSegment } from './path.js';
5
+ const DEFAULT_TIMEOUT_MS = 120_000;
6
+ const DEFAULT_RETRIES = 0;
7
+ export async function runSuites(config, options) {
8
+ validateTestConfig(config, 'test config');
9
+ const logger = options.logger ?? toLogger(console);
10
+ const selectedSuites = selectSuites(config.suites, options.suiteNames);
11
+ const defaultTimeoutMs = options.timeoutMs ?? config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12
+ const retries = options.retries ?? config.retries ?? DEFAULT_RETRIES;
13
+ const captureScreenshots = options.captureScreenshots ?? true;
14
+ const repoRoot = resolve(options.repoRoot ?? process.cwd());
15
+ const cliEntryPath = resolve(options.cliEntryPath);
16
+ const runArtifactRoot = createRunArtifactRoot(repoRoot, options.artifactDir ?? config.artifactDir);
17
+ validateRetryCount(retries);
18
+ logger.log(`[volt:test] running ${selectedSuites.length} suite(s)`);
19
+ logger.log(`[volt:test] retries per suite: ${retries}`);
20
+ logger.log(`[volt:test] artifact root: ${runArtifactRoot}`);
21
+ const startedAt = Date.now();
22
+ const suiteSummaries = [];
23
+ let completed = 0;
24
+ let firstFailure = null;
25
+ for (const suite of selectedSuites) {
26
+ const suiteTimeoutMs = suite.timeoutMs ?? defaultTimeoutMs;
27
+ const suiteStartedAt = Date.now();
28
+ const suiteResult = await runSuiteWithRetries({
29
+ suite,
30
+ retries,
31
+ suiteTimeoutMs,
32
+ repoRoot,
33
+ cliEntryPath,
34
+ runArtifactRoot,
35
+ captureScreenshots,
36
+ logger,
37
+ });
38
+ const suiteSummary = suiteResult.summary;
39
+ suiteSummaries.push(suiteSummary);
40
+ completed += 1;
41
+ if (suiteSummary.passed) {
42
+ logger.log(`[volt:test] [${suite.name}] passed in ${suiteSummary.durationMs}ms`);
43
+ }
44
+ else {
45
+ logger.error(`[volt:test] [${suite.name}] failed in ${suiteSummary.durationMs}ms`);
46
+ }
47
+ if (suiteSummary.passed && suiteSummary.flaky) {
48
+ logger.warn(`[volt:test] [${suite.name}] marked flaky: passed after retry.`);
49
+ }
50
+ writeJsonArtifact(join(runArtifactRoot, sanitizePathSegment(suite.name), 'suite-summary.json'), {
51
+ ...suiteSummary,
52
+ finishedAt: new Date().toISOString(),
53
+ suiteTimeoutMs,
54
+ suiteDurationMs: Date.now() - suiteStartedAt,
55
+ });
56
+ if (suiteResult.error && !firstFailure) {
57
+ firstFailure = suiteResult.error;
58
+ break;
59
+ }
60
+ }
61
+ const finishedAt = Date.now();
62
+ const runSummary = {
63
+ startedAt: new Date(startedAt).toISOString(),
64
+ finishedAt: new Date(finishedAt).toISOString(),
65
+ durationMs: finishedAt - startedAt,
66
+ suites: suiteSummaries,
67
+ };
68
+ writeJsonArtifact(join(runArtifactRoot, 'run-summary.json'), runSummary);
69
+ const flakySuites = suiteSummaries.filter((suite) => suite.flaky).map((suite) => suite.name);
70
+ if (flakySuites.length > 0) {
71
+ writeJsonArtifact(join(runArtifactRoot, 'flake-report.json'), {
72
+ flakySuites,
73
+ count: flakySuites.length,
74
+ generatedAt: new Date().toISOString(),
75
+ });
76
+ }
77
+ logger.log(`[volt:test] completed ${completed}/${selectedSuites.length} suite(s) in ${finishedAt - startedAt}ms`);
78
+ if (firstFailure) {
79
+ throw firstFailure;
80
+ }
81
+ }
82
+ async function runSuiteWithRetries(args) {
83
+ const { suite, retries, suiteTimeoutMs, repoRoot, cliEntryPath, runArtifactRoot, captureScreenshots, logger } = args;
84
+ const attempts = [];
85
+ const maxAttempts = retries + 1;
86
+ const suiteStart = Date.now();
87
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
88
+ const attemptStart = Date.now();
89
+ const attemptArtifactDir = createSuiteAttemptArtifactDir(runArtifactRoot, suite.name, attempt);
90
+ const prefixedLogger = withPrefix(logger, `[volt:test] [${suite.name}] [attempt ${attempt}/${maxAttempts}]`);
91
+ prefixedLogger.log('start');
92
+ prefixedLogger.log(`artifacts: ${attemptArtifactDir}`);
93
+ const captureScreenshot = async (name = 'screenshot') => {
94
+ const screenshotPath = join(attemptArtifactDir, `${sanitizePathSegment(name)}.png`);
95
+ if (!captureScreenshots) {
96
+ return { path: screenshotPath, captured: false };
97
+ }
98
+ const captured = await captureDesktopScreenshot(screenshotPath, prefixedLogger);
99
+ return { path: screenshotPath, captured };
100
+ };
101
+ try {
102
+ await withTimeout(suite.run({
103
+ repoRoot,
104
+ cliEntryPath,
105
+ logger: prefixedLogger,
106
+ timeoutMs: suiteTimeoutMs,
107
+ suiteName: suite.name,
108
+ attempt,
109
+ artifactsDir: attemptArtifactDir,
110
+ captureScreenshot,
111
+ }), suiteTimeoutMs, suite.name);
112
+ attempts.push({
113
+ attempt,
114
+ durationMs: Date.now() - attemptStart,
115
+ status: 'passed',
116
+ });
117
+ return {
118
+ summary: {
119
+ name: suite.name,
120
+ attempts,
121
+ flaky: attempt > 1,
122
+ durationMs: Date.now() - suiteStart,
123
+ passed: true,
124
+ },
125
+ error: null,
126
+ };
127
+ }
128
+ catch (error) {
129
+ const message = error instanceof Error ? error.message : String(error);
130
+ let screenshotPath;
131
+ if (captureScreenshots) {
132
+ const screenshot = await captureScreenshot(`failure-${attempt}`);
133
+ if (screenshot.captured) {
134
+ screenshotPath = screenshot.path;
135
+ }
136
+ }
137
+ attempts.push({
138
+ attempt,
139
+ durationMs: Date.now() - attemptStart,
140
+ status: 'failed',
141
+ error: message,
142
+ screenshotPath,
143
+ });
144
+ prefixedLogger.error(message);
145
+ if (attempt < maxAttempts) {
146
+ prefixedLogger.warn('retrying after failure');
147
+ continue;
148
+ }
149
+ return {
150
+ summary: {
151
+ name: suite.name,
152
+ attempts,
153
+ flaky: false,
154
+ durationMs: Date.now() - suiteStart,
155
+ passed: false,
156
+ },
157
+ error,
158
+ };
159
+ }
160
+ }
161
+ const internalError = new Error('[volt:test] internal runner error: no suite attempts executed.');
162
+ return {
163
+ summary: {
164
+ name: suite.name,
165
+ attempts: [],
166
+ flaky: false,
167
+ durationMs: 0,
168
+ passed: false,
169
+ },
170
+ error: internalError,
171
+ };
172
+ }
173
+ function selectSuites(suites, names) {
174
+ if (!names || names.length === 0) {
175
+ return [...suites];
176
+ }
177
+ const wanted = new Set(names);
178
+ const selected = suites.filter((suite) => wanted.has(suite.name));
179
+ if (selected.length === 0) {
180
+ throw new Error(`[volt:test] none of the requested suites were found: ${names.join(', ')}`);
181
+ }
182
+ const missing = names.filter((name) => !selected.some((suite) => suite.name === name));
183
+ if (missing.length > 0) {
184
+ throw new Error(`[volt:test] unknown suite(s): ${missing.join(', ')}`);
185
+ }
186
+ return selected;
187
+ }
188
+ async function withTimeout(promise, timeoutMs, suiteName) {
189
+ let timeoutHandle = null;
190
+ try {
191
+ await Promise.race([
192
+ promise,
193
+ new Promise((_, reject) => {
194
+ timeoutHandle = setTimeout(() => {
195
+ reject(new Error(`[volt:test] suite "${suiteName}" timed out after ${timeoutMs}ms`));
196
+ }, timeoutMs);
197
+ }),
198
+ ]);
199
+ }
200
+ finally {
201
+ if (timeoutHandle) {
202
+ clearTimeout(timeoutHandle);
203
+ }
204
+ }
205
+ }
206
+ function withPrefix(logger, prefix) {
207
+ return {
208
+ log: (message) => logger.log(`${prefix} ${message}`),
209
+ warn: (message) => logger.warn(`${prefix} ${message}`),
210
+ error: (message) => logger.error(`${prefix} ${message}`),
211
+ };
212
+ }
213
+ function validateRetryCount(retries) {
214
+ if (!Number.isInteger(retries) || retries < 0) {
215
+ throw new Error('[volt:test] retries must be a non-negative integer.');
216
+ }
217
+ }
218
+ function toLogger(source) {
219
+ return {
220
+ log: (message) => source.log(message),
221
+ warn: (message) => source.warn(message),
222
+ error: (message) => source.error(message),
223
+ };
224
+ }
225
+ export const __testOnly = {
226
+ selectSuites,
227
+ withTimeout,
228
+ withPrefix,
229
+ };
230
+ //# sourceMappingURL=runner.js.map