@itmetsam/samurai-framework 0.3.0 → 0.5.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 (66) hide show
  1. package/dist/cli/flow-reporter.d.ts +5 -2
  2. package/dist/cli/flow-reporter.js +4 -2
  3. package/dist/cli/flow.d.ts +1 -1
  4. package/dist/cli/flow.js +4 -2
  5. package/dist/config/config.d.ts +5 -0
  6. package/dist/config/config.js +13 -0
  7. package/dist/flows/core/check.d.ts +4 -1
  8. package/dist/flows/core/check.js +17 -14
  9. package/dist/flows/core/expression/check.d.ts +2 -0
  10. package/dist/flows/core/expression/check.js +3 -3
  11. package/dist/flows/core/expression/evaluate.d.ts +1 -1
  12. package/dist/flows/core/expression/scope.d.ts +3 -2
  13. package/dist/flows/core/expression/scope.js +15 -6
  14. package/dist/flows/core/index.d.ts +2 -1
  15. package/dist/flows/core/index.js +2 -1
  16. package/dist/flows/core/nodes/define.d.ts +4 -0
  17. package/dist/flows/core/nodes/index.d.ts +4 -66
  18. package/dist/flows/core/nodes/index.js +12 -38
  19. package/dist/flows/core/steps.d.ts +42 -0
  20. package/dist/flows/core/steps.js +58 -0
  21. package/dist/flows/run/events.d.ts +3 -1
  22. package/dist/flows/run/handlers/index.d.ts +1 -3
  23. package/dist/flows/run/handlers/index.js +3 -8
  24. package/dist/flows/run/handlers/support.d.ts +4 -1
  25. package/dist/flows/run/handlers/support.js +1 -1
  26. package/dist/flows/run/index.d.ts +2 -1
  27. package/dist/flows/run/index.js +2 -1
  28. package/dist/flows/run/project.js +18 -19
  29. package/dist/flows/run/run.d.ts +1 -1
  30. package/dist/flows/run/run.js +7 -1
  31. package/dist/flows/steps/api.d.ts +23 -0
  32. package/dist/flows/steps/api.js +63 -0
  33. package/dist/flows/steps/catalog.d.ts +22 -0
  34. package/dist/flows/steps/catalog.js +36 -0
  35. package/dist/flows/steps/define.d.ts +51 -0
  36. package/dist/flows/steps/define.js +51 -0
  37. package/dist/flows/steps/handler.d.ts +4 -0
  38. package/dist/flows/steps/handler.js +82 -0
  39. package/dist/flows/steps/index.d.ts +6 -0
  40. package/dist/flows/steps/index.js +5 -0
  41. package/dist/flows/{core/nodes → steps}/set-variable.d.ts +4 -1
  42. package/dist/flows/steps/set-variable.js +31 -0
  43. package/dist/flows/steps/set.d.ts +26 -0
  44. package/dist/flows/steps/set.js +43 -0
  45. package/dist/flows/steps/wait.d.ts +8 -0
  46. package/dist/flows/steps/wait.js +21 -0
  47. package/dist/runner/groups.d.ts +5 -0
  48. package/dist/runner/groups.js +13 -0
  49. package/dist/runner/test-runner.js +4 -3
  50. package/dist/types/config.d.ts +7 -0
  51. package/package.json +5 -1
  52. package/dist/flows/core/nodes/api.d.ts +0 -9
  53. package/dist/flows/core/nodes/api.js +0 -12
  54. package/dist/flows/core/nodes/database.d.ts +0 -9
  55. package/dist/flows/core/nodes/database.js +0 -12
  56. package/dist/flows/core/nodes/email.d.ts +0 -9
  57. package/dist/flows/core/nodes/email.js +0 -12
  58. package/dist/flows/core/nodes/script.d.ts +0 -9
  59. package/dist/flows/core/nodes/script.js +0 -12
  60. package/dist/flows/core/nodes/set-variable.js +0 -39
  61. package/dist/flows/core/nodes/wait.d.ts +0 -15
  62. package/dist/flows/core/nodes/wait.js +0 -18
  63. package/dist/flows/run/handlers/set-variable.d.ts +0 -2
  64. package/dist/flows/run/handlers/set-variable.js +0 -22
  65. package/dist/flows/run/handlers/wait.d.ts +0 -2
  66. package/dist/flows/run/handlers/wait.js +0 -10
@@ -0,0 +1,43 @@
1
+ import { projectConfig } from "../../config/config.js";
2
+ import { api } from "./api.js";
3
+ import { checkStep, infoOf, stepNode } from "./define.js";
4
+ import { stepHandler } from "./handler.js";
5
+ import { setVariable } from "./set-variable.js";
6
+ import { wait } from "./wait.js";
7
+ /** The built-in steps, in library order */
8
+ export const BUILTIN_STEPS = [wait, setVariable, api];
9
+ const WHERE = " (samurai.config.ts)";
10
+ /** The built-in steps plus a project's (its config's `steps`). Throws for a list that can't work. */
11
+ export function stepSet(projectSteps = []) {
12
+ if (!Array.isArray(projectSteps))
13
+ throw new Error("samurai.config.ts: steps must be a list of steps.");
14
+ const builtin = new Set(BUILTIN_STEPS.map((s) => s.kind));
15
+ const seen = new Set();
16
+ projectSteps.forEach((entry, i) => {
17
+ const kind = entry && typeof entry === "object"
18
+ ? entry.kind
19
+ : undefined;
20
+ if (typeof kind !== "string" || kind === "")
21
+ throw new Error(`Step #${i + 1}${WHERE}: it has no kind; make it with defineStep.`);
22
+ checkStep(entry, WHERE);
23
+ if (builtin.has(kind))
24
+ throw new Error(`Step "${kind}"${WHERE}: a built-in step already uses this kind.`);
25
+ if (seen.has(kind))
26
+ throw new Error(`Step "${kind}"${WHERE}: declared twice.`);
27
+ seen.add(kind);
28
+ });
29
+ const steps = [...BUILTIN_STEPS, ...projectSteps];
30
+ return {
31
+ steps,
32
+ nodes: steps.map(stepNode),
33
+ handlers: Object.fromEntries(steps.map((s) => [s.kind, stepHandler(s)])),
34
+ catalog: steps.map(infoOf),
35
+ };
36
+ }
37
+ /** The built-in steps alone */
38
+ export const BUILTIN_SET = stepSet();
39
+ /** The project's steps: its config's `steps` after the built-ins. `projectDir` defaults to the working directory. */
40
+ export async function projectSteps(options = {}) {
41
+ const config = await projectConfig(options);
42
+ return config.steps === undefined ? BUILTIN_SET : stepSet(config.steps);
43
+ }
@@ -0,0 +1,8 @@
1
+ export declare const wait: import("./define.js").StepDefinition<import("./define.js").RunConfigOf<readonly [{
2
+ readonly path: "config.ms";
3
+ readonly type: "number";
4
+ readonly label: "Duration";
5
+ readonly unit: "ms";
6
+ readonly default: 1000;
7
+ readonly min: 0;
8
+ }]>>;
@@ -0,0 +1,21 @@
1
+ import { defineStep } from "./define.js";
2
+ export const wait = defineStep({
3
+ kind: "wait",
4
+ label: "Wait",
5
+ icon: "timer",
6
+ section: "control",
7
+ fields: [
8
+ {
9
+ path: "config.ms",
10
+ type: "number",
11
+ label: "Duration",
12
+ unit: "ms",
13
+ default: 1000,
14
+ min: 0,
15
+ },
16
+ ],
17
+ summary: "{config.ms} ms",
18
+ async run({ config, sleep }) {
19
+ await sleep(config.ms);
20
+ },
21
+ });
@@ -23,6 +23,11 @@ export interface ResolvedGroup {
23
23
  title: string;
24
24
  }[];
25
25
  }
26
+ /**
27
+ * The real path of `p` (symlinks resolved), or `p` itself when it does not exist. Registered test files come
28
+ * from stack traces, which resolve symlinks, so paths compared with them must be real too
29
+ */
30
+ export declare function realPath(p: string): string;
26
31
  /** Separators of any platform become `/` */
27
32
  export declare const toPosix: (p: string) => string;
28
33
  /** The id of a test: its spec file relative to `srcDir` with `/` separators, then `::` and its full name */
@@ -1,8 +1,21 @@
1
+ import { realpathSync } from "node:fs";
1
2
  import { matchesGlob, relative, resolve } from "node:path";
2
3
  /** The test folder when the config names none */
3
4
  export const DEFAULT_SRC_DIR = "./src";
4
5
  /** The spec pattern when a group names none */
5
6
  export const DEFAULT_TEST_MATCH = "**/*.spec.ts";
7
+ /**
8
+ * The real path of `p` (symlinks resolved), or `p` itself when it does not exist. Registered test files come
9
+ * from stack traces, which resolve symlinks, so paths compared with them must be real too
10
+ */
11
+ export function realPath(p) {
12
+ try {
13
+ return realpathSync(p);
14
+ }
15
+ catch {
16
+ return p;
17
+ }
18
+ }
6
19
  /** Separators of any platform become `/` */
7
20
  export const toPosix = (p) => p.replace(/\\/g, "/");
8
21
  /** The id of a test: its spec file relative to `srcDir` with `/` separators, then `::` and its full name */
@@ -14,7 +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
+ import { DEFAULT_SRC_DIR, findGroup, realPath, resolveGroups, testId, toPosix, } from "./groups.js";
18
18
  import { judgePageLogs } from "./page-logs-report.js";
19
19
  /** Whether a test with this full name is selected by the options */
20
20
  export function isSelected(name, { grep, testNames }) {
@@ -81,8 +81,9 @@ export default class TestRunner {
81
81
  /** The config's groups, resolved against the tests the specs register */
82
82
  async resolveAll(tests) {
83
83
  const config = await loadConfig();
84
- const dir = projectDir();
85
- const srcDir = path.resolve(dir, config.srcDir ?? DEFAULT_SRC_DIR);
84
+ // Real paths: registered test files come from stack traces, which resolve symlinks
85
+ const dir = realPath(projectDir());
86
+ const srcDir = realPath(path.resolve(dir, config.srcDir ?? DEFAULT_SRC_DIR));
86
87
  const groups = resolveGroups(config, tests.map(({ file, name }) => ({
87
88
  file: toPosix(path.relative(srcDir, file)),
88
89
  name,
@@ -1,3 +1,4 @@
1
+ import type { StepDefinition } from "../flows/steps/define.js";
1
2
  import type { SupportedBrowser } from "./browser.js";
2
3
 
3
4
  interface SamuraiTest {
@@ -57,6 +58,12 @@ export interface SamuraiTestConfig {
57
58
  * Define a group of test's you want to have executed with a certain configuration
58
59
  **/
59
60
  groups?: SamuraiGroup[];
61
+ /**
62
+ * Flow steps of this project, made with `defineStep`. Listed after the built-in steps;
63
+ * a kind a built-in or another step uses is refused.
64
+ */
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ steps?: readonly StepDefinition<any>[];
60
67
  /**
61
68
  * The base URL to use when navigating the browser.
62
69
  * When recording starts, the browser will start at this URL.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itmetsam/samurai-framework",
3
- "version": "0.3.0",
3
+ "version": "0.5.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": {
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/flows/run/index.d.ts",
37
37
  "default": "./dist/flows/run/index.js"
38
38
  },
39
+ "./flows/steps": {
40
+ "types": "./dist/flows/steps/index.d.ts",
41
+ "default": "./dist/flows/steps/index.js"
42
+ },
39
43
  "./package.json": "./package.json"
40
44
  },
41
45
  "scripts": {
@@ -1,9 +0,0 @@
1
- export declare const api: import("./define.js").NodeCoreDefinition<"api", import("./define.js").ConfigOf<readonly [{
2
- readonly path: "title";
3
- readonly type: "text";
4
- readonly label: "Title";
5
- }, {
6
- readonly path: "subtitle";
7
- readonly type: "text";
8
- readonly label: "Description";
9
- }]>>;
@@ -1,12 +0,0 @@
1
- import { defineNode } from "./define.js";
2
- // Declared so flows can hold it; no handler runs it yet (unsupported).
3
- export const api = defineNode({
4
- kind: "api",
5
- label: "HTTP request",
6
- shape: "step",
7
- fields: [
8
- { path: "title", type: "text", label: "Title" },
9
- { path: "subtitle", type: "text", label: "Description" },
10
- ],
11
- outputs: true,
12
- });
@@ -1,9 +0,0 @@
1
- export declare const database: import("./define.js").NodeCoreDefinition<"database", import("./define.js").ConfigOf<readonly [{
2
- readonly path: "title";
3
- readonly type: "text";
4
- readonly label: "Title";
5
- }, {
6
- readonly path: "subtitle";
7
- readonly type: "text";
8
- readonly label: "Description";
9
- }]>>;
@@ -1,12 +0,0 @@
1
- import { defineNode } from "./define.js";
2
- // Declared so flows can hold it; no handler runs it yet (unsupported).
3
- export const database = defineNode({
4
- kind: "database",
5
- label: "Database query",
6
- shape: "step",
7
- fields: [
8
- { path: "title", type: "text", label: "Title" },
9
- { path: "subtitle", type: "text", label: "Description" },
10
- ],
11
- outputs: true,
12
- });
@@ -1,9 +0,0 @@
1
- export declare const email: import("./define.js").NodeCoreDefinition<"email", import("./define.js").ConfigOf<readonly [{
2
- readonly path: "title";
3
- readonly type: "text";
4
- readonly label: "Title";
5
- }, {
6
- readonly path: "subtitle";
7
- readonly type: "text";
8
- readonly label: "Description";
9
- }]>>;
@@ -1,12 +0,0 @@
1
- import { defineNode } from "./define.js";
2
- // Declared so flows can hold it; no handler runs it yet (unsupported).
3
- export const email = defineNode({
4
- kind: "email",
5
- label: "Email inbox",
6
- shape: "step",
7
- fields: [
8
- { path: "title", type: "text", label: "Title" },
9
- { path: "subtitle", type: "text", label: "Description" },
10
- ],
11
- outputs: true,
12
- });
@@ -1,9 +0,0 @@
1
- export declare const script: import("./define.js").NodeCoreDefinition<"script", import("./define.js").ConfigOf<readonly [{
2
- readonly path: "title";
3
- readonly type: "text";
4
- readonly label: "Title";
5
- }, {
6
- readonly path: "subtitle";
7
- readonly type: "text";
8
- readonly label: "Description";
9
- }]>>;
@@ -1,12 +0,0 @@
1
- import { defineNode } from "./define.js";
2
- // Declared so flows can hold it; no handler runs it yet (unsupported).
3
- export const script = defineNode({
4
- kind: "script",
5
- label: "Script",
6
- shape: "step",
7
- fields: [
8
- { path: "title", type: "text", label: "Title" },
9
- { path: "subtitle", type: "text", label: "Description" },
10
- ],
11
- outputs: true,
12
- });
@@ -1,39 +0,0 @@
1
- import { defineNode } from "./define.js";
2
- export const setVariable = defineNode({
3
- kind: "set-variable",
4
- label: "Set variable",
5
- shape: "step",
6
- fields: [
7
- {
8
- path: "config.name",
9
- type: "text",
10
- label: "Name",
11
- placeholder: "orderId",
12
- default: "",
13
- },
14
- {
15
- path: "config.value",
16
- type: "expression",
17
- label: "Value",
18
- placeholder: '"order-" + run.environment',
19
- default: "",
20
- },
21
- ],
22
- validate: (node, ctx) => node.config.name === ""
23
- ? [
24
- {
25
- level: "error",
26
- nodeId: node.id,
27
- message: `${ctx.label}: give the variable a name.`,
28
- },
29
- ]
30
- : /^[a-z][a-z0-9_]*$/.test(node.config.name)
31
- ? []
32
- : [
33
- {
34
- level: "error",
35
- nodeId: node.id,
36
- message: `${ctx.label}: use lowercase letters, digits and _ for the name.`,
37
- },
38
- ],
39
- });
@@ -1,15 +0,0 @@
1
- export declare const wait: import("./define.js").NodeCoreDefinition<"wait", import("./define.js").ConfigOf<readonly [{
2
- readonly path: "title";
3
- readonly type: "text";
4
- readonly label: "Title";
5
- }, {
6
- readonly path: "subtitle";
7
- readonly type: "text";
8
- readonly label: "Description";
9
- }, {
10
- readonly path: "config.ms";
11
- readonly type: "number";
12
- readonly label: "Duration";
13
- readonly unit: "ms";
14
- readonly default: 1000;
15
- }]>>;
@@ -1,18 +0,0 @@
1
- import { defineNode } from "./define.js";
2
- export const wait = defineNode({
3
- kind: "wait",
4
- label: "Wait",
5
- shape: "step",
6
- fields: [
7
- { path: "title", type: "text", label: "Title" },
8
- { path: "subtitle", type: "text", label: "Description" },
9
- {
10
- path: "config.ms",
11
- type: "number",
12
- label: "Duration",
13
- unit: "ms",
14
- default: 1000,
15
- },
16
- ],
17
- outputs: true,
18
- });
@@ -1,2 +0,0 @@
1
- import { type Handler } from "./support.js";
2
- export declare const setVariable: Handler;
@@ -1,22 +0,0 @@
1
- import { evaluateText, KEY_PATTERN } from "../../core/index.js";
2
- import { fail, problemAt } from "./support.js";
3
- export const setVariable = async (node, run) => {
4
- const name = node.config?.name;
5
- if (typeof name !== "string" || !KEY_PATTERN.test(name))
6
- return {
7
- status: "failed",
8
- error: "Give the variable a name: lowercase letters, digits and _.",
9
- };
10
- const text = String(node.config?.value ?? "");
11
- const problem = problemAt(node, run, text);
12
- if (problem)
13
- return { status: "failed", error: problem };
14
- try {
15
- const value = evaluateText(text, run.context);
16
- run.context.vars[name] = value;
17
- return { status: "passed", wrote: { [name]: value } };
18
- }
19
- catch (e) {
20
- return fail(e);
21
- }
22
- };
@@ -1,2 +0,0 @@
1
- import type { Handler } from "./support.js";
2
- export declare const wait: Handler;
@@ -1,10 +0,0 @@
1
- export const wait = async (node, run) => {
2
- const ms = typeof node.config?.ms === "number" ? node.config.ms : 1000;
3
- try {
4
- await run.sleep(ms, run.signal);
5
- }
6
- catch {
7
- return "cancelled";
8
- }
9
- return { status: "passed" };
10
- };