@riddledc/riddle-proof 0.7.89 → 0.7.91
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 +23 -0
- package/dist/{chunk-CEBHV23V.js → chunk-2SGLFPFX.js} +517 -6
- package/dist/cli.cjs +548 -6
- package/dist/cli.js +32 -1
- package/dist/index.cjs +517 -6
- package/dist/index.js +1 -1
- package/dist/profile.cjs +517 -6
- package/dist/profile.d.cts +9 -1
- package/dist/profile.d.ts +9 -1
- package/dist/profile.js +1 -1
- package/dist/proof-run-core.d.cts +1 -1
- package/dist/proof-run-core.d.ts +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/dist/cli.cjs
CHANGED
|
@@ -6914,6 +6914,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
6914
6914
|
"frame_no_horizontal_overflow",
|
|
6915
6915
|
"text_visible",
|
|
6916
6916
|
"text_absent",
|
|
6917
|
+
"link_status",
|
|
6918
|
+
"artifact_link_status",
|
|
6917
6919
|
"route_inventory",
|
|
6918
6920
|
"no_horizontal_overflow",
|
|
6919
6921
|
"no_mobile_horizontal_overflow",
|
|
@@ -7578,6 +7580,29 @@ function normalizeStringList(value, label) {
|
|
|
7578
7580
|
if (!values.length) throw new Error(`${label} must contain non-empty strings.`);
|
|
7579
7581
|
return values;
|
|
7580
7582
|
}
|
|
7583
|
+
function normalizeHttpStatus(value, label) {
|
|
7584
|
+
if (value === void 0) return void 0;
|
|
7585
|
+
const status = numberValue(value);
|
|
7586
|
+
if (status === void 0 || !Number.isInteger(status) || status < 100 || status > 599) {
|
|
7587
|
+
throw new Error(`${label} must be an HTTP status code.`);
|
|
7588
|
+
}
|
|
7589
|
+
return status;
|
|
7590
|
+
}
|
|
7591
|
+
function normalizeHttpStatuses(value, label) {
|
|
7592
|
+
if (value === void 0) return void 0;
|
|
7593
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
7594
|
+
if (!value.length) throw new Error(`${label} must not be empty.`);
|
|
7595
|
+
const statuses = value.map((item, index) => normalizeHttpStatus(item, `${label}[${index}]`));
|
|
7596
|
+
return Array.from(new Set(statuses));
|
|
7597
|
+
}
|
|
7598
|
+
function normalizePositiveInteger(value, label, max) {
|
|
7599
|
+
if (value === void 0) return void 0;
|
|
7600
|
+
const number = numberValue(value);
|
|
7601
|
+
if (number === void 0 || !Number.isInteger(number) || number < 1 || max !== void 0 && number > max) {
|
|
7602
|
+
throw new Error(`${label} must be an integer from 1 to ${max ?? "unbounded"}.`);
|
|
7603
|
+
}
|
|
7604
|
+
return number;
|
|
7605
|
+
}
|
|
7581
7606
|
function validateRegexPatterns(patterns, label) {
|
|
7582
7607
|
for (const pattern of patterns || []) {
|
|
7583
7608
|
try {
|
|
@@ -7621,7 +7646,8 @@ function normalizeCheck(input, index) {
|
|
|
7621
7646
|
if (type === "url_search_param_equals" && expectedValue === void 0) {
|
|
7622
7647
|
throw new Error(`checks[${index}] url_search_param_equals requires expected_value.`);
|
|
7623
7648
|
}
|
|
7624
|
-
|
|
7649
|
+
const minCount = numberValue(input.min_count) ?? numberValue(input.minCount);
|
|
7650
|
+
if (type === "selector_count_at_least" && minCount === void 0) {
|
|
7625
7651
|
throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
|
|
7626
7652
|
}
|
|
7627
7653
|
const expectedCount = numberValue(input.expected_count) ?? numberValue(input.expectedCount) ?? numberValue(input.count);
|
|
@@ -7637,6 +7663,21 @@ function normalizeCheck(input, index) {
|
|
|
7637
7663
|
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
7638
7664
|
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
7639
7665
|
}
|
|
7666
|
+
const isLinkStatusCheck = type === "link_status" || type === "artifact_link_status";
|
|
7667
|
+
const expectedStatus = isLinkStatusCheck ? normalizeHttpStatus(input.expected_status ?? input.expectedStatus ?? input.status, `checks[${index}] expected_status`) : void 0;
|
|
7668
|
+
const allowedStatuses = isLinkStatusCheck ? normalizeHttpStatuses(
|
|
7669
|
+
input.allowed_statuses ?? input.allowedStatuses ?? input.expected_statuses ?? input.expectedStatuses,
|
|
7670
|
+
`checks[${index}] allowed_statuses`
|
|
7671
|
+
) : void 0;
|
|
7672
|
+
const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
|
|
7673
|
+
if (isLinkStatusCheck) {
|
|
7674
|
+
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
7675
|
+
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
7676
|
+
}
|
|
7677
|
+
if (expectedCount !== void 0 && (!Number.isInteger(expectedCount) || expectedCount < 0)) {
|
|
7678
|
+
throw new Error(`checks[${index}] ${type} expected_count must be a non-negative integer.`);
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7640
7681
|
return {
|
|
7641
7682
|
type,
|
|
7642
7683
|
label: stringValue2(input.label),
|
|
@@ -7662,8 +7703,15 @@ function normalizeCheck(input, index) {
|
|
|
7662
7703
|
allowed_console_patterns: normalizeStringList(input.allowed_console_patterns ?? input.allowedConsolePatterns ?? input.allow_console_patterns ?? input.allowConsolePatterns, `checks[${index}] allowed_console_patterns`),
|
|
7663
7704
|
allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
|
|
7664
7705
|
allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
|
|
7665
|
-
min_count:
|
|
7706
|
+
min_count: minCount,
|
|
7666
7707
|
expected_count: expectedCount,
|
|
7708
|
+
expected_status: expectedStatus,
|
|
7709
|
+
allowed_statuses: allowedStatuses,
|
|
7710
|
+
max_links: maxLinks,
|
|
7711
|
+
same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
|
|
7712
|
+
dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
|
|
7713
|
+
require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
|
|
7714
|
+
allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
|
|
7667
7715
|
max_overflow_px: numberValue(input.max_overflow_px),
|
|
7668
7716
|
timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
|
|
7669
7717
|
run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
|
|
@@ -7756,6 +7804,97 @@ function checkLabel(check) {
|
|
|
7756
7804
|
function selectorKey(check) {
|
|
7757
7805
|
return check.selector || "";
|
|
7758
7806
|
}
|
|
7807
|
+
function linkStatusSelector(check) {
|
|
7808
|
+
return check.selector || check.link_selector || "a[href]";
|
|
7809
|
+
}
|
|
7810
|
+
function linkStatusAllowedStatuses(check) {
|
|
7811
|
+
if (check.allowed_statuses?.length) return check.allowed_statuses;
|
|
7812
|
+
if (check.expected_status !== void 0) return [check.expected_status];
|
|
7813
|
+
return void 0;
|
|
7814
|
+
}
|
|
7815
|
+
function linkStatusIsAllowed(status, check) {
|
|
7816
|
+
if (status === void 0) return false;
|
|
7817
|
+
const allowed = linkStatusAllowedStatuses(check);
|
|
7818
|
+
return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
7819
|
+
}
|
|
7820
|
+
function linkStatusResultOk(result, check) {
|
|
7821
|
+
const status = numberValue(result.status);
|
|
7822
|
+
if (!linkStatusIsAllowed(status, check)) return false;
|
|
7823
|
+
if (stringValue2(result.error)) return false;
|
|
7824
|
+
if (result.ok === false) return false;
|
|
7825
|
+
if (check.require_nonzero_bytes) {
|
|
7826
|
+
const bytes = numberValue(result.bytes);
|
|
7827
|
+
const contentLength = numberValue(result.content_length);
|
|
7828
|
+
return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
|
|
7829
|
+
}
|
|
7830
|
+
return true;
|
|
7831
|
+
}
|
|
7832
|
+
function linkStatusEvidenceForCheck(viewport, check) {
|
|
7833
|
+
const evidence = viewport.link_statuses?.[linkStatusSelector(check)];
|
|
7834
|
+
return isRecord(evidence) ? evidence : void 0;
|
|
7835
|
+
}
|
|
7836
|
+
function summarizeLinkStatusEvidence(viewport, check) {
|
|
7837
|
+
const linkEvidence = linkStatusEvidenceForCheck(viewport, check);
|
|
7838
|
+
if (!linkEvidence) {
|
|
7839
|
+
return {
|
|
7840
|
+
viewport: viewport.name,
|
|
7841
|
+
selector: linkStatusSelector(check),
|
|
7842
|
+
total_count: 0,
|
|
7843
|
+
ok_count: 0,
|
|
7844
|
+
failed_count: 1,
|
|
7845
|
+
failures: [{ code: "link_status_evidence_missing" }]
|
|
7846
|
+
};
|
|
7847
|
+
}
|
|
7848
|
+
const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter(isRecord) : [];
|
|
7849
|
+
const totalCount = numberValue(linkEvidence.total_count) ?? results.length;
|
|
7850
|
+
const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
|
|
7851
|
+
const failures = results.filter((result) => !linkStatusResultOk(result, check)).map((result) => ({
|
|
7852
|
+
code: "link_status_failed",
|
|
7853
|
+
url: stringValue2(result.url) ?? null,
|
|
7854
|
+
status: numberValue(result.status) ?? null,
|
|
7855
|
+
method: stringValue2(result.method) ?? null,
|
|
7856
|
+
error: stringValue2(result.error) ?? null,
|
|
7857
|
+
bytes: numberValue(result.bytes) ?? numberValue(result.content_length) ?? null
|
|
7858
|
+
}));
|
|
7859
|
+
if (stringValue2(linkEvidence.error)) {
|
|
7860
|
+
failures.push({ code: "link_status_capture_failed", error: stringValue2(linkEvidence.error) ?? "" });
|
|
7861
|
+
}
|
|
7862
|
+
const recordedFailedCount = numberValue(linkEvidence.failed_count) ?? 0;
|
|
7863
|
+
if (!failures.length && recordedFailedCount > 0) {
|
|
7864
|
+
const recordedFailures = Array.isArray(linkEvidence.failures) ? linkEvidence.failures.slice(0, 20) : [];
|
|
7865
|
+
failures.push({
|
|
7866
|
+
code: "link_status_recorded_failures",
|
|
7867
|
+
failed_count: recordedFailedCount,
|
|
7868
|
+
failures: toJsonValue(recordedFailures)
|
|
7869
|
+
});
|
|
7870
|
+
}
|
|
7871
|
+
if (linkEvidence.truncated === true) {
|
|
7872
|
+
failures.push({
|
|
7873
|
+
code: "link_status_probe_truncated",
|
|
7874
|
+
discovered_count: numberValue(linkEvidence.discovered_count) ?? totalCount,
|
|
7875
|
+
max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100
|
|
7876
|
+
});
|
|
7877
|
+
}
|
|
7878
|
+
if (check.expected_count !== void 0 && totalCount !== check.expected_count) {
|
|
7879
|
+
failures.push({ code: "link_status_count_mismatch", expected: check.expected_count, actual: totalCount });
|
|
7880
|
+
}
|
|
7881
|
+
if (check.min_count !== void 0 && totalCount < check.min_count) {
|
|
7882
|
+
failures.push({ code: "link_status_count_below_minimum", min_count: check.min_count, actual: totalCount });
|
|
7883
|
+
}
|
|
7884
|
+
const statusCounts = isRecord(linkEvidence.status_counts) ? linkEvidence.status_counts : {};
|
|
7885
|
+
return {
|
|
7886
|
+
viewport: viewport.name,
|
|
7887
|
+
selector: linkStatusSelector(check),
|
|
7888
|
+
total_count: totalCount,
|
|
7889
|
+
discovered_count: numberValue(linkEvidence.discovered_count) ?? totalCount,
|
|
7890
|
+
ok_count: numberValue(linkEvidence.ok_count) ?? okCount,
|
|
7891
|
+
failed_count: failures.length,
|
|
7892
|
+
truncated: linkEvidence.truncated === true,
|
|
7893
|
+
max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
|
|
7894
|
+
status_counts: statusCounts,
|
|
7895
|
+
failures: failures.slice(0, 20)
|
|
7896
|
+
};
|
|
7897
|
+
}
|
|
7759
7898
|
function textKey(check) {
|
|
7760
7899
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
7761
7900
|
}
|
|
@@ -8235,6 +8374,26 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
8235
8374
|
message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
|
|
8236
8375
|
};
|
|
8237
8376
|
}
|
|
8377
|
+
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
8378
|
+
const selector = linkStatusSelector(check);
|
|
8379
|
+
const summaries = viewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
|
|
8380
|
+
const failed = summaries.filter((summary) => (numberValue(summary.failed_count) ?? 0) > 0);
|
|
8381
|
+
return {
|
|
8382
|
+
type: check.type,
|
|
8383
|
+
label: checkLabel(check),
|
|
8384
|
+
status: failed.length ? "failed" : "passed",
|
|
8385
|
+
evidence: {
|
|
8386
|
+
selector,
|
|
8387
|
+
expected_count: check.expected_count ?? null,
|
|
8388
|
+
min_count: check.min_count ?? null,
|
|
8389
|
+
allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
8390
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
8391
|
+
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
8392
|
+
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue2(summary.viewport) ?? null, failure })) : [])
|
|
8393
|
+
},
|
|
8394
|
+
message: failed.length ? `Link status failed in ${failed.length} viewport(s).` : void 0
|
|
8395
|
+
};
|
|
8396
|
+
}
|
|
8238
8397
|
if (check.type === "route_inventory") {
|
|
8239
8398
|
const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord(item.inventory));
|
|
8240
8399
|
if (!inventories.length) {
|
|
@@ -8801,6 +8960,98 @@ function textOrderMatch(texts, expectedTexts) {
|
|
|
8801
8960
|
}
|
|
8802
8961
|
return { matched: true, positions };
|
|
8803
8962
|
}
|
|
8963
|
+
function linkStatusSelector(check) {
|
|
8964
|
+
return check.selector || check.link_selector || "a[href]";
|
|
8965
|
+
}
|
|
8966
|
+
function linkStatusAllowedStatuses(check) {
|
|
8967
|
+
if (Array.isArray(check.allowed_statuses) && check.allowed_statuses.length) return check.allowed_statuses;
|
|
8968
|
+
if (typeof check.expected_status === "number" && Number.isFinite(check.expected_status)) return [check.expected_status];
|
|
8969
|
+
return undefined;
|
|
8970
|
+
}
|
|
8971
|
+
function linkStatusIsAllowed(status, check) {
|
|
8972
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
8973
|
+
const allowed = linkStatusAllowedStatuses(check);
|
|
8974
|
+
return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
8975
|
+
}
|
|
8976
|
+
function linkStatusResultOk(result, check) {
|
|
8977
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
8978
|
+
if (!linkStatusIsAllowed(result.status, check)) return false;
|
|
8979
|
+
if (typeof result.error === "string" && result.error.trim()) return false;
|
|
8980
|
+
if (result.ok === false) return false;
|
|
8981
|
+
if (check.require_nonzero_bytes === true) {
|
|
8982
|
+
const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
|
|
8983
|
+
const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
|
|
8984
|
+
return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
|
|
8985
|
+
}
|
|
8986
|
+
return true;
|
|
8987
|
+
}
|
|
8988
|
+
function summarizeLinkStatusEvidence(viewport, check) {
|
|
8989
|
+
const selector = linkStatusSelector(check);
|
|
8990
|
+
const linkEvidence = viewport && viewport.link_statuses && viewport.link_statuses[selector];
|
|
8991
|
+
if (!linkEvidence || typeof linkEvidence !== "object" || Array.isArray(linkEvidence)) {
|
|
8992
|
+
return {
|
|
8993
|
+
viewport: viewport && viewport.name,
|
|
8994
|
+
selector,
|
|
8995
|
+
total_count: 0,
|
|
8996
|
+
ok_count: 0,
|
|
8997
|
+
failed_count: 1,
|
|
8998
|
+
failures: [{ code: "link_status_evidence_missing" }],
|
|
8999
|
+
};
|
|
9000
|
+
}
|
|
9001
|
+
const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter((result) => result && typeof result === "object" && !Array.isArray(result)) : [];
|
|
9002
|
+
const totalCount = typeof linkEvidence.total_count === "number" && Number.isFinite(linkEvidence.total_count) ? linkEvidence.total_count : results.length;
|
|
9003
|
+
const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
|
|
9004
|
+
const failures = results
|
|
9005
|
+
.filter((result) => !linkStatusResultOk(result, check))
|
|
9006
|
+
.map((result) => ({
|
|
9007
|
+
code: "link_status_failed",
|
|
9008
|
+
url: typeof result.url === "string" ? result.url : null,
|
|
9009
|
+
status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
|
|
9010
|
+
method: typeof result.method === "string" ? result.method : null,
|
|
9011
|
+
error: typeof result.error === "string" ? result.error : null,
|
|
9012
|
+
bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
|
|
9013
|
+
? result.bytes
|
|
9014
|
+
: typeof result.content_length === "number" && Number.isFinite(result.content_length)
|
|
9015
|
+
? result.content_length
|
|
9016
|
+
: null,
|
|
9017
|
+
}));
|
|
9018
|
+
if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
|
|
9019
|
+
failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
|
|
9020
|
+
}
|
|
9021
|
+
const recordedFailedCount = typeof linkEvidence.failed_count === "number" && Number.isFinite(linkEvidence.failed_count) ? linkEvidence.failed_count : 0;
|
|
9022
|
+
if (!failures.length && recordedFailedCount > 0) {
|
|
9023
|
+
failures.push({
|
|
9024
|
+
code: "link_status_recorded_failures",
|
|
9025
|
+
failed_count: recordedFailedCount,
|
|
9026
|
+
failures: Array.isArray(linkEvidence.failures) ? linkEvidence.failures.slice(0, 20) : [],
|
|
9027
|
+
});
|
|
9028
|
+
}
|
|
9029
|
+
if (linkEvidence.truncated === true) {
|
|
9030
|
+
failures.push({
|
|
9031
|
+
code: "link_status_probe_truncated",
|
|
9032
|
+
discovered_count: typeof linkEvidence.discovered_count === "number" ? linkEvidence.discovered_count : totalCount,
|
|
9033
|
+
max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
|
|
9034
|
+
});
|
|
9035
|
+
}
|
|
9036
|
+
if (typeof check.expected_count === "number" && Number.isFinite(check.expected_count) && totalCount !== check.expected_count) {
|
|
9037
|
+
failures.push({ code: "link_status_count_mismatch", expected: check.expected_count, actual: totalCount });
|
|
9038
|
+
}
|
|
9039
|
+
if (typeof check.min_count === "number" && Number.isFinite(check.min_count) && totalCount < check.min_count) {
|
|
9040
|
+
failures.push({ code: "link_status_count_below_minimum", min_count: check.min_count, actual: totalCount });
|
|
9041
|
+
}
|
|
9042
|
+
return {
|
|
9043
|
+
viewport: viewport && viewport.name,
|
|
9044
|
+
selector,
|
|
9045
|
+
total_count: totalCount,
|
|
9046
|
+
discovered_count: typeof linkEvidence.discovered_count === "number" ? linkEvidence.discovered_count : totalCount,
|
|
9047
|
+
ok_count: typeof linkEvidence.ok_count === "number" ? linkEvidence.ok_count : okCount,
|
|
9048
|
+
failed_count: failures.length,
|
|
9049
|
+
truncated: linkEvidence.truncated === true,
|
|
9050
|
+
max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
|
|
9051
|
+
status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
|
|
9052
|
+
failures: failures.slice(0, 20),
|
|
9053
|
+
};
|
|
9054
|
+
}
|
|
8804
9055
|
function frameEvidenceForSelector(viewport, selector) {
|
|
8805
9056
|
const container = viewport.frames && viewport.frames[selector || ""];
|
|
8806
9057
|
if (!container || typeof container !== "object" || Array.isArray(container)) return [];
|
|
@@ -9454,6 +9705,29 @@ function assessProfile(profile, evidence) {
|
|
|
9454
9705
|
});
|
|
9455
9706
|
continue;
|
|
9456
9707
|
}
|
|
9708
|
+
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
9709
|
+
const selector = linkStatusSelector(check);
|
|
9710
|
+
const summaries = checkViewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
|
|
9711
|
+
const failed = summaries.filter((summary) => summary.failed_count > 0);
|
|
9712
|
+
checks.push({
|
|
9713
|
+
type: check.type,
|
|
9714
|
+
label: check.label || check.type,
|
|
9715
|
+
status: failed.length ? "failed" : "passed",
|
|
9716
|
+
evidence: {
|
|
9717
|
+
selector,
|
|
9718
|
+
expected_count: check.expected_count ?? null,
|
|
9719
|
+
min_count: check.min_count ?? null,
|
|
9720
|
+
allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
9721
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
9722
|
+
viewports: summaries,
|
|
9723
|
+
failures: failed.flatMap((summary) => Array.isArray(summary.failures)
|
|
9724
|
+
? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
|
|
9725
|
+
: []),
|
|
9726
|
+
},
|
|
9727
|
+
message: failed.length ? "Link status failed in " + failed.length + " viewport(s)." : undefined,
|
|
9728
|
+
});
|
|
9729
|
+
continue;
|
|
9730
|
+
}
|
|
9457
9731
|
if (check.type === "route_inventory") {
|
|
9458
9732
|
const inventories = checkViewports
|
|
9459
9733
|
.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory }))
|
|
@@ -9694,6 +9968,10 @@ function textMatches(sample, check) {
|
|
|
9694
9968
|
}
|
|
9695
9969
|
return String(sample || "").includes(check.text || "");
|
|
9696
9970
|
}
|
|
9971
|
+
function profileCheckAppliesToViewport(check, viewport) {
|
|
9972
|
+
if (!Array.isArray(check.viewports) || !check.viewports.length) return true;
|
|
9973
|
+
return Boolean(viewport && viewport.name && check.viewports.includes(viewport.name));
|
|
9974
|
+
}
|
|
9697
9975
|
function setupActionType(action) {
|
|
9698
9976
|
return String(action && action.type ? action.type : "").replace(/-/g, "_");
|
|
9699
9977
|
}
|
|
@@ -9711,6 +9989,30 @@ function setupTextMatches(sample, action) {
|
|
|
9711
9989
|
}
|
|
9712
9990
|
return String(sample || "").includes(action.text || "");
|
|
9713
9991
|
}
|
|
9992
|
+
async function waitForAnyVisibleSelector(context, selector, timeout) {
|
|
9993
|
+
const deadline = Date.now() + setupNumber(timeout, 15000);
|
|
9994
|
+
let lastReason = "selector_not_found";
|
|
9995
|
+
while (Date.now() <= deadline) {
|
|
9996
|
+
try {
|
|
9997
|
+
const locator = context.locator(selector);
|
|
9998
|
+
const count = await locator.count();
|
|
9999
|
+
if (!count) {
|
|
10000
|
+
lastReason = "selector_not_found";
|
|
10001
|
+
} else {
|
|
10002
|
+
lastReason = "no_visible_match";
|
|
10003
|
+
for (let index = 0; index < count; index += 1) {
|
|
10004
|
+
if (await locator.nth(index).isVisible().catch(() => false)) {
|
|
10005
|
+
return { ok: true, count, index };
|
|
10006
|
+
}
|
|
10007
|
+
}
|
|
10008
|
+
}
|
|
10009
|
+
} catch (error) {
|
|
10010
|
+
lastReason = String(error && error.message ? error.message : error).slice(0, 500);
|
|
10011
|
+
}
|
|
10012
|
+
await page.waitForTimeout(200);
|
|
10013
|
+
}
|
|
10014
|
+
throw new Error("No visible match for selector " + selector + ": " + lastReason);
|
|
10015
|
+
}
|
|
9714
10016
|
function setupHasOwn(action, key) {
|
|
9715
10017
|
return Boolean(action) && Object.keys(action).includes(key);
|
|
9716
10018
|
}
|
|
@@ -10044,7 +10346,7 @@ async function executeSetupAction(action, ordinal, viewport) {
|
|
|
10044
10346
|
if (type === "wait_for_selector") {
|
|
10045
10347
|
const scope = await setupActionScope(action, timeout);
|
|
10046
10348
|
if (!scope.ok) return setupScopeFailure(base, scope);
|
|
10047
|
-
await scope.context
|
|
10349
|
+
await waitForAnyVisibleSelector(scope.context, action.selector, timeout);
|
|
10048
10350
|
return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
|
|
10049
10351
|
}
|
|
10050
10352
|
if (type === "drag") {
|
|
@@ -10512,6 +10814,196 @@ async function selectorTextSequence(selector) {
|
|
|
10512
10814
|
};
|
|
10513
10815
|
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
10514
10816
|
}
|
|
10817
|
+
function linkProbeMaxLinks(check) {
|
|
10818
|
+
const value = Number(check.max_links || check.maxLinks || check.limit || 100);
|
|
10819
|
+
return Number.isInteger(value) && value > 0 ? Math.min(value, 500) : 100;
|
|
10820
|
+
}
|
|
10821
|
+
function linkProbeDedupe(check) {
|
|
10822
|
+
return check.dedupe !== false;
|
|
10823
|
+
}
|
|
10824
|
+
function linkProbeSameOriginOnly(check) {
|
|
10825
|
+
return check.same_origin_only === true;
|
|
10826
|
+
}
|
|
10827
|
+
async function collectLinkCandidates(selector) {
|
|
10828
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
10829
|
+
const compact = (value, limit) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit || 160);
|
|
10830
|
+
return elements.map((element, index) => {
|
|
10831
|
+
const tag = element && element.tagName ? element.tagName.toLowerCase() : "element";
|
|
10832
|
+
const href = element && typeof element.href === "string" ? element.href : "";
|
|
10833
|
+
const src = element && typeof element.src === "string" ? element.src : "";
|
|
10834
|
+
const currentSrc = element && typeof element.currentSrc === "string" ? element.currentSrc : "";
|
|
10835
|
+
const attrHref = element && typeof element.getAttribute === "function" ? element.getAttribute("href") || "" : "";
|
|
10836
|
+
const attrSrc = element && typeof element.getAttribute === "function" ? element.getAttribute("src") || "" : "";
|
|
10837
|
+
const attrPoster = element && typeof element.getAttribute === "function" ? element.getAttribute("poster") || "" : "";
|
|
10838
|
+
const raw = href || currentSrc || src || attrHref || attrSrc || attrPoster;
|
|
10839
|
+
if (!raw) return null;
|
|
10840
|
+
let url = "";
|
|
10841
|
+
try {
|
|
10842
|
+
url = new URL(raw, location.href).href;
|
|
10843
|
+
} catch {}
|
|
10844
|
+
if (!url) return null;
|
|
10845
|
+
return {
|
|
10846
|
+
index,
|
|
10847
|
+
tag,
|
|
10848
|
+
url,
|
|
10849
|
+
raw,
|
|
10850
|
+
text: compact(element.innerText || element.textContent || "", 160),
|
|
10851
|
+
alt: compact(element.getAttribute && element.getAttribute("alt"), 80),
|
|
10852
|
+
};
|
|
10853
|
+
}).filter(Boolean);
|
|
10854
|
+
}).catch((error) => ({ error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
10855
|
+
}
|
|
10856
|
+
function linkProbeAllowed(status, check) {
|
|
10857
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
10858
|
+
const allowed = Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
|
|
10859
|
+
? check.allowed_statuses
|
|
10860
|
+
: typeof check.expected_status === "number" && Number.isFinite(check.expected_status)
|
|
10861
|
+
? [check.expected_status]
|
|
10862
|
+
: null;
|
|
10863
|
+
return allowed ? allowed.includes(status) : status >= 200 && status < 400;
|
|
10864
|
+
}
|
|
10865
|
+
function linkProbeResponseFields(response, method) {
|
|
10866
|
+
const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
|
|
10867
|
+
const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
|
|
10868
|
+
return {
|
|
10869
|
+
method,
|
|
10870
|
+
status: response.status,
|
|
10871
|
+
redirected: Boolean(response.redirected),
|
|
10872
|
+
final_url: response.url || null,
|
|
10873
|
+
content_type: response.headers && typeof response.headers.get === "function" ? response.headers.get("content-type") : null,
|
|
10874
|
+
content_length: contentLength,
|
|
10875
|
+
};
|
|
10876
|
+
}
|
|
10877
|
+
async function probeLinkStatus(candidate, check) {
|
|
10878
|
+
const requireNonzeroBytes = check.require_nonzero_bytes === true;
|
|
10879
|
+
const allowGetFallback = check.allow_get_fallback !== false;
|
|
10880
|
+
const result = {
|
|
10881
|
+
url: candidate.url,
|
|
10882
|
+
tag: candidate.tag,
|
|
10883
|
+
text: candidate.text || null,
|
|
10884
|
+
status: null,
|
|
10885
|
+
method: null,
|
|
10886
|
+
ok: false,
|
|
10887
|
+
content_type: null,
|
|
10888
|
+
content_length: null,
|
|
10889
|
+
bytes: null,
|
|
10890
|
+
redirected: false,
|
|
10891
|
+
final_url: null,
|
|
10892
|
+
error: null,
|
|
10893
|
+
};
|
|
10894
|
+
const applyResponse = async (response, method, readBytes) => {
|
|
10895
|
+
Object.assign(result, linkProbeResponseFields(response, method));
|
|
10896
|
+
if (readBytes || requireNonzeroBytes) {
|
|
10897
|
+
try {
|
|
10898
|
+
const buffer = await response.arrayBuffer();
|
|
10899
|
+
result.bytes = buffer.byteLength;
|
|
10900
|
+
} catch (error) {
|
|
10901
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
result.ok = linkProbeAllowed(result.status, check)
|
|
10905
|
+
&& (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
|
|
10906
|
+
&& !result.error;
|
|
10907
|
+
};
|
|
10908
|
+
try {
|
|
10909
|
+
const response = await fetch(candidate.url, { method: "HEAD", redirect: "follow", cache: "no-store" });
|
|
10910
|
+
await applyResponse(response, "HEAD", false);
|
|
10911
|
+
if (result.ok || !allowGetFallback) return result;
|
|
10912
|
+
} catch (error) {
|
|
10913
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
10914
|
+
if (!allowGetFallback) return result;
|
|
10915
|
+
}
|
|
10916
|
+
try {
|
|
10917
|
+
result.error = null;
|
|
10918
|
+
const response = await fetch(candidate.url, {
|
|
10919
|
+
method: "GET",
|
|
10920
|
+
redirect: "follow",
|
|
10921
|
+
cache: "no-store",
|
|
10922
|
+
headers: { Range: "bytes=0-0" },
|
|
10923
|
+
});
|
|
10924
|
+
await applyResponse(response, "GET", requireNonzeroBytes);
|
|
10925
|
+
return result;
|
|
10926
|
+
} catch (error) {
|
|
10927
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
10928
|
+
result.ok = false;
|
|
10929
|
+
return result;
|
|
10930
|
+
}
|
|
10931
|
+
}
|
|
10932
|
+
async function collectLinkStatus(check) {
|
|
10933
|
+
const selector = linkStatusSelector(check);
|
|
10934
|
+
const candidateResult = await collectLinkCandidates(selector);
|
|
10935
|
+
if (candidateResult && candidateResult.error) {
|
|
10936
|
+
return {
|
|
10937
|
+
version: "riddle-proof.link-status.v1",
|
|
10938
|
+
selector,
|
|
10939
|
+
total_count: 0,
|
|
10940
|
+
discovered_count: 0,
|
|
10941
|
+
ok_count: 0,
|
|
10942
|
+
failed_count: 1,
|
|
10943
|
+
failures: [{ code: "link_status_capture_failed", error: candidateResult.error }],
|
|
10944
|
+
results: [],
|
|
10945
|
+
status_counts: {},
|
|
10946
|
+
error: candidateResult.error,
|
|
10947
|
+
};
|
|
10948
|
+
}
|
|
10949
|
+
const baseUrl = page.url() || targetUrl;
|
|
10950
|
+
let candidates = Array.isArray(candidateResult) ? candidateResult : [];
|
|
10951
|
+
if (linkProbeSameOriginOnly(check)) {
|
|
10952
|
+
const origin = new URL(baseUrl).origin;
|
|
10953
|
+
candidates = candidates.filter((candidate) => {
|
|
10954
|
+
try { return new URL(candidate.url).origin === origin; } catch { return false; }
|
|
10955
|
+
});
|
|
10956
|
+
}
|
|
10957
|
+
const discoveredCount = candidates.length;
|
|
10958
|
+
if (linkProbeDedupe(check)) {
|
|
10959
|
+
const seen = new Set();
|
|
10960
|
+
candidates = candidates.filter((candidate) => {
|
|
10961
|
+
if (seen.has(candidate.url)) return false;
|
|
10962
|
+
seen.add(candidate.url);
|
|
10963
|
+
return true;
|
|
10964
|
+
});
|
|
10965
|
+
}
|
|
10966
|
+
const maxLinks = linkProbeMaxLinks(check);
|
|
10967
|
+
const selected = candidates.slice(0, maxLinks);
|
|
10968
|
+
const results = [];
|
|
10969
|
+
for (const candidate of selected) {
|
|
10970
|
+
results.push(await probeLinkStatus(candidate, check));
|
|
10971
|
+
}
|
|
10972
|
+
const failures = results.filter((result) => !result.ok).map((result) => ({
|
|
10973
|
+
code: "link_status_failed",
|
|
10974
|
+
url: result.url,
|
|
10975
|
+
status: result.status,
|
|
10976
|
+
method: result.method,
|
|
10977
|
+
error: result.error,
|
|
10978
|
+
bytes: result.bytes == null ? result.content_length : result.bytes,
|
|
10979
|
+
}));
|
|
10980
|
+
const statusCounts = {};
|
|
10981
|
+
for (const result of results) {
|
|
10982
|
+
const key = result.status == null ? "error" : String(result.status);
|
|
10983
|
+
statusCounts[key] = (statusCounts[key] || 0) + 1;
|
|
10984
|
+
}
|
|
10985
|
+
return {
|
|
10986
|
+
version: "riddle-proof.link-status.v1",
|
|
10987
|
+
selector,
|
|
10988
|
+
max_links: maxLinks,
|
|
10989
|
+
same_origin_only: linkProbeSameOriginOnly(check),
|
|
10990
|
+
dedupe: linkProbeDedupe(check),
|
|
10991
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
10992
|
+
allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
|
|
10993
|
+
? check.allowed_statuses
|
|
10994
|
+
: typeof check.expected_status === "number"
|
|
10995
|
+
? [check.expected_status]
|
|
10996
|
+
: ["2xx", "3xx"],
|
|
10997
|
+
discovered_count: discoveredCount,
|
|
10998
|
+
total_count: selected.length,
|
|
10999
|
+
truncated: candidates.length > selected.length,
|
|
11000
|
+
ok_count: results.filter((result) => result.ok).length,
|
|
11001
|
+
failed_count: failures.length,
|
|
11002
|
+
status_counts: statusCounts,
|
|
11003
|
+
failures: failures.slice(0, 20),
|
|
11004
|
+
results,
|
|
11005
|
+
};
|
|
11006
|
+
}
|
|
10515
11007
|
async function frameEvidence(selector) {
|
|
10516
11008
|
const result = { selector, count: 0, frame_count: 0, frames: [], errors: [] };
|
|
10517
11009
|
let handles = [];
|
|
@@ -10870,7 +11362,7 @@ async function collectRouteInventory(check, viewport) {
|
|
|
10870
11362
|
try {
|
|
10871
11363
|
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 90000 });
|
|
10872
11364
|
if (profile.target.wait_for_selector) {
|
|
10873
|
-
await page
|
|
11365
|
+
await waitForAnyVisibleSelector(page, profile.target.wait_for_selector, 15000).catch(() => {});
|
|
10874
11366
|
}
|
|
10875
11367
|
const index = await findInventoryLinkIndex(check, expectedRoute.path);
|
|
10876
11368
|
if (index < 0) {
|
|
@@ -10929,7 +11421,7 @@ async function captureViewport(viewport) {
|
|
|
10929
11421
|
}
|
|
10930
11422
|
if (!navigationError && profile.target.wait_for_selector) {
|
|
10931
11423
|
try {
|
|
10932
|
-
await page
|
|
11424
|
+
await waitForAnyVisibleSelector(page, profile.target.wait_for_selector, 15000);
|
|
10933
11425
|
} catch (error) {
|
|
10934
11426
|
waitError = String(error && error.message ? error.message : error).slice(0, 1000);
|
|
10935
11427
|
}
|
|
@@ -11031,7 +11523,9 @@ async function captureViewport(viewport) {
|
|
|
11031
11523
|
const frames = {};
|
|
11032
11524
|
const text_sequences = {};
|
|
11033
11525
|
const text_matches = {};
|
|
11526
|
+
const link_statuses = {};
|
|
11034
11527
|
for (const check of profile.checks || []) {
|
|
11528
|
+
if (!profileCheckAppliesToViewport(check, viewport)) continue;
|
|
11035
11529
|
if (
|
|
11036
11530
|
(
|
|
11037
11531
|
check.type === "selector_visible"
|
|
@@ -11055,6 +11549,10 @@ async function captureViewport(viewport) {
|
|
|
11055
11549
|
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
11056
11550
|
frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
|
|
11057
11551
|
}
|
|
11552
|
+
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
11553
|
+
const selector = linkStatusSelector(check);
|
|
11554
|
+
link_statuses[selector] = link_statuses[selector] || await collectLinkStatus(check);
|
|
11555
|
+
}
|
|
11058
11556
|
}
|
|
11059
11557
|
const screenshotLabel = profileSlug + "-" + viewport.name;
|
|
11060
11558
|
try {
|
|
@@ -11063,7 +11561,7 @@ async function captureViewport(viewport) {
|
|
|
11063
11561
|
pageErrors.push({ message: "saveScreenshot failed: " + String(error && error.message ? error.message : error).slice(0, 500) });
|
|
11064
11562
|
}
|
|
11065
11563
|
let routeInventory;
|
|
11066
|
-
const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
|
|
11564
|
+
const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory" && profileCheckAppliesToViewport(check, viewport));
|
|
11067
11565
|
const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
|
|
11068
11566
|
if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
|
|
11069
11567
|
try {
|
|
@@ -11118,6 +11616,7 @@ async function captureViewport(viewport) {
|
|
|
11118
11616
|
frames,
|
|
11119
11617
|
text_sequences,
|
|
11120
11618
|
text_matches,
|
|
11619
|
+
link_statuses,
|
|
11121
11620
|
route_inventory: routeInventory,
|
|
11122
11621
|
setup_action_results: setupActionResults,
|
|
11123
11622
|
screenshot_label: screenshotLabel,
|
|
@@ -11166,6 +11665,18 @@ function buildProfileEvidence(currentViewports) {
|
|
|
11166
11665
|
),
|
|
11167
11666
|
})),
|
|
11168
11667
|
})),
|
|
11668
|
+
link_status: currentViewports
|
|
11669
|
+
.filter((viewport) => viewport.link_statuses && Object.keys(viewport.link_statuses).length)
|
|
11670
|
+
.map((viewport) => ({
|
|
11671
|
+
viewport: viewport.name,
|
|
11672
|
+
selectors: Object.entries(viewport.link_statuses || {}).map(([selector, statusSet]) => ({
|
|
11673
|
+
selector,
|
|
11674
|
+
total_count: statusSet && statusSet.total_count,
|
|
11675
|
+
ok_count: statusSet && statusSet.ok_count,
|
|
11676
|
+
failed_count: statusSet && statusSet.failed_count,
|
|
11677
|
+
truncated: statusSet && statusSet.truncated === true,
|
|
11678
|
+
})),
|
|
11679
|
+
})),
|
|
11169
11680
|
route_inventory: currentViewports
|
|
11170
11681
|
.filter((viewport) => viewport.route_inventory)
|
|
11171
11682
|
.map((viewport) => ({
|
|
@@ -11602,6 +12113,10 @@ function profileResultMarkdown(result) {
|
|
|
11602
12113
|
if (routeInventorySummaryLines.length) {
|
|
11603
12114
|
lines.push("", "## Route Inventory", "", ...routeInventorySummaryLines);
|
|
11604
12115
|
}
|
|
12116
|
+
const linkStatusSummaryLines = profileLinkStatusSummaryMarkdown(result);
|
|
12117
|
+
if (linkStatusSummaryLines.length) {
|
|
12118
|
+
lines.push("", "## Link Status", "", ...linkStatusSummaryLines);
|
|
12119
|
+
}
|
|
11605
12120
|
const environmentBlockerLines = profileEnvironmentBlockerMarkdown(result);
|
|
11606
12121
|
if (environmentBlockerLines.length) {
|
|
11607
12122
|
lines.push("", "## Environment Blocker", "", ...environmentBlockerLines);
|
|
@@ -11671,6 +12186,13 @@ function profileCheckMarkdownTarget(check) {
|
|
|
11671
12186
|
if (selector && maxOverflow !== void 0) return `${markdownInlineCode(selector)} <= ${maxOverflow}px`;
|
|
11672
12187
|
return selector ? markdownInlineCode(selector) : maxOverflow !== void 0 ? `<= ${maxOverflow}px` : void 0;
|
|
11673
12188
|
}
|
|
12189
|
+
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
12190
|
+
const expectedCount = cliFiniteNumber(evidence.expected_count);
|
|
12191
|
+
const minCount = cliFiniteNumber(evidence.min_count);
|
|
12192
|
+
if (selector && expectedCount !== void 0) return `${markdownInlineCode(selector)} links = ${expectedCount}`;
|
|
12193
|
+
if (selector && minCount !== void 0) return `${markdownInlineCode(selector)} links >= ${minCount}`;
|
|
12194
|
+
return selector ? markdownInlineCode(selector) : void 0;
|
|
12195
|
+
}
|
|
11674
12196
|
if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
|
|
11675
12197
|
const maxOverflow = cliFiniteNumber(evidence.max_overflow_px);
|
|
11676
12198
|
return maxOverflow !== void 0 ? `<= ${maxOverflow}px` : void 0;
|
|
@@ -11864,6 +12386,26 @@ function profileRouteInventorySummaryMarkdown(result) {
|
|
|
11864
12386
|
}
|
|
11865
12387
|
return lines;
|
|
11866
12388
|
}
|
|
12389
|
+
function profileLinkStatusSummaryMarkdown(result) {
|
|
12390
|
+
const linkStatusChecks = result.checks.filter((check) => check.type === "link_status" || check.type === "artifact_link_status");
|
|
12391
|
+
const lines = [];
|
|
12392
|
+
for (const check of linkStatusChecks) {
|
|
12393
|
+
const evidence = cliRecord(check.evidence);
|
|
12394
|
+
if (!evidence) continue;
|
|
12395
|
+
const label = check.label || check.type;
|
|
12396
|
+
const selector = cliString(evidence.selector);
|
|
12397
|
+
const viewports = Array.isArray(evidence.viewports) ? evidence.viewports.map(cliRecord).filter((viewport) => Boolean(viewport)) : [];
|
|
12398
|
+
const totals = viewports.map((viewport) => cliFiniteNumber(viewport.total_count)).filter((count) => count !== void 0);
|
|
12399
|
+
const okTotal = viewports.reduce((sum, viewport) => sum + (cliFiniteNumber(viewport.ok_count) || 0), 0);
|
|
12400
|
+
const failedTotal = viewports.reduce((sum, viewport) => sum + (cliFiniteNumber(viewport.failed_count) || 0), 0);
|
|
12401
|
+
const truncatedTotal = viewports.filter((viewport) => viewport.truncated === true).length;
|
|
12402
|
+
const countText = totals.length ? totals.join("/") : "unknown";
|
|
12403
|
+
lines.push(
|
|
12404
|
+
`- ${label}${selector ? ` ${markdownInlineCode(selector)}` : ""}: links ${countText}, ok ${okTotal}, failures ${failedTotal}${truncatedTotal ? `, truncated viewports ${truncatedTotal}` : ""}`
|
|
12405
|
+
);
|
|
12406
|
+
}
|
|
12407
|
+
return lines;
|
|
12408
|
+
}
|
|
11867
12409
|
function writeProfileOutput(outputDir, result) {
|
|
11868
12410
|
if (!outputDir) return;
|
|
11869
12411
|
(0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
|