@progress/kendo-e2e
Version:
Kendo UI end-to-end test utilities.
582 lines • 26.9 kB
JavaScript
;
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