@itmetsam/samurai-framework 0.15.1 → 0.16.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 (64) hide show
  1. package/dist/api.d.ts +1 -1
  2. package/dist/browser/browser.js +2 -1
  3. package/dist/browser/page-network.js +3 -2
  4. package/dist/browser/page-settings.js +2 -5
  5. package/dist/browser/page.js +2 -1
  6. package/dist/browser/session.d.ts +1 -1
  7. package/dist/browser/session.js +1 -1
  8. package/dist/browser/viewport.d.ts +0 -5
  9. package/dist/browser/viewport.js +0 -2
  10. package/dist/capabilities.d.ts +32 -0
  11. package/dist/capabilities.js +30 -0
  12. package/dist/cli/main.js +1 -1
  13. package/dist/{runner/browsers.js → config/browsers-for.js} +1 -2
  14. package/dist/config/config.d.ts +3 -1
  15. package/dist/config/config.js +12 -11
  16. package/dist/config/defaults.d.ts +41 -0
  17. package/dist/config/defaults.js +38 -0
  18. package/dist/config/resolved-config.d.ts +106 -0
  19. package/dist/config/resolved-config.js +112 -0
  20. package/dist/config/run-settings.d.ts +2 -0
  21. package/dist/config/run-settings.js +2 -3
  22. package/dist/config/timeouts.js +1 -2
  23. package/dist/{runner → config}/workers-option.js +1 -1
  24. package/dist/flows/run/project.js +5 -4
  25. package/dist/network/data-collector.js +1 -2
  26. package/dist/network/router.js +1 -2
  27. package/dist/project/files-store.js +2 -1
  28. package/dist/project/index.d.ts +1 -0
  29. package/dist/project/index.js +1 -0
  30. package/dist/recorder/session-browser.js +1 -1
  31. package/dist/recorder/session.d.ts +5 -2
  32. package/dist/recorder/session.js +18 -3
  33. package/dist/runner/groups.d.ts +4 -5
  34. package/dist/runner/groups.js +2 -5
  35. package/dist/runner/page-logs-report.js +6 -3
  36. package/dist/runner/pooled-run.js +3 -3
  37. package/dist/runner/project.js +2 -2
  38. package/dist/runner/registry.d.ts +13 -4
  39. package/dist/runner/registry.js +26 -9
  40. package/dist/runner/retries-option.js +2 -1
  41. package/dist/runner/run-test.js +1 -1
  42. package/dist/runner/run.d.ts +14 -2
  43. package/dist/runner/run.js +21 -6
  44. package/dist/runner/select.js +4 -4
  45. package/dist/runner/skip.d.ts +2 -1
  46. package/dist/runner/skip.js +3 -2
  47. package/dist/runner/suite.d.ts +10 -4
  48. package/dist/runner/suite.js +19 -10
  49. package/dist/runner/test-dir.js +2 -1
  50. package/dist/runner/test-runner.d.ts +8 -2
  51. package/dist/runner/test-runner.js +14 -10
  52. package/dist/steps/edit.d.ts +10 -0
  53. package/dist/steps/edit.js +4 -0
  54. package/dist/steps/index.d.ts +2 -2
  55. package/dist/steps/index.js +1 -1
  56. package/dist/steps/model.d.ts +19 -0
  57. package/dist/steps/parse.d.ts +6 -1
  58. package/dist/steps/parse.js +89 -19
  59. package/dist/transport/bidi-connection.d.ts +0 -2
  60. package/dist/transport/bidi-connection.js +1 -2
  61. package/dist/types/test.d.ts +10 -0
  62. package/package.json +6 -2
  63. /package/dist/{runner/browsers.d.ts → config/browsers-for.d.ts} +0 -0
  64. /package/dist/{runner → config}/workers-option.d.ts +0 -0
@@ -1,11 +1,10 @@
1
+ import { DEFAULT_ROUTE_TIMEOUT } from "../config/defaults.js";
1
2
  import logger from "../logger/index.js";
2
3
  import { toRequest } from "./bidi-wire.js";
3
4
  import { Intercept } from "./intercept.js";
4
5
  import { PendingRoute, } from "./route.js";
5
6
  import { matchUrl, samePattern } from "./url-match.js";
6
7
  export { Route } from "./route.js";
7
- /** Time (ms) a handler may take to answer a request before the router lets it continue */
8
- const DEFAULT_ROUTE_TIMEOUT = 30000;
9
8
  /** How many handler errors the router keeps */
10
9
  const MAX_ERRORS = 100;
11
10
  /**
@@ -2,6 +2,7 @@ import { constants, createReadStream } from "node:fs";
2
2
  import { copyFile, mkdir, readFile, stat } from "node:fs/promises";
3
3
  import { createHash } from "node:crypto";
4
4
  import path from "node:path";
5
+ import { DEFAULT_FILES_DIR } from "../config/defaults.js";
5
6
  /** Files bigger than this aren't stored while recording */
6
7
  export const RECORD_LIMIT = 50 * 1024 * 1024;
7
8
  const NAME_LIMIT = 120;
@@ -20,7 +21,7 @@ export function cleanFileName(name) {
20
21
  return cleaned === "" ? "file" : cleaned;
21
22
  }
22
23
  /** The files folder: absolute, and relative to the project with forward slashes */
23
- export function filesFolder(projectDir, filesDir = "files") {
24
+ export function filesFolder(projectDir, filesDir = DEFAULT_FILES_DIR) {
24
25
  const absolute = path.resolve(projectDir, filesDir);
25
26
  return {
26
27
  absolute,
@@ -2,3 +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";
@@ -2,3 +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";
@@ -1,7 +1,7 @@
1
1
  import { Browser } from "../browser/browser.js";
2
2
  import { ensureWarm } from "../browser/warm-up.js";
3
3
  import { readConfig } from "../config/config.js";
4
- import { browsersFor } from "../runner/browsers.js";
4
+ import { browsersFor } from "../config/browsers-for.js";
5
5
  import { DEFAULT_PORT } from "../runner/ports.js";
6
6
  /**
7
7
  * The browser a recording uses: the one asked for, else the first of the project's (recording in two browsers at
@@ -31,8 +31,10 @@ export interface RecordSpecOptions extends ProjectOptions {
31
31
  /** Called after each step is written to the file, with the file's new content */
32
32
  onEvent?: (event: RecorderEvent, source: string) => void;
33
33
  /**
34
- * Called when the recording is paused or resumed, or the toolbar fails (or comes back) on a page.
35
- * There is no initial call: until the first one the recording is `{ state: "recording", toolbar: true }`
34
+ * Called once with the recording's first status right after `onStarted` resolves (`{ state: "recording" }`,
35
+ * and `toolbar: false` if the toolbar had already failed to load), then whenever the recording is paused
36
+ * or resumed, or the toolbar fails (or comes back) on a page. Statuses that arrive while `onStarted` runs are held
37
+ * and sent once it resolves, after the first status.
36
38
  */
37
39
  onStatus?: (status: {
38
40
  state: "recording" | "paused";
@@ -46,6 +48,7 @@ export interface RecordSpecOptions extends ProjectOptions {
46
48
  browser: Browser;
47
49
  }) => void | Promise<void>;
48
50
  }
51
+ /** What a recording returns; every field is always present, empty when there is nothing to report. */
49
52
  export interface RecordResult {
50
53
  file: string;
51
54
  /** Full name of the test the steps went into */
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { loadConfig, projectDir } from "../config/config.js";
3
+ import { loadRawConfig, projectDir } from "../config/config.js";
4
4
  import { loadSecrets } from "../config/secrets.js";
5
5
  import { filesFolder, isFolderIgnored } from "../project/index.js";
6
6
  import { withProject } from "../runner/project.js";
@@ -30,11 +30,21 @@ export async function recordSpec(options) {
30
30
  ...options,
31
31
  headless: options.headless ?? settings.headless,
32
32
  });
33
- const loaded = await loadConfig();
33
+ const loaded = await loadRawConfig();
34
34
  const folder = filesFolder(projectDir(), loaded.filesDir);
35
35
  const hadFolder = existsSync(folder.absolute);
36
36
  let recorder;
37
37
  const addedSecrets = new Set();
38
+ // Statuses that come while `onStarted` runs wait behind the first one, which is always the first
39
+ let waiting = [];
40
+ let toolbarLoaded = true;
41
+ const onStatus = (status) => {
42
+ toolbarLoaded = status.toolbar;
43
+ if (waiting)
44
+ waiting.push(status);
45
+ else
46
+ options.onStatus?.(status);
47
+ };
38
48
  let stopRequested;
39
49
  const stopped = new Promise((resolve) => (stopRequested = resolve));
40
50
  try {
@@ -50,7 +60,7 @@ export async function recordSpec(options) {
50
60
  toolbar: {
51
61
  ...(await toolbarOptions(run, spec.source, projectDir(), loaded, testIndex)),
52
62
  projectDir: projectDir(),
53
- ...(options.onStatus && { onStatus: options.onStatus }),
63
+ ...(options.onStatus && { onStatus }),
54
64
  onSecretAdded: (name) => addedSecrets.add(name),
55
65
  onStop: () => stopRequested(),
56
66
  },
@@ -65,6 +75,11 @@ export async function recordSpec(options) {
65
75
  },
66
76
  });
67
77
  await options.onStarted?.({ page, browser });
78
+ const queued = waiting ?? [];
79
+ waiting = undefined;
80
+ options.onStatus?.({ state: "recording", toolbar: toolbarLoaded });
81
+ for (const status of queued)
82
+ options.onStatus?.(status);
68
83
  let windowClosed = false;
69
84
  void browser.exited.then(() => (windowClosed = true));
70
85
  await new Promise((resolve) => {
@@ -1,9 +1,5 @@
1
1
  import type { SupportedBrowser } from "../types/browser.js";
2
2
  import type { SamuraiGroup } from "../types/config.js";
3
- /** The test folder when the config names none */
4
- export declare const DEFAULT_SRC_DIR = "./src";
5
- /** The spec pattern when a group names none */
6
- export declare const DEFAULT_TEST_MATCH = "**/*.spec.ts";
7
3
  /** A test as groups see it: its spec file (relative to `srcDir`, or any path inside it) and full name */
8
4
  export interface GroupTest {
9
5
  file: string;
@@ -16,7 +12,10 @@ export interface ResolvedGroup {
16
12
  kind: "picked" | "pattern";
17
13
  /** What selects the tests of a pattern group, for display */
18
14
  pattern?: string;
19
- /** Ids (see `testId`) of the found tests in the group */
15
+ /**
16
+ * Ids of the found tests in the group: the spec file relative to `srcDir` with `/` separators, `::`, then the
17
+ * full name (`FoundTest.id` is the same)
18
+ */
20
19
  testIds: string[];
21
20
  /** Picked tests that no longer exist */
22
21
  missing: {
@@ -1,10 +1,7 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { matchesGlob, relative, resolve } from "node:path";
3
- import { browsersFor } from "./browsers.js";
4
- /** The test folder when the config names none */
5
- export const DEFAULT_SRC_DIR = "./src";
6
- /** The spec pattern when a group names none */
7
- export const DEFAULT_TEST_MATCH = "**/*.spec.ts";
3
+ import { DEFAULT_TEST_MATCH } from "../config/defaults.js";
4
+ import { browsersFor } from "../config/browsers-for.js";
8
5
  /**
9
6
  * The real path of `p` (symlinks resolved), or `p` itself when it does not exist. Registered test files come
10
7
  * from stack traces, which resolve symlinks, so paths compared with them must be real too
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_LOGS_CAPTURE } from "../config/defaults.js";
1
2
  import { toTestError } from "./test-error.js";
2
3
  /** The page threw exceptions nobody caught, and config `logs.failOnPageError` says that fails the test */
3
4
  export class PageErrorsError extends Error {
@@ -16,15 +17,17 @@ export class RouteErrorsError extends Error {
16
17
  }
17
18
  }
18
19
  function isIgnored(error, ignore) {
19
- return ignore.some((pattern) => typeof pattern === "string" ? error.message.includes(pattern) : pattern.test(error.message));
20
+ return ignore.some((pattern) => typeof pattern === "string"
21
+ ? error.message.includes(pattern)
22
+ : pattern.test(error.message));
20
23
  }
21
24
  /**
22
25
  * Decides, once a test body is done, whether page errors fail it and which log entries go in the report:
23
26
  * `capture` "failures" (default) attaches them to failed tests, "all" always, "off" never.
24
27
  */
25
28
  export function judgePageLogs(input) {
26
- const { config, entries, errors, dropped, testFailed, allowPageErrors, routeErrors = [] } = input;
27
- const capture = config?.capture ?? "failures";
29
+ const { config, entries, errors, dropped, testFailed, allowPageErrors, routeErrors = [], } = input;
30
+ const capture = config?.capture ?? DEFAULT_LOGS_CAPTURE;
28
31
  let error;
29
32
  if (routeErrors.length > 0 && !testFailed) {
30
33
  error = toTestError(new RouteErrorsError(routeErrors));
@@ -70,9 +70,9 @@ export function poolMode(input) {
70
70
  export async function runPooled(pool, planned, items, reporter, signal) {
71
71
  reporter.setOrder(planned);
72
72
  for (const { test, browser } of planned) {
73
- const annotation = staticSkip(test);
74
- if (annotation)
75
- reporter.onTestSkipped(test, browser, { annotation });
73
+ const skip = staticSkip(test);
74
+ if (skip)
75
+ reporter.onTestSkipped(test, browser, skip);
76
76
  }
77
77
  const started = new Set();
78
78
  const ended = new Set();
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { removeGeneratedFiles } from "../browser/generated-files.js";
3
- import { loadConfig, projectDir, setActiveProject } from "../config/config.js";
3
+ import { loadRawConfig, projectDir, setActiveProject, } from "../config/config.js";
4
4
  import { resetSecrets } from "../config/mask.js";
5
5
  import { setRunSettings } from "../config/run-settings.js";
6
6
  import { keepLogFiles } from "../logger/index.js";
@@ -20,7 +20,7 @@ export async function withProject(options, work) {
20
20
  const releaseLogs = keepLogFiles(path.resolve(dir ?? previousDir));
21
21
  try {
22
22
  setActiveProject({ dir: dir ?? previousDir, config, dataDir });
23
- const loaded = config ?? (await loadConfig());
23
+ const loaded = config ?? (await loadRawConfig());
24
24
  setActiveProject({ dir: dir ?? previousDir, config: loaded, dataDir });
25
25
  resetSecrets();
26
26
  const run = prepareRun(loaded, {
@@ -1,4 +1,4 @@
1
- import type { AllHook, EachHook, RegisteredTestCase, TestCaseBase } from "../types/test.js";
1
+ import type { AllHook, EachHook, RegisteredTestCase, TestCaseBase, TestDetails } from "../types/test.js";
2
2
  type Body = TestCaseBase["function"];
3
3
  /** `test(title, fn)`, `test.skip(title, fn)`, `test.skip(condition, reason)` and the other forms */
4
4
  export interface TestFunction {
@@ -10,16 +10,25 @@ export interface TestFunction {
10
10
  /** Runs only this test (and the other `only` ones) */
11
11
  only(title: string, fn: Body): void;
12
12
  }
13
- /** With a title and a body it registers a skipped test; with a condition it skips the running test when the condition is true */
13
+ /**
14
+ * With a title and a body it registers a skipped test, `{ reason }` saying why; with a condition it skips the
15
+ * running test when the condition is true
16
+ */
14
17
  interface SkipFunction {
15
18
  (title: string, fn: Body): void;
19
+ (title: string, details: TestDetails, fn: Body): void;
16
20
  (condition: boolean, reason?: string): void;
17
21
  }
22
+ /** `describe.skip` and `describe.fixme`: every test inside gets the modifier, and `{ reason }` says why */
23
+ interface DescribeSkipFunction {
24
+ (title: string, fn: () => void): void;
25
+ (title: string, details: TestDetails, fn: () => void): void;
26
+ }
18
27
  /** `describe(title, fn)` and the same with a modifier on every test inside */
19
28
  export interface DescribeFunction {
20
29
  (title: string, fn: () => void): void;
21
- skip(title: string, fn: () => void): void;
22
- fixme(title: string, fn: () => void): void;
30
+ skip: DescribeSkipFunction;
31
+ fixme: DescribeSkipFunction;
23
32
  only(title: string, fn: () => void): void;
24
33
  }
25
34
  export declare const test: TestFunction;
@@ -4,8 +4,8 @@ import { SkipSignal } from "./skip.js";
4
4
  import { addHook, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
5
5
  const registered = [];
6
6
  /** Runs `fn` as the `describe` block `title`, under `modifier` when there is one. `fn` runs immediately and must be synchronous. */
7
- function openDescribe(file, title, fn, modifier) {
8
- enterSuite(file, title, modifier);
7
+ function openDescribe(file, title, fn, modifier, reason) {
8
+ enterSuite(file, title, modifier, reason);
9
9
  try {
10
10
  const result = fn();
11
11
  if (result instanceof Promise) {
@@ -17,18 +17,17 @@ function openDescribe(file, title, fn, modifier) {
17
17
  leaveSuite();
18
18
  }
19
19
  }
20
- function add(file, title, fn, modifier) {
20
+ function add(file, title, fn, modifier, reason) {
21
21
  const suite = currentSuite(file);
22
22
  const titlePath = [...titlesOf(suite), title];
23
23
  const name = titlePath.join(" > ");
24
- const effective = modifierOf(modifier, suite);
25
24
  registered.push({
26
25
  name,
27
26
  titlePath,
28
27
  function: fn,
29
28
  file,
30
29
  suite,
31
- ...(effective && { modifier: effective }),
30
+ ...modifierOf({ modifier, ...(reason !== undefined && { reason }) }, suite),
32
31
  });
33
32
  logger.debug("Registered test %s", name);
34
33
  }
@@ -40,26 +39,44 @@ function titlesOf(suite) {
40
39
  titles.unshift(s.title);
41
40
  return titles;
42
41
  }
42
+ /** The body and `{ reason }` of `(title, fn)` and `(title, details, fn)` */
43
+ function bodyAndReason(call, title, second, third) {
44
+ const [details, fn] = typeof second === "function"
45
+ ? [undefined, second]
46
+ : [second, third];
47
+ if (typeof fn !== "function")
48
+ throw new TypeError(`${call}(${JSON.stringify(title)}) needs a function as its last argument`);
49
+ const reason = details?.reason;
50
+ return { fn: fn, ...(reason !== undefined && { reason }) };
51
+ }
43
52
  /** `test.skip` and `test.fixme`: a title and a body register a test with the modifier; a condition skips the running test */
44
53
  function skipping(annotation) {
45
- return ((first, second) => {
54
+ return ((first, second, third) => {
46
55
  if (typeof first === "string") {
56
+ const { fn, reason } = bodyAndReason(`test.${annotation}`, first, second, third);
47
57
  // Frames: Error, callerFile, this function, the spec
48
- add(callerFile(), first, second, annotation);
58
+ add(callerFile(), first, fn, annotation, reason);
49
59
  return;
50
60
  }
51
61
  if (first)
52
62
  throw new SkipSignal(annotation, second);
53
63
  });
54
64
  }
65
+ /** `describe.skip` and `describe.fixme`, with or without `{ reason }` */
66
+ function describeSkipping(annotation) {
67
+ return ((title, second, third) => {
68
+ const { fn, reason } = bodyAndReason(`describe.${annotation}`, title, second, third);
69
+ openDescribe(callerFile(), title, fn, annotation, reason);
70
+ });
71
+ }
55
72
  export const test = Object.assign((title, fn) => add(callerFile(), title, fn), {
56
73
  skip: skipping("skip"),
57
74
  fixme: skipping("fixme"),
58
75
  only: (title, fn) => add(callerFile(), title, fn, "only"),
59
76
  });
60
77
  export const describe = Object.assign((title, fn) => openDescribe(callerFile(), title, fn), {
61
- skip: (title, fn) => openDescribe(callerFile(), title, fn, "skip"),
62
- fixme: (title, fn) => openDescribe(callerFile(), title, fn, "fixme"),
78
+ skip: describeSkipping("skip"),
79
+ fixme: describeSkipping("fixme"),
63
80
  only: (title, fn) => openDescribe(callerFile(), title, fn, "only"),
64
81
  });
65
82
  /** Runs before each test of the `describe` block it is written in (of the file, at the top level) */
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_RETRIES } from "../config/defaults.js";
1
2
  import { parseRetries } from "../config/retries.js";
2
3
  /** How many times a failing test is run again: the flag, else `SAMURAI_RETRIES`, else the config's `retries`, else 0 */
3
4
  export function resolveRetries({ flag, env, config }) {
@@ -8,5 +9,5 @@ export function resolveRetries({ flag, env, config }) {
8
9
  return parseRetries(fromEnv, "SAMURAI_RETRIES");
9
10
  if (config !== undefined)
10
11
  return parseRetries(config, "retries");
11
- return 0;
12
+ return DEFAULT_RETRIES;
12
13
  }
@@ -67,7 +67,7 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
67
67
  // A test marked skip or fixme never runs, whoever asked for it (a flow's test node, a worker)
68
68
  const marked = staticSkip(test);
69
69
  if (marked) {
70
- reporter.onTestSkipped(test, browserName, { annotation: marked });
70
+ reporter.onTestSkipped(test, browserName, marked);
71
71
  return;
72
72
  }
73
73
  logger.verbose("Starting test: %s in %s", test.name, browserName);
@@ -1,6 +1,6 @@
1
1
  import type { Modifier, TestSummary } from "../types/test.js";
2
2
  import { type ProjectOptions } from "./project.js";
3
- import type { ResolvedGroup } from "./groups.js";
3
+ import { type ResolvedGroup } from "./groups.js";
4
4
  import type { RunnerOptions } from "./options.js";
5
5
  export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
6
6
  }
@@ -16,13 +16,25 @@ export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
16
16
  export declare function runTests(options?: RunTestsOptions): Promise<TestSummary>;
17
17
  /** A test found in a spec file */
18
18
  export interface FoundTest {
19
+ /**
20
+ * The id the groups use for this test (`ResolvedGroup.testIds`): its spec file relative to `srcDir` with `/`
21
+ * separators, then `::` and its full name, e.g. `checkout/cart.spec.ts::Cart > adds`
22
+ */
23
+ id: string;
19
24
  /** Full name: `describe` titles and the test title joined with " > " */
20
25
  name: string;
21
26
  file: string;
27
+ /** The spec file relative to `srcDir`, with `/` separators */
28
+ relativeFile: string;
22
29
  /** `skip` or `fixme` (not run), or `only` (focused); absent for a plain test. A modifier on a `describe` counts. */
23
30
  modifier?: Modifier;
31
+ /** The `{ reason }` given with the `skip` or `fixme` that applies */
32
+ reason?: string;
24
33
  }
25
- /** Imports the project's specs without running anything and lists the tests the options select */
34
+ /**
35
+ * Imports the project's specs without running anything and lists the tests the options select, each with the id
36
+ * its group uses and its spec file relative to `srcDir`
37
+ */
26
38
  export declare function listTests(options?: RunTestsOptions): Promise<FoundTest[]>;
27
39
  /** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
28
40
  export declare function listGroups(options?: RunTestsOptions): Promise<ResolvedGroup[]>;
@@ -1,4 +1,6 @@
1
+ import path from "path";
1
2
  import { withProject } from "./project.js";
3
+ import { testId, toPosix } from "./groups.js";
2
4
  import TestRunner from "./test-runner.js";
3
5
  /**
4
6
  * Sets the project up (config, environment, secrets), builds a runner from the rest of the options and hands both to
@@ -33,13 +35,26 @@ function inProject(options, work) {
33
35
  export async function runTests(options = {}) {
34
36
  return inProject(options, (runner) => runner.start());
35
37
  }
36
- /** Imports the project's specs without running anything and lists the tests the options select */
38
+ /**
39
+ * Imports the project's specs without running anything and lists the tests the options select, each with the id
40
+ * its group uses and its spec file relative to `srcDir`
41
+ */
37
42
  export async function listTests(options = {}) {
38
- return inProject(options, async (runner) => (await runner.register()).map(({ name, file, modifier }) => ({
39
- name,
40
- file,
41
- ...(modifier && { modifier }),
42
- })));
43
+ return inProject(options, async (runner) => {
44
+ const { tests, srcDir } = await runner.list();
45
+ return tests.map((test) => {
46
+ const { name, file, modifier, reason } = test;
47
+ const relativeFile = toPosix(path.relative(srcDir, file));
48
+ return {
49
+ id: testId(relativeFile, name),
50
+ name,
51
+ file,
52
+ relativeFile,
53
+ ...(modifier && { modifier }),
54
+ ...(reason !== undefined && { reason }),
55
+ };
56
+ });
57
+ });
43
58
  }
44
59
  /** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
45
60
  export async function listGroups(options = {}) {
@@ -1,8 +1,8 @@
1
1
  import path from "path";
2
2
  import { pathToFileURL } from "url";
3
- import { loadConfig } from "../config/config.js";
3
+ import { loadRawConfig } from "../config/config.js";
4
4
  import logger from "../logger/index.js";
5
- import { browsersFor } from "./browsers.js";
5
+ import { browsersFor } from "../config/browsers-for.js";
6
6
  import { findGroup, resolveGroups, testId, toPosix, } from "./groups.js";
7
7
  import { assertModuleProject } from "./module-type.js";
8
8
  import { clearRegistry, registeredTests } from "./registry.js";
@@ -40,7 +40,7 @@ export function idOf(test, srcDir) {
40
40
  }
41
41
  /** The config's groups, resolved against the tests the specs register */
42
42
  async function resolveAll(tests) {
43
- const config = await loadConfig();
43
+ const config = await loadRawConfig();
44
44
  const { dir, srcDir } = realDirs(config.srcDir);
45
45
  const groups = resolveGroups(config, tests.map(({ file, name }) => ({
46
46
  file: toPosix(path.relative(srcDir, file)),
@@ -69,7 +69,7 @@ export function selectTests({ all, groups, srcDir }, options) {
69
69
  */
70
70
  export async function browsersOfRun(groups, options) {
71
71
  const { browsers: flag, group } = options;
72
- const config = await loadConfig();
72
+ const config = await loadRawConfig();
73
73
  const ofGroup = group !== undefined && !(flag && flag.length > 0)
74
74
  ? findGroup(groups, group).browsers
75
75
  : undefined;
@@ -1,4 +1,5 @@
1
1
  import type { RegisteredTestCase } from "../types/test.js";
2
+ import type { TestSkip } from "./reporter.js";
2
3
  /** Thrown by `test.skip(condition, reason)` and `test.fixme(condition, reason)` inside a test: the test ends as skipped */
3
4
  export declare class SkipSignal extends Error {
4
5
  readonly annotation: "skip" | "fixme";
@@ -7,4 +8,4 @@ export declare class SkipSignal extends Error {
7
8
  }
8
9
  export declare function isSkipSignal(error: unknown): error is SkipSignal;
9
10
  /** Why a test does not run at all: it is marked `skip` or `fixme`, itself or by a `describe` around it */
10
- export declare function staticSkip(test: Pick<RegisteredTestCase, "modifier">): "skip" | "fixme" | undefined;
11
+ export declare function staticSkip(test: Pick<RegisteredTestCase, "modifier" | "reason">): TestSkip | undefined;
@@ -12,7 +12,8 @@ export function isSkipSignal(error) {
12
12
  }
13
13
  /** Why a test does not run at all: it is marked `skip` or `fixme`, itself or by a `describe` around it */
14
14
  export function staticSkip(test) {
15
- return test.modifier === "skip" || test.modifier === "fixme"
16
- ? test.modifier
15
+ const { modifier, reason } = test;
16
+ return modifier === "skip" || modifier === "fixme"
17
+ ? { annotation: modifier, ...(reason !== undefined && { reason }) }
17
18
  : undefined;
18
19
  }
@@ -1,19 +1,25 @@
1
1
  import type { EachHook, Modifier, Suite } from "../types/test.js";
2
2
  /** The four hook lists a suite can hold */
3
3
  export type HookKind = "beforeEach" | "afterEach" | "beforeAll" | "afterAll";
4
- export declare function newSuite(title?: string, parent?: Suite, modifier?: Modifier): Suite;
4
+ export declare function newSuite(title?: string, parent?: Suite, modifier?: Modifier, reason?: string): Suite;
5
5
  /** The suite a test or hook written now belongs to: the innermost open `describe`, else the file's own */
6
6
  export declare function currentSuite(file: string): Suite;
7
7
  /** Opens a `describe` block under the current suite; `leaveSuite` closes it */
8
- export declare function enterSuite(file: string, title: string, modifier?: Modifier): Suite;
8
+ export declare function enterSuite(file: string, title: string, modifier?: Modifier, reason?: string): Suite;
9
9
  export declare function leaveSuite(): void;
10
10
  /** Forgets every suite: a run registers its specs afresh */
11
11
  export declare function resetSuites(): void;
12
12
  /**
13
13
  * What applies to a test: the innermost `skip` or `fixme` (the test's own, then its suites', inside out), else
14
- * `only` when the test or any suite around it is focused. A skip beats a focus.
14
+ * `only` when the test or any suite around it is focused. A skip beats a focus. The reason comes with the skip
15
+ * or fixme that applies.
15
16
  */
16
- export declare function modifierOf(own: Modifier | undefined, suite: Suite): Modifier | undefined;
17
+ export declare function modifierOf(own: Mark, suite: Suite): Mark;
18
+ /** A modifier and the reason given with it */
19
+ export interface Mark {
20
+ modifier?: Modifier;
21
+ reason?: string;
22
+ }
17
23
  /** The suites from the file's root down to `suite` */
18
24
  export declare function suiteChain(suite: Suite): Suite[];
19
25
  /** The `beforeEach` hooks of a test: outermost suite first, in the order they were written */
@@ -2,11 +2,12 @@
2
2
  const roots = new Map();
3
3
  /** The `describe` blocks being registered, outermost first */
4
4
  const open = [];
5
- export function newSuite(title, parent, modifier) {
5
+ export function newSuite(title, parent, modifier, reason) {
6
6
  return {
7
7
  ...(title !== undefined && { title }),
8
8
  ...(parent && { parent }),
9
9
  ...(modifier && { modifier }),
10
+ ...(reason !== undefined && { reason }),
10
11
  };
11
12
  }
12
13
  /** The suite a test or hook written now belongs to: the innermost open `describe`, else the file's own */
@@ -22,8 +23,8 @@ export function currentSuite(file) {
22
23
  return root;
23
24
  }
24
25
  /** Opens a `describe` block under the current suite; `leaveSuite` closes it */
25
- export function enterSuite(file, title, modifier) {
26
- const suite = newSuite(title, currentSuite(file), modifier);
26
+ export function enterSuite(file, title, modifier, reason) {
27
+ const suite = newSuite(title, currentSuite(file), modifier, reason);
27
28
  open.push(suite);
28
29
  return suite;
29
30
  }
@@ -37,18 +38,26 @@ export function resetSuites() {
37
38
  }
38
39
  /**
39
40
  * What applies to a test: the innermost `skip` or `fixme` (the test's own, then its suites', inside out), else
40
- * `only` when the test or any suite around it is focused. A skip beats a focus.
41
+ * `only` when the test or any suite around it is focused. A skip beats a focus. The reason comes with the skip
42
+ * or fixme that applies.
41
43
  */
42
44
  export function modifierOf(own, suite) {
43
- let focused = own === "only";
44
- let found = own === "only" ? undefined : own;
45
- for (let s = suite; s && !found; s = s.parent) {
45
+ if (own.modifier === "skip" || own.modifier === "fixme")
46
+ return markOf(own);
47
+ let focused = own.modifier === "only";
48
+ for (let s = suite; s; s = s.parent) {
46
49
  if (s.modifier === "only")
47
50
  focused = true;
48
- else
49
- found = s.modifier;
51
+ else if (s.modifier)
52
+ return markOf(s);
50
53
  }
51
- return found ?? (focused ? "only" : undefined);
54
+ return focused ? { modifier: "only" } : {};
55
+ }
56
+ function markOf({ modifier, reason }) {
57
+ return {
58
+ ...(modifier && { modifier }),
59
+ ...(reason !== undefined && { reason }),
60
+ };
52
61
  }
53
62
  /** The suites from the file's root down to `suite` */
54
63
  export function suiteChain(suite) {
@@ -1,7 +1,8 @@
1
1
  import path from "path";
2
2
  import { projectDir } from "../config/config.js";
3
3
  import { cleanName } from "../browser/evidence.js";
4
- import { DEFAULT_SRC_DIR, realPath, toPosix } from "./groups.js";
4
+ import { DEFAULT_SRC_DIR } from "../config/defaults.js";
5
+ import { realPath, toPosix } from "./groups.js";
5
6
  /** The project and source folders as real paths: registered test files come from stack traces, which resolve symlinks */
6
7
  export function realDirs(configuredSrcDir) {
7
8
  const dir = realPath(projectDir());
@@ -26,8 +26,14 @@ export default class TestRunner {
26
26
  static init(run: PreparedRun, options?: RunnerOptions, project?: ProjectInfo): Promise<TestRunner>;
27
27
  /** The groups of the config with the tests each holds; see `listGroups` */
28
28
  groups(): Promise<ResolvedGroup[]>;
29
- /** Imports the spec files and returns the tests the options select: by `group`, `testNames` and `grep` */
30
- register(): Promise<RegisteredTestCase[]>;
29
+ /**
30
+ * Imports the spec files and returns the tests the options select (by `group`, `testNames` and `grep`)
31
+ * with the real `srcDir` their ids are relative to
32
+ */
33
+ list(): Promise<{
34
+ tests: RegisteredTestCase[];
35
+ srcDir: string;
36
+ }>;
31
37
  /**
32
38
  * Runs the selected tests in every browser of the run, one browser after the other, and returns the summary that
33
39
  * was also written to the report. With more than one worker the tests run at the same time in worker processes.