@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.
@@ -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.18";
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.map(
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:oklch(17.5% 0.006 265);",
2456
- "border:1px solid oklch(28% 0.009 265);border-radius:12px;padding:10px;",
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:oklch(96% 0.003 265);",
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:oklch(70% 0.008 265)}",
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:oklch(21% 0.008 265);color:oklch(96% 0.003 265);",
2483
- "border:1px solid oklch(28% 0.009 265);transition:background .15s}",
2484
- "button:hover{background:oklch(14.5% 0.006 265)}",
2485
- "button.primary{background:oklch(68% 0.16 275);color:oklch(17% 0.02 275);border-color:transparent}",
2486
- "button.danger{background:oklch(68% 0.18 22);color:oklch(17% 0.02 22);border-color:transparent}",
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:oklch(21% 0.008 265);border:1px solid oklch(28% 0.009 265);",
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:oklch(70% 0.008 265);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
2496
- ".as .tgt b{color:oklch(96% 0.003 265);font-weight:600}",
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:oklch(14.5% 0.006 265);color:oklch(96% 0.003 265);",
2499
- "border:1px solid oklch(28% 0.009 265);min-width:0}",
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:oklch(70% 0.008 265);cursor:pointer}",
2504
- ".as .err{color:oklch(72% 0.17 22);font-size:11px}",
2505
- ".as .hint{font-size:11px;color:oklch(70% 0.008 265)}",
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:oklch(36% 0.011 265);border-radius:99px}",
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:oklch(21% 0.008 265);border:1px solid oklch(28% 0.009 265);",
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:oklch(68% 0.16 275);box-shadow:0 0 0 1px oklch(68% 0.16 275)}",
2517
- ".c.passed{border-color:oklch(72% 0.15 155 / .5);background:oklch(26% 0.05 155 / .3)}",
2518
- ".c.failed{border-color:oklch(68% 0.18 22 / .6);background:oklch(27% 0.06 22 / .4)}",
2519
- ".c.skipped{border-color:oklch(74% 0.11 237 / .5);background:oklch(27% 0.045 237 / .4)}",
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:oklch(68% 0.16 275 / .7);background:oklch(28% 0.06 275 / .45)}",
2524
- ".c.new .o::after{content:' new';color:oklch(80% 0.12 275);font-size:9px}",
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:oklch(68% 0.16 275)}",
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:oklch(56% 0.009 265)}",
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:oklch(28% 0.06 275);color:oklch(80% 0.12 275)}",
2540
- ".t.click,.t.press{background:oklch(26% 0.05 155);color:oklch(72% 0.15 155)}",
2541
- ".t.check{background:oklch(28% 0.05 75);color:oklch(80% 0.14 75)}",
2542
- ".t.back,.t.forward{background:oklch(27% 0.045 237);color:oklch(74% 0.11 237)}",
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:oklch(96% 0.003 265);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
2545
- ".sc{font-size:10px;color:oklch(56% 0.009 265);overflow:hidden;",
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:oklch(70% 0.008 265);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
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 oklch(56% 0.009 265);background:transparent;padding:0;cursor:pointer}",
2551
- ".bp:hover{border-color:oklch(68% 0.18 22)}",
2552
- ".bp.on{background:oklch(68% 0.18 22);border-color:oklch(68% 0.18 22)}",
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:oklch(56% 0.009 265);font-size:13px;line-height:1;cursor:pointer}",
2555
- ".x:hover{background:transparent;color:oklch(68% 0.18 22)}",
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:oklch(27% 0.06 22 / .5);border:1px solid oklch(68% 0.18 22 / .4);",
2558
- "color:oklch(68% 0.18 22);white-space:normal;max-width:640px}",
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:oklch(68% 0.18 22);",
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">&#160;&#183;&#160;' + state.steps.length + (state.steps.length === 1 ? " step" : " steps") + "</span></span>" + // Pausing capture matters: navigating to the part you actually want
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">&#160;&#183;&#160;' + 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 ? "&#8596;" : "&#8597;") + '</button><button id="hide" title="Collapse">&#8211;</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 ? "&#8596;" : "&#8597;") + '</button><button id="hide" title="Collapse">&#8211;</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">&#160;&#183;&#160;' + ran + "/" + state.steps.length + '</span></span><button id="play" class="primary" ' + (running || done ? "disabled" : "") + ">" + (state.failure ? "Skip &amp; 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" : "") + ">Replay</button>" + // While armed, this is the only way to stop it — so it says so, rather
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">&#160;&#183;&#160;' + 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 &amp; 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">Record</button>') + '<span class="sp"></span><button id="orient" title="Switch layout">' + (vertical ? "&#8596;" : "&#8597;") + '</button><button id="hide" title="Collapse">&#8211;</button><button id="finish" class="primary">Save &amp; close</button><button id="discard" class="danger">Discard</button></div>' + // Only when there is something to show. An empty .steps container is
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 ? "&#8596;" : "&#8597;") + '</button><button id="hide" title="Collapse">&#8211;</button><span class="dv"></span><button id="finish" class="primary" title="Keep the edits and close">Save &amp; 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
- try {
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({ content: `${accNameScript2()}
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
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-ZGXVQ2QA.js";
11
+ } from "./chunk-K6L6NDH6.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { parseArgs } from "util";
package/dist/engine.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-ZGXVQ2QA.js";
12
+ } from "./chunk-K6L6NDH6.js";
13
13
  export {
14
14
  AGENT_VERSION,
15
15
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.18",
3
+ "version": "0.1.0-beta.19",
4
4
  "description": "Run Flowriki UI tests on your own computer. Connects this machine to your Flowriki account.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",