UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

582 lines 26.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.compareHtml = compareHtml; exports.getPartialHtml = getPartialHtml; exports.sanitizeNGSelectors = sanitizeNGSelectors; exports.formatDiff = formatDiff; exports.generateHtmlReport = generateHtmlReport; /* eslint-disable prefer-const */ const sanitize_html_1 = __importDefault(require("sanitize-html")); const parse_html_1 = require("./parse-html"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const helpers_1 = require("./helpers"); const html_report_1 = require("./html-report"); /** Escape special regex characters so the string is matched literally. */ function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } const a11y_comparer_1 = require("./a11y-comparer"); const a11y_id_validator_1 = require("./a11y-id-validator"); /** * Check if two html documents have same class hierarchy. * * @example * await compareHtml("<div class="set">SET</div>", "<div class="qa">QA</div>"); * * @param actualHtml An html object. * @param expectedHtml An html object. */ function compareHtml(actualHtml, expectedHtml, options) { var _a, _b, _c, _d; const passed = []; const missing = []; const extra = []; let actualSelectors = []; let expectedSelectors = []; const result = { passed, missing, extra, actualSelectors, expectedSelectors }; const config = { allowedTags: false, allowVulnerableTags: true, allowedAttributes: { "*": ["class"] }, }; 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; let parent = ""; (0, helpers_1.parseDom)(actualDom, actualSelectors, parent); parent = ""; (0, helpers_1.parseDom)(expectedDom, expectedSelectors); if (options === null || options === void 0 ? void 0 : options.sanitizeNGSelectors) { const sanitizedActualSelectors = []; for (const actualSelector of actualSelectors) { sanitizedActualSelectors.push(sanitizeNGSelectors(actualSelector)); } actualSelectors = sanitizedActualSelectors; } result.actualSelectors = actualSelectors; result.expectedSelectors = expectedSelectors; // Delete all allowed missing from expected selectors let expectedSelectorsWithoutMissing = []; if ((options === null || options === void 0 ? void 0 : options.allowMissing) !== undefined) { for (const selector of expectedSelectors) { let replacedSelector = selector; for (const missingItem of options === null || options === void 0 ? void 0 : options.allowMissing) { replacedSelector = (0, helpers_1.removeDuplicatedSpaces)(replacedSelector.replace(new RegExp(`${escapeRegExp(missingItem)}(?![\\w-])`, 'g'), '')); } if (replacedSelector !== '') { expectedSelectorsWithoutMissing.push(replacedSelector); } } expectedSelectorsWithoutMissing = [...new Set(expectedSelectorsWithoutMissing)]; } else { expectedSelectorsWithoutMissing = expectedSelectors; } // Delete all allowed extra from actual selectors let actualWithoutExtra = []; if ((options === null || options === void 0 ? void 0 : options.allowExtra) !== undefined) { for (const selector of actualSelectors) { let replacedSelector = selector; for (const extraItem of options === null || options === void 0 ? void 0 : options.allowExtra) { replacedSelector = (0, helpers_1.removeDuplicatedSpaces)(replacedSelector.replace(new RegExp(`${escapeRegExp(extraItem)}(?![\\w-])`, 'g'), '')); } if (replacedSelector !== '') { actualWithoutExtra.push(replacedSelector); } } actualWithoutExtra = [...new Set(actualWithoutExtra)]; } else { actualWithoutExtra = actualSelectors; } result.missing = expectedSelectorsWithoutMissing.filter((element) => !actualWithoutExtra.includes(element)); // Detect extra elements result.extra = []; result.passed = []; if (actualWithoutExtra.length > 0) { for (const selector of actualWithoutExtra) { const doc = (0, parse_html_1.parseHtml)((0, sanitize_html_1.default)(expectedHtml, config)); const element = doc.querySelector(selector); if (element === null) { result.extra.push(selector); } else { result.passed.push(selector); } } result.extra = [...new Set(result.extra)]; result.passed = [...new Set(result.passed)]; } // Build tree diff for visualization try { let actualTreeNodes = (0, helpers_1.buildDomTree)(actualDom); const expectedTreeNodes = (0, helpers_1.buildDomTree)(expectedDom); if (options === null || options === void 0 ? void 0 : options.sanitizeNGSelectors) { actualTreeNodes = (0, helpers_1.sanitizeDomTreeNG)(actualTreeNodes); } result.treeDiff = (0, helpers_1.diffTrees)(actualTreeNodes, expectedTreeNodes); // Post-process tree diff to hide allowed diffs if (result.treeDiff && (((_a = options === null || options === void 0 ? void 0 : options.allowMissing) === null || _a === void 0 ? void 0 : _a.length) || ((_b = options === null || options === void 0 ? void 0 : options.allowExtra) === null || _b === void 0 ? void 0 : _b.length))) { const allowMissingClasses = (0, helpers_1.extractAllowedClasses)((_c = options === null || options === void 0 ? void 0 : options.allowMissing) !== null && _c !== void 0 ? _c : []); const allowExtraClasses = (0, helpers_1.extractAllowedClasses)((_d = options === null || options === void 0 ? void 0 : options.allowExtra) !== null && _d !== void 0 ? _d : []); (0, helpers_1.applyAllowsToDiff)(result.treeDiff, allowMissingClasses, allowExtraClasses); } const suggestions = (0, helpers_1.generateSuggestions)(result); result.suggestedAllowMissing = suggestions.allowMissing; result.suggestedAllowExtra = suggestions.allowExtra; } catch (_e) { // Tree diff is supplementary; don't fail the comparison } // A11y attribute comparison (opt-in) if (options === null || options === void 0 ? void 0 : options.compareA11yAttributes) { try { result.a11yResult = (0, a11y_comparer_1.compareA11yAttributes)(actualHtml, expectedHtml, { sanitizeNGSelectors: options === null || options === void 0 ? void 0 : options.sanitizeNGSelectors, allowMissing: options === null || options === void 0 ? void 0 : options.allowMissingA11yAttributes, allowExtra: options === null || options === void 0 ? void 0 : options.allowExtraA11yAttributes, ignoreValueAttributes: options === null || options === void 0 ? void 0 : options.ignoreValueAttributes, collapseExtraClasses: (options === null || options === void 0 ? void 0 : options.collapseExtraClasses) !== undefined ? options.collapseExtraClasses : options === null || options === void 0 ? void 0 : options.allowExtra, collapseMissingClasses: (options === null || options === void 0 ? void 0 : options.collapseMissingClasses) !== undefined ? options.collapseMissingClasses : options === null || options === void 0 ? void 0 : options.allowMissing, ignoreClasslessNodes: options === null || options === void 0 ? void 0 : options.ignoreClasslessNodes, }); } catch (_f) { // A11y comparison is supplementary; don't fail the class comparison } // ID reference validation on actual HTML (enabled by default, opt-out with validateIdReferences: false) if ((options === null || options === void 0 ? void 0 : options.validateIdReferences) !== false) { try { result.idRefResult = (0, a11y_id_validator_1.validateIdReferences)(actualHtml); } catch (_g) { // ID validation is supplementary; don't fail the comparison } } } // Auto-generate HTML report when the report option is provided if (options === null || options === void 0 ? void 0 : options.report) { try { result.reportPath = generateHtmlReport(result, Object.assign(Object.assign({}, options.report), { actualHtml, expectedHtml, allowMissing: options === null || options === void 0 ? void 0 : options.allowMissing, allowExtra: options === null || options === void 0 ? void 0 : options.allowExtra, ignoreClasslessNodes: options === null || options === void 0 ? void 0 : options.ignoreClasslessNodes })); } catch (_h) { // Report generation is supplementary; don't fail the comparison } } return result; } /** * Get partial html of bigger html block. * * @example * await getPartialHtml("<div><ul class="k-list"><ul></div>", ".k-list"); * * @param originalHtml An html object. * @param selector Css selector. */ function getPartialHtml(originalHtml, selector) { const config = { allowedTags: false, allowVulnerableTags: true, allowedAttributes: { "*": ["class"] }, }; const doc = (0, parse_html_1.parseHtml)((0, sanitize_html_1.default)(originalHtml, config)); const element = doc.querySelector(selector); if (element !== null) { return element.outerHTML; } else { return ''; } } /** * Remove angular specific selectors. * * @example * await sanitize(".k-scrollview.ng-tns-c43-0 .k-scrollview-wrap.ng-tns-c43-0.ng-trigger.ng-trigger-animateTo"); * * @param selector Css selector as string. */ function sanitizeNGSelectors(selector) { const selectorsArray = selector.split(" "); const filteredArray = []; for (const selectorPart of selectorsArray) { const filteredPart = selectorPart.split(".").filter((s) => !s.startsWith('ng-')).join("."); filteredArray.push(filteredPart); } return (0, helpers_1.removeDuplicatedSpaces)(filteredArray.join(" ")); } /** * Format a comparison result for display. * * - `format: 'llm'` (default) — structured plain text optimised for LLM / AI consumption. * No ANSI codes; uses `KEY: value` and `[STATUS] selector` notation. * - `format: 'human'` — coloured, human-readable diff with Unicode box-drawing. * * @example * // LLM format (default) * console.log(formatDiff(result)); * // Human-readable with colours * console.log(formatDiff(result, { format: 'human', useColors: true })); * * @param result The comparison result from compareHtml. * @param options Formatting options. */ function formatDiff(result, options) { var _a, _b, _c, _d, _e, _f; if (((_a = options === null || options === void 0 ? void 0 : options.format) !== null && _a !== void 0 ? _a : 'llm') === 'llm') { return formatDiffLLMImpl(result); } const c = (options === null || options === void 0 ? void 0 : options.useColors) ? helpers_1.ANSI : helpers_1.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} HTML CLASS HIERARCHY DIFF${c.reset}`); lines.push(`${c.dim}${sep}${c.reset}`); lines.push(''); // Summary const passedCount = result.passed.length; const missingCount = result.missing.length; const extraCount = result.extra.length; lines.push(` ${c.green}${passedCount} passed${c.reset}` + (missingCount > 0 ? ` ${c.red}${missingCount} missing${c.reset}` : '') + (extraCount > 0 ? ` ${c.magenta}+ ${extraCount} extra${c.reset}` : '')); if (missingCount === 0 && extraCount === 0) { lines.push(''); lines.push(` ${c.green}All selectors matched!${c.reset}`); // Still append a11y diff if present if (result.a11yResult) { lines.push((0, a11y_comparer_1.formatA11yDiff)(result.a11yResult, { useColors: options === null || options === void 0 ? void 0 : options.useColors })); } // ID reference validation if (result.idRefResult) { lines.push((0, a11y_id_validator_1.formatIdRefValidation)(result.idRefResult, { useColors: options === null || options === void 0 ? void 0 : options.useColors })); } 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} Tree Diff${c.reset} ${c.dim}(✓=match ~=different +=extra −=missing)${c.reset}`); lines.push(`${c.dim}${thinSep}${c.reset}`); lines.push(''); lines.push((0, helpers_1.renderDiffTree)(result.treeDiff, c)); } // Missing selectors if (result.missing.length > 0) { lines.push(''); lines.push(`${c.dim}${thinSep}${c.reset}`); lines.push(`${c.bold}${c.red} MISSING${c.reset}${c.red} (expected but not found in actual):${c.reset}`); lines.push(`${c.dim}${thinSep}${c.reset}`); for (const sel of result.missing) { lines.push(` ${c.red}${sel}${c.reset}`); } } // Extra selectors if (result.extra.length > 0) { lines.push(''); lines.push(`${c.dim}${thinSep}${c.reset}`); lines.push(`${c.bold}${c.magenta} EXTRA${c.reset}${c.magenta} (in actual but not found in expected):${c.reset}`); lines.push(`${c.dim}${thinSep}${c.reset}`); for (const sel of result.extra) { lines.push(` ${c.magenta}+ ${sel}${c.reset}`); } } // Suggestions if (((_b = result.suggestedAllowMissing) === null || _b === void 0 ? void 0 : _b.length) || ((_c = result.suggestedAllowExtra) === null || _c === void 0 ? void 0 : _c.length)) { lines.push(''); lines.push(`${c.dim}${sep}${c.reset}`); lines.push(`${c.bold}${c.cyan} SUGGESTED OPTIONS${c.reset} ${c.dim}(copy-paste into your test):${c.reset}`); lines.push(`${c.dim}${sep}${c.reset}`); lines.push(''); if ((_d = result.suggestedAllowMissing) === null || _d === void 0 ? void 0 : _d.length) { lines.push(`${c.red} allowMissing: [${c.reset}`); for (let i = 0; i < result.suggestedAllowMissing.length; i++) { const comma = i < result.suggestedAllowMissing.length - 1 ? ',' : ''; lines.push(` ${c.red}"${result.suggestedAllowMissing[i]}"${comma}${c.reset}`); } lines.push(`${c.red} ]${c.reset}`); } if ((_e = result.suggestedAllowExtra) === null || _e === void 0 ? void 0 : _e.length) { if ((_f = result.suggestedAllowMissing) === null || _f === void 0 ? void 0 : _f.length) lines.push(''); lines.push(`${c.magenta} allowExtra: [${c.reset}`); for (let i = 0; i < result.suggestedAllowExtra.length; i++) { const comma = i < result.suggestedAllowExtra.length - 1 ? ',' : ''; lines.push(` ${c.magenta}"${result.suggestedAllowExtra[i]}"${comma}${c.reset}`); } lines.push(`${c.magenta} ]${c.reset}`); } } // A11y attribute diff (appended when present) if (result.a11yResult) { lines.push((0, a11y_comparer_1.formatA11yDiff)(result.a11yResult, { useColors: options === null || options === void 0 ? void 0 : options.useColors })); } // ID reference validation (appended when present) if (result.idRefResult) { lines.push((0, a11y_id_validator_1.formatIdRefValidation)(result.idRefResult, { useColors: options === null || options === void 0 ? void 0 : options.useColors })); } lines.push(''); lines.push(`${c.dim}${sep}${c.reset}`); return lines.join('\n'); } // ============= LLM Format Implementation ============= /** * Render a class-hierarchy tree diff in plain text for LLM consumption. * Each node is prefixed with [STATUS] and indented by depth. */ function renderTreeForLLM(nodes, depth = 0) { const lines = []; const pad = ' '.repeat(depth); for (const node of nodes) { const tag = node.status === 'matched' ? '[MATCH]' : node.status === 'missing' ? '[MISSING]' : node.status === 'extra' ? '[EXTRA]' : '[DIFF]'; let info = `${pad}${tag} ${node.classSelector}`; if (node.status === 'different') { if (node.missingClasses.length > 0) info += ` missing_classes=[${node.missingClasses.join(',')}]`; if (node.extraClasses.length > 0) info += ` extra_classes=[${node.extraClasses.join(',')}]`; } lines.push(info); if (node.children.length > 0) { lines.push(renderTreeForLLM(node.children, depth + 1)); } } return lines.join('\n'); } /** * Render an a11y tree diff in plain text for LLM consumption. */ function renderA11yTreeForLLM(nodes, depth = 0) { const lines = []; const pad = ' '.repeat(depth); for (const node of nodes) { const tag = node.status === 'matched' ? '[MATCH]' : node.status === 'missing' ? '[MISSING]' : node.status === 'extra' ? '[EXTRA]' : '[DIFF]'; let info = `${pad}${tag} ${node.classSelector}`; if (node.matchedAttributes.length > 0) { info += ` matched=[${node.matchedAttributes.map(a => `${a.name}="${a.value}"`).join(',')}]`; } if (node.missingAttributes.length > 0) { info += ` missing=[${node.missingAttributes.map(a => `${a.name}="${a.value}"`).join(',')}]`; } if (node.extraAttributes.length > 0) { info += ` extra=[${node.extraAttributes.map(a => `${a.name}="${a.value}"`).join(',')}]`; } if (node.differentAttributes.length > 0) { info += ` different=[${node.differentAttributes.map(a => `${a.name}: expected="${a.expectedValue}" actual="${a.actualValue}"`).join(',')}]`; } if (node.carriedAttributes && node.carriedAttributes.length > 0) { info += ` carried=[${node.carriedAttributes.map(a => `${a.name}="${a.value}"`).join(',')}]`; } lines.push(info); if (node.children.length > 0) { lines.push(renderA11yTreeForLLM(node.children, depth + 1)); } } return lines.join('\n'); } /** Internal: LLM-format implementation called by formatDiff. */ function formatDiffLLMImpl(result) { var _a, _b, _c, _d; const sep = '='.repeat(60); const thin = '-'.repeat(60); const lines = []; // Header lines.push(sep); lines.push('HTML COMPARISON RESULT'); lines.push(sep); const allClassesOk = result.missing.length === 0 && result.extra.length === 0; const a11yOk = !result.a11yResult || (result.a11yResult.missing.length === 0 && result.a11yResult.extra.length === 0 && result.a11yResult.different.length === 0); const idRefOk = !result.idRefResult || result.idRefResult.invalid.length === 0; const overallPass = allClassesOk && a11yOk && idRefOk; lines.push(`STATUS: ${overallPass ? 'PASS' : 'FAIL'}`); lines.push(`PASSED: ${result.passed.length}`); lines.push(`MISSING: ${result.missing.length}`); lines.push(`EXTRA: ${result.extra.length}`); // --- CLASS DIFF --- if (!allClassesOk) { lines.push(''); lines.push(thin); lines.push('CLASS DIFF'); lines.push(thin); if (result.treeDiff && result.treeDiff.length > 0) { lines.push(''); lines.push('TREE:'); lines.push(renderTreeForLLM(result.treeDiff)); } if (result.missing.length > 0) { lines.push(''); lines.push('MISSING_SELECTORS:'); for (const sel of result.missing) { lines.push(` - ${sel}`); } } if (result.extra.length > 0) { lines.push(''); lines.push('EXTRA_SELECTORS:'); for (const sel of result.extra) { lines.push(` + ${sel}`); } } if (((_a = result.suggestedAllowMissing) === null || _a === void 0 ? void 0 : _a.length) || ((_b = result.suggestedAllowExtra) === null || _b === void 0 ? void 0 : _b.length)) { lines.push(''); lines.push('SUGGESTED_OPTIONS:'); if ((_c = result.suggestedAllowMissing) === null || _c === void 0 ? void 0 : _c.length) { lines.push(` allowMissing: [${result.suggestedAllowMissing.map(s => `"${s}"`).join(', ')}]`); } if ((_d = result.suggestedAllowExtra) === null || _d === void 0 ? void 0 : _d.length) { lines.push(` allowExtra: [${result.suggestedAllowExtra.map(s => `"${s}"`).join(', ')}]`); } } } // --- A11Y DIFF --- if (result.a11yResult) { const a = result.a11yResult; lines.push(''); lines.push(thin); lines.push('A11Y DIFF'); lines.push(thin); lines.push(`A11Y_STATUS: ${a11yOk ? 'PASS' : 'FAIL'}`); lines.push(`A11Y_PASSED: ${a.passed.length}`); lines.push(`A11Y_MISSING: ${a.missing.length}`); lines.push(`A11Y_EXTRA: ${a.extra.length}`); lines.push(`A11Y_DIFFERENT: ${a.different.length}`); lines.push(`A11Y_CARRIED: ${a.carried.length}`); if (a.treeDiff && a.treeDiff.length > 0) { lines.push(''); lines.push('A11Y_TREE:'); lines.push(renderA11yTreeForLLM(a.treeDiff)); } if (a.missing.length > 0) { lines.push(''); lines.push('MISSING_A11Y:'); for (const m of a.missing) { lines.push(` - ${m.selector} ${m.attribute.name}="${m.attribute.value}"`); } } if (a.extra.length > 0) { lines.push(''); lines.push('EXTRA_A11Y:'); for (const e of a.extra) { lines.push(` + ${e.selector} ${e.attribute.name}="${e.attribute.value}"`); } } if (a.different.length > 0) { lines.push(''); lines.push('DIFFERENT_A11Y:'); for (const d of a.different) { lines.push(` ~ ${d.selector} ${d.name}: expected="${d.expectedValue}" actual="${d.actualValue}"`); } } if (a.carried.length > 0) { lines.push(''); lines.push('CARRIED_A11Y:'); for (const c of a.carried) { lines.push(` ! ${c.selector} ${c.attribute.name}="${c.attribute.value}"`); } } } // --- ID REFERENCE VALIDATION --- if (result.idRefResult) { lines.push((0, a11y_id_validator_1.formatIdRefForLLM)(result.idRefResult)); } lines.push(''); lines.push(sep); return lines.join('\n'); } /** * Generate an HTML report file visualizing the comparison result. * * The file is named: `{specFileName}_{testName}_{MM-DD-HH-mm-ss}.html` * * By default the report is written to a `rendering-reports` subfolder next to the spec file * (when `specFilePath` is provided). You can override this via `outputDir`. * * @param result The comparison result from compareHtml. * @param options Report options including file naming and optional raw HTML. * @returns The absolute path of the generated report file. */ function generateHtmlReport(result, options) { const now = new Date(); const mm = String(now.getMonth() + 1).padStart(2, '0'); const dd = String(now.getDate()).padStart(2, '0'); const hh = String(now.getHours()).padStart(2, '0'); const min = String(now.getMinutes()).padStart(2, '0'); const ss = String(now.getSeconds()).padStart(2, '0'); const timestamp = `${mm}-${dd}-${hh}-${min}-${ss}`; const sanitizedTestName = options.testName.replace(/[^a-zA-Z0-9_-]/g, '_').replace(/_+/g, '_'); const sanitizedSpecName = options.specFileName.replace(/[^a-zA-Z0-9_-]/g, '_'); const fileName = `${sanitizedSpecName}_${sanitizedTestName}_${timestamp}.html`; // Determine output directory: // 1. Explicit outputDir (custom path) // 2. rendering-reports subfolder next to the spec file // 3. Fallback to ./rendering-reports let outputDir; if (options.outputDir) { outputDir = options.outputDir; } else if (options.specFilePath) { outputDir = path.resolve(path.dirname(options.specFilePath), 'rendering-reports'); } else { outputDir = './rendering-reports'; } if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const filePath = path.resolve(outputDir, fileName); const html = (0, html_report_1.buildHtmlReportContent)(result, options); fs.writeFileSync(filePath, html, 'utf-8'); return filePath; } //# sourceMappingURL=html-comparer.js.map