@itmetsam/samurai-framework 0.20.0 → 0.21.1
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.
- package/README.md +2 -2
- package/dist/api.d.ts +1 -1
- package/dist/assert/assertion-error.d.ts +3 -1
- package/dist/assert/assertion-error.js +2 -2
- package/dist/assert/locator-assertions.d.ts +35 -4
- package/dist/assert/locator-assertions.js +56 -28
- package/dist/assert/page-assertions.js +1 -1
- package/dist/assert/pending-assertions.d.ts +3 -2
- package/dist/assert/pending-assertions.js +5 -4
- package/dist/assert/text-match.d.ts +6 -0
- package/dist/assert/text-match.js +12 -0
- package/dist/browser/browser.js +19 -3
- package/dist/browser/chrome-launch.d.ts +2 -0
- package/dist/browser/chrome-launch.js +5 -0
- package/dist/capabilities.d.ts +16 -5
- package/dist/capabilities.js +16 -5
- package/dist/cli/flow-reporter.d.ts +1 -1
- package/dist/cli/flow-reporter.js +2 -2
- package/dist/cli/flow.js +4 -4
- package/dist/config/mask.d.ts +6 -2
- package/dist/config/mask.js +4 -1
- package/dist/flows/core/check-io.d.ts +23 -0
- package/dist/flows/core/check-io.js +84 -0
- package/dist/flows/core/check.d.ts +16 -2
- package/dist/flows/core/check.js +20 -4
- package/dist/flows/core/expression/scope.d.ts +9 -2
- package/dist/flows/core/expression/scope.js +12 -4
- package/dist/flows/core/index.d.ts +2 -1
- package/dist/flows/core/index.js +2 -1
- package/dist/flows/core/nodes/condition.js +0 -1
- package/dist/flows/core/nodes/define.d.ts +0 -2
- package/dist/flows/core/nodes/group.js +0 -1
- package/dist/flows/core/nodes/test.js +0 -1
- package/dist/flows/core/schema.d.ts +10 -5
- package/dist/flows/core/schema.js +29 -1
- package/dist/flows/core/steps.d.ts +7 -0
- package/dist/flows/core/steps.js +15 -0
- package/dist/flows/core/wiring.d.ts +72 -0
- package/dist/flows/core/wiring.js +151 -0
- package/dist/flows/run/branch-context.d.ts +10 -4
- package/dist/flows/run/branch-context.js +6 -4
- package/dist/flows/run/events.d.ts +30 -2
- package/dist/flows/run/handlers/group.js +6 -2
- package/dist/flows/run/handlers/support.d.ts +7 -2
- package/dist/flows/run/handlers/support.js +36 -7
- package/dist/flows/run/handlers/test.js +6 -2
- package/dist/flows/run/handlers/types.d.ts +11 -3
- package/dist/flows/run/node-folder.d.ts +8 -3
- package/dist/flows/run/node-folder.js +28 -4
- package/dist/flows/run/node-inputs.d.ts +5 -0
- package/dist/flows/run/node-inputs.js +62 -0
- package/dist/flows/run/node-pool.d.ts +2 -2
- package/dist/flows/run/node-pool.js +6 -6
- package/dist/flows/run/node-runner.d.ts +24 -0
- package/dist/flows/run/node-runner.js +48 -0
- package/dist/flows/run/per-browser.d.ts +21 -0
- package/dist/flows/run/per-browser.js +46 -0
- package/dist/flows/run/project-facts.d.ts +13 -0
- package/dist/flows/run/project-facts.js +12 -0
- package/dist/flows/run/project-options.d.ts +9 -2
- package/dist/flows/run/project.d.ts +13 -8
- package/dist/flows/run/project.js +48 -51
- package/dist/flows/run/report.d.ts +7 -7
- package/dist/flows/run/report.js +3 -5
- package/dist/flows/run/run.d.ts +21 -7
- package/dist/flows/run/run.js +21 -11
- package/dist/flows/run/walker.d.ts +23 -7
- package/dist/flows/run/walker.js +18 -7
- package/dist/locator/locator.d.ts +3 -1
- package/dist/locator/locator.js +6 -0
- package/dist/recorder/apply.js +17 -7
- package/dist/recorder/normalise.d.ts +18 -3
- package/dist/recorder/normalise.js +18 -3
- package/dist/recorder/session-secrets.js +4 -0
- package/dist/recorder/toolbar/action-guard.d.ts +2 -5
- package/dist/recorder/toolbar/action-guard.js +9 -5
- package/dist/recorder/toolbar/actions/insert.d.ts +5 -2
- package/dist/recorder/toolbar/actions/insert.js +43 -7
- package/dist/recorder/toolbar/actions/pick.d.ts +3 -0
- package/dist/recorder/toolbar/actions/pick.js +12 -0
- package/dist/recorder/toolbar/actions/reply.d.ts +2 -0
- package/dist/recorder/toolbar/actions/reply.js +9 -0
- package/dist/recorder/toolbar/actions/save-output.d.ts +15 -0
- package/dist/recorder/toolbar/actions/save-output.js +73 -0
- package/dist/recorder/toolbar/controller-fixture.d.ts +2 -2
- package/dist/recorder/toolbar/controller-fixture.js +2 -0
- package/dist/recorder/toolbar/controller.d.ts +2 -1
- package/dist/recorder/toolbar/controller.js +16 -7
- package/dist/recorder/toolbar/insertables.d.ts +7 -0
- package/dist/recorder/toolbar/insertables.js +12 -0
- package/dist/recorder/toolbar/inspect.js +3 -1
- package/dist/recorder/toolbar/protocol.d.ts +43 -3
- package/dist/recorder/toolbar/source/events-page.js +2 -1
- package/dist/recorder/toolbar/source/fields.js +11 -0
- package/dist/recorder/toolbar/source/handlers.js +19 -12
- package/dist/recorder/toolbar/source/picking.js +22 -7
- package/dist/recorder/toolbar/source/recorder-side.js +14 -0
- package/dist/recorder/toolbar/source/setup.js +3 -3
- package/dist/recorder/toolbar/source/views-panels.d.ts +1 -1
- package/dist/recorder/toolbar/source/views-panels.js +62 -10
- package/dist/recorder/toolbar/source/views-rail.js +5 -2
- package/dist/recorder/toolbar/source.d.ts +6 -5
- package/dist/recorder/toolbar/source.js +6 -5
- package/dist/recorder/toolbar/types.d.ts +12 -5
- package/dist/runner/options.d.ts +5 -0
- package/dist/runner/pooled-run.d.ts +3 -2
- package/dist/runner/pooled-run.js +4 -2
- package/dist/runner/registry.d.ts +7 -3
- package/dist/runner/registry.js +29 -11
- package/dist/runner/reporter.d.ts +4 -0
- package/dist/runner/reporter.js +2 -0
- package/dist/runner/run-test.js +23 -2
- package/dist/runner/run.d.ts +7 -0
- package/dist/runner/run.js +8 -1
- package/dist/runner/test-io.d.ts +30 -0
- package/dist/runner/test-io.js +111 -0
- package/dist/runner/test-runner.js +1 -1
- package/dist/runner/worker-protocol.d.ts +2 -0
- package/dist/runner/worker-session.js +1 -0
- package/dist/runner/worker.d.ts +1 -0
- package/dist/runner/worker.js +4 -1
- package/dist/steps/edit.d.ts +14 -3
- package/dist/steps/edit.js +65 -23
- package/dist/steps/emit.d.ts +2 -0
- package/dist/steps/emit.js +32 -8
- package/dist/steps/index.d.ts +1 -1
- package/dist/steps/model.d.ts +42 -2
- package/dist/steps/parse/expectation.d.ts +8 -1
- package/dist/steps/parse/expectation.js +17 -11
- package/dist/steps/parse/step.js +64 -8
- package/dist/steps/parse.js +25 -16
- package/dist/steps/test-options.d.ts +25 -0
- package/dist/steps/test-options.js +158 -0
- package/dist/steps/text.d.ts +7 -0
- package/dist/steps/text.js +12 -0
- package/dist/types/test.d.ts +34 -0
- package/dist/wait/output-sink.d.ts +24 -0
- package/dist/wait/output-sink.js +59 -0
- package/package.json +44 -4
|
@@ -19,14 +19,8 @@ export const HANDLERS_JS = String.raw ` // Every action is what a click on its
|
|
|
19
19
|
expand: () => post({ action: "collapse", collapsed: false }),
|
|
20
20
|
"resume-yes": () => { if (state.resumeQuestion) post({ action: "resume-goto", add: true }); },
|
|
21
21
|
"resume-no": () => { if (state.resumeQuestion) post({ action: "resume-goto", add: false }); },
|
|
22
|
-
assert: () =>
|
|
23
|
-
|
|
24
|
-
const wasOn = toolOn("assert");
|
|
25
|
-
stopPicking();
|
|
26
|
-
closePanel();
|
|
27
|
-
ui.picking = !wasOn;
|
|
28
|
-
render();
|
|
29
|
-
},
|
|
22
|
+
assert: () => startPicking("assert"),
|
|
23
|
+
output: () => startPicking("output"),
|
|
30
24
|
page: () => openTool("page"),
|
|
31
25
|
insert: () => openTool("insert"),
|
|
32
26
|
wait: () => openTool("wait"),
|
|
@@ -57,6 +51,17 @@ export const HANDLERS_JS = String.raw ` // Every action is what a click on its
|
|
|
57
51
|
post(message);
|
|
58
52
|
render();
|
|
59
53
|
},
|
|
54
|
+
"output-save": () => {
|
|
55
|
+
const o = ui.panel === "output" && ui.output;
|
|
56
|
+
if (!o || ui.busy || !o.info || !o.info.ok) return;
|
|
57
|
+
ui.busy = true;
|
|
58
|
+
o.nameError = null;
|
|
59
|
+
o.error = null;
|
|
60
|
+
const message = { action: "save-output", id: o.id, target: o.target, candidates: o.candidates, name: o.name.trim() };
|
|
61
|
+
if (o.pattern.trim()) message.pattern = o.pattern.trim();
|
|
62
|
+
post(message);
|
|
63
|
+
render();
|
|
64
|
+
},
|
|
60
65
|
"page-matcher": (detail) => {
|
|
61
66
|
const p = ui.panel === "page" && ui.page;
|
|
62
67
|
if (!p || !p.info || !p.info.ok || !PAGE_MATCHERS.some(([matcher]) => matcher === detail.matcher)) return;
|
|
@@ -85,6 +90,8 @@ export const HANDLERS_JS = String.raw ` // Every action is what a click on its
|
|
|
85
90
|
const secret = state.secrets.find((s) => s.name === detail.name);
|
|
86
91
|
if (!secret) return;
|
|
87
92
|
if (!secret.set) return openForm("secret", secret.name, true);
|
|
93
|
+
} else if (detail.kind === "input") {
|
|
94
|
+
if (!(state.inputs || []).some((i) => i.name === detail.name)) return;
|
|
88
95
|
} else if (!state.canAddVariables || !state.variables.some((v) => v.name === detail.name)) {
|
|
89
96
|
return;
|
|
90
97
|
}
|
|
@@ -93,20 +100,20 @@ export const HANDLERS_JS = String.raw ` // Every action is what a click on its
|
|
|
93
100
|
"insert-first": () => {
|
|
94
101
|
if (ui.panel !== "insert" || !ui.insert.field) return;
|
|
95
102
|
const found = matches();
|
|
96
|
-
const first = found.variables[0] ? { kind: "env", name: found.variables[0].name } : found.secrets[0]
|
|
103
|
+
const first = found.variables[0] ? { kind: "env", name: found.variables[0].name } : found.secrets[0] ? { kind: "secret", name: found.secrets[0].name } : found.inputs[0] && { kind: "input", name: found.inputs[0].name };
|
|
97
104
|
if (first) handlers["insert-item"](first);
|
|
98
105
|
},
|
|
99
106
|
"insert-new": (detail) => {
|
|
100
107
|
if (ui.panel !== "insert" || !ui.insert.field) return;
|
|
101
108
|
const filter = ui.insert.filter.trim();
|
|
102
109
|
const base = filter || fieldName(ui.insert.field);
|
|
103
|
-
const name = detail.kind === "secret" ? snake(base) : filter ? variableName(filter) : camel(base);
|
|
104
|
-
openForm(detail.kind === "secret" ? "secret" : "env", name, false);
|
|
110
|
+
const name = detail.kind === "secret" ? snake(base) : detail.kind === "input" ? camel(base) : filter ? variableName(filter) : camel(base);
|
|
111
|
+
openForm(detail.kind === "secret" ? "secret" : detail.kind === "input" ? "input" : "env", name, false);
|
|
105
112
|
},
|
|
106
113
|
"new-kind": (detail) => {
|
|
107
114
|
const f = ui.panel === "new" && ui.form;
|
|
108
115
|
if (!f || ui.busy || f.kind === detail.kind) return;
|
|
109
|
-
f.kind = detail.kind === "secret" ? "secret" : "env";
|
|
116
|
+
f.kind = detail.kind === "secret" ? "secret" : detail.kind === "input" ? "input" : "env";
|
|
110
117
|
f.name = f.kind === "secret" ? snake(f.name) : camel(f.name);
|
|
111
118
|
f.nameError = null;
|
|
112
119
|
f.error = null;
|
|
@@ -3,6 +3,7 @@ export const PICKING_JS = String.raw ` // ---- Panels and picking ----
|
|
|
3
3
|
const closePanel = () => {
|
|
4
4
|
ui.panel = null;
|
|
5
5
|
ui.assert = null;
|
|
6
|
+
ui.output = null;
|
|
6
7
|
ui.page = null;
|
|
7
8
|
ui.busy = false;
|
|
8
9
|
ui.closeOnUpdate = false;
|
|
@@ -11,7 +12,7 @@ export const PICKING_JS = String.raw ` // ---- Panels and picking ----
|
|
|
11
12
|
};
|
|
12
13
|
const stopPicking = () => {
|
|
13
14
|
ui.picking = false;
|
|
14
|
-
if (ui.panel !== "assert") hideOutline();
|
|
15
|
+
if (ui.panel !== "assert" && ui.panel !== "output") hideOutline();
|
|
15
16
|
};
|
|
16
17
|
const usable = () => !paused() && !state.collapsed;
|
|
17
18
|
const openTool = (tool) => {
|
|
@@ -43,6 +44,16 @@ export const PICKING_JS = String.raw ` // ---- Panels and picking ----
|
|
|
43
44
|
}
|
|
44
45
|
render();
|
|
45
46
|
};
|
|
47
|
+
// The tool's picker: Assert or Save as output
|
|
48
|
+
const startPicking = (tool) => {
|
|
49
|
+
if (!usable()) return;
|
|
50
|
+
const wasOn = toolOn(tool);
|
|
51
|
+
stopPicking();
|
|
52
|
+
closePanel();
|
|
53
|
+
ui.picking = !wasOn;
|
|
54
|
+
ui.pickFor = tool;
|
|
55
|
+
render();
|
|
56
|
+
};
|
|
46
57
|
const pick = (target) => {
|
|
47
58
|
if (!ui.picking || !target || target.nodeType !== 1) return;
|
|
48
59
|
ui.picking = false;
|
|
@@ -50,12 +61,16 @@ export const PICKING_JS = String.raw ` // ---- Panels and picking ----
|
|
|
50
61
|
const id = nextId++;
|
|
51
62
|
const candidates = candidatesOf(target);
|
|
52
63
|
const r = target.getBoundingClientRect();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
const rect = { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
|
|
65
|
+
if (ui.pickFor === "output") {
|
|
66
|
+
ui.output = { id, target, candidates, info: null, name: "", pattern: "", preview: null, nameError: null, error: null, rect };
|
|
67
|
+
} else {
|
|
68
|
+
ui.assert = {
|
|
69
|
+
id, target, candidates, info: null, matcher: "toBeVisible", expected: "", attribute: null, countIndex: 0,
|
|
70
|
+
cssName: "", cssValue: "", not: false, error: null, rect,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
ui.panel = ui.pickFor === "output" ? "output" : "assert";
|
|
59
74
|
ui.pending = id;
|
|
60
75
|
showOutline(target);
|
|
61
76
|
post({ action: "pick", id, target, candidates });
|
|
@@ -27,6 +27,13 @@ export const RECORDER_SIDE_JS = String.raw ` // ---- The recorder's side ----
|
|
|
27
27
|
render();
|
|
28
28
|
};
|
|
29
29
|
const reply = (id, result) => {
|
|
30
|
+
// A Save as output preview, for the pattern still typed (a late one is dropped); it doesn't end a pending action
|
|
31
|
+
if (result && result.preview) {
|
|
32
|
+
const o = ui.panel === "output" && ui.output;
|
|
33
|
+
if (!o || o.id !== id || o.pattern.trim() !== result.pattern) return;
|
|
34
|
+
o.preview = result;
|
|
35
|
+
return render();
|
|
36
|
+
}
|
|
30
37
|
if (!ui.pending || id !== ui.pending || !result) return;
|
|
31
38
|
ui.busy = false;
|
|
32
39
|
const ok = result.ok === true;
|
|
@@ -41,6 +48,10 @@ export const RECORDER_SIDE_JS = String.raw ` // ---- The recorder's side ----
|
|
|
41
48
|
a.cssValue = result.css.value;
|
|
42
49
|
a.expected = prefill(a, a.matcher);
|
|
43
50
|
}
|
|
51
|
+
} else if (ui.panel === "output" && ui.output && !ui.output.info) {
|
|
52
|
+
ui.output.info = result;
|
|
53
|
+
if (ok) ui.focus = "outputName";
|
|
54
|
+
if (ok && result.showsSecret) askPreview(ui.output);
|
|
44
55
|
} else if (ui.panel === "page" && ui.page && !ui.page.info) {
|
|
45
56
|
ui.page.info = result;
|
|
46
57
|
if (ok) {
|
|
@@ -51,6 +62,9 @@ export const RECORDER_SIDE_JS = String.raw ` // ---- The recorder's side ----
|
|
|
51
62
|
closePanel();
|
|
52
63
|
} else if (ui.panel === "assert" && ui.assert) {
|
|
53
64
|
ui.assert.error = result.error;
|
|
65
|
+
} else if (ui.panel === "output" && ui.output) {
|
|
66
|
+
if (result.field === "name") ui.output.nameError = result.error;
|
|
67
|
+
else ui.output.error = result.error;
|
|
54
68
|
} else if (ui.panel === "page" && ui.page) {
|
|
55
69
|
ui.page.error = result.error;
|
|
56
70
|
} else if (ui.panel === "insert") {
|
|
@@ -14,7 +14,7 @@ export const SETUP_JS = String.raw ` if (window !== window.top) return;
|
|
|
14
14
|
const CSS = ${JSON.stringify(TOOLBAR_CSS)};
|
|
15
15
|
const DEFAULTS = {
|
|
16
16
|
status: "recording", collapsed: false, position: null, steps: 0, environment: "", canAddVariables: false,
|
|
17
|
-
variables: [], secrets: [], secretsIgnored: true, resumeQuestion: false, nextScreenshot: "screenshot-1",
|
|
17
|
+
variables: [], secrets: [], inputs: [], secretsIgnored: true, resumeQuestion: false, nextScreenshot: "screenshot-1",
|
|
18
18
|
warnings: [],
|
|
19
19
|
};
|
|
20
20
|
const MATCHERS = [
|
|
@@ -34,8 +34,8 @@ export const SETUP_JS = String.raw ` if (window !== window.top) return;
|
|
|
34
34
|
let toastTimer = 0;
|
|
35
35
|
// Local UI state; everything about steps lives in the recorder
|
|
36
36
|
const ui = {
|
|
37
|
-
panel: null, picking: false, busy: false, closeOnUpdate: false, pending: 0, focus: null,
|
|
38
|
-
field: null, assert: null, page: null, insert: null, form: null,
|
|
37
|
+
panel: null, picking: false, pickFor: "assert", busy: false, closeOnUpdate: false, pending: 0, focus: null,
|
|
38
|
+
field: null, assert: null, output: null, page: null, insert: null, form: null,
|
|
39
39
|
wait: { kind: "idle", seconds: "2" }, shot: { name: "", fullPage: false },
|
|
40
40
|
drag: null, dropped: null, toastId: null,
|
|
41
41
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** Views: the Assert, Page, Insert, New, Wait and Screenshot panels. A slice of the toolbar function: it shares one closure with its neighbours, see `../source.ts`. */
|
|
1
|
+
/** Views: the Assert, Save as output, Page, Insert, New, Wait and Screenshot panels. A slice of the toolbar function: it shares one closure with its neighbours, see `../source.ts`. */
|
|
2
2
|
export declare const VIEWS_PANELS_JS: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Views: the Assert, Page, Insert, New, Wait and Screenshot panels. A slice of the toolbar function: it shares one closure with its neighbours, see `../source.ts`. */
|
|
1
|
+
/** Views: the Assert, Save as output, Page, Insert, New, Wait and Screenshot panels. A slice of the toolbar function: it shares one closure with its neighbours, see `../source.ts`. */
|
|
2
2
|
export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher) =>
|
|
3
3
|
(matcher === "toHaveValue" && info.value === undefined) ||
|
|
4
4
|
(matcher === "toHaveAttribute" && info.attributes.length === 0) ||
|
|
@@ -73,6 +73,50 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
73
73
|
];
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
// What the output would hold, as outputs.save reads it: the pattern's first group, else its whole match, else the text
|
|
77
|
+
const captureOf = (text, pattern) => {
|
|
78
|
+
if (!pattern.trim()) return { value: text };
|
|
79
|
+
let re;
|
|
80
|
+
try { re = new RegExp(pattern.trim()); } catch (err) { return { error: "Not a valid pattern" }; }
|
|
81
|
+
const match = re.exec(text);
|
|
82
|
+
if (!match) return { error: "No match" };
|
|
83
|
+
return { value: match[1] !== undefined ? match[1] : match[0] };
|
|
84
|
+
};
|
|
85
|
+
// An element that shows a secret reaches the page with it masked: the recorder works out its preview from the real text
|
|
86
|
+
const askPreview = (o) => {
|
|
87
|
+
o.preview = null;
|
|
88
|
+
post({ action: "output-preview", id: o.id, pattern: o.pattern.trim() });
|
|
89
|
+
};
|
|
90
|
+
const outputCapture = (o) => {
|
|
91
|
+
if (!o.info.showsSecret) return captureOf(o.info.outputText, o.pattern);
|
|
92
|
+
if (!o.preview) return { value: "…" };
|
|
93
|
+
return o.preview.ok ? { value: o.preview.value, secret: o.preview.secret } : { error: o.preview.error };
|
|
94
|
+
};
|
|
95
|
+
const outputPanel = () => {
|
|
96
|
+
const o = ui.output;
|
|
97
|
+
const info = o.info;
|
|
98
|
+
const head = h("div", { class: "h" }, "Save as output", info && info.ok && h("code", { title: info.label }, info.label));
|
|
99
|
+
if (!info) return [head, h("div", { class: "msg" }, "…")];
|
|
100
|
+
if (!info.ok) return [head, h("div", { class: "err" }, info.error)];
|
|
101
|
+
const capture = outputCapture(o);
|
|
102
|
+
return [
|
|
103
|
+
head,
|
|
104
|
+
info.showsSecret && h("div", { class: "warn out-secret" }, "This element shows a secret. The saved output may contain it; use a pattern that captures only the non-secret part."),
|
|
105
|
+
h("div", { class: "lbl" }, "Text"),
|
|
106
|
+
h("div", { class: "msg out-text" }, info.showsSecret ? info.outputText : info.text),
|
|
107
|
+
h("div", { class: "lbl" }, "Name"),
|
|
108
|
+
h("div", { class: "f" }, h("input", { type: "text", class: "in code", "data-field": "outputName", value: o.name, "data-enter": "output-save", autocomplete: "off", spellcheck: "false" })),
|
|
109
|
+
o.nameError && h("div", { class: "field-error" }, o.nameError),
|
|
110
|
+
h("div", { class: "lbl" }, "Pattern (optional)"),
|
|
111
|
+
h("div", { class: "f" }, h("input", { type: "text", class: "in code", "data-field": "outputPattern", value: o.pattern, placeholder: "Order #(\\d+)", "data-enter": "output-save", autocomplete: "off", spellcheck: "false" })),
|
|
112
|
+
h("div", { class: "lbl" }, "Saves"),
|
|
113
|
+
capture.error ? h("div", { class: "warn out-capture" }, capture.error) : h("code", { class: "out-capture" }, capture.value),
|
|
114
|
+
capture.secret && h("div", { class: "err out-leak" }, "The output will contain a secret: reports mask it, but whatever reads the output gets the real value."),
|
|
115
|
+
o.error && h("div", { class: "err" }, o.error),
|
|
116
|
+
h("div", { class: "foot" }, h("button", { class: "add", "data-action": "output-save", disabled: ui.busy || !o.name.trim() }, "Save")),
|
|
117
|
+
];
|
|
118
|
+
};
|
|
119
|
+
|
|
76
120
|
const pagePanel = () => {
|
|
77
121
|
const p = ui.page;
|
|
78
122
|
const head = h("div", { class: "h" }, "Page");
|
|
@@ -98,13 +142,14 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
98
142
|
filter,
|
|
99
143
|
variables: state.canAddVariables ? state.variables.filter(hit) : [],
|
|
100
144
|
secrets: state.secrets.filter(hit),
|
|
145
|
+
inputs: (state.inputs || []).filter(hit),
|
|
101
146
|
};
|
|
102
147
|
};
|
|
103
148
|
const insertPanel = () => {
|
|
104
149
|
const ins = ui.insert;
|
|
105
150
|
if (!ins.field) return [h("div", { class: "h" }, "Insert"), h("div", { class: "msg" }, "Click into a field first")];
|
|
106
151
|
const found = matches();
|
|
107
|
-
const none = found.filter && found.variables.length === 0 && found.secrets.length === 0;
|
|
152
|
+
const none = found.filter && found.variables.length === 0 && found.secrets.length === 0 && found.inputs.length === 0;
|
|
108
153
|
const item = (kind, name, value) =>
|
|
109
154
|
h("button", { class: "it", "data-action": "insert-item", "data-kind": kind, "data-name": name, disabled: ui.busy }, h("code", null, name), value);
|
|
110
155
|
return [
|
|
@@ -120,13 +165,19 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
120
165
|
found.secrets.length
|
|
121
166
|
? found.secrets.map((s) => item("secret", s.name, s.set ? h("span", { class: "v" }, "••••••") : h("span", { class: "warn" }, "not set")))
|
|
122
167
|
: h("div", { class: "empty" }, found.filter ? "No secret matches." : "No secrets yet."),
|
|
168
|
+
h("div", { class: "grp" }, "Inputs"),
|
|
169
|
+
found.inputs.length
|
|
170
|
+
? found.inputs.map((i) => item("input", i.name, h("span", { class: "v", title: i.value }, i.value)))
|
|
171
|
+
: h("div", { class: "empty" }, found.filter ? "No input matches." : "No inputs yet."),
|
|
123
172
|
ins.error && h("div", { class: "err" }, ins.error),
|
|
124
173
|
h("button", { class: "act", "data-action": "insert-new", "data-kind": "env" }, "+ Add variable" + (none ? ' "' + variableName(ins.filter) + '"' : "")),
|
|
125
174
|
h("button", { class: "act", "data-action": "insert-new", "data-kind": "secret" }, "+ Add secret" + (none ? ' "' + snake(ins.filter) + '"' : "")),
|
|
175
|
+
h("button", { class: "act", "data-action": "insert-new", "data-kind": "input" }, "+ Add input" + (none ? ' "' + camel(ins.filter) + '"' : "")),
|
|
126
176
|
];
|
|
127
177
|
};
|
|
128
178
|
|
|
129
|
-
const newHint = (secret, env, name) => {
|
|
179
|
+
const newHint = (kind, secret, env, name) => {
|
|
180
|
+
if (kind === "input") return "Declared in the test's inputs, with this value as its default.";
|
|
130
181
|
if (!secret) return ["Saved to ", h("code", null, "environments." + env + ".variables"), " in samurai.config.ts. Other environments don't get it — add it there before running against them."];
|
|
131
182
|
if (!state.secretsIgnored) return h("span", { class: "warn" }, "Add .env." + env + " to .gitignore before committing.");
|
|
132
183
|
return ["Saved as ", h("code", null, "SAMURAI_SECRET_" + (name.trim() || "<NAME>")), " in ", h("code", null, ".env." + env), " (git-ignored)."];
|
|
@@ -134,22 +185,23 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
134
185
|
const newPanel = () => {
|
|
135
186
|
const f = ui.form;
|
|
136
187
|
const secret = f.kind === "secret";
|
|
137
|
-
const blocked =
|
|
188
|
+
const blocked = f.kind === "env" && !state.canAddVariables;
|
|
138
189
|
const env = state.environment;
|
|
139
190
|
return [
|
|
140
|
-
h("div", { class: "h stack" }, f.setValue ? "Set value" : secret ? "New secret" : "New variable", h("small", null, 'For "' + fieldLabel(ui.insert.field) + '"')),
|
|
191
|
+
h("div", { class: "h stack" }, f.setValue ? "Set value" : secret ? "New secret" : f.kind === "input" ? "New input" : "New variable", h("small", null, 'For "' + fieldLabel(ui.insert.field) + '"')),
|
|
141
192
|
h("div", { class: "seg" },
|
|
142
|
-
h("button", { class:
|
|
143
|
-
h("button", { class: secret ? "sel" : "", "data-action": "new-kind", "data-kind": "secret" }, "Secret")
|
|
193
|
+
h("button", { class: f.kind === "env" ? "sel" : "", "data-action": "new-kind", "data-kind": "env" }, "Variable"),
|
|
194
|
+
h("button", { class: secret ? "sel" : "", "data-action": "new-kind", "data-kind": "secret" }, "Secret"),
|
|
195
|
+
h("button", { class: f.kind === "input" ? "sel" : "", "data-action": "new-kind", "data-kind": "input" }, "Input")),
|
|
144
196
|
blocked
|
|
145
197
|
? h("div", { class: "msg" }, "This recording has no environment")
|
|
146
198
|
: [
|
|
147
199
|
h("div", { class: "lbl" }, "Name"),
|
|
148
200
|
h("div", { class: "f" }, h("input", { type: "text", class: "in code", "data-field": "name", value: f.name, "data-enter": "new-save", autocomplete: "off", spellcheck: "false" })),
|
|
149
201
|
f.nameError && h("div", { class: "field-error" }, f.nameError),
|
|
150
|
-
h("div", { class: "lbl" }, "Value in " + env),
|
|
202
|
+
h("div", { class: "lbl" }, f.kind === "input" ? "Default value" : "Value in " + env),
|
|
151
203
|
h("div", { class: "f" }, h("input", { type: secret ? "password" : "text", class: "in", "data-field": "value", value: f.value, "data-enter": "new-save", autocomplete: secret ? "new-password" : "off" })),
|
|
152
|
-
h("div", { class: "hint" }, newHint(secret, env, f.name)),
|
|
204
|
+
h("div", { class: "hint" }, newHint(f.kind, secret, env, f.name)),
|
|
153
205
|
f.error && h("div", { class: "err" }, f.error),
|
|
154
206
|
],
|
|
155
207
|
h("div", { class: "foot plain" },
|
|
@@ -181,5 +233,5 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
181
233
|
radio("fullPage", "true", ui.shot.fullPage, "Whole page"),
|
|
182
234
|
h("div", { class: "foot" }, h("button", { class: "add", "data-action": "screenshot-add", disabled: ui.busy }, "Add")),
|
|
183
235
|
];
|
|
184
|
-
const PANELS = { assert: assertPanel, page: pagePanel, insert: insertPanel, new: newPanel, wait: waitPanel, screenshot: screenshotPanel };
|
|
236
|
+
const PANELS = { assert: assertPanel, output: outputPanel, page: pagePanel, insert: insertPanel, new: newPanel, wait: waitPanel, screenshot: screenshotPanel };
|
|
185
237
|
`;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Views: the chip and the rail. A slice of the toolbar function: it shares one closure with its neighbours, see `../source.ts`. */
|
|
2
2
|
export const VIEWS_RAIL_JS = String.raw ` // ---- Views ----
|
|
3
3
|
const toolOn = (tool) =>
|
|
4
|
-
tool === "assert"
|
|
4
|
+
tool === "assert" || tool === "output" ? (ui.picking && ui.pickFor === tool) || ui.panel === tool
|
|
5
|
+
: tool === "insert" ? ui.panel === "insert" || ui.panel === "new" : ui.panel === tool;
|
|
5
6
|
|
|
6
7
|
const chipView = () =>
|
|
7
8
|
h("div", { class: "chip" },
|
|
@@ -30,7 +31,9 @@ export const VIEWS_RAIL_JS = String.raw ` // ---- Views ----
|
|
|
30
31
|
? h("button", { class: "b on", "data-action": "resume" }, ico("▶"), "Resume")
|
|
31
32
|
: h("button", { class: "b", "data-action": "pause" }, ico("❚❚"), "Pause"),
|
|
32
33
|
tool("assert", "✓", "Assert"),
|
|
33
|
-
ui.picking && h("div", { class: "note" }, "Click an element · Esc cancels"),
|
|
34
|
+
ui.picking && ui.pickFor === "assert" && h("div", { class: "note" }, "Click an element · Esc cancels"),
|
|
35
|
+
tool("output", "⇥", "Output"),
|
|
36
|
+
ui.picking && ui.pickFor === "output" && h("div", { class: "note" }, "Click an element · Esc cancels"),
|
|
34
37
|
tool("page", "⌂", "Page"),
|
|
35
38
|
tool("insert", "{x}", "Insert"),
|
|
36
39
|
tool("wait", "⏱", "Wait"),
|
|
@@ -9,22 +9,23 @@
|
|
|
9
9
|
* `globalThis[Symbol.for("samurai.toolbar")]` in the sandbox holds the hook:
|
|
10
10
|
* - `update(state)`: the recorder's state, after every change and every page load.
|
|
11
11
|
* - `reply(id, result)`: the answer to `pick` (a `PickInfo`), to `page-info` (a `PageInfo`) or to `assert`,
|
|
12
|
-
* `assert-page`, `insert` and `
|
|
12
|
+
* `assert-page`, `insert`, `add-insertable` and `save-output` (an `ActionReply`), or to `output-preview` (an
|
|
13
|
+
* `OutputPreview`, which carries `preview: true` and doesn't end a pending action).
|
|
13
14
|
* - `root`: the shadow root, for tests.
|
|
14
15
|
* - `press(action, detail?)`: runs the handler a click on that control runs (for tests). Actions:
|
|
15
16
|
* `pause`, `resume`, `stop`, `collapse`, `expand`, `resume-yes`, `resume-no` (rail);
|
|
16
|
-
* `assert`, `page`, `insert`, `wait`, `screenshot` (open or close the tool);
|
|
17
|
+
* `assert`, `output`, `page`, `insert`, `wait`, `screenshot` (open or close the tool);
|
|
17
18
|
* `pick { target }` (the element a picking click hit);
|
|
18
|
-
* `matcher { matcher }`, `not`, `assert-add` (Assert panel);
|
|
19
|
+
* `matcher { matcher }`, `not`, `assert-add` (Assert panel); `output-save` (Save as output panel);
|
|
19
20
|
* `page-matcher { matcher }`, `page-not`, `page-add` (Page panel);
|
|
20
21
|
* `insert-item { kind, name }`, `insert-first`, `insert-new { kind }` (Insert list);
|
|
21
22
|
* `new-kind { kind }`, `new-save`, `new-cancel` (New variable or secret);
|
|
22
23
|
* `wait-add`, `screenshot-add`; `escape` (the Esc key); `dismiss { id }` (a warning's Dismiss button).
|
|
23
|
-
* Form inputs carry `data-field` (`expected`, `attribute`, `cssName`, `countIndex`, `pageExpected`, `filter`,
|
|
24
|
+
* Form inputs carry `data-field` (`expected`, `attribute`, `cssName`, `countIndex`, `outputName`, `outputPattern`, `pageExpected`, `filter`,
|
|
24
25
|
* `name`, `value`, `waitKind`, `seconds`, `shotName`, `fullPage`) and update on `input`/`change`.
|
|
25
26
|
* - `rebind(send)`: used by a second run in the same document (a new recording), which keeps the host and
|
|
26
27
|
* swaps in that recording's toolbar channel.
|
|
27
|
-
* - `state()`, `picking()`: the last state from `update` and whether the Assert picker is on (for tests).
|
|
28
|
+
* - `state()`, `picking()`: the last state from `update` and whether the Assert or Save as output picker is on (for tests).
|
|
28
29
|
* - `dispose()`: removes the host and every listener the toolbar put on the window and document (the
|
|
29
30
|
* recording ended while the document stays open).
|
|
30
31
|
*
|
|
@@ -24,22 +24,23 @@ import { ATTACH_JS } from "./source/attach.js";
|
|
|
24
24
|
* `globalThis[Symbol.for("samurai.toolbar")]` in the sandbox holds the hook:
|
|
25
25
|
* - `update(state)`: the recorder's state, after every change and every page load.
|
|
26
26
|
* - `reply(id, result)`: the answer to `pick` (a `PickInfo`), to `page-info` (a `PageInfo`) or to `assert`,
|
|
27
|
-
* `assert-page`, `insert` and `
|
|
27
|
+
* `assert-page`, `insert`, `add-insertable` and `save-output` (an `ActionReply`), or to `output-preview` (an
|
|
28
|
+
* `OutputPreview`, which carries `preview: true` and doesn't end a pending action).
|
|
28
29
|
* - `root`: the shadow root, for tests.
|
|
29
30
|
* - `press(action, detail?)`: runs the handler a click on that control runs (for tests). Actions:
|
|
30
31
|
* `pause`, `resume`, `stop`, `collapse`, `expand`, `resume-yes`, `resume-no` (rail);
|
|
31
|
-
* `assert`, `page`, `insert`, `wait`, `screenshot` (open or close the tool);
|
|
32
|
+
* `assert`, `output`, `page`, `insert`, `wait`, `screenshot` (open or close the tool);
|
|
32
33
|
* `pick { target }` (the element a picking click hit);
|
|
33
|
-
* `matcher { matcher }`, `not`, `assert-add` (Assert panel);
|
|
34
|
+
* `matcher { matcher }`, `not`, `assert-add` (Assert panel); `output-save` (Save as output panel);
|
|
34
35
|
* `page-matcher { matcher }`, `page-not`, `page-add` (Page panel);
|
|
35
36
|
* `insert-item { kind, name }`, `insert-first`, `insert-new { kind }` (Insert list);
|
|
36
37
|
* `new-kind { kind }`, `new-save`, `new-cancel` (New variable or secret);
|
|
37
38
|
* `wait-add`, `screenshot-add`; `escape` (the Esc key); `dismiss { id }` (a warning's Dismiss button).
|
|
38
|
-
* Form inputs carry `data-field` (`expected`, `attribute`, `cssName`, `countIndex`, `pageExpected`, `filter`,
|
|
39
|
+
* Form inputs carry `data-field` (`expected`, `attribute`, `cssName`, `countIndex`, `outputName`, `outputPattern`, `pageExpected`, `filter`,
|
|
39
40
|
* `name`, `value`, `waitKind`, `seconds`, `shotName`, `fullPage`) and update on `input`/`change`.
|
|
40
41
|
* - `rebind(send)`: used by a second run in the same document (a new recording), which keeps the host and
|
|
41
42
|
* swaps in that recording's toolbar channel.
|
|
42
|
-
* - `state()`, `picking()`: the last state from `update` and whether the Assert picker is on (for tests).
|
|
43
|
+
* - `state()`, `picking()`: the last state from `update` and whether the Assert or Save as output picker is on (for tests).
|
|
43
44
|
* - `dispose()`: removes the host and every listener the toolbar put on the window and document (the
|
|
44
45
|
* recording ended while the document stays open).
|
|
45
46
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ElementHandle } from "../../script/element-handle.js";
|
|
2
2
|
import type { LocatorCall, LocatorSpec } from "../../steps/model.js";
|
|
3
3
|
import type { Observation } from "../normalise.js";
|
|
4
|
-
import type { ActionReply, PageInfo, PickInfo, ToolbarMessage, ToolbarState } from "./protocol.js";
|
|
4
|
+
import type { ActionReply, OutputPreview, PageInfo, PickInfo, ToolbarMessage, ToolbarState } from "./protocol.js";
|
|
5
5
|
/** What `inspect` found for a picked element: the reply, and the locator behind each Count choice */
|
|
6
6
|
export interface Inspection {
|
|
7
7
|
info: PickInfo & {
|
|
@@ -14,7 +14,7 @@ export interface Inspection {
|
|
|
14
14
|
export interface ControllerDeps {
|
|
15
15
|
/** Sends the state to every loaded top-level document's toolbar */
|
|
16
16
|
push(state: ToolbarState): void;
|
|
17
|
-
reply(id: number, result: PickInfo | PageInfo | ActionReply): void;
|
|
17
|
+
reply(id: number, result: PickInfo | PageInfo | ActionReply | OutputPreview): void;
|
|
18
18
|
currentUrl(): Promise<string>;
|
|
19
19
|
/** The recorded page's URL and title */
|
|
20
20
|
pageInfo(): Promise<{
|
|
@@ -47,6 +47,8 @@ export interface ControllerOptions {
|
|
|
47
47
|
/** Name → value; undefined when not set */
|
|
48
48
|
secrets: Record<string, string | undefined>;
|
|
49
49
|
secretsIgnored: boolean;
|
|
50
|
+
/** The test's declared inputs: name → default */
|
|
51
|
+
inputs?: Record<string, string>;
|
|
50
52
|
/** Screenshot steps already in the test, for the default name */
|
|
51
53
|
screenshots: number;
|
|
52
54
|
}
|
|
@@ -56,20 +58,25 @@ export type Message = ToolbarMessage & {
|
|
|
56
58
|
export type Of<A extends Message["action"]> = Extract<Message, {
|
|
57
59
|
action: A;
|
|
58
60
|
}>;
|
|
59
|
-
/** The element picked for an Assert,
|
|
61
|
+
/** The element picked for an Assert or Save as output, the locator behind each of its Count choices, and its text */
|
|
60
62
|
export interface PickState {
|
|
61
63
|
id: number;
|
|
62
64
|
locator: LocatorSpec;
|
|
63
65
|
counts: LocatorCall[];
|
|
66
|
+
/** Its text as `outputs.save` reads it, when it was picked, secrets included: it never goes back to the page */
|
|
67
|
+
text: string;
|
|
64
68
|
}
|
|
65
|
-
/**
|
|
69
|
+
/** The messages that expect a reply, and that a double click sends twice */
|
|
70
|
+
export type Replying = Of<"assert" | "assert-page" | "insert" | "add-insertable" | "save-output">;
|
|
71
|
+
/** What the action handlers (assert, insert, add a variable, secret or input, save as output) use of the controller */
|
|
66
72
|
export interface ActionContext {
|
|
67
73
|
readonly deps: ControllerDeps;
|
|
68
74
|
readonly options: ControllerOptions;
|
|
69
75
|
readonly variables: Map<string, string>;
|
|
70
76
|
readonly secrets: Map<string, string | undefined>;
|
|
77
|
+
readonly inputs: Map<string, string>;
|
|
71
78
|
isPaused(): boolean;
|
|
72
|
-
/** The picked element an `assert` with the same id refers to */
|
|
79
|
+
/** The picked element an `assert` or `save-output` with the same id refers to */
|
|
73
80
|
pick(): PickState | undefined;
|
|
74
81
|
setPick(pick: PickState | undefined): void;
|
|
75
82
|
showToast(text: string): void;
|
package/dist/runner/options.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ export interface RunnerOptions {
|
|
|
24
24
|
* @default the config's `retries`, else 0
|
|
25
25
|
*/
|
|
26
26
|
retries?: number | string;
|
|
27
|
+
/**
|
|
28
|
+
* Values for the inputs the tests declare, by name: every test of the run reads the names it declares from
|
|
29
|
+
* here, and its own default for a name missing here
|
|
30
|
+
*/
|
|
31
|
+
inputs?: Record<string, string>;
|
|
27
32
|
/** Run the browser without a window. @default false */
|
|
28
33
|
headless?: boolean;
|
|
29
34
|
/** Remote debugging port of the browser. @default DEFAULT_PORT (9223) */
|
|
@@ -16,9 +16,10 @@ export interface PlannedTest {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Lists the run: every selected test in the first browser, then every selected test in the next one. The
|
|
19
|
-
* position in the list is the item's `index`. `serialKey` says which group makes a test run alone.
|
|
19
|
+
* position in the list is the item's `index`. `serialKey` says which group makes a test run alone. `inputs`, the
|
|
20
|
+
* run's input values, go with every item.
|
|
20
21
|
*/
|
|
21
|
-
export declare function planItems(selected: RegisteredTestCase[], browsers: SupportedBrowser[], serialKey: (test: RegisteredTestCase) => string | undefined): {
|
|
22
|
+
export declare function planItems(selected: RegisteredTestCase[], browsers: SupportedBrowser[], serialKey: (test: RegisteredTestCase) => string | undefined, inputs?: Record<string, string>): {
|
|
22
23
|
items: WorkItem[];
|
|
23
24
|
planned: PlannedTest[];
|
|
24
25
|
};
|
|
@@ -2,9 +2,10 @@ import { idOf } from "./select.js";
|
|
|
2
2
|
import { staticSkip } from "./skip.js";
|
|
3
3
|
/**
|
|
4
4
|
* Lists the run: every selected test in the first browser, then every selected test in the next one. The
|
|
5
|
-
* position in the list is the item's `index`. `serialKey` says which group makes a test run alone.
|
|
5
|
+
* position in the list is the item's `index`. `serialKey` says which group makes a test run alone. `inputs`, the
|
|
6
|
+
* run's input values, go with every item.
|
|
6
7
|
*/
|
|
7
|
-
export function planItems(selected, browsers, serialKey) {
|
|
8
|
+
export function planItems(selected, browsers, serialKey, inputs) {
|
|
8
9
|
const planned = browsers.flatMap((browser) => selected.map((test) => ({ test, browser })));
|
|
9
10
|
// A test that does not run (skip, fixme) is in the plan but not in the queue; `index` is its place in the plan
|
|
10
11
|
const items = planned.flatMap(({ test, browser }, index) => {
|
|
@@ -18,6 +19,7 @@ export function planItems(selected, browsers, serialKey) {
|
|
|
18
19
|
testName: test.name,
|
|
19
20
|
browser,
|
|
20
21
|
...(key !== undefined && { serialKey: key }),
|
|
22
|
+
...(inputs !== undefined && { inputs }),
|
|
21
23
|
},
|
|
22
24
|
];
|
|
23
25
|
});
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import type { AllHook, EachHook, RegisteredTestCase, TestCaseBase, TestDetails } from "../types/test.js";
|
|
1
|
+
import type { AllHook, EachHook, RegisteredTestCase, TestCaseBase, TestDetails, TestOptions } 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 {
|
|
5
5
|
(title: string, fn: Body): void;
|
|
6
|
+
(title: string, options: TestOptions, fn: Body): void;
|
|
6
7
|
/** Registers a test that is not run */
|
|
7
8
|
skip: SkipFunction;
|
|
8
9
|
/** Registers a test that is not run because it is known to be broken */
|
|
9
10
|
fixme: SkipFunction;
|
|
10
11
|
/** Runs only this test (and the other `only` ones) */
|
|
11
|
-
only
|
|
12
|
+
only: {
|
|
13
|
+
(title: string, fn: Body): void;
|
|
14
|
+
(title: string, options: TestOptions, fn: Body): void;
|
|
15
|
+
};
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
14
18
|
* With a title and a body it registers a skipped test, `{ reason }` saying why; with a condition it skips the
|
|
@@ -16,7 +20,7 @@ export interface TestFunction {
|
|
|
16
20
|
*/
|
|
17
21
|
interface SkipFunction {
|
|
18
22
|
(title: string, fn: Body): void;
|
|
19
|
-
(title: string,
|
|
23
|
+
(title: string, options: TestOptions, fn: Body): void;
|
|
20
24
|
(condition: boolean, reason?: string): void;
|
|
21
25
|
}
|
|
22
26
|
/** `describe.skip` and `describe.fixme`: every test inside gets the modifier, and `{ reason }` says why */
|
package/dist/runner/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import logger from "../logger/index.js";
|
|
3
3
|
import { SkipSignal } from "./skip.js";
|
|
4
|
+
import { validateTestIO } from "./test-io.js";
|
|
4
5
|
import { addHook, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
|
|
5
6
|
const registered = [];
|
|
6
7
|
/** Runs `fn` as the `describe` block `title`, under `modifier` when there is one. `fn` runs immediately and must be synchronous. */
|
|
@@ -17,7 +18,8 @@ function openDescribe(file, title, fn, modifier, reason) {
|
|
|
17
18
|
leaveSuite();
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
function add(file, title, fn, modifier,
|
|
21
|
+
function add(file, title, fn, modifier, extras = {}) {
|
|
22
|
+
const { reason, inputs, outputs } = extras;
|
|
21
23
|
const suite = currentSuite(file);
|
|
22
24
|
const titlePath = [...titlesOf(suite), title];
|
|
23
25
|
const name = titlePath.join(" > ");
|
|
@@ -28,6 +30,8 @@ function add(file, title, fn, modifier, reason) {
|
|
|
28
30
|
file,
|
|
29
31
|
suite,
|
|
30
32
|
...modifierOf({ modifier, ...(reason !== undefined && { reason }) }, suite),
|
|
33
|
+
...(inputs && { inputs }),
|
|
34
|
+
...(outputs && { outputs }),
|
|
31
35
|
});
|
|
32
36
|
logger.debug("Registered test %s", name);
|
|
33
37
|
}
|
|
@@ -39,23 +43,37 @@ function titlesOf(suite) {
|
|
|
39
43
|
titles.unshift(s.title);
|
|
40
44
|
return titles;
|
|
41
45
|
}
|
|
42
|
-
/** The body and
|
|
43
|
-
function
|
|
44
|
-
const [
|
|
46
|
+
/** The body and options of `(title, fn)` and `(title, options, fn)`; `describe` only reads `reason` from them */
|
|
47
|
+
function bodyAndOptions(call, title, second, third) {
|
|
48
|
+
const [options, fn] = typeof second === "function"
|
|
45
49
|
? [undefined, second]
|
|
46
50
|
: [second, third];
|
|
47
51
|
if (typeof fn !== "function")
|
|
48
52
|
throw new TypeError(`${call}(${JSON.stringify(title)}) needs a function as its last argument`);
|
|
49
|
-
const reason =
|
|
50
|
-
return {
|
|
53
|
+
const reason = options?.reason;
|
|
54
|
+
return {
|
|
55
|
+
fn: fn,
|
|
56
|
+
...(reason !== undefined && { reason }),
|
|
57
|
+
...(options && call.startsWith("test") && validateTestIO(call, options)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** `test()` and `test.only()`: a reason only makes sense with skip and fixme */
|
|
61
|
+
function registering(call, modifier) {
|
|
62
|
+
return ((title, second, third) => {
|
|
63
|
+
const { fn, reason, ...io } = bodyAndOptions(call, title, second, third);
|
|
64
|
+
if (reason !== undefined)
|
|
65
|
+
throw new Error(`${call}(): reason only applies to skip and fixme`);
|
|
66
|
+
// Frames: Error, callerFile, this function, the spec
|
|
67
|
+
add(callerFile(), title, fn, modifier, io);
|
|
68
|
+
});
|
|
51
69
|
}
|
|
52
70
|
/** `test.skip` and `test.fixme`: a title and a body register a test with the modifier; a condition skips the running test */
|
|
53
71
|
function skipping(annotation) {
|
|
54
72
|
return ((first, second, third) => {
|
|
55
73
|
if (typeof first === "string") {
|
|
56
|
-
const { fn,
|
|
74
|
+
const { fn, ...extras } = bodyAndOptions(`test.${annotation}`, first, second, third);
|
|
57
75
|
// Frames: Error, callerFile, this function, the spec
|
|
58
|
-
add(callerFile(), first, fn, annotation,
|
|
76
|
+
add(callerFile(), first, fn, annotation, extras);
|
|
59
77
|
return;
|
|
60
78
|
}
|
|
61
79
|
if (first)
|
|
@@ -65,14 +83,14 @@ function skipping(annotation) {
|
|
|
65
83
|
/** `describe.skip` and `describe.fixme`, with or without `{ reason }` */
|
|
66
84
|
function describeSkipping(annotation) {
|
|
67
85
|
return ((title, second, third) => {
|
|
68
|
-
const { fn, reason } =
|
|
86
|
+
const { fn, reason } = bodyAndOptions(`describe.${annotation}`, title, second, third);
|
|
69
87
|
openDescribe(callerFile(), title, fn, annotation, reason);
|
|
70
88
|
});
|
|
71
89
|
}
|
|
72
|
-
export const test = Object.assign((
|
|
90
|
+
export const test = Object.assign(registering("test"), {
|
|
73
91
|
skip: skipping("skip"),
|
|
74
92
|
fixme: skipping("fixme"),
|
|
75
|
-
only: (
|
|
93
|
+
only: registering("test.only", "only"),
|
|
76
94
|
});
|
|
77
95
|
export const describe = Object.assign((title, fn) => openDescribe(callerFile(), title, fn), {
|
|
78
96
|
skip: describeSkipping("skip"),
|