swarmbox 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +27 -1
  2. package/dist/fleet/browser.d.ts +10 -4
  3. package/dist/fleet/browser.d.ts.map +1 -1
  4. package/dist/fleet/browser.js +15 -5
  5. package/dist/fleet/browser.js.map +1 -1
  6. package/dist/fleet/sight.d.ts +45 -0
  7. package/dist/fleet/sight.d.ts.map +1 -1
  8. package/dist/fleet/sight.js +185 -2
  9. package/dist/fleet/sight.js.map +1 -1
  10. package/dist/harness.d.ts +55 -1
  11. package/dist/harness.d.ts.map +1 -1
  12. package/dist/harness.js +204 -37
  13. package/dist/harness.js.map +1 -1
  14. package/dist/hooks.d.ts.map +1 -1
  15. package/dist/hooks.js +5 -1
  16. package/dist/hooks.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp/server.d.ts.map +1 -1
  22. package/dist/mcp/server.js +77 -5
  23. package/dist/mcp/server.js.map +1 -1
  24. package/dist/product.d.ts +19 -3
  25. package/dist/product.d.ts.map +1 -1
  26. package/dist/redact.d.ts +13 -0
  27. package/dist/redact.d.ts.map +1 -0
  28. package/dist/redact.js +226 -0
  29. package/dist/redact.js.map +1 -0
  30. package/dist/requests.d.ts +60 -0
  31. package/dist/requests.d.ts.map +1 -0
  32. package/dist/requests.js +137 -0
  33. package/dist/requests.js.map +1 -0
  34. package/dist/standins/builtin/email/index.d.ts.map +1 -1
  35. package/dist/standins/builtin/email/index.js +238 -8
  36. package/dist/standins/builtin/email/index.js.map +1 -1
  37. package/dist/standins/builtin/email/manifest.json +6 -2
  38. package/dist/standins/runner.d.ts.map +1 -1
  39. package/dist/standins/runner.js +25 -9
  40. package/dist/standins/runner.js.map +1 -1
  41. package/dist/standins/sdk.js +8 -0
  42. package/dist/standins/sdk.js.map +1 -1
  43. package/package.json +2 -2
package/dist/harness.d.ts CHANGED
@@ -2,8 +2,9 @@ import type { CDPSession } from 'playwright-core';
2
2
  import type { CheckResult, Settled } from './checks.ts';
3
3
  import { type Appearance, type Fleet, type Screen } from './fleet/browser.ts';
4
4
  import { type Stroke } from './fleet/sight.ts';
5
- import type { ClockMove, ClockState, FeedItem, Hooks } from './hooks.ts';
5
+ import { type ClockMove, type ClockState, type FeedItem, type Hooks } from './hooks.ts';
6
6
  import type { Persona, Product, ProductWorld, WorldSpec } from './product.ts';
7
+ import { type RequestLog, type RequestQuery, type RequestRecord } from './requests.ts';
7
8
  import { type StandIn } from './standins/runner.ts';
8
9
  import { type DeviceState } from './device/device.ts';
9
10
  import { type Prop, type PropKind } from './device/props.ts';
@@ -23,6 +24,8 @@ export interface WorldEntry<W extends ProductWorld = ProductWorld> {
23
24
  feedAt: Map<string, string>;
24
25
  /** The world's stand-ins, by name. */
25
26
  standins: Map<string, StandIn>;
27
+ /** Whether its browsers are kept inside it (Product.contain): nothing they load reaches outside the world. */
28
+ contained: boolean;
26
29
  }
27
30
  export interface ScreenEntry {
28
31
  id: string;
@@ -51,6 +54,8 @@ export interface ScreenEntry {
51
54
  releaseStalled(): void;
52
55
  /** Its camera and the files queued for its file choosers. */
53
56
  device: DeviceState;
57
+ /** What it asked its servers for, newest last (bounded, secrets redacted). */
58
+ requests: RequestLog;
54
59
  }
55
60
  /**
56
61
  * Something a screen's browser saw go wrong without anyone asking: an error in the console or an uncaught
@@ -116,6 +121,8 @@ export interface Harness<W extends ProductWorld = ProductWorld> {
116
121
  }): Promise<ScreenEntry>;
117
122
  /** Change what a screen's network does: airplane mode, a signal that stalls, a slow one, or back to normal. */
118
123
  setNetwork(screenId: string, mode: NetworkMode): Promise<void>;
124
+ /** What a screen asked its servers for, newest last. */
125
+ requests(screenId: string, query?: RequestQuery): RequestRecord[];
119
126
  screens(): ScreenEntry[];
120
127
  screen(screenId: string): ScreenEntry;
121
128
  read(screenId: string): Promise<string>;
@@ -202,6 +209,46 @@ export type Action = {
202
209
  exact?: boolean;
203
210
  index?: number;
204
211
  strokes?: Stroke[];
212
+ }
213
+ /**
214
+ * Drag an element found by role and name with the pointer: pressed at grab (fractions of its box, default
215
+ * its middle; an edge to resize), dropped on another element (at, fractions of its box, default its middle)
216
+ * or moved by a distance in CSS pixels.
217
+ */
218
+ | {
219
+ kind: 'drag';
220
+ role: string;
221
+ name: string;
222
+ exact?: boolean;
223
+ index?: number;
224
+ grab?: [number, number];
225
+ to?: {
226
+ role: string;
227
+ name: string;
228
+ exact?: boolean;
229
+ index?: number;
230
+ at?: [number, number];
231
+ };
232
+ by?: {
233
+ x: number;
234
+ y: number;
235
+ };
236
+ }
237
+ /**
238
+ * Scroll as a person does: an element (role and name) into view; or, with by or to, the panel that element is
239
+ * (or the page, without one) by a distance or to its top or bottom. With nothing at all, the page to its bottom.
240
+ */
241
+ | {
242
+ kind: 'scroll';
243
+ role?: string;
244
+ name?: string;
245
+ exact?: boolean;
246
+ index?: number;
247
+ by?: {
248
+ x: number;
249
+ y: number;
250
+ };
251
+ to?: 'top' | 'bottom';
205
252
  };
206
253
  /**
207
254
  * Playwright's URL glob, for matching inside one route: ** is anything, * is anything but a slash,
@@ -213,6 +260,11 @@ export interface LinkWorld {
213
260
  tenants: readonly string[];
214
261
  tenantOfPath?(path: string): string | undefined;
215
262
  }
263
+ /**
264
+ * A path that stays on the screen's own host. A browser reads "//host/x" (and "/\host/x", and either with a tab
265
+ * or newline in it) as a link to another host, and page.goto follows it.
266
+ */
267
+ export declare function ownPath(path: string, noun?: string): string;
216
268
  /**
217
269
  * A link or path on a tenant's own host, as the path to open there. A full link must be on the tenant's
218
270
  * host (`<tenant>.<baseDomain>`, with or without the world's port: products write links without it). A
@@ -228,5 +280,7 @@ export declare function linkPath(link: string, world: LinkWorld, only?: string,
228
280
  * was refused, such as a text the worker couldn't send a second after the office was told it went.
229
281
  */
230
282
  export declare function feedLine(it: FeedItem, s: Pick<ScreenEntry, 'customer' | 'persona'>): string | null;
283
+ /** Whether a world's clock stands anywhere but on this machine's: moved, or paused. */
284
+ export declare function clockMoved(state: ClockState): boolean;
231
285
  export declare function createHarness<W extends ProductWorld>(product: Product<W>, options?: HarnessOptions): Harness<W>;
232
286
  //# sourceMappingURL=harness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
package/dist/harness.js CHANGED
@@ -9,8 +9,10 @@
9
9
  // refused. Reads are still allowed, because watching what the person does is useful to the agent.
10
10
  import net from 'node:net';
11
11
  import { join } from 'node:path';
12
- import { launchFleet, setCookies, } from "./fleet/browser.js";
13
- import { behindModal, changes, dialogsText, draw, glance, openDialogs, pickAmong, whyNotClicked, } from "./fleet/sight.js";
12
+ import { contained, launchFleet, setCookies, } from "./fleet/browser.js";
13
+ import { behindModal, changes, coveredByOwnLabel, dialogsText, drag, scroll, draw, glance, openDialogs, pickAmong, whyNotClicked, } from "./fleet/sight.js";
14
+ import { HooksMissing } from "./hooks.js";
15
+ import { pendingSince, requestLog, selectRequests, } from "./requests.js";
14
16
  import { isThisMachine } from "./standins/guard.js";
15
17
  import { startStandIn } from "./standins/runner.js";
16
18
  import { setLocation, setNotifications, setUpDevice, showOnCamera, } from "./device/device.js";
@@ -32,22 +34,46 @@ export class HarnessError extends Error {
32
34
  const SCREENCAST = { format: 'jpeg', quality: 60, maxWidth: 1280, maxHeight: 800, everyNthFrame: 1 };
33
35
  /** Long enough that a click's re-render has landed, short enough not to stall the agent. */
34
36
  const SETTLE_MS = 400;
37
+ /** The longest an action waits for the requests it started (a signature sealed, a report built). */
38
+ const ACTION_WAIT_MS = 15_000;
35
39
  const READ_LIMIT = 12_000;
36
40
  /** After a command, how long to wait for the worker's first consequences (a text sent or refused). */
37
41
  const WORKER_MS = 1_200;
38
42
  /** How many ports past the product's first a world may take. */
39
43
  const PORT_RANGE = 50;
44
+ /**
45
+ * After an action: wait for what it started (its requests, a page load) to finish, then a moment for the page to
46
+ * render, again while that moment started more; at most ACTION_WAIT_MS. What is still open then, for the readout.
47
+ */
48
+ async function settleAfter(page, log, since) {
49
+ const end = Date.now() + ACTION_WAIT_MS;
50
+ for (;;) {
51
+ while (pendingSince(log.records, since).length && Date.now() < end)
52
+ await page.waitForTimeout(50);
53
+ await page.waitForLoadState('domcontentloaded');
54
+ await page.waitForTimeout(SETTLE_MS);
55
+ const open = pendingSince(log.records, since);
56
+ if (!open.length || Date.now() >= end)
57
+ return open;
58
+ }
59
+ }
40
60
  /** A request the tenant's server refused, as one line: what was asked, the status and the reason. */
41
- async function refusal(r, origin) {
61
+ async function refusal(r, origin, seq) {
42
62
  let why = '';
43
63
  try {
44
64
  const b = (await r.json());
45
- why = [b.error?.code ?? b.code, b.error?.message ?? b.message].filter(Boolean).join(': ');
65
+ const detail = typeof b.detail === 'string'
66
+ ? b.detail
67
+ : Array.isArray(b.detail)
68
+ ? b.detail.map((d) => [d.loc?.slice(1).join('.'), d.msg].filter(Boolean).join(': ')).join('; ')
69
+ : undefined;
70
+ why = [b.error?.code ?? b.code, b.error?.message ?? b.message ?? detail].filter(Boolean).join(': ');
46
71
  }
47
72
  catch {
48
73
  // Not JSON: the status says enough.
49
74
  }
50
- return `${r.request().method()} ${r.url().slice(origin.length) || '/'} → ${r.status()}${why ? ` ${why.slice(0, 200)}` : ''}`;
75
+ const at = seq === undefined ? '' : ` (screen_requests #${seq})`;
76
+ return `${r.request().method()} ${r.url().slice(origin.length) || '/'} → ${r.status()}${why ? ` ${why.slice(0, 200)}` : ''}${at}`;
51
77
  }
52
78
  /** A port the system has free right now: for a world whose port must be known before it starts. */
53
79
  function ephemeralPort() {
@@ -84,6 +110,19 @@ export function globMatches(glob, url) {
84
110
  .join('.*');
85
111
  return new RegExp(`^${re}$`).test(url);
86
112
  }
113
+ /** Stands for a tenant's origin when checking that a path stays on it. */
114
+ const ON_HOST = 'http://tenant.invalid';
115
+ /**
116
+ * A path that stays on the screen's own host. A browser reads "//host/x" (and "/\host/x", and either with a tab
117
+ * or newline in it) as a link to another host, and page.goto follows it.
118
+ */
119
+ export function ownPath(path, noun = 'tenant') {
120
+ if (!path.startsWith('/'))
121
+ throw new HarnessError(`a path on the ${noun}’s host starts with /, or give the full link`);
122
+ if (new URL(path, ON_HOST).origin !== ON_HOST)
123
+ throw new HarnessError(`${JSON.stringify(path)} would leave the ${noun}’s host: a path starts with a single /`);
124
+ return path;
125
+ }
87
126
  /**
88
127
  * A link or path on a tenant's own host, as the path to open there. A full link must be on the tenant's
89
128
  * host (`<tenant>.<baseDomain>`, with or without the world's port: products write links without it). A
@@ -101,8 +140,7 @@ export function linkPath(link, world, only, noun = 'tenant') {
101
140
  tenant = host.slice(0, -(baseDomain.length + 1));
102
141
  path = `${u.pathname}${u.search}${u.hash}`;
103
142
  }
104
- if (!path.startsWith('/'))
105
- throw new HarnessError(`a path on the ${noun}’s host starts with /, or give the full link`);
143
+ ownPath(path, noun);
106
144
  tenant ??= only ?? world.tenantOfPath?.(path) ?? (tenants.length === 1 ? tenants[0] : undefined);
107
145
  if (!tenant)
108
146
  throw new HarnessError(`say which ${noun}: give the full link (this world has ${tenants.join(', ')})`);
@@ -170,6 +208,14 @@ async function field(page, roles, name, exact) {
170
208
  const byRole = roles.map((role) => page.getByRole(role, { name, exact })).reduce((a, b) => a.or(b));
171
209
  return (await byRole.count()) > 0 ? byRole : page.getByLabel(name, { exact });
172
210
  }
211
+ /** Elements by accessible role and name, as screen_read shows them. */
212
+ function byRole(page, role, name, exact = false) {
213
+ return page.getByRole(role, { name, exact });
214
+ }
215
+ /** Whether a world's clock stands anywhere but on this machine's: moved, or paused. */
216
+ export function clockMoved(state) {
217
+ return state.clock.offsetMs !== 0 || state.clock.frozenAt !== null;
218
+ }
173
219
  /** Bring one screen's browser to the world's sandbox time, paused or running. */
174
220
  async function followClock(s, clock) {
175
221
  const c = s.screen.context.clock;
@@ -228,7 +274,7 @@ export function createHarness(product, options = {}) {
228
274
  throw e;
229
275
  }
230
276
  try {
231
- const fleet = await launchFleet({
277
+ const fleetOptions = {
232
278
  baseDomain: world.baseDomain,
233
279
  port: world.port,
234
280
  headless: options.headless ?? false,
@@ -238,8 +284,27 @@ export function createHarness(product, options = {}) {
238
284
  // product loads from outside.
239
285
  standins: [...standins.values()].map((s) => s.origin),
240
286
  allowOrigins: product.browserAllow ?? [],
241
- });
242
- return { id, spec, world, fleet, createdAt: new Date(), clock: null, feedAt: new Map(), standins };
287
+ contain: product.contain ?? product.browserAllow !== undefined,
288
+ };
289
+ const fleet = await launchFleet(fleetOptions);
290
+ const entry = {
291
+ id,
292
+ spec,
293
+ world,
294
+ fleet,
295
+ contained: contained(fleetOptions),
296
+ createdAt: new Date(),
297
+ clock: null,
298
+ feedAt: new Map(),
299
+ standins,
300
+ };
301
+ // A kept world can come back with its clock moved on an earlier run: its stand-ins and screens start
302
+ // on its time, not this machine's.
303
+ const hooks = await hooked(entry);
304
+ const now = hooks ? await hooks.clock().catch(() => null) : null;
305
+ if (now && clockMoved(now))
306
+ adoptClock(entry, now);
307
+ return entry;
243
308
  }
244
309
  catch (e) {
245
310
  try {
@@ -359,6 +424,8 @@ export function createHarness(product, options = {}) {
359
424
  }
360
425
  /** Whether the world's product has its hooks (asked once per world, kept once they answer). */
361
426
  const hooksUp = new Map();
427
+ /** Worlds whose product serves hooks but no feed. */
428
+ const feedless = new Set();
362
429
  async function hooked(w) {
363
430
  const hooks = w.world.hooks;
364
431
  if (!hooks)
@@ -370,17 +437,35 @@ export function createHarness(product, options = {}) {
370
437
  hooksUp.set(w.id, true);
371
438
  return up ? hooks : null;
372
439
  }
373
- /** Read a tenant's feed to its end, once, so later reads start from now. */
440
+ /** Take the world's clock as the one its screens and stand-ins follow from here on. */
441
+ function adoptClock(w, state) {
442
+ w.clock = { at: state.process.at, paused: state.clock.frozenAt !== null };
443
+ // Stand-ins that sign with the world's time sign with the new one.
444
+ for (const standin of w.standins.values())
445
+ standin.setClock(w.clock);
446
+ return w.clock;
447
+ }
448
+ /**
449
+ * Read a tenant's feed to its end, once, so later reads start from now. A product whose hooks have no feed
450
+ * (only a clock, say) leaves the readout without its "server:" lines, and nothing else changes.
451
+ */
374
452
  async function feedCursor(w, tenant) {
375
- if (w.feedAt.has(tenant))
453
+ if (w.feedAt.has(tenant) || feedless.has(w.id))
376
454
  return;
377
455
  const hooks = await hooked(w);
378
456
  if (!hooks)
379
457
  return;
380
- let page = await hooks.feed(tenant, { limit: 500 });
381
- for (let i = 0; i < 200 && page.items.length === 500; i++)
382
- page = await hooks.feed(tenant, { after: page.next, limit: 500 });
383
- w.feedAt.set(tenant, page.next);
458
+ try {
459
+ let page = await hooks.feed(tenant, { limit: 500 });
460
+ for (let i = 0; i < 200 && page.items.length === 500; i++)
461
+ page = await hooks.feed(tenant, { after: page.next, limit: 500 });
462
+ w.feedAt.set(tenant, page.next);
463
+ }
464
+ catch (e) {
465
+ if (!(e instanceof HooksMissing))
466
+ throw e;
467
+ feedless.add(w.id);
468
+ }
384
469
  }
385
470
  /** What the server did since a tenant's feed was last read, as lines for this screen's agent. */
386
471
  async function serverSince(w, s) {
@@ -420,6 +505,7 @@ export function createHarness(product, options = {}) {
420
505
  // Their addresses too: Chrome's console line for a request cut off carries the URL, not the request.
421
506
  const cutUrls = new Set();
422
507
  const stalled = new Set();
508
+ const requests = requestLog(w.fleet.originFor(persona.tenant));
423
509
  const signal = (kind, text, url) => {
424
510
  signals.push({
425
511
  at: new Date().toISOString(),
@@ -432,11 +518,13 @@ export function createHarness(product, options = {}) {
432
518
  signals.shift();
433
519
  };
434
520
  // The world's time now, read before the screen opens so its first page starts on it: a running clock has
435
- // moved on since it was last set.
436
- if (w.clock && w.world.hooks) {
437
- const now = await w.world.hooks.clock();
438
- w.clock = { at: now.process.at, paused: now.clock.frozenAt !== null };
439
- }
521
+ // moved on since it was last set, and a kept world can come back with its clock already moved.
522
+ // A clock that can't be read leaves the screen on the time it last had (or this machine's), as before:
523
+ // it never stops a screen opening.
524
+ const hooks = await hooked(w);
525
+ const now = hooks ? await hooks.clock().catch(() => null) : null;
526
+ if (now && (w.clock || clockMoved(now)))
527
+ adoptClock(w, now);
440
528
  const device = { files: [], camera: null, cameraVersion: 0 };
441
529
  const screen = await w.fleet.open(who, path ?? who.signIn?.path ?? '/', {
442
530
  ...(w.clock ? { clock: w.clock } : {}),
@@ -456,11 +544,18 @@ export function createHarness(product, options = {}) {
456
544
  return;
457
545
  if (cutUrls.delete(m.location().url))
458
546
  return;
547
+ // Blocked by containment: the request-failed signal names it already.
548
+ if (/net::ERR_BLOCKED_BY_CLIENT/.test(m.text()))
549
+ return;
459
550
  }
460
551
  signal('console-error', m.text(), m.location().url || undefined);
461
552
  });
462
553
  ctx.on('weberror', (e) => signal('page-error', String(e.error()?.stack ?? e.error())));
554
+ ctx.on('request', (r) => requests.onRequest(r));
555
+ ctx.on('requestfinished', (r) => void requests.onFinished(r));
556
+ ctx.on('requestfailed', (r) => requests.onFailed(r));
463
557
  ctx.on('response', (r) => {
558
+ requests.onResponse(r);
464
559
  if (r.status() >= 500)
465
560
  signal('server-error', `${r.status()} ${r.request().method()}`, r.url());
466
561
  });
@@ -472,6 +567,9 @@ export function createHarness(product, options = {}) {
472
567
  // app's own AbortController. Nothing failed.
473
568
  if (r.failure()?.errorText === 'net::ERR_ABORTED')
474
569
  return;
570
+ // Blocked by containment: the product reaching outside its world, which is worth seeing by name.
571
+ if (r.failure()?.errorText.startsWith('net::ERR_BLOCKED_BY_CLIENT'))
572
+ return signal('request-failed', `${r.method()} blocked: outside the world (not in browserAllow)`, r.url());
475
573
  signal('request-failed', `${r.method()} ${r.failure()?.errorText ?? 'failed'}`, r.url());
476
574
  });
477
575
  },
@@ -506,6 +604,7 @@ export function createHarness(product, options = {}) {
506
604
  signals,
507
605
  network: { mode: 'online' },
508
606
  device,
607
+ requests,
509
608
  releaseStalled: () => {
510
609
  for (const route of stalled) {
511
610
  deliberate.add(route.request());
@@ -590,6 +689,8 @@ export function createHarness(product, options = {}) {
590
689
  throw new HarnessError(`world ${worldId} has nobody with id ${personId}`);
591
690
  if (options.app !== undefined && !(product.apps ?? []).includes(options.app))
592
691
  throw new HarnessError(`${product.name} has no app ${options.app}; its apps: ${['(main)', ...(product.apps ?? [])].join(', ')}`);
692
+ if (path !== undefined)
693
+ ownPath(path, noun);
593
694
  const signIn = await w.world.signIn(persona, options.app);
594
695
  return attach(w, persona, { persona, signIn }, path, options);
595
696
  },
@@ -611,6 +712,7 @@ export function createHarness(product, options = {}) {
611
712
  },
612
713
  screens: () => [...screens.values()],
613
714
  screen: getScreen,
715
+ requests: (screenId, query) => selectRequests(getScreen(screenId).requests.records, query),
614
716
  async setNetwork(screenId, mode) {
615
717
  const s = getScreen(screenId);
616
718
  // The screen's one network route (attach) reads the mode on every request, so a change is just
@@ -639,29 +741,87 @@ export function createHarness(product, options = {}) {
639
741
  const w = getWorld(s.worldId);
640
742
  const page = s.screen.page;
641
743
  const before = await glance(page);
744
+ const since = s.requests.last();
642
745
  // From here: what the server does next is what this action caused (or something late finishing).
643
746
  await feedCursor(w, s.persona.tenant);
644
747
  const origin = w.fleet.originFor(s.persona.tenant);
645
748
  const refused = [];
646
749
  const onResponse = (r) => {
647
750
  if (r.status() >= 400 && r.url().startsWith(origin))
648
- refused.push(refusal(r, origin));
751
+ refused.push(refusal(r, origin, s.requests.seqOf(r.request())));
649
752
  };
650
753
  page.on('response', onResponse);
651
754
  let did = '';
755
+ let stillOpen;
652
756
  try {
653
757
  switch (action.kind) {
654
758
  case 'click': {
655
759
  const what = `role=${action.role} name="${action.name}"`;
656
- const t = await target(s, page.getByRole(action.role, {
657
- name: action.name,
658
- exact: action.exact ?? false,
659
- }), what, action.index);
760
+ const t = await target(s, byRole(page, action.role, action.name, action.exact), what, action.index);
660
761
  await reachable(t.el, what);
661
762
  did = `clicked ${action.role} "${action.name}"${t.why ? `, ${t.why}` : ''}`;
662
- await t.el.click({ timeout: 10_000 }).catch((e) => {
663
- throw new HarnessError(`could not click ${what}: ${whyNotClicked(e)}`);
763
+ await t.el.click({ timeout: 10_000 }).catch(async (e) => {
764
+ // A styled radio or checkbox: its own label is drawn over it, and clicking that is the same.
765
+ if (!(await coveredByOwnLabel(t.el)))
766
+ throw new HarnessError(`could not click ${what}: ${whyNotClicked(e)}`);
767
+ await t.el.click({ timeout: 10_000, force: true });
768
+ did += ' (through its label, which is drawn over it)';
769
+ });
770
+ break;
771
+ }
772
+ case 'drag': {
773
+ const what = `role=${action.role} name="${action.name}"`;
774
+ const t = await target(s, byRole(page, action.role, action.name, action.exact), what, action.index);
775
+ await reachable(t.el, what);
776
+ if (!action.to === !action.by)
777
+ throw new HarnessError('a drag needs exactly one of to (an element) or by (a distance)');
778
+ let where;
779
+ let to;
780
+ if (action.to) {
781
+ const dest = action.to;
782
+ const toWhat = `role=${dest.role} name="${dest.name}"`;
783
+ const d = await target(s, byRole(page, dest.role, dest.name, dest.exact), toWhat, dest.index);
784
+ to = { el: d.el, at: dest.at ?? [0.5, 0.5] };
785
+ where = `onto ${dest.role} "${dest.name}"${dest.at ? ` at ${dest.at.join(', ')}` : ''}`;
786
+ }
787
+ else {
788
+ to = { by: action.by };
789
+ where = `by ${action.by.x}, ${action.by.y} px`;
790
+ }
791
+ const scrolled = await drag(page, t.el, action.grab ?? [0.5, 0.5], to).catch((e) => {
792
+ throw new HarnessError(`could not drag ${what}: ${e.message}`);
664
793
  });
794
+ did = `dragged ${action.role} "${action.name}"${action.grab ? ` (held at ${action.grab.join(', ')})` : ''} ${where}${scrolled ? `, scrolling ${scrolled} px on the way` : ''}${t.why ? `, ${t.why}` : ''}`;
795
+ break;
796
+ }
797
+ case 'scroll': {
798
+ if (!action.role !== !action.name)
799
+ throw new HarnessError('to scroll to or over an element, give both its role and its name');
800
+ if (action.by && action.to)
801
+ throw new HarnessError('scroll by a distance or to an end, not both');
802
+ const what = action.role ? `role=${action.role} name="${action.name}"` : 'the page';
803
+ const el = action.role
804
+ ? (await target(s, byRole(page, action.role, action.name, action.exact), what, action.index))
805
+ .el
806
+ : undefined;
807
+ const how = action.by
808
+ ? { by: action.by, over: el }
809
+ : action.to
810
+ ? { end: action.to, over: el }
811
+ : el
812
+ ? { el }
813
+ : { end: 'bottom' };
814
+ const moved = await scroll(page, how).catch((e) => {
815
+ throw new HarnessError(`could not scroll ${what}: ${e.message}`);
816
+ });
817
+ const whither = action.by
818
+ ? `by ${action.by.x}, ${action.by.y} px`
819
+ : action.to
820
+ ? `to the ${action.to}`
821
+ : el
822
+ ? 'into view'
823
+ : 'to the bottom';
824
+ did = `scrolled ${action.role && (action.by || action.to) ? `${what} ` : action.role ? `${what} ` : ''}${whither}${moved ? ` (${moved} px)` : ' (it was already there)'}`;
665
825
  break;
666
826
  }
667
827
  case 'fill': {
@@ -705,9 +865,9 @@ export function createHarness(product, options = {}) {
705
865
  break;
706
866
  }
707
867
  }
708
- // Not networkidle: an app holding an event stream open never lets the network go idle.
709
- await page.waitForLoadState('domcontentloaded');
710
- await page.waitForTimeout(SETTLE_MS);
868
+ // Not networkidle: an app holding an event stream open never lets the network go idle. What this action
869
+ // started is waited for instead: the page's own requests, and any page load, then a moment to render.
870
+ stillOpen = await settleAfter(page, s.requests, since);
711
871
  }
712
872
  finally {
713
873
  page.off('response', onResponse);
@@ -734,6 +894,14 @@ export function createHarness(product, options = {}) {
734
894
  ? ['server: nothing from this screen']
735
895
  : []),
736
896
  ...(refusals.length ? [`refused: ${refusals.join('\n ')}`] : []),
897
+ ...(stillOpen.length
898
+ ? [
899
+ `waiting: still open after ${ACTION_WAIT_MS / 1000} s: ${stillOpen
900
+ .slice(0, 3)
901
+ .map((r) => `#${r.seq} ${r.method} ${r.url}`)
902
+ .join(', ')}${stillOpen.length > 3 ? ` and ${stillOpen.length - 3} more` : ''} (screen_requests)`,
903
+ ]
904
+ : []),
737
905
  ].join('\n');
738
906
  return `${report}\n---\n${await harness.read(screenId)}`;
739
907
  },
@@ -800,13 +968,12 @@ export function createHarness(product, options = {}) {
800
968
  const w = getWorld(worldId);
801
969
  const hooks = await harness.hooks(worldId);
802
970
  const state = move ? await hooks.moveClock(move) : await hooks.clock();
803
- if (move) {
804
- w.clock = { at: state.process.at, paused: state.clock.frozenAt !== null };
805
- // Stand-ins that sign with the world's time sign with the new one.
806
- for (const standin of w.standins.values())
807
- standin.setClock(w.clock);
971
+ // A read brings the screens along too when they don't yet follow a clock that has moved (a kept world
972
+ // resumed with its clock moved before).
973
+ if (move || (!w.clock && clockMoved(state))) {
974
+ const clock = adoptClock(w, state);
808
975
  for (const s of [...screens.values()].filter((x) => x.worldId === worldId)) {
809
- await followClock(s, w.clock);
976
+ await followClock(s, clock);
810
977
  // Signed in again on the new time, as a person who kept using the app would still be. A customer's
811
978
  // screen has no sign-in to keep.
812
979
  if (!s.customer && w.world.renewSignIn)