@riddledc/riddle-proof 0.7.125 → 0.7.127

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/dist/index.cjs CHANGED
@@ -8872,6 +8872,9 @@ function valueFromOwn(input, ...keys) {
8872
8872
  function numberValue3(value) {
8873
8873
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8874
8874
  }
8875
+ function booleanValue(value) {
8876
+ return typeof value === "boolean" ? value : void 0;
8877
+ }
8875
8878
  function horizontalBoundsOverflowPx2(value) {
8876
8879
  if (!isRecord2(value)) return 0;
8877
8880
  let max = maxPositiveNumber2(
@@ -8966,6 +8969,156 @@ function toJsonValue(value) {
8966
8969
  if (isRecord2(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
8967
8970
  return String(value);
8968
8971
  }
8972
+ function jsonValueType(value) {
8973
+ if (value === null) return "null";
8974
+ if (Array.isArray(value)) return "array";
8975
+ if (typeof value === "boolean") return "boolean";
8976
+ if (typeof value === "number") return "number";
8977
+ if (typeof value === "string") return "string";
8978
+ return "object";
8979
+ }
8980
+ function deepJsonEqual(left, right) {
8981
+ if (left === right) return true;
8982
+ if (typeof left !== typeof right) return false;
8983
+ if (left === null || right === null) return left === right;
8984
+ if (typeof left !== "object" || typeof right !== "object") return false;
8985
+ if (Array.isArray(left) || Array.isArray(right)) {
8986
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
8987
+ return left.every((item, index) => deepJsonEqual(item, right[index]));
8988
+ }
8989
+ if (!isRecord2(left) || !isRecord2(right)) return false;
8990
+ const leftKeys = Object.keys(left).sort();
8991
+ const rightKeys = Object.keys(right).sort();
8992
+ if (!deepJsonEqual(leftKeys, rightKeys)) return false;
8993
+ return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
8994
+ }
8995
+ function jsonContains(observed, expected) {
8996
+ if (typeof observed === "string" && typeof expected === "string") {
8997
+ return observed.includes(expected);
8998
+ }
8999
+ if (Array.isArray(observed)) {
9000
+ return observed.some((item) => deepJsonEqual(item, expected));
9001
+ }
9002
+ if (isRecord2(observed) && isRecord2(expected)) {
9003
+ return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
9004
+ }
9005
+ return false;
9006
+ }
9007
+ function parseJsonPathSegments(path6) {
9008
+ let input = path6.trim();
9009
+ if (!input) throw new Error("path is empty");
9010
+ if (input === "$") return [];
9011
+ if (input.startsWith("$.")) input = input.slice(2);
9012
+ else if (input.startsWith("$[")) input = input.slice(1);
9013
+ const segments = [];
9014
+ let token = "";
9015
+ const pushToken = () => {
9016
+ if (!token) return;
9017
+ segments.push(token);
9018
+ token = "";
9019
+ };
9020
+ for (let index = 0; index < input.length; index += 1) {
9021
+ const char = input[index];
9022
+ if (char === ".") {
9023
+ pushToken();
9024
+ continue;
9025
+ }
9026
+ if (char !== "[") {
9027
+ token += char;
9028
+ continue;
9029
+ }
9030
+ pushToken();
9031
+ const closeIndex = input.indexOf("]", index + 1);
9032
+ if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
9033
+ const bracket = input.slice(index + 1, closeIndex).trim();
9034
+ if (!bracket) throw new Error(`empty bracket at ${index}`);
9035
+ if (/^\d+$/.test(bracket)) {
9036
+ segments.push(Number(bracket));
9037
+ } else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
9038
+ const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
9039
+ segments.push(String(JSON.parse(quoted)));
9040
+ } else {
9041
+ segments.push(bracket);
9042
+ }
9043
+ index = closeIndex;
9044
+ }
9045
+ pushToken();
9046
+ return segments;
9047
+ }
9048
+ function resolveJsonPath(root, path6) {
9049
+ let segments;
9050
+ try {
9051
+ segments = parseJsonPathSegments(path6);
9052
+ } catch (error) {
9053
+ return { exists: false, error: String(error instanceof Error ? error.message : error) };
9054
+ }
9055
+ let current = root;
9056
+ for (const segment of segments) {
9057
+ if (typeof segment === "number") {
9058
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
9059
+ current = current[segment];
9060
+ continue;
9061
+ }
9062
+ if (!isRecord2(current) || !hasOwn(current, segment)) return { exists: false };
9063
+ current = current[segment];
9064
+ }
9065
+ return { exists: true, value: current };
9066
+ }
9067
+ function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
9068
+ const resolved = resolveJsonPath(root, assertion.path);
9069
+ const errors = [];
9070
+ const result = {
9071
+ label: assertion.label || assertion.path,
9072
+ path: assertion.path,
9073
+ ok: true,
9074
+ exists: resolved.exists,
9075
+ observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
9076
+ };
9077
+ if (resolved.exists) result.observed = toJsonValue(resolved.value);
9078
+ if (resolved.error) errors.push(resolved.error);
9079
+ if (hasOwn(assertion, "exists")) {
9080
+ result.expected_exists = assertion.exists;
9081
+ if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
9082
+ }
9083
+ if (hasOwn(assertion, "type")) {
9084
+ result.type = assertion.type;
9085
+ if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
9086
+ }
9087
+ if (hasOwn(assertion, "equals")) {
9088
+ result.equals = assertion.equals;
9089
+ if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
9090
+ }
9091
+ if (hasOwn(assertion, "not_equals")) {
9092
+ result.not_equals = assertion.not_equals;
9093
+ if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
9094
+ }
9095
+ if (hasOwn(assertion, "contains")) {
9096
+ result.contains = assertion.contains;
9097
+ if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
9098
+ }
9099
+ result.ok = errors.length === 0;
9100
+ if (errors.length) result.errors = errors;
9101
+ return result;
9102
+ }
9103
+ function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
9104
+ const expected = assertions?.filter((assertion) => assertion.path) ?? [];
9105
+ if (!expected.length) return [];
9106
+ let parsed;
9107
+ try {
9108
+ parsed = JSON.parse(bodyText);
9109
+ } catch (error) {
9110
+ const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
9111
+ return expected.map((assertion) => ({
9112
+ label: assertion.label || assertion.path,
9113
+ path: assertion.path,
9114
+ ok: false,
9115
+ exists: false,
9116
+ observed_type: "missing",
9117
+ errors: [message]
9118
+ }));
9119
+ }
9120
+ return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
9121
+ }
8969
9122
  function compactProfileSetupSummaryText(value, limit = 160) {
8970
9123
  const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
8971
9124
  if (!text) return void 0;
@@ -9329,6 +9482,20 @@ function normalizeNetworkMock(input, index) {
9329
9482
  if (maxHitCount !== void 0 && effectiveRequiredHitCount > maxHitCount) {
9330
9483
  throw new Error(`target.network_mocks[${index}].max_hit_count cannot be less than its required hit count.`);
9331
9484
  }
9485
+ const sequenceScopeInput = stringValue5(
9486
+ input.sequence_scope ?? input.sequenceScope ?? input.response_sequence_scope ?? input.responseSequenceScope
9487
+ );
9488
+ let sequenceScope;
9489
+ if (sequenceScopeInput) {
9490
+ const normalizedScope = sequenceScopeInput.toLowerCase().replace(/[-\s]+/g, "_");
9491
+ if (normalizedScope === "global" || normalizedScope === "profile" || normalizedScope === "run") {
9492
+ sequenceScope = "global";
9493
+ } else if (normalizedScope === "viewport" || normalizedScope === "per_viewport" || normalizedScope === "viewport_scoped") {
9494
+ sequenceScope = "viewport";
9495
+ } else {
9496
+ throw new Error(`target.network_mocks[${index}].sequence_scope must be "global" or "viewport".`);
9497
+ }
9498
+ }
9332
9499
  return {
9333
9500
  ...payload,
9334
9501
  label: normalizeName(input.label || input.name, `network-mock-${index + 1}`),
@@ -9336,6 +9503,7 @@ function normalizeNetworkMock(input, index) {
9336
9503
  method: stringValue5(input.method)?.toUpperCase(),
9337
9504
  responses,
9338
9505
  repeat_responses: input.repeat_responses === true || input.repeatResponses === true || input.cycle_responses === true || input.cycleResponses === true,
9506
+ sequence_scope: sequenceScope,
9339
9507
  required_hit_count: requiredHitCount,
9340
9508
  max_hit_count: maxHitCount,
9341
9509
  forbidden,
@@ -9605,6 +9773,46 @@ function validateRegexPatterns(patterns, label) {
9605
9773
  }
9606
9774
  }
9607
9775
  }
9776
+ function normalizeHttpStatusBodyJsonAssertions(value, label) {
9777
+ if (value === void 0) return void 0;
9778
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
9779
+ if (!value.length) throw new Error(`${label} must not be empty.`);
9780
+ return value.map((item, index) => {
9781
+ const itemLabel = `${label}[${index}]`;
9782
+ if (typeof item === "string") {
9783
+ const path7 = stringValue5(item);
9784
+ if (!path7) throw new Error(`${itemLabel} path must not be empty.`);
9785
+ return { path: path7, exists: true };
9786
+ }
9787
+ if (!isRecord2(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
9788
+ const path6 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
9789
+ if (!path6) throw new Error(`${itemLabel}.path is required.`);
9790
+ const assertion = {
9791
+ label: stringValue5(item.label),
9792
+ path: path6
9793
+ };
9794
+ const exists = booleanValue(valueFromOwn(item, "exists", "present"));
9795
+ if (exists !== void 0) assertion.exists = exists;
9796
+ const type = stringValue5(valueFromOwn(item, "type", "value_type", "valueType"));
9797
+ if (type !== void 0) {
9798
+ const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
9799
+ if (!allowedTypes.includes(type)) {
9800
+ throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
9801
+ }
9802
+ assertion.type = type;
9803
+ }
9804
+ const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
9805
+ if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
9806
+ const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
9807
+ if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
9808
+ const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
9809
+ if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
9810
+ if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
9811
+ assertion.exists = true;
9812
+ }
9813
+ return assertion;
9814
+ });
9815
+ }
9608
9816
  function isDialogCountCheckType(type) {
9609
9817
  return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
9610
9818
  }
@@ -9704,6 +9912,10 @@ function normalizeCheck(input, index) {
9704
9912
  `checks[${index}] body_not_patterns`
9705
9913
  ) : void 0;
9706
9914
  if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
9915
+ const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
9916
+ input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
9917
+ `checks[${index}] body_json_assertions`
9918
+ ) : void 0;
9707
9919
  if (isLinkStatusCheck) {
9708
9920
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
9709
9921
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -9729,6 +9941,7 @@ function normalizeCheck(input, index) {
9729
9941
  body_contains: bodyContains,
9730
9942
  body_not_contains: bodyNotContains,
9731
9943
  body_not_patterns: bodyNotPatterns,
9944
+ body_json_assertions: bodyJsonAssertions,
9732
9945
  expected_texts: expectedTexts,
9733
9946
  link_selector: stringValue5(input.link_selector) || stringValue5(input.linkSelector),
9734
9947
  source_selector: stringValue5(input.source_selector) || stringValue5(input.sourceSelector),
@@ -9935,6 +10148,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
9935
10148
  const observed = isRecord2(result.body_not_patterns) ? result.body_not_patterns : {};
9936
10149
  return forbidden.filter((pattern) => observed[pattern] !== false);
9937
10150
  }
10151
+ function httpStatusBodyJsonAssertionFailures(result, check) {
10152
+ const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
10153
+ if (!expected.length) return [];
10154
+ if (!Array.isArray(result.body_json_assertions)) {
10155
+ return expected.map((assertion) => ({
10156
+ label: assertion.label || assertion.path,
10157
+ path: assertion.path,
10158
+ ok: false,
10159
+ exists: false,
10160
+ observed_type: "missing",
10161
+ errors: ["body_json_assertions evidence missing"]
10162
+ }));
10163
+ }
10164
+ return result.body_json_assertions.filter((assertion) => isRecord2(assertion) && assertion.ok !== true).map((assertion) => ({
10165
+ label: stringValue5(assertion.label) || stringValue5(assertion.path) || "json assertion",
10166
+ path: stringValue5(assertion.path) || "",
10167
+ ok: false,
10168
+ exists: assertion.exists === true,
10169
+ observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
10170
+ observed_type: stringValue5(assertion.observed_type) || "missing",
10171
+ expected_exists: booleanValue(assertion.expected_exists),
10172
+ equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
10173
+ not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
10174
+ contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
10175
+ type: stringValue5(assertion.type),
10176
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
10177
+ }));
10178
+ }
9938
10179
  function linkStatusResultOk(result, check) {
9939
10180
  const status = numberValue3(result.status);
9940
10181
  if (!httpStatusIsAllowed(status, check)) return false;
@@ -9952,6 +10193,7 @@ function linkStatusResultOk(result, check) {
9952
10193
  if (httpStatusBodyContainsFailures(result, check).length) return false;
9953
10194
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
9954
10195
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
10196
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
9955
10197
  return true;
9956
10198
  }
9957
10199
  function responseHeader(response, name) {
@@ -10020,7 +10262,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10020
10262
  statusText = typeof response.statusText === "string" ? response.statusText : "";
10021
10263
  result.content_type = responseHeader(response, "content-type");
10022
10264
  result.content_length = responseContentLength(response);
10023
- const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length);
10265
+ const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length) || Boolean(check.body_json_assertions?.length);
10024
10266
  if (shouldReadBody && method !== "HEAD") {
10025
10267
  const body = await responseBodyText(response);
10026
10268
  result.bytes = body.bytes;
@@ -10033,6 +10275,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10033
10275
  if (check.body_not_patterns?.length) {
10034
10276
  result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
10035
10277
  }
10278
+ if (check.body_json_assertions?.length) {
10279
+ result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
10280
+ }
10036
10281
  }
10037
10282
  } catch (caught) {
10038
10283
  error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
@@ -10041,6 +10286,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10041
10286
  const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
10042
10287
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
10043
10288
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
10289
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
10044
10290
  const ok = !error && linkStatusResultOk(result, check);
10045
10291
  return {
10046
10292
  index,
@@ -10059,7 +10305,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10059
10305
  body_not_contains: isRecord2(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
10060
10306
  body_not_contains_found: bodyNotContainsFound,
10061
10307
  body_not_patterns: isRecord2(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
10062
- body_not_patterns_found: bodyNotPatternsFound
10308
+ body_not_patterns_found: bodyNotPatternsFound,
10309
+ body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
10310
+ body_json_assertions_failed: bodyJsonAssertionsFailed
10063
10311
  };
10064
10312
  }
10065
10313
  async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
@@ -10104,6 +10352,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
10104
10352
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
10105
10353
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
10106
10354
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
10355
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
10107
10356
  if (!linkStatusResultOk(statusEvidence, check)) {
10108
10357
  failures.push({
10109
10358
  code: "http_status_failed",
@@ -10122,6 +10371,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
10122
10371
  body_not_contains_found: bodyNotContainsFound,
10123
10372
  body_not_patterns: check.body_not_patterns ?? null,
10124
10373
  body_not_patterns_found: bodyNotPatternsFound,
10374
+ body_json_assertions: check.body_json_assertions ?? null,
10375
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
10125
10376
  body_sample: stringValue5(statusEvidence.body_sample) ?? null
10126
10377
  });
10127
10378
  }
@@ -10143,6 +10394,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
10143
10394
  body_not_contains_found: bodyNotContainsFound,
10144
10395
  body_not_patterns: isRecord2(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
10145
10396
  body_not_patterns_found: bodyNotPatternsFound,
10397
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
10398
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
10146
10399
  body_sample: stringValue5(statusEvidence.body_sample) ?? null,
10147
10400
  failures
10148
10401
  };
@@ -10779,6 +11032,7 @@ function assessCheckFromEvidence(check, evidence) {
10779
11032
  body_contains: check.body_contains ?? [],
10780
11033
  body_not_contains: check.body_not_contains ?? [],
10781
11034
  body_not_patterns: check.body_not_patterns ?? [],
11035
+ body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
10782
11036
  viewports: summaries.map((summary) => toJsonValue(summary)),
10783
11037
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue5(summary.viewport) ?? null, failure })) : [])
10784
11038
  },
@@ -11522,6 +11776,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
11522
11776
  : {};
11523
11777
  return forbidden.filter((pattern) => observed[pattern] !== false);
11524
11778
  }
11779
+ function httpStatusBodyJsonAssertionFailures(result, check) {
11780
+ const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
11781
+ if (!expected.length) return [];
11782
+ if (!Array.isArray(result.body_json_assertions)) {
11783
+ return expected.map((assertion) => ({
11784
+ label: assertion.label || assertion.path,
11785
+ path: assertion.path,
11786
+ ok: false,
11787
+ exists: false,
11788
+ observed_type: "missing",
11789
+ errors: ["body_json_assertions evidence missing"],
11790
+ }));
11791
+ }
11792
+ return result.body_json_assertions
11793
+ .filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
11794
+ .map((assertion) => ({
11795
+ label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
11796
+ path: typeof assertion.path === "string" ? assertion.path : "",
11797
+ ok: false,
11798
+ exists: assertion.exists === true,
11799
+ observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
11800
+ observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
11801
+ expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
11802
+ equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
11803
+ not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
11804
+ contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
11805
+ type: typeof assertion.type === "string" ? assertion.type : undefined,
11806
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
11807
+ }));
11808
+ }
11525
11809
  function linkStatusResultOk(result, check) {
11526
11810
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
11527
11811
  if (!httpStatusIsAllowed(result.status, check)) return false;
@@ -11539,6 +11823,7 @@ function linkStatusResultOk(result, check) {
11539
11823
  if (httpStatusBodyContainsFailures(result, check).length) return false;
11540
11824
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
11541
11825
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
11826
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
11542
11827
  return true;
11543
11828
  }
11544
11829
  function summarizeHttpStatusEvidence(viewport, check) {
@@ -11559,6 +11844,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
11559
11844
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
11560
11845
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
11561
11846
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
11847
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
11562
11848
  if (!linkStatusResultOk(statusEvidence, check)) {
11563
11849
  failures.push({
11564
11850
  code: "http_status_failed",
@@ -11577,6 +11863,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
11577
11863
  body_not_contains_found: bodyNotContainsFound,
11578
11864
  body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
11579
11865
  body_not_patterns_found: bodyNotPatternsFound,
11866
+ body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
11867
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
11580
11868
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
11581
11869
  });
11582
11870
  }
@@ -11604,6 +11892,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
11604
11892
  ? statusEvidence.body_not_patterns
11605
11893
  : null,
11606
11894
  body_not_patterns_found: bodyNotPatternsFound,
11895
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
11896
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
11607
11897
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
11608
11898
  failures,
11609
11899
  };
@@ -12987,6 +13277,7 @@ async function setupLocatorVisible(locator, index) {
12987
13277
  async function registerNetworkMocks(mocks) {
12988
13278
  for (const mock of mocks || []) {
12989
13279
  let hitCount = 0;
13280
+ const scopedHitCounts = {};
12990
13281
  await page.route(mock.url, async (route) => {
12991
13282
  const request = route.request();
12992
13283
  const method = request.method ? request.method() : "";
@@ -13002,8 +13293,13 @@ async function registerNetworkMocks(mocks) {
13002
13293
  const responses = Array.isArray(mock.responses) ? mock.responses : [];
13003
13294
  const hitIndex = hitCount;
13004
13295
  hitCount += 1;
13296
+ const sequenceScope = mock.sequence_scope === "viewport" ? "viewport" : "global";
13297
+ const viewportName = activeViewportName || null;
13298
+ const sequenceScopeKey = sequenceScope === "viewport" ? (viewportName || "__unknown_viewport__") : "__global__";
13299
+ const sequenceHitIndex = sequenceScope === "viewport" ? (scopedHitCounts[sequenceScopeKey] || 0) : hitIndex;
13300
+ if (sequenceScope === "viewport") scopedHitCounts[sequenceScopeKey] = sequenceHitIndex + 1;
13005
13301
  const sequenceResponseIndex = responses.length
13006
- ? (mock.repeat_responses ? hitIndex % responses.length : Math.min(hitIndex, responses.length - 1))
13302
+ ? (mock.repeat_responses ? sequenceHitIndex % responses.length : Math.min(sequenceHitIndex, responses.length - 1))
13007
13303
  : null;
13008
13304
  let responseIndex = sequenceResponseIndex;
13009
13305
  let responseSelection = responseIndex === null ? "mock" : "sequence";
@@ -13038,11 +13334,14 @@ async function registerNetworkMocks(mocks) {
13038
13334
  label: mock.label,
13039
13335
  response_label: response.label || null,
13040
13336
  hit_index: hitIndex,
13337
+ sequence_hit_index: responseIndex === null ? undefined : sequenceHitIndex,
13338
+ sequence_scope: responseIndex === null ? undefined : sequenceScope,
13339
+ viewport: viewportName,
13041
13340
  response_index: responseIndex,
13042
13341
  sequence_response_index: responseSelection === "request_body" ? sequenceResponseIndex : undefined,
13043
13342
  response_selection: responseIndex === null ? null : responseSelection,
13044
- sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses && hitIndex >= responses.length,
13045
- sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true && hitIndex >= responses.length,
13343
+ sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses && sequenceHitIndex >= responses.length,
13344
+ sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true && sequenceHitIndex >= responses.length,
13046
13345
  url: request.url(),
13047
13346
  method,
13048
13347
  };
@@ -13084,6 +13383,7 @@ async function registerNetworkMocks(mocks) {
13084
13383
  });
13085
13384
  }
13086
13385
  }
13386
+ let activeViewportName = null;
13087
13387
  async function executeSetupAction(action, ordinal, viewport) {
13088
13388
  const type = setupActionType(action);
13089
13389
  const frameSelector = setupFrameSelector(action);
@@ -13696,6 +13996,155 @@ function linkProbeResponseFields(response, method) {
13696
13996
  content_length: contentLength,
13697
13997
  };
13698
13998
  }
13999
+ function jsonProbeValueType(value) {
14000
+ if (value === null) return "null";
14001
+ if (Array.isArray(value)) return "array";
14002
+ if (typeof value === "boolean") return "boolean";
14003
+ if (typeof value === "number") return "number";
14004
+ if (typeof value === "string") return "string";
14005
+ return "object";
14006
+ }
14007
+ function jsonProbeDeepEqual(left, right) {
14008
+ if (left === right) return true;
14009
+ if (typeof left !== typeof right) return false;
14010
+ if (left === null || right === null) return left === right;
14011
+ if (typeof left !== "object" || typeof right !== "object") return false;
14012
+ if (Array.isArray(left) || Array.isArray(right)) {
14013
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
14014
+ return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
14015
+ }
14016
+ const leftKeys = Object.keys(left).sort();
14017
+ const rightKeys = Object.keys(right).sort();
14018
+ if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
14019
+ return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
14020
+ }
14021
+ function jsonProbeContains(observed, expected) {
14022
+ if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
14023
+ if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
14024
+ if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
14025
+ return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
14026
+ }
14027
+ return false;
14028
+ }
14029
+ function parseJsonProbePathSegments(path) {
14030
+ let input = String(path || "").trim();
14031
+ if (!input) throw new Error("path is empty");
14032
+ if (input === "$") return [];
14033
+ if (input.startsWith("$.")) input = input.slice(2);
14034
+ else if (input.startsWith("$[")) input = input.slice(1);
14035
+ const segments = [];
14036
+ let token = "";
14037
+ const pushToken = () => {
14038
+ if (!token) return;
14039
+ segments.push(token);
14040
+ token = "";
14041
+ };
14042
+ for (let index = 0; index < input.length; index += 1) {
14043
+ const char = input[index];
14044
+ if (char === ".") {
14045
+ pushToken();
14046
+ continue;
14047
+ }
14048
+ if (char !== "[") {
14049
+ token += char;
14050
+ continue;
14051
+ }
14052
+ pushToken();
14053
+ const closeIndex = input.indexOf("]", index + 1);
14054
+ if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
14055
+ const bracket = input.slice(index + 1, closeIndex).trim();
14056
+ if (!bracket) throw new Error("empty bracket at " + index);
14057
+ if (/^\d+$/.test(bracket)) {
14058
+ segments.push(Number(bracket));
14059
+ } else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
14060
+ const quoted = bracket.startsWith("'")
14061
+ ? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
14062
+ : bracket;
14063
+ segments.push(String(JSON.parse(quoted)));
14064
+ } else {
14065
+ segments.push(bracket);
14066
+ }
14067
+ index = closeIndex;
14068
+ }
14069
+ pushToken();
14070
+ return segments;
14071
+ }
14072
+ function resolveJsonProbePath(root, path) {
14073
+ let segments;
14074
+ try {
14075
+ segments = parseJsonProbePathSegments(path);
14076
+ } catch (error) {
14077
+ return { exists: false, error: String(error && error.message ? error.message : error) };
14078
+ }
14079
+ let current = root;
14080
+ for (const segment of segments) {
14081
+ if (typeof segment === "number") {
14082
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
14083
+ current = current[segment];
14084
+ continue;
14085
+ }
14086
+ if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
14087
+ return { exists: false };
14088
+ }
14089
+ current = current[segment];
14090
+ }
14091
+ return { exists: true, value: current };
14092
+ }
14093
+ function evaluateJsonProbeAssertion(root, assertion) {
14094
+ const resolved = resolveJsonProbePath(root, assertion.path);
14095
+ const errors = [];
14096
+ const result = {
14097
+ label: assertion.label || assertion.path,
14098
+ path: assertion.path,
14099
+ ok: true,
14100
+ exists: resolved.exists,
14101
+ observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
14102
+ };
14103
+ if (resolved.exists) result.observed = resolved.value;
14104
+ if (resolved.error) errors.push(resolved.error);
14105
+ if (Object.hasOwn(assertion, "exists")) {
14106
+ result.expected_exists = assertion.exists;
14107
+ if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
14108
+ }
14109
+ if (Object.hasOwn(assertion, "type")) {
14110
+ result.type = assertion.type;
14111
+ if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
14112
+ }
14113
+ if (Object.hasOwn(assertion, "equals")) {
14114
+ result.equals = assertion.equals;
14115
+ if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
14116
+ }
14117
+ if (Object.hasOwn(assertion, "not_equals")) {
14118
+ result.not_equals = assertion.not_equals;
14119
+ if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
14120
+ }
14121
+ if (Object.hasOwn(assertion, "contains")) {
14122
+ result.contains = assertion.contains;
14123
+ if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
14124
+ }
14125
+ result.ok = errors.length === 0;
14126
+ if (errors.length) result.errors = errors;
14127
+ return result;
14128
+ }
14129
+ function evaluateJsonProbeAssertions(text, assertions) {
14130
+ const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
14131
+ if (!expected.length) return [];
14132
+ let parsed;
14133
+ try {
14134
+ parsed = JSON.parse(text);
14135
+ } catch (error) {
14136
+ const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
14137
+ return expected.map((assertion) => ({
14138
+ label: assertion.label || assertion.path,
14139
+ path: assertion.path,
14140
+ ok: false,
14141
+ exists: false,
14142
+ observed_type: "missing",
14143
+ errors: [message],
14144
+ }));
14145
+ }
14146
+ return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
14147
+ }
13699
14148
  async function collectHttpStatus(check) {
13700
14149
  const url = httpStatusRequestUrl(check, page.url() || targetUrl);
13701
14150
  const method = httpStatusMethod(check);
@@ -13712,6 +14161,7 @@ async function collectHttpStatus(check) {
13712
14161
  const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
13713
14162
  const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
13714
14163
  const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
14164
+ const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
13715
14165
  const options = {
13716
14166
  method,
13717
14167
  redirect: "follow",
@@ -13737,17 +14187,18 @@ async function collectHttpStatus(check) {
13737
14187
  Object.assign(result, linkProbeResponseFields(response, method));
13738
14188
  result.url = url;
13739
14189
  result.status_text = response.statusText || "";
13740
- const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0;
14190
+ const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0 || bodyJsonAssertions.length > 0;
13741
14191
  if (shouldReadBody) {
13742
14192
  try {
13743
14193
  const buffer = await response.arrayBuffer();
13744
14194
  result.bytes = buffer.byteLength;
13745
- if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
14195
+ if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
13746
14196
  const text = new TextDecoder().decode(buffer);
13747
14197
  result.body_sample = text.slice(0, 1000);
13748
14198
  if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
13749
14199
  if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
13750
14200
  if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
14201
+ if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
13751
14202
  }
13752
14203
  } catch (error) {
13753
14204
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -13760,6 +14211,7 @@ async function collectHttpStatus(check) {
13760
14211
  && (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
13761
14212
  && (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
13762
14213
  && (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
14214
+ && (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
13763
14215
  && !result.error;
13764
14216
  return result;
13765
14217
  } catch (error) {
@@ -14346,6 +14798,7 @@ async function collectRouteInventory(check, viewport) {
14346
14798
  };
14347
14799
  }
14348
14800
  async function captureViewport(viewport) {
14801
+ activeViewportName = viewport && viewport.name ? viewport.name : null;
14349
14802
  await page.setViewportSize({ width: viewport.width, height: viewport.height });
14350
14803
  let httpStatus = null;
14351
14804
  let navigationError;