diff --git a/CHANGELOG.md b/CHANGELOG.md index 31def98..efb9c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - XPath locator support — `page.locator()`, `locator.locator()`, and `frameLocator.locator()` now accept XPath expressions in addition to CSS selectors. Prefix with `//` (e.g. `//button[@id='submit']`) or with `xpath=` (e.g. `xpath=//button[@id='submit']`). Evaluated via `document.evaluate()` using `ORDERED_NODE_SNAPSHOT_TYPE`. +- `HtmlReporter` — suite (group) headers are now collapsible; suites containing failures start expanded, all-passing suites start collapsed; **Expand all** / **Collapse all** buttons in the toolbar toggle all suites at once +- `HtmlReporter` — log groups produced by `log.group()` / the `step` fixture are now rendered as collapsible nested step lists inside the test detail panel; failing and warning groups start expanded, passing groups start collapsed; a `warn` state icon (amber triangle) is shown for `warn`-state steps +- `LogEntry.state` now includes `'warn'` in the reporter type, matching the runtime state emitted by `log.group()` and `logCommand().warn()` ### Changed - Command log entries no longer have a coloured left border — status is conveyed by the icon only - Inline test log now collapses automatically when a test finishes, regardless of pass or fail +- `HtmlReporter` — each test row now displays only the leaf test name; the suite prefix is stripped since it is already shown in the group header ### Removed - `:has-text("…")` pseudo-class support in selectors — use `locator.filter({ hasText: '…' })` instead diff --git a/src/reporters/HtmlReporter.ts b/src/reporters/HtmlReporter.ts index 2fabc33..7533350 100644 --- a/src/reporters/HtmlReporter.ts +++ b/src/reporters/HtmlReporter.ts @@ -5,8 +5,9 @@ import type { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult, Log interface StepEntry { cmd: string; message: string; - state: 'pass' | 'fail' | 'info'; + state: 'pass' | 'fail' | 'info' | 'warn'; duration?: number; + children?: StepEntry[]; } interface AttachmentEntry { @@ -48,13 +49,11 @@ export class HtmlReporter implements Reporter { } onTestEnd(test: TestCase, result: TestResult): void { - const allLogs = flattenLogs(result.logs ?? []); + const logs = result.logs ?? []; - const steps = allLogs - .filter((l: LogEntry) => l.cmd !== 'attach') - .map((l: LogEntry) => ({ cmd: l.cmd, message: l.message, state: l.state, duration: l.duration })); + const steps = logsToSteps(logs); - const attachments = allLogs + const attachments = flattenLogs(logs) .filter((l: LogEntry) => l.cmd === 'attach' && l.attachment) .map((l: LogEntry) => ({ label: l.message, @@ -92,6 +91,18 @@ function flattenLogs(logs: LogEntry[]): LogEntry[] { return out; } +function logsToSteps(logs: LogEntry[]): StepEntry[] { + return logs + .filter(l => l.cmd !== 'attach') + .map(l => ({ + cmd: l.cmd, + message: l.message, + state: l.state as StepEntry['state'], + duration: l.duration, + children: l.children?.length ? logsToSteps(l.children) : undefined, + })); +} + function esc(s: unknown): string { return String(s ?? '') .replace(/&/g, '&') @@ -105,6 +116,11 @@ function parseSuite(fullTitle: string): string { return idx >= 0 ? fullTitle.slice(0, idx) : '(root)'; } +function parseTitle(fullTitle: string): string { + const idx = fullTitle.lastIndexOf(' > '); + return idx >= 0 ? fullTitle.slice(idx + 3) : fullTitle; +} + function fmtDur(ms: number): string { if (ms >= 60000) return `${(ms / 60000).toFixed(1)}m`; if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; @@ -183,6 +199,11 @@ main { max-width: 1440px; margin: 0 auto; padding: 20px 24px 40px; } .jump { padding: 6px 12px; background: var(--fail-light); color: var(--fail); border: 1px solid #fecaca; border-radius: 8px; font-size: 0.8rem; cursor: pointer; white-space: nowrap; transition: background .15s; } .jump:hover { background: #fecaca; } +/* ── Suite buttons ── */ +.suite-btns { display: flex; gap: 5px; } +.suite-btn { padding: 6px 12px; border: 1px solid var(--border); background: var(--surface); border-radius: 8px; font-size: 0.8rem; cursor: pointer; transition: all .15s; white-space: nowrap; color: var(--text); } +.suite-btn:hover { background: #f8fafc; border-color: #cbd5e1; } + /* ── Groups ── */ #groups { display: flex; flex-direction: column; gap: 8px; } @@ -235,9 +256,19 @@ main { max-width: 1440px; margin: 0 auto; padding: 20px 24px 40px; } .sico.pass { color: var(--pass); } .sico.fail { color: var(--fail); } .sico.info { color: #94a3b8; } +.sico.warn { color: var(--skip); } .step span { font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; font-size: 0.79rem; } .sdur { color: #94a3b8; margin-left: auto; white-space: nowrap; padding-left: 10px; } +/* ── Step groups ── */ +.step-group { list-style: none; } +.step-group-hdr { display: flex; align-items: flex-start; gap: 7px; padding: 2px 0; font-size: 0.8rem; color: #475569; line-height: 1.4; cursor: pointer; user-select: none; } +.step-group-hdr:hover { color: var(--text); } +.step-group-chev { font-size: 0.58rem; color: #94a3b8; transition: transform .15s; margin-top: 2px; flex-shrink: 0; } +.step-group.open > .step-group-hdr .step-group-chev { transform: rotate(90deg); } +.step-group-body { display: none; padding-left: 18px; border-left: 2px solid #e2e8f0; margin-left: 5px; margin-top: 2px; margin-bottom: 2px; } +.step-group.open > .step-group-body { display: block; } + /* ── Error ── */ .err-block { background: #fff5f5; border: 1px solid #fecaca; border-radius: 7px; padding: 10px 12px; margin-bottom: 10px; } .err-hdr { display: flex; align-items: center; margin-bottom: 5px; } @@ -302,22 +333,39 @@ const JS = ` var STEP_ICONS = { pass: '', fail: '', - info: '' + info: '', + warn: '' }; var CHEV = ''; var EXI = ''; var ATT_ICON = ''; + function renderStepList(steps) { + if (!steps || !steps.length) return ''; + return '