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.
- package/README.md +27 -1
- package/dist/fleet/browser.d.ts +10 -4
- package/dist/fleet/browser.d.ts.map +1 -1
- package/dist/fleet/browser.js +15 -5
- package/dist/fleet/browser.js.map +1 -1
- package/dist/fleet/sight.d.ts +45 -0
- package/dist/fleet/sight.d.ts.map +1 -1
- package/dist/fleet/sight.js +185 -2
- package/dist/fleet/sight.js.map +1 -1
- package/dist/harness.d.ts +55 -1
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +204 -37
- package/dist/harness.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +5 -1
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +77 -5
- package/dist/mcp/server.js.map +1 -1
- package/dist/product.d.ts +19 -3
- package/dist/product.d.ts.map +1 -1
- package/dist/redact.d.ts +13 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +226 -0
- package/dist/redact.js.map +1 -0
- package/dist/requests.d.ts +60 -0
- package/dist/requests.d.ts.map +1 -0
- package/dist/requests.js +137 -0
- package/dist/requests.js.map +1 -0
- package/dist/standins/builtin/email/index.d.ts.map +1 -1
- package/dist/standins/builtin/email/index.js +238 -8
- package/dist/standins/builtin/email/index.js.map +1 -1
- package/dist/standins/builtin/email/manifest.json +6 -2
- package/dist/standins/runner.d.ts.map +1 -1
- package/dist/standins/runner.js +25 -9
- package/dist/standins/runner.js.map +1 -1
- package/dist/standins/sdk.js +8 -0
- package/dist/standins/sdk.js.map +1 -1
- 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
|
|
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
|
package/dist/harness.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,UAAU,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
710
|
-
await page.
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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,
|
|
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)
|