UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

302 lines 11.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NO_ANSI = exports.ANSI = void 0; exports.parseDom = parseDom; exports.removeDuplicatedSpaces = removeDuplicatedSpaces; exports.buildDomTree = buildDomTree; exports.sanitizeDomTreeNG = sanitizeDomTreeNG; exports.jaccardSimilarity = jaccardSimilarity; exports.diffTrees = diffTrees; exports.markNodeSubtree = markNodeSubtree; exports.extractAllowedClasses = extractAllowedClasses; exports.applyAllowsToDiff = applyAllowsToDiff; exports.generateSuggestions = generateSuggestions; exports.renderDiffTree = renderDiffTree; exports.escHtml = escHtml; // ============= ANSI Color Constants ============= exports.ANSI = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m', magenta: '\x1b[35m', gray: '\x1b[90m', orange: '\x1b[38;5;208m', }; exports.NO_ANSI = { reset: '', bold: '', dim: '', green: '', red: '', yellow: '', cyan: '', magenta: '', gray: '', orange: '', }; // ============= DOM Parsing ============= function parseDom(node, attributes, parent = "") { if (node === null) { return; } let newParent = parent; if (node.attributes[0] !== undefined) { const value = removeDuplicatedSpaces(node.attributes[0].value).replace(new RegExp('!', 'g'), '\\!'); attributes.push(`${parent}.${value.split(" ").sort().join(".")}`); newParent = `${parent}.${value.split(" ").sort().join(".")} `; } parseDom(node.firstElementChild, attributes, newParent); parseDom(node.nextElementSibling, attributes, parent); } function removeDuplicatedSpaces(text) { return text.replace(new RegExp(/ +/g, 'g'), ' ').trim(); } // ============= Tree Diff Engine ============= function buildDomTree(element) { const nodes = []; let child = element.firstElementChild; while (child) { if (child.attributes.length > 0 && child.attributes[0] !== undefined) { const rawValue = removeDuplicatedSpaces(child.attributes[0].value); const escapedValue = rawValue.replace(new RegExp('!', 'g'), '\\!'); const classes = escapedValue.split(' ').filter(Boolean).sort(); if (classes.length > 0) { nodes.push({ classes, classSelector: '.' + classes.join('.'), children: buildDomTree(child), }); } else { nodes.push(...buildDomTree(child)); } } else { // Skip classless nodes, promote their children nodes.push(...buildDomTree(child)); } child = child.nextElementSibling; } return nodes; } function sanitizeDomTreeNG(nodes) { return nodes.map(node => { const filteredClasses = node.classes.filter(c => !c.startsWith('ng-')); if (filteredClasses.length === 0) { return sanitizeDomTreeNG(node.children); } return [{ classes: filteredClasses, classSelector: '.' + filteredClasses.join('.'), children: sanitizeDomTreeNG(node.children), }]; }).flat(); } 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; } function diffTrees(actual, expected, depth = 0) { const results = []; const usedExpected = new Set(); for (const aNode of actual) { let bestMatch = -1; let bestScore = -1; let isExact = false; // First try exact 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]; if (isExact) { results.push({ depth, matchedClasses: [...aNode.classes], extraClasses: [], missingClasses: [], status: 'matched', classSelector: aNode.classSelector, children: diffTrees(aNode.children, eNode.children, depth + 1), }); } else { 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({ depth, matchedClasses: common, extraClasses: extra, missingClasses: miss, status: 'different', classSelector: aNode.classSelector, children: diffTrees(aNode.children, eNode.children, depth + 1), }); } } else { results.push(markNodeSubtree(aNode, 'extra', depth)); } } // Unmatched expected → missing for (let ei = 0; ei < expected.length; ei++) { if (!usedExpected.has(ei)) { results.push(markNodeSubtree(expected[ei], 'missing', depth)); } } return results; } function markNodeSubtree(node, status, depth) { return { depth, matchedClasses: [], extraClasses: status === 'extra' ? [...node.classes] : [], missingClasses: status === 'missing' ? [...node.classes] : [], status, classSelector: node.classSelector, children: node.children.map(child => markNodeSubtree(child, status, depth + 1)), }; } /** * Extract bare class names from allow-list entries (e.g. ".k-button" → "k-button"). */ function extractAllowedClasses(allowList) { const set = new Set(); for (const item of allowList) { set.add(item.startsWith('.') ? item.slice(1) : item); } return set; } /** * Post-process a tree diff to remove allowed classes from diffs. * - 'different' nodes: filter allowed classes; upgrade to 'matched' if no diffs remain. * - 'missing' nodes: if ALL classes are covered by allowMissing, mark subtree as matched. * - 'extra' nodes: if ALL classes are covered by allowExtra, mark subtree as matched. */ function applyAllowsToDiff(nodes, allowMissingClasses, allowExtraClasses) { for (const node of nodes) { if (node.status === 'different') { const nowAllowedMissing = node.missingClasses.filter(c => allowMissingClasses.has(c)); const nowAllowedExtra = node.extraClasses.filter(c => allowExtraClasses.has(c)); node.missingClasses = node.missingClasses.filter(c => !allowMissingClasses.has(c)); node.extraClasses = node.extraClasses.filter(c => !allowExtraClasses.has(c)); node.matchedClasses = [...node.matchedClasses, ...nowAllowedMissing, ...nowAllowedExtra]; if (node.missingClasses.length === 0 && node.extraClasses.length === 0) { node.status = 'matched'; } } else if (node.status === 'missing' && node.missingClasses.length > 0 && node.missingClasses.every(c => allowMissingClasses.has(c))) { markSubtreeAllowed(node); } else if (node.status === 'extra' && node.extraClasses.length > 0 && node.extraClasses.every(c => allowExtraClasses.has(c))) { markSubtreeAllowed(node); } applyAllowsToDiff(node.children, allowMissingClasses, allowExtraClasses); } } function markSubtreeAllowed(node) { node.matchedClasses = [...node.matchedClasses, ...node.missingClasses, ...node.extraClasses]; node.missingClasses = []; node.extraClasses = []; node.status = 'matched'; for (const child of node.children) { markSubtreeAllowed(child); } } function generateSuggestions(result) { const allowMissing = result.missing.map(sel => { const parts = sel.trim().split(/\s+/).filter(Boolean); return parts[parts.length - 1]; }); const allowExtra = result.extra.map(sel => { const parts = sel.trim().split(/\s+/).filter(Boolean); return parts[parts.length - 1]; }); return { allowMissing: [...new Set(allowMissing)], allowExtra: [...new Set(allowExtra)], }; } // ============= ANSI Diff Rendering ============= function renderDiffTree(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; switch (node.status) { case 'matched': line = `${indent}${connector}${c.green}${node.classSelector}${c.reset}`; break; case 'extra': line = `${indent}${connector}${c.magenta}+ ${node.classSelector}${c.reset}${c.dim} ← EXTRA${c.reset}`; break; case 'missing': line = `${indent}${connector}${c.red}${node.classSelector}${c.reset}${c.dim} ← MISSING${c.reset}`; break; case 'different': { const matchedPart = node.matchedClasses.length > 0 ? `${c.green}.${node.matchedClasses.join('.')}${c.reset}` : ''; const extraPart = node.extraClasses.length > 0 ? ` ${c.magenta}+[.${node.extraClasses.join('.')}]${c.reset}` : ''; const missingPart = node.missingClasses.length > 0 ? ` ${c.red}−[.${node.missingClasses.join('.')}]${c.reset}` : ''; line = `${indent}${connector}${c.cyan}~ ${c.reset}${matchedPart}${extraPart}${missingPart}`; break; } } lines.push(line); if (node.children.length > 0) { lines.push(renderDiffTree(node.children, c, childIndent)); } } return lines.join('\n'); } // ============= Utilities ============= function escHtml(text) { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;'); } //# sourceMappingURL=helpers.js.map