@progress/kendo-e2e
Version:
Kendo UI end-to-end test utilities.
677 lines • 31 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_PRESENCE_ONLY_ATTRIBUTES = void 0;
exports.buildA11yDomTree = buildA11yDomTree;
exports.sanitizeA11yDomTreeNG = sanitizeA11yDomTreeNG;
exports.diffA11yTrees = diffA11yTrees;
exports.compareA11yAttributes = compareA11yAttributes;
exports.formatA11yDiff = formatA11yDiff;
exports.renderA11yHtmlDiffTree = renderA11yHtmlDiffTree;
const sanitize_html_1 = __importDefault(require("sanitize-html"));
const parse_html_1 = require("./parse-html");
const helpers_1 = require("./helpers");
// ============= A11y Attribute Helpers =============
/** Check if an attribute name is an a11y attribute (aria-* or role) */
function isA11yAttribute(name) {
return name.startsWith('aria-') || name === 'role';
}
/** Extract a11y attributes from a DOM Element */
function extractA11yAttributes(element) {
const attrs = [];
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
if (isA11yAttribute(attr.name)) {
attrs.push({ name: attr.name, value: attr.value });
}
}
return attrs.sort((a, b) => a.name.localeCompare(b.name));
}
// ============= DOM Tree Building =============
/**
* Build a DOM tree that preserves both class selectors and a11y attributes.
* Nodes without classes are "transparent" – their children are promoted,
* but their a11y attributes are carried to the first class-bearing ancestor.
*
* When `collapseClasses` is provided, nodes whose classes are **entirely**
* covered by one of these selectors are also treated as transparent wrappers
* (children promoted, a11y attributes carried down). This automatically
* collapses framework-specific wrappers like `.k-icon-wrapper-host`.
*/
function buildA11yDomTree(element, collapseClasses) {
const nodes = [];
let child = element.firstElementChild;
while (child) {
const classAttr = child.getAttribute('class') || '';
const rawValue = (0, helpers_1.removeDuplicatedSpaces)(classAttr);
const escapedValue = rawValue.replace(/!/g, '\\!');
const classes = escapedValue.split(' ').filter(Boolean).sort();
const a11yAttrs = extractA11yAttributes(child);
// Treat node as transparent if classless OR all classes are in the collapse set
const isCollapsed = classes.length > 0
&& collapseClasses !== undefined
&& classes.every(c => collapseClasses.has(c));
if (classes.length > 0 && !isCollapsed) {
const childNodes = buildA11yDomTree(child, collapseClasses);
nodes.push({
classes,
classSelector: '.' + classes.join('.'),
a11yAttributes: a11yAttrs,
children: childNodes,
});
}
else {
// Transparent node: promote children, merge a11y attributes into them
const promoted = buildA11yDomTree(child, collapseClasses);
if (a11yAttrs.length > 0 && promoted.length > 0) {
// Attach the parent's a11y attributes to the first promoted child
promoted[0].a11yAttributes = mergeA11yAttributes(a11yAttrs, promoted[0].a11yAttributes);
// Track which attributes were carried from the classless ancestor
const carried = new Set(promoted[0].carriedAttrNames || []);
for (const attr of a11yAttrs) {
carried.add(attr.name);
}
promoted[0].carriedAttrNames = carried;
}
else if (a11yAttrs.length > 0 && promoted.length === 0) {
// Classless leaf with a11y attrs – create a synthetic node
nodes.push({
classes: [],
classSelector: '(classless)',
a11yAttributes: a11yAttrs,
children: [],
});
}
nodes.push(...promoted);
}
child = child.nextElementSibling;
}
return nodes;
}
/** Merge two sorted a11y attribute arrays, preferring the first array for duplicates */
function mergeA11yAttributes(primary, secondary) {
const map = new Map();
for (const attr of secondary) {
map.set(attr.name, attr.value);
}
for (const attr of primary) {
map.set(attr.name, attr.value);
}
return Array.from(map.entries())
.map(([name, value]) => ({ name, value }))
.sort((a, b) => a.name.localeCompare(b.name));
}
/** Sanitize Angular-specific selectors from a11y DOM tree */
function sanitizeA11yDomTreeNG(nodes) {
return nodes.map(node => {
const filteredClasses = node.classes.filter(c => !c.startsWith('ng-'));
if (filteredClasses.length === 0) {
// Promote children, pass a11y attributes down
const promoted = sanitizeA11yDomTreeNG(node.children);
if (node.a11yAttributes.length > 0 && promoted.length > 0) {
promoted[0].a11yAttributes = mergeA11yAttributes(node.a11yAttributes, promoted[0].a11yAttributes);
// Track carried attrs from NG-sanitized classless node
const carried = new Set(promoted[0].carriedAttrNames || []);
for (const attr of node.a11yAttributes) {
carried.add(attr.name);
}
promoted[0].carriedAttrNames = carried;
}
return promoted;
}
return [{
classes: filteredClasses,
classSelector: '.' + filteredClasses.join('.'),
a11yAttributes: node.a11yAttributes,
children: sanitizeA11yDomTreeNG(node.children),
carriedAttrNames: node.carriedAttrNames,
}];
}).flat();
}
// ============= Tree Diff Engine =============
function jaccardSimilarity(a, b) {
if (a.length === 0 && b.length === 0)
return 1;
const setA = new Set(a);
let intersection = 0;
for (const x of b) {
if (setA.has(x))
intersection++;
}
const union = new Set([...a, ...b]).size;
return union === 0 ? 0 : intersection / union;
}
/**
* Diff two a11y DOM trees, matching nodes by class similarity
* and comparing their a11y attributes.
*/
function diffA11yTrees(actual, expected, depth = 0, presenceOnlyAttrs) {
const results = [];
const usedExpected = new Set();
for (const aNode of actual) {
let bestMatch = -1;
let bestScore = -1;
let isExact = false;
// First try exact class match
for (let ei = 0; ei < expected.length; ei++) {
if (usedExpected.has(ei))
continue;
if (aNode.classSelector === expected[ei].classSelector) {
bestMatch = ei;
isExact = true;
break;
}
}
// If no exact match, try similarity
if (!isExact) {
for (let ei = 0; ei < expected.length; ei++) {
if (usedExpected.has(ei))
continue;
const score = jaccardSimilarity(aNode.classes, expected[ei].classes);
if (score > bestScore && score >= 0.3) {
bestScore = score;
bestMatch = ei;
}
}
}
if (bestMatch >= 0) {
usedExpected.add(bestMatch);
const eNode = expected[bestMatch];
const attrDiff = diffA11yAttributes(aNode.a11yAttributes, eNode.a11yAttributes, presenceOnlyAttrs);
// Collect carried attribute names from both actual and expected nodes
const carriedNames = new Set();
if (aNode.carriedAttrNames) {
for (const name of aNode.carriedAttrNames)
carriedNames.add(name);
}
if (eNode.carriedAttrNames) {
for (const name of eNode.carriedAttrNames)
carriedNames.add(name);
}
const carriedAttributes = [];
if (carriedNames.size > 0) {
for (const name of carriedNames) {
const eAttr = eNode.a11yAttributes.find(a => a.name === name);
const aAttr = aNode.a11yAttributes.find(a => a.name === name);
const attr = eAttr || aAttr;
if (attr)
carriedAttributes.push(attr);
}
}
const isAttrMatch = attrDiff.missing.length === 0 &&
attrDiff.extra.length === 0 &&
attrDiff.different.length === 0;
results.push({
classSelector: aNode.classSelector,
depth,
matchedAttributes: attrDiff.matched,
missingAttributes: attrDiff.missing,
extraAttributes: attrDiff.extra,
differentAttributes: attrDiff.different,
carriedAttributes: carriedAttributes.length > 0 ? carriedAttributes : undefined,
status: isAttrMatch ? 'matched' : 'different',
children: diffA11yTrees(aNode.children, eNode.children, depth + 1, presenceOnlyAttrs),
});
}
else {
// Actual node with no matching expected node → extra
results.push(markA11ySubtree(aNode, 'extra', depth));
}
}
// Unmatched expected nodes → missing
for (let ei = 0; ei < expected.length; ei++) {
if (!usedExpected.has(ei)) {
results.push(markA11ySubtree(expected[ei], 'missing', depth));
}
}
return results;
}
function diffA11yAttributes(actual, expected, presenceOnlyAttrs) {
const matched = [];
const missing = [];
const extra = [];
const different = [];
const expectedMap = new Map(expected.map(a => [a.name, a.value]));
const actualMap = new Map(actual.map(a => [a.name, a.value]));
// Check expected attributes against actual
for (const [name, expectedValue] of expectedMap) {
if (actualMap.has(name)) {
const actualValue = actualMap.get(name);
if (actualValue === expectedValue || (presenceOnlyAttrs === null || presenceOnlyAttrs === void 0 ? void 0 : presenceOnlyAttrs.has(name))) {
// Presence-only: treat as matched if both sides have the attribute
matched.push({ name, value: actualValue });
}
else {
different.push({ name, actualValue, expectedValue });
}
}
else {
missing.push({ name, value: expectedValue });
}
}
// Check for extra attributes in actual
for (const [name, value] of actualMap) {
if (!expectedMap.has(name)) {
extra.push({ name, value });
}
}
return { matched, missing, extra, different };
}
function markA11ySubtree(node, status, depth) {
var _a;
const carriedAttributes = [];
if ((_a = node.carriedAttrNames) === null || _a === void 0 ? void 0 : _a.size) {
for (const name of node.carriedAttrNames) {
const attr = node.a11yAttributes.find(a => a.name === name);
if (attr)
carriedAttributes.push(attr);
}
}
return {
classSelector: node.classSelector,
depth,
matchedAttributes: [],
missingAttributes: status === 'missing' ? [...node.a11yAttributes] : [],
extraAttributes: status === 'extra' ? [...node.a11yAttributes] : [],
differentAttributes: [],
carriedAttributes: carriedAttributes.length > 0 ? carriedAttributes : undefined,
status,
children: node.children.map(child => markA11ySubtree(child, status, depth + 1)),
};
}
/**
* Default attributes that are compared by presence only (value ignored).
* These attributes typically contain dynamic IDs or locale-specific text
* that differ between actual and expected HTML.
*/
exports.DEFAULT_PRESENCE_ONLY_ATTRIBUTES = [
'aria-label',
'title',
'aria-controls',
'aria-labelledby',
'aria-owns',
'aria-describedby',
];
/**
* Build the set of attribute names to compare by presence only.
* - `undefined` → use the default list
* - `[]` → disable (compare all values exactly)
* - `[...names]` → use exactly these names
*/
function buildPresenceOnlySet(custom) {
if (custom !== undefined) {
return custom.length > 0 ? new Set(custom) : undefined;
}
return new Set(exports.DEFAULT_PRESENCE_ONLY_ATTRIBUTES);
}
/**
* Check whether an attribute on a given node is covered by an allow list.
*
* A selector key from the allow list matches a node when **every class** in
* the key is present as a class in the node's selector. For example the key
* `".k-button"` matches nodes `.k-button`, `.k-button.k-button-md`, etc.
*/
function isAllowedForNode(nodeSelector, attrName, allowList) {
// Parse the set of classes in the node selector (e.g. ".k-button.k-button-md" → ["k-button", "k-button-md"])
const nodeClasses = new Set(nodeSelector.split('.').filter(Boolean));
for (const entry of allowList) {
for (const [keySelector, allowedAttrs] of Object.entries(entry)) {
// Parse key selector classes (e.g. ".k-button" → ["k-button"])
const keyClasses = keySelector.split('.').filter(Boolean);
// Every class in the key must be present in the node
const matches = keyClasses.length > 0 && keyClasses.every(cls => nodeClasses.has(cls));
if (matches && allowedAttrs.includes(attrName)) {
return true;
}
}
}
return false;
}
/**
* Compare a11y attributes (aria-* and role) between two HTML documents.
*
* Nodes are matched by their CSS class hierarchy. For each matched node pair,
* `aria-*` and `role` attributes are compared between actual and expected.
*
* @example
* const result = compareA11yAttributes(
* '<button class="k-button" aria-disabled="true" role="button">OK</button>',
* '<button class="k-button" aria-disabled="false" role="button" aria-label="Confirm">OK</button>'
* );
* // result.missing → aria-label on .k-button
* // result.different → aria-disabled has different values
*
* @param actualHtml The actual HTML markup.
* @param expectedHtml The expected HTML markup.
* @param options Comparison options.
*/
function compareA11yAttributes(actualHtml, expectedHtml, options) {
var _a, _b;
const result = {
passed: [],
missing: [],
extra: [],
different: [],
carried: [],
};
const config = {
allowedTags: false,
allowVulnerableTags: true,
allowedAttributes: { "*": ["class", "role", "aria-*"] },
};
const actualDom = (0, parse_html_1.parseHtml)((0, sanitize_html_1.default)(actualHtml, config)).documentElement;
const expectedDom = (0, parse_html_1.parseHtml)((0, sanitize_html_1.default)(expectedHtml, config)).documentElement;
// Build collapse sets from collapseExtraClasses / collapseMissingClasses (bare class names)
const collapseExtraSet = ((_a = options === null || options === void 0 ? void 0 : options.collapseExtraClasses) === null || _a === void 0 ? void 0 : _a.length)
? new Set(options.collapseExtraClasses.flatMap(s => s.split('.').filter(Boolean)))
: undefined;
const collapseMissingSet = ((_b = options === null || options === void 0 ? void 0 : options.collapseMissingClasses) === null || _b === void 0 ? void 0 : _b.length)
? new Set(options.collapseMissingClasses.flatMap(s => s.split('.').filter(Boolean)))
: undefined;
let actualTreeNodes = buildA11yDomTree(actualDom, collapseExtraSet);
const expectedTreeNodes = buildA11yDomTree(expectedDom, collapseMissingSet);
if (options === null || options === void 0 ? void 0 : options.sanitizeNGSelectors) {
actualTreeNodes = sanitizeA11yDomTreeNG(actualTreeNodes);
}
// Build the set of attributes to compare by presence only (ignore value)
const presenceOnlyAttrs = buildPresenceOnlySet(options === null || options === void 0 ? void 0 : options.ignoreValueAttributes);
const treeDiff = diffA11yTrees(actualTreeNodes, expectedTreeNodes, 0, presenceOnlyAttrs);
result.treeDiff = treeDiff;
// Flatten tree diff into result lists
flattenA11yDiff(treeDiff, result);
// Apply per-node allow filters
const allowMissing = options === null || options === void 0 ? void 0 : options.allowMissing;
const allowExtra = options === null || options === void 0 ? void 0 : options.allowExtra;
if (allowMissing === null || allowMissing === void 0 ? void 0 : allowMissing.length) {
result.missing = result.missing.filter(m => !isAllowedForNode(m.selector, m.attribute.name, allowMissing));
result.different = result.different.filter(d => !isAllowedForNode(d.selector, d.name, allowMissing));
}
if (allowExtra === null || allowExtra === void 0 ? void 0 : allowExtra.length) {
result.extra = result.extra.filter(e => !isAllowedForNode(e.selector, e.attribute.name, allowExtra));
}
// Filter out classless nodes when ignoreClasslessNodes is enabled
if (options === null || options === void 0 ? void 0 : options.ignoreClasslessNodes) {
const isClassless = (selector) => selector === '(classless)';
result.ignoredClassless = {
missing: result.missing.filter(m => isClassless(m.selector)),
extra: result.extra.filter(e => isClassless(e.selector)),
};
result.missing = result.missing.filter(m => !isClassless(m.selector));
result.extra = result.extra.filter(e => !isClassless(e.selector));
result.different = result.different.filter(d => !isClassless(d.selector));
result.carried = result.carried.filter(c => !isClassless(c.selector));
}
return result;
}
function flattenA11yDiff(nodes, result) {
var _a;
for (const node of nodes) {
const selector = node.classSelector;
if (node.status === 'matched' && node.matchedAttributes.length > 0) {
result.passed.push({ selector, attributes: node.matchedAttributes });
}
if (node.status === 'different') {
// Report matched attributes
if (node.matchedAttributes.length > 0) {
result.passed.push({ selector, attributes: node.matchedAttributes });
}
// Report missing
for (const attr of node.missingAttributes) {
result.missing.push({ selector, attribute: attr });
}
// Report extra
for (const attr of node.extraAttributes) {
result.extra.push({ selector, attribute: attr });
}
// Report different values
for (const diff of node.differentAttributes) {
result.different.push(Object.assign({ selector }, diff));
}
}
if (node.status === 'missing') {
for (const attr of node.missingAttributes) {
result.missing.push({ selector, attribute: attr });
}
}
if (node.status === 'extra') {
for (const attr of node.extraAttributes) {
result.extra.push({ selector, attribute: attr });
}
}
// Recurse into children
flattenA11yDiff(node.children, result);
// Collect carried attribute warnings (orthogonal to match/miss/extra/different)
if ((_a = node.carriedAttributes) === null || _a === void 0 ? void 0 : _a.length) {
for (const attr of node.carriedAttributes) {
result.carried.push({ selector, attribute: attr });
}
}
}
}
// ============= Formatting =============
const helpers_2 = require("./helpers");
/**
* Format an a11y comparison result as a human-readable diff.
*
* @param result The a11y comparison result.
* @param options Formatting options.
*/
function formatA11yDiff(result, options) {
const c = (options === null || options === void 0 ? void 0 : options.useColors) ? helpers_2.ANSI : helpers_2.NO_ANSI;
const sep = '═'.repeat(62);
const thinSep = '─'.repeat(62);
const lines = [];
lines.push('');
lines.push(`${c.dim}${sep}${c.reset}`);
lines.push(`${c.bold} A11Y ATTRIBUTE DIFF (aria-* / role)${c.reset}`);
lines.push(`${c.dim}${sep}${c.reset}`);
lines.push('');
const passedCount = result.passed.length;
const missingCount = result.missing.length;
const extraCount = result.extra.length;
const differentCount = result.different.length;
const carriedCount = result.carried.length;
lines.push(` ${c.green}✓ ${passedCount} nodes passed${c.reset}` +
(missingCount > 0 ? ` ${c.red}− ${missingCount} missing${c.reset}` : '') +
(extraCount > 0 ? ` ${c.magenta}+ ${extraCount} extra${c.reset}` : '') +
(differentCount > 0 ? ` ${c.yellow}~ ${differentCount} different${c.reset}` : '') +
(carriedCount > 0 ? ` ${c.orange}⚠ ${carriedCount} carried${c.reset}` : ''));
if (missingCount === 0 && extraCount === 0 && differentCount === 0) {
lines.push('');
lines.push(` ${c.green}All a11y attributes matched!${c.reset}`);
// Still show carried warning when present
if (result.carried.length > 0) {
lines.push('');
lines.push(`${c.dim}${thinSep}${c.reset}`);
lines.push(`${c.bold}${c.orange} CARRIED A11Y ATTRIBUTES${c.reset}${c.orange} (inherited from classless ancestor — review placement):${c.reset}`);
lines.push(`${c.dim}${thinSep}${c.reset}`);
for (const item of result.carried) {
lines.push(` ${c.orange}⚠ ${item.selector} ${item.attribute.name}="${item.attribute.value}"${c.reset}`);
}
}
lines.push(`${c.dim}${sep}${c.reset}`);
return lines.join('\n');
}
// Tree diff
if (result.treeDiff && result.treeDiff.length > 0) {
lines.push('');
lines.push(`${c.dim}${thinSep}${c.reset}`);
lines.push(`${c.bold} A11y Tree Diff${c.reset} ${c.dim}(✓=match ~=different +=extra −=missing)${c.reset}`);
lines.push(`${c.dim}${thinSep}${c.reset}`);
lines.push('');
lines.push(renderA11yDiffTree(result.treeDiff, c));
}
// Missing attributes
if (result.missing.length > 0) {
lines.push('');
lines.push(`${c.dim}${thinSep}${c.reset}`);
lines.push(`${c.bold}${c.red} MISSING A11Y ATTRIBUTES${c.reset}${c.red} (expected but not found in actual):${c.reset}`);
lines.push(`${c.dim}${thinSep}${c.reset}`);
for (const item of result.missing) {
lines.push(` ${c.red}− ${item.selector} ${item.attribute.name}="${item.attribute.value}"${c.reset}`);
}
}
// Extra attributes
if (result.extra.length > 0) {
lines.push('');
lines.push(`${c.dim}${thinSep}${c.reset}`);
lines.push(`${c.bold}${c.magenta} EXTRA A11Y ATTRIBUTES${c.reset}${c.magenta} (in actual but not in expected):${c.reset}`);
lines.push(`${c.dim}${thinSep}${c.reset}`);
for (const item of result.extra) {
lines.push(` ${c.magenta}+ ${item.selector} ${item.attribute.name}="${item.attribute.value}"${c.reset}`);
}
}
// Different values
if (result.different.length > 0) {
lines.push('');
lines.push(`${c.dim}${thinSep}${c.reset}`);
lines.push(`${c.bold}${c.yellow} DIFFERENT A11Y VALUES${c.reset}${c.yellow} (same attribute, different value):${c.reset}`);
lines.push(`${c.dim}${thinSep}${c.reset}`);
for (const item of result.different) {
lines.push(` ${c.yellow}~ ${item.selector} ${item.name}: "${item.actualValue}" → "${item.expectedValue}"${c.reset}`);
}
}
// Carried attributes (orange warning)
if (result.carried.length > 0) {
lines.push('');
lines.push(`${c.dim}${thinSep}${c.reset}`);
lines.push(`${c.bold}${c.orange} CARRIED A11Y ATTRIBUTES${c.reset}${c.orange} (inherited from classless ancestor — review placement):${c.reset}`);
lines.push(`${c.dim}${thinSep}${c.reset}`);
for (const item of result.carried) {
lines.push(` ${c.orange}⚠ ${item.selector} ${item.attribute.name}="${item.attribute.value}"${c.reset}`);
}
}
lines.push('');
lines.push(`${c.dim}${sep}${c.reset}`);
return lines.join('\n');
}
function renderA11yDiffTree(nodes, c, indent = '') {
const lines = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const isLast = i === nodes.length - 1;
const connector = isLast ? '└── ' : '├── ';
const childIndent = indent + (isLast ? ' ' : '│ ');
let line;
const attrSummary = formatNodeAttrSummary(node, c);
switch (node.status) {
case 'matched':
line = `${indent}${connector}${c.green}✓ ${node.classSelector}${c.reset}`;
if (attrSummary)
line += ` ${attrSummary}`;
break;
case 'extra':
line = `${indent}${connector}${c.magenta}+ ${node.classSelector}${c.reset}${c.dim} ← EXTRA${c.reset}`;
if (attrSummary)
line += ` ${attrSummary}`;
break;
case 'missing':
line = `${indent}${connector}${c.red}− ${node.classSelector}${c.reset}${c.dim} ← MISSING${c.reset}`;
if (attrSummary)
line += ` ${attrSummary}`;
break;
case 'different': {
line = `${indent}${connector}${c.cyan}~ ${node.classSelector}${c.reset}`;
if (attrSummary)
line += ` ${attrSummary}`;
break;
}
}
lines.push(line);
if (node.children.length > 0) {
lines.push(renderA11yDiffTree(node.children, c, childIndent));
}
}
return lines.join('\n');
}
function formatNodeAttrSummary(node, c) {
var _a;
const parts = [];
if (node.matchedAttributes.length > 0) {
const attrs = node.matchedAttributes.map(a => `${a.name}="${a.value}"`).join(' ');
parts.push(`${c.green}[${attrs}]${c.reset}`);
}
if (node.missingAttributes.length > 0) {
const attrs = node.missingAttributes.map(a => `${a.name}="${a.value}"`).join(' ');
parts.push(`${c.red}−[${attrs}]${c.reset}`);
}
if (node.extraAttributes.length > 0) {
const attrs = node.extraAttributes.map(a => `${a.name}="${a.value}"`).join(' ');
parts.push(`${c.magenta}+[${attrs}]${c.reset}`);
}
if (node.differentAttributes.length > 0) {
const attrs = node.differentAttributes.map(d => `${d.name}: "${d.actualValue}"→"${d.expectedValue}"`).join(' ');
parts.push(`${c.yellow}~[${attrs}]${c.reset}`);
}
if ((_a = node.carriedAttributes) === null || _a === void 0 ? void 0 : _a.length) {
const attrs = node.carriedAttributes.map(a => `${a.name}="${a.value}"`).join(' ');
parts.push(`${c.orange}⚠[${attrs}]${c.reset}`);
}
return parts.join(' ');
}
// ============= HTML Report Rendering =============
const helpers_3 = require("./helpers");
function renderA11yHtmlDiffTree(nodes, ignoreClasslessNodes) {
if (nodes.length === 0)
return '';
const items = nodes.map(node => {
let content;
let cssClass;
const isIgnoredClassless = ignoreClasslessNodes && node.classSelector === '(classless)';
const attrHtml = renderA11yNodeAttrHtml(node);
switch (node.status) {
case 'matched':
cssClass = 'node-matched';
content = `<span class="node ${cssClass}">✓ ${(0, helpers_3.escHtml)(node.classSelector)}</span>${attrHtml}`;
break;
case 'extra':
cssClass = 'node-extra';
content = `<span class="node ${cssClass}">+ ${(0, helpers_3.escHtml)(node.classSelector)}</span><span class="node-label">EXTRA</span>${attrHtml}`;
break;
case 'missing':
cssClass = 'node-missing';
content = `<span class="node ${cssClass}">− ${(0, helpers_3.escHtml)(node.classSelector)}</span><span class="node-label">MISSING</span>${attrHtml}`;
break;
case 'different':
cssClass = 'node-different';
content = `<span class="node ${cssClass}">~ ${(0, helpers_3.escHtml)(node.classSelector)}</span>${attrHtml}`;
break;
}
if (isIgnoredClassless) {
content = `<span class="a11y-ignored-node">${content} <span class="node-label" style="color: var(--gray);">IGNORED</span></span>`;
}
const childrenHtml = node.children.length > 0 ? renderA11yHtmlDiffTree(node.children, ignoreClasslessNodes) : '';
return `<li>${content}${childrenHtml}</li>`;
}).join('\n');
return `<ul>${items}</ul>`;
}
function renderA11yNodeAttrHtml(node) {
var _a;
const parts = [];
if (node.matchedAttributes.length > 0) {
const attrs = node.matchedAttributes.map(a => `${(0, helpers_3.escHtml)(a.name)}="${(0, helpers_3.escHtml)(a.value)}"`).join(' ');
parts.push(`<span class="a11y-matched">[${attrs}]</span>`);
}
if (node.missingAttributes.length > 0) {
const attrs = node.missingAttributes.map(a => `${(0, helpers_3.escHtml)(a.name)}="${(0, helpers_3.escHtml)(a.value)}"`).join(' ');
parts.push(`<span class="a11y-missing">−[${attrs}]</span>`);
}
if (node.extraAttributes.length > 0) {
const attrs = node.extraAttributes.map(a => `${(0, helpers_3.escHtml)(a.name)}="${(0, helpers_3.escHtml)(a.value)}"`).join(' ');
parts.push(`<span class="a11y-extra">+[${attrs}]</span>`);
}
if (node.differentAttributes.length > 0) {
const attrs = node.differentAttributes.map(d => `${(0, helpers_3.escHtml)(d.name)}: "${(0, helpers_3.escHtml)(d.actualValue)}" → "${(0, helpers_3.escHtml)(d.expectedValue)}"`).join(' ');
parts.push(`<span class="a11y-different">~[${attrs}]</span>`);
}
if ((_a = node.carriedAttributes) === null || _a === void 0 ? void 0 : _a.length) {
const attrs = node.carriedAttributes.map(a => `${(0, helpers_3.escHtml)(a.name)}="${(0, helpers_3.escHtml)(a.value)}"`).join(' ');
parts.push(`<span class="a11y-carried">⚠[${attrs}]</span>`);
}
if (parts.length === 0)
return '';
return ` <span class="a11y-attrs">${parts.join(' ')}</span>`;
}
//# sourceMappingURL=a11y-comparer.js.map