|
| 1 | +import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs' |
| 2 | +import { relative } from 'node:path' |
| 3 | + |
| 4 | +/* |
| 5 | + * This Source Code Form is subject to the terms of the Mozilla Public |
| 6 | + * License, v. 2.0. If a copy of the MPL was not distributed with this |
| 7 | + * file, you can obtain one at https://mozilla.org/MPL/2.0/. |
| 8 | + * |
| 9 | + * Copyright Oxide Computer Company |
| 10 | + */ |
| 11 | +import type { FullConfig, FullResult, Reporter, Suite } from '@playwright/test/reporter' |
| 12 | +import stripAnsi from 'strip-ansi' |
| 13 | + |
| 14 | +// Compact plain-text reporter for local runs. Writes a timestamped file to |
| 15 | +// `.e2e-logs/` per run with a summary line and one block per failed or flaky |
| 16 | +// test (ANSI stripped). Cheap for an LLM agent to read end-to-end. |
| 17 | +// Latest: `ls .e2e-logs | tail -1`. |
| 18 | + |
| 19 | +export default class CompactReporter implements Reporter { |
| 20 | + private suite!: Suite |
| 21 | + private rootDir = '' |
| 22 | + |
| 23 | + onBegin(config: FullConfig, suite: Suite) { |
| 24 | + this.suite = suite |
| 25 | + this.rootDir = config.rootDir |
| 26 | + } |
| 27 | + |
| 28 | + async onEnd(result: FullResult) { |
| 29 | + const tests = this.suite.allTests() |
| 30 | + const counts = { |
| 31 | + total: tests.length, |
| 32 | + passed: tests.filter((t) => t.outcome() === 'expected').length, |
| 33 | + failed: tests.filter((t) => t.outcome() === 'unexpected').length, |
| 34 | + flaky: tests.filter((t) => t.outcome() === 'flaky').length, |
| 35 | + skipped: tests.filter((t) => t.outcome() === 'skipped').length, |
| 36 | + } |
| 37 | + |
| 38 | + const lines = [ |
| 39 | + `status: ${result.status} total=${counts.total} passed=${counts.passed} failed=${counts.failed} flaky=${counts.flaky} skipped=${counts.skipped}`, |
| 40 | + ] |
| 41 | + |
| 42 | + for (const t of tests) { |
| 43 | + const outcome = t.outcome() |
| 44 | + if (outcome !== 'unexpected' && outcome !== 'flaky') continue |
| 45 | + const last = t.results[t.results.length - 1] |
| 46 | + const err = stripAnsi(last?.error?.message ?? last?.errors[0]?.message ?? '') |
| 47 | + const file = relative(this.rootDir, t.location.file) |
| 48 | + const title = t.titlePath().slice(1).join(' › ') |
| 49 | + lines.push( |
| 50 | + '', |
| 51 | + `── ${outcome.toUpperCase()} ${file}:${t.location.line} ${title}`, |
| 52 | + ` attempts=${t.results.length} duration=${Math.round(last?.duration ?? 0)}ms`, |
| 53 | + '', |
| 54 | + err |
| 55 | + ) |
| 56 | + } |
| 57 | + |
| 58 | + const stamp = new Date().toISOString().replace(/[:.]/g, '-') |
| 59 | + mkdirSync('.e2e-logs', { recursive: true }) |
| 60 | + writeFileSync(`.e2e-logs/${stamp}.log`, lines.join('\n') + '\n') |
| 61 | + |
| 62 | + // Keep only the 10 most recent runs. Timestamps sort lexicographically. |
| 63 | + const KEEP = 10 |
| 64 | + const old = readdirSync('.e2e-logs') |
| 65 | + .filter((f) => f.endsWith('.log')) |
| 66 | + .sort() |
| 67 | + .slice(0, -KEEP) |
| 68 | + for (const f of old) rmSync(`.e2e-logs/${f}`) |
| 69 | + } |
| 70 | +} |
0 commit comments