@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/README.md +34 -2
- package/dist/{chunk-JVZWSI55.js → chunk-JLINSUKO.js} +460 -7
- package/dist/cli.cjs +478 -8
- package/dist/cli.js +19 -2
- package/dist/index.cjs +460 -7
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/profile.cjs +460 -7
- package/dist/profile.d.cts +29 -1
- package/dist/profile.d.ts +29 -1
- package/dist/profile.js +1 -1
- package/package.json +1 -1
package/dist/profile.cjs
CHANGED
|
@@ -186,6 +186,9 @@ function valueFromOwn(input, ...keys) {
|
|
|
186
186
|
function numberValue(value) {
|
|
187
187
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
188
188
|
}
|
|
189
|
+
function booleanValue(value) {
|
|
190
|
+
return typeof value === "boolean" ? value : void 0;
|
|
191
|
+
}
|
|
189
192
|
function horizontalBoundsOverflowPx(value) {
|
|
190
193
|
if (!isRecord(value)) return 0;
|
|
191
194
|
let max = maxPositiveNumber(
|
|
@@ -280,6 +283,156 @@ function toJsonValue(value) {
|
|
|
280
283
|
if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
|
|
281
284
|
return String(value);
|
|
282
285
|
}
|
|
286
|
+
function jsonValueType(value) {
|
|
287
|
+
if (value === null) return "null";
|
|
288
|
+
if (Array.isArray(value)) return "array";
|
|
289
|
+
if (typeof value === "boolean") return "boolean";
|
|
290
|
+
if (typeof value === "number") return "number";
|
|
291
|
+
if (typeof value === "string") return "string";
|
|
292
|
+
return "object";
|
|
293
|
+
}
|
|
294
|
+
function deepJsonEqual(left, right) {
|
|
295
|
+
if (left === right) return true;
|
|
296
|
+
if (typeof left !== typeof right) return false;
|
|
297
|
+
if (left === null || right === null) return left === right;
|
|
298
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
299
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
300
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
301
|
+
return left.every((item, index) => deepJsonEqual(item, right[index]));
|
|
302
|
+
}
|
|
303
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
304
|
+
const leftKeys = Object.keys(left).sort();
|
|
305
|
+
const rightKeys = Object.keys(right).sort();
|
|
306
|
+
if (!deepJsonEqual(leftKeys, rightKeys)) return false;
|
|
307
|
+
return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
|
|
308
|
+
}
|
|
309
|
+
function jsonContains(observed, expected) {
|
|
310
|
+
if (typeof observed === "string" && typeof expected === "string") {
|
|
311
|
+
return observed.includes(expected);
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(observed)) {
|
|
314
|
+
return observed.some((item) => deepJsonEqual(item, expected));
|
|
315
|
+
}
|
|
316
|
+
if (isRecord(observed) && isRecord(expected)) {
|
|
317
|
+
return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
function parseJsonPathSegments(path) {
|
|
322
|
+
let input = path.trim();
|
|
323
|
+
if (!input) throw new Error("path is empty");
|
|
324
|
+
if (input === "$") return [];
|
|
325
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
326
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
327
|
+
const segments = [];
|
|
328
|
+
let token = "";
|
|
329
|
+
const pushToken = () => {
|
|
330
|
+
if (!token) return;
|
|
331
|
+
segments.push(token);
|
|
332
|
+
token = "";
|
|
333
|
+
};
|
|
334
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
335
|
+
const char = input[index];
|
|
336
|
+
if (char === ".") {
|
|
337
|
+
pushToken();
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (char !== "[") {
|
|
341
|
+
token += char;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
pushToken();
|
|
345
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
346
|
+
if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
|
|
347
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
348
|
+
if (!bracket) throw new Error(`empty bracket at ${index}`);
|
|
349
|
+
if (/^\d+$/.test(bracket)) {
|
|
350
|
+
segments.push(Number(bracket));
|
|
351
|
+
} else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
|
|
352
|
+
const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
|
|
353
|
+
segments.push(String(JSON.parse(quoted)));
|
|
354
|
+
} else {
|
|
355
|
+
segments.push(bracket);
|
|
356
|
+
}
|
|
357
|
+
index = closeIndex;
|
|
358
|
+
}
|
|
359
|
+
pushToken();
|
|
360
|
+
return segments;
|
|
361
|
+
}
|
|
362
|
+
function resolveJsonPath(root, path) {
|
|
363
|
+
let segments;
|
|
364
|
+
try {
|
|
365
|
+
segments = parseJsonPathSegments(path);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return { exists: false, error: String(error instanceof Error ? error.message : error) };
|
|
368
|
+
}
|
|
369
|
+
let current = root;
|
|
370
|
+
for (const segment of segments) {
|
|
371
|
+
if (typeof segment === "number") {
|
|
372
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
373
|
+
current = current[segment];
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
|
|
377
|
+
current = current[segment];
|
|
378
|
+
}
|
|
379
|
+
return { exists: true, value: current };
|
|
380
|
+
}
|
|
381
|
+
function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
|
|
382
|
+
const resolved = resolveJsonPath(root, assertion.path);
|
|
383
|
+
const errors = [];
|
|
384
|
+
const result = {
|
|
385
|
+
label: assertion.label || assertion.path,
|
|
386
|
+
path: assertion.path,
|
|
387
|
+
ok: true,
|
|
388
|
+
exists: resolved.exists,
|
|
389
|
+
observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
|
|
390
|
+
};
|
|
391
|
+
if (resolved.exists) result.observed = toJsonValue(resolved.value);
|
|
392
|
+
if (resolved.error) errors.push(resolved.error);
|
|
393
|
+
if (hasOwn(assertion, "exists")) {
|
|
394
|
+
result.expected_exists = assertion.exists;
|
|
395
|
+
if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
|
|
396
|
+
}
|
|
397
|
+
if (hasOwn(assertion, "type")) {
|
|
398
|
+
result.type = assertion.type;
|
|
399
|
+
if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
|
|
400
|
+
}
|
|
401
|
+
if (hasOwn(assertion, "equals")) {
|
|
402
|
+
result.equals = assertion.equals;
|
|
403
|
+
if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
404
|
+
}
|
|
405
|
+
if (hasOwn(assertion, "not_equals")) {
|
|
406
|
+
result.not_equals = assertion.not_equals;
|
|
407
|
+
if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
408
|
+
}
|
|
409
|
+
if (hasOwn(assertion, "contains")) {
|
|
410
|
+
result.contains = assertion.contains;
|
|
411
|
+
if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
412
|
+
}
|
|
413
|
+
result.ok = errors.length === 0;
|
|
414
|
+
if (errors.length) result.errors = errors;
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
|
|
418
|
+
const expected = assertions?.filter((assertion) => assertion.path) ?? [];
|
|
419
|
+
if (!expected.length) return [];
|
|
420
|
+
let parsed;
|
|
421
|
+
try {
|
|
422
|
+
parsed = JSON.parse(bodyText);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
|
|
425
|
+
return expected.map((assertion) => ({
|
|
426
|
+
label: assertion.label || assertion.path,
|
|
427
|
+
path: assertion.path,
|
|
428
|
+
ok: false,
|
|
429
|
+
exists: false,
|
|
430
|
+
observed_type: "missing",
|
|
431
|
+
errors: [message]
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
|
|
435
|
+
}
|
|
283
436
|
function compactProfileSetupSummaryText(value, limit = 160) {
|
|
284
437
|
const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
285
438
|
if (!text) return void 0;
|
|
@@ -643,6 +796,20 @@ function normalizeNetworkMock(input, index) {
|
|
|
643
796
|
if (maxHitCount !== void 0 && effectiveRequiredHitCount > maxHitCount) {
|
|
644
797
|
throw new Error(`target.network_mocks[${index}].max_hit_count cannot be less than its required hit count.`);
|
|
645
798
|
}
|
|
799
|
+
const sequenceScopeInput = stringValue(
|
|
800
|
+
input.sequence_scope ?? input.sequenceScope ?? input.response_sequence_scope ?? input.responseSequenceScope
|
|
801
|
+
);
|
|
802
|
+
let sequenceScope;
|
|
803
|
+
if (sequenceScopeInput) {
|
|
804
|
+
const normalizedScope = sequenceScopeInput.toLowerCase().replace(/[-\s]+/g, "_");
|
|
805
|
+
if (normalizedScope === "global" || normalizedScope === "profile" || normalizedScope === "run") {
|
|
806
|
+
sequenceScope = "global";
|
|
807
|
+
} else if (normalizedScope === "viewport" || normalizedScope === "per_viewport" || normalizedScope === "viewport_scoped") {
|
|
808
|
+
sequenceScope = "viewport";
|
|
809
|
+
} else {
|
|
810
|
+
throw new Error(`target.network_mocks[${index}].sequence_scope must be "global" or "viewport".`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
646
813
|
return {
|
|
647
814
|
...payload,
|
|
648
815
|
label: normalizeName(input.label || input.name, `network-mock-${index + 1}`),
|
|
@@ -650,6 +817,7 @@ function normalizeNetworkMock(input, index) {
|
|
|
650
817
|
method: stringValue(input.method)?.toUpperCase(),
|
|
651
818
|
responses,
|
|
652
819
|
repeat_responses: input.repeat_responses === true || input.repeatResponses === true || input.cycle_responses === true || input.cycleResponses === true,
|
|
820
|
+
sequence_scope: sequenceScope,
|
|
653
821
|
required_hit_count: requiredHitCount,
|
|
654
822
|
max_hit_count: maxHitCount,
|
|
655
823
|
forbidden,
|
|
@@ -919,6 +1087,46 @@ function validateRegexPatterns(patterns, label) {
|
|
|
919
1087
|
}
|
|
920
1088
|
}
|
|
921
1089
|
}
|
|
1090
|
+
function normalizeHttpStatusBodyJsonAssertions(value, label) {
|
|
1091
|
+
if (value === void 0) return void 0;
|
|
1092
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
1093
|
+
if (!value.length) throw new Error(`${label} must not be empty.`);
|
|
1094
|
+
return value.map((item, index) => {
|
|
1095
|
+
const itemLabel = `${label}[${index}]`;
|
|
1096
|
+
if (typeof item === "string") {
|
|
1097
|
+
const path2 = stringValue(item);
|
|
1098
|
+
if (!path2) throw new Error(`${itemLabel} path must not be empty.`);
|
|
1099
|
+
return { path: path2, exists: true };
|
|
1100
|
+
}
|
|
1101
|
+
if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
|
|
1102
|
+
const path = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
|
|
1103
|
+
if (!path) throw new Error(`${itemLabel}.path is required.`);
|
|
1104
|
+
const assertion = {
|
|
1105
|
+
label: stringValue(item.label),
|
|
1106
|
+
path
|
|
1107
|
+
};
|
|
1108
|
+
const exists = booleanValue(valueFromOwn(item, "exists", "present"));
|
|
1109
|
+
if (exists !== void 0) assertion.exists = exists;
|
|
1110
|
+
const type = stringValue(valueFromOwn(item, "type", "value_type", "valueType"));
|
|
1111
|
+
if (type !== void 0) {
|
|
1112
|
+
const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
|
|
1113
|
+
if (!allowedTypes.includes(type)) {
|
|
1114
|
+
throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
|
|
1115
|
+
}
|
|
1116
|
+
assertion.type = type;
|
|
1117
|
+
}
|
|
1118
|
+
const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
|
|
1119
|
+
if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
|
|
1120
|
+
const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
|
|
1121
|
+
if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
|
|
1122
|
+
const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
|
|
1123
|
+
if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
|
|
1124
|
+
if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
|
|
1125
|
+
assertion.exists = true;
|
|
1126
|
+
}
|
|
1127
|
+
return assertion;
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
922
1130
|
function isDialogCountCheckType(type) {
|
|
923
1131
|
return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
|
|
924
1132
|
}
|
|
@@ -1018,6 +1226,10 @@ function normalizeCheck(input, index) {
|
|
|
1018
1226
|
`checks[${index}] body_not_patterns`
|
|
1019
1227
|
) : void 0;
|
|
1020
1228
|
if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
|
|
1229
|
+
const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
|
|
1230
|
+
input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
|
|
1231
|
+
`checks[${index}] body_json_assertions`
|
|
1232
|
+
) : void 0;
|
|
1021
1233
|
if (isLinkStatusCheck) {
|
|
1022
1234
|
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
1023
1235
|
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
@@ -1043,6 +1255,7 @@ function normalizeCheck(input, index) {
|
|
|
1043
1255
|
body_contains: bodyContains,
|
|
1044
1256
|
body_not_contains: bodyNotContains,
|
|
1045
1257
|
body_not_patterns: bodyNotPatterns,
|
|
1258
|
+
body_json_assertions: bodyJsonAssertions,
|
|
1046
1259
|
expected_texts: expectedTexts,
|
|
1047
1260
|
link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
|
|
1048
1261
|
source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
|
|
@@ -1249,6 +1462,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
1249
1462
|
const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
|
|
1250
1463
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
1251
1464
|
}
|
|
1465
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
1466
|
+
const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
|
|
1467
|
+
if (!expected.length) return [];
|
|
1468
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
1469
|
+
return expected.map((assertion) => ({
|
|
1470
|
+
label: assertion.label || assertion.path,
|
|
1471
|
+
path: assertion.path,
|
|
1472
|
+
ok: false,
|
|
1473
|
+
exists: false,
|
|
1474
|
+
observed_type: "missing",
|
|
1475
|
+
errors: ["body_json_assertions evidence missing"]
|
|
1476
|
+
}));
|
|
1477
|
+
}
|
|
1478
|
+
return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
|
|
1479
|
+
label: stringValue(assertion.label) || stringValue(assertion.path) || "json assertion",
|
|
1480
|
+
path: stringValue(assertion.path) || "",
|
|
1481
|
+
ok: false,
|
|
1482
|
+
exists: assertion.exists === true,
|
|
1483
|
+
observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
|
|
1484
|
+
observed_type: stringValue(assertion.observed_type) || "missing",
|
|
1485
|
+
expected_exists: booleanValue(assertion.expected_exists),
|
|
1486
|
+
equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
|
|
1487
|
+
not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
|
|
1488
|
+
contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
|
|
1489
|
+
type: stringValue(assertion.type),
|
|
1490
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
|
|
1491
|
+
}));
|
|
1492
|
+
}
|
|
1252
1493
|
function linkStatusResultOk(result, check) {
|
|
1253
1494
|
const status = numberValue(result.status);
|
|
1254
1495
|
if (!httpStatusIsAllowed(status, check)) return false;
|
|
@@ -1266,6 +1507,7 @@ function linkStatusResultOk(result, check) {
|
|
|
1266
1507
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
1267
1508
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
1268
1509
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
1510
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
1269
1511
|
return true;
|
|
1270
1512
|
}
|
|
1271
1513
|
function responseHeader(response, name) {
|
|
@@ -1334,7 +1576,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1334
1576
|
statusText = typeof response.statusText === "string" ? response.statusText : "";
|
|
1335
1577
|
result.content_type = responseHeader(response, "content-type");
|
|
1336
1578
|
result.content_length = responseContentLength(response);
|
|
1337
|
-
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);
|
|
1579
|
+
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);
|
|
1338
1580
|
if (shouldReadBody && method !== "HEAD") {
|
|
1339
1581
|
const body = await responseBodyText(response);
|
|
1340
1582
|
result.bytes = body.bytes;
|
|
@@ -1347,6 +1589,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1347
1589
|
if (check.body_not_patterns?.length) {
|
|
1348
1590
|
result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
|
|
1349
1591
|
}
|
|
1592
|
+
if (check.body_json_assertions?.length) {
|
|
1593
|
+
result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
|
|
1594
|
+
}
|
|
1350
1595
|
}
|
|
1351
1596
|
} catch (caught) {
|
|
1352
1597
|
error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
|
|
@@ -1355,6 +1600,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1355
1600
|
const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
|
|
1356
1601
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
|
|
1357
1602
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
|
|
1603
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
|
|
1358
1604
|
const ok = !error && linkStatusResultOk(result, check);
|
|
1359
1605
|
return {
|
|
1360
1606
|
index,
|
|
@@ -1373,7 +1619,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1373
1619
|
body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
|
|
1374
1620
|
body_not_contains_found: bodyNotContainsFound,
|
|
1375
1621
|
body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
|
|
1376
|
-
body_not_patterns_found: bodyNotPatternsFound
|
|
1622
|
+
body_not_patterns_found: bodyNotPatternsFound,
|
|
1623
|
+
body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
|
|
1624
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed
|
|
1377
1625
|
};
|
|
1378
1626
|
}
|
|
1379
1627
|
async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
|
|
@@ -1418,6 +1666,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1418
1666
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
1419
1667
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
1420
1668
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
1669
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
1421
1670
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
1422
1671
|
failures.push({
|
|
1423
1672
|
code: "http_status_failed",
|
|
@@ -1436,6 +1685,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1436
1685
|
body_not_contains_found: bodyNotContainsFound,
|
|
1437
1686
|
body_not_patterns: check.body_not_patterns ?? null,
|
|
1438
1687
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
1688
|
+
body_json_assertions: check.body_json_assertions ?? null,
|
|
1689
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
1439
1690
|
body_sample: stringValue(statusEvidence.body_sample) ?? null
|
|
1440
1691
|
});
|
|
1441
1692
|
}
|
|
@@ -1457,6 +1708,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1457
1708
|
body_not_contains_found: bodyNotContainsFound,
|
|
1458
1709
|
body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
|
|
1459
1710
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
1711
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
|
|
1712
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
1460
1713
|
body_sample: stringValue(statusEvidence.body_sample) ?? null,
|
|
1461
1714
|
failures
|
|
1462
1715
|
};
|
|
@@ -2093,6 +2346,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
2093
2346
|
body_contains: check.body_contains ?? [],
|
|
2094
2347
|
body_not_contains: check.body_not_contains ?? [],
|
|
2095
2348
|
body_not_patterns: check.body_not_patterns ?? [],
|
|
2349
|
+
body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
|
|
2096
2350
|
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
2097
2351
|
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
|
|
2098
2352
|
},
|
|
@@ -2836,6 +3090,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
2836
3090
|
: {};
|
|
2837
3091
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
2838
3092
|
}
|
|
3093
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
3094
|
+
const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
3095
|
+
if (!expected.length) return [];
|
|
3096
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
3097
|
+
return expected.map((assertion) => ({
|
|
3098
|
+
label: assertion.label || assertion.path,
|
|
3099
|
+
path: assertion.path,
|
|
3100
|
+
ok: false,
|
|
3101
|
+
exists: false,
|
|
3102
|
+
observed_type: "missing",
|
|
3103
|
+
errors: ["body_json_assertions evidence missing"],
|
|
3104
|
+
}));
|
|
3105
|
+
}
|
|
3106
|
+
return result.body_json_assertions
|
|
3107
|
+
.filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
|
|
3108
|
+
.map((assertion) => ({
|
|
3109
|
+
label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
|
|
3110
|
+
path: typeof assertion.path === "string" ? assertion.path : "",
|
|
3111
|
+
ok: false,
|
|
3112
|
+
exists: assertion.exists === true,
|
|
3113
|
+
observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
|
|
3114
|
+
observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
|
|
3115
|
+
expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
|
|
3116
|
+
equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
|
|
3117
|
+
not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
|
|
3118
|
+
contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
|
|
3119
|
+
type: typeof assertion.type === "string" ? assertion.type : undefined,
|
|
3120
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
|
|
3121
|
+
}));
|
|
3122
|
+
}
|
|
2839
3123
|
function linkStatusResultOk(result, check) {
|
|
2840
3124
|
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
2841
3125
|
if (!httpStatusIsAllowed(result.status, check)) return false;
|
|
@@ -2853,6 +3137,7 @@ function linkStatusResultOk(result, check) {
|
|
|
2853
3137
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
2854
3138
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
2855
3139
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
3140
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
2856
3141
|
return true;
|
|
2857
3142
|
}
|
|
2858
3143
|
function summarizeHttpStatusEvidence(viewport, check) {
|
|
@@ -2873,6 +3158,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2873
3158
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
2874
3159
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
2875
3160
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
3161
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
2876
3162
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
2877
3163
|
failures.push({
|
|
2878
3164
|
code: "http_status_failed",
|
|
@@ -2891,6 +3177,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2891
3177
|
body_not_contains_found: bodyNotContainsFound,
|
|
2892
3178
|
body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
|
|
2893
3179
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
3180
|
+
body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
|
|
3181
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
2894
3182
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2895
3183
|
});
|
|
2896
3184
|
}
|
|
@@ -2918,6 +3206,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2918
3206
|
? statusEvidence.body_not_patterns
|
|
2919
3207
|
: null,
|
|
2920
3208
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
3209
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
|
|
3210
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
2921
3211
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2922
3212
|
failures,
|
|
2923
3213
|
};
|
|
@@ -4301,6 +4591,7 @@ async function setupLocatorVisible(locator, index) {
|
|
|
4301
4591
|
async function registerNetworkMocks(mocks) {
|
|
4302
4592
|
for (const mock of mocks || []) {
|
|
4303
4593
|
let hitCount = 0;
|
|
4594
|
+
const scopedHitCounts = {};
|
|
4304
4595
|
await page.route(mock.url, async (route) => {
|
|
4305
4596
|
const request = route.request();
|
|
4306
4597
|
const method = request.method ? request.method() : "";
|
|
@@ -4316,8 +4607,13 @@ async function registerNetworkMocks(mocks) {
|
|
|
4316
4607
|
const responses = Array.isArray(mock.responses) ? mock.responses : [];
|
|
4317
4608
|
const hitIndex = hitCount;
|
|
4318
4609
|
hitCount += 1;
|
|
4610
|
+
const sequenceScope = mock.sequence_scope === "viewport" ? "viewport" : "global";
|
|
4611
|
+
const viewportName = activeViewportName || null;
|
|
4612
|
+
const sequenceScopeKey = sequenceScope === "viewport" ? (viewportName || "__unknown_viewport__") : "__global__";
|
|
4613
|
+
const sequenceHitIndex = sequenceScope === "viewport" ? (scopedHitCounts[sequenceScopeKey] || 0) : hitIndex;
|
|
4614
|
+
if (sequenceScope === "viewport") scopedHitCounts[sequenceScopeKey] = sequenceHitIndex + 1;
|
|
4319
4615
|
const sequenceResponseIndex = responses.length
|
|
4320
|
-
? (mock.repeat_responses ?
|
|
4616
|
+
? (mock.repeat_responses ? sequenceHitIndex % responses.length : Math.min(sequenceHitIndex, responses.length - 1))
|
|
4321
4617
|
: null;
|
|
4322
4618
|
let responseIndex = sequenceResponseIndex;
|
|
4323
4619
|
let responseSelection = responseIndex === null ? "mock" : "sequence";
|
|
@@ -4352,11 +4648,14 @@ async function registerNetworkMocks(mocks) {
|
|
|
4352
4648
|
label: mock.label,
|
|
4353
4649
|
response_label: response.label || null,
|
|
4354
4650
|
hit_index: hitIndex,
|
|
4651
|
+
sequence_hit_index: responseIndex === null ? undefined : sequenceHitIndex,
|
|
4652
|
+
sequence_scope: responseIndex === null ? undefined : sequenceScope,
|
|
4653
|
+
viewport: viewportName,
|
|
4355
4654
|
response_index: responseIndex,
|
|
4356
4655
|
sequence_response_index: responseSelection === "request_body" ? sequenceResponseIndex : undefined,
|
|
4357
4656
|
response_selection: responseIndex === null ? null : responseSelection,
|
|
4358
|
-
sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses &&
|
|
4359
|
-
sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true &&
|
|
4657
|
+
sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses && sequenceHitIndex >= responses.length,
|
|
4658
|
+
sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true && sequenceHitIndex >= responses.length,
|
|
4360
4659
|
url: request.url(),
|
|
4361
4660
|
method,
|
|
4362
4661
|
};
|
|
@@ -4398,6 +4697,7 @@ async function registerNetworkMocks(mocks) {
|
|
|
4398
4697
|
});
|
|
4399
4698
|
}
|
|
4400
4699
|
}
|
|
4700
|
+
let activeViewportName = null;
|
|
4401
4701
|
async function executeSetupAction(action, ordinal, viewport) {
|
|
4402
4702
|
const type = setupActionType(action);
|
|
4403
4703
|
const frameSelector = setupFrameSelector(action);
|
|
@@ -5010,6 +5310,155 @@ function linkProbeResponseFields(response, method) {
|
|
|
5010
5310
|
content_length: contentLength,
|
|
5011
5311
|
};
|
|
5012
5312
|
}
|
|
5313
|
+
function jsonProbeValueType(value) {
|
|
5314
|
+
if (value === null) return "null";
|
|
5315
|
+
if (Array.isArray(value)) return "array";
|
|
5316
|
+
if (typeof value === "boolean") return "boolean";
|
|
5317
|
+
if (typeof value === "number") return "number";
|
|
5318
|
+
if (typeof value === "string") return "string";
|
|
5319
|
+
return "object";
|
|
5320
|
+
}
|
|
5321
|
+
function jsonProbeDeepEqual(left, right) {
|
|
5322
|
+
if (left === right) return true;
|
|
5323
|
+
if (typeof left !== typeof right) return false;
|
|
5324
|
+
if (left === null || right === null) return left === right;
|
|
5325
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
5326
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
5327
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
5328
|
+
return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
|
|
5329
|
+
}
|
|
5330
|
+
const leftKeys = Object.keys(left).sort();
|
|
5331
|
+
const rightKeys = Object.keys(right).sort();
|
|
5332
|
+
if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
|
|
5333
|
+
return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
|
|
5334
|
+
}
|
|
5335
|
+
function jsonProbeContains(observed, expected) {
|
|
5336
|
+
if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
|
|
5337
|
+
if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
|
|
5338
|
+
if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
|
|
5339
|
+
return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
|
|
5340
|
+
}
|
|
5341
|
+
return false;
|
|
5342
|
+
}
|
|
5343
|
+
function parseJsonProbePathSegments(path) {
|
|
5344
|
+
let input = String(path || "").trim();
|
|
5345
|
+
if (!input) throw new Error("path is empty");
|
|
5346
|
+
if (input === "$") return [];
|
|
5347
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
5348
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
5349
|
+
const segments = [];
|
|
5350
|
+
let token = "";
|
|
5351
|
+
const pushToken = () => {
|
|
5352
|
+
if (!token) return;
|
|
5353
|
+
segments.push(token);
|
|
5354
|
+
token = "";
|
|
5355
|
+
};
|
|
5356
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
5357
|
+
const char = input[index];
|
|
5358
|
+
if (char === ".") {
|
|
5359
|
+
pushToken();
|
|
5360
|
+
continue;
|
|
5361
|
+
}
|
|
5362
|
+
if (char !== "[") {
|
|
5363
|
+
token += char;
|
|
5364
|
+
continue;
|
|
5365
|
+
}
|
|
5366
|
+
pushToken();
|
|
5367
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
5368
|
+
if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
|
|
5369
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
5370
|
+
if (!bracket) throw new Error("empty bracket at " + index);
|
|
5371
|
+
if (/^\d+$/.test(bracket)) {
|
|
5372
|
+
segments.push(Number(bracket));
|
|
5373
|
+
} else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
|
|
5374
|
+
const quoted = bracket.startsWith("'")
|
|
5375
|
+
? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
|
|
5376
|
+
: bracket;
|
|
5377
|
+
segments.push(String(JSON.parse(quoted)));
|
|
5378
|
+
} else {
|
|
5379
|
+
segments.push(bracket);
|
|
5380
|
+
}
|
|
5381
|
+
index = closeIndex;
|
|
5382
|
+
}
|
|
5383
|
+
pushToken();
|
|
5384
|
+
return segments;
|
|
5385
|
+
}
|
|
5386
|
+
function resolveJsonProbePath(root, path) {
|
|
5387
|
+
let segments;
|
|
5388
|
+
try {
|
|
5389
|
+
segments = parseJsonProbePathSegments(path);
|
|
5390
|
+
} catch (error) {
|
|
5391
|
+
return { exists: false, error: String(error && error.message ? error.message : error) };
|
|
5392
|
+
}
|
|
5393
|
+
let current = root;
|
|
5394
|
+
for (const segment of segments) {
|
|
5395
|
+
if (typeof segment === "number") {
|
|
5396
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
5397
|
+
current = current[segment];
|
|
5398
|
+
continue;
|
|
5399
|
+
}
|
|
5400
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
|
|
5401
|
+
return { exists: false };
|
|
5402
|
+
}
|
|
5403
|
+
current = current[segment];
|
|
5404
|
+
}
|
|
5405
|
+
return { exists: true, value: current };
|
|
5406
|
+
}
|
|
5407
|
+
function evaluateJsonProbeAssertion(root, assertion) {
|
|
5408
|
+
const resolved = resolveJsonProbePath(root, assertion.path);
|
|
5409
|
+
const errors = [];
|
|
5410
|
+
const result = {
|
|
5411
|
+
label: assertion.label || assertion.path,
|
|
5412
|
+
path: assertion.path,
|
|
5413
|
+
ok: true,
|
|
5414
|
+
exists: resolved.exists,
|
|
5415
|
+
observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
|
|
5416
|
+
};
|
|
5417
|
+
if (resolved.exists) result.observed = resolved.value;
|
|
5418
|
+
if (resolved.error) errors.push(resolved.error);
|
|
5419
|
+
if (Object.hasOwn(assertion, "exists")) {
|
|
5420
|
+
result.expected_exists = assertion.exists;
|
|
5421
|
+
if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
|
|
5422
|
+
}
|
|
5423
|
+
if (Object.hasOwn(assertion, "type")) {
|
|
5424
|
+
result.type = assertion.type;
|
|
5425
|
+
if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
|
|
5426
|
+
}
|
|
5427
|
+
if (Object.hasOwn(assertion, "equals")) {
|
|
5428
|
+
result.equals = assertion.equals;
|
|
5429
|
+
if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
5430
|
+
}
|
|
5431
|
+
if (Object.hasOwn(assertion, "not_equals")) {
|
|
5432
|
+
result.not_equals = assertion.not_equals;
|
|
5433
|
+
if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
5434
|
+
}
|
|
5435
|
+
if (Object.hasOwn(assertion, "contains")) {
|
|
5436
|
+
result.contains = assertion.contains;
|
|
5437
|
+
if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
5438
|
+
}
|
|
5439
|
+
result.ok = errors.length === 0;
|
|
5440
|
+
if (errors.length) result.errors = errors;
|
|
5441
|
+
return result;
|
|
5442
|
+
}
|
|
5443
|
+
function evaluateJsonProbeAssertions(text, assertions) {
|
|
5444
|
+
const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
5445
|
+
if (!expected.length) return [];
|
|
5446
|
+
let parsed;
|
|
5447
|
+
try {
|
|
5448
|
+
parsed = JSON.parse(text);
|
|
5449
|
+
} catch (error) {
|
|
5450
|
+
const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
|
|
5451
|
+
return expected.map((assertion) => ({
|
|
5452
|
+
label: assertion.label || assertion.path,
|
|
5453
|
+
path: assertion.path,
|
|
5454
|
+
ok: false,
|
|
5455
|
+
exists: false,
|
|
5456
|
+
observed_type: "missing",
|
|
5457
|
+
errors: [message],
|
|
5458
|
+
}));
|
|
5459
|
+
}
|
|
5460
|
+
return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
|
|
5461
|
+
}
|
|
5013
5462
|
async function collectHttpStatus(check) {
|
|
5014
5463
|
const url = httpStatusRequestUrl(check, page.url() || targetUrl);
|
|
5015
5464
|
const method = httpStatusMethod(check);
|
|
@@ -5026,6 +5475,7 @@ async function collectHttpStatus(check) {
|
|
|
5026
5475
|
const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
|
|
5027
5476
|
const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
|
|
5028
5477
|
const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
|
|
5478
|
+
const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
5029
5479
|
const options = {
|
|
5030
5480
|
method,
|
|
5031
5481
|
redirect: "follow",
|
|
@@ -5051,17 +5501,18 @@ async function collectHttpStatus(check) {
|
|
|
5051
5501
|
Object.assign(result, linkProbeResponseFields(response, method));
|
|
5052
5502
|
result.url = url;
|
|
5053
5503
|
result.status_text = response.statusText || "";
|
|
5054
|
-
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;
|
|
5504
|
+
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;
|
|
5055
5505
|
if (shouldReadBody) {
|
|
5056
5506
|
try {
|
|
5057
5507
|
const buffer = await response.arrayBuffer();
|
|
5058
5508
|
result.bytes = buffer.byteLength;
|
|
5059
|
-
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
|
|
5509
|
+
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
|
|
5060
5510
|
const text = new TextDecoder().decode(buffer);
|
|
5061
5511
|
result.body_sample = text.slice(0, 1000);
|
|
5062
5512
|
if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
|
|
5063
5513
|
if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
|
|
5064
5514
|
if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
|
|
5515
|
+
if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
|
|
5065
5516
|
}
|
|
5066
5517
|
} catch (error) {
|
|
5067
5518
|
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
@@ -5074,6 +5525,7 @@ async function collectHttpStatus(check) {
|
|
|
5074
5525
|
&& (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
|
|
5075
5526
|
&& (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
|
|
5076
5527
|
&& (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
|
|
5528
|
+
&& (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
|
|
5077
5529
|
&& !result.error;
|
|
5078
5530
|
return result;
|
|
5079
5531
|
} catch (error) {
|
|
@@ -5660,6 +6112,7 @@ async function collectRouteInventory(check, viewport) {
|
|
|
5660
6112
|
};
|
|
5661
6113
|
}
|
|
5662
6114
|
async function captureViewport(viewport) {
|
|
6115
|
+
activeViewportName = viewport && viewport.name ? viewport.name : null;
|
|
5663
6116
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
5664
6117
|
let httpStatus = null;
|
|
5665
6118
|
let navigationError;
|