@riddledc/riddle-proof 0.7.58 → 0.7.60
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 +17 -0
- package/dist/{chunk-7ZBK2GQX.js → chunk-FLQ4HHTT.js} +226 -38
- package/dist/cli.cjs +226 -38
- package/dist/cli.js +1 -1
- package/dist/index.cjs +226 -38
- package/dist/index.js +1 -1
- package/dist/profile.cjs +226 -38
- package/dist/profile.d.cts +4 -1
- package/dist/profile.d.ts +4 -1
- package/dist/profile.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -270,6 +270,13 @@ can include `repeat` / `repeat_count` / `times` from 1 to 100; each repetition
|
|
|
270
270
|
is recorded with `repeat_index` and `repeat_count`, and `after_ms` runs after
|
|
271
271
|
each repetition. Use it for bounded game proof helpers, retry controls, or other
|
|
272
272
|
workflows where one declarative action needs to advance the app several times.
|
|
273
|
+
Add `frame_selector` / `frameSelector` to a setup action when the interaction
|
|
274
|
+
target lives inside an embedded iframe, such as a community game player or
|
|
275
|
+
hosted preview surface. Selector-based actions, storage actions, window calls,
|
|
276
|
+
and setup assertions then execute in that frame context and record
|
|
277
|
+
`frame_selector`, `frame_index`, and `frame_count` in setup-action evidence.
|
|
278
|
+
Use `frame_index` / `frameIndex` when more than one matching iframe is present;
|
|
279
|
+
it defaults to the first frame.
|
|
273
280
|
|
|
274
281
|
`target.timeout_sec` is optional. Use it for known-heavy profile targets so the
|
|
275
282
|
profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
|
|
@@ -348,6 +355,11 @@ game, or preview surfaces that render inside iframes:
|
|
|
348
355
|
|
|
349
356
|
```json
|
|
350
357
|
[
|
|
358
|
+
{
|
|
359
|
+
"type": "frame_url_matches",
|
|
360
|
+
"selector": ".game-player-root iframe",
|
|
361
|
+
"pattern": "/saved/hot-path-.+/index\\.html$"
|
|
362
|
+
},
|
|
351
363
|
{
|
|
352
364
|
"type": "frame_text_visible",
|
|
353
365
|
"selector": ".game-player-root iframe",
|
|
@@ -361,6 +373,11 @@ game, or preview surfaces that render inside iframes:
|
|
|
361
373
|
]
|
|
362
374
|
```
|
|
363
375
|
|
|
376
|
+
Use `frame_url_equals` when the iframe must resolve to one exact embedded
|
|
377
|
+
resource, or `frame_url_matches` when a preview/job/saved-game URL has a stable
|
|
378
|
+
shape but a generated ID. URL checks fail when the frame is missing, just like
|
|
379
|
+
frame text and overflow checks.
|
|
380
|
+
|
|
364
381
|
Frame checks capture each matching iframe's URL, title, compact text sample,
|
|
365
382
|
scroll width, client width, measured horizontal overflow, and top visible
|
|
366
383
|
overflow offenders. This keeps embedded-player audits in profile mode instead
|
|
@@ -22,6 +22,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
22
22
|
"selector_count_eq",
|
|
23
23
|
"selector_text_order",
|
|
24
24
|
"frame_text_visible",
|
|
25
|
+
"frame_url_equals",
|
|
26
|
+
"frame_url_matches",
|
|
25
27
|
"frame_no_horizontal_overflow",
|
|
26
28
|
"text_visible",
|
|
27
29
|
"text_absent",
|
|
@@ -236,6 +238,11 @@ function normalizeSetupAction(input, index) {
|
|
|
236
238
|
if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
|
|
237
239
|
const type = normalizeSetupActionType(stringValue(input.type), index);
|
|
238
240
|
const selector = stringValue(input.selector);
|
|
241
|
+
const frameSelector = stringFromOwn(input, "frame_selector", "frameSelector", "iframe_selector", "iframeSelector");
|
|
242
|
+
const frameIndex = numberValue(valueFromOwn(input, "frame_index", "frameIndex", "iframe_index", "iframeIndex"));
|
|
243
|
+
if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
|
|
244
|
+
throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
|
|
245
|
+
}
|
|
239
246
|
if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
|
|
240
247
|
throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
|
|
241
248
|
}
|
|
@@ -288,6 +295,8 @@ function normalizeSetupAction(input, index) {
|
|
|
288
295
|
return {
|
|
289
296
|
type,
|
|
290
297
|
selector,
|
|
298
|
+
frame_selector: frameSelector,
|
|
299
|
+
frame_index: frameIndex,
|
|
291
300
|
force: type === "click" && (input.force === true || input.force_click === true || input.forceClick === true),
|
|
292
301
|
key,
|
|
293
302
|
value,
|
|
@@ -499,12 +508,19 @@ function normalizeCheck(input, index) {
|
|
|
499
508
|
if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue(input.selector)) {
|
|
500
509
|
throw new Error(`checks[${index}] ${type} requires selector.`);
|
|
501
510
|
}
|
|
502
|
-
if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
|
|
511
|
+
if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
|
|
503
512
|
throw new Error(`checks[${index}] ${type} requires selector.`);
|
|
504
513
|
}
|
|
505
514
|
if (type === "frame_text_visible" && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
506
515
|
throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
|
|
507
516
|
}
|
|
517
|
+
const expectedUrl = stringFromOwn(input, "expected_url", "expectedUrl", "url", "expected_value", "expectedValue", "value");
|
|
518
|
+
if (type === "frame_url_equals" && expectedUrl === void 0) {
|
|
519
|
+
throw new Error(`checks[${index}] frame_url_equals requires expected_url.`);
|
|
520
|
+
}
|
|
521
|
+
if (type === "frame_url_matches" && !stringValue(input.pattern)) {
|
|
522
|
+
throw new Error(`checks[${index}] frame_url_matches requires pattern.`);
|
|
523
|
+
}
|
|
508
524
|
if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
509
525
|
throw new Error(`checks[${index}] ${type} requires text or pattern.`);
|
|
510
526
|
}
|
|
@@ -537,6 +553,7 @@ function normalizeCheck(input, index) {
|
|
|
537
553
|
expected_path: stringValue(input.expected_path),
|
|
538
554
|
param: stringValue(input.param) || stringValue(input.search_param) || stringValue(input.searchParam) || stringValue(input.key),
|
|
539
555
|
expected_value: expectedValue,
|
|
556
|
+
expected_url: expectedUrl,
|
|
540
557
|
expected_routes: expectedRoutes,
|
|
541
558
|
selector: stringValue(input.selector),
|
|
542
559
|
expected_texts: expectedTexts,
|
|
@@ -1004,6 +1021,35 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
1004
1021
|
message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
|
|
1005
1022
|
};
|
|
1006
1023
|
}
|
|
1024
|
+
if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
|
|
1025
|
+
const key = selectorKey(check);
|
|
1026
|
+
const expectedUrl = check.expected_url || check.expected_value || "";
|
|
1027
|
+
const results = viewports.map((viewport) => {
|
|
1028
|
+
const frames = frameEvidenceForSelector(viewport, key);
|
|
1029
|
+
const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
|
|
1030
|
+
const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : matchText(url, check));
|
|
1031
|
+
return {
|
|
1032
|
+
viewport: viewport.name,
|
|
1033
|
+
frame_count: frames.length,
|
|
1034
|
+
matched_count: matches.length,
|
|
1035
|
+
matched: matches.length > 0,
|
|
1036
|
+
urls: urls.slice(0, 10)
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
1040
|
+
return {
|
|
1041
|
+
type: check.type,
|
|
1042
|
+
label: checkLabel(check),
|
|
1043
|
+
status: failed ? "failed" : "passed",
|
|
1044
|
+
evidence: {
|
|
1045
|
+
selector: key,
|
|
1046
|
+
expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
|
|
1047
|
+
pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
|
|
1048
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
1049
|
+
},
|
|
1050
|
+
message: failed ? `Frame selector ${key} URL assertion failed in ${failed} viewport(s).` : void 0
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1007
1053
|
if (check.type === "frame_no_horizontal_overflow") {
|
|
1008
1054
|
const key = selectorKey(check);
|
|
1009
1055
|
const maxOverflow = check.max_overflow_px ?? 4;
|
|
@@ -1174,6 +1220,8 @@ function assessSetupActionsFromEvidence(profile, evidence) {
|
|
|
1174
1220
|
viewport: viewport.name,
|
|
1175
1221
|
action: result.action ?? result.type ?? null,
|
|
1176
1222
|
selector: result.selector ?? null,
|
|
1223
|
+
frame_selector: result.frame_selector ?? null,
|
|
1224
|
+
frame_index: result.frame_index ?? null,
|
|
1177
1225
|
reason: result.reason ?? result.error ?? null
|
|
1178
1226
|
});
|
|
1179
1227
|
}
|
|
@@ -1686,10 +1734,12 @@ function assessProfile(profile, evidence) {
|
|
|
1686
1734
|
if (result && result.ok === false) {
|
|
1687
1735
|
failed.push({
|
|
1688
1736
|
viewport: viewport.name,
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1737
|
+
action: result.action || result.type || null,
|
|
1738
|
+
selector: result.selector || null,
|
|
1739
|
+
frame_selector: result.frame_selector || null,
|
|
1740
|
+
frame_index: result.frame_index ?? null,
|
|
1741
|
+
reason: result.reason || result.error || null,
|
|
1742
|
+
});
|
|
1693
1743
|
}
|
|
1694
1744
|
}
|
|
1695
1745
|
if (results.length < actionCount && results.every((result) => !result || result.ok !== false)) {
|
|
@@ -1891,6 +1941,36 @@ function assessProfile(profile, evidence) {
|
|
|
1891
1941
|
});
|
|
1892
1942
|
continue;
|
|
1893
1943
|
}
|
|
1944
|
+
if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
|
|
1945
|
+
const selector = check.selector || "";
|
|
1946
|
+
const expectedUrl = check.expected_url || check.expected_value || "";
|
|
1947
|
+
const results = checkViewports.map((viewport) => {
|
|
1948
|
+
const frames = frameEvidenceForSelector(viewport, selector);
|
|
1949
|
+
const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
|
|
1950
|
+
const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : textMatches(url, check));
|
|
1951
|
+
return {
|
|
1952
|
+
viewport: viewport.name,
|
|
1953
|
+
frame_count: frames.length,
|
|
1954
|
+
matched_count: matches.length,
|
|
1955
|
+
matched: matches.length > 0,
|
|
1956
|
+
urls: urls.slice(0, 10),
|
|
1957
|
+
};
|
|
1958
|
+
});
|
|
1959
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
1960
|
+
checks.push({
|
|
1961
|
+
type: check.type,
|
|
1962
|
+
label: check.label || check.type,
|
|
1963
|
+
status: failed ? "failed" : "passed",
|
|
1964
|
+
evidence: {
|
|
1965
|
+
selector,
|
|
1966
|
+
expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
|
|
1967
|
+
pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
|
|
1968
|
+
viewports: results,
|
|
1969
|
+
},
|
|
1970
|
+
message: failed ? "Frame selector " + selector + " URL assertion failed in " + failed + " viewport(s)." : undefined,
|
|
1971
|
+
});
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1894
1974
|
if (check.type === "frame_no_horizontal_overflow") {
|
|
1895
1975
|
const selector = check.selector || "";
|
|
1896
1976
|
const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
|
|
@@ -2152,8 +2232,8 @@ function setupJsonValue(value) {
|
|
|
2152
2232
|
function setupValuesEqual(left, right) {
|
|
2153
2233
|
return JSON.stringify(setupJsonValue(left)) === JSON.stringify(setupJsonValue(right));
|
|
2154
2234
|
}
|
|
2155
|
-
async function setupReadWindowValue(path) {
|
|
2156
|
-
return await
|
|
2235
|
+
async function setupReadWindowValue(context, path) {
|
|
2236
|
+
return await context.evaluate(({ path }) => {
|
|
2157
2237
|
const toJsonValue = (value) => {
|
|
2158
2238
|
if (value === null || value === undefined) return null;
|
|
2159
2239
|
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -2175,6 +2255,80 @@ async function setupReadWindowValue(path) {
|
|
|
2175
2255
|
return { ok: true, value: toJsonValue(current) };
|
|
2176
2256
|
}, { path });
|
|
2177
2257
|
}
|
|
2258
|
+
function setupFrameSelector(action) {
|
|
2259
|
+
return String(action?.frame_selector || action?.frameSelector || action?.iframe_selector || action?.iframeSelector || "").trim();
|
|
2260
|
+
}
|
|
2261
|
+
function setupFrameIndex(action) {
|
|
2262
|
+
const raw = action?.frame_index ?? action?.frameIndex ?? action?.iframe_index ?? action?.iframeIndex ?? 0;
|
|
2263
|
+
const number = Number(raw);
|
|
2264
|
+
return Number.isInteger(number) && number >= 0 ? number : 0;
|
|
2265
|
+
}
|
|
2266
|
+
function setupScopeEvidence(scope) {
|
|
2267
|
+
if (!scope || !scope.frame_selector) return {};
|
|
2268
|
+
return {
|
|
2269
|
+
frame_selector: scope.frame_selector,
|
|
2270
|
+
frame_index: scope.frame_index,
|
|
2271
|
+
frame_count: scope.frame_count,
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
function setupScopeFailure(base, scope) {
|
|
2275
|
+
return {
|
|
2276
|
+
...base,
|
|
2277
|
+
...setupScopeEvidence(scope),
|
|
2278
|
+
reason: scope?.reason || "frame_scope_unavailable",
|
|
2279
|
+
error: scope?.error || undefined,
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
async function setupActionScope(action, timeout) {
|
|
2283
|
+
const frameSelector = setupFrameSelector(action);
|
|
2284
|
+
if (!frameSelector) return { ok: true, context: page };
|
|
2285
|
+
const frameIndex = setupFrameIndex(action);
|
|
2286
|
+
let frameCount = 0;
|
|
2287
|
+
let locator = null;
|
|
2288
|
+
try {
|
|
2289
|
+
await page.waitForSelector(frameSelector, { state: "attached", timeout });
|
|
2290
|
+
locator = page.locator(frameSelector);
|
|
2291
|
+
frameCount = await locator.count();
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
return {
|
|
2294
|
+
ok: false,
|
|
2295
|
+
reason: "frame_selector_not_found",
|
|
2296
|
+
frame_selector: frameSelector,
|
|
2297
|
+
frame_index: frameIndex,
|
|
2298
|
+
frame_count: frameCount,
|
|
2299
|
+
error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
if (!frameCount) {
|
|
2303
|
+
return { ok: false, reason: "frame_selector_not_found", frame_selector: frameSelector, frame_index: frameIndex, frame_count: frameCount };
|
|
2304
|
+
}
|
|
2305
|
+
if (frameIndex >= frameCount) {
|
|
2306
|
+
return { ok: false, reason: "frame_index_out_of_range", frame_selector: frameSelector, frame_index: frameIndex, frame_count: frameCount };
|
|
2307
|
+
}
|
|
2308
|
+
const handle = await locator.nth(frameIndex).elementHandle({ timeout }).catch((error) => ({ __riddle_error: error }));
|
|
2309
|
+
if (!handle || handle.__riddle_error) {
|
|
2310
|
+
return {
|
|
2311
|
+
ok: false,
|
|
2312
|
+
reason: "frame_element_unavailable",
|
|
2313
|
+
frame_selector: frameSelector,
|
|
2314
|
+
frame_index: frameIndex,
|
|
2315
|
+
frame_count: frameCount,
|
|
2316
|
+
error: handle?.__riddle_error ? String(handle.__riddle_error && handle.__riddle_error.message ? handle.__riddle_error.message : handle.__riddle_error).slice(0, 1000) : undefined,
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
const frame = typeof handle.contentFrame === "function" ? await handle.contentFrame().catch((error) => ({ __riddle_error: error })) : null;
|
|
2320
|
+
if (!frame || frame.__riddle_error) {
|
|
2321
|
+
return {
|
|
2322
|
+
ok: false,
|
|
2323
|
+
reason: "content_frame_unavailable",
|
|
2324
|
+
frame_selector: frameSelector,
|
|
2325
|
+
frame_index: frameIndex,
|
|
2326
|
+
frame_count: frameCount,
|
|
2327
|
+
error: frame?.__riddle_error ? String(frame.__riddle_error && frame.__riddle_error.message ? frame.__riddle_error.message : frame.__riddle_error).slice(0, 1000) : undefined,
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
return { ok: true, context: frame, frame_selector: frameSelector, frame_index: frameIndex, frame_count: frameCount };
|
|
2331
|
+
}
|
|
2178
2332
|
async function setupLocatorText(locator, index) {
|
|
2179
2333
|
return await locator.nth(index).textContent({ timeout: 1000 }).catch(() => "");
|
|
2180
2334
|
}
|
|
@@ -2336,7 +2490,8 @@ async function registerNetworkMocks(mocks) {
|
|
|
2336
2490
|
}
|
|
2337
2491
|
async function executeSetupAction(action, ordinal) {
|
|
2338
2492
|
const type = setupActionType(action);
|
|
2339
|
-
const
|
|
2493
|
+
const frameSelector = setupFrameSelector(action);
|
|
2494
|
+
const base = { ok: false, action: type || "unknown", ordinal, selector: action.selector || null, frame_selector: frameSelector || null };
|
|
2340
2495
|
const timeout = setupNumber(action.timeout_ms, 5000);
|
|
2341
2496
|
try {
|
|
2342
2497
|
if (type === "wait") {
|
|
@@ -2345,51 +2500,65 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2345
2500
|
return { ...base, ok: true, ms };
|
|
2346
2501
|
}
|
|
2347
2502
|
if (type === "wait_for_selector") {
|
|
2348
|
-
await
|
|
2349
|
-
return
|
|
2503
|
+
const scope = await setupActionScope(action, timeout);
|
|
2504
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2505
|
+
await scope.context.waitForSelector(action.selector, { state: "visible", timeout });
|
|
2506
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
|
|
2350
2507
|
}
|
|
2351
2508
|
if (type === "press") {
|
|
2352
2509
|
const key = String(action.key || "").trim();
|
|
2353
2510
|
if (!key) return { ...base, reason: "missing_key" };
|
|
2511
|
+
const scope = await setupActionScope(action, timeout);
|
|
2512
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2354
2513
|
if (!action.selector) {
|
|
2355
|
-
|
|
2356
|
-
|
|
2514
|
+
if (scope.frame_selector) {
|
|
2515
|
+
await scope.context.locator("body").press(key, { timeout });
|
|
2516
|
+
} else {
|
|
2517
|
+
await page.keyboard.press(key);
|
|
2518
|
+
}
|
|
2519
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, key };
|
|
2357
2520
|
}
|
|
2358
|
-
const locator =
|
|
2521
|
+
const locator = scope.context.locator(action.selector);
|
|
2359
2522
|
const count = await locator.count();
|
|
2360
2523
|
if (!count) return { ...base, reason: "selector_not_found", count, key };
|
|
2361
2524
|
const targetIndex = Number.isInteger(action.index) ? action.index : 0;
|
|
2362
2525
|
if (targetIndex < 0 || targetIndex >= count) return { ...base, reason: "index_out_of_range", count, target_index: targetIndex, key };
|
|
2363
2526
|
await locator.nth(targetIndex).press(key, { timeout });
|
|
2364
|
-
return { ...base, ok: true, count, target_index: targetIndex, key };
|
|
2527
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, count, target_index: targetIndex, key };
|
|
2365
2528
|
}
|
|
2366
2529
|
if (type === "local_storage" || type === "session_storage") {
|
|
2367
2530
|
const value = setupActionValue(action);
|
|
2368
|
-
await
|
|
2531
|
+
const scope = await setupActionScope(action, timeout);
|
|
2532
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2533
|
+
await scope.context.evaluate(({ type, key, value }) => {
|
|
2369
2534
|
const storage = type === "session_storage" ? window.sessionStorage : window.localStorage;
|
|
2370
2535
|
storage.setItem(key, value);
|
|
2371
2536
|
}, { type, key: action.key, value });
|
|
2372
2537
|
if (action.reload === true) {
|
|
2373
2538
|
await page.reload({ waitUntil: "domcontentloaded", timeout: 45000 });
|
|
2374
2539
|
}
|
|
2375
|
-
return { ...base, ok: true, key: action.key, value_length: value.length, reload: action.reload === true };
|
|
2540
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, key: action.key, value_length: value.length, reload: action.reload === true };
|
|
2376
2541
|
}
|
|
2377
2542
|
if (type === "clear_storage") {
|
|
2378
2543
|
const storage = action.storage || "both";
|
|
2379
|
-
await
|
|
2544
|
+
const scope = await setupActionScope(action, timeout);
|
|
2545
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2546
|
+
await scope.context.evaluate(({ storage }) => {
|
|
2380
2547
|
if (storage === "local" || storage === "both") window.localStorage.clear();
|
|
2381
2548
|
if (storage === "session" || storage === "both") window.sessionStorage.clear();
|
|
2382
2549
|
}, { storage });
|
|
2383
2550
|
if (action.reload === true) {
|
|
2384
2551
|
await page.reload({ waitUntil: "domcontentloaded", timeout: 45000 });
|
|
2385
2552
|
}
|
|
2386
|
-
return { ...base, ok: true, storage, reload: action.reload === true };
|
|
2553
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, storage, reload: action.reload === true };
|
|
2387
2554
|
}
|
|
2388
2555
|
if (type === "window_call") {
|
|
2389
2556
|
const path = String(action.path || action.function_path || action.functionPath || "");
|
|
2390
2557
|
const args = Array.isArray(action.args) ? action.args : [];
|
|
2391
2558
|
if (!path) return { ...base, path, reason: "missing_path" };
|
|
2392
|
-
const
|
|
2559
|
+
const scope = await setupActionScope(action, timeout);
|
|
2560
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2561
|
+
const result = await scope.context.evaluate(async ({ path, args }) => {
|
|
2393
2562
|
const toJsonValue = (value) => {
|
|
2394
2563
|
if (value === null || value === undefined) return null;
|
|
2395
2564
|
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -2429,6 +2598,7 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2429
2598
|
const expectationMet = !hasExpectation || setupValuesEqual(result.returned, expected);
|
|
2430
2599
|
return {
|
|
2431
2600
|
...base,
|
|
2601
|
+
...setupScopeEvidence(scope),
|
|
2432
2602
|
ok: Boolean(result.ok && expectationMet),
|
|
2433
2603
|
path,
|
|
2434
2604
|
arg_count: args.length,
|
|
@@ -2459,13 +2629,16 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2459
2629
|
: action.expect;
|
|
2460
2630
|
if (!path) return { ...base, path, reason: "missing_path" };
|
|
2461
2631
|
if (!hasExpected) return { ...base, path, reason: "missing_expected_value" };
|
|
2632
|
+
const scope = await setupActionScope(action, timeout);
|
|
2633
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2462
2634
|
const startedAt = Date.now();
|
|
2463
2635
|
let result = null;
|
|
2464
2636
|
while (Date.now() - startedAt <= timeout) {
|
|
2465
|
-
result = await setupReadWindowValue(path);
|
|
2637
|
+
result = await setupReadWindowValue(scope.context, path);
|
|
2466
2638
|
if (result.ok && setupValuesEqual(result.value, expected)) {
|
|
2467
2639
|
return {
|
|
2468
2640
|
...base,
|
|
2641
|
+
...setupScopeEvidence(scope),
|
|
2469
2642
|
ok: true,
|
|
2470
2643
|
path,
|
|
2471
2644
|
value: setupJsonValue(result.value),
|
|
@@ -2477,6 +2650,7 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2477
2650
|
}
|
|
2478
2651
|
return {
|
|
2479
2652
|
...base,
|
|
2653
|
+
...setupScopeEvidence(scope),
|
|
2480
2654
|
path,
|
|
2481
2655
|
reason: result?.ok ? "unexpected_value" : result?.reason || "path_not_found",
|
|
2482
2656
|
missing_part: result?.missing_part || undefined,
|
|
@@ -2493,11 +2667,13 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2493
2667
|
const hasExpected = expected !== undefined;
|
|
2494
2668
|
if (!path) return { ...base, path, reason: "missing_path" };
|
|
2495
2669
|
if (!hasExpected && minValue === undefined && maxValue === undefined) return { ...base, path, reason: "missing_number_expectation" };
|
|
2670
|
+
const scope = await setupActionScope(action, timeout);
|
|
2671
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2496
2672
|
const startedAt = Date.now();
|
|
2497
2673
|
let result = null;
|
|
2498
2674
|
let lastReason = "path_not_found";
|
|
2499
2675
|
while (Date.now() - startedAt <= timeout) {
|
|
2500
|
-
result = await setupReadWindowValue(path);
|
|
2676
|
+
result = await setupReadWindowValue(scope.context, path);
|
|
2501
2677
|
if (result.ok) {
|
|
2502
2678
|
const actual = setupFiniteNumber(result.value);
|
|
2503
2679
|
if (actual === undefined) {
|
|
@@ -2511,6 +2687,7 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2511
2687
|
} else {
|
|
2512
2688
|
return {
|
|
2513
2689
|
...base,
|
|
2690
|
+
...setupScopeEvidence(scope),
|
|
2514
2691
|
ok: true,
|
|
2515
2692
|
path,
|
|
2516
2693
|
value: actual,
|
|
@@ -2527,6 +2704,7 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2527
2704
|
}
|
|
2528
2705
|
return {
|
|
2529
2706
|
...base,
|
|
2707
|
+
...setupScopeEvidence(scope),
|
|
2530
2708
|
path,
|
|
2531
2709
|
reason: lastReason,
|
|
2532
2710
|
missing_part: result?.missing_part || undefined,
|
|
@@ -2538,7 +2716,9 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2538
2716
|
};
|
|
2539
2717
|
}
|
|
2540
2718
|
if (type === "click") {
|
|
2541
|
-
const
|
|
2719
|
+
const scope = await setupActionScope(action, timeout);
|
|
2720
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2721
|
+
const locator = scope.context.locator(action.selector);
|
|
2542
2722
|
const count = await locator.count();
|
|
2543
2723
|
if (!count) return { ...base, reason: "selector_not_found", count };
|
|
2544
2724
|
let targetIndex = Number.isInteger(action.index) ? action.index : 0;
|
|
@@ -2570,33 +2750,39 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2570
2750
|
? { timeout, noWaitAfter: true, force: true }
|
|
2571
2751
|
: { timeout, noWaitAfter: true };
|
|
2572
2752
|
await locator.nth(targetIndex).click(clickOptions);
|
|
2573
|
-
return { ...base, ok: true, count, target_index: targetIndex, text: matchedText, force: action.force === true || undefined };
|
|
2753
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, count, target_index: targetIndex, text: matchedText, force: action.force === true || undefined };
|
|
2574
2754
|
}
|
|
2575
2755
|
if (type === "fill" || type === "set_input_value") {
|
|
2576
|
-
const
|
|
2756
|
+
const scope = await setupActionScope(action, timeout);
|
|
2757
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2758
|
+
const locator = scope.context.locator(action.selector);
|
|
2577
2759
|
const count = await locator.count();
|
|
2578
2760
|
if (!count) return { ...base, reason: "selector_not_found", count };
|
|
2579
2761
|
const targetIndex = Number.isInteger(action.index) ? action.index : 0;
|
|
2580
2762
|
if (targetIndex < 0 || targetIndex >= count) return { ...base, reason: "index_out_of_range", count, target_index: targetIndex };
|
|
2581
2763
|
const value = setupActionValue(action);
|
|
2582
2764
|
await locator.nth(targetIndex).fill(value, { timeout });
|
|
2583
|
-
return { ...base, ok: true, count, target_index: targetIndex, value_length: value.length };
|
|
2765
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, count, target_index: targetIndex, value_length: value.length };
|
|
2584
2766
|
}
|
|
2585
2767
|
if (type === "assert_selector_count") {
|
|
2586
|
-
const
|
|
2768
|
+
const scope = await setupActionScope(action, timeout);
|
|
2769
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2770
|
+
const locator = scope.context.locator(action.selector);
|
|
2587
2771
|
const expectedCount = setupNumber(action.expected_count, -1);
|
|
2588
2772
|
if (!Number.isInteger(expectedCount) || expectedCount < 0) return { ...base, reason: "invalid_expected_count", expected_count: action.expected_count };
|
|
2589
2773
|
const startedAt = Date.now();
|
|
2590
2774
|
let count = 0;
|
|
2591
2775
|
while (Date.now() - startedAt <= timeout) {
|
|
2592
2776
|
count = await locator.count().catch(() => 0);
|
|
2593
|
-
if (count === expectedCount) return { ...base, ok: true, count, expected_count: expectedCount, timeout_ms: timeout };
|
|
2777
|
+
if (count === expectedCount) return { ...base, ...setupScopeEvidence(scope), ok: true, count, expected_count: expectedCount, timeout_ms: timeout };
|
|
2594
2778
|
await page.waitForTimeout(100);
|
|
2595
2779
|
}
|
|
2596
|
-
return { ...base, reason: "selector_count_mismatch", count, expected_count: expectedCount, timeout_ms: timeout };
|
|
2780
|
+
return { ...base, ...setupScopeEvidence(scope), reason: "selector_count_mismatch", count, expected_count: expectedCount, timeout_ms: timeout };
|
|
2597
2781
|
}
|
|
2598
2782
|
if (type === "wait_for_text") {
|
|
2599
|
-
const
|
|
2783
|
+
const scope = await setupActionScope(action, timeout);
|
|
2784
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2785
|
+
const locator = scope.context.locator(action.selector);
|
|
2600
2786
|
const startedAt = Date.now();
|
|
2601
2787
|
let lastText = "";
|
|
2602
2788
|
while (Date.now() - startedAt <= timeout) {
|
|
@@ -2605,15 +2791,17 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2605
2791
|
const text = await setupLocatorText(locator, index);
|
|
2606
2792
|
lastText = text || lastText;
|
|
2607
2793
|
if (setupTextMatches(text, action)) {
|
|
2608
|
-
return { ...base, ok: true, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
|
|
2794
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
|
|
2609
2795
|
}
|
|
2610
2796
|
}
|
|
2611
2797
|
await page.waitForTimeout(100);
|
|
2612
2798
|
}
|
|
2613
|
-
return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
|
|
2799
|
+
return { ...base, ...setupScopeEvidence(scope), reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
|
|
2614
2800
|
}
|
|
2615
2801
|
if (type === "assert_text_visible" || type === "assert_text_absent") {
|
|
2616
|
-
const
|
|
2802
|
+
const scope = await setupActionScope(action, timeout);
|
|
2803
|
+
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
2804
|
+
const locator = scope.context.locator(action.selector);
|
|
2617
2805
|
const startedAt = Date.now();
|
|
2618
2806
|
let lastText = "";
|
|
2619
2807
|
let matchedText = "";
|
|
@@ -2632,7 +2820,7 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2632
2820
|
if (type === "assert_text_visible") {
|
|
2633
2821
|
const visible = await setupLocatorVisible(locator, index);
|
|
2634
2822
|
if (visible) {
|
|
2635
|
-
return { ...base, ok: true, count, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
|
|
2823
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, count, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
|
|
2636
2824
|
}
|
|
2637
2825
|
hiddenMatch = true;
|
|
2638
2826
|
break;
|
|
@@ -2641,17 +2829,17 @@ async function executeSetupAction(action, ordinal) {
|
|
|
2641
2829
|
}
|
|
2642
2830
|
}
|
|
2643
2831
|
if (type === "assert_text_absent" && !matched) {
|
|
2644
|
-
return { ...base, ok: true, count, timeout_ms: timeout };
|
|
2832
|
+
return { ...base, ...setupScopeEvidence(scope), ok: true, count, timeout_ms: timeout };
|
|
2645
2833
|
}
|
|
2646
2834
|
await page.waitForTimeout(100);
|
|
2647
2835
|
}
|
|
2648
2836
|
if (type === "assert_text_visible") {
|
|
2649
2837
|
if (hiddenMatch) {
|
|
2650
|
-
return { ...base, reason: "matching_element_not_visible", text: compactSetupResultText(matchedText), timeout_ms: timeout };
|
|
2838
|
+
return { ...base, ...setupScopeEvidence(scope), reason: "matching_element_not_visible", text: compactSetupResultText(matchedText), timeout_ms: timeout };
|
|
2651
2839
|
}
|
|
2652
|
-
return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
|
|
2840
|
+
return { ...base, ...setupScopeEvidence(scope), reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
|
|
2653
2841
|
}
|
|
2654
|
-
return { ...base, reason: "text_still_present", text: compactSetupResultText(matchedText || lastText), timeout_ms: timeout };
|
|
2842
|
+
return { ...base, ...setupScopeEvidence(scope), reason: "text_still_present", text: compactSetupResultText(matchedText || lastText), timeout_ms: timeout };
|
|
2655
2843
|
}
|
|
2656
2844
|
return { ...base, reason: "unsupported_action" };
|
|
2657
2845
|
} catch (error) {
|
|
@@ -3254,7 +3442,7 @@ async function captureViewport(viewport) {
|
|
|
3254
3442
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
3255
3443
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
3256
3444
|
}
|
|
3257
|
-
if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
|
|
3445
|
+
if ((check.type === "frame_text_visible" || check.type === "frame_url_equals" || check.type === "frame_url_matches" || check.type === "frame_no_horizontal_overflow") && check.selector) {
|
|
3258
3446
|
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
3259
3447
|
frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
|
|
3260
3448
|
}
|