@progress/kendo-e2e
Version:
Kendo UI end-to-end test utilities.
1,055 lines (1,030 loc) • 45.6 kB
JavaScript
"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}<<span class="ft-tag">${tag}</span>${attrs}></span>`);
lines.push(renderFullTreeCode(node.children, side, depth + 1));
lines.push(`<span class="${cssClass}">${pad}</<span class="ft-tag">${tag}</span>></span>`);
}
else {
lines.push(`<span class="${cssClass}">${pad}<<span class="ft-tag">${tag}</span>${attrs} /></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}<<span class="ft-tag">${tag}</span>${attrs}></span>`);
lines.push(renderFullTreeCodeA11y(node.children, side, depth + 1, carriedSet));
lines.push(`<span class="${cssClass}">${pad}</<span class="ft-tag">${tag}</span>></span>`);
}
else {
lines.push(`<span class="${cssClass}">${pad}<<span class="ft-tag">${tag}</span>${attrs} /></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}">✓ ${(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}">− ${(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">−[.${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"><div</span> <span class="dom-classes ${bgClass}">class="${(0, helpers_1.escHtml)(classStr)}"</span><span class="dom-tag">></span>`
: `<span class="dom-tag"><div></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">− <strong>${(0, helpers_1.escHtml)(m.selector)}</strong> ${(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> ${(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> ${(0, helpers_1.escHtml)(d.name)}: "${(0, helpers_1.escHtml)(d.actualValue)}" → "${(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 — review placement)</span></h2>
<ul class="a11y-list">
${a11y.carried.map(c => `<li class="a11y-carried-item">⚠ <strong>${(0, helpers_1.escHtml)(c.selector)}</strong> ${(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) ? '✓ enabled' : '✗ 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">− <strong>${(0, helpers_1.escHtml)(m.selector)}</strong> ${(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> ${(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">✓ All a11y attributes matched!</div>' : `
${allA11yGood && carriedCount > 0 ? '<div class="a11y-all-good">✓ All a11y attributes matched!</div>' : ''}
${a11yTreeDiffHtml ? `
<div class="section">
<h2 style="color: var(--cyan);">A11y Tree Diff <span class="subtitle">(✓=match ~=different +=extra −=missing ⚠=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 • actual right — 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 • actual right — 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">− ${(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 `
<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)} |
<strong>Test:</strong> ${(0, helpers_1.escHtml)(options.testName)} |
<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">✓ All selectors matched!</div>
${domComparisonHtml}` : `
<div class="section">
<h2>Tree Diff <span class="subtitle">(✓=match ~=different +=extra −=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">✗ <strong>${(0, helpers_1.escHtml)(issue.selector)}</strong> ${(0, helpers_1.escHtml)(issue.attribute)}="${(0, helpers_1.escHtml)(issue.missingId)}" → 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">✓ All ID references resolve to existing elements!</div>' : brokenSection}
`;
}
//# sourceMappingURL=html-report.js.map