@riddledc/riddle-proof 0.7.26 → 0.7.28

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.
@@ -22,6 +22,9 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
22
22
  ];
23
23
  var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
24
24
  "click",
25
+ "fill",
26
+ "set_input_value",
27
+ "local_storage",
25
28
  "wait",
26
29
  "wait_for_selector",
27
30
  "wait_for_text"
@@ -35,6 +38,18 @@ function isRecord(value) {
35
38
  function stringValue(value) {
36
39
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
37
40
  }
41
+ function hasOwn(value, key) {
42
+ return Object.prototype.hasOwnProperty.call(value, key);
43
+ }
44
+ function stringFromOwn(input, ...keys) {
45
+ for (const key of keys) {
46
+ if (hasOwn(input, key)) {
47
+ const value = input[key];
48
+ if (value !== void 0 && value !== null) return String(value);
49
+ }
50
+ }
51
+ return void 0;
52
+ }
38
53
  function numberValue(value) {
39
54
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
40
55
  }
@@ -119,6 +134,11 @@ function jsonRecord(value) {
119
134
  if (!isRecord(value)) return void 0;
120
135
  return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
121
136
  }
137
+ function stringRecord(value) {
138
+ if (!isRecord(value)) return void 0;
139
+ const entries = Object.entries(value).map(([key, child]) => [key, String(child)]).filter(([key]) => key.trim());
140
+ return entries.length ? Object.fromEntries(entries) : void 0;
141
+ }
122
142
  function toJsonValue(value) {
123
143
  if (value === null || value === void 0) return null;
124
144
  if (typeof value === "string" || typeof value === "boolean") return value;
@@ -166,15 +186,30 @@ function normalizeSetupAction(input, index) {
166
186
  if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
167
187
  const type = normalizeSetupActionType(stringValue(input.type), index);
168
188
  const selector = stringValue(input.selector);
169
- if ((type === "click" || type === "wait_for_selector" || type === "wait_for_text") && !selector) {
189
+ if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text") && !selector) {
170
190
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
171
191
  }
172
192
  if (type === "wait_for_text" && !stringValue(input.text) && !stringValue(input.pattern)) {
173
193
  throw new Error(`target.setup_actions[${index}] wait_for_text requires text or pattern.`);
174
194
  }
195
+ const value = stringFromOwn(input, "value", "input_value", "inputValue");
196
+ const hasJsonValue = hasOwn(input, "value_json") || hasOwn(input, "valueJson") || hasOwn(input, "json");
197
+ if ((type === "fill" || type === "set_input_value") && value === void 0 && !hasJsonValue) {
198
+ throw new Error(`target.setup_actions[${index}] ${type} requires value.`);
199
+ }
200
+ const key = stringValue(input.key);
201
+ if (type === "local_storage" && !key) {
202
+ throw new Error(`target.setup_actions[${index}] local_storage requires key.`);
203
+ }
204
+ if (type === "local_storage" && value === void 0 && !hasJsonValue) {
205
+ throw new Error(`target.setup_actions[${index}] local_storage requires value.`);
206
+ }
175
207
  return {
176
208
  type,
177
209
  selector,
210
+ key,
211
+ value,
212
+ value_json: hasJsonValue ? toJsonValue(input.value_json ?? input.valueJson ?? input.json) : void 0,
178
213
  text: stringValue(input.text),
179
214
  pattern: stringValue(input.pattern),
180
215
  flags: stringValue(input.flags),
@@ -182,6 +217,7 @@ function normalizeSetupAction(input, index) {
182
217
  ms: numberValue(input.ms) ?? numberValue(input.wait_ms) ?? numberValue(input.waitMs),
183
218
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
184
219
  after_ms: numberValue(input.after_ms) ?? numberValue(input.afterMs),
220
+ reload: input.reload === true,
185
221
  continue_on_failure: input.continue_on_failure === true || input.continueOnFailure === true
186
222
  };
187
223
  }
@@ -190,6 +226,33 @@ function normalizeSetupActions(value) {
190
226
  if (!Array.isArray(value)) throw new Error("target.setup_actions must be an array.");
191
227
  return value.map(normalizeSetupAction);
192
228
  }
229
+ function normalizeNetworkMock(input, index) {
230
+ if (!isRecord(input)) throw new Error(`target.network_mocks[${index}] must be an object.`);
231
+ const url = stringValue(input.url) || stringValue(input.glob) || stringValue(input.pattern);
232
+ if (!url) throw new Error(`target.network_mocks[${index}] requires url.`);
233
+ const status = numberValue(input.status) ?? 200;
234
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
235
+ throw new Error(`target.network_mocks[${index}].status must be an HTTP status code.`);
236
+ }
237
+ const body = stringValue(input.body) ?? stringValue(input.body_text) ?? stringValue(input.bodyText);
238
+ const hasJsonBody = Object.prototype.hasOwnProperty.call(input, "body_json") || Object.prototype.hasOwnProperty.call(input, "bodyJson") || Object.prototype.hasOwnProperty.call(input, "json");
239
+ return {
240
+ label: normalizeName(input.label || input.name, `network-mock-${index + 1}`),
241
+ url,
242
+ method: stringValue(input.method)?.toUpperCase(),
243
+ status,
244
+ content_type: stringValue(input.content_type) || stringValue(input.contentType),
245
+ headers: stringRecord(input.headers),
246
+ body,
247
+ body_json: hasJsonBody ? toJsonValue(input.body_json ?? input.bodyJson ?? input.json) : void 0,
248
+ required: input.required === false ? false : true
249
+ };
250
+ }
251
+ function normalizeNetworkMocks(value) {
252
+ if (value === void 0) return void 0;
253
+ if (!Array.isArray(value)) throw new Error("target.network_mocks must be an array.");
254
+ return value.map(normalizeNetworkMock);
255
+ }
193
256
  function normalizeCheck(input, index) {
194
257
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
195
258
  const type = stringValue(input.type);
@@ -259,7 +322,8 @@ function normalizeRiddleProofProfile(input, options = {}) {
259
322
  timeout_sec: timeoutSecValue(targetInput.timeout_sec) ?? timeoutSecValue(targetInput.timeoutSec) ?? timeoutSecValue(targetInput.riddle_timeout_sec) ?? timeoutSecValue(targetInput.riddleTimeoutSec),
260
323
  wait_for_selector: stringValue(targetInput.wait_for_selector) || stringValue(targetInput.waitForSelector),
261
324
  wait_ms: numberValue(targetInput.wait_ms) ?? numberValue(targetInput.waitMs),
262
- setup_actions: normalizeSetupActions(targetInput.setup_actions ?? targetInput.setupActions)
325
+ setup_actions: normalizeSetupActions(targetInput.setup_actions ?? targetInput.setupActions),
326
+ network_mocks: normalizeNetworkMocks(targetInput.network_mocks ?? targetInput.networkMocks)
263
327
  },
264
328
  checks,
265
329
  artifacts: Array.isArray(input.artifacts) ? input.artifacts.map((item) => String(item)).filter(Boolean) : ["screenshot", "console", "dom_summary", "proof_json"],
@@ -531,6 +595,50 @@ function assessSetupActionsFromEvidence(profile, evidence) {
531
595
  message: failed.length ? `Setup actions failed in ${failed.length} viewport action(s).` : void 0
532
596
  };
533
597
  }
598
+ function assessNetworkMocksFromEvidence(profile, evidence) {
599
+ const mocks = profile.target.network_mocks || [];
600
+ if (!mocks.length) return void 0;
601
+ const events = evidence.network_mocks || [];
602
+ const failed = [];
603
+ const requiredMocks = mocks.filter((mock) => mock.required !== false);
604
+ for (const mock of requiredMocks) {
605
+ const hits = events.filter((event) => event.label === mock.label && event.ok !== false).length;
606
+ if (hits < 1) {
607
+ failed.push({
608
+ label: mock.label,
609
+ url: mock.url,
610
+ method: mock.method || null,
611
+ reason: "required_mock_not_hit"
612
+ });
613
+ }
614
+ }
615
+ for (const event of events) {
616
+ if (event.ok === false) {
617
+ failed.push({
618
+ label: event.label ?? null,
619
+ url: event.url ?? null,
620
+ method: event.method ?? null,
621
+ reason: event.reason ?? event.error ?? "mock_failed"
622
+ });
623
+ }
624
+ }
625
+ return {
626
+ type: "network_mocks_succeeded",
627
+ label: "network mocks succeeded",
628
+ status: failed.length ? "failed" : "passed",
629
+ evidence: {
630
+ mock_count: mocks.length,
631
+ required_count: requiredMocks.length,
632
+ hit_count: events.filter((event) => event.ok !== false).length,
633
+ hits_by_label: Object.fromEntries(mocks.map((mock) => [
634
+ mock.label,
635
+ events.filter((event) => event.label === mock.label && event.ok !== false).length
636
+ ])),
637
+ failed
638
+ },
639
+ message: failed.length ? `Network mocks failed or were not hit for ${failed.length} mock(s).` : void 0
640
+ };
641
+ }
534
642
  function profileStatusFromEvidence(profile, evidence, checks) {
535
643
  if (!evidence) return "proof_insufficient";
536
644
  const viewports = evidence.viewports || [];
@@ -545,6 +653,7 @@ function profileStatusFromEvidence(profile, evidence, checks) {
545
653
  function assessRiddleProofProfileEvidence(profile, evidence, options = {}) {
546
654
  const capturedAt = evidence?.captured_at || (/* @__PURE__ */ new Date()).toISOString();
547
655
  const checks = evidence ? [
656
+ assessNetworkMocksFromEvidence(profile, evidence),
548
657
  assessSetupActionsFromEvidence(profile, evidence),
549
658
  ...profile.checks.map((check) => assessCheckFromEvidence(check, evidence))
550
659
  ].filter((check) => Boolean(check)) : [];
@@ -761,6 +870,49 @@ function horizontalBoundsOverflowPx(value) {
761
870
  function assessProfile(profile, evidence) {
762
871
  const checks = [];
763
872
  const viewports = evidence.viewports || [];
873
+ if (profile.target && Array.isArray(profile.target.network_mocks) && profile.target.network_mocks.length) {
874
+ const events = evidence.network_mocks || [];
875
+ const requiredMocks = profile.target.network_mocks.filter((mock) => mock && mock.required !== false);
876
+ const failed = [];
877
+ for (const mock of requiredMocks) {
878
+ const hits = events.filter((event) => event && event.label === mock.label && event.ok !== false).length;
879
+ if (hits < 1) {
880
+ failed.push({
881
+ label: mock.label,
882
+ url: mock.url,
883
+ method: mock.method || null,
884
+ reason: "required_mock_not_hit",
885
+ });
886
+ }
887
+ }
888
+ for (const event of events) {
889
+ if (event && event.ok === false) {
890
+ failed.push({
891
+ label: event.label || null,
892
+ url: event.url || null,
893
+ method: event.method || null,
894
+ reason: event.reason || event.error || "mock_failed",
895
+ });
896
+ }
897
+ }
898
+ const hitsByLabel = {};
899
+ for (const mock of profile.target.network_mocks) {
900
+ hitsByLabel[mock.label] = events.filter((event) => event && event.label === mock.label && event.ok !== false).length;
901
+ }
902
+ checks.push({
903
+ type: "network_mocks_succeeded",
904
+ label: "network mocks succeeded",
905
+ status: failed.length ? "failed" : "passed",
906
+ evidence: {
907
+ mock_count: profile.target.network_mocks.length,
908
+ required_count: requiredMocks.length,
909
+ hit_count: events.filter((event) => event && event.ok !== false).length,
910
+ hits_by_label: hitsByLabel,
911
+ failed,
912
+ },
913
+ message: failed.length ? "Network mocks failed or were not hit for " + failed.length + " mock(s)." : undefined,
914
+ });
915
+ }
764
916
  if (profile.target && Array.isArray(profile.target.setup_actions) && profile.target.setup_actions.length) {
765
917
  const actionCount = profile.target.setup_actions.length;
766
918
  const failed = [];
@@ -950,6 +1102,7 @@ const profileSlug = ${serializableSlug};
950
1102
  const capturedAt = new Date().toISOString();
951
1103
  const consoleEvents = [];
952
1104
  const pageErrors = [];
1105
+ const networkMockEvents = [];
953
1106
  page.on("console", (message) => {
954
1107
  const type = message.type();
955
1108
  if (type === "error" || type === "warning" || type === "assert") {
@@ -985,6 +1138,14 @@ function setupTextMatches(sample, action) {
985
1138
  }
986
1139
  return String(sample || "").includes(action.text || "");
987
1140
  }
1141
+ function setupHasOwn(action, key) {
1142
+ return Boolean(action) && Object.keys(action).includes(key);
1143
+ }
1144
+ function setupActionValue(action) {
1145
+ if (setupHasOwn(action, "value_json")) return JSON.stringify(action.value_json);
1146
+ if (setupHasOwn(action, "value")) return String(action.value ?? "");
1147
+ return "";
1148
+ }
988
1149
  async function setupLocatorText(locator, index) {
989
1150
  return await locator.nth(index).textContent({ timeout: 1000 }).catch(() => "");
990
1151
  }
@@ -996,6 +1157,49 @@ function compactSetupResultText(value) {
996
1157
  async function setupLocatorVisible(locator, index) {
997
1158
  return await locator.nth(index).isVisible({ timeout: 1000 }).catch(() => false);
998
1159
  }
1160
+ async function registerNetworkMocks(mocks) {
1161
+ for (const mock of mocks || []) {
1162
+ await page.route(mock.url, async (route) => {
1163
+ const request = route.request();
1164
+ const method = request.method ? request.method() : "";
1165
+ if (mock.method && method.toUpperCase() !== String(mock.method).toUpperCase()) {
1166
+ await route.continue();
1167
+ return;
1168
+ }
1169
+ try {
1170
+ const headers = { ...(mock.headers || {}) };
1171
+ let body = mock.body || "";
1172
+ let contentType = mock.content_type || headers["content-type"] || headers["Content-Type"] || "text/plain";
1173
+ if (mock.body_json !== undefined) {
1174
+ body = JSON.stringify(mock.body_json);
1175
+ contentType = mock.content_type || headers["content-type"] || headers["Content-Type"] || "application/json";
1176
+ }
1177
+ networkMockEvents.push({
1178
+ ok: true,
1179
+ label: mock.label,
1180
+ url: request.url(),
1181
+ method,
1182
+ status: mock.status || 200,
1183
+ });
1184
+ await route.fulfill({
1185
+ status: mock.status || 200,
1186
+ headers,
1187
+ contentType,
1188
+ body,
1189
+ });
1190
+ } catch (error) {
1191
+ networkMockEvents.push({
1192
+ ok: false,
1193
+ label: mock.label,
1194
+ url: request.url(),
1195
+ method,
1196
+ error: String(error && error.message ? error.message : error).slice(0, 1000),
1197
+ });
1198
+ throw error;
1199
+ }
1200
+ });
1201
+ }
1202
+ }
999
1203
  async function executeSetupAction(action, ordinal) {
1000
1204
  const type = setupActionType(action);
1001
1205
  const base = { ok: false, action: type || "unknown", ordinal, selector: action.selector || null };
@@ -1010,6 +1214,16 @@ async function executeSetupAction(action, ordinal) {
1010
1214
  await page.waitForSelector(action.selector, { state: "visible", timeout });
1011
1215
  return { ...base, ok: true, timeout_ms: timeout };
1012
1216
  }
1217
+ if (type === "local_storage") {
1218
+ const value = setupActionValue(action);
1219
+ await page.evaluate(({ key, value }) => {
1220
+ window.localStorage.setItem(key, value);
1221
+ }, { key: action.key, value });
1222
+ if (action.reload === true) {
1223
+ await page.reload({ waitUntil: "domcontentloaded", timeout: 45000 });
1224
+ }
1225
+ return { ...base, ok: true, key: action.key, value_length: value.length, reload: action.reload === true };
1226
+ }
1013
1227
  if (type === "click") {
1014
1228
  const locator = page.locator(action.selector);
1015
1229
  const count = await locator.count();
@@ -1042,6 +1256,16 @@ async function executeSetupAction(action, ordinal) {
1042
1256
  await locator.nth(targetIndex).click({ timeout });
1043
1257
  return { ...base, ok: true, count, target_index: targetIndex, text: matchedText };
1044
1258
  }
1259
+ if (type === "fill" || type === "set_input_value") {
1260
+ const locator = page.locator(action.selector);
1261
+ const count = await locator.count();
1262
+ if (!count) return { ...base, reason: "selector_not_found", count };
1263
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
1264
+ if (targetIndex < 0 || targetIndex >= count) return { ...base, reason: "index_out_of_range", count, target_index: targetIndex };
1265
+ const value = setupActionValue(action);
1266
+ await locator.nth(targetIndex).fill(value, { timeout });
1267
+ return { ...base, ok: true, count, target_index: targetIndex, value_length: value.length };
1268
+ }
1045
1269
  if (type === "wait_for_text") {
1046
1270
  const locator = page.locator(action.selector);
1047
1271
  const startedAt = Date.now();
@@ -1257,6 +1481,7 @@ function buildProfileEvidence(currentViewports) {
1257
1481
  fatal_count: consoleEvents.filter((event) => event.type === "error" || event.type === "assert").length,
1258
1482
  },
1259
1483
  page_errors: pageErrors,
1484
+ network_mocks: networkMockEvents.slice(),
1260
1485
  dom_summary: {
1261
1486
  expected_viewport_count: expectedViewportCount,
1262
1487
  viewport_count: currentViewports.length,
@@ -1266,6 +1491,8 @@ function buildProfileEvidence(currentViewports) {
1266
1491
  overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
1267
1492
  bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
1268
1493
  overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
1494
+ network_mock_count: (profile.target.network_mocks || []).length,
1495
+ network_mock_hit_count: networkMockEvents.filter((event) => event.ok !== false).length,
1269
1496
  },
1270
1497
  };
1271
1498
  }
@@ -1279,6 +1506,9 @@ async function saveProfileArtifacts(currentViewports) {
1279
1506
  }
1280
1507
  return result;
1281
1508
  }
1509
+ await registerNetworkMocks(profile.target.network_mocks || []);
1510
+ consoleEvents.length = 0;
1511
+ pageErrors.length = 0;
1282
1512
  let result = await saveProfileArtifacts(viewports);
1283
1513
  for (const viewport of profile.target.viewports || []) {
1284
1514
  viewports.push(await captureViewport(viewport));