@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
|
@@ -139,6 +139,9 @@ function valueFromOwn(input, ...keys) {
|
|
|
139
139
|
function numberValue(value) {
|
|
140
140
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
141
141
|
}
|
|
142
|
+
function booleanValue(value) {
|
|
143
|
+
return typeof value === "boolean" ? value : void 0;
|
|
144
|
+
}
|
|
142
145
|
function horizontalBoundsOverflowPx(value) {
|
|
143
146
|
if (!isRecord(value)) return 0;
|
|
144
147
|
let max = maxPositiveNumber(
|
|
@@ -233,6 +236,156 @@ function toJsonValue(value) {
|
|
|
233
236
|
if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
|
|
234
237
|
return String(value);
|
|
235
238
|
}
|
|
239
|
+
function jsonValueType(value) {
|
|
240
|
+
if (value === null) return "null";
|
|
241
|
+
if (Array.isArray(value)) return "array";
|
|
242
|
+
if (typeof value === "boolean") return "boolean";
|
|
243
|
+
if (typeof value === "number") return "number";
|
|
244
|
+
if (typeof value === "string") return "string";
|
|
245
|
+
return "object";
|
|
246
|
+
}
|
|
247
|
+
function deepJsonEqual(left, right) {
|
|
248
|
+
if (left === right) return true;
|
|
249
|
+
if (typeof left !== typeof right) return false;
|
|
250
|
+
if (left === null || right === null) return left === right;
|
|
251
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
252
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
253
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
254
|
+
return left.every((item, index) => deepJsonEqual(item, right[index]));
|
|
255
|
+
}
|
|
256
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
257
|
+
const leftKeys = Object.keys(left).sort();
|
|
258
|
+
const rightKeys = Object.keys(right).sort();
|
|
259
|
+
if (!deepJsonEqual(leftKeys, rightKeys)) return false;
|
|
260
|
+
return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
|
|
261
|
+
}
|
|
262
|
+
function jsonContains(observed, expected) {
|
|
263
|
+
if (typeof observed === "string" && typeof expected === "string") {
|
|
264
|
+
return observed.includes(expected);
|
|
265
|
+
}
|
|
266
|
+
if (Array.isArray(observed)) {
|
|
267
|
+
return observed.some((item) => deepJsonEqual(item, expected));
|
|
268
|
+
}
|
|
269
|
+
if (isRecord(observed) && isRecord(expected)) {
|
|
270
|
+
return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
function parseJsonPathSegments(path) {
|
|
275
|
+
let input = path.trim();
|
|
276
|
+
if (!input) throw new Error("path is empty");
|
|
277
|
+
if (input === "$") return [];
|
|
278
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
279
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
280
|
+
const segments = [];
|
|
281
|
+
let token = "";
|
|
282
|
+
const pushToken = () => {
|
|
283
|
+
if (!token) return;
|
|
284
|
+
segments.push(token);
|
|
285
|
+
token = "";
|
|
286
|
+
};
|
|
287
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
288
|
+
const char = input[index];
|
|
289
|
+
if (char === ".") {
|
|
290
|
+
pushToken();
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (char !== "[") {
|
|
294
|
+
token += char;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
pushToken();
|
|
298
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
299
|
+
if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
|
|
300
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
301
|
+
if (!bracket) throw new Error(`empty bracket at ${index}`);
|
|
302
|
+
if (/^\d+$/.test(bracket)) {
|
|
303
|
+
segments.push(Number(bracket));
|
|
304
|
+
} else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
|
|
305
|
+
const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
|
|
306
|
+
segments.push(String(JSON.parse(quoted)));
|
|
307
|
+
} else {
|
|
308
|
+
segments.push(bracket);
|
|
309
|
+
}
|
|
310
|
+
index = closeIndex;
|
|
311
|
+
}
|
|
312
|
+
pushToken();
|
|
313
|
+
return segments;
|
|
314
|
+
}
|
|
315
|
+
function resolveJsonPath(root, path) {
|
|
316
|
+
let segments;
|
|
317
|
+
try {
|
|
318
|
+
segments = parseJsonPathSegments(path);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return { exists: false, error: String(error instanceof Error ? error.message : error) };
|
|
321
|
+
}
|
|
322
|
+
let current = root;
|
|
323
|
+
for (const segment of segments) {
|
|
324
|
+
if (typeof segment === "number") {
|
|
325
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
326
|
+
current = current[segment];
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
|
|
330
|
+
current = current[segment];
|
|
331
|
+
}
|
|
332
|
+
return { exists: true, value: current };
|
|
333
|
+
}
|
|
334
|
+
function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
|
|
335
|
+
const resolved = resolveJsonPath(root, assertion.path);
|
|
336
|
+
const errors = [];
|
|
337
|
+
const result = {
|
|
338
|
+
label: assertion.label || assertion.path,
|
|
339
|
+
path: assertion.path,
|
|
340
|
+
ok: true,
|
|
341
|
+
exists: resolved.exists,
|
|
342
|
+
observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
|
|
343
|
+
};
|
|
344
|
+
if (resolved.exists) result.observed = toJsonValue(resolved.value);
|
|
345
|
+
if (resolved.error) errors.push(resolved.error);
|
|
346
|
+
if (hasOwn(assertion, "exists")) {
|
|
347
|
+
result.expected_exists = assertion.exists;
|
|
348
|
+
if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
|
|
349
|
+
}
|
|
350
|
+
if (hasOwn(assertion, "type")) {
|
|
351
|
+
result.type = assertion.type;
|
|
352
|
+
if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
|
|
353
|
+
}
|
|
354
|
+
if (hasOwn(assertion, "equals")) {
|
|
355
|
+
result.equals = assertion.equals;
|
|
356
|
+
if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
357
|
+
}
|
|
358
|
+
if (hasOwn(assertion, "not_equals")) {
|
|
359
|
+
result.not_equals = assertion.not_equals;
|
|
360
|
+
if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
361
|
+
}
|
|
362
|
+
if (hasOwn(assertion, "contains")) {
|
|
363
|
+
result.contains = assertion.contains;
|
|
364
|
+
if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
365
|
+
}
|
|
366
|
+
result.ok = errors.length === 0;
|
|
367
|
+
if (errors.length) result.errors = errors;
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
|
|
371
|
+
const expected = assertions?.filter((assertion) => assertion.path) ?? [];
|
|
372
|
+
if (!expected.length) return [];
|
|
373
|
+
let parsed;
|
|
374
|
+
try {
|
|
375
|
+
parsed = JSON.parse(bodyText);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
|
|
378
|
+
return expected.map((assertion) => ({
|
|
379
|
+
label: assertion.label || assertion.path,
|
|
380
|
+
path: assertion.path,
|
|
381
|
+
ok: false,
|
|
382
|
+
exists: false,
|
|
383
|
+
observed_type: "missing",
|
|
384
|
+
errors: [message]
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
|
|
388
|
+
}
|
|
236
389
|
function compactProfileSetupSummaryText(value, limit = 160) {
|
|
237
390
|
const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
238
391
|
if (!text) return void 0;
|
|
@@ -596,6 +749,20 @@ function normalizeNetworkMock(input, index) {
|
|
|
596
749
|
if (maxHitCount !== void 0 && effectiveRequiredHitCount > maxHitCount) {
|
|
597
750
|
throw new Error(`target.network_mocks[${index}].max_hit_count cannot be less than its required hit count.`);
|
|
598
751
|
}
|
|
752
|
+
const sequenceScopeInput = stringValue(
|
|
753
|
+
input.sequence_scope ?? input.sequenceScope ?? input.response_sequence_scope ?? input.responseSequenceScope
|
|
754
|
+
);
|
|
755
|
+
let sequenceScope;
|
|
756
|
+
if (sequenceScopeInput) {
|
|
757
|
+
const normalizedScope = sequenceScopeInput.toLowerCase().replace(/[-\s]+/g, "_");
|
|
758
|
+
if (normalizedScope === "global" || normalizedScope === "profile" || normalizedScope === "run") {
|
|
759
|
+
sequenceScope = "global";
|
|
760
|
+
} else if (normalizedScope === "viewport" || normalizedScope === "per_viewport" || normalizedScope === "viewport_scoped") {
|
|
761
|
+
sequenceScope = "viewport";
|
|
762
|
+
} else {
|
|
763
|
+
throw new Error(`target.network_mocks[${index}].sequence_scope must be "global" or "viewport".`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
599
766
|
return {
|
|
600
767
|
...payload,
|
|
601
768
|
label: normalizeName(input.label || input.name, `network-mock-${index + 1}`),
|
|
@@ -603,6 +770,7 @@ function normalizeNetworkMock(input, index) {
|
|
|
603
770
|
method: stringValue(input.method)?.toUpperCase(),
|
|
604
771
|
responses,
|
|
605
772
|
repeat_responses: input.repeat_responses === true || input.repeatResponses === true || input.cycle_responses === true || input.cycleResponses === true,
|
|
773
|
+
sequence_scope: sequenceScope,
|
|
606
774
|
required_hit_count: requiredHitCount,
|
|
607
775
|
max_hit_count: maxHitCount,
|
|
608
776
|
forbidden,
|
|
@@ -872,6 +1040,46 @@ function validateRegexPatterns(patterns, label) {
|
|
|
872
1040
|
}
|
|
873
1041
|
}
|
|
874
1042
|
}
|
|
1043
|
+
function normalizeHttpStatusBodyJsonAssertions(value, label) {
|
|
1044
|
+
if (value === void 0) return void 0;
|
|
1045
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
1046
|
+
if (!value.length) throw new Error(`${label} must not be empty.`);
|
|
1047
|
+
return value.map((item, index) => {
|
|
1048
|
+
const itemLabel = `${label}[${index}]`;
|
|
1049
|
+
if (typeof item === "string") {
|
|
1050
|
+
const path2 = stringValue(item);
|
|
1051
|
+
if (!path2) throw new Error(`${itemLabel} path must not be empty.`);
|
|
1052
|
+
return { path: path2, exists: true };
|
|
1053
|
+
}
|
|
1054
|
+
if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
|
|
1055
|
+
const path = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
|
|
1056
|
+
if (!path) throw new Error(`${itemLabel}.path is required.`);
|
|
1057
|
+
const assertion = {
|
|
1058
|
+
label: stringValue(item.label),
|
|
1059
|
+
path
|
|
1060
|
+
};
|
|
1061
|
+
const exists = booleanValue(valueFromOwn(item, "exists", "present"));
|
|
1062
|
+
if (exists !== void 0) assertion.exists = exists;
|
|
1063
|
+
const type = stringValue(valueFromOwn(item, "type", "value_type", "valueType"));
|
|
1064
|
+
if (type !== void 0) {
|
|
1065
|
+
const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
|
|
1066
|
+
if (!allowedTypes.includes(type)) {
|
|
1067
|
+
throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
|
|
1068
|
+
}
|
|
1069
|
+
assertion.type = type;
|
|
1070
|
+
}
|
|
1071
|
+
const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
|
|
1072
|
+
if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
|
|
1073
|
+
const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
|
|
1074
|
+
if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
|
|
1075
|
+
const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
|
|
1076
|
+
if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
|
|
1077
|
+
if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
|
|
1078
|
+
assertion.exists = true;
|
|
1079
|
+
}
|
|
1080
|
+
return assertion;
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
875
1083
|
function isDialogCountCheckType(type) {
|
|
876
1084
|
return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
|
|
877
1085
|
}
|
|
@@ -971,6 +1179,10 @@ function normalizeCheck(input, index) {
|
|
|
971
1179
|
`checks[${index}] body_not_patterns`
|
|
972
1180
|
) : void 0;
|
|
973
1181
|
if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
|
|
1182
|
+
const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
|
|
1183
|
+
input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
|
|
1184
|
+
`checks[${index}] body_json_assertions`
|
|
1185
|
+
) : void 0;
|
|
974
1186
|
if (isLinkStatusCheck) {
|
|
975
1187
|
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
976
1188
|
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
@@ -996,6 +1208,7 @@ function normalizeCheck(input, index) {
|
|
|
996
1208
|
body_contains: bodyContains,
|
|
997
1209
|
body_not_contains: bodyNotContains,
|
|
998
1210
|
body_not_patterns: bodyNotPatterns,
|
|
1211
|
+
body_json_assertions: bodyJsonAssertions,
|
|
999
1212
|
expected_texts: expectedTexts,
|
|
1000
1213
|
link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
|
|
1001
1214
|
source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
|
|
@@ -1202,6 +1415,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
1202
1415
|
const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
|
|
1203
1416
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
1204
1417
|
}
|
|
1418
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
1419
|
+
const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
|
|
1420
|
+
if (!expected.length) return [];
|
|
1421
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
1422
|
+
return expected.map((assertion) => ({
|
|
1423
|
+
label: assertion.label || assertion.path,
|
|
1424
|
+
path: assertion.path,
|
|
1425
|
+
ok: false,
|
|
1426
|
+
exists: false,
|
|
1427
|
+
observed_type: "missing",
|
|
1428
|
+
errors: ["body_json_assertions evidence missing"]
|
|
1429
|
+
}));
|
|
1430
|
+
}
|
|
1431
|
+
return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
|
|
1432
|
+
label: stringValue(assertion.label) || stringValue(assertion.path) || "json assertion",
|
|
1433
|
+
path: stringValue(assertion.path) || "",
|
|
1434
|
+
ok: false,
|
|
1435
|
+
exists: assertion.exists === true,
|
|
1436
|
+
observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
|
|
1437
|
+
observed_type: stringValue(assertion.observed_type) || "missing",
|
|
1438
|
+
expected_exists: booleanValue(assertion.expected_exists),
|
|
1439
|
+
equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
|
|
1440
|
+
not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
|
|
1441
|
+
contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
|
|
1442
|
+
type: stringValue(assertion.type),
|
|
1443
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
|
|
1444
|
+
}));
|
|
1445
|
+
}
|
|
1205
1446
|
function linkStatusResultOk(result, check) {
|
|
1206
1447
|
const status = numberValue(result.status);
|
|
1207
1448
|
if (!httpStatusIsAllowed(status, check)) return false;
|
|
@@ -1219,6 +1460,7 @@ function linkStatusResultOk(result, check) {
|
|
|
1219
1460
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
1220
1461
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
1221
1462
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
1463
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
1222
1464
|
return true;
|
|
1223
1465
|
}
|
|
1224
1466
|
function responseHeader(response, name) {
|
|
@@ -1287,7 +1529,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1287
1529
|
statusText = typeof response.statusText === "string" ? response.statusText : "";
|
|
1288
1530
|
result.content_type = responseHeader(response, "content-type");
|
|
1289
1531
|
result.content_length = responseContentLength(response);
|
|
1290
|
-
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);
|
|
1532
|
+
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);
|
|
1291
1533
|
if (shouldReadBody && method !== "HEAD") {
|
|
1292
1534
|
const body = await responseBodyText(response);
|
|
1293
1535
|
result.bytes = body.bytes;
|
|
@@ -1300,6 +1542,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1300
1542
|
if (check.body_not_patterns?.length) {
|
|
1301
1543
|
result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
|
|
1302
1544
|
}
|
|
1545
|
+
if (check.body_json_assertions?.length) {
|
|
1546
|
+
result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
|
|
1547
|
+
}
|
|
1303
1548
|
}
|
|
1304
1549
|
} catch (caught) {
|
|
1305
1550
|
error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
|
|
@@ -1308,6 +1553,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1308
1553
|
const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
|
|
1309
1554
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
|
|
1310
1555
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
|
|
1556
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
|
|
1311
1557
|
const ok = !error && linkStatusResultOk(result, check);
|
|
1312
1558
|
return {
|
|
1313
1559
|
index,
|
|
@@ -1326,7 +1572,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1326
1572
|
body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
|
|
1327
1573
|
body_not_contains_found: bodyNotContainsFound,
|
|
1328
1574
|
body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
|
|
1329
|
-
body_not_patterns_found: bodyNotPatternsFound
|
|
1575
|
+
body_not_patterns_found: bodyNotPatternsFound,
|
|
1576
|
+
body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
|
|
1577
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed
|
|
1330
1578
|
};
|
|
1331
1579
|
}
|
|
1332
1580
|
async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
|
|
@@ -1371,6 +1619,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1371
1619
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
1372
1620
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
1373
1621
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
1622
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
1374
1623
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
1375
1624
|
failures.push({
|
|
1376
1625
|
code: "http_status_failed",
|
|
@@ -1389,6 +1638,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1389
1638
|
body_not_contains_found: bodyNotContainsFound,
|
|
1390
1639
|
body_not_patterns: check.body_not_patterns ?? null,
|
|
1391
1640
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
1641
|
+
body_json_assertions: check.body_json_assertions ?? null,
|
|
1642
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
1392
1643
|
body_sample: stringValue(statusEvidence.body_sample) ?? null
|
|
1393
1644
|
});
|
|
1394
1645
|
}
|
|
@@ -1410,6 +1661,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1410
1661
|
body_not_contains_found: bodyNotContainsFound,
|
|
1411
1662
|
body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
|
|
1412
1663
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
1664
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
|
|
1665
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
1413
1666
|
body_sample: stringValue(statusEvidence.body_sample) ?? null,
|
|
1414
1667
|
failures
|
|
1415
1668
|
};
|
|
@@ -2046,6 +2299,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
2046
2299
|
body_contains: check.body_contains ?? [],
|
|
2047
2300
|
body_not_contains: check.body_not_contains ?? [],
|
|
2048
2301
|
body_not_patterns: check.body_not_patterns ?? [],
|
|
2302
|
+
body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
|
|
2049
2303
|
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
2050
2304
|
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
|
|
2051
2305
|
},
|
|
@@ -2789,6 +3043,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
2789
3043
|
: {};
|
|
2790
3044
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
2791
3045
|
}
|
|
3046
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
3047
|
+
const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
3048
|
+
if (!expected.length) return [];
|
|
3049
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
3050
|
+
return expected.map((assertion) => ({
|
|
3051
|
+
label: assertion.label || assertion.path,
|
|
3052
|
+
path: assertion.path,
|
|
3053
|
+
ok: false,
|
|
3054
|
+
exists: false,
|
|
3055
|
+
observed_type: "missing",
|
|
3056
|
+
errors: ["body_json_assertions evidence missing"],
|
|
3057
|
+
}));
|
|
3058
|
+
}
|
|
3059
|
+
return result.body_json_assertions
|
|
3060
|
+
.filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
|
|
3061
|
+
.map((assertion) => ({
|
|
3062
|
+
label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
|
|
3063
|
+
path: typeof assertion.path === "string" ? assertion.path : "",
|
|
3064
|
+
ok: false,
|
|
3065
|
+
exists: assertion.exists === true,
|
|
3066
|
+
observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
|
|
3067
|
+
observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
|
|
3068
|
+
expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
|
|
3069
|
+
equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
|
|
3070
|
+
not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
|
|
3071
|
+
contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
|
|
3072
|
+
type: typeof assertion.type === "string" ? assertion.type : undefined,
|
|
3073
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
|
|
3074
|
+
}));
|
|
3075
|
+
}
|
|
2792
3076
|
function linkStatusResultOk(result, check) {
|
|
2793
3077
|
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
2794
3078
|
if (!httpStatusIsAllowed(result.status, check)) return false;
|
|
@@ -2806,6 +3090,7 @@ function linkStatusResultOk(result, check) {
|
|
|
2806
3090
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
2807
3091
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
2808
3092
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
3093
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
2809
3094
|
return true;
|
|
2810
3095
|
}
|
|
2811
3096
|
function summarizeHttpStatusEvidence(viewport, check) {
|
|
@@ -2826,6 +3111,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2826
3111
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
2827
3112
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
2828
3113
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
3114
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
2829
3115
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
2830
3116
|
failures.push({
|
|
2831
3117
|
code: "http_status_failed",
|
|
@@ -2844,6 +3130,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2844
3130
|
body_not_contains_found: bodyNotContainsFound,
|
|
2845
3131
|
body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
|
|
2846
3132
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
3133
|
+
body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
|
|
3134
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
2847
3135
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2848
3136
|
});
|
|
2849
3137
|
}
|
|
@@ -2871,6 +3159,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2871
3159
|
? statusEvidence.body_not_patterns
|
|
2872
3160
|
: null,
|
|
2873
3161
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
3162
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
|
|
3163
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
2874
3164
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2875
3165
|
failures,
|
|
2876
3166
|
};
|
|
@@ -4254,6 +4544,7 @@ async function setupLocatorVisible(locator, index) {
|
|
|
4254
4544
|
async function registerNetworkMocks(mocks) {
|
|
4255
4545
|
for (const mock of mocks || []) {
|
|
4256
4546
|
let hitCount = 0;
|
|
4547
|
+
const scopedHitCounts = {};
|
|
4257
4548
|
await page.route(mock.url, async (route) => {
|
|
4258
4549
|
const request = route.request();
|
|
4259
4550
|
const method = request.method ? request.method() : "";
|
|
@@ -4269,8 +4560,13 @@ async function registerNetworkMocks(mocks) {
|
|
|
4269
4560
|
const responses = Array.isArray(mock.responses) ? mock.responses : [];
|
|
4270
4561
|
const hitIndex = hitCount;
|
|
4271
4562
|
hitCount += 1;
|
|
4563
|
+
const sequenceScope = mock.sequence_scope === "viewport" ? "viewport" : "global";
|
|
4564
|
+
const viewportName = activeViewportName || null;
|
|
4565
|
+
const sequenceScopeKey = sequenceScope === "viewport" ? (viewportName || "__unknown_viewport__") : "__global__";
|
|
4566
|
+
const sequenceHitIndex = sequenceScope === "viewport" ? (scopedHitCounts[sequenceScopeKey] || 0) : hitIndex;
|
|
4567
|
+
if (sequenceScope === "viewport") scopedHitCounts[sequenceScopeKey] = sequenceHitIndex + 1;
|
|
4272
4568
|
const sequenceResponseIndex = responses.length
|
|
4273
|
-
? (mock.repeat_responses ?
|
|
4569
|
+
? (mock.repeat_responses ? sequenceHitIndex % responses.length : Math.min(sequenceHitIndex, responses.length - 1))
|
|
4274
4570
|
: null;
|
|
4275
4571
|
let responseIndex = sequenceResponseIndex;
|
|
4276
4572
|
let responseSelection = responseIndex === null ? "mock" : "sequence";
|
|
@@ -4305,11 +4601,14 @@ async function registerNetworkMocks(mocks) {
|
|
|
4305
4601
|
label: mock.label,
|
|
4306
4602
|
response_label: response.label || null,
|
|
4307
4603
|
hit_index: hitIndex,
|
|
4604
|
+
sequence_hit_index: responseIndex === null ? undefined : sequenceHitIndex,
|
|
4605
|
+
sequence_scope: responseIndex === null ? undefined : sequenceScope,
|
|
4606
|
+
viewport: viewportName,
|
|
4308
4607
|
response_index: responseIndex,
|
|
4309
4608
|
sequence_response_index: responseSelection === "request_body" ? sequenceResponseIndex : undefined,
|
|
4310
4609
|
response_selection: responseIndex === null ? null : responseSelection,
|
|
4311
|
-
sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses &&
|
|
4312
|
-
sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true &&
|
|
4610
|
+
sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses && sequenceHitIndex >= responses.length,
|
|
4611
|
+
sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true && sequenceHitIndex >= responses.length,
|
|
4313
4612
|
url: request.url(),
|
|
4314
4613
|
method,
|
|
4315
4614
|
};
|
|
@@ -4351,6 +4650,7 @@ async function registerNetworkMocks(mocks) {
|
|
|
4351
4650
|
});
|
|
4352
4651
|
}
|
|
4353
4652
|
}
|
|
4653
|
+
let activeViewportName = null;
|
|
4354
4654
|
async function executeSetupAction(action, ordinal, viewport) {
|
|
4355
4655
|
const type = setupActionType(action);
|
|
4356
4656
|
const frameSelector = setupFrameSelector(action);
|
|
@@ -4963,6 +5263,155 @@ function linkProbeResponseFields(response, method) {
|
|
|
4963
5263
|
content_length: contentLength,
|
|
4964
5264
|
};
|
|
4965
5265
|
}
|
|
5266
|
+
function jsonProbeValueType(value) {
|
|
5267
|
+
if (value === null) return "null";
|
|
5268
|
+
if (Array.isArray(value)) return "array";
|
|
5269
|
+
if (typeof value === "boolean") return "boolean";
|
|
5270
|
+
if (typeof value === "number") return "number";
|
|
5271
|
+
if (typeof value === "string") return "string";
|
|
5272
|
+
return "object";
|
|
5273
|
+
}
|
|
5274
|
+
function jsonProbeDeepEqual(left, right) {
|
|
5275
|
+
if (left === right) return true;
|
|
5276
|
+
if (typeof left !== typeof right) return false;
|
|
5277
|
+
if (left === null || right === null) return left === right;
|
|
5278
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
5279
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
5280
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
5281
|
+
return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
|
|
5282
|
+
}
|
|
5283
|
+
const leftKeys = Object.keys(left).sort();
|
|
5284
|
+
const rightKeys = Object.keys(right).sort();
|
|
5285
|
+
if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
|
|
5286
|
+
return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
|
|
5287
|
+
}
|
|
5288
|
+
function jsonProbeContains(observed, expected) {
|
|
5289
|
+
if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
|
|
5290
|
+
if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
|
|
5291
|
+
if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
|
|
5292
|
+
return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
|
|
5293
|
+
}
|
|
5294
|
+
return false;
|
|
5295
|
+
}
|
|
5296
|
+
function parseJsonProbePathSegments(path) {
|
|
5297
|
+
let input = String(path || "").trim();
|
|
5298
|
+
if (!input) throw new Error("path is empty");
|
|
5299
|
+
if (input === "$") return [];
|
|
5300
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
5301
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
5302
|
+
const segments = [];
|
|
5303
|
+
let token = "";
|
|
5304
|
+
const pushToken = () => {
|
|
5305
|
+
if (!token) return;
|
|
5306
|
+
segments.push(token);
|
|
5307
|
+
token = "";
|
|
5308
|
+
};
|
|
5309
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
5310
|
+
const char = input[index];
|
|
5311
|
+
if (char === ".") {
|
|
5312
|
+
pushToken();
|
|
5313
|
+
continue;
|
|
5314
|
+
}
|
|
5315
|
+
if (char !== "[") {
|
|
5316
|
+
token += char;
|
|
5317
|
+
continue;
|
|
5318
|
+
}
|
|
5319
|
+
pushToken();
|
|
5320
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
5321
|
+
if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
|
|
5322
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
5323
|
+
if (!bracket) throw new Error("empty bracket at " + index);
|
|
5324
|
+
if (/^\d+$/.test(bracket)) {
|
|
5325
|
+
segments.push(Number(bracket));
|
|
5326
|
+
} else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
|
|
5327
|
+
const quoted = bracket.startsWith("'")
|
|
5328
|
+
? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
|
|
5329
|
+
: bracket;
|
|
5330
|
+
segments.push(String(JSON.parse(quoted)));
|
|
5331
|
+
} else {
|
|
5332
|
+
segments.push(bracket);
|
|
5333
|
+
}
|
|
5334
|
+
index = closeIndex;
|
|
5335
|
+
}
|
|
5336
|
+
pushToken();
|
|
5337
|
+
return segments;
|
|
5338
|
+
}
|
|
5339
|
+
function resolveJsonProbePath(root, path) {
|
|
5340
|
+
let segments;
|
|
5341
|
+
try {
|
|
5342
|
+
segments = parseJsonProbePathSegments(path);
|
|
5343
|
+
} catch (error) {
|
|
5344
|
+
return { exists: false, error: String(error && error.message ? error.message : error) };
|
|
5345
|
+
}
|
|
5346
|
+
let current = root;
|
|
5347
|
+
for (const segment of segments) {
|
|
5348
|
+
if (typeof segment === "number") {
|
|
5349
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
5350
|
+
current = current[segment];
|
|
5351
|
+
continue;
|
|
5352
|
+
}
|
|
5353
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
|
|
5354
|
+
return { exists: false };
|
|
5355
|
+
}
|
|
5356
|
+
current = current[segment];
|
|
5357
|
+
}
|
|
5358
|
+
return { exists: true, value: current };
|
|
5359
|
+
}
|
|
5360
|
+
function evaluateJsonProbeAssertion(root, assertion) {
|
|
5361
|
+
const resolved = resolveJsonProbePath(root, assertion.path);
|
|
5362
|
+
const errors = [];
|
|
5363
|
+
const result = {
|
|
5364
|
+
label: assertion.label || assertion.path,
|
|
5365
|
+
path: assertion.path,
|
|
5366
|
+
ok: true,
|
|
5367
|
+
exists: resolved.exists,
|
|
5368
|
+
observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
|
|
5369
|
+
};
|
|
5370
|
+
if (resolved.exists) result.observed = resolved.value;
|
|
5371
|
+
if (resolved.error) errors.push(resolved.error);
|
|
5372
|
+
if (Object.hasOwn(assertion, "exists")) {
|
|
5373
|
+
result.expected_exists = assertion.exists;
|
|
5374
|
+
if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
|
|
5375
|
+
}
|
|
5376
|
+
if (Object.hasOwn(assertion, "type")) {
|
|
5377
|
+
result.type = assertion.type;
|
|
5378
|
+
if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
|
|
5379
|
+
}
|
|
5380
|
+
if (Object.hasOwn(assertion, "equals")) {
|
|
5381
|
+
result.equals = assertion.equals;
|
|
5382
|
+
if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
5383
|
+
}
|
|
5384
|
+
if (Object.hasOwn(assertion, "not_equals")) {
|
|
5385
|
+
result.not_equals = assertion.not_equals;
|
|
5386
|
+
if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
5387
|
+
}
|
|
5388
|
+
if (Object.hasOwn(assertion, "contains")) {
|
|
5389
|
+
result.contains = assertion.contains;
|
|
5390
|
+
if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
5391
|
+
}
|
|
5392
|
+
result.ok = errors.length === 0;
|
|
5393
|
+
if (errors.length) result.errors = errors;
|
|
5394
|
+
return result;
|
|
5395
|
+
}
|
|
5396
|
+
function evaluateJsonProbeAssertions(text, assertions) {
|
|
5397
|
+
const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
5398
|
+
if (!expected.length) return [];
|
|
5399
|
+
let parsed;
|
|
5400
|
+
try {
|
|
5401
|
+
parsed = JSON.parse(text);
|
|
5402
|
+
} catch (error) {
|
|
5403
|
+
const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
|
|
5404
|
+
return expected.map((assertion) => ({
|
|
5405
|
+
label: assertion.label || assertion.path,
|
|
5406
|
+
path: assertion.path,
|
|
5407
|
+
ok: false,
|
|
5408
|
+
exists: false,
|
|
5409
|
+
observed_type: "missing",
|
|
5410
|
+
errors: [message],
|
|
5411
|
+
}));
|
|
5412
|
+
}
|
|
5413
|
+
return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
|
|
5414
|
+
}
|
|
4966
5415
|
async function collectHttpStatus(check) {
|
|
4967
5416
|
const url = httpStatusRequestUrl(check, page.url() || targetUrl);
|
|
4968
5417
|
const method = httpStatusMethod(check);
|
|
@@ -4979,6 +5428,7 @@ async function collectHttpStatus(check) {
|
|
|
4979
5428
|
const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
|
|
4980
5429
|
const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
|
|
4981
5430
|
const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
|
|
5431
|
+
const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
4982
5432
|
const options = {
|
|
4983
5433
|
method,
|
|
4984
5434
|
redirect: "follow",
|
|
@@ -5004,17 +5454,18 @@ async function collectHttpStatus(check) {
|
|
|
5004
5454
|
Object.assign(result, linkProbeResponseFields(response, method));
|
|
5005
5455
|
result.url = url;
|
|
5006
5456
|
result.status_text = response.statusText || "";
|
|
5007
|
-
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;
|
|
5457
|
+
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;
|
|
5008
5458
|
if (shouldReadBody) {
|
|
5009
5459
|
try {
|
|
5010
5460
|
const buffer = await response.arrayBuffer();
|
|
5011
5461
|
result.bytes = buffer.byteLength;
|
|
5012
|
-
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
|
|
5462
|
+
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
|
|
5013
5463
|
const text = new TextDecoder().decode(buffer);
|
|
5014
5464
|
result.body_sample = text.slice(0, 1000);
|
|
5015
5465
|
if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
|
|
5016
5466
|
if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
|
|
5017
5467
|
if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
|
|
5468
|
+
if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
|
|
5018
5469
|
}
|
|
5019
5470
|
} catch (error) {
|
|
5020
5471
|
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
@@ -5027,6 +5478,7 @@ async function collectHttpStatus(check) {
|
|
|
5027
5478
|
&& (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
|
|
5028
5479
|
&& (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
|
|
5029
5480
|
&& (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
|
|
5481
|
+
&& (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
|
|
5030
5482
|
&& !result.error;
|
|
5031
5483
|
return result;
|
|
5032
5484
|
} catch (error) {
|
|
@@ -5613,6 +6065,7 @@ async function collectRouteInventory(check, viewport) {
|
|
|
5613
6065
|
};
|
|
5614
6066
|
}
|
|
5615
6067
|
async function captureViewport(viewport) {
|
|
6068
|
+
activeViewportName = viewport && viewport.name ? viewport.name : null;
|
|
5616
6069
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
5617
6070
|
let httpStatus = null;
|
|
5618
6071
|
let navigationError;
|