UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

677 lines 31 kB
"use strict"; 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}">&#10003; ${(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}">&minus; ${(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">&minus;[${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)}" &rarr; "${(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">&#9888;[${attrs}]</span>`); } if (parts.length === 0) return ''; return ` <span class="a11y-attrs">${parts.join(' ')}</span>`; } //# sourceMappingURL=a11y-comparer.js.map