UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

1,055 lines (1,030 loc) 45.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildFullDomTree = buildFullDomTree; exports.diffFullTrees = diffFullTrees; exports.renderFullTreeCode = renderFullTreeCode; exports.renderHtmlDiffTree = renderHtmlDiffTree; exports.renderSideTree = renderSideTree; exports.buildHtmlReportContent = buildHtmlReportContent; const a11y_comparer_1 = require("./a11y-comparer"); const helpers_1 = require("./helpers"); const parse_html_1 = require("./parse-html"); // ============= Full DOM Tree Engine ============= function buildFullDomTree(html) { const doc = (0, parse_html_1.parseHtml)(html); const body = doc.body; if (!body) return []; return collectFullDomChildren(body); } function collectFullDomChildren(parent) { const nodes = []; let child = parent.firstElementChild; while (child) { const tagName = child.tagName.toLowerCase(); const attrs = []; const classes = []; for (let i = 0; i < child.attributes.length; i++) { const a = child.attributes[i]; attrs.push({ name: a.name, value: a.value }); if (a.name === 'class') { classes.push(...a.value.trim().split(/\s+/).filter(Boolean).sort()); } } nodes.push({ tagName, classes, classSelector: classes.length > 0 ? '.' + classes.join('.') : '', attributes: attrs, children: collectFullDomChildren(child), }); child = child.nextElementSibling; } return nodes; } function diffFullTrees(actual, expected) { const results = []; const usedExpected = new Set(); for (const aNode of actual) { let bestMatch = -1; let bestScore = -1; let isExact = false; // Exact match: same tag + same classSelector for (let ei = 0; ei < expected.length; ei++) { if (usedExpected.has(ei)) continue; if (aNode.tagName === expected[ei].tagName && aNode.classSelector === expected[ei].classSelector) { bestMatch = ei; isExact = true; break; } } // Similarity match if (!isExact) { for (let ei = 0; ei < expected.length; ei++) { if (usedExpected.has(ei)) continue; let score; if (aNode.classes.length === 0 && expected[ei].classes.length === 0) { // Both classless: match only by tag name score = aNode.tagName === expected[ei].tagName ? 1.0 : 0; } else { score = (0, helpers_1.jaccardSimilarity)(aNode.classes, expected[ei].classes); if (aNode.tagName === expected[ei].tagName) score += 0.05; } if (score > bestScore && score >= 0.3) { bestScore = score; bestMatch = ei; } } } if (bestMatch >= 0) { usedExpected.add(bestMatch); const eNode = expected[bestMatch]; const common = aNode.classes.filter(c => eNode.classes.includes(c)); const extra = aNode.classes.filter(c => !eNode.classes.includes(c)); const miss = eNode.classes.filter(c => !aNode.classes.includes(c)); results.push({ status: isExact ? 'matched' : 'different', actualNode: aNode, expectedNode: eNode, matchedClasses: isExact ? [...aNode.classes] : common, extraClasses: isExact ? [] : extra, missingClasses: isExact ? [] : miss, children: diffFullTrees(aNode.children, eNode.children), }); } else { results.push(markFullSubtree(aNode, 'extra')); } } // Unmatched expected → missing for (let ei = 0; ei < expected.length; ei++) { if (!usedExpected.has(ei)) { results.push(markFullSubtree(expected[ei], 'missing')); } } return results; } function markFullSubtree(node, status) { return { status, actualNode: status === 'extra' ? node : undefined, expectedNode: status === 'missing' ? node : undefined, matchedClasses: [], extraClasses: status === 'extra' ? [...node.classes] : [], missingClasses: status === 'missing' ? [...node.classes] : [], children: node.children.map(c => markFullSubtree(c, status)), }; } function renderFullNodeAttrs(src, diff, side) { const parts = []; for (const attr of src.attributes) { if (attr.name === 'class' && diff.status === 'different') { const classSpans = []; for (const cls of diff.matchedClasses) { classSpans.push(`<span class="ft-cls-match">${(0, helpers_1.escHtml)(cls)}</span>`); } if (side === 'actual') { for (const cls of diff.extraClasses) { classSpans.push(`<span class="ft-cls-extra">${(0, helpers_1.escHtml)(cls)}</span>`); } } else { for (const cls of diff.missingClasses) { classSpans.push(`<span class="ft-cls-missing">${(0, helpers_1.escHtml)(cls)}</span>`); } } parts.push(`<span class="ft-attr-name">class</span>=<span class="ft-attr-val">"${classSpans.join(' ')}"</span>`); } else { parts.push(`<span class="ft-attr-name">${(0, helpers_1.escHtml)(attr.name)}</span>=<span class="ft-attr-val">"${(0, helpers_1.escHtml)(attr.value)}"</span>`); } } return parts.length > 0 ? ' ' + parts.join(' ') : ''; } function renderFullTreeCode(nodes, side, depth = 0) { const lines = []; const pad = ' '.repeat(depth); for (const node of nodes) { if (side === 'expected' && node.status === 'extra') continue; if (side === 'actual' && node.status === 'missing') continue; const src = side === 'expected' ? node.expectedNode : node.actualNode; if (!src) continue; const cssClass = `ft-${node.status}`; const tag = (0, helpers_1.escHtml)(src.tagName); const attrs = renderFullNodeAttrs(src, node, side); const hasVisibleChildren = node.children.some(c => { if (side === 'expected') return c.status !== 'extra'; return c.status !== 'missing'; }); if (hasVisibleChildren || src.children.length > 0) { lines.push(`<span class="${cssClass}">${pad}&lt;<span class="ft-tag">${tag}</span>${attrs}&gt;</span>`); lines.push(renderFullTreeCode(node.children, side, depth + 1)); lines.push(`<span class="${cssClass}">${pad}&lt;/<span class="ft-tag">${tag}</span>&gt;</span>`); } else { lines.push(`<span class="${cssClass}">${pad}&lt;<span class="ft-tag">${tag}</span>${attrs} /&gt;</span>`); } } return lines.filter(Boolean).join('\n'); } // ============= A11y Full DOM Rendering ============= function isA11yAttrName(name) { return name.startsWith('aria-') || name === 'role'; } function computeA11yAttrDiff(actualNode, expectedNode) { const result = { matched: [], missing: [], extra: [], different: [] }; const actualA11y = new Map(); const expectedA11y = new Map(); if (actualNode) { for (const attr of actualNode.attributes) { if (isA11yAttrName(attr.name)) actualA11y.set(attr.name, attr.value); } } if (expectedNode) { for (const attr of expectedNode.attributes) { if (isA11yAttrName(attr.name)) expectedA11y.set(attr.name, attr.value); } } for (const [name, expectedValue] of expectedA11y) { if (actualA11y.has(name)) { const actualValue = actualA11y.get(name); if (actualValue === expectedValue) { result.matched.push({ name, value: expectedValue }); } else { result.different.push({ name, actualValue, expectedValue }); } } else { result.missing.push({ name, value: expectedValue }); } } for (const [name, value] of actualA11y) { if (!expectedA11y.has(name)) { result.extra.push({ name, value }); } } return result; } function renderFullNodeAttrsA11y(src, diff, side, carriedSet) { const a11yDiff = computeA11yAttrDiff(diff.actualNode, diff.expectedNode); const matchedNames = new Set(a11yDiff.matched.map(a => a.name)); const missingNames = new Set(a11yDiff.missing.map(a => a.name)); const extraNames = new Set(a11yDiff.extra.map(a => a.name)); const differentNames = new Set(a11yDiff.different.map(a => a.name)); const parts = []; for (const attr of src.attributes) { if (!isA11yAttrName(attr.name)) { // Non-a11y attribute: render dim parts.push(`<span class="ft-attr-name">${(0, helpers_1.escHtml)(attr.name)}</span>=<span class="ft-attr-val">"${(0, helpers_1.escHtml)(attr.value)}"</span>`); continue; } // A11y attribute: color-code based on diff // Check if this attr was carried from a classless ancestor const isCarried = carriedSet === null || carriedSet === void 0 ? void 0 : carriedSet.has(`${src.classSelector}:${attr.name}`); let cssClass = 'ft-a11y-match'; if (isCarried) { cssClass = 'ft-a11y-carried'; } else if (diff.status === 'extra') { cssClass = 'ft-a11y-extra'; } else if (diff.status === 'missing') { cssClass = 'ft-a11y-missing'; } else if (matchedNames.has(attr.name)) { cssClass = 'ft-a11y-match'; } else if (differentNames.has(attr.name)) { cssClass = 'ft-a11y-different'; } else if (side === 'expected' && missingNames.has(attr.name)) { cssClass = 'ft-a11y-missing'; } else if (side === 'actual' && extraNames.has(attr.name)) { cssClass = 'ft-a11y-extra'; } parts.push(`<span class="${cssClass}"><span class="ft-attr-name">${(0, helpers_1.escHtml)(attr.name)}</span>=<span class="ft-attr-val">"${(0, helpers_1.escHtml)(attr.value)}"</span></span>`); } return parts.length > 0 ? ' ' + parts.join(' ') : ''; } function renderFullTreeCodeA11y(nodes, side, depth = 0, carriedSet) { const lines = []; const pad = ' '.repeat(depth); for (const node of nodes) { if (side === 'expected' && node.status === 'extra') continue; if (side === 'actual' && node.status === 'missing') continue; const src = side === 'expected' ? node.expectedNode : node.actualNode; if (!src) continue; // Determine node-level CSS class based on a11y attr diffs const a11yDiff = computeA11yAttrDiff(node.actualNode, node.expectedNode); const hasA11yDiffs = a11yDiff.missing.length > 0 || a11yDiff.extra.length > 0 || a11yDiff.different.length > 0; let cssClass; if (node.status === 'extra') { cssClass = 'ft-extra'; } else if (node.status === 'missing') { cssClass = 'ft-missing'; } else if (hasA11yDiffs) { cssClass = 'ft-different'; } else { cssClass = 'ft-matched'; } const tag = (0, helpers_1.escHtml)(src.tagName); const attrs = renderFullNodeAttrsA11y(src, node, side, carriedSet); const hasVisibleChildren = node.children.some(c => { if (side === 'expected') return c.status !== 'extra'; return c.status !== 'missing'; }); if (hasVisibleChildren || src.children.length > 0) { lines.push(`<span class="${cssClass}">${pad}&lt;<span class="ft-tag">${tag}</span>${attrs}&gt;</span>`); lines.push(renderFullTreeCodeA11y(node.children, side, depth + 1, carriedSet)); lines.push(`<span class="${cssClass}">${pad}&lt;/<span class="ft-tag">${tag}</span>&gt;</span>`); } else { lines.push(`<span class="${cssClass}">${pad}&lt;<span class="ft-tag">${tag}</span>${attrs} /&gt;</span>`); } } return lines.filter(Boolean).join('\n'); } // ============= HTML Report Tree Renderers ============= function renderHtmlDiffTree(nodes) { if (nodes.length === 0) return ''; const items = nodes.map(node => { let content; let cssClass; switch (node.status) { case 'matched': cssClass = 'node-matched'; content = `<span class="node ${cssClass}">&#10003; ${(0, helpers_1.escHtml)(node.classSelector)}</span>`; break; case 'extra': cssClass = 'node-extra'; content = `<span class="node ${cssClass}">+ ${(0, helpers_1.escHtml)(node.classSelector)}</span><span class="node-label">EXTRA</span>`; break; case 'missing': cssClass = 'node-missing'; content = `<span class="node ${cssClass}">&minus; ${(0, helpers_1.escHtml)(node.classSelector)}</span><span class="node-label">MISSING</span>`; break; case 'different': { cssClass = 'node-different'; const parts = []; if (node.matchedClasses.length > 0) { parts.push(`<span class="class-matched">.${node.matchedClasses.join('.')}</span>`); } if (node.extraClasses.length > 0) { parts.push(`<span class="class-extra">+[.${node.extraClasses.join('.')}]</span>`); } if (node.missingClasses.length > 0) { parts.push(`<span class="class-missing">&minus;[.${node.missingClasses.join('.')}]</span>`); } content = `<span class="node ${cssClass}">~ ${parts.join(' ')}</span>`; break; } } const childrenHtml = node.children.length > 0 ? renderHtmlDiffTree(node.children) : ''; return `<li>${content}${childrenHtml}</li>`; }).join('\n'); return `<ul>${items}</ul>`; } function renderSideTree(nodes, side) { if (nodes.length === 0) return ''; const items = nodes.map(node => { const nodeInfo = getSideNodeInfo(node, side); if (!nodeInfo) return ''; let classStr; let bgClass; switch (node.status) { case 'matched': classStr = nodeInfo.classes.join(' '); bgClass = 'matched-bg'; break; case 'extra': if (side === 'expected') return ''; classStr = nodeInfo.classes.join(' '); bgClass = 'extra-bg'; break; case 'missing': if (side === 'actual') return ''; classStr = nodeInfo.classes.join(' '); bgClass = 'missing-bg'; break; case 'different': bgClass = 'different-bg'; if (side === 'actual') { classStr = [...node.matchedClasses, ...node.extraClasses].join(' '); } else { classStr = [...node.matchedClasses, ...node.missingClasses].join(' '); } break; default: classStr = nodeInfo.classes.join(' '); bgClass = ''; } const childrenHtml = renderSideTree(node.children, side); const classDisplay = classStr ? `<span class="dom-tag">&lt;div</span> <span class="dom-classes ${bgClass}">class="${(0, helpers_1.escHtml)(classStr)}"</span><span class="dom-tag">&gt;</span>` : `<span class="dom-tag">&lt;div&gt;</span>`; return `<li>${classDisplay}${childrenHtml}</li>`; }).filter(Boolean).join('\n'); if (!items) return ''; return `<ul>${items}</ul>`; } function getSideNodeInfo(node, side) { switch (node.status) { case 'matched': return { classes: node.matchedClasses }; case 'extra': return side === 'actual' ? { classes: node.extraClasses } : null; case 'missing': return side === 'expected' ? { classes: node.missingClasses } : null; case 'different': { if (side === 'actual') { return { classes: [...node.matchedClasses, ...node.extraClasses] }; } else { return { classes: [...node.matchedClasses, ...node.missingClasses] }; } } default: return null; } } function generateSuggestionsCode(result) { var _a, _b; const lines = []; if ((_a = result.suggestedAllowMissing) === null || _a === void 0 ? void 0 : _a.length) { lines.push('<span class="code-missing">allowMissing: [</span>'); result.suggestedAllowMissing.forEach((s, i) => { const comma = i < result.suggestedAllowMissing.length - 1 ? ',' : ''; lines.push(` <span class="code-missing">"${(0, helpers_1.escHtml)(s)}"${comma}</span>`); }); lines.push('<span class="code-missing">]</span>'); } if ((_b = result.suggestedAllowExtra) === null || _b === void 0 ? void 0 : _b.length) { if (lines.length > 0) lines.push(''); lines.push('<span class="code-extra">allowExtra: [</span>'); result.suggestedAllowExtra.forEach((s, i) => { const comma = i < result.suggestedAllowExtra.length - 1 ? ',' : ''; lines.push(` <span class="code-extra">"${(0, helpers_1.escHtml)(s)}"${comma}</span>`); }); lines.push('<span class="code-extra">]</span>'); } return lines.join('\n'); } // ============= CSS Styles ============= function getReportStyles() { return ` :root { --green: #22c55e; --red: #ef4444; --purple: #a855f7; --cyan: #06b6d4; --gray: #6b7280; --orange: #f97316; --bg: #0f172a; --bg-card: #1e293b; --bg-code: #0d1117; --text: #e2e8f0; --text-muted: #94a3b8; --border: #334155; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace; background: var(--bg); color: var(--text); padding: 24px; font-size: 13px; line-height: 1.5; } h1 { font-size: 18px; margin-bottom: 4px; color: var(--text); } h2 { font-size: 14px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); } .subtitle { color: var(--text-muted); font-weight: normal; font-size: 12px; } .header { margin-bottom: 24px; padding-bottom: 16px; border-bottom: 2px solid var(--border); } .header .meta { color: var(--text-muted); font-size: 12px; margin-top: 4px; } .summary { display: flex; gap: 24px; margin-bottom: 24px; } .stat { padding: 12px 20px; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border); text-align: center; min-width: 120px; } .stat .num { font-size: 28px; font-weight: bold; display: block; } .stat .label { font-size: 11px; text-transform: uppercase; color: var(--text-muted); } .stat.passed .num { color: var(--green); } .stat.missing .num { color: var(--red); } .stat.extra .num { color: var(--purple); } .all-good { background: #064e3b; border: 1px solid var(--green); color: var(--green); padding: 16px; border-radius: 8px; text-align: center; font-size: 16px; font-weight: bold; } .section { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 20px; } .missing-title { color: var(--red); } .extra-title { color: var(--purple); } /* Selector lists */ .selector-list { list-style: none; padding: 0; } .selector-list li { padding: 4px 8px; margin: 2px 0; border-radius: 4px; font-size: 12px; } .missing-item { color: var(--red); background: rgba(239,68,68,0.08); } .extra-item { color: var(--purple); background: rgba(168,85,247,0.08); } /* Tree */ .tree { padding: 0; } .tree ul { list-style: none; padding-left: 24px; border-left: 1px solid var(--border); margin-left: 8px; } .tree li { position: relative; padding: 3px 0; } .tree li::before { content: ''; position: absolute; left: -24px; top: 50%; width: 20px; height: 1px; background: var(--border); } .node { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; } .node-matched { color: var(--green); background: rgba(34,197,94,0.08); } .node-missing { color: var(--red); background: rgba(239,68,68,0.1); } .node-extra { color: var(--purple); background: rgba(168,85,247,0.1); } .node-different { color: var(--cyan); background: rgba(6,182,212,0.08); } .node-label { color: var(--text-muted); font-size: 10px; margin-left: 6px; } .class-matched { color: var(--green); } .class-extra { color: var(--purple); font-weight: bold; } .class-missing { color: var(--red); font-weight: bold; text-decoration: line-through; } /* Side-by-side */ .side-by-side { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; } .side-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; overflow-x: auto; } .side-panel h2.actual-title { color: var(--purple); } .side-panel h2.expected-title { color: var(--cyan); } /* DOM reconstruction */ .dom-tree { font-size: 12px; overflow-x: auto; white-space: nowrap; } .dom-tree ul { list-style: none; padding-left: 20px; } .dom-tree li { padding: 2px 0; } .dom-tag { color: var(--gray); } .dom-classes { padding: 2px 6px; border-radius: 3px; margin: 0 2px; } .dom-classes.matched-bg { background: rgba(34,197,94,0.1); color: var(--green); } .dom-classes.extra-bg { background: rgba(168,85,247,0.15); color: var(--purple); border: 1px dashed var(--purple); } .dom-classes.missing-bg { background: rgba(239,68,68,0.15); color: var(--red); border: 1px dashed var(--red); } .dom-classes.different-bg { background: rgba(6,182,212,0.1); color: var(--cyan); border: 1px solid var(--cyan); } /* Full DOM Tree Visualization */ .full-dom-section h2 { border-bottom: none; margin-bottom: 8px; } .full-dom-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 12px; } .full-dom-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; min-width: 0; } .full-dom-panel h3 { font-size: 13px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); } .full-dom-panel h3.expected-title { color: var(--cyan); } .full-dom-panel h3.actual-title { color: var(--purple); } .full-dom-code { margin: 0; padding: 12px; background: var(--bg-code); border: 1px solid var(--border); border-radius: 6px; font-size: 12px; line-height: 1.7; overflow-x: auto; overflow-y: auto; white-space: pre; max-height: 700px; } .ft-matched { color: var(--text-muted); } .ft-different { color: var(--cyan); } .ft-extra { color: var(--purple); } .ft-missing { color: var(--red); } .ft-tag { font-weight: bold; } .ft-attr-name { opacity: 0.7; } .ft-attr-val { opacity: 0.6; } .ft-cls-match { color: var(--text-muted); } .ft-cls-extra { color: var(--purple); font-weight: bold; text-decoration: underline; } .ft-cls-missing { color: var(--red); font-weight: bold; text-decoration: line-through; } .ft-legend { font-size: 11px; color: var(--text-muted); margin-top: 8px; display: flex; gap: 16px; flex-wrap: wrap; } .ft-legend span { display: inline-flex; align-items: center; gap: 4px; } .ft-legend .swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; } .ft-legend .swatch-match { background: var(--text-muted); } .ft-legend .swatch-diff { background: var(--cyan); } .ft-legend .swatch-extra { background: var(--purple); } .ft-legend .swatch-missing { background: var(--red); } /* A11y attribute highlighting in Full DOM Tree */ .ft-a11y-match .ft-attr-name, .ft-a11y-match .ft-attr-val { color: var(--green); opacity: 1; } .ft-a11y-missing { text-decoration: line-through; } .ft-a11y-missing .ft-attr-name, .ft-a11y-missing .ft-attr-val { color: var(--red); opacity: 1; font-weight: bold; } .ft-a11y-extra { text-decoration: underline; } .ft-a11y-extra .ft-attr-name, .ft-a11y-extra .ft-attr-val { color: var(--purple); opacity: 1; font-weight: bold; } .ft-a11y-different .ft-attr-name, .ft-a11y-different .ft-attr-val { color: var(--yellow, #eab308); opacity: 1; font-weight: bold; } .ft-a11y-carried { text-decoration: wavy underline; } .ft-a11y-carried .ft-attr-name, .ft-a11y-carried .ft-attr-val { color: var(--orange); opacity: 1; font-weight: bold; } .ft-legend .swatch-a11y-match { background: var(--green); } .ft-legend .swatch-a11y-diff { background: var(--yellow, #eab308); } .ft-legend .swatch-a11y-carried { background: var(--orange); } /* Suggestions */ .suggestions h2 { color: var(--cyan); } .code-block { background: var(--bg-code); border: 1px solid var(--border); border-radius: 6px; padding: 16px; font-size: 12px; overflow-x: auto; white-space: pre; line-height: 1.6; } .code-missing { color: var(--red); } .code-extra { color: var(--purple); } /* A11y section */ .a11y-section-header { color: var(--cyan); font-size: 16px; margin-top: 32px; margin-bottom: 16px; padding-top: 16px; border-top: 2px solid var(--border); } .a11y-summary { display: flex; gap: 16px; margin-bottom: 20px; } .a11y-summary .stat { min-width: 100px; } .stat.different .num { color: var(--yellow, #eab308); } .a11y-attrs { font-size: 11px; margin-left: 6px; } .a11y-matched { color: var(--green); } .a11y-missing { color: var(--red); font-weight: bold; } .a11y-extra { color: var(--purple); font-weight: bold; } .a11y-different { color: var(--yellow, #eab308); font-weight: bold; } .a11y-carried { color: var(--orange); font-weight: bold; } .a11y-list { list-style: none; padding: 0; } .a11y-list li { padding: 4px 8px; margin: 2px 0; border-radius: 4px; font-size: 12px; } .a11y-missing-item { color: var(--red); background: rgba(239,68,68,0.08); } .a11y-extra-item { color: var(--purple); background: rgba(168,85,247,0.08); } .a11y-different-item { color: var(--yellow, #eab308); background: rgba(234,179,8,0.08); } .a11y-carried-item { color: var(--orange); background: rgba(249,115,22,0.08); } .a11y-ignored-item { color: var(--gray); background: rgba(107,114,128,0.08); text-decoration: line-through; } .a11y-ignored-node { opacity: 0.45; text-decoration: line-through; } .a11y-config-banner { background: rgba(107,114,128,0.08); border: 1px solid var(--gray); border-radius: 8px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: var(--gray); } .a11y-config-banner h3 { color: #e5e7eb; margin: 0 0 6px; font-size: 13px; } .a11y-config-banner ul { margin: 0; padding-left: 18px; list-style: disc; } .a11y-config-banner li { margin-bottom: 2px; } .a11y-config-banner code { background: rgba(255,255,255,0.06); padding: 1px 5px; border-radius: 3px; font-size: 12px; } .a11y-all-good { background: #064e3b; border: 1px solid var(--green); color: var(--green); padding: 12px; border-radius: 8px; text-align: center; font-size: 14px; font-weight: bold; margin-bottom: 20px; } @media (max-width: 900px) { .side-by-side { grid-template-columns: 1fr; } .full-dom-grid { grid-template-columns: 1fr; } }`; } // ============= A11y Report Section ============= function buildA11yReportSection(result, fullDiff, reportOptions) { var _a, _b, _c, _d, _e, _f; const a11y = result.a11yResult; if (!a11y) return ''; const passedCount = a11y.passed.length; const missingCount = a11y.missing.length; const extraCount = a11y.extra.length; const differentCount = a11y.different.length; const carriedCount = a11y.carried.length; const ignoredMissingCount = (_b = (_a = a11y.ignoredClassless) === null || _a === void 0 ? void 0 : _a.missing.length) !== null && _b !== void 0 ? _b : 0; const ignoredExtraCount = (_d = (_c = a11y.ignoredClassless) === null || _c === void 0 ? void 0 : _c.extra.length) !== null && _d !== void 0 ? _d : 0; const totalIgnored = ignoredMissingCount + ignoredExtraCount; const allA11yGood = missingCount === 0 && extraCount === 0 && differentCount === 0; // Build carried lookup for Full DOM View highlighting const carriedSet = new Set(); if (carriedCount > 0) { for (const c of a11y.carried) { carriedSet.add(`${c.selector}:${c.attribute.name}`); } } const a11yTreeDiffHtml = a11y.treeDiff && a11y.treeDiff.length > 0 ? (0, a11y_comparer_1.renderA11yHtmlDiffTree)(a11y.treeDiff, reportOptions === null || reportOptions === void 0 ? void 0 : reportOptions.ignoreClasslessNodes) : ''; const a11yMissingSection = missingCount > 0 ? ` <div class="section"> <h2 class="missing-title">MISSING A11Y ATTRIBUTES <span class="subtitle">(expected but not found in actual)</span></h2> <ul class="a11y-list"> ${a11y.missing.map(m => `<li class="a11y-missing-item">&minus; <strong>${(0, helpers_1.escHtml)(m.selector)}</strong> &nbsp; ${(0, helpers_1.escHtml)(m.attribute.name)}="${(0, helpers_1.escHtml)(m.attribute.value)}"</li>`).join('\n ')} </ul> </div>` : ''; const a11yExtraSection = extraCount > 0 ? ` <div class="section"> <h2 class="extra-title">EXTRA A11Y ATTRIBUTES <span class="subtitle">(in actual but not in expected)</span></h2> <ul class="a11y-list"> ${a11y.extra.map(e => `<li class="a11y-extra-item">+ <strong>${(0, helpers_1.escHtml)(e.selector)}</strong> &nbsp; ${(0, helpers_1.escHtml)(e.attribute.name)}="${(0, helpers_1.escHtml)(e.attribute.value)}"</li>`).join('\n ')} </ul> </div>` : ''; const a11yDifferentSection = differentCount > 0 ? ` <div class="section"> <h2 style="color: var(--yellow, #eab308);">DIFFERENT A11Y VALUES <span class="subtitle">(same attribute, different value)</span></h2> <ul class="a11y-list"> ${a11y.different.map(d => `<li class="a11y-different-item">~ <strong>${(0, helpers_1.escHtml)(d.selector)}</strong> &nbsp; ${(0, helpers_1.escHtml)(d.name)}: "${(0, helpers_1.escHtml)(d.actualValue)}" &rarr; "${(0, helpers_1.escHtml)(d.expectedValue)}"</li>`).join('\n ')} </ul> </div>` : ''; const a11yCarriedSection = carriedCount > 0 ? ` <div class="section"> <h2 style="color: var(--orange);">CARRIED A11Y ATTRIBUTES <span class="subtitle">(inherited from classless ancestor &mdash; review placement)</span></h2> <ul class="a11y-list"> ${a11y.carried.map(c => `<li class="a11y-carried-item">&#9888; <strong>${(0, helpers_1.escHtml)(c.selector)}</strong> &nbsp; ${(0, helpers_1.escHtml)(c.attribute.name)}="${(0, helpers_1.escHtml)(c.attribute.value)}"</li>`).join('\n ')} </ul> </div>` : ''; // Build config banner showing active test options const configItems = []; if ((_e = reportOptions === null || reportOptions === void 0 ? void 0 : reportOptions.allowMissing) === null || _e === void 0 ? void 0 : _e.length) { configItems.push(`<strong>allowMissing:</strong> ${reportOptions.allowMissing.map(s => `<code>${(0, helpers_1.escHtml)(s)}</code>`).join(', ')}`); } if ((_f = reportOptions === null || reportOptions === void 0 ? void 0 : reportOptions.allowExtra) === null || _f === void 0 ? void 0 : _f.length) { configItems.push(`<strong>allowExtra:</strong> ${reportOptions.allowExtra.map(s => `<code>${(0, helpers_1.escHtml)(s)}</code>`).join(', ')}`); } configItems.push(`<strong>ignoreClasslessNodes:</strong> ${(reportOptions === null || reportOptions === void 0 ? void 0 : reportOptions.ignoreClasslessNodes) ? '&#10003; enabled' : '&#10007; disabled'}`); const configBanner = ` <div class="a11y-config-banner"> <h3>Test Configuration</h3> <ul>${configItems.map(item => `<li>${item}</li>`).join('')}</ul> </div>`; // Build ignored classless section const ignoredClasslessSection = totalIgnored > 0 ? ` <div class="section"> <h2 style="color: var(--gray);">IGNORED CLASSLESS NODES <span class="subtitle">(filtered by ignoreClasslessNodes)</span></h2> <ul class="a11y-list"> ${a11y.ignoredClassless.missing.map(m => `<li class="a11y-ignored-item">&minus; <strong>${(0, helpers_1.escHtml)(m.selector)}</strong> &nbsp; ${(0, helpers_1.escHtml)(m.attribute.name)}="${(0, helpers_1.escHtml)(m.attribute.value)}" <span class="subtitle">(missing)</span></li>`).join('\n ')} ${a11y.ignoredClassless.extra.map(e => `<li class="a11y-ignored-item">+ <strong>${(0, helpers_1.escHtml)(e.selector)}</strong> &nbsp; ${(0, helpers_1.escHtml)(e.attribute.name)}="${(0, helpers_1.escHtml)(e.attribute.value)}" <span class="subtitle">(extra)</span></li>`).join('\n ')} </ul> </div>` : ''; return ` <h2 class="a11y-section-header">A11Y Attribute Comparison (aria-* / role)</h2> ${configBanner} <div class="a11y-summary"> <div class="stat passed"><span class="num">${passedCount}</span><span class="label">Nodes Passed</span></div> <div class="stat missing"><span class="num">${missingCount}</span><span class="label">Missing</span></div> <div class="stat extra"><span class="num">${extraCount}</span><span class="label">Extra</span></div> <div class="stat different"><span class="num">${differentCount}</span><span class="label">Different</span></div> ${carriedCount > 0 ? `<div class="stat" style="border-color: var(--orange);"><span class="num" style="color: var(--orange);">${carriedCount}</span><span class="label">Carried</span></div>` : ''} ${totalIgnored > 0 ? `<div class="stat" style="border-color: var(--gray);"><span class="num" style="color: var(--gray);">${totalIgnored}</span><span class="label">Ignored</span></div>` : ''} </div> ${allA11yGood && carriedCount === 0 ? '<div class="a11y-all-good">&#10003; All a11y attributes matched!</div>' : ` ${allA11yGood && carriedCount > 0 ? '<div class="a11y-all-good">&#10003; All a11y attributes matched!</div>' : ''} ${a11yTreeDiffHtml ? ` <div class="section"> <h2 style="color: var(--cyan);">A11y Tree Diff <span class="subtitle">(&#10003;=match &nbsp; ~=different &nbsp; +=extra &nbsp; &minus;=missing &nbsp; &#9888;=carried)</span></h2> <div class="tree">${a11yTreeDiffHtml}</div> </div>` : ''} ${buildA11yFullDomSection(fullDiff, carriedSet)} ${a11yMissingSection} ${a11yExtraSection} ${a11yDifferentSection} ${a11yCarriedSection} `} ${ignoredClasslessSection} `; } function buildA11yFullDomSection(fullDiff, carriedSet) { if (!fullDiff || fullDiff.length === 0) return ''; // Only show the section if there are any a11y attributes in the tree const hasAnyA11y = fullDiff.some(n => nodeTreeHasA11y(n)); if (!hasAnyA11y) return ''; const expectedA11yCode = renderFullTreeCodeA11y(fullDiff, 'expected', 0, carriedSet); const actualA11yCode = renderFullTreeCodeA11y(fullDiff, 'actual', 0, carriedSet); return ` <div class="section full-dom-section"> <h2 style="color: var(--cyan);">A11y Full DOM View <span class="subtitle">(expected left &bull; actual right &mdash; a11y attributes color-coded)</span></h2> <div class="full-dom-grid"> <div class="full-dom-panel"> <h3 class="expected-title">Expected <span class="subtitle">(reference)</span></h3> <pre class="full-dom-code"><code>${expectedA11yCode}</code></pre> </div> <div class="full-dom-panel"> <h3 class="actual-title">Actual <span class="subtitle">(component)</span></h3> <pre class="full-dom-code"><code>${actualA11yCode}</code></pre> </div> </div> <div class="ft-legend"> <span><span class="swatch swatch-a11y-match"></span> Matched attr</span> <span><span class="swatch swatch-a11y-diff"></span> Different value</span> <span><span class="swatch swatch-extra"></span> Extra (actual only)</span> <span><span class="swatch swatch-missing"></span> Missing (expected only)</span> <span><span class="swatch swatch-a11y-carried"></span> Carried (from classless ancestor)</span> </div> </div>`; } function nodeTreeHasA11y(node) { const check = (n) => { var _a; return (_a = n === null || n === void 0 ? void 0 : n.attributes.some(a => isA11yAttrName(a.name))) !== null && _a !== void 0 ? _a : false; }; if (check(node.actualNode) || check(node.expectedNode)) return true; return node.children.some(c => nodeTreeHasA11y(c)); } // ============= Main Report Builder ============= function buildHtmlReportContent(result, options) { var _a, _b, _c, _d, _e, _f; const now = new Date(); const passedCount = result.passed.length; const missingCount = result.missing.length; const extraCount = result.extra.length; const allGood = missingCount === 0 && extraCount === 0; const treeDiffHtml = result.treeDiff && result.treeDiff.length > 0 ? renderHtmlDiffTree(result.treeDiff) : '<p class="muted">No tree diff available.</p>'; const actualTreeHtml = result.treeDiff && result.treeDiff.length > 0 ? renderSideTree(result.treeDiff, 'actual') : '<p class="muted">N/A</p>'; const expectedTreeHtml = result.treeDiff && result.treeDiff.length > 0 ? renderSideTree(result.treeDiff, 'expected') : '<p class="muted">N/A</p>'; // Full DOM tree comparison (when raw HTML is available) let domComparisonHtml = ''; let fullDiff; if (options.actualHtml && options.expectedHtml) { try { const actualFullTree = buildFullDomTree(options.actualHtml); const expectedFullTree = buildFullDomTree(options.expectedHtml); fullDiff = diffFullTrees(actualFullTree, expectedFullTree); // Post-process full diff to hide allowed diffs if (((_a = options.allowMissing) === null || _a === void 0 ? void 0 : _a.length) || ((_b = options.allowExtra) === null || _b === void 0 ? void 0 : _b.length)) { const allowMissingClasses = (0, helpers_1.extractAllowedClasses)((_c = options.allowMissing) !== null && _c !== void 0 ? _c : []); const allowExtraClasses = (0, helpers_1.extractAllowedClasses)((_d = options.allowExtra) !== null && _d !== void 0 ? _d : []); (0, helpers_1.applyAllowsToDiff)(fullDiff, allowMissingClasses, allowExtraClasses); } const expectedFullCode = renderFullTreeCode(fullDiff, 'expected'); const actualFullCode = renderFullTreeCode(fullDiff, 'actual'); domComparisonHtml = ` <div class="section full-dom-section"> <h2>Full DOM Tree <span class="subtitle">(expected left &bull; actual right &mdash; scroll horizontally for wide components)</span></h2> <div class="full-dom-grid"> <div class="full-dom-panel"> <h3 class="expected-title">Expected <span class="subtitle">(reference)</span></h3> <pre class="full-dom-code"><code>${expectedFullCode}</code></pre> </div> <div class="full-dom-panel"> <h3 class="actual-title">Actual <span class="subtitle">(component)</span></h3> <pre class="full-dom-code"><code>${actualFullCode}</code></pre> </div> </div> <div class="ft-legend"> <span><span class="swatch swatch-match"></span> Matched</span> <span><span class="swatch swatch-diff"></span> Different classes</span> <span><span class="swatch swatch-extra"></span> Extra (actual only)</span> <span><span class="swatch swatch-missing"></span> Missing (expected only)</span> </div> </div>`; } catch (_g) { // Full DOM visualization is supplementary } } // Fallback to old class-only side-by-side if (!domComparisonHtml) { domComparisonHtml = ` <div class="side-by-side"> <div class="side-panel"> <h2 class="expected-title">Expected DOM <span class="subtitle">(reference spec)</span></h2> <div class="dom-tree">${expectedTreeHtml}</div> </div> <div class="side-panel"> <h2 class="actual-title">Actual DOM <span class="subtitle">(your component)</span></h2> <div class="dom-tree">${actualTreeHtml}</div> </div> </div>`; } const missingSection = missingCount > 0 ? ` <div class="section"> <h2 class="missing-title">MISSING <span class="subtitle">(expected but not found in actual)</span></h2> <ul class="selector-list"> ${result.missing.map(s => `<li class="missing-item">&minus; ${(0, helpers_1.escHtml)(s)}</li>`).join('\n ')} </ul> </div>` : ''; const extraSection = extraCount > 0 ? ` <div class="section"> <h2 class="extra-title">EXTRA <span class="subtitle">(in actual but not found in expected)</span></h2> <ul class="selector-list"> ${result.extra.map(s => `<li class="extra-item">+ ${(0, helpers_1.escHtml)(s)}</li>`).join('\n ')} </ul> </div>` : ''; const suggestionsSection = (((_e = result.suggestedAllowMissing) === null || _e === void 0 ? void 0 : _e.length) || ((_f = result.suggestedAllowExtra) === null || _f === void 0 ? void 0 : _f.length)) ? ` <div class="section suggestions"> <h2>Suggested Options <span class="subtitle">(copy-paste into your test)</span></h2> <pre class="code-block">${generateSuggestionsCode(result)}</pre> </div>` : ''; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Rendering Report: ${(0, helpers_1.escHtml)(options.testName)}</title> <style>${getReportStyles()}</style> </head> <body> <div class="header"> <h1>HTML Class Hierarchy Diff</h1> <div class="meta"> <strong>Spec:</strong> ${(0, helpers_1.escHtml)(options.specFileName)} &nbsp;|&nbsp; <strong>Test:</strong> ${(0, helpers_1.escHtml)(options.testName)} &nbsp;|&nbsp; <strong>Generated:</strong> ${now.toISOString()} </div> </div> <div class="summary"> <div class="stat passed"><span class="num">${passedCount}</span><span class="label">Passed</span></div> <div class="stat missing"><span class="num">${missingCount}</span><span class="label">Missing</span></div> <div class="stat extra"><span class="num">${extraCount}</span><span class="label">Extra</span></div> </div> ${allGood ? `<div class="all-good">&#10003; All selectors matched!</div> ${domComparisonHtml}` : ` <div class="section"> <h2>Tree Diff <span class="subtitle">(&#10003;=match &nbsp; ~=different &nbsp; +=extra &nbsp; &minus;=missing)</span></h2> <div class="tree">${treeDiffHtml}</div> </div> ${domComparisonHtml} ${missingSection} ${extraSection} ${suggestionsSection} `} ${buildA11yReportSection(result, fullDiff, options)} ${buildIdRefReportSection(result)} </body> </html>`; } function buildIdRefReportSection(result) { const idRef = result.idRefResult; if (!idRef) return ''; const validCount = idRef.valid.length; const brokenCount = idRef.invalid.length; const allGood = brokenCount === 0; const brokenSection = brokenCount > 0 ? ` <div class="section"> <h2 class="missing-title">BROKEN ID REFERENCES <span class="subtitle">(attribute points to non-existent ID)</span></h2> <ul class="a11y-list"> ${idRef.invalid.map(issue => `<li class="a11y-missing-item">&#10007; <strong>${(0, helpers_1.escHtml)(issue.selector)}</strong> &nbsp; ${(0, helpers_1.escHtml)(issue.attribute)}="${(0, helpers_1.escHtml)(issue.missingId)}" &rarr; ID not found</li>`).join('\n ')} </ul> </div>` : ''; return ` <h2 class="a11y-section-header">A11Y ID Reference Validation</h2> <div class="a11y-summary"> <div class="stat passed"><span class="num">${validCount}</span><span class="label">Valid Refs</span></div> <div class="stat missing"><span class="num">${brokenCount}</span><span class="label">Broken Refs</span></div> </div> ${allGood ? '<div class="a11y-all-good">&#10003; All ID references resolve to existing elements!</div>' : brokenSection} `; } //# sourceMappingURL=html-report.js.map