@itmetsam/samurai-framework 0.20.0 → 0.21.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.
- 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/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/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.js +2 -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 +9 -0
- package/dist/recorder/toolbar/actions/save-output.js +46 -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 +14 -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 +20 -3
- package/dist/recorder/toolbar/source/events-page.js +2 -1
- package/dist/recorder/toolbar/source/fields.js +8 -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 +6 -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 +50 -10
- package/dist/recorder/toolbar/source/views-rail.js +5 -2
- package/dist/recorder/toolbar/source.d.ts +5 -5
- package/dist/recorder/toolbar/source.js +5 -5
- package/dist/recorder/toolbar/types.d.ts +10 -3
- 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 +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ActionReply } from "../protocol.js";
|
|
2
|
+
import type { ActionContext, Of } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Turns the Save as output panel's choice into a `saveOutput` step for the element that was picked. Refuses an
|
|
5
|
+
* element whose text holds a secret, a name `test()` would reject, an element with no text when there is no
|
|
6
|
+
* pattern, and a pattern that isn't valid or doesn't match the element's text as picked, read the way
|
|
7
|
+
* `outputs.save` reads it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveOutputAction(ctx: ActionContext, message: Of<"save-output">): Promise<ActionReply>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { matchesText } from "../../../assert/text-match.js";
|
|
2
|
+
import { holdsSecret } from "./pick.js";
|
|
3
|
+
import { fail, nameProblem, PAUSED } from "./reply.js";
|
|
4
|
+
/**
|
|
5
|
+
* Turns the Save as output panel's choice into a `saveOutput` step for the element that was picked. Refuses an
|
|
6
|
+
* element whose text holds a secret, a name `test()` would reject, an element with no text when there is no
|
|
7
|
+
* pattern, and a pattern that isn't valid or doesn't match the element's text as picked, read the way
|
|
8
|
+
* `outputs.save` reads it.
|
|
9
|
+
*/
|
|
10
|
+
export async function saveOutputAction(ctx, message) {
|
|
11
|
+
if (ctx.isPaused())
|
|
12
|
+
return fail(PAUSED);
|
|
13
|
+
const pick = ctx.pick();
|
|
14
|
+
if (!pick || pick.id !== message.id)
|
|
15
|
+
return fail("Pick the element again");
|
|
16
|
+
// The step would write the secret into the run's results
|
|
17
|
+
if (holdsSecret(ctx.secrets)(pick.text))
|
|
18
|
+
return fail("this element shows a secret; it can't be an output");
|
|
19
|
+
const name = String(message.name ?? "").trim();
|
|
20
|
+
const problem = nameProblem(name);
|
|
21
|
+
if (problem)
|
|
22
|
+
return fail(problem, "name");
|
|
23
|
+
const source = String(message.pattern ?? "").trim();
|
|
24
|
+
let pattern;
|
|
25
|
+
if (source !== "") {
|
|
26
|
+
try {
|
|
27
|
+
pattern = new RegExp(source);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return fail("pattern is not a valid regular expression");
|
|
31
|
+
}
|
|
32
|
+
if (!matchesText(pick.text, pattern, "exact"))
|
|
33
|
+
return fail("pattern doesn't match the element's text");
|
|
34
|
+
}
|
|
35
|
+
else if (pick.text === "")
|
|
36
|
+
return fail("the element has no text");
|
|
37
|
+
ctx.setPick(undefined);
|
|
38
|
+
ctx.deps.observe({
|
|
39
|
+
type: "saveOutput",
|
|
40
|
+
locator: pick.locator,
|
|
41
|
+
name,
|
|
42
|
+
// The built pattern's source: a `/` comes out escaped, so the spec it is written into compiles
|
|
43
|
+
...(pattern && { pattern: { source: pattern.source, flags: "" } }),
|
|
44
|
+
});
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
@@ -18,8 +18,8 @@ export type Message = ToolbarMessage & {
|
|
|
18
18
|
target?: ElementHandle;
|
|
19
19
|
};
|
|
20
20
|
export declare const msg: (message: Record<string, unknown>) => Message;
|
|
21
|
-
export declare const insert: (id: number, kind: "env" | "secret", name: string) => Message;
|
|
22
|
-
export declare const add: (id: number, kind: "env" | "secret", name: string, value: string) => Message;
|
|
21
|
+
export declare const insert: (id: number, kind: "env" | "secret" | "input", name: string) => Message;
|
|
22
|
+
export declare const add: (id: number, kind: "env" | "secret" | "input", name: string, value: string) => Message;
|
|
23
23
|
export declare const pick: (id: number) => Message;
|
|
24
24
|
/** A controller over fakes that record everything */
|
|
25
25
|
export declare function setup(options?: Partial<ControllerOptions>, overrides?: Partial<ControllerDeps>): {
|
|
@@ -10,6 +10,7 @@ const OPTIONS = {
|
|
|
10
10
|
secrets: { ADMIN_PASSWORD: SECRET, API_TOKEN: undefined },
|
|
11
11
|
secretsIgnored: true,
|
|
12
12
|
screenshots: 0,
|
|
13
|
+
inputs: { email: "a@b.c" },
|
|
13
14
|
};
|
|
14
15
|
export const TARGET = new ElementHandle("el-1");
|
|
15
16
|
export const CANDIDATES = [
|
|
@@ -24,6 +25,7 @@ export const INFO = {
|
|
|
24
25
|
ok: true,
|
|
25
26
|
label: `textbox "Email"`,
|
|
26
27
|
text: "",
|
|
28
|
+
outputText: "",
|
|
27
29
|
value: "a@b.c",
|
|
28
30
|
attributes: [{ name: "id", value: "email" }],
|
|
29
31
|
first: { matcher: "toHaveValue", expected: "a@b.c" },
|
|
@@ -15,6 +15,7 @@ export declare class ToolbarController {
|
|
|
15
15
|
private screenshots;
|
|
16
16
|
private readonly variables;
|
|
17
17
|
private readonly secrets;
|
|
18
|
+
private readonly inputs;
|
|
18
19
|
private pausedAt;
|
|
19
20
|
/** The URL the resume question offers; set while it is asked */
|
|
20
21
|
private question;
|
|
@@ -70,7 +71,7 @@ export declare class ToolbarController {
|
|
|
70
71
|
private answer;
|
|
71
72
|
/**
|
|
72
73
|
* Whether to process `message`; call it as the message arrives, before queueing it. An `assert`, `assert-page`,
|
|
73
|
-
* `insert` or `
|
|
74
|
+
* `insert`, `add-insertable` or `save-output` whose id is in flight or was already answered `{ ok: true }` is a double click and is
|
|
74
75
|
* dropped (its first copy answers). A retry after `{ ok: false }` is accepted.
|
|
75
76
|
*/
|
|
76
77
|
accept(message: Message): boolean;
|
|
@@ -4,7 +4,9 @@ import { assertAction } from "./actions/assert.js";
|
|
|
4
4
|
import { assertPageAction, pageInfoOf } from "./actions/assert-page.js";
|
|
5
5
|
import { addInsertableAction, insertAction } from "./actions/insert.js";
|
|
6
6
|
import { withoutSecrets } from "./actions/pick.js";
|
|
7
|
+
import { saveOutputAction } from "./actions/save-output.js";
|
|
7
8
|
import { fail, PAUSED, UNIDENTIFIABLE } from "./actions/reply.js";
|
|
9
|
+
import { insertablesOf } from "./insertables.js";
|
|
8
10
|
import { LEFT, PageScope } from "./page-scope.js";
|
|
9
11
|
/** A wait for time is 0.1–60 s */
|
|
10
12
|
const WAIT_MIN_MS = 100;
|
|
@@ -39,11 +41,13 @@ export class ToolbarController {
|
|
|
39
41
|
String(value),
|
|
40
42
|
]));
|
|
41
43
|
this.secrets = new Map(Object.entries(options.secrets));
|
|
44
|
+
this.inputs = new Map(Object.entries(options.inputs ?? {}));
|
|
42
45
|
this.actions = {
|
|
43
46
|
deps,
|
|
44
47
|
options,
|
|
45
48
|
variables: this.variables,
|
|
46
49
|
secrets: this.secrets,
|
|
50
|
+
inputs: this.inputs,
|
|
47
51
|
isPaused: () => this.paused,
|
|
48
52
|
pick: () => this.pick,
|
|
49
53
|
setPick: (pick) => {
|
|
@@ -64,11 +68,7 @@ export class ToolbarController {
|
|
|
64
68
|
steps: this.steps,
|
|
65
69
|
environment: this.options.environment,
|
|
66
70
|
canAddVariables: this.options.canAddVariables,
|
|
67
|
-
|
|
68
|
-
secrets: [...this.secrets].map(([name, value]) => ({
|
|
69
|
-
name,
|
|
70
|
-
set: value !== undefined,
|
|
71
|
-
})),
|
|
71
|
+
...insertablesOf(this.actions),
|
|
72
72
|
secretsIgnored: this.options.secretsIgnored,
|
|
73
73
|
resumeQuestion: this.question !== undefined,
|
|
74
74
|
nextScreenshot: `screenshot-${this.screenshots + 1}`,
|
|
@@ -146,6 +146,8 @@ export class ToolbarController {
|
|
|
146
146
|
return this.replying(message, () => insertAction(this.actions, message));
|
|
147
147
|
case "add-insertable":
|
|
148
148
|
return this.replying(message, () => addInsertableAction(this.actions, message));
|
|
149
|
+
case "save-output":
|
|
150
|
+
return this.replying(message, () => saveOutputAction(this.actions, message));
|
|
149
151
|
case "wait":
|
|
150
152
|
if (this.paused)
|
|
151
153
|
return;
|
|
@@ -278,7 +280,7 @@ export class ToolbarController {
|
|
|
278
280
|
}
|
|
279
281
|
/**
|
|
280
282
|
* Whether to process `message`; call it as the message arrives, before queueing it. An `assert`, `assert-page`,
|
|
281
|
-
* `insert` or `
|
|
283
|
+
* `insert`, `add-insertable` or `save-output` whose id is in flight or was already answered `{ ok: true }` is a double click and is
|
|
282
284
|
* dropped (its first copy answers). A retry after `{ ok: false }` is accepted.
|
|
283
285
|
*/
|
|
284
286
|
accept(message) {
|
|
@@ -319,7 +321,12 @@ export class ToolbarController {
|
|
|
319
321
|
return undefined;
|
|
320
322
|
const inspected = await this.deps.inspect(message.target, locator, message.candidates);
|
|
321
323
|
if (page === this.scope.current)
|
|
322
|
-
this.pick = {
|
|
324
|
+
this.pick = {
|
|
325
|
+
id: message.id,
|
|
326
|
+
locator,
|
|
327
|
+
counts: inspected.counts,
|
|
328
|
+
text: inspected.info.outputText,
|
|
329
|
+
};
|
|
323
330
|
return inspected;
|
|
324
331
|
});
|
|
325
332
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ToolbarState } from "./protocol.js";
|
|
2
|
+
/** What the Insert list shows of the variables, secrets (names only) and inputs the controller holds */
|
|
3
|
+
export declare function insertablesOf(held: {
|
|
4
|
+
variables: ReadonlyMap<string, string>;
|
|
5
|
+
secrets: ReadonlyMap<string, string | undefined>;
|
|
6
|
+
inputs: ReadonlyMap<string, string>;
|
|
7
|
+
}): Pick<ToolbarState, "variables" | "secrets" | "inputs">;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** What the Insert list shows of the variables, secrets (names only) and inputs the controller holds */
|
|
2
|
+
export function insertablesOf(held) {
|
|
3
|
+
const named = ([name, value]) => ({ name, value });
|
|
4
|
+
return {
|
|
5
|
+
variables: [...held.variables].map(named),
|
|
6
|
+
secrets: [...held.secrets].map(([name, value]) => ({
|
|
7
|
+
name,
|
|
8
|
+
set: value !== undefined,
|
|
9
|
+
})),
|
|
10
|
+
inputs: [...held.inputs].map(named),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -8,7 +8,7 @@ const MAX_COUNTS = 3;
|
|
|
8
8
|
/** The property Has CSS starts with */
|
|
9
9
|
const CSS_PROPERTY = "color";
|
|
10
10
|
/**
|
|
11
|
-
* Run in the toolbar's sandbox on the picked element: its text (cut at 4 KB), value (as the Alt+click rule reads it), first
|
|
11
|
+
* Run in the toolbar's sandbox on the picked element: its text (cut at 4 KB), its whole text as `outputs.save` reads it, value (as the Alt+click rule reads it), first
|
|
12
12
|
* 20 attributes, its role without a name and its `tag.class` selector. `roleOf` only knows the roles the
|
|
13
13
|
* candidates use, so the common list and table roles are added for Count. Also whether it can be checked or
|
|
14
14
|
* disabled, its class attribute and the computed value of `CSS_PROPERTY`.
|
|
@@ -27,6 +27,7 @@ const INSPECT_SOURCE = String.raw `(el, cssName) => {
|
|
|
27
27
|
const className = el.getAttribute("class");
|
|
28
28
|
return {
|
|
29
29
|
text: norm(String(el.textContent == null ? "" : el.textContent).slice(0, TEXT_CAP * 2)).slice(0, TEXT_CAP),
|
|
30
|
+
outputText: String(el.textContent == null ? "" : el.textContent).trim(),
|
|
30
31
|
value: hasValue && !secret ? el.value : null,
|
|
31
32
|
attributes: Array.from(el.attributes).slice(0, 20).map((a) => ({ name: a.name, value: a.value })),
|
|
32
33
|
role: roleOf(el) || MORE_ROLES[tag] || null,
|
|
@@ -94,6 +95,7 @@ export async function inspectElement(page, connector, context, target, locator,
|
|
|
94
95
|
ok: true,
|
|
95
96
|
label: describeCall(locator.chain[0]),
|
|
96
97
|
text: found.text,
|
|
98
|
+
outputText: found.outputText,
|
|
97
99
|
...(value !== undefined && { value }),
|
|
98
100
|
attributes: found.attributes,
|
|
99
101
|
first: {
|
|
@@ -25,6 +25,11 @@ export interface ToolbarState {
|
|
|
25
25
|
name: string;
|
|
26
26
|
set: boolean;
|
|
27
27
|
}[];
|
|
28
|
+
/** The recording test's declared inputs and their defaults */
|
|
29
|
+
inputs: {
|
|
30
|
+
name: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}[];
|
|
28
33
|
/** Whether `.env.<environment>` is git-ignored */
|
|
29
34
|
secretsIgnored: boolean;
|
|
30
35
|
/** Ask "Add a step that opens this page?" */
|
|
@@ -93,17 +98,26 @@ export type ToolbarMessage = {
|
|
|
93
98
|
id: number;
|
|
94
99
|
target: unknown;
|
|
95
100
|
candidates: LocatorCall[];
|
|
96
|
-
kind: "env" | "secret";
|
|
101
|
+
kind: "env" | "secret" | "input";
|
|
97
102
|
name: string;
|
|
98
103
|
} | {
|
|
99
104
|
action: "add-insertable";
|
|
100
105
|
id: number;
|
|
101
106
|
target: unknown;
|
|
102
107
|
candidates: LocatorCall[];
|
|
103
|
-
kind: "env" | "secret";
|
|
108
|
+
kind: "env" | "secret" | "input";
|
|
104
109
|
name: string;
|
|
105
110
|
value: string;
|
|
106
111
|
}
|
|
112
|
+
/** Save as output for the element picked with the same id; `pattern` is a RegExp's source, no flags */
|
|
113
|
+
| {
|
|
114
|
+
action: "save-output";
|
|
115
|
+
id: number;
|
|
116
|
+
target: unknown;
|
|
117
|
+
candidates: LocatorCall[];
|
|
118
|
+
name: string;
|
|
119
|
+
pattern?: string;
|
|
120
|
+
}
|
|
107
121
|
/** The Page panel opened: answered with a `PageInfo` */
|
|
108
122
|
| {
|
|
109
123
|
action: "page-info";
|
|
@@ -130,7 +144,10 @@ export type ToolbarMessage = {
|
|
|
130
144
|
export type PickInfo = {
|
|
131
145
|
ok: true;
|
|
132
146
|
label: string;
|
|
147
|
+
/** Whitespace collapsed and trimmed, cut at 4 KB: what the Assert panel shows and prefills */
|
|
133
148
|
text: string;
|
|
149
|
+
/** The whole text as `outputs.save` reads it (`textContent`, trimmed): what Save as output captures from */
|
|
150
|
+
outputText: string;
|
|
134
151
|
value?: string;
|
|
135
152
|
attributes: {
|
|
136
153
|
name: string;
|
|
@@ -169,7 +186,7 @@ export type PageInfo = {
|
|
|
169
186
|
ok: false;
|
|
170
187
|
error: string;
|
|
171
188
|
};
|
|
172
|
-
/** The answer to `insert`, `add-insertable`, `assert` and `
|
|
189
|
+
/** The answer to `insert`, `add-insertable`, `assert`, `assert-page` and `save-output` */
|
|
173
190
|
export type ActionReply = {
|
|
174
191
|
ok: true;
|
|
175
192
|
} | {
|
|
@@ -62,6 +62,7 @@ export const EVENTS_PAGE_JS = String.raw ` // ---- Events on the page (window c
|
|
|
62
62
|
}, true);
|
|
63
63
|
listen(window, "resize", () => render());
|
|
64
64
|
listen(window, "scroll", () => {
|
|
65
|
-
|
|
65
|
+
const picked = ui.panel === "assert" ? ui.assert : ui.panel === "output" ? ui.output : null;
|
|
66
|
+
if (picked && !outline.hidden) showOutline(picked.target);
|
|
66
67
|
}, true);
|
|
67
68
|
`;
|
|
@@ -11,6 +11,14 @@ export const FIELDS_JS = String.raw ` const press = (action, detail) => {
|
|
|
11
11
|
const value = t.value;
|
|
12
12
|
if (field === "expected" && ui.assert) ui.assert.expected = value;
|
|
13
13
|
else if (field === "pageExpected" && ui.page) ui.page.expected = value;
|
|
14
|
+
else if ((field === "outputName" || field === "outputPattern") && ui.output) {
|
|
15
|
+
const key = field === "outputName" ? "name" : "pattern";
|
|
16
|
+
if (ui.output[key] === value) return;
|
|
17
|
+
ui.output[key] = value;
|
|
18
|
+
if (key === "name") ui.output.nameError = null;
|
|
19
|
+
else ui.output.error = null;
|
|
20
|
+
render();
|
|
21
|
+
}
|
|
14
22
|
else if (field === "attribute" && ui.assert) {
|
|
15
23
|
ui.assert.attribute = value;
|
|
16
24
|
ui.assert.expected = prefill(ui.assert, "toHaveAttribute");
|
|
@@ -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: "", 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 });
|
|
@@ -41,6 +41,9 @@ export const RECORDER_SIDE_JS = String.raw ` // ---- The recorder's side ----
|
|
|
41
41
|
a.cssValue = result.css.value;
|
|
42
42
|
a.expected = prefill(a, a.matcher);
|
|
43
43
|
}
|
|
44
|
+
} else if (ui.panel === "output" && ui.output && !ui.output.info) {
|
|
45
|
+
ui.output.info = result;
|
|
46
|
+
if (ok) ui.focus = "outputName";
|
|
44
47
|
} else if (ui.panel === "page" && ui.page && !ui.page.info) {
|
|
45
48
|
ui.page.info = result;
|
|
46
49
|
if (ok) {
|
|
@@ -51,6 +54,9 @@ export const RECORDER_SIDE_JS = String.raw ` // ---- The recorder's side ----
|
|
|
51
54
|
closePanel();
|
|
52
55
|
} else if (ui.panel === "assert" && ui.assert) {
|
|
53
56
|
ui.assert.error = result.error;
|
|
57
|
+
} else if (ui.panel === "output" && ui.output) {
|
|
58
|
+
if (result.field === "name") ui.output.nameError = result.error;
|
|
59
|
+
else ui.output.error = result.error;
|
|
54
60
|
} else if (ui.panel === "page" && ui.page) {
|
|
55
61
|
ui.page.error = result.error;
|
|
56
62
|
} 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,38 @@ 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
|
+
const outputPanel = () => {
|
|
86
|
+
const o = ui.output;
|
|
87
|
+
const info = o.info;
|
|
88
|
+
const head = h("div", { class: "h" }, "Save as output", info && info.ok && h("code", { title: info.label }, info.label));
|
|
89
|
+
if (!info) return [head, h("div", { class: "msg" }, "…")];
|
|
90
|
+
if (!info.ok) return [head, h("div", { class: "err" }, info.error)];
|
|
91
|
+
const capture = captureOf(info.outputText, o.pattern);
|
|
92
|
+
return [
|
|
93
|
+
head,
|
|
94
|
+
h("div", { class: "lbl" }, "Text"),
|
|
95
|
+
h("div", { class: "msg out-text" }, info.text),
|
|
96
|
+
h("div", { class: "lbl" }, "Name"),
|
|
97
|
+
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" })),
|
|
98
|
+
o.nameError && h("div", { class: "field-error" }, o.nameError),
|
|
99
|
+
h("div", { class: "lbl" }, "Pattern (optional)"),
|
|
100
|
+
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" })),
|
|
101
|
+
h("div", { class: "lbl" }, "Saves"),
|
|
102
|
+
capture.error ? h("div", { class: "warn out-capture" }, capture.error) : h("code", { class: "out-capture" }, capture.value),
|
|
103
|
+
o.error && h("div", { class: "err" }, o.error),
|
|
104
|
+
h("div", { class: "foot" }, h("button", { class: "add", "data-action": "output-save", disabled: ui.busy || !o.name.trim() }, "Save")),
|
|
105
|
+
];
|
|
106
|
+
};
|
|
107
|
+
|
|
76
108
|
const pagePanel = () => {
|
|
77
109
|
const p = ui.page;
|
|
78
110
|
const head = h("div", { class: "h" }, "Page");
|
|
@@ -98,13 +130,14 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
98
130
|
filter,
|
|
99
131
|
variables: state.canAddVariables ? state.variables.filter(hit) : [],
|
|
100
132
|
secrets: state.secrets.filter(hit),
|
|
133
|
+
inputs: (state.inputs || []).filter(hit),
|
|
101
134
|
};
|
|
102
135
|
};
|
|
103
136
|
const insertPanel = () => {
|
|
104
137
|
const ins = ui.insert;
|
|
105
138
|
if (!ins.field) return [h("div", { class: "h" }, "Insert"), h("div", { class: "msg" }, "Click into a field first")];
|
|
106
139
|
const found = matches();
|
|
107
|
-
const none = found.filter && found.variables.length === 0 && found.secrets.length === 0;
|
|
140
|
+
const none = found.filter && found.variables.length === 0 && found.secrets.length === 0 && found.inputs.length === 0;
|
|
108
141
|
const item = (kind, name, value) =>
|
|
109
142
|
h("button", { class: "it", "data-action": "insert-item", "data-kind": kind, "data-name": name, disabled: ui.busy }, h("code", null, name), value);
|
|
110
143
|
return [
|
|
@@ -120,13 +153,19 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
120
153
|
found.secrets.length
|
|
121
154
|
? found.secrets.map((s) => item("secret", s.name, s.set ? h("span", { class: "v" }, "••••••") : h("span", { class: "warn" }, "not set")))
|
|
122
155
|
: h("div", { class: "empty" }, found.filter ? "No secret matches." : "No secrets yet."),
|
|
156
|
+
h("div", { class: "grp" }, "Inputs"),
|
|
157
|
+
found.inputs.length
|
|
158
|
+
? found.inputs.map((i) => item("input", i.name, h("span", { class: "v", title: i.value }, i.value)))
|
|
159
|
+
: h("div", { class: "empty" }, found.filter ? "No input matches." : "No inputs yet."),
|
|
123
160
|
ins.error && h("div", { class: "err" }, ins.error),
|
|
124
161
|
h("button", { class: "act", "data-action": "insert-new", "data-kind": "env" }, "+ Add variable" + (none ? ' "' + variableName(ins.filter) + '"' : "")),
|
|
125
162
|
h("button", { class: "act", "data-action": "insert-new", "data-kind": "secret" }, "+ Add secret" + (none ? ' "' + snake(ins.filter) + '"' : "")),
|
|
163
|
+
h("button", { class: "act", "data-action": "insert-new", "data-kind": "input" }, "+ Add input" + (none ? ' "' + camel(ins.filter) + '"' : "")),
|
|
126
164
|
];
|
|
127
165
|
};
|
|
128
166
|
|
|
129
|
-
const newHint = (secret, env, name) => {
|
|
167
|
+
const newHint = (kind, secret, env, name) => {
|
|
168
|
+
if (kind === "input") return "Declared in the test's inputs, with this value as its default.";
|
|
130
169
|
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
170
|
if (!state.secretsIgnored) return h("span", { class: "warn" }, "Add .env." + env + " to .gitignore before committing.");
|
|
132
171
|
return ["Saved as ", h("code", null, "SAMURAI_SECRET_" + (name.trim() || "<NAME>")), " in ", h("code", null, ".env." + env), " (git-ignored)."];
|
|
@@ -134,22 +173,23 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
134
173
|
const newPanel = () => {
|
|
135
174
|
const f = ui.form;
|
|
136
175
|
const secret = f.kind === "secret";
|
|
137
|
-
const blocked =
|
|
176
|
+
const blocked = f.kind === "env" && !state.canAddVariables;
|
|
138
177
|
const env = state.environment;
|
|
139
178
|
return [
|
|
140
|
-
h("div", { class: "h stack" }, f.setValue ? "Set value" : secret ? "New secret" : "New variable", h("small", null, 'For "' + fieldLabel(ui.insert.field) + '"')),
|
|
179
|
+
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
180
|
h("div", { class: "seg" },
|
|
142
|
-
h("button", { class:
|
|
143
|
-
h("button", { class: secret ? "sel" : "", "data-action": "new-kind", "data-kind": "secret" }, "Secret")
|
|
181
|
+
h("button", { class: f.kind === "env" ? "sel" : "", "data-action": "new-kind", "data-kind": "env" }, "Variable"),
|
|
182
|
+
h("button", { class: secret ? "sel" : "", "data-action": "new-kind", "data-kind": "secret" }, "Secret"),
|
|
183
|
+
h("button", { class: f.kind === "input" ? "sel" : "", "data-action": "new-kind", "data-kind": "input" }, "Input")),
|
|
144
184
|
blocked
|
|
145
185
|
? h("div", { class: "msg" }, "This recording has no environment")
|
|
146
186
|
: [
|
|
147
187
|
h("div", { class: "lbl" }, "Name"),
|
|
148
188
|
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
189
|
f.nameError && h("div", { class: "field-error" }, f.nameError),
|
|
150
|
-
h("div", { class: "lbl" }, "Value in " + env),
|
|
190
|
+
h("div", { class: "lbl" }, f.kind === "input" ? "Default value" : "Value in " + env),
|
|
151
191
|
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)),
|
|
192
|
+
h("div", { class: "hint" }, newHint(f.kind, secret, env, f.name)),
|
|
153
193
|
f.error && h("div", { class: "err" }, f.error),
|
|
154
194
|
],
|
|
155
195
|
h("div", { class: "foot plain" },
|
|
@@ -181,5 +221,5 @@ export const VIEWS_PANELS_JS = String.raw ` const unavailable = (info, matcher)
|
|
|
181
221
|
radio("fullPage", "true", ui.shot.fullPage, "Whole page"),
|
|
182
222
|
h("div", { class: "foot" }, h("button", { class: "add", "data-action": "screenshot-add", disabled: ui.busy }, "Add")),
|
|
183
223
|
];
|
|
184
|
-
const PANELS = { assert: assertPanel, page: pagePanel, insert: insertPanel, new: newPanel, wait: waitPanel, screenshot: screenshotPanel };
|
|
224
|
+
const PANELS = { assert: assertPanel, output: outputPanel, page: pagePanel, insert: insertPanel, new: newPanel, wait: waitPanel, screenshot: screenshotPanel };
|
|
185
225
|
`;
|
|
@@ -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"),
|