@itmetsam/samurai-framework 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/browser/browser.js +2 -1
  2. package/dist/browser/page-network.js +3 -2
  3. package/dist/browser/page-settings.js +2 -5
  4. package/dist/browser/page.js +2 -1
  5. package/dist/browser/session.d.ts +1 -1
  6. package/dist/browser/session.js +1 -1
  7. package/dist/browser/viewport.d.ts +0 -5
  8. package/dist/browser/viewport.js +0 -2
  9. package/dist/capabilities.d.ts +36 -0
  10. package/dist/capabilities.js +34 -0
  11. package/dist/{runner/browsers.js → config/browsers-for.js} +1 -2
  12. package/dist/config/config.d.ts +5 -1
  13. package/dist/config/config.js +18 -12
  14. package/dist/config/defaults.d.ts +41 -0
  15. package/dist/config/defaults.js +38 -0
  16. package/dist/config/resolved-config.d.ts +134 -0
  17. package/dist/config/resolved-config.js +135 -0
  18. package/dist/config/run-settings.d.ts +2 -0
  19. package/dist/config/run-settings.js +2 -3
  20. package/dist/config/timeouts.js +1 -2
  21. package/dist/{runner → config}/workers-option.js +1 -1
  22. package/dist/flows/run/project-options.d.ts +2 -0
  23. package/dist/flows/run/project.js +11 -4
  24. package/dist/network/data-collector.js +1 -2
  25. package/dist/network/router.js +1 -2
  26. package/dist/project/files-store.js +2 -1
  27. package/dist/project/index.d.ts +1 -0
  28. package/dist/project/index.js +1 -0
  29. package/dist/recorder/session-browser.js +1 -1
  30. package/dist/recorder/session.d.ts +5 -2
  31. package/dist/recorder/session.js +18 -3
  32. package/dist/runner/groups.d.ts +4 -5
  33. package/dist/runner/groups.js +2 -5
  34. package/dist/runner/options.d.ts +7 -1
  35. package/dist/runner/output.d.ts +6 -0
  36. package/dist/runner/output.js +14 -0
  37. package/dist/runner/page-logs-report.js +6 -3
  38. package/dist/runner/project.js +2 -2
  39. package/dist/runner/retries-option.js +2 -1
  40. package/dist/runner/run-test.js +2 -5
  41. package/dist/runner/run.d.ts +12 -2
  42. package/dist/runner/run.js +21 -7
  43. package/dist/runner/select.js +4 -4
  44. package/dist/runner/test-dir.js +2 -1
  45. package/dist/runner/test-runner.d.ts +8 -2
  46. package/dist/runner/test-runner.js +16 -10
  47. package/dist/runner/worker-protocol.d.ts +2 -0
  48. package/dist/runner/worker-session.js +3 -0
  49. package/dist/steps/edit.d.ts +10 -0
  50. package/dist/steps/edit.js +4 -0
  51. package/dist/steps/index.d.ts +2 -2
  52. package/dist/steps/index.js +1 -1
  53. package/dist/steps/model.d.ts +17 -0
  54. package/dist/steps/parse.d.ts +6 -1
  55. package/dist/steps/parse.js +54 -10
  56. package/dist/transport/bidi-connection.d.ts +0 -2
  57. package/dist/transport/bidi-connection.js +1 -2
  58. package/package.json +6 -2
  59. /package/dist/{runner/browsers.d.ts → config/browsers-for.d.ts} +0 -0
  60. /package/dist/{runner → config}/workers-option.d.ts +0 -0
@@ -1,5 +1,6 @@
1
1
  import { setTimeout as sleep } from "node:timers/promises";
2
2
  import path from "node:path";
3
+ import { DEFAULT_DOWNLOADS_DIR } from "../config/defaults.js";
3
4
  import { projectBrowser } from "../browsers/resolve.js";
4
5
  import Page from "./page.js";
5
6
  import { launchChrome } from "./chrome-launch.js";
@@ -77,7 +78,7 @@ export class Browser {
77
78
  /** Sets up a browser whose BiDi session is open: downloads folder, the first page and what it tracks */
78
79
  static async attach(browserProc, biDiConnector, browserName) {
79
80
  biDiConnector.browserName = browserName;
80
- const downloadsDir = await prepareDownloadsDir(biDiConnector, path.resolve(projectDir(), (await readDownloadsDir()) ?? "result/downloads"));
81
+ const downloadsDir = await prepareDownloadsDir(biDiConnector, path.resolve(projectDir(), (await readDownloadsDir()) ?? DEFAULT_DOWNLOADS_DIR));
81
82
  // Chrome replaces a file that has the name of a download; this keeps every download
82
83
  const downloadFolders = browserName === "chrome" && downloadsDir
83
84
  ? new ChromeDownloadFolders(biDiConnector, downloadsDir)
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_COLLECT_BODIES, DEFAULT_TRACK_NETWORK, } from "../config/defaults.js";
1
2
  import { DataCollector, ResponseBodyUnavailableError, } from "../network/data-collector.js";
2
3
  import { NetworkTracker } from "../network/network-tracker.js";
3
4
  import { RequestTimeoutError, ResponseTimeoutError, toPredicate, } from "../network/network-wait.js";
@@ -36,14 +37,14 @@ export class PageNetwork {
36
37
  tracker() {
37
38
  return this.lazyTracker.get(async () => {
38
39
  const settings = await this.deps.config.network();
39
- if (settings.track === false)
40
+ if ((settings.track ?? DEFAULT_TRACK_NETWORK) === false)
40
41
  throw new NetworkTrackingDisabledError();
41
42
  const tracker = await NetworkTracker.start(this.connector, await this.deps.tree.ensure(), this.contextId);
42
43
  const { emitter } = this.deps;
43
44
  tracker.on("request", (request) => emitter.emit("request", request));
44
45
  tracker.on("response", (response) => emitter.emit("response", this.responseFor(response)));
45
46
  tracker.on("requestfailed", (failed) => emitter.emit("requestfailed", failed));
46
- if (settings.collectBodies)
47
+ if (settings.collectBodies ?? DEFAULT_COLLECT_BODIES)
47
48
  await this.startBodyCollection();
48
49
  return tracker;
49
50
  }, (err) => err instanceof NetworkTrackingDisabledError);
@@ -1,9 +1,6 @@
1
1
  import { readConfig } from "../config/config.js";
2
+ import { DEFAULT_IDLE_TIME, DEFAULT_NAVIGATION_TIMEOUT, DEFAULT_WAIT_UNTIL, } from "../config/defaults.js";
2
3
  import logger from "../logger/index.js";
3
- /** Time (ms) a navigation or a wait for a load state may take */
4
- const DEFAULT_NAVIGATION_TIMEOUT = 30000;
5
- /** Time (ms) without requests in flight that counts as idle */
6
- const DEFAULT_IDLE_TIME = 500;
7
4
  /** A section of the config, or `{}` when it cannot be read (a page can be used without a project) */
8
5
  async function readSection(key) {
9
6
  try {
@@ -42,7 +39,7 @@ export class PageConfig {
42
39
  }
43
40
  /** What a navigation waits for: the call's, else config `navigation.waitUntil`, else the load event */
44
41
  async navigationWait(override) {
45
- return override ?? (await this.navigation()).waitUntil ?? "complete";
42
+ return (override ?? (await this.navigation()).waitUntil ?? DEFAULT_WAIT_UNTIL);
46
43
  }
47
44
  /** Time (ms) without requests in flight that counts as idle: the call's, else config `network.idleTime`, else 500 */
48
45
  async idleTime(override) {
@@ -3,6 +3,7 @@ import { cssSelector, labelSelector, roleSelector, testIdSelector, textSelector,
3
3
  import EventEmitter from "node:events";
4
4
  import path from "node:path";
5
5
  import { projectDir } from "../config/config.js";
6
+ import { DEFAULT_VIEWPORT } from "../config/defaults.js";
6
7
  import logger from "../logger/index.js";
7
8
  import { ContextTree } from "./context-tree.js";
8
9
  import { printPdf } from "./pdf.js";
@@ -21,7 +22,7 @@ import { PageTransfers } from "./page-transfers.js";
21
22
  import { initScriptSource } from "./init-script.js";
22
23
  import { sourcesFor, waitForPageEvent, } from "./page-events.js";
23
24
  import { PageConfig, } from "./page-settings.js";
24
- import { applyViewport, DEFAULT_VIEWPORT } from "./viewport.js";
25
+ import { applyViewport } from "./viewport.js";
25
26
  import { HelperRealm } from "../locator/helper-realm.js";
26
27
  import { addPreload } from "../script/preload.js";
27
28
  import { BiDiError } from "../transport/bidi-error.js";
@@ -1,4 +1,4 @@
1
- import { type BiDiConnector } from "../transport/bidi-connection.js";
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
2
  /** session.new got no reply in time */
3
3
  export declare class BrowserStartTimeoutError extends Error {
4
4
  constructor(label: string, timeout: number);
@@ -1,4 +1,4 @@
1
- import { DEFAULT_COMMAND_TIMEOUT, } from "../transport/bidi-connection.js";
1
+ import { DEFAULT_COMMAND_TIMEOUT } from "../config/defaults.js";
2
2
  import { BiDiError } from "../transport/bidi-error.js";
3
3
  import { SESSION_CAPABILITIES } from "./session-capabilities.js";
4
4
  /** session.new waits at least this long (ms): the first start after an install can take a while (macOS checks the new app) */
@@ -1,9 +1,4 @@
1
1
  import type { BiDiConnector } from "../transport/bidi-connection.js";
2
- /** The size a page gets unless config `use.viewport` says otherwise */
3
- export declare const DEFAULT_VIEWPORT: {
4
- width: number;
5
- height: number;
6
- };
7
2
  /** A viewport as `page.setViewport` takes it: a size and optionally a device pixel ratio */
8
3
  export interface Viewport {
9
4
  width: number;
@@ -1,6 +1,4 @@
1
1
  import { callFunction } from "../script/call-function.js";
2
- /** The size a page gets unless config `use.viewport` says otherwise */
3
- export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
4
2
  /** Time (ms) without a `resize` event after which a viewport reset counts as done */
5
3
  const RESIZE_QUIET_TIME = 50;
6
4
  /** Time (ms) a viewport reset waits for a `resize` event that may never come (the size did not change) */
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What this framework can do, as plain data. The SAMURAI app reads it from a project's own copy
3
+ * of the framework to know which features that copy supports, so it asks instead of guessing.
4
+ * Imports nothing, so it is cheap to load; a key that is missing means the copy is too old to
5
+ * support it. Every value must survive `JSON.stringify`.
6
+ */
7
+ export declare const CAPABILITIES: {
8
+ /** Test groups: a group name on a test and running one group. */
9
+ readonly groups: true;
10
+ /** The toolbar the recorder shows in the page it records. */
11
+ readonly recorderToolbar: true;
12
+ /** The recorder writing its recording to files in the project. */
13
+ readonly recorderFiles: true;
14
+ /** The flows contract version; a test keeps it equal to `FLOWS_API`. */
15
+ readonly flowsApi: 2;
16
+ /** Steps that flows can run, including project steps made with `defineStep`. */
17
+ readonly flowSteps: true;
18
+ /** Browsers the framework downloads and manages itself. */
19
+ readonly managedBrowsers: true;
20
+ /** Modifiers on tests, such as `skip`, `only` and `fixme`. */
21
+ readonly modifiers: true;
22
+ /** A reason given with a skipped test, shown in the results. */
23
+ readonly skipReason: true;
24
+ /** An id and a relative file on each test `listTests` finds, the same ids the groups use. */
25
+ readonly testIds: true;
26
+ /** `loadConfig` from `./project`: the config with every default applied, the way a run uses it. */
27
+ readonly resolvedConfig: true;
28
+ /** `loadConfig({ written: true })`: the config as written next to the resolved one, and `writtenConfig`. */
29
+ readonly writtenConfig: true;
30
+ /** The recorder's first `onStatus` call, `{ state: "recording", toolbar }`, right after it starts, and a `RecordResult` with every field always present. */
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
+ };
35
+ /** The shape of {@link CAPABILITIES}. */
36
+ export type Capabilities = typeof CAPABILITIES;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What this framework can do, as plain data. The SAMURAI app reads it from a project's own copy
3
+ * of the framework to know which features that copy supports, so it asks instead of guessing.
4
+ * Imports nothing, so it is cheap to load; a key that is missing means the copy is too old to
5
+ * support it. Every value must survive `JSON.stringify`.
6
+ */
7
+ export const CAPABILITIES = {
8
+ /** Test groups: a group name on a test and running one group. */
9
+ groups: true,
10
+ /** The toolbar the recorder shows in the page it records. */
11
+ recorderToolbar: true,
12
+ /** The recorder writing its recording to files in the project. */
13
+ recorderFiles: true,
14
+ /** The flows contract version; a test keeps it equal to `FLOWS_API`. */
15
+ flowsApi: 2,
16
+ /** Steps that flows can run, including project steps made with `defineStep`. */
17
+ flowSteps: true,
18
+ /** Browsers the framework downloads and manages itself. */
19
+ managedBrowsers: true,
20
+ /** Modifiers on tests, such as `skip`, `only` and `fixme`. */
21
+ modifiers: true,
22
+ /** A reason given with a skipped test, shown in the results. */
23
+ skipReason: true,
24
+ /** An id and a relative file on each test `listTests` finds, the same ids the groups use. */
25
+ testIds: true,
26
+ /** `loadConfig` from `./project`: the config with every default applied, the way a run uses it. */
27
+ resolvedConfig: true,
28
+ /** `loadConfig({ written: true })`: the config as written next to the resolved one, and `writtenConfig`. */
29
+ writtenConfig: true,
30
+ /** The recorder's first `onStatus` call, `{ state: "recording", toolbar }`, right after it starts, and a `RecordResult` with every field always present. */
31
+ recorderStartStatus: true,
32
+ /** `outputDir` on `runTests` and `runFlowInProject`: a folder per run for the report and the screenshots. */
33
+ outputDir: true,
34
+ };
@@ -1,5 +1,4 @@
1
- /** The browsers of a run that names none */
2
- const DEFAULT_BROWSERS = ["firefox"];
1
+ import { DEFAULT_BROWSERS } from "./defaults.js";
3
2
  /**
4
3
  * The browsers a run uses, the first that is set winning: the command line, the group being run, the project, then
5
4
  * Firefox. An empty list counts as not given.
@@ -17,8 +17,12 @@ export declare function projectDir(): string;
17
17
  * An embedding app points it at a folder it may write to.
18
18
  */
19
19
  export declare function browsersDir(): string;
20
+ /** Throws the error a run throws for a config with a bad key */
21
+ export declare function validateConfig(config: SamuraiTestConfig): void;
20
22
  /** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
21
- export declare function loadConfig(): Promise<SamuraiTestConfig>;
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>;
22
26
  /** A project's config without keeping the project active (`withProject` is one at a time): the given one, else its samurai.config.ts */
23
27
  export declare function projectConfig(options: {
24
28
  projectDir?: string;
@@ -24,25 +24,31 @@ export function browsersDir() {
24
24
  activeProject?.dataDir ??
25
25
  path.join(projectDir(), "browsers"));
26
26
  }
27
+ /** Throws the error a run throws for a config with a bad key */
28
+ export function validateConfig(config) {
29
+ validateBrowserConfig(config);
30
+ validateWorkersConfig(config);
31
+ validateForbidOnlyConfig(config);
32
+ validateRetriesConfig(config);
33
+ }
27
34
  /** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
28
- export async function loadConfig() {
35
+ export async function loadRawConfig() {
29
36
  if (activeProject?.config) {
30
- validateBrowserConfig(activeProject.config);
31
- validateWorkersConfig(activeProject.config);
32
- validateForbidOnlyConfig(activeProject.config);
33
- validateRetriesConfig(activeProject.config);
37
+ validateConfig(activeProject.config);
34
38
  return activeProject.config;
35
39
  }
36
- 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");
37
47
  logger.debug("Reading config from: %s", configUrl);
38
48
  const config = (await import(pathToFileURL(configUrl).href));
39
49
  if (!config.default) {
40
50
  throw new Error("samurai.config.ts does not export defineConfig as default!");
41
51
  }
42
- validateBrowserConfig(config.default);
43
- validateWorkersConfig(config.default);
44
- validateForbidOnlyConfig(config.default);
45
- validateRetriesConfig(config.default);
46
52
  return config.default;
47
53
  }
48
54
  /** A project's config without keeping the project active (`withProject` is one at a time): the given one, else its samurai.config.ts */
@@ -52,14 +58,14 @@ export async function projectConfig(options) {
52
58
  const previous = activeProject;
53
59
  setActiveProject({ dir: path.resolve(options.projectDir ?? process.cwd()) });
54
60
  try {
55
- return await loadConfig();
61
+ return await loadRawConfig();
56
62
  }
57
63
  finally {
58
64
  setActiveProject(previous);
59
65
  }
60
66
  }
61
67
  export async function readConfig(key) {
62
- const value = (await loadConfig())[key];
68
+ const value = (await loadRawConfig())[key];
63
69
  logger.debug("Resolved config value: ", { value });
64
70
  return value;
65
71
  }
@@ -0,0 +1,41 @@
1
+ /** The test folder when the config names none */
2
+ export declare const DEFAULT_SRC_DIR = "./src";
3
+ /** The spec pattern when a group names none */
4
+ export declare const DEFAULT_TEST_MATCH = "**/*.spec.ts";
5
+ /** The browsers of a run that names none */
6
+ export declare const DEFAULT_BROWSERS: readonly ["firefox"];
7
+ /** Time (ms) a test may take */
8
+ export declare const DEFAULT_TIMEOUT = 30000;
9
+ /** The retry time of actions and assertions when nothing sets one (ms) */
10
+ export declare const DEFAULT_EXPECT_TIMEOUT = 5000;
11
+ /** Time (ms) a navigation or a wait for a load state may take */
12
+ export declare const DEFAULT_NAVIGATION_TIMEOUT = 30000;
13
+ /** What a navigation waits for when nothing says otherwise: the load event */
14
+ export declare const DEFAULT_WAIT_UNTIL = "complete";
15
+ /** Time (ms) a command may wait for its reply before it fails with code "timeout" */
16
+ export declare const DEFAULT_COMMAND_TIMEOUT = 30000;
17
+ /** Time (ms) without requests in flight that counts as idle */
18
+ export declare const DEFAULT_IDLE_TIME = 500;
19
+ /** Time (ms) a handler may take to answer a request before the router lets it continue */
20
+ export declare const DEFAULT_ROUTE_TIMEOUT = 30000;
21
+ /** How large a response body may be to be kept: 10 MiB */
22
+ export declare const DEFAULT_MAX_BODY_SIZE: number;
23
+ /** Whether response bodies are collected for every request */
24
+ export declare const DEFAULT_COLLECT_BODIES = false;
25
+ /** Whether the network of a page is tracked */
26
+ export declare const DEFAULT_TRACK_NETWORK = true;
27
+ /** The size a page gets unless config `use.viewport` says otherwise */
28
+ export declare const DEFAULT_VIEWPORT: {
29
+ width: number;
30
+ height: number;
31
+ };
32
+ /** Where downloads are saved, relative to the project */
33
+ export declare const DEFAULT_DOWNLOADS_DIR = "result/downloads";
34
+ /** The files folder, relative to the project */
35
+ export declare const DEFAULT_FILES_DIR = "files";
36
+ /** How many times a failing test is run again */
37
+ export declare const DEFAULT_RETRIES = 0;
38
+ /** Which page log entries go in the report: those of failed tests */
39
+ export declare const DEFAULT_LOGS_CAPTURE = "failures";
40
+ /** Whether uncaught page errors fail a test */
41
+ export declare const DEFAULT_FAIL_ON_PAGE_ERROR = false;
@@ -0,0 +1,38 @@
1
+ /** The test folder when the config names none */
2
+ export const DEFAULT_SRC_DIR = "./src";
3
+ /** The spec pattern when a group names none */
4
+ export const DEFAULT_TEST_MATCH = "**/*.spec.ts";
5
+ /** The browsers of a run that names none */
6
+ export const DEFAULT_BROWSERS = ["firefox"];
7
+ /** Time (ms) a test may take */
8
+ export const DEFAULT_TIMEOUT = 30000;
9
+ /** The retry time of actions and assertions when nothing sets one (ms) */
10
+ export const DEFAULT_EXPECT_TIMEOUT = 5000;
11
+ /** Time (ms) a navigation or a wait for a load state may take */
12
+ export const DEFAULT_NAVIGATION_TIMEOUT = 30000;
13
+ /** What a navigation waits for when nothing says otherwise: the load event */
14
+ export const DEFAULT_WAIT_UNTIL = "complete";
15
+ /** Time (ms) a command may wait for its reply before it fails with code "timeout" */
16
+ export const DEFAULT_COMMAND_TIMEOUT = 30000;
17
+ /** Time (ms) without requests in flight that counts as idle */
18
+ export const DEFAULT_IDLE_TIME = 500;
19
+ /** Time (ms) a handler may take to answer a request before the router lets it continue */
20
+ export const DEFAULT_ROUTE_TIMEOUT = 30000;
21
+ /** How large a response body may be to be kept: 10 MiB */
22
+ export const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
23
+ /** Whether response bodies are collected for every request */
24
+ export const DEFAULT_COLLECT_BODIES = false;
25
+ /** Whether the network of a page is tracked */
26
+ export const DEFAULT_TRACK_NETWORK = true;
27
+ /** The size a page gets unless config `use.viewport` says otherwise */
28
+ export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
29
+ /** Where downloads are saved, relative to the project */
30
+ export const DEFAULT_DOWNLOADS_DIR = "result/downloads";
31
+ /** The files folder, relative to the project */
32
+ export const DEFAULT_FILES_DIR = "files";
33
+ /** How many times a failing test is run again */
34
+ export const DEFAULT_RETRIES = 0;
35
+ /** Which page log entries go in the report: those of failed tests */
36
+ export const DEFAULT_LOGS_CAPTURE = "failures";
37
+ /** Whether uncaught page errors fail a test */
38
+ export const DEFAULT_FAIL_ON_PAGE_ERROR = false;
@@ -0,0 +1,134 @@
1
+ import type { SupportedBrowser } from "../types/browser.js";
2
+ import type { SamuraiGroup, SamuraiTestConfig } from "../types/config.js";
3
+ import { type Variables } from "./run-settings.js";
4
+ /** Where the environment variables come from: `CI` and the like */
5
+ type Env = Record<string, string | undefined>;
6
+ /** One environment with the project's values and the built-in defaults merged in, as a run without overrides uses it */
7
+ export interface ResolvedEnvironment {
8
+ baseURL?: string;
9
+ /** Time (ms) one test may take */
10
+ timeout: number;
11
+ /** Time (ms) actions and assertions retry */
12
+ expectTimeout: number;
13
+ /** Whether the browser runs without a window */
14
+ headless: boolean;
15
+ variables: Variables;
16
+ }
17
+ /** An entry of `logs.ignoreErrors` as data: a `RegExp` keeps its source and flags */
18
+ export interface ResolvedIgnoreError {
19
+ text: string;
20
+ regexp: boolean;
21
+ flags?: string;
22
+ }
23
+ /**
24
+ * A project's config with every default applied and the run-level precedence resolved: what a run without command
25
+ * line flags uses. Plain JSON data, so it survives `process.send`.
26
+ */
27
+ export interface ResolvedConfig {
28
+ /** The project folder, absolute */
29
+ projectDir: string;
30
+ /** The test folder, absolute and not resolved through symlinks: use `FoundTest.relativeFile` rather than computing paths against it */
31
+ srcDir: string;
32
+ /** The test folder as the config writes it */
33
+ srcDirRelative: string;
34
+ browsers: SupportedBrowser[];
35
+ browserPaths: {
36
+ firefox?: string;
37
+ chrome?: string;
38
+ };
39
+ timeout: number;
40
+ expectTimeout: number;
41
+ navigationTimeout: number;
42
+ waitUntil: "none" | "interactive" | "complete";
43
+ bidiCommandTimeout: number;
44
+ /** The config's `workers` as written */
45
+ workers?: number | string;
46
+ /** The workers a run without `--workers` and `SAMURAI_WORKERS` uses on this machine */
47
+ workersResolved: number;
48
+ retries: number;
49
+ forbidOnly: boolean;
50
+ headless: boolean;
51
+ /** `null` keeps the browser's own size */
52
+ viewport: {
53
+ width: number;
54
+ height: number;
55
+ } | null;
56
+ downloadsDir: string;
57
+ filesDir: string;
58
+ logs: {
59
+ capture: "off" | "failures" | "all";
60
+ failOnPageError: boolean;
61
+ ignoreErrors: ResolvedIgnoreError[];
62
+ };
63
+ network: {
64
+ track: boolean;
65
+ idleTime: number;
66
+ routeTimeout: number;
67
+ collectBodies: boolean;
68
+ maxBodySize: number;
69
+ };
70
+ /** The project's `baseURL` */
71
+ baseURL?: string;
72
+ /** The config's `defaultEnvironment` as written */
73
+ defaultEnvironment?: string;
74
+ /** Why a run without `--env` would stop: no environment chosen, or an unknown `defaultEnvironment` */
75
+ environmentProblem?: string;
76
+ /** Every environment by name; a config without `environments` has one, `"default"` */
77
+ environments: Record<string, ResolvedEnvironment>;
78
+ /** The groups as written, each with the browsers it runs in */
79
+ groups: (SamuraiGroup & {
80
+ browsers: SupportedBrowser[];
81
+ })[];
82
+ /** The kinds of the project's own flow steps */
83
+ steps: string[];
84
+ }
85
+ /**
86
+ * The config with every default applied, from the same constants and functions a run uses. Throws the errors a run
87
+ * throws for a bad config; an environment a run could not choose is reported in `environmentProblem` instead.
88
+ * Pure: reads no files.
89
+ */
90
+ export declare function resolveConfig(config: SamuraiTestConfig, options: {
91
+ projectDir: string;
92
+ env: Env;
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;
114
+ /**
115
+ * A project's `samurai.config.ts` with every default applied, the way a run uses it (see {@link ResolvedConfig}).
116
+ * Leaves no project active. The caller registers `tsx` first (the framework does not), so the config file can be
117
+ * imported; an app does that in the child process that calls this. The config module is imported once per process
118
+ * (Node caches it): a later edit to `samurai.config.ts` is seen only in a new process.
119
+ * @param options.projectDir The project folder. @default the working directory
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})
122
+ */
123
+ export declare function loadConfig(options?: {
124
+ projectDir?: string;
125
+ env?: Env;
126
+ written?: false;
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>;
134
+ export {};
@@ -0,0 +1,135 @@
1
+ import path from "node:path";
2
+ import { browsersFor } from "./browsers-for.js";
3
+ import { importConfigFile, projectConfig, validateConfig } from "./config.js";
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
+ import { forbidsOnly } from "./forbid-only.js";
6
+ import { IMPLICIT_ENVIRONMENT, resolveRunSettings, UnknownEnvironmentError, } from "./run-settings.js";
7
+ import { resolveWorkers } from "./workers-option.js";
8
+ /** The message a run without `--env` throws for this config, if any */
9
+ function environmentProblem(config, env) {
10
+ try {
11
+ resolveRunSettings(config, {}, env);
12
+ return undefined;
13
+ }
14
+ catch (error) {
15
+ if (error instanceof UnknownEnvironmentError)
16
+ return error.message;
17
+ throw error;
18
+ }
19
+ }
20
+ /** Every environment of the config merged the way `resolveRunSettings` merges it */
21
+ function resolveEnvironments(config, env) {
22
+ const names = Object.keys(config.environments ?? {});
23
+ return Object.fromEntries((names.length > 0 ? names : [IMPLICIT_ENVIRONMENT]).map((name) => {
24
+ const { environment, ...settings } = resolveRunSettings(config, { environment: name }, env);
25
+ return [environment, settings];
26
+ }));
27
+ }
28
+ /** `logs.ignoreErrors` as JSON-safe data */
29
+ function ignoreErrorsOf(list = []) {
30
+ return list.map((entry) => typeof entry === "string"
31
+ ? { text: entry, regexp: false }
32
+ : {
33
+ text: entry.source,
34
+ regexp: true,
35
+ ...(entry.flags !== "" && { flags: entry.flags }),
36
+ });
37
+ }
38
+ /**
39
+ * The config with every default applied, from the same constants and functions a run uses. Throws the errors a run
40
+ * throws for a bad config; an environment a run could not choose is reported in `environmentProblem` instead.
41
+ * Pure: reads no files.
42
+ */
43
+ export function resolveConfig(config, options) {
44
+ validateConfig(config);
45
+ const { env } = options;
46
+ const projectDir = path.resolve(options.projectDir);
47
+ const srcDirRelative = config.srcDir ?? DEFAULT_SRC_DIR;
48
+ // The project level: the same precedence without an environment
49
+ const project = resolveRunSettings({ ...config, environments: undefined, defaultEnvironment: undefined }, {}, env);
50
+ const problem = environmentProblem(config, env);
51
+ const viewport = config.use?.viewport;
52
+ return {
53
+ projectDir,
54
+ srcDir: path.resolve(projectDir, srcDirRelative),
55
+ srcDirRelative,
56
+ browsers: browsersFor({ project: config.browsers }),
57
+ browserPaths: { ...config.browserPaths },
58
+ timeout: project.timeout,
59
+ expectTimeout: project.expectTimeout,
60
+ navigationTimeout: config.navigation?.timeout ?? DEFAULT_NAVIGATION_TIMEOUT,
61
+ waitUntil: config.navigation?.waitUntil ?? DEFAULT_WAIT_UNTIL,
62
+ bidiCommandTimeout: config.bidi?.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT,
63
+ ...(config.workers !== undefined && { workers: config.workers }),
64
+ workersResolved: resolveWorkers({ config: config.workers }),
65
+ // Matches the runner's `resolveRetries` without the command line flag and the environment (config/ cannot import runner/)
66
+ retries: config.retries ?? DEFAULT_RETRIES,
67
+ forbidOnly: forbidsOnly(config, env),
68
+ headless: project.headless,
69
+ viewport: viewport === null ? null : { ...(viewport ?? DEFAULT_VIEWPORT) },
70
+ downloadsDir: config.downloadsDir ?? DEFAULT_DOWNLOADS_DIR,
71
+ filesDir: config.filesDir ?? DEFAULT_FILES_DIR,
72
+ logs: {
73
+ capture: config.logs?.capture ?? DEFAULT_LOGS_CAPTURE,
74
+ failOnPageError: config.logs?.failOnPageError ?? DEFAULT_FAIL_ON_PAGE_ERROR,
75
+ ignoreErrors: ignoreErrorsOf(config.logs?.ignoreErrors),
76
+ },
77
+ network: {
78
+ track: config.network?.track ?? DEFAULT_TRACK_NETWORK,
79
+ idleTime: config.network?.idleTime ?? DEFAULT_IDLE_TIME,
80
+ routeTimeout: config.network?.routeTimeout ?? DEFAULT_ROUTE_TIMEOUT,
81
+ collectBodies: config.network?.collectBodies ?? DEFAULT_COLLECT_BODIES,
82
+ maxBodySize: config.network?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE,
83
+ },
84
+ ...(config.baseURL !== undefined && { baseURL: config.baseURL }),
85
+ ...(config.defaultEnvironment !== undefined && {
86
+ defaultEnvironment: config.defaultEnvironment,
87
+ }),
88
+ ...(problem !== undefined && { environmentProblem: problem }),
89
+ environments: resolveEnvironments(config, env),
90
+ groups: (config.groups ?? []).map((group) => ({
91
+ ...group,
92
+ browsers: browsersFor({
93
+ group: group.browsers,
94
+ project: config.browsers,
95
+ }),
96
+ })),
97
+ steps: (config.steps ?? []).map((step) => step.kind),
98
+ };
99
+ }
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
+ }
117
+ export async function loadConfig(options = {}) {
118
+ const projectDir = path.resolve(options.projectDir ?? process.cwd());
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
+ }
135
+ }
@@ -20,6 +20,8 @@ export interface RunSettings {
20
20
  /** Whether the browser runs without a window */
21
21
  headless: boolean;
22
22
  }
23
+ /** The environment of a config without `environments` */
24
+ export declare const IMPLICIT_ENVIRONMENT = "default";
23
25
  export declare class UnknownEnvironmentError extends Error {
24
26
  constructor(message: string);
25
27
  }
@@ -1,7 +1,6 @@
1
- const DEFAULT_TIMEOUT = 30000;
2
- const DEFAULT_EXPECT_TIMEOUT = 5000;
1
+ import { DEFAULT_EXPECT_TIMEOUT, DEFAULT_TIMEOUT } from "./defaults.js";
3
2
  /** The environment of a config without `environments` */
4
- const IMPLICIT_ENVIRONMENT = "default";
3
+ export const IMPLICIT_ENVIRONMENT = "default";
5
4
  export class UnknownEnvironmentError extends Error {
6
5
  constructor(message) {
7
6
  super(message);
@@ -1,7 +1,6 @@
1
1
  import { readConfig } from "./config.js";
2
+ import { DEFAULT_EXPECT_TIMEOUT } from "./defaults.js";
2
3
  import { peekRunSettings } from "./run-settings.js";
3
- /** The retry time of actions and assertions when nothing sets one (ms) */
4
- const DEFAULT_EXPECT_TIMEOUT = 5000;
5
4
  /** Per-call timeout, else the active run's expect timeout, else config `expect.timeout`, else 5000 */
6
5
  export async function resolveTimeout(perCall) {
7
6
  if (perCall !== undefined)