@broberg/lens-daemon 0.1.42 → 0.1.44
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/package.json +1 -1
- package/src/browser.ts +13 -3
- package/src/flow-store.ts +1 -1
- package/src/flow.ts +74 -7
- package/src/mcp.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broberg/lens-daemon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "The Lens module of the cardmem daemon (flows, critic, coverage, capture sessions, the /lens/* routes and the cardmem-lens MCP) as a package. The daemon mounts createLensRouter(deps) + createLensMcpRouter(deps). Bun runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/browser.ts
CHANGED
|
@@ -360,6 +360,16 @@ export interface LoadedExtension {
|
|
|
360
360
|
export const EXTENSION_CHANNEL = 'chromium';
|
|
361
361
|
const EXTENSION_LAUNCH_TIMEOUT_MS = 30_000;
|
|
362
362
|
const EXTENSION_LAUNCH_ATTEMPTS = 2;
|
|
363
|
+
// Owner 25 Oct (jam, runs 2e72d502 + b64af4b1): a KEPT profile (cookies, cache,
|
|
364
|
+
// history of a real login) took longer than 30 s to start on M1 at load ~175 and
|
|
365
|
+
// the run failed, while a fresh temp profile started in 26 s. A slow start of a
|
|
366
|
+
// kept profile is waited for; the bound stays, so a launch that truly hangs still
|
|
367
|
+
// fails by name.
|
|
368
|
+
const PROFILE_LAUNCH_TIMEOUT_MS = 90_000;
|
|
369
|
+
/** Exported for test: the launch bound for a run, by whether it opens a kept profile. */
|
|
370
|
+
export function launchTimeoutFor(profile: string | undefined): number {
|
|
371
|
+
return profile ? PROFILE_LAUNCH_TIMEOUT_MS : EXTENSION_LAUNCH_TIMEOUT_MS;
|
|
372
|
+
}
|
|
363
373
|
|
|
364
374
|
async function launchWithExtension(
|
|
365
375
|
opts: WithPageOptions,
|
|
@@ -381,7 +391,7 @@ async function launchWithExtension(
|
|
|
381
391
|
...contextOptions(opts),
|
|
382
392
|
headless: true,
|
|
383
393
|
channel: EXTENSION_CHANNEL,
|
|
384
|
-
timeout:
|
|
394
|
+
timeout: launchTimeoutFor(profile),
|
|
385
395
|
args: [
|
|
386
396
|
`--disable-extensions-except=${extPath}`,
|
|
387
397
|
`--load-extension=${extPath}`,
|
|
@@ -396,7 +406,7 @@ async function launchWithExtension(
|
|
|
396
406
|
}
|
|
397
407
|
if (!context) {
|
|
398
408
|
throw new Error(
|
|
399
|
-
`extension browser did not start (${EXTENSION_LAUNCH_ATTEMPTS} attempts × ${
|
|
409
|
+
`extension browser did not start (${EXTENSION_LAUNCH_ATTEMPTS} attempts × ${launchTimeoutFor(profile)} ms): ${lastLaunchError}`,
|
|
400
410
|
);
|
|
401
411
|
}
|
|
402
412
|
// MV3 registers a service worker; MV2 a background page. Wait for whichever
|
|
@@ -468,7 +478,7 @@ async function launchRunBrowser(opts: WithPageOptions, b: RunBrowser): Promise<{
|
|
|
468
478
|
const context = await trackChromeLaunch(b.channel, () => chromium.launchPersistentContext(userDataDir, {
|
|
469
479
|
...contextOptions({ ...opts, ...(userAgent ? { userAgent } : {}) }),
|
|
470
480
|
...runBrowserLaunchOptions(b),
|
|
471
|
-
timeout:
|
|
481
|
+
timeout: launchTimeoutFor(b.profile),
|
|
472
482
|
}));
|
|
473
483
|
return { context, userDataDir };
|
|
474
484
|
} catch (err) {
|
package/src/flow-store.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface StoredFlowStep {
|
|
|
20
20
|
/** F009.122 — the named browser the step ran in; absent = the default one. */
|
|
21
21
|
browser?: string;
|
|
22
22
|
/** F009.125 — the step ran in the extension popup. */
|
|
23
|
-
in?: 'popup';
|
|
23
|
+
in?: 'popup' | 'opened';
|
|
24
24
|
status: 'pass' | 'fail' | 'skip';
|
|
25
25
|
testid: string | null;
|
|
26
26
|
detail: string | null;
|
package/src/flow.ts
CHANGED
|
@@ -167,6 +167,10 @@ export type FlowStep =
|
|
|
167
167
|
// then put the page under test back in front so the extension sees it as the active
|
|
168
168
|
// tab (measured). `path` defaults to the manifest's action.default_popup.
|
|
169
169
|
| { type: 'openPopup'; path?: string; label?: string; timeout?: number }
|
|
170
|
+
// F009.139 — take over a window the PAGE opened (window.open / target=_blank: an
|
|
171
|
+
// OAuth dialog), and wait for it to close again. Steps with in:"opened" run in it.
|
|
172
|
+
| { type: 'waitForPopup'; label?: string; timeout?: number }
|
|
173
|
+
| { type: 'waitForPopupClose'; label?: string; timeout?: number }
|
|
170
174
|
// F009.126 — pass when the run made a matching request (page, popup, any page in the
|
|
171
175
|
// run's browser). `url` is a substring, or a regex with match:"regex" (a path like
|
|
172
176
|
// "/api/clips" would otherwise read as a /pattern/flags literal). Fails listing what WAS seen.
|
|
@@ -296,7 +300,7 @@ export interface FlowStepResult {
|
|
|
296
300
|
/** F009.122 — the named browser this step ran in. Absent = the default one. */
|
|
297
301
|
browser?: string;
|
|
298
302
|
/** F009.125 — present when the step ran in the extension popup. */
|
|
299
|
-
in?: 'popup';
|
|
303
|
+
in?: 'popup' | 'opened';
|
|
300
304
|
status: 'pass' | 'fail' | 'skip';
|
|
301
305
|
testid: string | null;
|
|
302
306
|
detail: string | null;
|
|
@@ -512,6 +516,8 @@ export const STEP_TYPES = new Set([
|
|
|
512
516
|
'openPopup',
|
|
513
517
|
// F009.126 — assert a request the run made
|
|
514
518
|
'expectRequest',
|
|
519
|
+
// F009.139 — follow a window the page opened
|
|
520
|
+
'waitForPopup', 'waitForPopupClose',
|
|
515
521
|
]);
|
|
516
522
|
|
|
517
523
|
/** Validate + normalise an untrusted flow (HTTP/MCP boundary). Throws on the
|
|
@@ -751,6 +757,8 @@ export const STEP_FIELDS: Record<string, string[]> = {
|
|
|
751
757
|
conditional: ['testid', 'present', 'steps'],
|
|
752
758
|
loop: ['data', 'steps', 'parallel'],
|
|
753
759
|
openPopup: ['path'],
|
|
760
|
+
waitForPopup: [],
|
|
761
|
+
waitForPopupClose: [],
|
|
754
762
|
expectRequest: ['url', 'match', 'method', 'status'],
|
|
755
763
|
};
|
|
756
764
|
|
|
@@ -815,17 +823,22 @@ function validateAndNormalizeSteps(steps: unknown[], depth = 0, inParallel = fal
|
|
|
815
823
|
// F009.125 — `in: "popup"` runs a step in the extension popup an earlier
|
|
816
824
|
// openPopup opened. Refused where it cannot mean anything, the same way
|
|
817
825
|
// `browser` is: the popup lives in the main browser only.
|
|
826
|
+
// F009.139 — `in: "opened"` runs a step in the window the page opened, taken
|
|
827
|
+
// over by an earlier waitForPopup. Same refusals as in:"popup".
|
|
818
828
|
if ('in' in s) {
|
|
819
829
|
const where = (s as { in?: unknown }).in;
|
|
820
|
-
if (where !== 'popup') throw new Error(`step ${index} (${s.type}): in must be "popup", got ${JSON.stringify(where)}`);
|
|
821
|
-
if (s.type === 'loop' || s.type === 'conditional' || s.type === 'openPopup') {
|
|
822
|
-
throw new Error(`step ${index} (${s.type}): in:"
|
|
830
|
+
if (where !== 'popup' && where !== 'opened') throw new Error(`step ${index} (${s.type}): in must be "popup" or "opened", got ${JSON.stringify(where)}`);
|
|
831
|
+
if (s.type === 'loop' || s.type === 'conditional' || s.type === 'openPopup' || s.type === 'waitForPopup' || s.type === 'waitForPopupClose') {
|
|
832
|
+
throw new Error(`step ${index} (${s.type}): in:"${where}" goes on the steps that act in that window, not on a ${s.type}`);
|
|
823
833
|
}
|
|
824
834
|
const b = (s as { browser?: unknown }).browser;
|
|
825
835
|
if (b !== undefined && b !== 'main') {
|
|
826
|
-
throw new Error(`step ${index} (${s.type}): in:"
|
|
836
|
+
throw new Error(`step ${index} (${s.type}): in:"${where}" and browser:${JSON.stringify(b)} cannot be combined — it lives in the main browser only`);
|
|
827
837
|
}
|
|
828
|
-
if (inParallel) throw new Error(`step ${index} (${s.type}): in:"
|
|
838
|
+
if (inParallel) throw new Error(`step ${index} (${s.type}): in:"${where}" is not allowed inside a parallel loop`);
|
|
839
|
+
}
|
|
840
|
+
if ((s.type === 'waitForPopup' || s.type === 'waitForPopupClose') && (('browser' in s && s.browser !== 'main') || inParallel)) {
|
|
841
|
+
throw new Error(`step ${index} (${s.type}): follows a window of the main browser only`);
|
|
829
842
|
}
|
|
830
843
|
if (s.type === 'openPopup') {
|
|
831
844
|
const pth = (s as { path?: unknown }).path;
|
|
@@ -1165,7 +1178,8 @@ function parseStepBlock(lines: IndentLine[], start: number, minIndent: number):
|
|
|
1165
1178
|
// F009.122 — `- in <name>: <step>` runs the step in that named browser.
|
|
1166
1179
|
const inM = body.match(/^in\s+([^\s:]+)\s*:\s*(.+)$/);
|
|
1167
1180
|
if (inM) {
|
|
1168
|
-
|
|
1181
|
+
// F009.139 — `- in opened: <step>` is the window the page opened, not a browser.
|
|
1182
|
+
steps.push(Object.assign(parseManuscriptStep(inM[2]!), inM[1] === 'opened' ? { in: 'opened' as const } : { browser: inM[1]! }));
|
|
1169
1183
|
} else {
|
|
1170
1184
|
steps.push(parseManuscriptStep(body));
|
|
1171
1185
|
}
|
|
@@ -1233,6 +1247,7 @@ export const MANUSCRIPT_VERBS = [
|
|
|
1233
1247
|
'expectText', 'expectVisible', 'expectEditable', 'expectAbsent', 'expectTopmost',
|
|
1234
1248
|
'fillSelector', 'clickSelector',
|
|
1235
1249
|
'inspect', 'assert', 'screenshot', 'capture',
|
|
1250
|
+
'waitForPopup', 'waitForPopupClose',
|
|
1236
1251
|
] as const;
|
|
1237
1252
|
|
|
1238
1253
|
function parseManuscriptStep(s: string): FlowStep {
|
|
@@ -1426,6 +1441,9 @@ function parseManuscriptStep(s: string): FlowStep {
|
|
|
1426
1441
|
case 'screenshot':
|
|
1427
1442
|
return { type: 'screenshot', label: s.replace(/^screenshot\s*/i, '').trim() || undefined };
|
|
1428
1443
|
// F255.7 — `waitForBuild <sha>` or bare `waitForBuild` (any change from the live build).
|
|
1444
|
+
// F009.139 — `- waitForPopup` / `- waitForPopupClose`.
|
|
1445
|
+
case 'waitforpopup': return { type: 'waitForPopup' };
|
|
1446
|
+
case 'waitforpopupclose': return { type: 'waitForPopupClose' };
|
|
1429
1447
|
case 'waitforbuild': {
|
|
1430
1448
|
const sha = s.replace(/^waitforbuild\s*/i, '').trim();
|
|
1431
1449
|
return { type: 'waitForBuild', ...(sha ? { sha } : {}) };
|
|
@@ -1638,6 +1656,8 @@ function describe(step: FlowStep): string {
|
|
|
1638
1656
|
case 'conditional': return `${step.present ? 'if' : 'unless'} [${step.testid}]`;
|
|
1639
1657
|
case 'loop': return 'loop';
|
|
1640
1658
|
case 'openPopup': return `openPopup ${step.path ?? '(manifest popup)'}`;
|
|
1659
|
+
case 'waitForPopup': return 'waitForPopup';
|
|
1660
|
+
case 'waitForPopupClose': return 'waitForPopupClose';
|
|
1641
1661
|
case 'expectRequest': return `expectRequest ${step.method ? `${step.method.toUpperCase()} ` : ''}${step.url}${step.status ? ` → ${step.status}` : ''}`;
|
|
1642
1662
|
case 'waitForBuild': return `waitForBuild${step.sha ? ` ${step.sha.slice(0, 8)}` : ' (changed)'}`;
|
|
1643
1663
|
// F255.4 — show the offset in the step label so a positioned click is
|
|
@@ -2632,6 +2652,9 @@ async function execStep(page: Page, step: FlowStep, base: string, vars: Record<s
|
|
|
2632
2652
|
throw new Error('loop steps are flattened before execution');
|
|
2633
2653
|
case 'openPopup':
|
|
2634
2654
|
throw new Error('openPopup steps are handled by runSeq, never execStep');
|
|
2655
|
+
case 'waitForPopup':
|
|
2656
|
+
case 'waitForPopupClose':
|
|
2657
|
+
throw new Error(`${step.type} steps are handled by runSeq, never execStep`);
|
|
2635
2658
|
case 'expectRequest':
|
|
2636
2659
|
throw new Error('expectRequest steps are handled by runSeq, never execStep');
|
|
2637
2660
|
}
|
|
@@ -2974,6 +2997,7 @@ export async function runFlow(flow: Flow, env: AuthEnv, opts: RunFlowOptions = {
|
|
|
2974
2997
|
const onBrowser = {
|
|
2975
2998
|
...(browserName !== 'main' ? { browser: browserName } : {}),
|
|
2976
2999
|
...((step as { in?: string }).in === 'popup' ? { in: 'popup' as const } : {}),
|
|
3000
|
+
...((step as { in?: string }).in === 'opened' ? { in: 'opened' as const } : {}),
|
|
2977
3001
|
};
|
|
2978
3002
|
let sp: Page = page;
|
|
2979
3003
|
if (i < from) {
|
|
@@ -3000,9 +3024,18 @@ export async function runFlow(flow: Flow, env: AuthEnv, opts: RunFlowOptions = {
|
|
|
3000
3024
|
// Keep the page under test the extension's active tab while the popup is driven.
|
|
3001
3025
|
await extCtx.main.bringToFront();
|
|
3002
3026
|
}
|
|
3027
|
+
if ((step as { in?: string }).in === 'opened') {
|
|
3028
|
+
if (!openedWin.page) throw new Error('in:"opened" before any waitForPopup step — take over the opened window first');
|
|
3029
|
+
if (openedWin.page.isClosed()) throw new Error('the opened window has closed — steps after waitForPopupClose run in the opener (drop in:"opened")');
|
|
3030
|
+
sp = openedWin.page;
|
|
3031
|
+
}
|
|
3003
3032
|
const out =
|
|
3004
3033
|
step.type === 'openPopup'
|
|
3005
3034
|
? await openPopup(step)
|
|
3035
|
+
: step.type === 'waitForPopup'
|
|
3036
|
+
? await waitForPopup(step)
|
|
3037
|
+
: step.type === 'waitForPopupClose'
|
|
3038
|
+
? await waitForPopupClose(step)
|
|
3006
3039
|
: step.type === 'expectRequest'
|
|
3007
3040
|
? await expectRequest(step, vars)
|
|
3008
3041
|
: await execStep(sp, step, base, vars, opts.critic ?? 'off', opts.localPath, sink, filledPasswords, notListening, opts.secretRoot, mailCtx);
|
|
@@ -3171,6 +3204,39 @@ export async function runFlow(flow: Flow, env: AuthEnv, opts: RunFlowOptions = {
|
|
|
3171
3204
|
extCtx.popup = popup;
|
|
3172
3205
|
return null;
|
|
3173
3206
|
};
|
|
3207
|
+
// F009.139 — every window a page of the run's main browser opens, in order, from
|
|
3208
|
+
// the first step on: a popup can open DURING the click, before any wait runs.
|
|
3209
|
+
const openedWin: { seen: Page[]; taken: number; page: Page | null; arrived: (() => void) | null } = { seen: [], taken: 0, page: null, arrived: null };
|
|
3210
|
+
const watchOpened = (context: BrowserContext): void => {
|
|
3211
|
+
context.on('page', (p) => {
|
|
3212
|
+
void p.opener().then((op) => {
|
|
3213
|
+
if (!op) return; // a page Lens itself opened (openPopup, a new tab) — not the page's doing
|
|
3214
|
+
openedWin.seen.push(p);
|
|
3215
|
+
openedWin.arrived?.();
|
|
3216
|
+
});
|
|
3217
|
+
});
|
|
3218
|
+
};
|
|
3219
|
+
const waitForPopup = async (step: Extract<FlowStep, { type: 'waitForPopup' }>): Promise<null> => {
|
|
3220
|
+
const timeout = step.timeout ?? 15_000;
|
|
3221
|
+
if (openedWin.seen.length <= openedWin.taken) {
|
|
3222
|
+
await new Promise<void>((resolve, reject) => {
|
|
3223
|
+
const t = setTimeout(() => { openedWin.arrived = null; reject(new Error(`no window was opened within ${timeout} ms`)); }, timeout);
|
|
3224
|
+
openedWin.arrived = () => { clearTimeout(t); openedWin.arrived = null; resolve(); };
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
const p = openedWin.seen[openedWin.seen.length - 1]!;
|
|
3228
|
+
openedWin.taken = openedWin.seen.length;
|
|
3229
|
+
openedWin.page = p;
|
|
3230
|
+
collectFailures(p);
|
|
3231
|
+
await p.waitForLoadState('domcontentloaded', { timeout });
|
|
3232
|
+
return null;
|
|
3233
|
+
};
|
|
3234
|
+
const waitForPopupClose = async (step: Extract<FlowStep, { type: 'waitForPopupClose' }>): Promise<null> => {
|
|
3235
|
+
if (!openedWin.page) throw new Error('waitForPopupClose before any waitForPopup step');
|
|
3236
|
+
if (openedWin.page.isClosed()) return null;
|
|
3237
|
+
await openedWin.page.waitForEvent('close', { timeout: step.timeout ?? 15_000 });
|
|
3238
|
+
return null;
|
|
3239
|
+
};
|
|
3174
3240
|
// F009.128 — files the run downloads. A download is awaited before the context
|
|
3175
3241
|
// closes; otherwise the bytes die with the browser and the report says nothing.
|
|
3176
3242
|
const downloads: FlowDownload[] = [];
|
|
@@ -3320,6 +3386,7 @@ export async function runFlow(flow: Flow, env: AuthEnv, opts: RunFlowOptions = {
|
|
|
3320
3386
|
collectFailures(page);
|
|
3321
3387
|
watchRequests(context);
|
|
3322
3388
|
watchDownloads(context);
|
|
3389
|
+
watchOpened(context);
|
|
3323
3390
|
if (ext && flow.extension && prepared) {
|
|
3324
3391
|
extCtx = { context, id: ext.id, main: page, popup: null };
|
|
3325
3392
|
const mint = flow.extension.storage_local_mint;
|
package/src/mcp.ts
CHANGED
|
@@ -405,7 +405,7 @@ export const TOOLS = [
|
|
|
405
405
|
{
|
|
406
406
|
name: 'lens_run_manuscript',
|
|
407
407
|
description:
|
|
408
|
-
`Compile a human-readable markdown manuscript (fmc-automation style: "# flow: name", "base: <url>", then one "- <verb> …" per line) into a flow and run it E2E. VERBS (${MANUSCRIPT_VERBS.length}, this is the complete list): ${MANUSCRIPT_VERBS.join(' / ')}. Shapes: "- goto /x" · "- fill [testid] = value" · a PASSWORD without plaintext: "- fill [testid] from file <path>" (absolute, or relative to local_path) or "- fill [testid] from env <NAME>" (F009.121 — password inputs only; the value never appears in the run, the report or a screenshot) · "- click [testid]" · "- press Enter" (global) or "- press Enter [testid]" (on a focused element) · "- expectText [testid] ~ text" · "- drag [source] -> [target]" or "- drag [node] by 120,-40" (a canvas offset; both move in 10 mousemoves so React Flow / d3-drag nodes really move — "steps N" to change, F009.123) · "- upload [input-testid] = <fixture>" · an input with NO testid, also inside an open shadow root: "- uploadSelector <css> = <fixture>[, <fixture>]" (F009.136; fixtures repo-relative to local_path or data:) · a CODE FROM MAIL: "- fill [testid] from mail to jam@jams.party subject ~ Jam match /\\b\\d{6}\\b/" (F009.137 — waits ≤60 s for a mail received after the step started, read through mail-mcp; only the test mailbox webhouse.developer@gmail.com / jam@jams.party; the mail is deleted after the run, report carries mail {consumed, cleaned}; the code never appears in the report) · the BUILT SCREEN'S HTML after the click path: "- inspect html [testid]" (or "- inspect html" for the whole body) — the step detail is that outerHTML, for cardmem_check_mockup_fidelity's built_html (F009.138) · "- assert: <js>" · "- screenshot label" · frame-aware "- fillSelector <css> = <value> @frame <iframe>" and "- clickSelector <css> @frame <iframe>" · "- waitForUrl <substring>". TWO BROWSERS AT ONCE (F009.122): prefix a step with "in <name>:" — "- in b: goto /" · "- in b: expectText [note] == Hej" — and it runs in its own browser (own cookies + storage), open alongside the default one for the rest of the run; steps without the prefix run in the default browser. Click modifiers: "- click [testid] via enter" (F255.1, for Angular/MDC buttons a mouse click does not activate) and "- click [testid] @ 50%,28%" (F255.4, aim at a point inside a canvas/3D surface). A field that already HOLDS a value may no longer render an input — a combobox typically swaps to its selected state, so "fill" times out there while "press ArrowDown" / "press Enter" still drive it (fd-sundhed, 2026-08-21). The authoring shortcut — write prose, not JSON.`,
|
|
408
|
+
`Compile a human-readable markdown manuscript (fmc-automation style: "# flow: name", "base: <url>", then one "- <verb> …" per line) into a flow and run it E2E. VERBS (${MANUSCRIPT_VERBS.length}, this is the complete list): ${MANUSCRIPT_VERBS.join(' / ')}. Shapes: "- goto /x" · "- fill [testid] = value" · a PASSWORD without plaintext: "- fill [testid] from file <path>" (absolute, or relative to local_path) or "- fill [testid] from env <NAME>" (F009.121 — password inputs only; the value never appears in the run, the report or a screenshot) · "- click [testid]" · "- press Enter" (global) or "- press Enter [testid]" (on a focused element) · "- expectText [testid] ~ text" · "- drag [source] -> [target]" or "- drag [node] by 120,-40" (a canvas offset; both move in 10 mousemoves so React Flow / d3-drag nodes really move — "steps N" to change, F009.123) · "- upload [input-testid] = <fixture>" · an input with NO testid, also inside an open shadow root: "- uploadSelector <css> = <fixture>[, <fixture>]" (F009.136; fixtures repo-relative to local_path or data:) · a CODE FROM MAIL: "- fill [testid] from mail to jam@jams.party subject ~ Jam match /\\b\\d{6}\\b/" (F009.137 — waits ≤60 s for a mail received after the step started, read through mail-mcp; only the test mailbox webhouse.developer@gmail.com / jam@jams.party; the mail is deleted after the run, report carries mail {consumed, cleaned}; the code never appears in the report) · the BUILT SCREEN'S HTML after the click path: "- inspect html [testid]" (or "- inspect html" for the whole body) — the step detail is that outerHTML, for cardmem_check_mockup_fidelity's built_html (F009.138) · a WINDOW THE PAGE OPENS (window.open / target=_blank — an OAuth dialog like «Continue with Facebook/Google»): "- waitForPopup" takes it over (also one opened by the click just before), "- in opened: <step>" acts in it, "- waitForPopupClose" waits for it to close; steps without the prefix stay in the opener (F009.139) · "- assert: <js>" · "- screenshot label" · frame-aware "- fillSelector <css> = <value> @frame <iframe>" and "- clickSelector <css> @frame <iframe>" · "- waitForUrl <substring>". TWO BROWSERS AT ONCE (F009.122): prefix a step with "in <name>:" — "- in b: goto /" · "- in b: expectText [note] == Hej" — and it runs in its own browser (own cookies + storage), open alongside the default one for the rest of the run; steps without the prefix run in the default browser. Click modifiers: "- click [testid] via enter" (F255.1, for Angular/MDC buttons a mouse click does not activate) and "- click [testid] @ 50%,28%" (F255.4, aim at a point inside a canvas/3D surface). A field that already HOLDS a value may no longer render an input — a combobox typically swaps to its selected state, so "fill" times out there while "press ArrowDown" / "press Enter" still drive it (fd-sundhed, 2026-08-21). The authoring shortcut — write prose, not JSON.`,
|
|
409
409
|
inputSchema: {
|
|
410
410
|
type: 'object',
|
|
411
411
|
required: ['project', 'manuscript'],
|