@riddledc/riddle-proof 0.7.93 → 0.7.95
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 +25 -3
- package/dist/{chunk-QLBOFUON.js → chunk-5OCJKEHE.js} +188 -25
- package/dist/cli.cjs +203 -29
- package/dist/cli.js +16 -5
- package/dist/index.cjs +188 -25
- package/dist/index.js +1 -1
- package/dist/profile.cjs +188 -25
- package/dist/profile.d.cts +3 -1
- package/dist/profile.d.ts +3 -1
- package/dist/profile.js +1 -1
- package/dist/proof-run-engine.d.cts +3 -3
- package/dist/proof-run-engine.d.ts +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,7 @@ or as a stronger proof base before a change loop.
|
|
|
163
163
|
{ "type": "selector_visible", "selector": "[data-testid='pricing-cards']" },
|
|
164
164
|
{ "type": "selector_absent", "selector": "[data-testid='loading-spinner']" },
|
|
165
165
|
{ "type": "selector_count_equals", "selector": "[data-testid='pricing-card']", "expected_count": 3 },
|
|
166
|
+
{ "type": "selector_text_visible", "selector": "[data-testid='pricing-cards']", "text": "Pro" },
|
|
166
167
|
{ "type": "text_visible", "text": "Start building" },
|
|
167
168
|
{ "type": "text_visible", "text": "Compare plans", "viewports": ["desktop"] },
|
|
168
169
|
{ "type": "no_mobile_horizontal_overflow" },
|
|
@@ -417,6 +418,22 @@ The check records the visible text sequence for the selector and passes when
|
|
|
417
418
|
the expected texts appear in that order as a subsequence. This is less brittle
|
|
418
419
|
than matching one large body-text regex when only row or card order matters.
|
|
419
420
|
|
|
421
|
+
Use `selector_text_visible` and `selector_text_absent` when the durable
|
|
422
|
+
assertion belongs to one panel, code sample, result area, or card group rather
|
|
423
|
+
than the whole page:
|
|
424
|
+
|
|
425
|
+
```json
|
|
426
|
+
{
|
|
427
|
+
"type": "selector_text_visible",
|
|
428
|
+
"selector": ".result-state",
|
|
429
|
+
"text": "\"sync\": false"
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
The check records visible text for the selector in each viewport and reports
|
|
434
|
+
matched counts plus short samples, which makes generated-command and evidence
|
|
435
|
+
card audits easier to diagnose than global `text_visible` checks.
|
|
436
|
+
|
|
420
437
|
Use `frame_text_visible` and `frame_no_horizontal_overflow` for embedded app,
|
|
421
438
|
game, or preview surfaces that render inside iframes:
|
|
422
439
|
|
|
@@ -462,6 +479,8 @@ artifact links should fail the profile:
|
|
|
462
479
|
"expected_count": 88,
|
|
463
480
|
"same_origin_only": true,
|
|
464
481
|
"require_nonzero_bytes": true,
|
|
482
|
+
"min_bytes": 32,
|
|
483
|
+
"allowed_content_types": ["image/*", "application/json"],
|
|
465
484
|
"max_links": 150
|
|
466
485
|
}
|
|
467
486
|
```
|
|
@@ -469,9 +488,12 @@ artifact links should fail the profile:
|
|
|
469
488
|
The check defaults to `a[href]`, deduplicates URLs, probes up to 100 selected
|
|
470
489
|
URLs, and treats HTTP `2xx` / `3xx` responses as healthy. Use
|
|
471
490
|
`allowed_statuses` or `expected_status` when a narrower status contract is
|
|
472
|
-
intentional, `min_count` for lower-bound audits,
|
|
473
|
-
|
|
474
|
-
|
|
491
|
+
intentional, `min_count` for lower-bound audits, `min_bytes` when a one-byte
|
|
492
|
+
range response is too weak, `allowed_content_types` for MIME checks, and
|
|
493
|
+
`max_links` when the selected set is intentionally larger than 100.
|
|
494
|
+
`allowed_content_types` accepts exact types and family wildcards such as
|
|
495
|
+
`image/*`. `artifact_link_status` is an alias with the same behavior for
|
|
496
|
+
profiles that want artifact-specific wording.
|
|
475
497
|
|
|
476
498
|
Use the `route_inventory` check for source-page route coverage audits where a
|
|
477
499
|
navigation surface must expose a known set of routes and each route must load
|
|
@@ -20,6 +20,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
20
20
|
"selector_count_equals",
|
|
21
21
|
"selector_count_equal",
|
|
22
22
|
"selector_count_eq",
|
|
23
|
+
"selector_text_visible",
|
|
24
|
+
"selector_text_absent",
|
|
23
25
|
"selector_text_order",
|
|
24
26
|
"frame_text_visible",
|
|
25
27
|
"frame_url_equals",
|
|
@@ -733,7 +735,7 @@ function normalizeCheck(input, index) {
|
|
|
733
735
|
if (!isSupportedCheckType(type)) {
|
|
734
736
|
throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
|
|
735
737
|
}
|
|
736
|
-
if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue(input.selector)) {
|
|
738
|
+
if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq" || type === "selector_text_visible" || type === "selector_text_absent") && !stringValue(input.selector)) {
|
|
737
739
|
throw new Error(`checks[${index}] ${type} requires selector.`);
|
|
738
740
|
}
|
|
739
741
|
if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
|
|
@@ -749,7 +751,7 @@ function normalizeCheck(input, index) {
|
|
|
749
751
|
if (type === "frame_url_matches" && !stringValue(input.pattern)) {
|
|
750
752
|
throw new Error(`checks[${index}] frame_url_matches requires pattern.`);
|
|
751
753
|
}
|
|
752
|
-
if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
754
|
+
if ((type === "text_visible" || type === "text_absent" || type === "selector_text_visible" || type === "selector_text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
753
755
|
throw new Error(`checks[${index}] ${type} requires text or pattern.`);
|
|
754
756
|
}
|
|
755
757
|
if ((type === "url_search_param_equals" || type === "url_search_param_absent") && !stringValue(input.param) && !stringValue(input.search_param) && !stringValue(input.searchParam) && !stringValue(input.key)) {
|
|
@@ -783,6 +785,12 @@ function normalizeCheck(input, index) {
|
|
|
783
785
|
`checks[${index}] allowed_statuses`
|
|
784
786
|
) : void 0;
|
|
785
787
|
const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
|
|
788
|
+
const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
|
|
789
|
+
const expectedContentType = isLinkStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
|
|
790
|
+
const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
|
|
791
|
+
input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
|
|
792
|
+
`checks[${index}] allowed_content_types`
|
|
793
|
+
) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
|
|
786
794
|
if (isLinkStatusCheck) {
|
|
787
795
|
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
788
796
|
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
@@ -824,6 +832,8 @@ function normalizeCheck(input, index) {
|
|
|
824
832
|
same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
|
|
825
833
|
dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
|
|
826
834
|
require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
|
|
835
|
+
min_bytes: minBytes,
|
|
836
|
+
allowed_content_types: allowedContentTypes,
|
|
827
837
|
allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
|
|
828
838
|
max_overflow_px: numberValue(input.max_overflow_px),
|
|
829
839
|
timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
|
|
@@ -930,15 +940,40 @@ function linkStatusIsAllowed(status, check) {
|
|
|
930
940
|
const allowed = linkStatusAllowedStatuses(check);
|
|
931
941
|
return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
932
942
|
}
|
|
943
|
+
function linkStatusObservedBytes(result) {
|
|
944
|
+
const bytes = numberValue(result.bytes);
|
|
945
|
+
const contentLength = numberValue(result.content_length);
|
|
946
|
+
return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
|
|
947
|
+
}
|
|
948
|
+
function normalizeLinkStatusContentType(value) {
|
|
949
|
+
const normalized = value?.split(";")[0]?.trim().toLowerCase();
|
|
950
|
+
return normalized || void 0;
|
|
951
|
+
}
|
|
952
|
+
function linkStatusContentTypeOk(result, check) {
|
|
953
|
+
const expected = check.allowed_content_types;
|
|
954
|
+
if (!expected?.length) return true;
|
|
955
|
+
const actual = normalizeLinkStatusContentType(stringValue(result.content_type));
|
|
956
|
+
if (!actual) return false;
|
|
957
|
+
return expected.some((contentType) => {
|
|
958
|
+
const normalized = normalizeLinkStatusContentType(contentType);
|
|
959
|
+
if (!normalized) return false;
|
|
960
|
+
if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
|
|
961
|
+
return actual === normalized;
|
|
962
|
+
});
|
|
963
|
+
}
|
|
933
964
|
function linkStatusResultOk(result, check) {
|
|
934
965
|
const status = numberValue(result.status);
|
|
935
966
|
if (!linkStatusIsAllowed(status, check)) return false;
|
|
936
967
|
if (stringValue(result.error)) return false;
|
|
937
968
|
if (result.ok === false) return false;
|
|
969
|
+
if (!linkStatusContentTypeOk(result, check)) return false;
|
|
938
970
|
if (check.require_nonzero_bytes) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
971
|
+
const observedBytes = linkStatusObservedBytes(result);
|
|
972
|
+
if (observedBytes === void 0 || observedBytes <= 0) return false;
|
|
973
|
+
}
|
|
974
|
+
if (check.min_bytes !== void 0) {
|
|
975
|
+
const observedBytes = linkStatusObservedBytes(result);
|
|
976
|
+
if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
|
|
942
977
|
}
|
|
943
978
|
return true;
|
|
944
979
|
}
|
|
@@ -967,7 +1002,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
|
|
|
967
1002
|
status: numberValue(result.status) ?? null,
|
|
968
1003
|
method: stringValue(result.method) ?? null,
|
|
969
1004
|
error: stringValue(result.error) ?? null,
|
|
970
|
-
|
|
1005
|
+
content_type: stringValue(result.content_type) ?? null,
|
|
1006
|
+
bytes: linkStatusObservedBytes(result) ?? null,
|
|
1007
|
+
min_bytes: check.min_bytes ?? null,
|
|
1008
|
+
allowed_content_types: check.allowed_content_types ?? null
|
|
971
1009
|
}));
|
|
972
1010
|
if (stringValue(linkEvidence.error)) {
|
|
973
1011
|
failures.push({ code: "link_status_capture_failed", error: stringValue(linkEvidence.error) ?? "" });
|
|
@@ -1004,6 +1042,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
|
|
|
1004
1042
|
failed_count: failures.length,
|
|
1005
1043
|
truncated: linkEvidence.truncated === true,
|
|
1006
1044
|
max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
|
|
1045
|
+
min_bytes: check.min_bytes ?? null,
|
|
1046
|
+
allowed_content_types: check.allowed_content_types ?? null,
|
|
1007
1047
|
status_counts: statusCounts,
|
|
1008
1048
|
failures: failures.slice(0, 20)
|
|
1009
1049
|
};
|
|
@@ -1015,9 +1055,11 @@ function textSequenceForCheck(viewport, check) {
|
|
|
1015
1055
|
const key = selectorKey(check);
|
|
1016
1056
|
const sequence = viewport.text_sequences?.[key];
|
|
1017
1057
|
if (isRecord(sequence)) {
|
|
1058
|
+
const visibleMatchTexts = Array.isArray(sequence.visible_match_texts) ? sequence.visible_match_texts : [];
|
|
1059
|
+
const matchTexts = Array.isArray(sequence.match_texts) ? sequence.match_texts : [];
|
|
1018
1060
|
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
1019
1061
|
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
1020
|
-
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
1062
|
+
const candidates = visibleMatchTexts.length ? visibleMatchTexts : matchTexts.length ? matchTexts : visibleTexts.length ? visibleTexts : texts;
|
|
1021
1063
|
return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
1022
1064
|
}
|
|
1023
1065
|
return [];
|
|
@@ -1351,6 +1393,35 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
1351
1393
|
message: failed.length ? `Selector ${key} count did not equal ${expectedCount} in ${failed.length} viewport(s).` : void 0
|
|
1352
1394
|
};
|
|
1353
1395
|
}
|
|
1396
|
+
if (check.type === "selector_text_visible" || check.type === "selector_text_absent") {
|
|
1397
|
+
const key = selectorKey(check);
|
|
1398
|
+
const expectedVisible = check.type === "selector_text_visible";
|
|
1399
|
+
const results = viewports.map((viewport) => {
|
|
1400
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
1401
|
+
const matches = texts.filter((text) => matchText(text, check));
|
|
1402
|
+
return {
|
|
1403
|
+
viewport: viewport.name,
|
|
1404
|
+
selector_count: viewport.selectors?.[key]?.count || 0,
|
|
1405
|
+
visible_count: viewport.selectors?.[key]?.visible_count || 0,
|
|
1406
|
+
matched_count: matches.length,
|
|
1407
|
+
matched: matches.length > 0,
|
|
1408
|
+
samples: matches.slice(0, 3).map((text) => text.slice(0, 240))
|
|
1409
|
+
};
|
|
1410
|
+
});
|
|
1411
|
+
const failed = results.filter((result) => result.matched !== expectedVisible).length;
|
|
1412
|
+
return {
|
|
1413
|
+
type: check.type,
|
|
1414
|
+
label: checkLabel(check),
|
|
1415
|
+
status: failed ? "failed" : "passed",
|
|
1416
|
+
evidence: {
|
|
1417
|
+
selector: key,
|
|
1418
|
+
text: check.text || null,
|
|
1419
|
+
pattern: check.pattern || null,
|
|
1420
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
1421
|
+
},
|
|
1422
|
+
message: failed ? `Selector ${key} text assertion failed in ${failed} viewport(s).` : void 0
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1354
1425
|
if (check.type === "selector_text_order") {
|
|
1355
1426
|
const key = selectorKey(check);
|
|
1356
1427
|
const expectedTexts = check.expected_texts || [];
|
|
@@ -1361,7 +1432,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
1361
1432
|
viewport: viewport.name,
|
|
1362
1433
|
matched: match.matched,
|
|
1363
1434
|
matched_positions: match.positions,
|
|
1364
|
-
visible_texts: texts.slice(0, 20)
|
|
1435
|
+
visible_texts: texts.slice(0, 20).map((text) => text.slice(0, 240))
|
|
1365
1436
|
};
|
|
1366
1437
|
});
|
|
1367
1438
|
const failed = results.filter((result) => !result.matched).length;
|
|
@@ -1501,6 +1572,8 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
1501
1572
|
min_count: check.min_count ?? null,
|
|
1502
1573
|
allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
1503
1574
|
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
1575
|
+
min_bytes: check.min_bytes ?? null,
|
|
1576
|
+
allowed_content_types: check.allowed_content_types ?? null,
|
|
1504
1577
|
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
1505
1578
|
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
|
|
1506
1579
|
},
|
|
@@ -2071,9 +2144,11 @@ function textSequenceForCheck(viewport, check) {
|
|
|
2071
2144
|
const key = check.selector || "";
|
|
2072
2145
|
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
2073
2146
|
if (sequence && typeof sequence === "object") {
|
|
2147
|
+
const visibleMatchTexts = Array.isArray(sequence.visible_match_texts) ? sequence.visible_match_texts : [];
|
|
2148
|
+
const matchTexts = Array.isArray(sequence.match_texts) ? sequence.match_texts : [];
|
|
2074
2149
|
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
2075
2150
|
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
2076
|
-
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
2151
|
+
const candidates = visibleMatchTexts.length ? visibleMatchTexts : matchTexts.length ? matchTexts : visibleTexts.length ? visibleTexts : texts;
|
|
2077
2152
|
return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
2078
2153
|
}
|
|
2079
2154
|
return [];
|
|
@@ -2102,15 +2177,41 @@ function linkStatusIsAllowed(status, check) {
|
|
|
2102
2177
|
const allowed = linkStatusAllowedStatuses(check);
|
|
2103
2178
|
return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
2104
2179
|
}
|
|
2180
|
+
function linkStatusObservedBytes(result) {
|
|
2181
|
+
const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
|
|
2182
|
+
const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
|
|
2183
|
+
const observed = Math.max(bytes || 0, contentLength || 0);
|
|
2184
|
+
return observed > 0 ? observed : undefined;
|
|
2185
|
+
}
|
|
2186
|
+
function normalizeLinkStatusContentType(value) {
|
|
2187
|
+
if (typeof value !== "string") return undefined;
|
|
2188
|
+
const normalized = (value.split(";")[0] || "").trim().toLowerCase();
|
|
2189
|
+
return normalized || undefined;
|
|
2190
|
+
}
|
|
2191
|
+
function linkStatusContentTypeOk(result, check) {
|
|
2192
|
+
if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
|
|
2193
|
+
const actual = normalizeLinkStatusContentType(result.content_type);
|
|
2194
|
+
if (!actual) return false;
|
|
2195
|
+
return check.allowed_content_types.some((contentType) => {
|
|
2196
|
+
const normalized = normalizeLinkStatusContentType(contentType);
|
|
2197
|
+
if (!normalized) return false;
|
|
2198
|
+
if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
|
|
2199
|
+
return actual === normalized;
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2105
2202
|
function linkStatusResultOk(result, check) {
|
|
2106
2203
|
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
2107
2204
|
if (!linkStatusIsAllowed(result.status, check)) return false;
|
|
2108
2205
|
if (typeof result.error === "string" && result.error.trim()) return false;
|
|
2109
2206
|
if (result.ok === false) return false;
|
|
2207
|
+
if (!linkStatusContentTypeOk(result, check)) return false;
|
|
2110
2208
|
if (check.require_nonzero_bytes === true) {
|
|
2111
|
-
const
|
|
2112
|
-
|
|
2113
|
-
|
|
2209
|
+
const observedBytes = linkStatusObservedBytes(result);
|
|
2210
|
+
if (observedBytes === undefined || observedBytes <= 0) return false;
|
|
2211
|
+
}
|
|
2212
|
+
if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
|
|
2213
|
+
const observedBytes = linkStatusObservedBytes(result);
|
|
2214
|
+
if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
|
|
2114
2215
|
}
|
|
2115
2216
|
return true;
|
|
2116
2217
|
}
|
|
@@ -2138,11 +2239,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
|
|
|
2138
2239
|
status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
|
|
2139
2240
|
method: typeof result.method === "string" ? result.method : null,
|
|
2140
2241
|
error: typeof result.error === "string" ? result.error : null,
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
: null,
|
|
2242
|
+
content_type: typeof result.content_type === "string" ? result.content_type : null,
|
|
2243
|
+
bytes: linkStatusObservedBytes(result) ?? null,
|
|
2244
|
+
min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
|
|
2245
|
+
allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
|
|
2146
2246
|
}));
|
|
2147
2247
|
if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
|
|
2148
2248
|
failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
|
|
@@ -2177,6 +2277,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
|
|
|
2177
2277
|
failed_count: failures.length,
|
|
2178
2278
|
truncated: linkEvidence.truncated === true,
|
|
2179
2279
|
max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
|
|
2280
|
+
min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
|
|
2281
|
+
allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
|
|
2180
2282
|
status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
|
|
2181
2283
|
failures: failures.slice(0, 20),
|
|
2182
2284
|
};
|
|
@@ -2711,6 +2813,31 @@ function assessProfile(profile, evidence) {
|
|
|
2711
2813
|
});
|
|
2712
2814
|
continue;
|
|
2713
2815
|
}
|
|
2816
|
+
if (check.type === "selector_text_visible" || check.type === "selector_text_absent") {
|
|
2817
|
+
const selector = check.selector || "";
|
|
2818
|
+
const expectedVisible = check.type === "selector_text_visible";
|
|
2819
|
+
const results = checkViewports.map((viewport) => {
|
|
2820
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
2821
|
+
const matches = texts.filter((text) => textMatches(text, check));
|
|
2822
|
+
return {
|
|
2823
|
+
viewport: viewport.name,
|
|
2824
|
+
selector_count: viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0,
|
|
2825
|
+
visible_count: viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].visible_count : 0,
|
|
2826
|
+
matched_count: matches.length,
|
|
2827
|
+
matched: matches.length > 0,
|
|
2828
|
+
samples: matches.slice(0, 3).map((text) => text.slice(0, 240)),
|
|
2829
|
+
};
|
|
2830
|
+
});
|
|
2831
|
+
const failed = results.filter((result) => result.matched !== expectedVisible).length;
|
|
2832
|
+
checks.push({
|
|
2833
|
+
type: check.type,
|
|
2834
|
+
label: check.label || check.type,
|
|
2835
|
+
status: failed ? "failed" : "passed",
|
|
2836
|
+
evidence: { selector, text: check.text || null, pattern: check.pattern || null, viewports: results },
|
|
2837
|
+
message: failed ? "Selector " + selector + " text assertion failed in " + failed + " viewport(s)." : undefined,
|
|
2838
|
+
});
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2714
2841
|
if (check.type === "selector_text_order") {
|
|
2715
2842
|
const selector = check.selector || "";
|
|
2716
2843
|
const expectedTexts = check.expected_texts || [];
|
|
@@ -2721,7 +2848,7 @@ function assessProfile(profile, evidence) {
|
|
|
2721
2848
|
viewport: viewport.name,
|
|
2722
2849
|
matched: match.matched,
|
|
2723
2850
|
matched_positions: match.positions,
|
|
2724
|
-
visible_texts: texts.slice(0, 20),
|
|
2851
|
+
visible_texts: texts.slice(0, 20).map((text) => text.slice(0, 240)),
|
|
2725
2852
|
};
|
|
2726
2853
|
});
|
|
2727
2854
|
const failed = results.filter((result) => !result.matched).length;
|
|
@@ -2848,6 +2975,8 @@ function assessProfile(profile, evidence) {
|
|
|
2848
2975
|
min_count: check.min_count ?? null,
|
|
2849
2976
|
allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
2850
2977
|
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
2978
|
+
min_bytes: check.min_bytes ?? null,
|
|
2979
|
+
allowed_content_types: check.allowed_content_types ?? null,
|
|
2851
2980
|
viewports: summaries,
|
|
2852
2981
|
failures: failed.flatMap((summary) => Array.isArray(summary.failures)
|
|
2853
2982
|
? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
|
|
@@ -3925,6 +4054,7 @@ async function selectorStats(selector) {
|
|
|
3925
4054
|
async function selectorTextSequence(selector) {
|
|
3926
4055
|
return page.locator(selector).evaluateAll((elements) => {
|
|
3927
4056
|
const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
4057
|
+
const matchText = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 8000);
|
|
3928
4058
|
const isVisible = (element) => {
|
|
3929
4059
|
const style = window.getComputedStyle(element);
|
|
3930
4060
|
const rect = element.getBoundingClientRect();
|
|
@@ -3933,6 +4063,7 @@ async function selectorTextSequence(selector) {
|
|
|
3933
4063
|
const rows = elements.map((element, index) => ({
|
|
3934
4064
|
index,
|
|
3935
4065
|
text: compact(element.innerText || element.textContent || ""),
|
|
4066
|
+
match_text: matchText(element.innerText || element.textContent || ""),
|
|
3936
4067
|
visible: isVisible(element),
|
|
3937
4068
|
}));
|
|
3938
4069
|
return {
|
|
@@ -3940,8 +4071,10 @@ async function selectorTextSequence(selector) {
|
|
|
3940
4071
|
visible_count: rows.filter((row) => row.visible).length,
|
|
3941
4072
|
texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
3942
4073
|
visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
4074
|
+
match_texts: rows.map((row) => row.match_text).filter(Boolean).slice(0, 40),
|
|
4075
|
+
visible_match_texts: rows.filter((row) => row.visible).map((row) => row.match_text).filter(Boolean).slice(0, 40),
|
|
3943
4076
|
};
|
|
3944
|
-
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
4077
|
+
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], match_texts: [], visible_match_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
3945
4078
|
}
|
|
3946
4079
|
function linkProbeMaxLinks(check) {
|
|
3947
4080
|
const value = Number(check.max_links || check.maxLinks || check.limit || 100);
|
|
@@ -3991,6 +4124,28 @@ function linkProbeAllowed(status, check) {
|
|
|
3991
4124
|
: null;
|
|
3992
4125
|
return allowed ? allowed.includes(status) : status >= 200 && status < 400;
|
|
3993
4126
|
}
|
|
4127
|
+
function linkProbeObservedBytes(result) {
|
|
4128
|
+
const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
|
|
4129
|
+
const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
|
|
4130
|
+
const observed = Math.max(bytes || 0, contentLength || 0);
|
|
4131
|
+
return observed > 0 ? observed : undefined;
|
|
4132
|
+
}
|
|
4133
|
+
function normalizeLinkProbeContentType(value) {
|
|
4134
|
+
if (typeof value !== "string") return undefined;
|
|
4135
|
+
const normalized = (value.split(";")[0] || "").trim().toLowerCase();
|
|
4136
|
+
return normalized || undefined;
|
|
4137
|
+
}
|
|
4138
|
+
function linkProbeContentTypeAllowed(result, check) {
|
|
4139
|
+
if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
|
|
4140
|
+
const actual = normalizeLinkProbeContentType(result.content_type);
|
|
4141
|
+
if (!actual) return false;
|
|
4142
|
+
return check.allowed_content_types.some((contentType) => {
|
|
4143
|
+
const normalized = normalizeLinkProbeContentType(contentType);
|
|
4144
|
+
if (!normalized) return false;
|
|
4145
|
+
if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
|
|
4146
|
+
return actual === normalized;
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
3994
4149
|
function linkProbeResponseFields(response, method) {
|
|
3995
4150
|
const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
|
|
3996
4151
|
const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
|
|
@@ -4005,6 +4160,7 @@ function linkProbeResponseFields(response, method) {
|
|
|
4005
4160
|
}
|
|
4006
4161
|
async function probeLinkStatus(candidate, check) {
|
|
4007
4162
|
const requireNonzeroBytes = check.require_nonzero_bytes === true;
|
|
4163
|
+
const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
|
|
4008
4164
|
const allowGetFallback = check.allow_get_fallback !== false;
|
|
4009
4165
|
const result = {
|
|
4010
4166
|
url: candidate.url,
|
|
@@ -4022,7 +4178,7 @@ async function probeLinkStatus(candidate, check) {
|
|
|
4022
4178
|
};
|
|
4023
4179
|
const applyResponse = async (response, method, readBytes) => {
|
|
4024
4180
|
Object.assign(result, linkProbeResponseFields(response, method));
|
|
4025
|
-
if (readBytes
|
|
4181
|
+
if (readBytes) {
|
|
4026
4182
|
try {
|
|
4027
4183
|
const buffer = await response.arrayBuffer();
|
|
4028
4184
|
result.bytes = buffer.byteLength;
|
|
@@ -4031,7 +4187,9 @@ async function probeLinkStatus(candidate, check) {
|
|
|
4031
4187
|
}
|
|
4032
4188
|
}
|
|
4033
4189
|
result.ok = linkProbeAllowed(result.status, check)
|
|
4034
|
-
&& (
|
|
4190
|
+
&& linkProbeContentTypeAllowed(result, check)
|
|
4191
|
+
&& (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
|
|
4192
|
+
&& (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
|
|
4035
4193
|
&& !result.error;
|
|
4036
4194
|
};
|
|
4037
4195
|
try {
|
|
@@ -4048,9 +4206,9 @@ async function probeLinkStatus(candidate, check) {
|
|
|
4048
4206
|
method: "GET",
|
|
4049
4207
|
redirect: "follow",
|
|
4050
4208
|
cache: "no-store",
|
|
4051
|
-
headers: { Range: "bytes=0-
|
|
4209
|
+
headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
|
|
4052
4210
|
});
|
|
4053
|
-
await applyResponse(response, "GET", requireNonzeroBytes);
|
|
4211
|
+
await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
|
|
4054
4212
|
return result;
|
|
4055
4213
|
} catch (error) {
|
|
4056
4214
|
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
@@ -4104,7 +4262,10 @@ async function collectLinkStatus(check) {
|
|
|
4104
4262
|
status: result.status,
|
|
4105
4263
|
method: result.method,
|
|
4106
4264
|
error: result.error,
|
|
4107
|
-
|
|
4265
|
+
content_type: result.content_type,
|
|
4266
|
+
bytes: linkProbeObservedBytes(result) || null,
|
|
4267
|
+
min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
|
|
4268
|
+
allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
|
|
4108
4269
|
}));
|
|
4109
4270
|
const statusCounts = {};
|
|
4110
4271
|
for (const result of results) {
|
|
@@ -4118,6 +4279,8 @@ async function collectLinkStatus(check) {
|
|
|
4118
4279
|
same_origin_only: linkProbeSameOriginOnly(check),
|
|
4119
4280
|
dedupe: linkProbeDedupe(check),
|
|
4120
4281
|
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
4282
|
+
min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
|
|
4283
|
+
allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
|
|
4121
4284
|
allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
|
|
4122
4285
|
? check.allowed_statuses
|
|
4123
4286
|
: typeof check.expected_status === "number"
|
|
@@ -4677,7 +4840,7 @@ async function captureViewport(viewport) {
|
|
|
4677
4840
|
) {
|
|
4678
4841
|
selectors[check.selector] = await selectorStats(check.selector);
|
|
4679
4842
|
}
|
|
4680
|
-
if (check.type === "selector_text_order" && check.selector) {
|
|
4843
|
+
if ((check.type === "selector_text_order" || check.type === "selector_text_visible" || check.type === "selector_text_absent") && check.selector) {
|
|
4681
4844
|
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
4682
4845
|
text_sequences[check.selector] = await selectorTextSequence(check.selector);
|
|
4683
4846
|
}
|