@itmetsam/samurai-framework 0.16.0 → 0.18.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.
@@ -25,8 +25,14 @@ export declare const CAPABILITIES: {
25
25
  readonly testIds: true;
26
26
  /** `loadConfig` from `./project`: the config with every default applied, the way a run uses it. */
27
27
  readonly resolvedConfig: true;
28
+ /** `loadConfig({ written: true })`: the config as written next to the resolved one, and `writtenConfig`. */
29
+ readonly writtenConfig: true;
28
30
  /** The recorder's first `onStatus` call, `{ state: "recording", toolbar }`, right after it starts, and a `RecordResult` with every field always present. */
29
31
  readonly recorderStartStatus: true;
32
+ /** `outputDir` on `runTests` and `runFlowInProject`: a folder per run for the report and the screenshots. */
33
+ readonly outputDir: true;
34
+ /** `runFlowInProject`'s `outputDir` gives each node its own folder, `nodes/<node id>/`. */
35
+ readonly flowNodeFolders: true;
30
36
  };
31
37
  /** The shape of {@link CAPABILITIES}. */
32
38
  export type Capabilities = typeof CAPABILITIES;
@@ -25,6 +25,12 @@ export const CAPABILITIES = {
25
25
  testIds: true,
26
26
  /** `loadConfig` from `./project`: the config with every default applied, the way a run uses it. */
27
27
  resolvedConfig: true,
28
+ /** `loadConfig({ written: true })`: the config as written next to the resolved one, and `writtenConfig`. */
29
+ writtenConfig: true,
28
30
  /** The recorder's first `onStatus` call, `{ state: "recording", toolbar }`, right after it starts, and a `RecordResult` with every field always present. */
29
31
  recorderStartStatus: true,
32
+ /** `outputDir` on `runTests` and `runFlowInProject`: a folder per run for the report and the screenshots. */
33
+ outputDir: true,
34
+ /** `runFlowInProject`'s `outputDir` gives each node its own folder, `nodes/<node id>/`. */
35
+ flowNodeFolders: true,
30
36
  };
@@ -21,6 +21,8 @@ export declare function browsersDir(): string;
21
21
  export declare function validateConfig(config: SamuraiTestConfig): void;
22
22
  /** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
23
23
  export declare function loadRawConfig(): Promise<SamuraiTestConfig>;
24
+ /** The default export of `samurai.config.ts` in `dir`, not validated */
25
+ export declare function importConfigFile(dir: string): Promise<SamuraiTestConfig>;
24
26
  /** A project's config without keeping the project active (`withProject` is one at a time): the given one, else its samurai.config.ts */
25
27
  export declare function projectConfig(options: {
26
28
  projectDir?: string;
@@ -37,13 +37,18 @@ export async function loadRawConfig() {
37
37
  validateConfig(activeProject.config);
38
38
  return activeProject.config;
39
39
  }
40
- const configUrl = path.join(projectDir(), "samurai.config.ts");
40
+ const config = await importConfigFile(projectDir());
41
+ validateConfig(config);
42
+ return config;
43
+ }
44
+ /** The default export of `samurai.config.ts` in `dir`, not validated */
45
+ export async function importConfigFile(dir) {
46
+ const configUrl = path.join(dir, "samurai.config.ts");
41
47
  logger.debug("Reading config from: %s", configUrl);
42
48
  const config = (await import(pathToFileURL(configUrl).href));
43
49
  if (!config.default) {
44
50
  throw new Error("samurai.config.ts does not export defineConfig as default!");
45
51
  }
46
- validateConfig(config.default);
47
52
  return config.default;
48
53
  }
49
54
  /** A project's config without keeping the project active (`withProject` is one at a time): the given one, else its samurai.config.ts */
@@ -91,6 +91,26 @@ export declare function resolveConfig(config: SamuraiTestConfig, options: {
91
91
  projectDir: string;
92
92
  env: Env;
93
93
  }): ResolvedConfig;
94
+ /**
95
+ * `samurai.config.ts` as the file states it, as JSON: nothing filled in (an absent `workers` stays absent, a config
96
+ * without `environments` has none), `logs.ignoreErrors` patterns as {@link ResolvedIgnoreError}, and `steps` as
97
+ * their kinds. For showing and editing the config; {@link ResolvedConfig} is what a run uses.
98
+ */
99
+ export type WrittenConfig = Omit<SamuraiTestConfig, "logs" | "steps"> & {
100
+ logs?: Omit<NonNullable<SamuraiTestConfig["logs"]>, "ignoreErrors"> & {
101
+ ignoreErrors?: ResolvedIgnoreError[];
102
+ };
103
+ steps?: string[];
104
+ };
105
+ /** Both views of a config: as written, and resolved; `resolved` is absent, and `error` says why, when the config is invalid */
106
+ export interface ConfigViews {
107
+ written: WrittenConfig;
108
+ resolved?: ResolvedConfig;
109
+ /** The error a run would throw for this config */
110
+ error?: string;
111
+ }
112
+ /** The config as written, as plain JSON (see {@link WrittenConfig}). Pure: reads no files. */
113
+ export declare function writtenConfig(config: SamuraiTestConfig): WrittenConfig;
94
114
  /**
95
115
  * A project's `samurai.config.ts` with every default applied, the way a run uses it (see {@link ResolvedConfig}).
96
116
  * Leaves no project active. The caller registers `tsx` first (the framework does not), so the config file can be
@@ -98,9 +118,17 @@ export declare function resolveConfig(config: SamuraiTestConfig, options: {
98
118
  * (Node caches it): a later edit to `samurai.config.ts` is seen only in a new process.
99
119
  * @param options.projectDir The project folder. @default the working directory
100
120
  * @param options.env Where `CI` is read from. @default `process.env`
121
+ * @param options.written Also return the config as written (see {@link ConfigViews})
101
122
  */
102
123
  export declare function loadConfig(options?: {
103
124
  projectDir?: string;
104
125
  env?: Env;
126
+ written?: false;
105
127
  }): Promise<ResolvedConfig>;
128
+ /** With `written: true`: the config as written next to the resolved one (see {@link ConfigViews}); an invalid config is reported in `error`, not thrown */
129
+ export declare function loadConfig(options: {
130
+ projectDir?: string;
131
+ env?: Env;
132
+ written: true;
133
+ }): Promise<ConfigViews>;
106
134
  export {};
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { browsersFor } from "./browsers-for.js";
3
- import { projectConfig, validateConfig } from "./config.js";
3
+ import { importConfigFile, projectConfig, validateConfig } from "./config.js";
4
4
  import { DEFAULT_COLLECT_BODIES, DEFAULT_COMMAND_TIMEOUT, DEFAULT_DOWNLOADS_DIR, DEFAULT_FAIL_ON_PAGE_ERROR, DEFAULT_FILES_DIR, DEFAULT_IDLE_TIME, DEFAULT_LOGS_CAPTURE, DEFAULT_MAX_BODY_SIZE, DEFAULT_NAVIGATION_TIMEOUT, DEFAULT_RETRIES, DEFAULT_ROUTE_TIMEOUT, DEFAULT_SRC_DIR, DEFAULT_TRACK_NETWORK, DEFAULT_VIEWPORT, DEFAULT_WAIT_UNTIL, } from "./defaults.js";
5
5
  import { forbidsOnly } from "./forbid-only.js";
6
6
  import { IMPLICIT_ENVIRONMENT, resolveRunSettings, UnknownEnvironmentError, } from "./run-settings.js";
@@ -97,16 +97,39 @@ export function resolveConfig(config, options) {
97
97
  steps: (config.steps ?? []).map((step) => step.kind),
98
98
  };
99
99
  }
100
- /**
101
- * A project's `samurai.config.ts` with every default applied, the way a run uses it (see {@link ResolvedConfig}).
102
- * Leaves no project active. The caller registers `tsx` first (the framework does not), so the config file can be
103
- * imported; an app does that in the child process that calls this. The config module is imported once per process
104
- * (Node caches it): a later edit to `samurai.config.ts` is seen only in a new process.
105
- * @param options.projectDir The project folder. @default the working directory
106
- * @param options.env Where `CI` is read from. @default `process.env`
107
- */
100
+ /** The config as written, as plain JSON (see {@link WrittenConfig}). Pure: reads no files. */
101
+ export function writtenConfig(config) {
102
+ const { logs, steps, ...rest } = config;
103
+ const { ignoreErrors, ...logSettings } = logs ?? {};
104
+ const written = {
105
+ ...rest,
106
+ ...(logs && {
107
+ logs: {
108
+ ...logSettings,
109
+ ...(ignoreErrors && { ignoreErrors: ignoreErrorsOf(ignoreErrors) }),
110
+ },
111
+ }),
112
+ ...(steps && { steps: steps.map((step) => step.kind) }),
113
+ };
114
+ // A JSON copy: shares nothing with the config, and leaves out what JSON can't carry
115
+ return JSON.parse(JSON.stringify(written));
116
+ }
108
117
  export async function loadConfig(options = {}) {
109
118
  const projectDir = path.resolve(options.projectDir ?? process.cwd());
110
- const config = await projectConfig({ projectDir });
111
- return resolveConfig(config, { projectDir, env: options.env ?? process.env });
119
+ const env = options.env ?? process.env;
120
+ if (!options.written) {
121
+ const config = await projectConfig({ projectDir });
122
+ return resolveConfig(config, { projectDir, env });
123
+ }
124
+ const config = await importConfigFile(projectDir);
125
+ const written = writtenConfig(config);
126
+ try {
127
+ return { written, resolved: resolveConfig(config, { projectDir, env }) };
128
+ }
129
+ catch (error) {
130
+ return {
131
+ written,
132
+ error: error instanceof Error ? error.message : String(error),
133
+ };
134
+ }
112
135
  }
@@ -7,7 +7,7 @@ export const fail = (e) => ({
7
7
  export async function runNodeTests(node, run, list) {
8
8
  let result;
9
9
  try {
10
- result = await run.runTests(list, (e) => run.onTestEvent(node.id, e), run.nodeBrowsers?.[node.id]);
10
+ result = await run.runTests(node.id, list, (e) => run.onTestEvent(node.id, e), run.nodeBrowsers?.[node.id]);
11
11
  }
12
12
  catch (e) {
13
13
  return fail(e);
@@ -14,7 +14,7 @@ export interface HandlerRun {
14
14
  /** The browsers of the nodes that have their own (a Group node's group); the others use the project's */
15
15
  nodeBrowsers?: Record<string, SupportedBrowser[]>;
16
16
  signal: AbortSignal;
17
- runTests: (tests: NodeTest[], onEvent: (e: RunEvent) => void, browsers?: SupportedBrowser[]) => Promise<"passed" | "failed" | "cancelled">;
17
+ runTests: (nodeId: string, tests: NodeTest[], onEvent: (e: RunEvent) => void, browsers?: SupportedBrowser[]) => Promise<"passed" | "failed" | "cancelled">;
18
18
  sleep: (ms: number, signal: AbortSignal) => Promise<void>;
19
19
  onTestEvent: (nodeId: string, e: RunEvent) => void;
20
20
  /** Definitions of kinds core doesn't have (steps), for expression scopes */
@@ -4,6 +4,6 @@ import type { WorkItem } from "../../runner/worker-protocol.js";
4
4
  import type { SupportedBrowser } from "../../types/browser.js";
5
5
  import type { NodeTest } from "./events.js";
6
6
  /** The work items of a node: each test in each browser, the browsers one after the other */
7
- export declare function itemsOf(tests: NodeTest[], browsers: readonly SupportedBrowser[]): WorkItem[];
7
+ export declare function itemsOf(tests: NodeTest[], browsers: readonly SupportedBrowser[], outputDir?: string): WorkItem[];
8
8
  /** Runs the tests of a node on the pool and reports them as a run would */
9
9
  export declare function runOnPool(pool: Pick<WorkerPool, "run">, items: WorkItem[], onEvent: (event: RunEvent) => void, signal: AbortSignal): Promise<"passed" | "failed" | "cancelled">;
@@ -1,7 +1,12 @@
1
1
  /** The work items of a node: each test in each browser, the browsers one after the other */
2
- export function itemsOf(tests, browsers) {
2
+ export function itemsOf(tests, browsers, outputDir) {
3
3
  return browsers
4
- .flatMap((browser) => tests.map((t) => ({ file: t.file, testName: t.name, browser })))
4
+ .flatMap((browser) => tests.map((t) => ({
5
+ file: t.file,
6
+ testName: t.name,
7
+ browser,
8
+ ...(outputDir !== undefined && { outputDir }),
9
+ })))
5
10
  .map((item, index) => ({ ...item, index }));
6
11
  }
7
12
  /** Runs the tests of a node on the pool and reports them as a run would */
@@ -20,6 +20,12 @@ export interface RunFlowInProjectOptions extends ProjectOptions {
20
20
  /** `"fail"` (default): a flow with a node that can't run is refused. `"skip"`: those nodes are skipped. */
21
21
  unsupported?: "fail" | "skip";
22
22
  port?: number;
23
+ /**
24
+ * The folder for the tests' named screenshots, instead of `<project>/result` (see `runTests`). Each node writes to
25
+ * `nodes/<node id>/` inside it, so a test that runs in two nodes keeps both sets; screenshot paths in a node's
26
+ * results are relative to that folder. Relative to the project folder
27
+ */
28
+ outputDir?: string;
23
29
  /**
24
30
  * How many worker processes the tests of the whole run share: a number, or a percentage like `"50%"`. Unset
25
31
  * takes `SAMURAI_WORKERS` (`workersEnv`), then the config's `workers`, then half the CPUs. 1 runs each Test
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { cleanName } from "../../browser/evidence.js";
2
3
  import { DEFAULT_SRC_DIR } from "../../config/defaults.js";
3
4
  import { projectConfig } from "../../config/config.js";
4
5
  import { checkFlow, withKeys, } from "../core/index.js";
@@ -194,9 +195,13 @@ async function walkFlowInProject(options) {
194
195
  unsupported,
195
196
  handlers: { ...HANDLERS, ...steps.handlers },
196
197
  nodes: steps.nodes,
197
- runTests: async ({ tests, onEvent, signal, browsers }) => {
198
+ runTests: async ({ nodeId, tests, onEvent, signal, browsers }) => {
199
+ // Each node gets a folder of its own: the same test in two nodes would overwrite its screenshots
200
+ const outputDir = options.outputDir !== undefined
201
+ ? path.join(options.outputDir, "nodes", cleanName(nodeId))
202
+ : undefined;
198
203
  if (pool)
199
- return runOnPool(pool, itemsOf(tests, browsers ?? browsersFor({ project: config.browsers })), onEvent, signal);
204
+ return runOnPool(pool, itemsOf(tests, browsers ?? browsersFor({ project: config.browsers }), outputDir), onEvent, signal);
200
205
  const summary = await run({
201
206
  projectDir,
202
207
  config,
@@ -214,6 +219,7 @@ async function walkFlowInProject(options) {
214
219
  files: [...new Set(tests.map((t) => t.file))],
215
220
  testNames: tests.map((t) => t.name),
216
221
  reportPath: false,
222
+ ...(outputDir !== undefined && { outputDir }),
217
223
  signal,
218
224
  onEvent,
219
225
  });
@@ -20,6 +20,8 @@ export interface RunFlowOptions {
20
20
  signal?: AbortSignal;
21
21
  onEvent?: (event: FlowEvent) => void;
22
22
  runTests: (input: {
23
+ /** The node the tests are for: a node runs at most once in a run */
24
+ nodeId: string;
23
25
  tests: NodeTest[];
24
26
  onEvent: (e: RunEvent) => void;
25
27
  signal: AbortSignal;
@@ -34,7 +34,8 @@ export async function runFlow(options) {
34
34
  ...(options.nodeBrowsers && { nodeBrowsers: options.nodeBrowsers }),
35
35
  signal,
36
36
  sleep: options.sleep ?? defaultSleep,
37
- runTests: (tests, onTestEvent, browsers) => options.runTests({
37
+ runTests: (nodeId, tests, onTestEvent, browsers) => options.runTests({
38
+ nodeId,
38
39
  tests,
39
40
  onEvent: onTestEvent,
40
41
  signal,
@@ -2,4 +2,4 @@ export { ConfigEditError, editConfig, hasConfigPath, propertyText, type ConfigVa
2
2
  export { addVariable, ExistsError, VARIABLE_NAME } from "./variables.js";
3
3
  export { isGitIgnored, SECRET_NAME, setSecret } from "./secrets-file.js";
4
4
  export { cleanFileName, filesFolder, isFolderIgnored, RECORD_LIMIT, storeFile, } from "./files-store.js";
5
- export { loadConfig, type ResolvedConfig, type ResolvedEnvironment, type ResolvedIgnoreError, } from "../config/resolved-config.js";
5
+ export { loadConfig, writtenConfig, type ConfigViews, type ResolvedConfig, type ResolvedEnvironment, type ResolvedIgnoreError, type WrittenConfig, } from "../config/resolved-config.js";
@@ -2,4 +2,4 @@ export { ConfigEditError, editConfig, hasConfigPath, propertyText, } from "./con
2
2
  export { addVariable, ExistsError, VARIABLE_NAME } from "./variables.js";
3
3
  export { isGitIgnored, SECRET_NAME, setSecret } from "./secrets-file.js";
4
4
  export { cleanFileName, filesFolder, isFolderIgnored, RECORD_LIMIT, storeFile, } from "./files-store.js";
5
- export { loadConfig, } from "../config/resolved-config.js";
5
+ export { loadConfig, writtenConfig, } from "../config/resolved-config.js";
@@ -28,7 +28,13 @@ export interface RunnerOptions {
28
28
  headless?: boolean;
29
29
  /** Remote debugging port of the browser. @default DEFAULT_PORT (9223) */
30
30
  port?: number;
31
- /** Where the report is written; `false` writes none. @default `<project>/result/report.json` */
31
+ /**
32
+ * The folder for what the run writes, instead of `<project>/result`: `report.json` and the named screenshots
33
+ * (their paths in the results are relative to it). Relative to the project folder.
34
+ * @default `<project>/result`
35
+ */
36
+ outputDir?: string;
37
+ /** Where the report is written; `false` writes none. Beats `outputDir` for the report. @default `report.json` in `outputDir` */
32
38
  reportPath?: string | false;
33
39
  /** Receives start and end of the run and of every test, as they happen */
34
40
  onEvent?: (event: RunEvent) => void;
@@ -0,0 +1,6 @@
1
+ import type { RunnerOptions } from "./options.js";
2
+ /** Where a run writes: the folder its screenshots go in, and its report (`false`: none) */
3
+ export declare function runOutput(options: Pick<RunnerOptions, "outputDir" | "reportPath">, projectDir: string): {
4
+ resultDir: string;
5
+ report: string | false;
6
+ };
@@ -0,0 +1,14 @@
1
+ import path from "node:path";
2
+ /** Where a run writes: the folder its screenshots go in, and its report (`false`: none) */
3
+ export function runOutput(options, projectDir) {
4
+ const { outputDir, reportPath } = options;
5
+ const resultDir = outputDir !== undefined
6
+ ? path.resolve(projectDir, outputDir)
7
+ : typeof reportPath === "string"
8
+ ? path.dirname(reportPath)
9
+ : path.join(projectDir, "result");
10
+ return {
11
+ resultDir,
12
+ report: reportPath ?? path.join(resultDir, "report.json"),
13
+ };
14
+ }
@@ -1,4 +1,3 @@
1
- import path from "path";
2
1
  import { takePendingAssertions } from "../assert/expect.js";
3
2
  import { Browser } from "../browser/browser.js";
4
3
  import { Evidence } from "../browser/evidence.js";
@@ -11,6 +10,7 @@ import { judgePageLogs } from "./page-logs-report.js";
11
10
  import { DEFAULT_PORT } from "./ports.js";
12
11
  import { runWithEachHooks } from "./hooks.js";
13
12
  import { isSkipSignal, staticSkip } from "./skip.js";
13
+ import { runOutput } from "./output.js";
14
14
  import { testDirOf } from "./test-dir.js";
15
15
  import { toTestError } from "./test-error.js";
16
16
  /**
@@ -75,10 +75,7 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
75
75
  const outcome = new TestOutcome(reporter, test, browserName);
76
76
  const { settings, secrets } = run;
77
77
  const timeout = settings.timeout;
78
- const { reportPath } = options;
79
- const resultDir = typeof reportPath === "string"
80
- ? path.dirname(reportPath)
81
- : path.join(projectDir(), "result");
78
+ const { resultDir } = runOutput(options, projectDir());
82
79
  let browser;
83
80
  let page;
84
81
  let timeoutTimer;
@@ -17,6 +17,7 @@ import { browsersOfRun, loadSpecs, selectTests } from "./select.js";
17
17
  import { resolveRetries } from "./retries-option.js";
18
18
  import { runWithRetries } from "./retry.js";
19
19
  import { resolveWorkers } from "../config/workers-option.js";
20
+ import { runOutput } from "./output.js";
20
21
  import { WorkerPool } from "./worker-pool.js";
21
22
  import { processWorkerFactory } from "./worker-process.js";
22
23
  /** Finds the spec files, picks the tests and browsers of a run and runs each test in each browser */
@@ -61,7 +62,7 @@ export default class TestRunner {
61
62
  * was also written to the report. With more than one worker the tests run at the same time in worker processes.
62
63
  */
63
64
  async start() {
64
- const { reportPath, onEvent, signal } = this.options;
65
+ const { onEvent, signal } = this.options;
65
66
  // The specs are imported once: their top-level code runs once per run
66
67
  const loaded = await loadSpecs(this.testFiles);
67
68
  const config = await loadRawConfig();
@@ -72,7 +73,7 @@ export default class TestRunner {
72
73
  const reporter = new TestReporter({
73
74
  environment: this.run.settings.environment,
74
75
  browsers,
75
- ...(reportPath !== undefined && { output: reportPath }),
76
+ output: runOutput(this.options, projectDir()).report,
76
77
  ...(onEvent && { onEvent }),
77
78
  });
78
79
  // How the run goes is settled first: a bad `workers` is an error before any browser starts
@@ -159,7 +160,7 @@ export default class TestRunner {
159
160
  /** The pool of a run: up to `workers` worker processes that read the same project and settings as this run */
160
161
  createPool(workers, retries) {
161
162
  const { settings } = this.run;
162
- const { reportPath, headless, port } = this.options;
163
+ const { reportPath, outputDir, headless, port } = this.options;
163
164
  const options = {
164
165
  workers,
165
166
  factory: processWorkerFactory(),
@@ -173,6 +174,7 @@ export default class TestRunner {
173
174
  expectTimeout: settings.expectTimeout,
174
175
  ...(headless !== undefined && { headless }),
175
176
  ...(typeof reportPath === "string" && { reportPath }),
177
+ ...(outputDir !== undefined && { outputDir }),
176
178
  files: this.testFiles,
177
179
  basePort: port ?? DEFAULT_PORT,
178
180
  ...(retries > 0 && { retries }),
@@ -11,6 +11,8 @@ export interface WorkItem {
11
11
  browser: SupportedBrowser;
12
12
  /** Items with the same key never run at the same time */
13
13
  serialKey?: string;
14
+ /** Where this test's screenshots go, instead of the run's `outputDir` (a flow gives each node its own) */
15
+ outputDir?: string;
14
16
  }
15
17
  /** What a worker needs to start: the project and the settings that are the same for every test */
16
18
  export interface WorkerInit {
@@ -24,6 +26,8 @@ export interface WorkerInit {
24
26
  retries?: number;
25
27
  /** Where the run's report goes when it is not the default; screenshots are kept beside it */
26
28
  reportPath?: string;
29
+ /** The run's `outputDir`: screenshots go there */
30
+ outputDir?: string;
27
31
  /** Spec files to import, absolute */
28
32
  files: string[];
29
33
  /** 0 for the first worker; picks the worker's browser port and profile folder */
@@ -36,7 +36,7 @@ export function openSession(init) {
36
36
  });
37
37
  resolve({
38
38
  tests: loaded.all,
39
- async runTest(test, browser, signal, onEvent) {
39
+ async runTest(test, browser, signal, onEvent, perTest = {}) {
40
40
  const reporter = new TestReporter({
41
41
  environment: run.settings.environment,
42
42
  browsers: [browser],
@@ -48,6 +48,9 @@ export function openSession(init) {
48
48
  ...(init.reportPath !== undefined && {
49
49
  reportPath: init.reportPath,
50
50
  }),
51
+ ...((perTest.outputDir ?? init.outputDir) !== undefined && {
52
+ outputDir: perTest.outputDir ?? init.outputDir,
53
+ }),
51
54
  port,
52
55
  signal,
53
56
  };
@@ -6,7 +6,9 @@ import type { FromWorker, ToWorker, WorkerInit } from "./worker-protocol.js";
6
6
  export interface WorkerSession {
7
7
  tests: readonly RegisteredTestCase[];
8
8
  /** Runs one test; `onEvent` gets the reporter's events for it. Aborting `signal` ends the test as aborted */
9
- runTest(test: RegisteredTestCase, browser: SupportedBrowser, signal: AbortSignal, onEvent: (event: RunEvent) => void): Promise<void>;
9
+ runTest(test: RegisteredTestCase, browser: SupportedBrowser, signal: AbortSignal, onEvent: (event: RunEvent) => void, options?: {
10
+ outputDir?: string;
11
+ }): Promise<void>;
10
12
  /** Ends the project's session */
11
13
  close(): Promise<void>;
12
14
  }
@@ -54,7 +54,7 @@ export function handleMessages(channel, { open, exit }) {
54
54
  id,
55
55
  result: { ...event.result, worker: workerIndex },
56
56
  });
57
- });
57
+ }, item.outputDir !== undefined ? { outputDir: item.outputDir } : {});
58
58
  }
59
59
  finally {
60
60
  current = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itmetsam/samurai-framework",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
5
5
  "main": "dist/api.js",
6
6
  "exports": {