@trailmark/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/baseline.d.ts +21 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +187 -0
- package/dist/baseline.js.map +1 -0
- package/dist/diff.d.ts +3 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +120 -0
- package/dist/diff.js.map +1 -0
- package/dist/fingerprint.d.ts +8 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +85 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/hash.d.ts +4 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +29 -0
- package/dist/hash.js.map +1 -0
- package/dist/impact.d.ts +5 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +27 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/issue.d.ts +10 -0
- package/dist/issue.d.ts.map +1 -0
- package/dist/issue.js +40 -0
- package/dist/issue.js.map +1 -0
- package/dist/normalize.d.ts +3 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +59 -0
- package/dist/normalize.js.map +1 -0
- package/dist/report/buildReport.d.ts +3 -0
- package/dist/report/buildReport.d.ts.map +1 -0
- package/dist/report/buildReport.js +223 -0
- package/dist/report/buildReport.js.map +1 -0
- package/dist/report/formatConsole.d.ts +3 -0
- package/dist/report/formatConsole.d.ts.map +1 -0
- package/dist/report/formatConsole.js +112 -0
- package/dist/report/formatConsole.js.map +1 -0
- package/dist/report/index.d.ts +4 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +20 -0
- package/dist/report/index.js.map +1 -0
- package/dist/report/sort.d.ts +9 -0
- package/dist/report/sort.d.ts.map +1 -0
- package/dist/report/sort.js +20 -0
- package/dist/report/sort.js.map +1 -0
- package/dist/report/types.d.ts +85 -0
- package/dist/report/types.d.ts.map +1 -0
- package/dist/report/types.js +3 -0
- package/dist/report/types.js.map +1 -0
- package/dist/run.d.ts +8 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +20 -0
- package/dist/run.js.map +1 -0
- package/dist/scope.d.ts +5 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +23 -0
- package/dist/scope.js.map +1 -0
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/url.d.ts +3 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +82 -0
- package/dist/url.js.map +1 -0
- package/package.json +23 -0
- package/src/baseline.ts +217 -0
- package/src/diff.ts +150 -0
- package/src/fingerprint.ts +107 -0
- package/src/hash.ts +29 -0
- package/src/impact.ts +27 -0
- package/src/index.ts +11 -0
- package/src/issue.ts +49 -0
- package/src/normalize.ts +66 -0
- package/src/report/buildReport.ts +276 -0
- package/src/report/formatConsole.ts +137 -0
- package/src/report/index.ts +3 -0
- package/src/report/sort.ts +28 -0
- package/src/report/types.ts +101 -0
- package/src/run.ts +22 -0
- package/src/scope.ts +25 -0
- package/src/types.ts +167 -0
- package/src/url.ts +102 -0
- package/test/baseline.test.ts +151 -0
- package/test/diff.test.ts +113 -0
- package/test/fingerprint.test.ts +41 -0
- package/test/issue.test.ts +79 -0
- package/test/report.build.test.ts +191 -0
- package/test/report.console.test.ts +102 -0
- package/test/url.test.ts +25 -0
- package/tsconfig.json +8 -0
package/src/diff.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaselineEntry,
|
|
3
|
+
BaselineFile,
|
|
4
|
+
DiffOptions,
|
|
5
|
+
DiffResult,
|
|
6
|
+
TrailmarkFinding,
|
|
7
|
+
TrailmarkRun,
|
|
8
|
+
} from "./types"
|
|
9
|
+
import { scopedIssueKey, scopeKeyFromBaselineEntry, scopeKeyFromMeta } from "./scope"
|
|
10
|
+
import { impactKey } from "./impact"
|
|
11
|
+
|
|
12
|
+
function dedupeFindings(findings: TrailmarkFinding[]): TrailmarkFinding[] {
|
|
13
|
+
const unique = new Map<string, TrailmarkFinding>()
|
|
14
|
+
for (const finding of findings) {
|
|
15
|
+
const key = scopedIssueKey(finding.issueId, scopeKeyFromMeta(finding.meta))
|
|
16
|
+
if (!unique.has(key)) {
|
|
17
|
+
unique.set(key, finding)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return Array.from(unique.entries())
|
|
22
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
23
|
+
.map(([, finding]) => finding)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeRelaxedMap(baselineEntries: BaselineEntry[]): Map<string, BaselineEntry[]> {
|
|
27
|
+
const map = new Map<string, BaselineEntry[]>()
|
|
28
|
+
for (const entry of baselineEntries) {
|
|
29
|
+
const key = scopedIssueKey(entry.relaxedIssueId, scopeKeyFromBaselineEntry(entry))
|
|
30
|
+
const list = map.get(key) ?? []
|
|
31
|
+
list.push(entry)
|
|
32
|
+
map.set(key, list)
|
|
33
|
+
}
|
|
34
|
+
return map
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeExactKey(entry: BaselineEntry): string {
|
|
38
|
+
return scopedIssueKey(entry.issueId, scopeKeyFromBaselineEntry(entry))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pickUnmatched(
|
|
42
|
+
list: BaselineEntry[] | undefined,
|
|
43
|
+
matchedBaselineIds: Set<string>,
|
|
44
|
+
): BaselineEntry | undefined {
|
|
45
|
+
if (!list || list.length === 0) {
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return list.find((entry) => !matchedBaselineIds.has(makeExactKey(entry)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function diffRuns(
|
|
53
|
+
baseline: BaselineFile,
|
|
54
|
+
current: TrailmarkRun,
|
|
55
|
+
options: DiffOptions = {},
|
|
56
|
+
): DiffResult {
|
|
57
|
+
const debug = options.onDebug
|
|
58
|
+
const failOnImpact = new Set(
|
|
59
|
+
(options.failOnImpact ?? ["serious", "critical"]).map((value) => value.toLowerCase()),
|
|
60
|
+
)
|
|
61
|
+
const baselineEntries = Object.values(baseline.findings)
|
|
62
|
+
const baselineByIssueId = new Map(baselineEntries.map((entry) => [makeExactKey(entry), entry]))
|
|
63
|
+
const baselineByRelaxed = makeRelaxedMap(baselineEntries)
|
|
64
|
+
|
|
65
|
+
const newFindings: TrailmarkFinding[] = []
|
|
66
|
+
const existing: DiffResult["existing"] = []
|
|
67
|
+
const matchedBaselineIds = new Set<string>()
|
|
68
|
+
|
|
69
|
+
for (const finding of dedupeFindings(current.findings)) {
|
|
70
|
+
const scopeKey = scopeKeyFromMeta(finding.meta)
|
|
71
|
+
const scopedFindingIssueKey = scopedIssueKey(finding.issueId, scopeKey)
|
|
72
|
+
const exact = baselineByIssueId.get(scopedFindingIssueKey)
|
|
73
|
+
|
|
74
|
+
if (exact) {
|
|
75
|
+
const baselineKey = makeExactKey(exact)
|
|
76
|
+
existing.push({
|
|
77
|
+
finding,
|
|
78
|
+
matchedBy: "exact",
|
|
79
|
+
baselineIssueId: exact.issueId,
|
|
80
|
+
baselineScopeKey: scopeKeyFromBaselineEntry(exact),
|
|
81
|
+
})
|
|
82
|
+
matchedBaselineIds.add(baselineKey)
|
|
83
|
+
debug?.("exact-match", {
|
|
84
|
+
findingIssueId: finding.issueId,
|
|
85
|
+
findingScopeKey: scopeKey,
|
|
86
|
+
baselineIssueId: exact.issueId,
|
|
87
|
+
baselineScopeKey: scopeKeyFromBaselineEntry(exact),
|
|
88
|
+
})
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const relaxedMatches = baselineByRelaxed.get(scopedIssueKey(finding.relaxedIssueId, scopeKey))
|
|
93
|
+
const relaxed = pickUnmatched(relaxedMatches, matchedBaselineIds)
|
|
94
|
+
|
|
95
|
+
if (relaxed) {
|
|
96
|
+
const baselineKey = makeExactKey(relaxed)
|
|
97
|
+
existing.push({
|
|
98
|
+
finding,
|
|
99
|
+
matchedBy: "relaxed",
|
|
100
|
+
baselineIssueId: relaxed.issueId,
|
|
101
|
+
baselineScopeKey: scopeKeyFromBaselineEntry(relaxed),
|
|
102
|
+
})
|
|
103
|
+
matchedBaselineIds.add(baselineKey)
|
|
104
|
+
debug?.("relaxed-match", {
|
|
105
|
+
findingIssueId: finding.issueId,
|
|
106
|
+
findingScopeKey: scopeKey,
|
|
107
|
+
baselineIssueId: relaxed.issueId,
|
|
108
|
+
baselineScopeKey: scopeKeyFromBaselineEntry(relaxed),
|
|
109
|
+
})
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
newFindings.push(finding)
|
|
114
|
+
debug?.("new-finding", {
|
|
115
|
+
findingIssueId: finding.issueId,
|
|
116
|
+
findingScopeKey: scopeKey,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const resolved = baselineEntries.filter((entry) => !matchedBaselineIds.has(makeExactKey(entry)))
|
|
121
|
+
for (const entry of resolved) {
|
|
122
|
+
debug?.("resolved-finding", {
|
|
123
|
+
baselineIssueId: entry.issueId,
|
|
124
|
+
baselineScopeKey: scopeKeyFromBaselineEntry(entry),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const newByImpact: Record<string, number> = {}
|
|
129
|
+
for (const finding of newFindings) {
|
|
130
|
+
const key = impactKey(finding.impact)
|
|
131
|
+
newByImpact[key] = (newByImpact[key] ?? 0) + 1
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const blockingNew = newFindings.filter((finding) => failOnImpact.has(impactKey(finding.impact)))
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
newFindings,
|
|
138
|
+
existing,
|
|
139
|
+
resolved,
|
|
140
|
+
blockingNew,
|
|
141
|
+
stats: {
|
|
142
|
+
totalCurrent: dedupeFindings(current.findings).length,
|
|
143
|
+
totalBaseline: baselineEntries.length,
|
|
144
|
+
newCount: newFindings.length,
|
|
145
|
+
existingCount: existing.length,
|
|
146
|
+
resolvedCount: resolved.length,
|
|
147
|
+
newByImpact,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { hashObject } from "./hash"
|
|
2
|
+
import { AxeNodeResult, TargetFingerprintCanonical } from "./types"
|
|
3
|
+
|
|
4
|
+
const STABLE_ATTR_NAMES = [
|
|
5
|
+
"data-testid",
|
|
6
|
+
"data-test",
|
|
7
|
+
"data-qa",
|
|
8
|
+
"aria-label",
|
|
9
|
+
"aria-labelledby",
|
|
10
|
+
"id",
|
|
11
|
+
"name",
|
|
12
|
+
"role",
|
|
13
|
+
"type",
|
|
14
|
+
"href",
|
|
15
|
+
"for",
|
|
16
|
+
] as const
|
|
17
|
+
|
|
18
|
+
const STABLE_ATTR_SET = new Set<string>(STABLE_ATTR_NAMES)
|
|
19
|
+
|
|
20
|
+
function truncate(value: string, maxLength = 120): string {
|
|
21
|
+
if (value.length <= maxLength) {
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
return value.slice(0, maxLength)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseTag(html: string | undefined): string | null {
|
|
28
|
+
if (!html) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const match = html.match(/^\s*<\s*([a-zA-Z0-9:-]+)/)
|
|
33
|
+
return match ? match[1].toLowerCase() : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseStableAttrs(html: string | undefined): Record<string, string> {
|
|
37
|
+
if (!html) {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const attrs: Record<string, string> = {}
|
|
42
|
+
const attrRegex = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
|
|
43
|
+
|
|
44
|
+
for (const match of html.matchAll(attrRegex)) {
|
|
45
|
+
const rawName = match[1]
|
|
46
|
+
const name = rawName.toLowerCase()
|
|
47
|
+
if (!STABLE_ATTR_SET.has(name)) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const value = match[3] ?? match[4] ?? match[5] ?? ""
|
|
52
|
+
attrs[name] = truncate(value.trim())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return attrs
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sanitizeSelectorHint(selector: string): string {
|
|
59
|
+
let output = selector
|
|
60
|
+
.replace(/:nth-[a-z-]+\([^)]*\)/gi, "")
|
|
61
|
+
.replace(/\[(\w[\w-]*)\s*=\s*["']?\d{2,}["']?\s*\]/g, "[$1]")
|
|
62
|
+
.replace(/\b\d{2,}\b/g, "")
|
|
63
|
+
.replace(/\s+/g, " ")
|
|
64
|
+
.trim()
|
|
65
|
+
return truncate(output, 120)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function selectorHintFromTarget(target: string[] | undefined): string | null {
|
|
69
|
+
const first = target?.[0]
|
|
70
|
+
if (!first) {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const chunks = first
|
|
75
|
+
.split(/\s+|>|\+|~/)
|
|
76
|
+
.map((part) => part.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
|
|
79
|
+
const last = chunks.length > 0 ? chunks[chunks.length - 1] : first
|
|
80
|
+
const hint = sanitizeSelectorHint(last)
|
|
81
|
+
return hint || null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TargetFingerprintResult {
|
|
85
|
+
canonical: TargetFingerprintCanonical
|
|
86
|
+
targetHash: string
|
|
87
|
+
selectorHint: string | null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildTargetFingerprint(
|
|
91
|
+
node: Pick<AxeNodeResult, "target" | "html">,
|
|
92
|
+
): TargetFingerprintResult {
|
|
93
|
+
const selectorHint = selectorHintFromTarget(node.target)
|
|
94
|
+
const stableAttrs = parseStableAttrs(node.html)
|
|
95
|
+
|
|
96
|
+
const canonical: TargetFingerprintCanonical = {
|
|
97
|
+
tag: parseTag(node.html),
|
|
98
|
+
stableAttrs,
|
|
99
|
+
structuralHint: Object.keys(stableAttrs).length === 0 ? selectorHint : null,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
canonical,
|
|
104
|
+
selectorHint,
|
|
105
|
+
targetHash: hashObject(canonical),
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
function sortValue(value: unknown): unknown {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value.map((item) => sortValue(item))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (value && typeof value === "object") {
|
|
9
|
+
const sorted: Record<string, unknown> = {}
|
|
10
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
11
|
+
sorted[key] = sortValue((value as Record<string, unknown>)[key])
|
|
12
|
+
}
|
|
13
|
+
return sorted
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function stableStringify(value: unknown): string {
|
|
20
|
+
return JSON.stringify(sortValue(value))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sha1Hex(input: string): string {
|
|
24
|
+
return createHash("sha1").update(input).digest("hex")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hashObject(input: unknown): string {
|
|
28
|
+
return sha1Hex(stableStringify(input))
|
|
29
|
+
}
|
package/src/impact.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TrailmarkImpact, TrailmarkImpactKey } from "./types"
|
|
2
|
+
|
|
3
|
+
export const IMPACT_ORDER: TrailmarkImpactKey[] = [
|
|
4
|
+
"critical",
|
|
5
|
+
"serious",
|
|
6
|
+
"moderate",
|
|
7
|
+
"minor",
|
|
8
|
+
"unknown",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export function impactKey(impact: TrailmarkImpact | TrailmarkImpactKey): TrailmarkImpactKey {
|
|
12
|
+
if (!impact) {
|
|
13
|
+
return "unknown"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return impact
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function impactRank(impact: TrailmarkImpact | TrailmarkImpactKey): number {
|
|
20
|
+
const key = impactKey(impact)
|
|
21
|
+
const index = IMPACT_ORDER.indexOf(key)
|
|
22
|
+
if (index >= 0) {
|
|
23
|
+
return index
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return IMPACT_ORDER.length + 1
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types"
|
|
2
|
+
export * from "./hash"
|
|
3
|
+
export * from "./url"
|
|
4
|
+
export * from "./issue"
|
|
5
|
+
export * from "./fingerprint"
|
|
6
|
+
export * from "./normalize"
|
|
7
|
+
export * from "./run"
|
|
8
|
+
export * from "./baseline"
|
|
9
|
+
export * from "./scope"
|
|
10
|
+
export * from "./diff"
|
|
11
|
+
export * from "./report"
|
package/src/issue.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { hashObject } from "./hash"
|
|
2
|
+
|
|
3
|
+
export interface IssueIdInput {
|
|
4
|
+
ruleId: string
|
|
5
|
+
pageKey: string
|
|
6
|
+
targetHash: string
|
|
7
|
+
failureHint?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeFailureHint(summary: string | undefined): string | undefined {
|
|
11
|
+
if (!summary) {
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalized = summary
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/["']/g, "")
|
|
18
|
+
.replace(/\d+/g, "#")
|
|
19
|
+
.replace(/\s+/g, " ")
|
|
20
|
+
.trim()
|
|
21
|
+
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return normalized
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildRelaxedIssueId(input: Omit<IssueIdInput, "failureHint">): string {
|
|
30
|
+
return hashObject({
|
|
31
|
+
ruleId: input.ruleId,
|
|
32
|
+
pageKey: input.pageKey,
|
|
33
|
+
targetHash: input.targetHash,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildIssueId(input: IssueIdInput): string {
|
|
38
|
+
const payload: Record<string, string> = {
|
|
39
|
+
ruleId: input.ruleId,
|
|
40
|
+
pageKey: input.pageKey,
|
|
41
|
+
targetHash: input.targetHash,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (input.failureHint) {
|
|
45
|
+
payload.failureHint = input.failureHint
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return hashObject(payload)
|
|
49
|
+
}
|
package/src/normalize.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { buildTargetFingerprint } from "./fingerprint"
|
|
2
|
+
import { buildIssueId, buildRelaxedIssueId, normalizeFailureHint } from "./issue"
|
|
3
|
+
import { normalizeUrlToPageKey } from "./url"
|
|
4
|
+
import { NormalizeInput, TrailmarkFinding } from "./types"
|
|
5
|
+
|
|
6
|
+
function truncate(value: string | undefined, maxLength: number): string | undefined {
|
|
7
|
+
if (!value) {
|
|
8
|
+
return undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (value.length <= maxLength) {
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return value.slice(0, maxLength)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeAxeResults(input: NormalizeInput): TrailmarkFinding[] {
|
|
19
|
+
const pageKey = normalizeUrlToPageKey(input.pageUrl, input.urlOptions)
|
|
20
|
+
const htmlMaxLength = input.htmlMaxLength ?? 400
|
|
21
|
+
const failureSummaryMaxLength = input.failureSummaryMaxLength ?? 400
|
|
22
|
+
|
|
23
|
+
const findings: TrailmarkFinding[] = []
|
|
24
|
+
|
|
25
|
+
for (const violation of input.results.violations ?? []) {
|
|
26
|
+
for (const node of violation.nodes ?? []) {
|
|
27
|
+
const fingerprint = buildTargetFingerprint(node)
|
|
28
|
+
const failureSummary = truncate(node.failureSummary, failureSummaryMaxLength)
|
|
29
|
+
const failureHint = normalizeFailureHint(failureSummary)
|
|
30
|
+
|
|
31
|
+
const relaxedIssueId = buildRelaxedIssueId({
|
|
32
|
+
ruleId: violation.id,
|
|
33
|
+
pageKey,
|
|
34
|
+
targetHash: fingerprint.targetHash,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const issueId = buildIssueId({
|
|
38
|
+
ruleId: violation.id,
|
|
39
|
+
pageKey,
|
|
40
|
+
targetHash: fingerprint.targetHash,
|
|
41
|
+
failureHint,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
findings.push({
|
|
45
|
+
issueId,
|
|
46
|
+
relaxedIssueId,
|
|
47
|
+
ruleId: violation.id,
|
|
48
|
+
impact: violation.impact,
|
|
49
|
+
tags: [...(violation.tags ?? [])],
|
|
50
|
+
help: violation.help,
|
|
51
|
+
helpUrl: violation.helpUrl,
|
|
52
|
+
pageUrl: input.pageUrl,
|
|
53
|
+
pageKey,
|
|
54
|
+
target: Array.isArray(node.target) ? [...node.target] : [],
|
|
55
|
+
selectorHint: fingerprint.selectorHint ?? undefined,
|
|
56
|
+
targetHash: fingerprint.targetHash,
|
|
57
|
+
nodeHtml: truncate(node.html, htmlMaxLength),
|
|
58
|
+
failureSummary,
|
|
59
|
+
failureHint,
|
|
60
|
+
meta: input.meta,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return findings
|
|
66
|
+
}
|