Skip to content

Commit 9881dd5

Browse files
committed
write e2e test output to .e2e-logs for agents
1 parent 8e89743 commit 9881dd5

5 files changed

Lines changed: 85 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Thumbs.db
3131
test-results/
3232
ci-e2e-traces/
3333
playwright-report/
34+
.e2e-logs/
3435
.vercel
3536

3637
# Visual regression snapshots

.oxlintrc.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,21 @@
9898
"app/layouts/**/*",
9999
"app/forms/**/*",
100100
"*.config.ts",
101-
"*.config.mjs"
101+
"*.config.mjs",
102+
"test/e2e/compact-reporter.ts"
102103
],
103104
"rules": {
104105
"import/no-default-export": "off"
105106
}
106107
},
107108
{
108-
"files": ["**/*.spec.ts", "**/*.config.ts", "**/*.config.mjs", "tools/**/*"],
109+
"files": [
110+
"**/*.spec.ts",
111+
"**/*.config.ts",
112+
"**/*.config.mjs",
113+
"tools/**/*",
114+
"test/e2e/compact-reporter.ts"
115+
],
109116
"rules": {
110117
"import/no-nodejs-modules": "off"
111118
}

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
- Co-locate Vitest specs next to the code they cover; use Testing Library utilities (`render`, `renderHook`, `fireEvent`, fake timers) to assert observable output rather than implementation details (`app/ui/lib/FileInput.spec.tsx`, `app/hooks/use-pagination.spec.ts`).
3333
- For sweeping styling changes, coordinate with the visual regression harness and follow `test/visual/README.md` for the workflow.
3434
- Fix root causes of flaky timing rather than adding `sleep()` workarounds in tests.
35+
- Local Playwright runs write a compact plain-text report to `.e2e-logs/` (gitignored, one timestamped `.log` per run, last 10 kept) via the custom reporter at `test/e2e/compact-reporter.ts`. Top line is `status: ... total=N passed=N ...`; each failure is a `── UNEXPECTED|FLAKY file:line title` block followed by the error (ANSI stripped). Latest run: `ls .e2e-logs | tail -1` — Read it directly, no parsing needed.
3536

3637
# Data fetching pattern
3738

playwright.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export default {
2424
// default is 5 seconds. somehow playwright really hates async route modules,
2525
// takes a long time to load them. https://playwright.dev/docs/test-timeouts
2626
expect: { timeout: 10_000 },
27+
// Local runs also emit a compact plain-text failure report to .e2e-logs/
28+
// (timestamped per run, last 10 kept) via test/e2e/compact-reporter.ts, so
29+
// an LLM agent can read the failures.
30+
reporter: process.env.CI ? 'list' : [['list'], ['./test/e2e/compact-reporter.ts']],
2731
use: {
2832
trace: process.env.CI ? 'on-first-retry' : 'retain-on-failure',
2933
baseURL: 'http://localhost:4009',

test/e2e/compact-reporter.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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

Comments
 (0)