Skip to content

Replace polling with SSE live updates; fix streaming edge cases#56

Draft
Copilot wants to merge 12 commits into
mainfrom
copilot/implement-live-stream-runs
Draft

Replace polling with SSE live updates; fix streaming edge cases#56
Copilot wants to merge 12 commits into
mainfrom
copilot/implement-live-stream-runs

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 26, 2026

Dashboard pages were polling every 5s via useAutoRefresh, causing periodic freezes. Replaces all polling with a push-based SSE model. Also fixes an EventSource is not defined SSR crash and several streaming-path bugs surfaced in review.

Server

  • /api/stream — new global SSE endpoint; broadcasts run-started, run-finished, run-submitted events to all connected dashboard clients with a 15s heartbeat
  • run-events.ts — extended RunEventBus with publishGlobal/subscribeGlobal on a separate globalEmitter; added single-instance limitation docs
  • start.post.ts / finish.post.ts / submit.post.ts / upload.post.ts — each publishes a GlobalRunEvent after mutating run state so connected clients are notified immediately

Client

  • useRunStream(refresh) — new composable replacing useAutoRefresh; opens one EventSource('/api/stream') per component, calls refresh() on every message, closes on unmount; guarded by import.meta.client to prevent SSR crash
  • pages/index.vue, projects/index.vue, projects/[id]/index.vue, layouts/default.vue, components/ProjectsMenu.vue — swapped useAutoRefresh for useRunStream
  • test-runs/[id].vue — per-run live view; added import.meta.client guard; handles init event to seed initial state; deduplicates incoming test cases with an O(1) Map

Bug fixes

  • reporter/index.js_uploadFilesForStreamingRun used testCase.index which is only assigned in the non-streaming uploadWithFiles path, resulting in all trace form fields named trace_undefined; fixed by iterating with entries() and using the loop index directly
  • upload.post.ts — batch-upload path was not calling publishGlobal, so uploaded runs never triggered a dashboard refresh
  • _journal.json — migration 0007 had a when timestamp earlier than 0006; corrected to maintain chronological order

Copilot AI and others added 2 commits May 26, 2026 13:18
- Add streamToken column to test_runs schema (sqlite + pg)
- Create database migration 0006_add_stream_token
- Create in-memory pub/sub utility (run-events.ts)
- Add POST /api/test-runs/start endpoint (start streaming run)
- Add POST /api/test-runs/[id]/events endpoint (stream test results)
- Add POST /api/test-runs/[id]/finish endpoint (finalize run)
- Add GET /api/test-runs/[id]/stream SSE endpoint (frontend live feed)
- Update reporter with streaming mode (batched events, fallback to batch)
- Update frontend with live SSE connection, progress bar, and live indicator
- Add 'running' status color support

Agent-Logs-Url: https://github.com/PhenX/playwright-dashboard/sessions/6cf5a5fe-b744-42ca-9685-88fac50c9c96

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds end-to-end live streaming of Playwright test run progress from the reporter to the dashboard using HTTP POST (reporter → server) and Server-Sent Events (server → UI), enabling users to watch runs update in real time.

Changes:

  • Introduces streaming lifecycle endpoints (start, events, finish) plus an SSE endpoint (stream) and an in-memory event bus for fan-out.
  • Extends the reporter with default-on streaming mode and batching controls, with documentation updates for new options/endpoints.
  • Adds a running run status and a streamToken field to support authenticated streaming updates.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
reporter/index.js Implements reporter-side streaming lifecycle (start, batch events, finish) and attempts file upload for streaming runs.
reporter/index.d.ts Adds TypeScript option types for streaming configuration.
docs/reporter.md Documents streaming options and usage patterns.
docs/api.md Documents streaming endpoints and SSE event payloads.
application/server/utils/run-events.ts Adds in-memory pub/sub (EventEmitter) for broadcasting run events to SSE clients.
application/server/database/schema.sqlite.ts Adds running status comment and streamToken column to SQLite schema.
application/server/database/schema.pg.ts Adds running status comment and streamToken column to Postgres schema.
application/server/database/migrations/meta/_journal.json Registers the new migration entry for 0006_add_stream_token.
application/server/database/migrations/0006_add_stream_token.sql Adds stream_token column to test_runs table.
application/server/api/test-runs/start.post.ts New endpoint to create a running test run and issue a stream token.
application/server/api/test-runs/[id]/events.post.ts New endpoint to append streamed test-case events, update counters, and publish SSE events.
application/server/api/test-runs/[id]/finish.post.ts New endpoint to finalize a streaming run, compute summary metrics, and publish completion SSE event.
application/server/api/test-runs/[id]/stream.get.ts New SSE endpoint providing init/catch-up, live events, and heartbeat.
application/app/utils/index.ts Adds running status color mapping for UI badges.
application/app/pages/test-runs/[id].vue Adds SSE client, live progress UI, and live test-case list rendering for running runs.

Comment thread reporter/index.js Outdated
Comment on lines +299 to +307
// Flush any remaining streaming events
if (this.streamingEnabled && this.pendingEvents.length > 0) {
this._flushStreamEvents();
}

// Wait for all pending stream flushes to complete
if (this.flushPromises.length > 0) {
await Promise.allSettled(this.flushPromises);
this.flushPromises = [];
Comment thread reporter/index.js
Comment on lines +412 to +416
// Add run ID to associate files with existing run
form.append('testRunId', String(this.streamingRunId));
form.append('projectName', this.options.projectName);

// We don't re-send testRun data or testCases — they're already on the server
Comment on lines +137 to +141
const updates: Record<string, number> = {
totalTests: testRun.totalTests + testCaseEvents.filter((tc: { title?: string }) => tc && tc.title).length,
passedTests: testRun.passedTests + (statusCounts['passed'] || 0),
failedTests: testRun.failedTests + (statusCounts['failed'] || 0),
skippedTests: testRun.skippedTests + (statusCounts['skipped'] || 0)
Comment on lines +68 to +77
const locationParts = tc.location.split(':')
if (locationParts.length >= 1) {
filePath = locationParts[0]
}
if (locationParts.length >= 2) {
line = parseInt(locationParts[1], 10) || null
}
if (locationParts.length >= 3) {
column = parseInt(locationParts[2], 10) || null
}
Comment on lines +80 to +84
// Get or create shared test case
const existingTestCases = await db.select()
.from(testCases)
.where(
and(
Comment on lines 29 to 31
metadata: jsonb('metadata'), // Additional metadata as JSON
streamToken: text('stream_token'), // Token for authenticating streaming updates
createdAt: timestamp('created_at', { mode: 'date' }).notNull().$defaultFn(() => new Date())
Comment on lines +51 to +55
} else if (parsed.type === 'run-progress') {
liveProgress.value = parsed.data
} else if (parsed.type === 'run-finished') {
// Run is done — refresh full data from server
disconnectStream()
Comment on lines +37 to +41
// Avoid duplicates (catch-up events have seq 0)
const existing = liveTestCases.value.find(
tc => tc.title === parsed.data.title && tc.location === parsed.data.location
)
if (!existing) {
Comment on lines +7 to +12
export default eventHandler(async (event) => {
// Require reporter or administrator role
await requireAuth(event, ['reporter', 'administrator'])

const body = await readBody(event)

Comment on lines +3 to +7
/**
* In-memory pub/sub for live test run streaming.
* Events are keyed by run ID and forwarded to SSE subscribers.
*/

@PhenX
Copy link
Copy Markdown
Owner

PhenX commented May 26, 2026

@copilot apply review suggestions

- Fix Windows path parsing in events.post.ts and upload.post.ts by parsing
  line/column from the rightmost numeric ':' segments
- Use atomic SQL increments (sql template) for run counters in events.post.ts
  to prevent lost updates under concurrent requests
- Prefetch test cases in a single batch query to avoid N+1 pattern
- Use '??' (nullish coalescing) instead of '||' for duration in finish.post.ts
  so that explicit falsy values like 0 are preserved
- Fix flakyTests to be persisted when explicitly provided (including 0)
- Exclude streamToken from GET /api/test-runs/:id response
- Handle 'init' SSE event in frontend to initialise liveProgress state
- Use Map for O(1) duplicate detection in live test case list
- Retry re-queued streaming events up to 3 times before finalising
- Honor testRunId in upload handler to attach reports to existing streaming run
- Document single-instance limitation of the in-memory event bus
- Add streaming lifecycle tests (streaming-api.spec.ts)
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 26, 2026

@copilot apply review suggestions

All 13 review suggestions have been applied in commit a05d8d2:

  • Windows path parsing — replaced split(':') with rightmost-colon logic in both events.post.ts and upload.post.ts so C:\repo\file.ts:10:5 is parsed correctly
  • Atomic SQL countersevents.post.ts now uses Drizzle's sql template for totalTests + N, passedTests + N, etc. and uses .returning() to get fresh values for SSE publish
  • N+1 → batch prefetch — single inArray query fetches all test cases for the whole batch; a Map provides O(1) lookups inside the loop
  • ?? instead of ||finish.post.ts now preserves explicit duration: 0 and flakyTests: 0
  • flakyTests = 0 — condition changed from > 0 to !== undefined so an explicit zero is written back
  • streamToken not leaked[id].get.ts destructures { streamToken: _streamToken, ...testRunPublic } before spreading into the response
  • init SSE event — frontend now handles parsed.type === 'init' to seed liveProgress from authoritative server state
  • O(1) dedup — replaced liveTestCases.find() with a Map<key, index> for O(1) duplicate detection
  • Retry loop — reporter retries re-queued pendingEvents up to 3 times before finalising the run
  • Upload + streaming runupload.post.ts reads testRunId from the form and, when present, attaches reports to the existing run instead of creating a duplicate
  • Single-instance docsrun-events.ts now has a JSDoc note about the in-memory limitation and Redis suggestion
  • Streaming tests — new streaming-api.spec.ts covers the full lifecycle: /start, /events (batching, token validation, Windows paths), /finish (flakyTests=0, duration=0, token rejection), and /stream SSE init event

…tivity

- Add `updatedAt` column to `testRuns` in both SQLite and PostgreSQL schemas
- SQLite migration 0007: ALTER TABLE test_runs ADD updated_at integer
- PG migration 0001: ADD COLUMN IF NOT EXISTS stream_token + updated_at
- Update events.post.ts to touch updatedAt on every test-case event batch
- Add Nitro server plugin that runs at startup and every 5 min to find
  runs stuck in 'running' with no activity for >1 hour and mark them
  'interrupted', clearing the stream token
Copilot AI changed the title feat: implement live streaming of test runs fix: recover stuck running test runs after crash or forced stop May 26, 2026
Copilot finished work on behalf of PhenX May 26, 2026 15:34
Copilot AI changed the title fix: recover stuck running test runs after crash or forced stop feat: live-refresh run lists and animated spinner for in-progress runs May 26, 2026
Copilot finished work on behalf of PhenX May 26, 2026 20:30
Copilot AI changed the title feat: live-refresh run lists and animated spinner for in-progress runs fix: guard EventSource against SSR in test run live stream May 26, 2026
Copilot finished work on behalf of PhenX May 26, 2026 20:36
Copilot AI changed the title fix: guard EventSource against SSR in test run live stream Replace polling with push-based SSE for live run status updates May 26, 2026
Copilot finished work on behalf of PhenX May 26, 2026 20:55
Copilot AI changed the title Replace polling with push-based SSE for live run status updates feat: replace polling with SSE for live dashboard updates + fix SSR crash May 26, 2026
Copilot finished work on behalf of PhenX May 26, 2026 21:03
@PhenX PhenX requested a review from Copilot May 26, 2026 21:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 33 changed files in this pull request and generated 4 comments.

Comment thread reporter/index.js
Comment on lines +478 to +492
if (this.options.uploadTraces) {
let traceCount = 0;
for (const testCase of this.testCases) {
const traceFiles = findTraceFiles(testCase);
for (const tracePath of traceFiles) {
if (fs.existsSync(tracePath)) {
console.log(`[Playwright Dashboard] Adding trace file: ${tracePath}`);
form.append(`trace_${testCase.index}`, fs.createReadStream(tracePath), {
filename: path.basename(tracePath)
});
traceCount++;
}
}
}
console.log(`[Playwright Dashboard] Found ${traceCount} trace files`);
Comment on lines +47 to 60
{
"idx": 6,
"version": "6",
"when": 1779801121713,
"tag": "0006_add_stream_token",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1748273287000,
"tag": "0007_add_updated_at_test_runs",
"breakpoints": true
}
Comment on lines +12 to +27
export function useRunStream(refresh: () => Promise<unknown> | void) {
if (!import.meta.client) return

const eventSource = new EventSource('/api/stream')

eventSource.onmessage = () => {
refresh()
}

eventSource.onerror = () => {
// EventSource will automatically attempt to reconnect on error
}

onUnmounted(() => {
eventSource.close()
})
statusCode: 500,
message: 'Failed to create test run'
})
}
Copilot AI changed the title feat: replace polling with SSE for live dashboard updates + fix SSR crash Replace polling with SSE live updates; fix streaming edge cases May 26, 2026
Copilot finished work on behalf of PhenX May 26, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants