@flowriki/agent 0.1.0-beta.18 → 0.1.0-beta.19
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/{chunk-ZGXVQ2QA.js → chunk-K6L6NDH6.js} +225 -88
- package/dist/cli.js +1 -1
- package/dist/engine.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import os from "os";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
9
|
+
var AGENT_VERSION = "0.1.0-beta.19";
|
|
10
10
|
async function prepareBrowsers() {
|
|
11
11
|
if (browsersReady()) {
|
|
12
12
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -1113,8 +1113,22 @@ var AuthNavigationSchema = z32.discriminatedUnion("kind", [
|
|
|
1113
1113
|
z32.object({ kind: z32.literal("open"), path: z32.string().min(1) })
|
|
1114
1114
|
]);
|
|
1115
1115
|
var AuthFieldSchema = z32.object({
|
|
1116
|
-
/** The step in the recorded procedure this value is typed into.
|
|
1116
|
+
/** The step in the recorded procedure this value is typed into. Where a
|
|
1117
|
+
* recording types into the same box more than once, this is the FIRST of
|
|
1118
|
+
* them — the one an account's values are keyed by. */
|
|
1117
1119
|
stepId: z32.string().min(1),
|
|
1120
|
+
/** EVERY step that types into this box, when there is more than one.
|
|
1121
|
+
*
|
|
1122
|
+
* A login typed twice — a first attempt with a typo, a page that re-rendered
|
|
1123
|
+
* and was filled again — records two steps for one field, because keystrokes
|
|
1124
|
+
* coalesce only with the step immediately before them. Asking an account for
|
|
1125
|
+
* "Username" twice is the visible half of that; the dangerous half is that
|
|
1126
|
+
* substituting only the first leaves the second typing whatever was
|
|
1127
|
+
* recorded, so a run signs in as the person who made the recording.
|
|
1128
|
+
*
|
|
1129
|
+
* Empty means the one step in `stepId`, so every profile written before this
|
|
1130
|
+
* reads correctly without being migrated. */
|
|
1131
|
+
stepIds: z32.array(z32.string().min(1)).default([]),
|
|
1118
1132
|
/** What it is called where somebody fills it in — the field's own name on
|
|
1119
1133
|
* the page, so an account form reads "Username" rather than "st_9f2a". */
|
|
1120
1134
|
label: z32.string().min(1),
|
|
@@ -2255,6 +2269,29 @@ function installDebugPanel() {
|
|
|
2255
2269
|
const head = first.filter((id) => !seen.has(id) && (seen.add(id), checkById(id)));
|
|
2256
2270
|
return [...head.map((id) => checkById(id)), ...CHECKS2.filter((c) => !seen.has(c.id))];
|
|
2257
2271
|
}
|
|
2272
|
+
const GROUP_LABELS = {
|
|
2273
|
+
state: "State",
|
|
2274
|
+
text: "Text",
|
|
2275
|
+
value: "Value",
|
|
2276
|
+
attributes: "Attributes",
|
|
2277
|
+
count: "How many",
|
|
2278
|
+
page: "The page"
|
|
2279
|
+
};
|
|
2280
|
+
function optionsHtml(options, selected) {
|
|
2281
|
+
const option = (c) => `<option value="${c.id}"${c.id === selected ? " selected" : ""}>${esc(c.label)}</option>`;
|
|
2282
|
+
const suggested = options.slice(0, 3);
|
|
2283
|
+
const rest = options.slice(3);
|
|
2284
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
2285
|
+
for (const c of rest) {
|
|
2286
|
+
const key = c.group ?? "state";
|
|
2287
|
+
const list = byGroup.get(key) ?? [];
|
|
2288
|
+
list.push(c);
|
|
2289
|
+
byGroup.set(key, list);
|
|
2290
|
+
}
|
|
2291
|
+
return (suggested.length ? `<optgroup label="Most likely">${suggested.map(option).join("")}</optgroup>` : "") + [...byGroup.entries()].map(
|
|
2292
|
+
([group, list]) => `<optgroup label="${esc(GROUP_LABELS[group] ?? group)}">${list.map(option).join("")}</optgroup>`
|
|
2293
|
+
).join("");
|
|
2294
|
+
}
|
|
2258
2295
|
function prefill(c, r) {
|
|
2259
2296
|
if (c.kind === "url") return location.pathname + location.search;
|
|
2260
2297
|
if (c.kind === "title") return document.title;
|
|
@@ -2278,9 +2315,7 @@ function installDebugPanel() {
|
|
|
2278
2315
|
const current = checkById(checkId) ?? options[0];
|
|
2279
2316
|
const target = picked?.target;
|
|
2280
2317
|
const subject = target ? `${esc(String(target.role || "element"))}${target.name ? " \xB7 <b>" + esc(String(target.name)) + "</b>" : ""}` : "the page";
|
|
2281
|
-
return `<div class="as"><div class="row"><span class="tgt">${subject}</span><span class="sp"></span>` + (picked ? '<button id="arepick">Pick another</button>' : "") + '</div><div class="row"><select id="acheck">' + options.
|
|
2282
|
-
(c) => `<option value="${c.id}"${c.id === current.id ? " selected" : ""}>${esc(c.label)}</option>`
|
|
2283
|
-
).join("") + "</select>" + (current.attr ? `<input type="text" id="aattr" placeholder="attribute" value="${esc(defaultAttr(r))}">` : "") + (current.input ? `<input type="text" id="aval" placeholder="expected" value="${esc(prefill(current, r))}">` : "") + `</div><div class="row"><label class="lb"><input type="checkbox" id="asoft"${softCheck ? " checked" : ""}>Keep going if this fails</label><span class="sp"></span><button id="aclose">Cancel</button><button id="aadd" class="primary"${trouble && !checkById(checkId)?.page ? " disabled" : ""}>Add check</button></div>` + (trouble ? `<div class="err">${esc(trouble)}</div>` : "") + (pickError ? `<div class="err">${esc(pickError)}</div>` : "") + "</div>";
|
|
2318
|
+
return `<div class="as"><div class="row"><span class="tgt">${subject}</span><span class="sp"></span>` + (picked ? '<button id="arepick">Pick another</button>' : "") + '</div><div class="row"><select id="acheck">' + optionsHtml(options, current.id) + "</select>" + (current.attr ? `<input type="text" id="aattr" placeholder="attribute" value="${esc(defaultAttr(r))}">` : "") + (current.input ? `<input type="text" id="aval" placeholder="expected" value="${esc(prefill(current, r))}">` : "") + `</div><div class="row"><label class="lb"><input type="checkbox" id="asoft"${softCheck ? " checked" : ""}>Keep going if this fails</label><span class="sp"></span><button id="aclose">Cancel</button><button id="aadd" class="primary"${trouble && !checkById(checkId)?.page ? " disabled" : ""}>Add check</button></div>` + (trouble ? `<div class="err">${esc(trouble)}</div>` : "") + (pickError ? `<div class="err">${esc(pickError)}</div>` : "") + "</div>";
|
|
2284
2319
|
}
|
|
2285
2320
|
function bindAssert() {
|
|
2286
2321
|
const el = (id) => root.getElementById(id);
|
|
@@ -2403,6 +2438,13 @@ function installDebugPanel() {
|
|
|
2403
2438
|
unresolvable: picked?.unresolvable ?? false
|
|
2404
2439
|
}),
|
|
2405
2440
|
checkOptions: () => Array.from(root.querySelectorAll("#acheck option")).map((o) => o.value),
|
|
2441
|
+
/** The dropdown's headings, in order. Thirty-one options in one layer
|
|
2442
|
+
* is a list nobody reads, and whether they are grouped is invisible to
|
|
2443
|
+
* a test that only looks at the values. */
|
|
2444
|
+
checkGroups: () => Array.from(root.querySelectorAll("#acheck optgroup")).map((g) => g.label),
|
|
2445
|
+
/** Options sitting directly under the select rather than inside a
|
|
2446
|
+
* group — the flat list this was, and the thing that must be empty. */
|
|
2447
|
+
looseCheckOptions: () => Array.from(root.getElementById("acheck")?.children ?? []).filter((c) => c.tagName === "OPTION").length,
|
|
2406
2448
|
set: (id, value) => {
|
|
2407
2449
|
const el = root.getElementById(id);
|
|
2408
2450
|
if (!el) return false;
|
|
@@ -2450,12 +2492,23 @@ function installDebugPanel() {
|
|
|
2450
2492
|
};
|
|
2451
2493
|
const STYLE = [
|
|
2452
2494
|
"<style>",
|
|
2453
|
-
":host{all:initial
|
|
2495
|
+
":host{all:initial;",
|
|
2496
|
+
"--surface:oklch(20% 0.018 285);--sunken:oklch(13% 0.013 285);",
|
|
2497
|
+
"--raised:oklch(24% 0.018 285);",
|
|
2498
|
+
"--border:oklch(29% 0.018 285);--border-strong:oklch(38% 0.022 285);",
|
|
2499
|
+
"--ink:oklch(97% 0.003 285);--muted:oklch(72% 0.012 285);--subtle:oklch(56% 0.014 285);",
|
|
2500
|
+
"--accent:oklch(70% 0.19 295);--accent-soft:oklch(29% 0.075 295);",
|
|
2501
|
+
"--accent-ink:oklch(82% 0.14 295);--accent-fg:oklch(16% 0.02 285);",
|
|
2502
|
+
"--pass:var(--pass);--pass-soft:var(--pass-soft);",
|
|
2503
|
+
"--fail:var(--fail);--fail-soft:oklch(27% 0.06 22);--fail-fg:var(--fail-fg);",
|
|
2504
|
+
"--warn:var(--warn);--warn-soft:var(--warn-soft);",
|
|
2505
|
+
"--skip:var(--skip);--skip-soft:var(--skip-soft);",
|
|
2506
|
+
"--live:oklch(72% 0.15 250)}",
|
|
2454
2507
|
".p{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);",
|
|
2455
|
-
"max-width:min(96vw,1100px);background:
|
|
2456
|
-
"border:1px solid
|
|
2508
|
+
"max-width:min(96vw,1100px);background:var(--surface);",
|
|
2509
|
+
"border:1px solid var(--border);border-radius:12px;padding:10px;",
|
|
2457
2510
|
"box-shadow:0 12px 48px oklch(0% 0 0 / 0.6);",
|
|
2458
|
-
"color:
|
|
2511
|
+
"color:var(--ink);",
|
|
2459
2512
|
"font:500 12px/1.3 'Inter var',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif}",
|
|
2460
2513
|
".p.v{top:16px;bottom:auto;left:16px;transform:none;width:300px}",
|
|
2461
2514
|
// No margin-bottom here: the gap belongs to the steps list, which is not
|
|
@@ -2465,7 +2518,7 @@ function installDebugPanel() {
|
|
|
2465
2518
|
"cursor:move;user-select:none}",
|
|
2466
2519
|
".grip{display:inline-flex;flex-direction:column;gap:2px;padding:4px 2px;opacity:.45}",
|
|
2467
2520
|
".grip i{display:block;width:12px;height:2px;border-radius:1px;",
|
|
2468
|
-
"background:
|
|
2521
|
+
"background:var(--muted)}",
|
|
2469
2522
|
".p.moved{left:auto;right:auto;bottom:auto;transform:none}",
|
|
2470
2523
|
// Collapsed: a small pill, not the full panel with nothing in it. The
|
|
2471
2524
|
// point of minimising is to get out of the way.
|
|
@@ -2478,50 +2531,55 @@ function installDebugPanel() {
|
|
|
2478
2531
|
// Discard falls off the edge.
|
|
2479
2532
|
"button{white-space:nowrap}",
|
|
2480
2533
|
".sp{flex:1}",
|
|
2534
|
+
// WHAT BELONGS WITH WHAT. Eight controls in an unbroken row is a row you
|
|
2535
|
+
// read left to right every time; a hairline between the groups — what the
|
|
2536
|
+
// run is doing, what you are adding to it, and how you leave — means the
|
|
2537
|
+
// one you want is found by position instead.
|
|
2538
|
+
".dv{width:1px;align-self:stretch;margin:2px 2px;background:var(--border);flex:0 0 auto}",
|
|
2481
2539
|
"button{font:inherit;border:0;border-radius:6px;cursor:pointer;padding:6px 10px;",
|
|
2482
|
-
"background:
|
|
2483
|
-
"border:1px solid
|
|
2484
|
-
"button:hover{background:
|
|
2485
|
-
"button.primary{background:
|
|
2486
|
-
"button.danger{background:
|
|
2540
|
+
"background:var(--raised);color:var(--ink);",
|
|
2541
|
+
"border:1px solid var(--border);transition:background .15s}",
|
|
2542
|
+
"button:hover{background:var(--sunken)}",
|
|
2543
|
+
"button.primary{background:var(--accent);color:var(--accent-fg);border-color:transparent}",
|
|
2544
|
+
"button.danger{background:var(--fail);color:var(--fail-fg);border-color:transparent}",
|
|
2487
2545
|
"button:disabled{opacity:.4;cursor:default}",
|
|
2488
2546
|
// The assertion form. A card of its own rather than controls jammed into
|
|
2489
2547
|
// the bar: it is a different job from driving the recording, and it is
|
|
2490
2548
|
// only on screen while that job is being done.
|
|
2491
2549
|
".as{margin-top:8px;padding:9px;border-radius:8px;",
|
|
2492
|
-
"background:
|
|
2550
|
+
"background:var(--raised);border:1px solid var(--border);",
|
|
2493
2551
|
"display:flex;flex-direction:column;gap:7px}",
|
|
2494
2552
|
".as .row{display:flex;gap:6px;align-items:center;flex-wrap:wrap}",
|
|
2495
|
-
".as .tgt{font-size:11px;color:
|
|
2496
|
-
".as .tgt b{color:
|
|
2553
|
+
".as .tgt{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
2554
|
+
".as .tgt b{color:var(--ink);font-weight:600}",
|
|
2497
2555
|
".as select,.as input[type=text]{font:inherit;padding:5px 7px;border-radius:6px;",
|
|
2498
|
-
"background:
|
|
2499
|
-
"border:1px solid
|
|
2556
|
+
"background:var(--sunken);color:var(--ink);",
|
|
2557
|
+
"border:1px solid var(--border);min-width:0}",
|
|
2500
2558
|
".as select{flex:1 1 150px}",
|
|
2501
2559
|
".as input[type=text]{flex:1 1 140px}",
|
|
2502
2560
|
".as .lb{display:inline-flex;align-items:center;gap:5px;font-size:11px;",
|
|
2503
|
-
"color:
|
|
2504
|
-
".as .err{color:
|
|
2505
|
-
".as .hint{font-size:11px;color:
|
|
2561
|
+
"color:var(--muted);cursor:pointer}",
|
|
2562
|
+
".as .err{color:var(--fail);font-size:11px}",
|
|
2563
|
+
".as .hint{font-size:11px;color:var(--muted)}",
|
|
2506
2564
|
".steps{display:flex;gap:4px;align-items:stretch;overflow:auto;padding:2px 0;margin-top:8px}",
|
|
2507
2565
|
".steps::-webkit-scrollbar{height:8px;width:8px}",
|
|
2508
|
-
".steps::-webkit-scrollbar-thumb{background:
|
|
2566
|
+
".steps::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:99px}",
|
|
2509
2567
|
".p.v .steps{flex-direction:column;max-height:70vh}",
|
|
2510
2568
|
// The step card, matching the Flows page: ordinal + type pill, then the
|
|
2511
2569
|
// accessible name, its scope, and its value.
|
|
2512
2570
|
".c{position:relative;display:flex;flex-direction:column;gap:5px;width:176px;flex:0 0 auto;",
|
|
2513
|
-
"background:
|
|
2571
|
+
"background:var(--raised);border:1px solid var(--border);",
|
|
2514
2572
|
"border-radius:8px;padding:9px}",
|
|
2515
2573
|
".p.v .c{width:auto}",
|
|
2516
|
-
".c.cur{border-color:
|
|
2517
|
-
".c.passed{border-color:
|
|
2518
|
-
".c.failed{border-color:
|
|
2519
|
-
".c.skipped{border-color:
|
|
2574
|
+
".c.cur{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}",
|
|
2575
|
+
".c.passed{border-color:color-mix(in oklch, var(--pass) 50%, transparent);background:color-mix(in oklch, var(--pass-soft) 55%, transparent)}",
|
|
2576
|
+
".c.failed{border-color:color-mix(in oklch, var(--fail) 60%, transparent);background:color-mix(in oklch, var(--fail-soft) 60%, transparent)}",
|
|
2577
|
+
".c.skipped{border-color:color-mix(in oklch, var(--skip) 50%, transparent);background:color-mix(in oklch, var(--skip-soft) 60%, transparent)}",
|
|
2520
2578
|
// Recorded in THIS session — the steps about to be committed. A colour of
|
|
2521
2579
|
// their own, because "new" is not a run outcome and reusing pass green or
|
|
2522
2580
|
// skip sky would say something untrue about whether they ran.
|
|
2523
|
-
".c.new{border-color:
|
|
2524
|
-
".c.new .o::after{content:' new';color:
|
|
2581
|
+
".c.new{border-color:color-mix(in oklch, var(--accent) 70%, transparent);background:color-mix(in oklch, var(--accent-soft) 80%, transparent)}",
|
|
2582
|
+
".c.new .o::after{content:' new';color:var(--accent-ink);font-size:9px}",
|
|
2525
2583
|
".c{cursor:grab}.c:active{cursor:grabbing}",
|
|
2526
2584
|
// Real sorting, not a drop hint. The card you grab is the card that moves,
|
|
2527
2585
|
// and the others slide to their new places as you go — so the list always
|
|
@@ -2530,34 +2588,34 @@ function installDebugPanel() {
|
|
|
2530
2588
|
"will-change:transform}",
|
|
2531
2589
|
".c.drag{transition:none;z-index:50;cursor:grabbing;",
|
|
2532
2590
|
"box-shadow:0 14px 36px oklch(0% 0 0 / .55);",
|
|
2533
|
-
"border-color:
|
|
2591
|
+
"border-color:var(--accent)}",
|
|
2534
2592
|
".h{display:flex;align-items:center;gap:6px}",
|
|
2535
2593
|
".o{font:500 10px ui-monospace,SFMono-Regular,Menlo,monospace;",
|
|
2536
|
-
"font-variant-numeric:tabular-nums;color:
|
|
2594
|
+
"font-variant-numeric:tabular-nums;color:var(--subtle)}",
|
|
2537
2595
|
".t{display:inline-flex;align-items:center;border-radius:999px;padding:2px 7px;",
|
|
2538
2596
|
"font-size:10px;font-weight:500}",
|
|
2539
|
-
".t.fill,.t.select{background:
|
|
2540
|
-
".t.click,.t.press{background:
|
|
2541
|
-
".t.check{background:
|
|
2542
|
-
".t.back,.t.forward{background:
|
|
2597
|
+
".t.fill,.t.select{background:var(--accent-soft);color:var(--accent-ink)}",
|
|
2598
|
+
".t.click,.t.press{background:var(--pass-soft);color:var(--pass)}",
|
|
2599
|
+
".t.check{background:var(--warn-soft);color:var(--warn)}",
|
|
2600
|
+
".t.back,.t.forward{background:var(--skip-soft);color:var(--skip)}",
|
|
2543
2601
|
".nm{font:500 12px ui-monospace,SFMono-Regular,Menlo,monospace;",
|
|
2544
|
-
"color:
|
|
2545
|
-
".sc{font-size:10px;color:
|
|
2602
|
+
"color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
2603
|
+
".sc{font-size:10px;color:var(--subtle);overflow:hidden;",
|
|
2546
2604
|
"text-overflow:ellipsis;white-space:nowrap}",
|
|
2547
2605
|
".vl{font:500 10px ui-monospace,SFMono-Regular,Menlo,monospace;",
|
|
2548
|
-
"color:
|
|
2606
|
+
"color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
2549
2607
|
".bp{position:absolute;top:7px;right:24px;width:10px;height:10px;border-radius:50%;",
|
|
2550
|
-
"border:1px solid
|
|
2551
|
-
".bp:hover{border-color:
|
|
2552
|
-
".bp.on{background:
|
|
2608
|
+
"border:1px solid var(--subtle);background:transparent;padding:0;cursor:pointer}",
|
|
2609
|
+
".bp:hover{border-color:var(--fail)}",
|
|
2610
|
+
".bp.on{background:var(--fail);border-color:var(--fail)}",
|
|
2553
2611
|
".x{position:absolute;top:4px;right:4px;background:transparent;border:0;padding:2px 4px;",
|
|
2554
|
-
"color:
|
|
2555
|
-
".x:hover{background:transparent;color:
|
|
2612
|
+
"color:var(--subtle);font-size:13px;line-height:1;cursor:pointer}",
|
|
2613
|
+
".x:hover{background:transparent;color:var(--fail)}",
|
|
2556
2614
|
".err{margin-top:8px;padding:7px 9px;border-radius:8px;",
|
|
2557
|
-
"background:
|
|
2558
|
-
"color:
|
|
2615
|
+
"background:color-mix(in oklch, var(--fail-soft) 70%, transparent);border:1px solid color-mix(in oklch, var(--fail) 45%, transparent);",
|
|
2616
|
+
"color:var(--fail);white-space:normal;max-width:640px}",
|
|
2559
2617
|
".rec{display:inline-flex;align-items:center;gap:6px}",
|
|
2560
|
-
".dot{width:7px;height:7px;border-radius:50%;background:
|
|
2618
|
+
".dot{width:7px;height:7px;border-radius:50%;background:var(--fail);",
|
|
2561
2619
|
"animation:b 1.6s infinite}",
|
|
2562
2620
|
"@keyframes b{0%,100%{opacity:1}50%{opacity:.25}}",
|
|
2563
2621
|
"</style>"
|
|
@@ -2628,16 +2686,13 @@ function installDebugPanel() {
|
|
|
2628
2686
|
}).join("");
|
|
2629
2687
|
const ran = Object.keys(state.results).length;
|
|
2630
2688
|
if (state.mode === "record") {
|
|
2631
|
-
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + (state.recording ? "running" : "paused") + '"><span class="d"></span><span>' + (state.recording ? "recording" : "paused") + '</span><span class="prog"> · ' + state.steps.length + (state.steps.length === 1 ? " step" : " steps") +
|
|
2632
|
-
// to record should not fill the flow with the steps you took to get
|
|
2633
|
-
// there.
|
|
2634
|
-
(state.recording ? '<button id="rec">Pause capture</button>' : '<button id="rec" class="primary">Resume</button>') + // Authoring a check is a first-class part of recording, not something
|
|
2689
|
+
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + (state.recording ? "running" : "paused") + '"><span class="d"></span><span>' + (state.recording ? "recording" : "paused") + '</span><span class="prog"> · ' + state.steps.length + (state.steps.length === 1 ? " step" : " steps") + '</span></span><span class="dv"></span>' + (state.recording ? '<button id="rec" title="Stop capturing while you navigate">Pause capture</button>' : '<button id="rec" class="primary" title="Capture again from here">Resume</button>') + // Authoring a check is a first-class part of recording, not something
|
|
2635
2690
|
// to go back to the Flowriki tab for — the state worth asserting on is
|
|
2636
2691
|
// frequently the one that disappears the moment you look away.
|
|
2637
2692
|
// Hidden rather than disabled while the form is open: the form has an
|
|
2638
2693
|
// Add check button of its own, and two on screen at once reads as a
|
|
2639
2694
|
// mistake even when only one of them works.
|
|
2640
|
-
(assertMode === "off" ? '<button id="assert" title="Check something on the page">Add check</button>' : "") + '<span class="sp"></span><button id="orient" title="Switch layout">' + (vertical ? "↔" : "↕") + '</button><button id="hide" title="Collapse">–</button>' + // Throwing a recording away has to be possible from HERE, in the page.
|
|
2695
|
+
(assertMode === "off" ? '<button id="assert" title="Check something on the page">Add check</button>' : "") + '<span class="sp"></span><button id="orient" title="Switch layout">' + (vertical ? "↔" : "↕") + '</button><button id="hide" title="Collapse">–</button><span class="dv"></span>' + // Throwing a recording away has to be possible from HERE, in the page.
|
|
2641
2696
|
// Without it a mis-recording had to be saved and then deleted, which
|
|
2642
2697
|
// means a wrong flow briefly exists and a wrong secret is briefly on
|
|
2643
2698
|
// disk.
|
|
@@ -2693,9 +2748,14 @@ function installDebugPanel() {
|
|
|
2693
2748
|
});
|
|
2694
2749
|
return;
|
|
2695
2750
|
}
|
|
2696
|
-
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><button id="play" class="primary" ' + (running || done ? "disabled" : "") + ">" + (state.failure ? "Skip & continue" : "Play") + "</button>" + (state.failure ? '<button id="retry">Retry</button>' : "") + '<button id="step" ' + (running || done ? "disabled" : "") + '>Step</button><button id="pause" ' + (running ? "" : "disabled") + '>Pause</button><button id="replay" ' + (running ? "disabled" : "") +
|
|
2751
|
+
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><span class="dv"></span><button id="play" class="primary" title="' + (state.failure ? "Give up on this step and carry on" : "Run until the next breakpoint or failure") + '" ' + (running || done ? "disabled" : "") + ">" + (state.failure ? "Skip & continue" : "Play") + "</button>" + (state.failure ? '<button id="retry" title="Run this step again">Retry</button>' : "") + '<button id="step" title="Run exactly one step" ' + (running || done ? "disabled" : "") + '>Step</button><button id="pause" title="Stop after the step in flight" ' + (running ? "" : "disabled") + '>Pause</button><button id="replay" title="Start over from the first step" ' + (running ? "disabled" : "") + '>Replay</button><span class="dv"></span>' + // While armed, this is the only way to stop it — so it says so, rather
|
|
2697
2752
|
// than reading as a status label nobody realises is a button.
|
|
2698
|
-
(state.recording ? '<button id="rec" class="danger"><span class="rec"><span class="dot"></span>Stop recording</span></button>' : '<button id="rec"
|
|
2753
|
+
(state.recording ? '<button id="rec" class="danger" title="Stop capturing"><span class="rec"><span class="dot"></span>Stop recording</span></button>' : '<button id="rec" title="Capture new steps at the cursor">Record</button>') + // AUTHORING A CHECK BELONGS HERE MOST OF ALL. Recording had it and
|
|
2754
|
+
// debugging did not, which is the wrong way round: the moment worth
|
|
2755
|
+
// adding a check is while looking at a failure, when the state that
|
|
2756
|
+
// explains it is on the screen — and going back to the flow editor to
|
|
2757
|
+
// add one means describing from memory a page that has since moved on.
|
|
2758
|
+
(assertMode === "off" ? '<button id="assert" title="Check something on the page">Add check</button>' : "") + '<span class="sp"></span><button id="orient" title="Switch layout">' + (vertical ? "↔" : "↕") + '</button><button id="hide" title="Collapse">–</button><span class="dv"></span><button id="finish" class="primary" title="Keep the edits and close">Save & close</button><button id="discard" class="danger" title="Close without keeping the edits">Discard</button></div>' + assertHtml() + // Only when there is something to show. An empty .steps container is
|
|
2699
2759
|
// still 4px of padding, which left the bar with 10px above it and 14px
|
|
2700
2760
|
// below — visibly off-centre before anything has been captured.
|
|
2701
2761
|
(cards ? '<div class="steps">' + cards + "</div>" : "") + (state.failure ? '<div class="err">' + esc(state.failure.error) + "</div>" : "");
|
|
@@ -2709,6 +2769,14 @@ function installDebugPanel() {
|
|
|
2709
2769
|
on("pause", () => cmd("pause"));
|
|
2710
2770
|
on("replay", () => cmd("replay"));
|
|
2711
2771
|
on("rec", () => cmd("record", { on: !state.recording }));
|
|
2772
|
+
on("assert", () => {
|
|
2773
|
+
assertMode = "picking";
|
|
2774
|
+
picked = null;
|
|
2775
|
+
pickError = null;
|
|
2776
|
+
beginPick();
|
|
2777
|
+
render();
|
|
2778
|
+
});
|
|
2779
|
+
bindAssert();
|
|
2712
2780
|
on("finish", () => cmd("end", { commit: true }));
|
|
2713
2781
|
on("discard", () => cmd("end", { commit: false }));
|
|
2714
2782
|
on("orient", () => {
|
|
@@ -4517,6 +4585,36 @@ async function replayCase(opts) {
|
|
|
4517
4585
|
warnings
|
|
4518
4586
|
};
|
|
4519
4587
|
}
|
|
4588
|
+
async function describePick(page, payload) {
|
|
4589
|
+
try {
|
|
4590
|
+
const all = await locateAll(page, {
|
|
4591
|
+
id: "pick",
|
|
4592
|
+
type: "click",
|
|
4593
|
+
value: "",
|
|
4594
|
+
secret: false,
|
|
4595
|
+
legacySelector: "",
|
|
4596
|
+
proposals: [],
|
|
4597
|
+
scope: "",
|
|
4598
|
+
...payload.target
|
|
4599
|
+
});
|
|
4600
|
+
const handles = await all.elementHandles();
|
|
4601
|
+
if (handles.length > 1) {
|
|
4602
|
+
for (const [index, handle] of handles.entries()) {
|
|
4603
|
+
const isIt = await handle.evaluate((el) => el === window.__flPickedEl).catch(() => false);
|
|
4604
|
+
if (isIt) {
|
|
4605
|
+
payload.target["nth"] = index;
|
|
4606
|
+
break;
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
if (payload.target["nth"] === void 0) payload.ambiguous = handles.length;
|
|
4610
|
+
} else if (handles.length === 0) {
|
|
4611
|
+
payload.unresolvable = true;
|
|
4612
|
+
}
|
|
4613
|
+
await Promise.all(handles.map((h) => h.dispose().catch(() => {
|
|
4614
|
+
})));
|
|
4615
|
+
} catch {
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4520
4618
|
var OUTCOME_WINDOW_MS = 2500;
|
|
4521
4619
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4522
4620
|
var COMMITS = /* @__PURE__ */ new Set([
|
|
@@ -4768,37 +4866,7 @@ async function startRecordingSession(opts) {
|
|
|
4768
4866
|
const chain = src?.frame ? frameChains.get(src.frame) : void 0;
|
|
4769
4867
|
if (chain?.length) payload.target.frame = chain;
|
|
4770
4868
|
if (src.page) payload.target.pageId = pageIds.get(src.page) ?? "main";
|
|
4771
|
-
|
|
4772
|
-
const on = src.page ?? page;
|
|
4773
|
-
const all = await locateAll(on, {
|
|
4774
|
-
id: "pick",
|
|
4775
|
-
type: "click",
|
|
4776
|
-
value: "",
|
|
4777
|
-
secret: false,
|
|
4778
|
-
legacySelector: "",
|
|
4779
|
-
proposals: [],
|
|
4780
|
-
scope: "",
|
|
4781
|
-
...payload.target
|
|
4782
|
-
});
|
|
4783
|
-
const handles = await all.elementHandles();
|
|
4784
|
-
if (handles.length > 1) {
|
|
4785
|
-
for (const [i, handle] of handles.entries()) {
|
|
4786
|
-
const isIt = await handle.evaluate((el) => el === window.__flPickedEl).catch(() => false);
|
|
4787
|
-
if (isIt) {
|
|
4788
|
-
payload.target.nth = i;
|
|
4789
|
-
break;
|
|
4790
|
-
}
|
|
4791
|
-
}
|
|
4792
|
-
if (payload.target.nth === void 0) {
|
|
4793
|
-
payload.ambiguous = handles.length;
|
|
4794
|
-
}
|
|
4795
|
-
} else if (handles.length === 0) {
|
|
4796
|
-
payload.unresolvable = true;
|
|
4797
|
-
}
|
|
4798
|
-
await Promise.all(handles.map((h) => h.dispose().catch(() => {
|
|
4799
|
-
})));
|
|
4800
|
-
} catch {
|
|
4801
|
-
}
|
|
4869
|
+
await describePick(src.page ?? page, payload);
|
|
4802
4870
|
void session.setPicking(false);
|
|
4803
4871
|
void toPanel({ picked: payload });
|
|
4804
4872
|
});
|
|
@@ -5179,6 +5247,13 @@ async function startDebugSession(opts) {
|
|
|
5179
5247
|
);
|
|
5180
5248
|
}
|
|
5181
5249
|
const targetIds = /* @__PURE__ */ new Map();
|
|
5250
|
+
const toPanel = async (msg) => {
|
|
5251
|
+
await context.pages()[0]?.evaluate(
|
|
5252
|
+
(v) => window.__flPanelMessage?.(v),
|
|
5253
|
+
msg
|
|
5254
|
+
).catch(() => {
|
|
5255
|
+
});
|
|
5256
|
+
};
|
|
5182
5257
|
async function installContext(context2) {
|
|
5183
5258
|
await context2.exposeBinding("__flRecord", (_src, payload) => {
|
|
5184
5259
|
if (!recording || !payload || payload.__sig !== void 0 || actors && _src.page.context() !== actors.current.context) return;
|
|
@@ -5227,6 +5302,17 @@ async function startDebugSession(opts) {
|
|
|
5227
5302
|
};
|
|
5228
5303
|
publish();
|
|
5229
5304
|
});
|
|
5305
|
+
await context2.exposeBinding("__flPicked", async (src, payload) => {
|
|
5306
|
+
if (payload?.cancelled) {
|
|
5307
|
+
void session.setPicking(false);
|
|
5308
|
+
void toPanel({ picked: null });
|
|
5309
|
+
return;
|
|
5310
|
+
}
|
|
5311
|
+
if (!payload?.target) return;
|
|
5312
|
+
await describePick(src.page ?? page, payload);
|
|
5313
|
+
void session.setPicking(false);
|
|
5314
|
+
void toPanel({ picked: payload });
|
|
5315
|
+
});
|
|
5230
5316
|
await context2.exposeBinding("__flCmd", (_src, msg) => {
|
|
5231
5317
|
void handleCommand(msg).catch((error) => {
|
|
5232
5318
|
failure = { stepId: cursor ?? steps[0]?.id ?? "", error: firstLine2(error) };
|
|
@@ -5234,9 +5320,12 @@ async function startDebugSession(opts) {
|
|
|
5234
5320
|
void paint(snapshot());
|
|
5235
5321
|
});
|
|
5236
5322
|
});
|
|
5237
|
-
await context2.addInitScript({
|
|
5323
|
+
await context2.addInitScript({
|
|
5324
|
+
content: `${accNameScript2()}
|
|
5325
|
+
;window.__flChecks=${JSON.stringify(CHECKS)};
|
|
5238
5326
|
;(${installRecorder.toString()})();
|
|
5239
|
-
;(${installDebugPanel.toString()})();`
|
|
5327
|
+
;(${installDebugPanel.toString()})();`
|
|
5328
|
+
});
|
|
5240
5329
|
context2.on("page", (p) => p.on("domcontentloaded", () => void paint(snapshot())));
|
|
5241
5330
|
}
|
|
5242
5331
|
await installContext(context);
|
|
@@ -5297,6 +5386,18 @@ async function startDebugSession(opts) {
|
|
|
5297
5386
|
case "record":
|
|
5298
5387
|
await session.setRecording(Boolean(msg.on));
|
|
5299
5388
|
break;
|
|
5389
|
+
case "pick":
|
|
5390
|
+
await session.setPicking(msg.on !== false);
|
|
5391
|
+
break;
|
|
5392
|
+
case "assert": {
|
|
5393
|
+
await session.setPicking(false);
|
|
5394
|
+
try {
|
|
5395
|
+
session.addAssertion(msg.assertion);
|
|
5396
|
+
} catch (err) {
|
|
5397
|
+
void toPanel({ pickError: firstLine2(err) });
|
|
5398
|
+
}
|
|
5399
|
+
break;
|
|
5400
|
+
}
|
|
5300
5401
|
case "moveStep":
|
|
5301
5402
|
session.moveStep(String(msg.stepId), Number(msg.toIndex));
|
|
5302
5403
|
break;
|
|
@@ -5488,6 +5589,42 @@ async function startDebugSession(opts) {
|
|
|
5488
5589
|
await setMuted(!on);
|
|
5489
5590
|
return publish();
|
|
5490
5591
|
},
|
|
5592
|
+
async setPicking(on) {
|
|
5593
|
+
if (on) await setMuted(true);
|
|
5594
|
+
await Promise.all(
|
|
5595
|
+
(actors?.all().flatMap((s2) => s2.context.pages()) ?? context.pages()).map(
|
|
5596
|
+
(p) => p.evaluate((v) => window.__flSetPicking?.(v), on).catch(() => {
|
|
5597
|
+
})
|
|
5598
|
+
)
|
|
5599
|
+
);
|
|
5600
|
+
if (!on) await setMuted(!recording);
|
|
5601
|
+
},
|
|
5602
|
+
addAssertion(assertion) {
|
|
5603
|
+
const parsed = RecordedAssertionSchema.parse({ enabled: true, ...assertion });
|
|
5604
|
+
const step = {
|
|
5605
|
+
id: `st_${crypto.randomUUID().slice(0, 8)}`,
|
|
5606
|
+
type: "assert",
|
|
5607
|
+
// Mirrored from the target so the step list can name what is being
|
|
5608
|
+
// checked without reaching inside the assertion to find out.
|
|
5609
|
+
role: parsed.target?.role ?? "",
|
|
5610
|
+
name: parsed.target?.name ?? "",
|
|
5611
|
+
...parsed.target?.nameFrom ? { nameFrom: parsed.target.nameFrom } : {},
|
|
5612
|
+
scope: parsed.target?.scope ?? "",
|
|
5613
|
+
...parsed.target?.scopeRole ? { scopeRole: parsed.target.scopeRole } : {},
|
|
5614
|
+
...parsed.target?.frame?.length ? { frame: parsed.target.frame } : {},
|
|
5615
|
+
...parsed.target?.locator ? { locator: parsed.target.locator } : {},
|
|
5616
|
+
value: "",
|
|
5617
|
+
secret: false,
|
|
5618
|
+
legacySelector: "",
|
|
5619
|
+
proposals: [parsed]
|
|
5620
|
+
};
|
|
5621
|
+
RecordedStepSchema.parse(step);
|
|
5622
|
+
const at = cursor ? steps.findIndex((s2) => s2.id === cursor) : steps.length;
|
|
5623
|
+
const insertAt = at === -1 ? steps.length : at;
|
|
5624
|
+
steps.splice(insertAt, 0, step);
|
|
5625
|
+
recordedInSession.add(step.id);
|
|
5626
|
+
return publish();
|
|
5627
|
+
},
|
|
5491
5628
|
async finish() {
|
|
5492
5629
|
closed = true;
|
|
5493
5630
|
status2 = "finished";
|
package/dist/cli.js
CHANGED
package/dist/engine.js
CHANGED
package/package.json
CHANGED