@itmetsam/samurai-framework 0.2.3 → 0.4.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 (101) hide show
  1. package/README.md +5 -0
  2. package/bin/samurai.mjs +7 -5
  3. package/dist/cli/args.d.ts +5 -1
  4. package/dist/cli/args.js +6 -1
  5. package/dist/cli/flow-reporter.d.ts +23 -0
  6. package/dist/cli/flow-reporter.js +98 -0
  7. package/dist/cli/flow.d.ts +4 -0
  8. package/dist/cli/flow.js +240 -0
  9. package/dist/cli/main.d.ts +1 -1
  10. package/dist/cli/main.js +14 -0
  11. package/dist/flows/core/check.d.ts +45 -0
  12. package/dist/flows/core/check.js +205 -0
  13. package/dist/flows/core/expression/check.d.ts +23 -0
  14. package/dist/flows/core/expression/check.js +95 -0
  15. package/dist/flows/core/expression/evaluate.d.ts +22 -0
  16. package/dist/flows/core/expression/evaluate.js +141 -0
  17. package/dist/flows/core/expression/help.d.ts +25 -0
  18. package/dist/flows/core/expression/help.js +75 -0
  19. package/dist/flows/core/expression/index.d.ts +5 -0
  20. package/dist/flows/core/expression/index.js +5 -0
  21. package/dist/flows/core/expression/parse.d.ts +61 -0
  22. package/dist/flows/core/expression/parse.js +317 -0
  23. package/dist/flows/core/expression/scope.d.ts +8 -0
  24. package/dist/flows/core/expression/scope.js +34 -0
  25. package/dist/flows/core/graph.d.ts +11 -0
  26. package/dist/flows/core/graph.js +65 -0
  27. package/dist/flows/core/index.d.ts +17 -0
  28. package/dist/flows/core/index.js +17 -0
  29. package/dist/flows/core/keys.d.ts +8 -0
  30. package/dist/flows/core/keys.js +46 -0
  31. package/dist/flows/core/nodes/condition.d.ts +7 -0
  32. package/dist/flows/core/nodes/condition.js +21 -0
  33. package/dist/flows/core/nodes/define.d.ts +132 -0
  34. package/dist/flows/core/nodes/define.js +21 -0
  35. package/dist/flows/core/nodes/end.d.ts +1 -0
  36. package/dist/flows/core/nodes/end.js +7 -0
  37. package/dist/flows/core/nodes/group.d.ts +5 -0
  38. package/dist/flows/core/nodes/group.js +18 -0
  39. package/dist/flows/core/nodes/index.d.ts +45 -0
  40. package/dist/flows/core/nodes/index.js +55 -0
  41. package/dist/flows/core/nodes/parallel.d.ts +1 -0
  42. package/dist/flows/core/nodes/parallel.js +11 -0
  43. package/dist/flows/core/nodes/path.d.ts +5 -0
  44. package/dist/flows/core/nodes/path.js +21 -0
  45. package/dist/flows/core/nodes/start.d.ts +1 -0
  46. package/dist/flows/core/nodes/start.js +7 -0
  47. package/dist/flows/core/nodes/test.d.ts +1 -0
  48. package/dist/flows/core/nodes/test.js +18 -0
  49. package/dist/flows/core/schema.d.ts +55 -0
  50. package/dist/flows/core/schema.js +33 -0
  51. package/dist/flows/core/steps.d.ts +42 -0
  52. package/dist/flows/core/steps.js +47 -0
  53. package/dist/flows/run/events.d.ts +45 -0
  54. package/dist/flows/run/events.js +1 -0
  55. package/dist/flows/run/files.d.ts +12 -0
  56. package/dist/flows/run/files.js +54 -0
  57. package/dist/flows/run/handlers/condition.d.ts +2 -0
  58. package/dist/flows/run/handlers/condition.js +17 -0
  59. package/dist/flows/run/handlers/end.d.ts +2 -0
  60. package/dist/flows/run/handlers/end.js +1 -0
  61. package/dist/flows/run/handlers/group.d.ts +2 -0
  62. package/dist/flows/run/handlers/group.js +7 -0
  63. package/dist/flows/run/handlers/index.d.ts +4 -0
  64. package/dist/flows/run/handlers/index.js +17 -0
  65. package/dist/flows/run/handlers/parallel.d.ts +3 -0
  66. package/dist/flows/run/handlers/parallel.js +2 -0
  67. package/dist/flows/run/handlers/start.d.ts +2 -0
  68. package/dist/flows/run/handlers/start.js +1 -0
  69. package/dist/flows/run/handlers/support.d.ts +29 -0
  70. package/dist/flows/run/handlers/support.js +22 -0
  71. package/dist/flows/run/handlers/test.d.ts +2 -0
  72. package/dist/flows/run/handlers/test.js +7 -0
  73. package/dist/flows/run/index.d.ts +8 -0
  74. package/dist/flows/run/index.js +7 -0
  75. package/dist/flows/run/project.d.ts +43 -0
  76. package/dist/flows/run/project.js +165 -0
  77. package/dist/flows/run/report.d.ts +39 -0
  78. package/dist/flows/run/report.js +46 -0
  79. package/dist/flows/run/run.d.ts +42 -0
  80. package/dist/flows/run/run.js +250 -0
  81. package/dist/flows/steps/api.d.ts +23 -0
  82. package/dist/flows/steps/api.js +40 -0
  83. package/dist/flows/steps/catalog.d.ts +16 -0
  84. package/dist/flows/steps/catalog.js +29 -0
  85. package/dist/flows/steps/define.d.ts +49 -0
  86. package/dist/flows/steps/define.js +40 -0
  87. package/dist/flows/steps/handler.d.ts +4 -0
  88. package/dist/flows/steps/handler.js +70 -0
  89. package/dist/flows/steps/index.d.ts +5 -0
  90. package/dist/flows/steps/index.js +4 -0
  91. package/dist/flows/steps/set-variable.d.ts +16 -0
  92. package/dist/flows/steps/set-variable.js +20 -0
  93. package/dist/flows/steps/wait.d.ts +8 -0
  94. package/dist/flows/steps/wait.js +14 -0
  95. package/dist/runner/groups.d.ts +39 -0
  96. package/dist/runner/groups.js +58 -0
  97. package/dist/runner/run.d.ts +4 -0
  98. package/dist/runner/run.js +12 -0
  99. package/dist/runner/test-runner.d.ts +12 -2
  100. package/dist/runner/test-runner.js +36 -6
  101. package/package.json +13 -1
@@ -0,0 +1,58 @@
1
+ import { matchesGlob, relative, resolve } from "node:path";
2
+ /** The test folder when the config names none */
3
+ export const DEFAULT_SRC_DIR = "./src";
4
+ /** The spec pattern when a group names none */
5
+ export const DEFAULT_TEST_MATCH = "**/*.spec.ts";
6
+ /** Separators of any platform become `/` */
7
+ export const toPosix = (p) => p.replace(/\\/g, "/");
8
+ /** The id of a test: its spec file relative to `srcDir` with `/` separators, then `::` and its full name */
9
+ export const testId = (file, name) => `${toPosix(file)}::${name}`;
10
+ /** Works out which of `tests` (files relative to `srcDir`) belong to the group */
11
+ export function resolveGroup(group, tests, projectDir, srcDir) {
12
+ const found = tests.map((t) => ({ ...t, id: testId(t.file, t.name) }));
13
+ // Tests picked by hand override src and testMatch
14
+ if (group.tests) {
15
+ const known = new Set(found.map((t) => t.id));
16
+ const picked = group.tests.map(({ file, title }) => ({
17
+ file,
18
+ title,
19
+ id: testId(file, title),
20
+ }));
21
+ return {
22
+ name: group.name,
23
+ kind: "picked",
24
+ testIds: picked.filter((t) => known.has(t.id)).map((t) => t.id),
25
+ missing: picked
26
+ .filter((t) => !known.has(t.id))
27
+ .map(({ file, title }) => ({ file, title })),
28
+ };
29
+ }
30
+ const base = group.src ? resolve(projectDir, group.src) : srcDir;
31
+ const pattern = group.testMatch ?? DEFAULT_TEST_MATCH;
32
+ const testIds = found
33
+ .filter((t) => {
34
+ const fromBase = toPosix(relative(base, resolve(srcDir, t.file)));
35
+ return !fromBase.startsWith("..") && matchesGlob(fromBase, pattern);
36
+ })
37
+ .map((t) => t.id);
38
+ return {
39
+ name: group.name,
40
+ kind: "pattern",
41
+ pattern: [group.src, group.testMatch].filter(Boolean).join(" · ") || pattern,
42
+ testIds,
43
+ missing: [],
44
+ };
45
+ }
46
+ /** Resolves every group of the config */
47
+ export function resolveGroups(config, tests, projectDir, srcDir) {
48
+ return (config.groups ?? []).map((group) => resolveGroup(group, tests, projectDir, srcDir));
49
+ }
50
+ /** The group called `name`, or an error naming the groups there are */
51
+ export function findGroup(groups, name) {
52
+ const group = groups.find((g) => g.name === name);
53
+ if (group)
54
+ return group;
55
+ throw new Error(groups.length === 0
56
+ ? `Unknown group "${name}". No groups are configured.`
57
+ : `Unknown group "${name}". Groups: ${groups.map((g) => g.name).join(", ")}.`);
58
+ }
@@ -1,5 +1,6 @@
1
1
  import type { TestSummary } from "../types/test.js";
2
2
  import { type ProjectOptions } from "./project.js";
3
+ import type { ResolvedGroup } from "./groups.js";
3
4
  import { type RunnerOptions } from "./test-runner.js";
4
5
  export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
5
6
  }
@@ -21,7 +22,10 @@ export interface FoundTest {
21
22
  }
22
23
  /** Imports the project's specs without running anything and lists the tests the options select */
23
24
  export declare function listTests(options?: RunTestsOptions): Promise<FoundTest[]>;
25
+ /** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
26
+ export declare function listGroups(options?: RunTestsOptions): Promise<ResolvedGroup[]>;
24
27
  export type { RunEvent } from "./reporter.js";
25
28
  export type { TestError, TestResult, TestSummary } from "../types/test.js";
29
+ export type { ResolvedGroup } from "./groups.js";
26
30
  export type { RunnerOptions } from "./test-runner.js";
27
31
  export type { ProjectOptions } from "./project.js";
@@ -32,3 +32,15 @@ export async function listTests(options = {}) {
32
32
  expectTimeout,
33
33
  }, async (run) => (await (await TestRunner.init(run, undefined, runner)).register()).map(({ name, file }) => ({ name, file })));
34
34
  }
35
+ /** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
36
+ export async function listGroups(options = {}) {
37
+ const { projectDir, config, dataDir, environment, timeout, expectTimeout, ...runner } = options;
38
+ return withProject({
39
+ projectDir,
40
+ config,
41
+ dataDir,
42
+ environment,
43
+ timeout,
44
+ expectTimeout,
45
+ }, async (run) => (await TestRunner.init(run, undefined, runner)).groups());
46
+ }
@@ -2,6 +2,7 @@ import type { RegisteredTestCase } from "../types/test.js";
2
2
  import type { PreparedRun } from "./prepare-run.js";
3
3
  import { type RunEvent } from "./reporter.js";
4
4
  import type { SamuraiGroup } from "../types/config.js";
5
+ import type { ResolvedGroup } from "./groups.js";
5
6
  /** What decides which tests of the found files run, and how the browser starts */
6
7
  export interface RunnerOptions {
7
8
  /** Spec files to run, absolute or relative to the project folder. Default: every `*.spec.ts` under `srcDir` */
@@ -10,6 +11,8 @@ export interface RunnerOptions {
10
11
  grep?: string | RegExp;
11
12
  /** Only tests with exactly one of these full names (`describe` titles joined with " > ") */
12
13
  testNames?: string[];
14
+ /** Only the tests of this group of the config's `groups` (unknown name: an error). Combines with the other filters */
15
+ group?: string;
13
16
  /** Run the browser without a window. @default false */
14
17
  headless?: boolean;
15
18
  /** Remote debugging port of the browser. @default 9223 */
@@ -30,12 +33,19 @@ export default class TestRunner {
30
33
  private options;
31
34
  constructor(testFiles: string[], run: PreparedRun, group?: SamuraiGroup, options?: RunnerOptions);
32
35
  static init(run: PreparedRun, group?: SamuraiGroup, options?: RunnerOptions): Promise<TestRunner>;
33
- /** Runs the selected tests and returns the summary that was also written to the report */
34
36
  /**
35
- * Imports the spec files and returns the tests the options select. A fresh registry and a fresh import of each
37
+ * Imports the spec files and returns every test they register. A fresh registry and a fresh import of each
36
38
  * file, so a second run in this process registers its tests again (and sees edited specs, for files loaded as
37
39
  * ES modules; CommonJS files stay cached until the process ends)
38
40
  */
41
+ private registerAll;
42
+ /** Id of a registered test: its spec file relative to `srcDir`, then its name */
43
+ private idOf;
44
+ /** The config's groups, resolved against the tests the specs register */
45
+ private resolveAll;
46
+ /** The groups of the config with the tests each holds; see `listGroups` */
47
+ groups(): Promise<ResolvedGroup[]>;
48
+ /** Imports the spec files and returns the tests the options select: by `group`, `testNames` and `grep` */
39
49
  register(): Promise<RegisteredTestCase[]>;
40
50
  /** Runs the selected tests and returns the summary that was also written to the report */
41
51
  start(): Promise<import("../types/test.js").TestSummary>;
@@ -3,7 +3,7 @@ import { glob } from "node:fs/promises";
3
3
  import path from "path";
4
4
  import { pathToFileURL } from "url";
5
5
  import { Browser } from "../browser/browser.js";
6
- import { projectDir, readConfig } from "../config/config.js";
6
+ import { loadConfig, projectDir, readConfig } from "../config/config.js";
7
7
  import logger from "../logger/index.js";
8
8
  import { assertModuleProject } from "./module-type.js";
9
9
  import { clearRegistry, registeredTests } from "./registry.js";
@@ -14,6 +14,7 @@ import Page from "../browser/page.js";
14
14
  import { takePendingAssertions } from "../assert/expect.js";
15
15
  import { toTestError } from "./test-error.js";
16
16
  import { ConnectionClosedError } from "../transport/bidi-connection.js";
17
+ import { DEFAULT_SRC_DIR, findGroup, resolveGroups, testId, toPosix, } from "./groups.js";
17
18
  import { judgePageLogs } from "./page-logs-report.js";
18
19
  /** Whether a test with this full name is selected by the options */
19
20
  export function isSelected(name, { grep, testNames }) {
@@ -57,13 +58,12 @@ export default class TestRunner {
57
58
  logger.debug("Found %d tests in srcDir", testFiles.length);
58
59
  return new TestRunner(testFiles.map((file) => path.join(cwd, file)), run, group, options);
59
60
  }
60
- /** Runs the selected tests and returns the summary that was also written to the report */
61
61
  /**
62
- * Imports the spec files and returns the tests the options select. A fresh registry and a fresh import of each
62
+ * Imports the spec files and returns every test they register. A fresh registry and a fresh import of each
63
63
  * file, so a second run in this process registers its tests again (and sees edited specs, for files loaded as
64
64
  * ES modules; CommonJS files stay cached until the process ends)
65
65
  */
66
- async register() {
66
+ async registerAll() {
67
67
  clearRegistry();
68
68
  const stamp = `?run=${Date.now().toString(36)}`;
69
69
  for (const file of this.testFiles)
@@ -72,8 +72,38 @@ export default class TestRunner {
72
72
  logger.verbose("Trying to register file: %s", file);
73
73
  await import(pathToFileURL(file).href + stamp);
74
74
  }
75
- // TODO: Implement test grouping
76
- return registeredTests().filter((test) => isSelected(test.name, this.options));
75
+ return [...registeredTests()];
76
+ }
77
+ /** Id of a registered test: its spec file relative to `srcDir`, then its name */
78
+ idOf(test, srcDir) {
79
+ return testId(toPosix(path.relative(srcDir, test.file)), test.name);
80
+ }
81
+ /** The config's groups, resolved against the tests the specs register */
82
+ async resolveAll(tests) {
83
+ const config = await loadConfig();
84
+ const dir = projectDir();
85
+ const srcDir = path.resolve(dir, config.srcDir ?? DEFAULT_SRC_DIR);
86
+ const groups = resolveGroups(config, tests.map(({ file, name }) => ({
87
+ file: toPosix(path.relative(srcDir, file)),
88
+ name,
89
+ })), dir, srcDir);
90
+ return { groups, srcDir };
91
+ }
92
+ /** The groups of the config with the tests each holds; see `listGroups` */
93
+ async groups() {
94
+ return (await this.resolveAll(await this.registerAll())).groups;
95
+ }
96
+ /** Imports the spec files and returns the tests the options select: by `group`, `testNames` and `grep` */
97
+ async register() {
98
+ const all = await this.registerAll();
99
+ const { group } = this.options;
100
+ let inGroup = () => true;
101
+ if (group !== undefined) {
102
+ const { groups, srcDir } = await this.resolveAll(all);
103
+ const members = new Set(findGroup(groups, group).testIds);
104
+ inGroup = (test) => members.has(this.idOf(test, srcDir));
105
+ }
106
+ return all.filter((test) => inGroup(test) && isSelected(test.name, this.options));
77
107
  }
78
108
  /** Runs the selected tests and returns the summary that was also written to the report */
79
109
  async start() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itmetsam/samurai-framework",
3
- "version": "0.2.3",
3
+ "version": "0.4.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": {
@@ -28,6 +28,18 @@
28
28
  "types": "./dist/browser/index.d.ts",
29
29
  "default": "./dist/browser/index.js"
30
30
  },
31
+ "./flows": {
32
+ "types": "./dist/flows/core/index.d.ts",
33
+ "default": "./dist/flows/core/index.js"
34
+ },
35
+ "./flows/run": {
36
+ "types": "./dist/flows/run/index.d.ts",
37
+ "default": "./dist/flows/run/index.js"
38
+ },
39
+ "./flows/steps": {
40
+ "types": "./dist/flows/steps/index.d.ts",
41
+ "default": "./dist/flows/steps/index.js"
42
+ },
31
43
  "./package.json": "./package.json"
32
44
  },
33
45
  "scripts": {