@riddledc/riddle-proof 0.7.59 → 0.7.61

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 CHANGED
@@ -240,7 +240,7 @@ where the second request must carry newer state.
240
240
  `target.setup_actions` is optional. Use it when the meaningful proof surface
241
241
  appears only after a picker, tab, login stub, storage seed, form fill,
242
242
  transport control, or other bounded interaction. Supported setup actions are
243
- `click`, `press`, `fill`, `set_input_value`, `assert_text_visible`,
243
+ `click`, `drag`, `press`, `fill`, `set_input_value`, `assert_text_visible`,
244
244
  `assert_text_absent`, `assert_selector_count`, `assert_window_value`,
245
245
  `assert_window_number`, `local_storage`, `session_storage`, `clear_storage`, `wait`,
246
246
  `wait_for_selector`, `wait_for_text`, and `window_call`; a failed setup action
@@ -253,6 +253,12 @@ stable enough for Playwright's default click actionability checks. Use `press`
253
253
  with a Playwright key name, such as `Enter`, `Space`, or `ArrowLeft`,
254
254
  when a route's intended browser control is keyboard-driven; omit `selector` for
255
255
  a page-level key press, or provide `selector` to press against a focused element.
256
+ Use `drag` for pointer-driven controls such as canvas launch areas, sliders, or
257
+ drag-to-aim games. Provide `selector`, `from_x`, `from_y`, `to_x`, and `to_y`;
258
+ coordinates are element-relative pixels by default. Set `coordinate_mode:
259
+ "ratio"` to make coordinates relative to the target element size, for example
260
+ `from_x: 0.5, from_y: 0.5, to_x: 0.2, to_y: 0.5`. Optional `steps` and
261
+ `duration_ms` control how gradually the pointer moves before release.
256
262
  Use setup assertions when the pre-click or pre-navigation state is part of the contract,
257
263
  for example a fresh row must be present, stale copy must be absent, exactly one
258
264
  source link must exist before clicking into the final route, or a canvas app's
@@ -355,6 +361,11 @@ game, or preview surfaces that render inside iframes:
355
361
 
356
362
  ```json
357
363
  [
364
+ {
365
+ "type": "frame_url_matches",
366
+ "selector": ".game-player-root iframe",
367
+ "pattern": "/saved/hot-path-.+/index\\.html$"
368
+ },
358
369
  {
359
370
  "type": "frame_text_visible",
360
371
  "selector": ".game-player-root iframe",
@@ -368,6 +379,11 @@ game, or preview surfaces that render inside iframes:
368
379
  ]
369
380
  ```
370
381
 
382
+ Use `frame_url_equals` when the iframe must resolve to one exact embedded
383
+ resource, or `frame_url_matches` when a preview/job/saved-game URL has a stable
384
+ shape but a generated ID. URL checks fail when the frame is missing, just like
385
+ frame text and overflow checks.
386
+
371
387
  Frame checks capture each matching iframe's URL, title, compact text sample,
372
388
  scroll width, client width, measured horizontal overflow, and top visible
373
389
  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",
@@ -32,6 +34,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
32
34
  ];
33
35
  var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
34
36
  "click",
37
+ "drag",
35
38
  "press",
36
39
  "fill",
37
40
  "set_input_value",
@@ -202,7 +205,7 @@ function isSupportedCheckType(value) {
202
205
  }
203
206
  function normalizeSetupActionType(value, index) {
204
207
  const normalizedInput = String(value || "").trim().replace(/-/g, "_");
205
- const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput;
208
+ const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "pointer_drag" || normalizedInput === "mouse_drag" || normalizedInput === "drag_to" ? "drag" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput;
206
209
  if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
207
210
  return normalized;
208
211
  }
@@ -232,6 +235,13 @@ function normalizeSetupActionRepeat(input, index) {
232
235
  }
233
236
  return repeat;
234
237
  }
238
+ function normalizeSetupActionCoordinateMode(value, index) {
239
+ if (value === void 0 || value === null || value === "") return void 0;
240
+ const normalized = String(value).trim().replace(/-/g, "_").toLowerCase();
241
+ if (normalized === "pixels" || normalized === "pixel" || normalized === "px") return "pixels";
242
+ if (normalized === "ratio" || normalized === "relative" || normalized === "fraction") return "ratio";
243
+ throw new Error(`target.setup_actions[${index}].coordinate_mode ${String(value)} is not supported. Supported coordinate modes: pixels, ratio.`);
244
+ }
235
245
  function normalizeSetupAction(input, index) {
236
246
  if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
237
247
  const type = normalizeSetupActionType(stringValue(input.type), index);
@@ -241,9 +251,25 @@ function normalizeSetupAction(input, index) {
241
251
  if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
242
252
  throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
243
253
  }
244
- 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) {
254
+ if ((type === "click" || type === "drag" || 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) {
245
255
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
246
256
  }
257
+ const fromX = numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
258
+ const fromY = numberValue(valueFromOwn(input, "from_y", "fromY", "start_y", "startY", "y1"));
259
+ const toX = numberValue(valueFromOwn(input, "to_x", "toX", "end_x", "endX", "x2"));
260
+ const toY = numberValue(valueFromOwn(input, "to_y", "toY", "end_y", "endY", "y2"));
261
+ const coordinateMode = normalizeSetupActionCoordinateMode(valueFromOwn(input, "coordinate_mode", "coordinateMode", "coords", "units"), index);
262
+ if (type === "drag") {
263
+ if (fromX === void 0 || fromY === void 0 || toX === void 0 || toY === void 0) {
264
+ throw new Error(`target.setup_actions[${index}] drag requires from_x, from_y, to_x, and to_y.`);
265
+ }
266
+ if (coordinateMode === "ratio" && [fromX, fromY, toX, toY].some((value2) => value2 < 0 || value2 > 1)) {
267
+ throw new Error(`target.setup_actions[${index}] drag ratio coordinates must be between 0 and 1.`);
268
+ }
269
+ if ((coordinateMode === void 0 || coordinateMode === "pixels") && [fromX, fromY, toX, toY].some((value2) => value2 < 0)) {
270
+ throw new Error(`target.setup_actions[${index}] drag pixel coordinates must be non-negative.`);
271
+ }
272
+ }
247
273
  if (type === "wait_for_text" && !stringValue(input.text) && !stringValue(input.pattern)) {
248
274
  throw new Error(`target.setup_actions[${index}] wait_for_text requires text or pattern.`);
249
275
  }
@@ -290,12 +316,23 @@ function normalizeSetupAction(input, index) {
290
316
  }
291
317
  }
292
318
  const hasExpectedReturn = hasOwn(input, "expect_return") || hasOwn(input, "expectReturn") || hasOwn(input, "expected_return") || hasOwn(input, "expectedReturn");
319
+ const steps = numberValue(input.steps);
320
+ if (type === "drag" && steps !== void 0 && (!Number.isInteger(steps) || steps < 1 || steps > 100)) {
321
+ throw new Error(`target.setup_actions[${index}].steps must be an integer from 1 to 100.`);
322
+ }
293
323
  return {
294
324
  type,
295
325
  selector,
296
326
  frame_selector: frameSelector,
297
327
  frame_index: frameIndex,
298
328
  force: type === "click" && (input.force === true || input.force_click === true || input.forceClick === true),
329
+ coordinate_mode: coordinateMode,
330
+ from_x: fromX,
331
+ from_y: fromY,
332
+ to_x: toX,
333
+ to_y: toY,
334
+ duration_ms: numberValue(input.duration_ms) ?? numberValue(input.durationMs),
335
+ steps,
299
336
  key,
300
337
  value,
301
338
  value_json: hasJsonValue ? toJsonValue(input.value_json ?? input.valueJson ?? input.json) : void 0,
@@ -506,12 +543,19 @@ function normalizeCheck(input, index) {
506
543
  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)) {
507
544
  throw new Error(`checks[${index}] ${type} requires selector.`);
508
545
  }
509
- if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
546
+ if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
510
547
  throw new Error(`checks[${index}] ${type} requires selector.`);
511
548
  }
512
549
  if (type === "frame_text_visible" && !stringValue(input.text) && !stringValue(input.pattern)) {
513
550
  throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
514
551
  }
552
+ const expectedUrl = stringFromOwn(input, "expected_url", "expectedUrl", "url", "expected_value", "expectedValue", "value");
553
+ if (type === "frame_url_equals" && expectedUrl === void 0) {
554
+ throw new Error(`checks[${index}] frame_url_equals requires expected_url.`);
555
+ }
556
+ if (type === "frame_url_matches" && !stringValue(input.pattern)) {
557
+ throw new Error(`checks[${index}] frame_url_matches requires pattern.`);
558
+ }
515
559
  if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
516
560
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
517
561
  }
@@ -544,6 +588,7 @@ function normalizeCheck(input, index) {
544
588
  expected_path: stringValue(input.expected_path),
545
589
  param: stringValue(input.param) || stringValue(input.search_param) || stringValue(input.searchParam) || stringValue(input.key),
546
590
  expected_value: expectedValue,
591
+ expected_url: expectedUrl,
547
592
  expected_routes: expectedRoutes,
548
593
  selector: stringValue(input.selector),
549
594
  expected_texts: expectedTexts,
@@ -1011,6 +1056,35 @@ function assessCheckFromEvidence(check, evidence) {
1011
1056
  message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
1012
1057
  };
1013
1058
  }
1059
+ if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
1060
+ const key = selectorKey(check);
1061
+ const expectedUrl = check.expected_url || check.expected_value || "";
1062
+ const results = viewports.map((viewport) => {
1063
+ const frames = frameEvidenceForSelector(viewport, key);
1064
+ const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
1065
+ const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : matchText(url, check));
1066
+ return {
1067
+ viewport: viewport.name,
1068
+ frame_count: frames.length,
1069
+ matched_count: matches.length,
1070
+ matched: matches.length > 0,
1071
+ urls: urls.slice(0, 10)
1072
+ };
1073
+ });
1074
+ const failed = results.filter((result) => !result.matched).length;
1075
+ return {
1076
+ type: check.type,
1077
+ label: checkLabel(check),
1078
+ status: failed ? "failed" : "passed",
1079
+ evidence: {
1080
+ selector: key,
1081
+ expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
1082
+ pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
1083
+ viewports: results.map((result) => toJsonValue(result))
1084
+ },
1085
+ message: failed ? `Frame selector ${key} URL assertion failed in ${failed} viewport(s).` : void 0
1086
+ };
1087
+ }
1014
1088
  if (check.type === "frame_no_horizontal_overflow") {
1015
1089
  const key = selectorKey(check);
1016
1090
  const maxOverflow = check.max_overflow_px ?? 4;
@@ -1902,6 +1976,36 @@ function assessProfile(profile, evidence) {
1902
1976
  });
1903
1977
  continue;
1904
1978
  }
1979
+ if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
1980
+ const selector = check.selector || "";
1981
+ const expectedUrl = check.expected_url || check.expected_value || "";
1982
+ const results = checkViewports.map((viewport) => {
1983
+ const frames = frameEvidenceForSelector(viewport, selector);
1984
+ const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
1985
+ const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : textMatches(url, check));
1986
+ return {
1987
+ viewport: viewport.name,
1988
+ frame_count: frames.length,
1989
+ matched_count: matches.length,
1990
+ matched: matches.length > 0,
1991
+ urls: urls.slice(0, 10),
1992
+ };
1993
+ });
1994
+ const failed = results.filter((result) => !result.matched).length;
1995
+ checks.push({
1996
+ type: check.type,
1997
+ label: check.label || check.type,
1998
+ status: failed ? "failed" : "passed",
1999
+ evidence: {
2000
+ selector,
2001
+ expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
2002
+ pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
2003
+ viewports: results,
2004
+ },
2005
+ message: failed ? "Frame selector " + selector + " URL assertion failed in " + failed + " viewport(s)." : undefined,
2006
+ });
2007
+ continue;
2008
+ }
1905
2009
  if (check.type === "frame_no_horizontal_overflow") {
1906
2010
  const selector = check.selector || "";
1907
2011
  const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
@@ -2436,6 +2540,71 @@ async function executeSetupAction(action, ordinal) {
2436
2540
  await scope.context.waitForSelector(action.selector, { state: "visible", timeout });
2437
2541
  return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
2438
2542
  }
2543
+ if (type === "drag") {
2544
+ const scope = await setupActionScope(action, timeout);
2545
+ if (!scope.ok) return setupScopeFailure(base, scope);
2546
+ const locator = scope.context.locator(action.selector);
2547
+ const count = await locator.count();
2548
+ if (!count) return { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count };
2549
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
2550
+ if (targetIndex < 0 || targetIndex >= count) return { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex };
2551
+ const target = locator.nth(targetIndex);
2552
+ await target.waitFor({ state: "visible", timeout });
2553
+ const box = await target.boundingBox();
2554
+ if (!box) return { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex };
2555
+ const mode = String(action.coordinate_mode || action.coordinateMode || "pixels").trim();
2556
+ const coordinate = (value, size) => mode === "ratio" ? value * size : value;
2557
+ const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.start_x ?? action.startX ?? action.x1);
2558
+ const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.start_y ?? action.startY ?? action.y1);
2559
+ const toX = setupFiniteNumber(action.to_x ?? action.toX ?? action.end_x ?? action.endX ?? action.x2);
2560
+ const toY = setupFiniteNumber(action.to_y ?? action.toY ?? action.end_y ?? action.endY ?? action.y2);
2561
+ if (fromX === undefined || fromY === undefined || toX === undefined || toY === undefined) return { ...base, ...setupScopeEvidence(scope), reason: "missing_drag_coordinates", count, target_index: targetIndex };
2562
+ if (mode === "ratio" && [fromX, fromY, toX, toY].some((value) => value < 0 || value > 1)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_ratio_coordinates", count, target_index: targetIndex };
2563
+ if (mode !== "ratio" && [fromX, fromY, toX, toY].some((value) => value < 0)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex };
2564
+ const start = {
2565
+ x: box.x + coordinate(fromX, box.width),
2566
+ y: box.y + coordinate(fromY, box.height),
2567
+ };
2568
+ const end = {
2569
+ x: box.x + coordinate(toX, box.width),
2570
+ y: box.y + coordinate(toY, box.height),
2571
+ };
2572
+ const requestedSteps = setupNumber(action.steps, 8);
2573
+ const steps = Math.min(100, Math.max(1, Math.floor(requestedSteps || 8)));
2574
+ const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
2575
+ await page.mouse.move(start.x, start.y);
2576
+ await page.mouse.down();
2577
+ try {
2578
+ if (durationMs && steps > 1) {
2579
+ for (let step = 1; step <= steps; step += 1) {
2580
+ const progress = step / steps;
2581
+ await page.mouse.move(
2582
+ start.x + (end.x - start.x) * progress,
2583
+ start.y + (end.y - start.y) * progress,
2584
+ );
2585
+ await page.waitForTimeout(durationMs / steps);
2586
+ }
2587
+ } else {
2588
+ await page.mouse.move(end.x, end.y, { steps });
2589
+ }
2590
+ } finally {
2591
+ await page.mouse.up().catch(() => {});
2592
+ }
2593
+ return {
2594
+ ...base,
2595
+ ...setupScopeEvidence(scope),
2596
+ ok: true,
2597
+ count,
2598
+ target_index: targetIndex,
2599
+ coordinate_mode: mode,
2600
+ from_x: fromX,
2601
+ from_y: fromY,
2602
+ to_x: toX,
2603
+ to_y: toY,
2604
+ steps,
2605
+ duration_ms: durationMs || undefined,
2606
+ };
2607
+ }
2439
2608
  if (type === "press") {
2440
2609
  const key = String(action.key || "").trim();
2441
2610
  if (!key) return { ...base, reason: "missing_key" };
@@ -3373,7 +3542,7 @@ async function captureViewport(viewport) {
3373
3542
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
3374
3543
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
3375
3544
  }
3376
- if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
3545
+ if ((check.type === "frame_text_visible" || check.type === "frame_url_equals" || check.type === "frame_url_matches" || check.type === "frame_no_horizontal_overflow") && check.selector) {
3377
3546
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
3378
3547
  frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
3379
3548
  }
package/dist/cli.cjs CHANGED
@@ -6895,6 +6895,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6895
6895
  "selector_count_eq",
6896
6896
  "selector_text_order",
6897
6897
  "frame_text_visible",
6898
+ "frame_url_equals",
6899
+ "frame_url_matches",
6898
6900
  "frame_no_horizontal_overflow",
6899
6901
  "text_visible",
6900
6902
  "text_absent",
@@ -6905,6 +6907,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6905
6907
  ];
6906
6908
  var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
6907
6909
  "click",
6910
+ "drag",
6908
6911
  "press",
6909
6912
  "fill",
6910
6913
  "set_input_value",
@@ -7075,7 +7078,7 @@ function isSupportedCheckType(value) {
7075
7078
  }
7076
7079
  function normalizeSetupActionType(value, index) {
7077
7080
  const normalizedInput = String(value || "").trim().replace(/-/g, "_");
7078
- const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput;
7081
+ const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "pointer_drag" || normalizedInput === "mouse_drag" || normalizedInput === "drag_to" ? "drag" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput;
7079
7082
  if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
7080
7083
  return normalized;
7081
7084
  }
@@ -7105,6 +7108,13 @@ function normalizeSetupActionRepeat(input, index) {
7105
7108
  }
7106
7109
  return repeat;
7107
7110
  }
7111
+ function normalizeSetupActionCoordinateMode(value, index) {
7112
+ if (value === void 0 || value === null || value === "") return void 0;
7113
+ const normalized = String(value).trim().replace(/-/g, "_").toLowerCase();
7114
+ if (normalized === "pixels" || normalized === "pixel" || normalized === "px") return "pixels";
7115
+ if (normalized === "ratio" || normalized === "relative" || normalized === "fraction") return "ratio";
7116
+ throw new Error(`target.setup_actions[${index}].coordinate_mode ${String(value)} is not supported. Supported coordinate modes: pixels, ratio.`);
7117
+ }
7108
7118
  function normalizeSetupAction(input, index) {
7109
7119
  if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
7110
7120
  const type = normalizeSetupActionType(stringValue2(input.type), index);
@@ -7114,9 +7124,25 @@ function normalizeSetupAction(input, index) {
7114
7124
  if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
7115
7125
  throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
7116
7126
  }
7117
- 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) {
7127
+ if ((type === "click" || type === "drag" || 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) {
7118
7128
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
7119
7129
  }
7130
+ const fromX = numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
7131
+ const fromY = numberValue(valueFromOwn(input, "from_y", "fromY", "start_y", "startY", "y1"));
7132
+ const toX = numberValue(valueFromOwn(input, "to_x", "toX", "end_x", "endX", "x2"));
7133
+ const toY = numberValue(valueFromOwn(input, "to_y", "toY", "end_y", "endY", "y2"));
7134
+ const coordinateMode = normalizeSetupActionCoordinateMode(valueFromOwn(input, "coordinate_mode", "coordinateMode", "coords", "units"), index);
7135
+ if (type === "drag") {
7136
+ if (fromX === void 0 || fromY === void 0 || toX === void 0 || toY === void 0) {
7137
+ throw new Error(`target.setup_actions[${index}] drag requires from_x, from_y, to_x, and to_y.`);
7138
+ }
7139
+ if (coordinateMode === "ratio" && [fromX, fromY, toX, toY].some((value2) => value2 < 0 || value2 > 1)) {
7140
+ throw new Error(`target.setup_actions[${index}] drag ratio coordinates must be between 0 and 1.`);
7141
+ }
7142
+ if ((coordinateMode === void 0 || coordinateMode === "pixels") && [fromX, fromY, toX, toY].some((value2) => value2 < 0)) {
7143
+ throw new Error(`target.setup_actions[${index}] drag pixel coordinates must be non-negative.`);
7144
+ }
7145
+ }
7120
7146
  if (type === "wait_for_text" && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7121
7147
  throw new Error(`target.setup_actions[${index}] wait_for_text requires text or pattern.`);
7122
7148
  }
@@ -7163,12 +7189,23 @@ function normalizeSetupAction(input, index) {
7163
7189
  }
7164
7190
  }
7165
7191
  const hasExpectedReturn = hasOwn(input, "expect_return") || hasOwn(input, "expectReturn") || hasOwn(input, "expected_return") || hasOwn(input, "expectedReturn");
7192
+ const steps = numberValue(input.steps);
7193
+ if (type === "drag" && steps !== void 0 && (!Number.isInteger(steps) || steps < 1 || steps > 100)) {
7194
+ throw new Error(`target.setup_actions[${index}].steps must be an integer from 1 to 100.`);
7195
+ }
7166
7196
  return {
7167
7197
  type,
7168
7198
  selector,
7169
7199
  frame_selector: frameSelector,
7170
7200
  frame_index: frameIndex,
7171
7201
  force: type === "click" && (input.force === true || input.force_click === true || input.forceClick === true),
7202
+ coordinate_mode: coordinateMode,
7203
+ from_x: fromX,
7204
+ from_y: fromY,
7205
+ to_x: toX,
7206
+ to_y: toY,
7207
+ duration_ms: numberValue(input.duration_ms) ?? numberValue(input.durationMs),
7208
+ steps,
7172
7209
  key,
7173
7210
  value,
7174
7211
  value_json: hasJsonValue ? toJsonValue(input.value_json ?? input.valueJson ?? input.json) : void 0,
@@ -7379,12 +7416,19 @@ function normalizeCheck(input, index) {
7379
7416
  if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue2(input.selector)) {
7380
7417
  throw new Error(`checks[${index}] ${type} requires selector.`);
7381
7418
  }
7382
- if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue2(input.selector)) {
7419
+ if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue2(input.selector)) {
7383
7420
  throw new Error(`checks[${index}] ${type} requires selector.`);
7384
7421
  }
7385
7422
  if (type === "frame_text_visible" && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7386
7423
  throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
7387
7424
  }
7425
+ const expectedUrl = stringFromOwn(input, "expected_url", "expectedUrl", "url", "expected_value", "expectedValue", "value");
7426
+ if (type === "frame_url_equals" && expectedUrl === void 0) {
7427
+ throw new Error(`checks[${index}] frame_url_equals requires expected_url.`);
7428
+ }
7429
+ if (type === "frame_url_matches" && !stringValue2(input.pattern)) {
7430
+ throw new Error(`checks[${index}] frame_url_matches requires pattern.`);
7431
+ }
7388
7432
  if ((type === "text_visible" || type === "text_absent") && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7389
7433
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
7390
7434
  }
@@ -7417,6 +7461,7 @@ function normalizeCheck(input, index) {
7417
7461
  expected_path: stringValue2(input.expected_path),
7418
7462
  param: stringValue2(input.param) || stringValue2(input.search_param) || stringValue2(input.searchParam) || stringValue2(input.key),
7419
7463
  expected_value: expectedValue,
7464
+ expected_url: expectedUrl,
7420
7465
  expected_routes: expectedRoutes,
7421
7466
  selector: stringValue2(input.selector),
7422
7467
  expected_texts: expectedTexts,
@@ -7884,6 +7929,35 @@ function assessCheckFromEvidence(check, evidence) {
7884
7929
  message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
7885
7930
  };
7886
7931
  }
7932
+ if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
7933
+ const key = selectorKey(check);
7934
+ const expectedUrl = check.expected_url || check.expected_value || "";
7935
+ const results = viewports.map((viewport) => {
7936
+ const frames = frameEvidenceForSelector(viewport, key);
7937
+ const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
7938
+ const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : matchText(url, check));
7939
+ return {
7940
+ viewport: viewport.name,
7941
+ frame_count: frames.length,
7942
+ matched_count: matches.length,
7943
+ matched: matches.length > 0,
7944
+ urls: urls.slice(0, 10)
7945
+ };
7946
+ });
7947
+ const failed = results.filter((result) => !result.matched).length;
7948
+ return {
7949
+ type: check.type,
7950
+ label: checkLabel(check),
7951
+ status: failed ? "failed" : "passed",
7952
+ evidence: {
7953
+ selector: key,
7954
+ expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
7955
+ pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
7956
+ viewports: results.map((result) => toJsonValue(result))
7957
+ },
7958
+ message: failed ? `Frame selector ${key} URL assertion failed in ${failed} viewport(s).` : void 0
7959
+ };
7960
+ }
7887
7961
  if (check.type === "frame_no_horizontal_overflow") {
7888
7962
  const key = selectorKey(check);
7889
7963
  const maxOverflow = check.max_overflow_px ?? 4;
@@ -8759,6 +8833,36 @@ function assessProfile(profile, evidence) {
8759
8833
  });
8760
8834
  continue;
8761
8835
  }
8836
+ if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
8837
+ const selector = check.selector || "";
8838
+ const expectedUrl = check.expected_url || check.expected_value || "";
8839
+ const results = checkViewports.map((viewport) => {
8840
+ const frames = frameEvidenceForSelector(viewport, selector);
8841
+ const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
8842
+ const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : textMatches(url, check));
8843
+ return {
8844
+ viewport: viewport.name,
8845
+ frame_count: frames.length,
8846
+ matched_count: matches.length,
8847
+ matched: matches.length > 0,
8848
+ urls: urls.slice(0, 10),
8849
+ };
8850
+ });
8851
+ const failed = results.filter((result) => !result.matched).length;
8852
+ checks.push({
8853
+ type: check.type,
8854
+ label: check.label || check.type,
8855
+ status: failed ? "failed" : "passed",
8856
+ evidence: {
8857
+ selector,
8858
+ expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
8859
+ pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
8860
+ viewports: results,
8861
+ },
8862
+ message: failed ? "Frame selector " + selector + " URL assertion failed in " + failed + " viewport(s)." : undefined,
8863
+ });
8864
+ continue;
8865
+ }
8762
8866
  if (check.type === "frame_no_horizontal_overflow") {
8763
8867
  const selector = check.selector || "";
8764
8868
  const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
@@ -9293,6 +9397,71 @@ async function executeSetupAction(action, ordinal) {
9293
9397
  await scope.context.waitForSelector(action.selector, { state: "visible", timeout });
9294
9398
  return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
9295
9399
  }
9400
+ if (type === "drag") {
9401
+ const scope = await setupActionScope(action, timeout);
9402
+ if (!scope.ok) return setupScopeFailure(base, scope);
9403
+ const locator = scope.context.locator(action.selector);
9404
+ const count = await locator.count();
9405
+ if (!count) return { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count };
9406
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
9407
+ if (targetIndex < 0 || targetIndex >= count) return { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex };
9408
+ const target = locator.nth(targetIndex);
9409
+ await target.waitFor({ state: "visible", timeout });
9410
+ const box = await target.boundingBox();
9411
+ if (!box) return { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex };
9412
+ const mode = String(action.coordinate_mode || action.coordinateMode || "pixels").trim();
9413
+ const coordinate = (value, size) => mode === "ratio" ? value * size : value;
9414
+ const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.start_x ?? action.startX ?? action.x1);
9415
+ const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.start_y ?? action.startY ?? action.y1);
9416
+ const toX = setupFiniteNumber(action.to_x ?? action.toX ?? action.end_x ?? action.endX ?? action.x2);
9417
+ const toY = setupFiniteNumber(action.to_y ?? action.toY ?? action.end_y ?? action.endY ?? action.y2);
9418
+ if (fromX === undefined || fromY === undefined || toX === undefined || toY === undefined) return { ...base, ...setupScopeEvidence(scope), reason: "missing_drag_coordinates", count, target_index: targetIndex };
9419
+ if (mode === "ratio" && [fromX, fromY, toX, toY].some((value) => value < 0 || value > 1)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_ratio_coordinates", count, target_index: targetIndex };
9420
+ if (mode !== "ratio" && [fromX, fromY, toX, toY].some((value) => value < 0)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex };
9421
+ const start = {
9422
+ x: box.x + coordinate(fromX, box.width),
9423
+ y: box.y + coordinate(fromY, box.height),
9424
+ };
9425
+ const end = {
9426
+ x: box.x + coordinate(toX, box.width),
9427
+ y: box.y + coordinate(toY, box.height),
9428
+ };
9429
+ const requestedSteps = setupNumber(action.steps, 8);
9430
+ const steps = Math.min(100, Math.max(1, Math.floor(requestedSteps || 8)));
9431
+ const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
9432
+ await page.mouse.move(start.x, start.y);
9433
+ await page.mouse.down();
9434
+ try {
9435
+ if (durationMs && steps > 1) {
9436
+ for (let step = 1; step <= steps; step += 1) {
9437
+ const progress = step / steps;
9438
+ await page.mouse.move(
9439
+ start.x + (end.x - start.x) * progress,
9440
+ start.y + (end.y - start.y) * progress,
9441
+ );
9442
+ await page.waitForTimeout(durationMs / steps);
9443
+ }
9444
+ } else {
9445
+ await page.mouse.move(end.x, end.y, { steps });
9446
+ }
9447
+ } finally {
9448
+ await page.mouse.up().catch(() => {});
9449
+ }
9450
+ return {
9451
+ ...base,
9452
+ ...setupScopeEvidence(scope),
9453
+ ok: true,
9454
+ count,
9455
+ target_index: targetIndex,
9456
+ coordinate_mode: mode,
9457
+ from_x: fromX,
9458
+ from_y: fromY,
9459
+ to_x: toX,
9460
+ to_y: toY,
9461
+ steps,
9462
+ duration_ms: durationMs || undefined,
9463
+ };
9464
+ }
9296
9465
  if (type === "press") {
9297
9466
  const key = String(action.key || "").trim();
9298
9467
  if (!key) return { ...base, reason: "missing_key" };
@@ -10230,7 +10399,7 @@ async function captureViewport(viewport) {
10230
10399
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
10231
10400
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
10232
10401
  }
10233
- if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
10402
+ if ((check.type === "frame_text_visible" || check.type === "frame_url_equals" || check.type === "frame_url_matches" || check.type === "frame_no_horizontal_overflow") && check.selector) {
10234
10403
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
10235
10404
  frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
10236
10405
  }
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-Q5VW6MAP.js";
13
+ } from "./chunk-T66DWLBE.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport