@progress/kendo-e2e
Version:
Kendo UI end-to-end test utilities.
302 lines • 11.3 kB
JavaScript
;
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
//# sourceMappingURL=helpers.js.map