@flowriki/agent 0.1.0-beta.17 → 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.17";
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" };
@@ -458,7 +458,7 @@ import path7 from "path";
458
458
  // ../engine-playwright/dist/index.js
459
459
  import path3 from "path";
460
460
 
461
- // ../test-model/dist/chunk-T76NYOOM.js
461
+ // ../test-model/dist/chunk-ZRUZU4TI.js
462
462
  import { z as z3 } from "zod";
463
463
  var LocatorOverrideSchema = z3.object({
464
464
  strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
@@ -758,6 +758,20 @@ var FlowDocumentSchema = z3.object({
758
758
  updatedAt: z3.string(),
759
759
  steps: z3.array(RecordedStepSchema),
760
760
  environment: ExecutionEnvironmentSchema.optional(),
761
+ /** WHICH DEPLOYMENT THIS WAS RECORDED AGAINST, by name — "staging",
762
+ * "production", whatever the project calls them.
763
+ *
764
+ * A record of fact, not a constraint: the flow can still be run against any
765
+ * environment, which is the point of naming them. What it answers is the
766
+ * question somebody asks of a test that fails only in one place — "where
767
+ * was this made?" — which was previously unanswerable, because the address
768
+ * it used belonged to a setting that had since moved on.
769
+ *
770
+ * Deliberately NOT the address. An origin in a git-tracked file is the
771
+ * mistake documented on `baseUrl` above; a name survives the deployment
772
+ * moving. Absent on everything recorded before this, and on a project that
773
+ * has never named its environments. */
774
+ recordedIn: z3.string().min(1).optional(),
761
775
  path: z3.string().optional(),
762
776
  actors: z3.array(ActorDefinitionSchema).optional(),
763
777
  defaultActorId: z3.string().optional(),
@@ -1099,8 +1113,22 @@ var AuthNavigationSchema = z32.discriminatedUnion("kind", [
1099
1113
  z32.object({ kind: z32.literal("open"), path: z32.string().min(1) })
1100
1114
  ]);
1101
1115
  var AuthFieldSchema = z32.object({
1102
- /** 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. */
1103
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([]),
1104
1132
  /** What it is called where somebody fills it in — the field's own name on
1105
1133
  * the page, so an account form reads "Username" rather than "st_9f2a". */
1106
1134
  label: z32.string().min(1),
@@ -1134,17 +1162,25 @@ var AuthProfileSchema = z32.object({
1134
1162
  updatedAt: z32.string(),
1135
1163
  revision: z32.number().int().nonnegative().default(0)
1136
1164
  }).strict();
1137
- var ACCOUNT_BINDING_SCHEMA_VERSION = 2;
1165
+ var ACCOUNT_BINDING_SCHEMA_VERSION = 3;
1138
1166
  var AccountBindingSchema = z32.object({
1139
1167
  schemaVersion: z32.literal(ACCOUNT_BINDING_SCHEMA_VERSION),
1140
1168
  id: z32.string().min(1),
1141
1169
  slug: z32.string().regex(/^[a-z0-9][a-z0-9-]*$/),
1142
1170
  name: z32.string().min(1),
1143
1171
  profileId: z32.string().min(1),
1144
- /** Which environment of the APPLICATION UNDER TEST these credentials belong
1145
- * to — never Flowriki's own dev or staging. Null means every environment,
1146
- * which is what a project with one environment wants. */
1147
- environment: z32.string().min(1).nullable().default(null),
1172
+ /** Which environments of the APPLICATION UNDER TEST these credentials work
1173
+ * in — never Flowriki's own dev or staging.
1174
+ *
1175
+ * EMPTY MEANS EVERY ONE, which is what a project with a single deployment
1176
+ * wants and what most accounts will always say.
1177
+ *
1178
+ * A LIST because the same person usually exists in more than one place. One
1179
+ * environment per account was the shape until version 3, and it forced a
1180
+ * duplicate account — same name, same password, same everything — for each
1181
+ * deployment the identity existed in, or a blank "works anywhere" that also
1182
+ * claimed environments it had never been created in. Neither was true. */
1183
+ environments: z32.array(z32.string().min(1)).default([]),
1148
1184
  /** Step id → what this identity types there. Ordinary values: a username is
1149
1185
  * not a credential, and keeping it here is what lets somebody read an
1150
1186
  * account and know who it is. */
@@ -2125,6 +2161,20 @@ function installRecorder() {
2125
2161
  },
2126
2162
  true
2127
2163
  );
2164
+ const SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|one[-_\s]?time[-_\s]?(code|pass)|security[-_\s]?(code|answer)|cvv|cvc|\bpin\b/i;
2165
+ const looksSecret = (el, type, name) => {
2166
+ if (type === "password") return true;
2167
+ const auto = String(el.autocomplete || el.getAttribute?.("autocomplete") || "").toLowerCase();
2168
+ if (/current-password|new-password|one-time-code/.test(auto)) return true;
2169
+ try {
2170
+ const masked = getComputedStyle(el).webkitTextSecurity;
2171
+ if (masked && masked !== "none") return true;
2172
+ } catch {
2173
+ }
2174
+ return SECRET_WORDS.test(
2175
+ [name, el.name, el.id, el.getAttribute?.("aria-label"), el.placeholder].filter(Boolean).join(" ")
2176
+ );
2177
+ };
2128
2178
  const editedValues = /* @__PURE__ */ new WeakMap();
2129
2179
  const onEdit = (e) => {
2130
2180
  if (e.isComposing) return;
@@ -2155,15 +2205,16 @@ function installRecorder() {
2155
2205
  const signature = JSON.stringify(values ?? value);
2156
2206
  if (editedValues.get(el) === signature) return;
2157
2207
  editedValues.set(el, signature);
2208
+ const identity = identify(el);
2158
2209
  send({
2159
2210
  type: type === "range" ? "setRange" : tag === "select" ? "select" : "fill",
2160
2211
  role: roleOf(el),
2161
- ...identify(el),
2162
- ...scoping(el, roleOf(el), identify(el).name),
2212
+ ...identity,
2213
+ ...scoping(el, roleOf(el), identity.name),
2163
2214
  legacySelector: cssPath(el),
2164
2215
  value,
2165
2216
  ...values ? { values } : {},
2166
- secret: type === "password"
2217
+ secret: looksSecret(el, type, identity.name)
2167
2218
  });
2168
2219
  };
2169
2220
  document.addEventListener("input", onEdit, true);
@@ -2218,6 +2269,29 @@ function installDebugPanel() {
2218
2269
  const head = first.filter((id) => !seen.has(id) && (seen.add(id), checkById(id)));
2219
2270
  return [...head.map((id) => checkById(id)), ...CHECKS2.filter((c) => !seen.has(c.id))];
2220
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
+ }
2221
2295
  function prefill(c, r) {
2222
2296
  if (c.kind === "url") return location.pathname + location.search;
2223
2297
  if (c.kind === "title") return document.title;
@@ -2241,9 +2315,7 @@ function installDebugPanel() {
2241
2315
  const current = checkById(checkId) ?? options[0];
2242
2316
  const target = picked?.target;
2243
2317
  const subject = target ? `${esc(String(target.role || "element"))}${target.name ? " \xB7 <b>" + esc(String(target.name)) + "</b>" : ""}` : "the page";
2244
- 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(
2245
- (c) => `<option value="${c.id}"${c.id === current.id ? " selected" : ""}>${esc(c.label)}</option>`
2246
- ).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>";
2247
2319
  }
2248
2320
  function bindAssert() {
2249
2321
  const el = (id) => root.getElementById(id);
@@ -2366,6 +2438,13 @@ function installDebugPanel() {
2366
2438
  unresolvable: picked?.unresolvable ?? false
2367
2439
  }),
2368
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,
2369
2448
  set: (id, value) => {
2370
2449
  const el = root.getElementById(id);
2371
2450
  if (!el) return false;
@@ -2413,12 +2492,23 @@ function installDebugPanel() {
2413
2492
  };
2414
2493
  const STYLE = [
2415
2494
  "<style>",
2416
- ":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)}",
2417
2507
  ".p{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);",
2418
- "max-width:min(96vw,1100px);background:oklch(17.5% 0.006 265);",
2419
- "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;",
2420
2510
  "box-shadow:0 12px 48px oklch(0% 0 0 / 0.6);",
2421
- "color:oklch(96% 0.003 265);",
2511
+ "color:var(--ink);",
2422
2512
  "font:500 12px/1.3 'Inter var',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif}",
2423
2513
  ".p.v{top:16px;bottom:auto;left:16px;transform:none;width:300px}",
2424
2514
  // No margin-bottom here: the gap belongs to the steps list, which is not
@@ -2428,7 +2518,7 @@ function installDebugPanel() {
2428
2518
  "cursor:move;user-select:none}",
2429
2519
  ".grip{display:inline-flex;flex-direction:column;gap:2px;padding:4px 2px;opacity:.45}",
2430
2520
  ".grip i{display:block;width:12px;height:2px;border-radius:1px;",
2431
- "background:oklch(70% 0.008 265)}",
2521
+ "background:var(--muted)}",
2432
2522
  ".p.moved{left:auto;right:auto;bottom:auto;transform:none}",
2433
2523
  // Collapsed: a small pill, not the full panel with nothing in it. The
2434
2524
  // point of minimising is to get out of the way.
@@ -2441,50 +2531,55 @@ function installDebugPanel() {
2441
2531
  // Discard falls off the edge.
2442
2532
  "button{white-space:nowrap}",
2443
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}",
2444
2539
  "button{font:inherit;border:0;border-radius:6px;cursor:pointer;padding:6px 10px;",
2445
- "background:oklch(21% 0.008 265);color:oklch(96% 0.003 265);",
2446
- "border:1px solid oklch(28% 0.009 265);transition:background .15s}",
2447
- "button:hover{background:oklch(14.5% 0.006 265)}",
2448
- "button.primary{background:oklch(68% 0.16 275);color:oklch(17% 0.02 275);border-color:transparent}",
2449
- "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}",
2450
2545
  "button:disabled{opacity:.4;cursor:default}",
2451
2546
  // The assertion form. A card of its own rather than controls jammed into
2452
2547
  // the bar: it is a different job from driving the recording, and it is
2453
2548
  // only on screen while that job is being done.
2454
2549
  ".as{margin-top:8px;padding:9px;border-radius:8px;",
2455
- "background:oklch(21% 0.008 265);border:1px solid oklch(28% 0.009 265);",
2550
+ "background:var(--raised);border:1px solid var(--border);",
2456
2551
  "display:flex;flex-direction:column;gap:7px}",
2457
2552
  ".as .row{display:flex;gap:6px;align-items:center;flex-wrap:wrap}",
2458
- ".as .tgt{font-size:11px;color:oklch(70% 0.008 265);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
2459
- ".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}",
2460
2555
  ".as select,.as input[type=text]{font:inherit;padding:5px 7px;border-radius:6px;",
2461
- "background:oklch(14.5% 0.006 265);color:oklch(96% 0.003 265);",
2462
- "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}",
2463
2558
  ".as select{flex:1 1 150px}",
2464
2559
  ".as input[type=text]{flex:1 1 140px}",
2465
2560
  ".as .lb{display:inline-flex;align-items:center;gap:5px;font-size:11px;",
2466
- "color:oklch(70% 0.008 265);cursor:pointer}",
2467
- ".as .err{color:oklch(72% 0.17 22);font-size:11px}",
2468
- ".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)}",
2469
2564
  ".steps{display:flex;gap:4px;align-items:stretch;overflow:auto;padding:2px 0;margin-top:8px}",
2470
2565
  ".steps::-webkit-scrollbar{height:8px;width:8px}",
2471
- ".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}",
2472
2567
  ".p.v .steps{flex-direction:column;max-height:70vh}",
2473
2568
  // The step card, matching the Flows page: ordinal + type pill, then the
2474
2569
  // accessible name, its scope, and its value.
2475
2570
  ".c{position:relative;display:flex;flex-direction:column;gap:5px;width:176px;flex:0 0 auto;",
2476
- "background:oklch(21% 0.008 265);border:1px solid oklch(28% 0.009 265);",
2571
+ "background:var(--raised);border:1px solid var(--border);",
2477
2572
  "border-radius:8px;padding:9px}",
2478
2573
  ".p.v .c{width:auto}",
2479
- ".c.cur{border-color:oklch(68% 0.16 275);box-shadow:0 0 0 1px oklch(68% 0.16 275)}",
2480
- ".c.passed{border-color:oklch(72% 0.15 155 / .5);background:oklch(26% 0.05 155 / .3)}",
2481
- ".c.failed{border-color:oklch(68% 0.18 22 / .6);background:oklch(27% 0.06 22 / .4)}",
2482
- ".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)}",
2483
2578
  // Recorded in THIS session — the steps about to be committed. A colour of
2484
2579
  // their own, because "new" is not a run outcome and reusing pass green or
2485
2580
  // skip sky would say something untrue about whether they ran.
2486
- ".c.new{border-color:oklch(68% 0.16 275 / .7);background:oklch(28% 0.06 275 / .45)}",
2487
- ".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}",
2488
2583
  ".c{cursor:grab}.c:active{cursor:grabbing}",
2489
2584
  // Real sorting, not a drop hint. The card you grab is the card that moves,
2490
2585
  // and the others slide to their new places as you go — so the list always
@@ -2493,34 +2588,34 @@ function installDebugPanel() {
2493
2588
  "will-change:transform}",
2494
2589
  ".c.drag{transition:none;z-index:50;cursor:grabbing;",
2495
2590
  "box-shadow:0 14px 36px oklch(0% 0 0 / .55);",
2496
- "border-color:oklch(68% 0.16 275)}",
2591
+ "border-color:var(--accent)}",
2497
2592
  ".h{display:flex;align-items:center;gap:6px}",
2498
2593
  ".o{font:500 10px ui-monospace,SFMono-Regular,Menlo,monospace;",
2499
- "font-variant-numeric:tabular-nums;color:oklch(56% 0.009 265)}",
2594
+ "font-variant-numeric:tabular-nums;color:var(--subtle)}",
2500
2595
  ".t{display:inline-flex;align-items:center;border-radius:999px;padding:2px 7px;",
2501
2596
  "font-size:10px;font-weight:500}",
2502
- ".t.fill,.t.select{background:oklch(28% 0.06 275);color:oklch(80% 0.12 275)}",
2503
- ".t.click,.t.press{background:oklch(26% 0.05 155);color:oklch(72% 0.15 155)}",
2504
- ".t.check{background:oklch(28% 0.05 75);color:oklch(80% 0.14 75)}",
2505
- ".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)}",
2506
2601
  ".nm{font:500 12px ui-monospace,SFMono-Regular,Menlo,monospace;",
2507
- "color:oklch(96% 0.003 265);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
2508
- ".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;",
2509
2604
  "text-overflow:ellipsis;white-space:nowrap}",
2510
2605
  ".vl{font:500 10px ui-monospace,SFMono-Regular,Menlo,monospace;",
2511
- "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}",
2512
2607
  ".bp{position:absolute;top:7px;right:24px;width:10px;height:10px;border-radius:50%;",
2513
- "border:1px solid oklch(56% 0.009 265);background:transparent;padding:0;cursor:pointer}",
2514
- ".bp:hover{border-color:oklch(68% 0.18 22)}",
2515
- ".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)}",
2516
2611
  ".x{position:absolute;top:4px;right:4px;background:transparent;border:0;padding:2px 4px;",
2517
- "color:oklch(56% 0.009 265);font-size:13px;line-height:1;cursor:pointer}",
2518
- ".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)}",
2519
2614
  ".err{margin-top:8px;padding:7px 9px;border-radius:8px;",
2520
- "background:oklch(27% 0.06 22 / .5);border:1px solid oklch(68% 0.18 22 / .4);",
2521
- "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}",
2522
2617
  ".rec{display:inline-flex;align-items:center;gap:6px}",
2523
- ".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);",
2524
2619
  "animation:b 1.6s infinite}",
2525
2620
  "@keyframes b{0%,100%{opacity:1}50%{opacity:.25}}",
2526
2621
  "</style>"
@@ -2591,16 +2686,13 @@ function installDebugPanel() {
2591
2686
  }).join("");
2592
2687
  const ran = Object.keys(state.results).length;
2593
2688
  if (state.mode === "record") {
2594
- 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
2595
- // to record should not fill the flow with the steps you took to get
2596
- // there.
2597
- (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
2598
2690
  // to go back to the Flowriki tab for — the state worth asserting on is
2599
2691
  // frequently the one that disappears the moment you look away.
2600
2692
  // Hidden rather than disabled while the form is open: the form has an
2601
2693
  // Add check button of its own, and two on screen at once reads as a
2602
2694
  // mistake even when only one of them works.
2603
- (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.
2604
2696
  // Without it a mis-recording had to be saved and then deleted, which
2605
2697
  // means a wrong flow briefly exists and a wrong secret is briefly on
2606
2698
  // disk.
@@ -2656,9 +2748,14 @@ function installDebugPanel() {
2656
2748
  });
2657
2749
  return;
2658
2750
  }
2659
- 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
2660
2752
  // than reading as a status label nobody realises is a button.
2661
- (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
2662
2759
  // still 4px of padding, which left the bar with 10px above it and 14px
2663
2760
  // below — visibly off-centre before anything has been captured.
2664
2761
  (cards ? '<div class="steps">' + cards + "</div>" : "") + (state.failure ? '<div class="err">' + esc(state.failure.error) + "</div>" : "");
@@ -2672,6 +2769,14 @@ function installDebugPanel() {
2672
2769
  on("pause", () => cmd("pause"));
2673
2770
  on("replay", () => cmd("replay"));
2674
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();
2675
2780
  on("finish", () => cmd("end", { commit: true }));
2676
2781
  on("discard", () => cmd("end", { commit: false }));
2677
2782
  on("orient", () => {
@@ -4480,6 +4585,36 @@ async function replayCase(opts) {
4480
4585
  warnings
4481
4586
  };
4482
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
+ }
4483
4618
  var OUTCOME_WINDOW_MS = 2500;
4484
4619
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
4485
4620
  var COMMITS = /* @__PURE__ */ new Set([
@@ -4731,37 +4866,7 @@ async function startRecordingSession(opts) {
4731
4866
  const chain = src?.frame ? frameChains.get(src.frame) : void 0;
4732
4867
  if (chain?.length) payload.target.frame = chain;
4733
4868
  if (src.page) payload.target.pageId = pageIds.get(src.page) ?? "main";
4734
- try {
4735
- const on = src.page ?? page;
4736
- const all = await locateAll(on, {
4737
- id: "pick",
4738
- type: "click",
4739
- value: "",
4740
- secret: false,
4741
- legacySelector: "",
4742
- proposals: [],
4743
- scope: "",
4744
- ...payload.target
4745
- });
4746
- const handles = await all.elementHandles();
4747
- if (handles.length > 1) {
4748
- for (const [i, handle] of handles.entries()) {
4749
- const isIt = await handle.evaluate((el) => el === window.__flPickedEl).catch(() => false);
4750
- if (isIt) {
4751
- payload.target.nth = i;
4752
- break;
4753
- }
4754
- }
4755
- if (payload.target.nth === void 0) {
4756
- payload.ambiguous = handles.length;
4757
- }
4758
- } else if (handles.length === 0) {
4759
- payload.unresolvable = true;
4760
- }
4761
- await Promise.all(handles.map((h) => h.dispose().catch(() => {
4762
- })));
4763
- } catch {
4764
- }
4869
+ await describePick(src.page ?? page, payload);
4765
4870
  void session.setPicking(false);
4766
4871
  void toPanel({ picked: payload });
4767
4872
  });
@@ -5142,6 +5247,13 @@ async function startDebugSession(opts) {
5142
5247
  );
5143
5248
  }
5144
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
+ };
5145
5257
  async function installContext(context2) {
5146
5258
  await context2.exposeBinding("__flRecord", (_src, payload) => {
5147
5259
  if (!recording || !payload || payload.__sig !== void 0 || actors && _src.page.context() !== actors.current.context) return;
@@ -5190,6 +5302,17 @@ async function startDebugSession(opts) {
5190
5302
  };
5191
5303
  publish();
5192
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
+ });
5193
5316
  await context2.exposeBinding("__flCmd", (_src, msg) => {
5194
5317
  void handleCommand(msg).catch((error) => {
5195
5318
  failure = { stepId: cursor ?? steps[0]?.id ?? "", error: firstLine2(error) };
@@ -5197,9 +5320,12 @@ async function startDebugSession(opts) {
5197
5320
  void paint(snapshot());
5198
5321
  });
5199
5322
  });
5200
- await context2.addInitScript({ content: `${accNameScript2()}
5323
+ await context2.addInitScript({
5324
+ content: `${accNameScript2()}
5325
+ ;window.__flChecks=${JSON.stringify(CHECKS)};
5201
5326
  ;(${installRecorder.toString()})();
5202
- ;(${installDebugPanel.toString()})();` });
5327
+ ;(${installDebugPanel.toString()})();`
5328
+ });
5203
5329
  context2.on("page", (p) => p.on("domcontentloaded", () => void paint(snapshot())));
5204
5330
  }
5205
5331
  await installContext(context);
@@ -5260,6 +5386,18 @@ async function startDebugSession(opts) {
5260
5386
  case "record":
5261
5387
  await session.setRecording(Boolean(msg.on));
5262
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
+ }
5263
5401
  case "moveStep":
5264
5402
  session.moveStep(String(msg.stepId), Number(msg.toIndex));
5265
5403
  break;
@@ -5451,6 +5589,42 @@ async function startDebugSession(opts) {
5451
5589
  await setMuted(!on);
5452
5590
  return publish();
5453
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
+ },
5454
5628
  async finish() {
5455
5629
  closed = true;
5456
5630
  status2 = "finished";
@@ -6310,7 +6484,7 @@ function toSummary(row) {
6310
6484
  };
6311
6485
  }
6312
6486
 
6313
- // ../repository-store/dist/chunk-QSTSWM2T.js
6487
+ // ../repository-store/dist/chunk-A4JZPKZT.js
6314
6488
  var FLOWRIKI_DIR = ".flowriki";
6315
6489
  var FEATURES_DIR = `${FLOWRIKI_DIR}/features`;
6316
6490
  var CONFIG_PATH = `${FLOWRIKI_DIR}/config.json`;
@@ -6351,6 +6525,7 @@ function parseConfig(raw) {
6351
6525
  return {
6352
6526
  baseUrl: typeof source["baseUrl"] === "string" ? source["baseUrl"] : "",
6353
6527
  ...typeof source["applicationEnvironment"] === "string" ? { applicationEnvironment: source["applicationEnvironment"] } : {},
6528
+ ...Array.isArray(source["environments"]) ? { environments: readEnvironments(source["environments"]) } : {},
6354
6529
  reuseAuthentication: source["reuseAuthentication"] === true,
6355
6530
  headless: source["headless"] === true,
6356
6531
  environment: ExecutionEnvironmentSchema.parse(source["environment"] ?? {}),
@@ -6358,6 +6533,20 @@ function parseConfig(raw) {
6358
6533
  flakinessRepeat: clampRepeat(source["flakinessRepeat"])
6359
6534
  };
6360
6535
  }
6536
+ function readEnvironments(raw) {
6537
+ const out = [];
6538
+ const seen = /* @__PURE__ */ new Set();
6539
+ for (const entry of raw) {
6540
+ if (!entry || typeof entry !== "object") continue;
6541
+ const { name, baseUrl } = entry;
6542
+ if (typeof name !== "string" || !name.trim()) continue;
6543
+ const key = name.trim().toLowerCase();
6544
+ if (seen.has(key)) continue;
6545
+ seen.add(key);
6546
+ out.push({ name: name.trim(), baseUrl: typeof baseUrl === "string" ? baseUrl : "" });
6547
+ }
6548
+ return out;
6549
+ }
6361
6550
  var FlowNotFound = class extends Error {
6362
6551
  constructor(message) {
6363
6552
  super(message);
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-NTARVVKX.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-NTARVVKX.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.17",
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",