Replace polling with SSE live updates; fix streaming edge cases#56
Draft
Copilot wants to merge 12 commits into
Draft
Replace polling with SSE live updates; fix streaming edge cases#56Copilot wants to merge 12 commits into
Copilot wants to merge 12 commits into
Conversation
- 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>
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>
There was a problem hiding this comment.
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
runningrun status and astreamTokenfield 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 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 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. | ||
| */ | ||
|
|
Owner
|
@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)
Contributor
Author
All 13 review suggestions have been applied in commit a05d8d2:
|
…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 May 26, 2026
running test runs after crash or forced stop
Copilot
AI
changed the title
fix: recover stuck
feat: live-refresh run lists and animated spinner for in-progress runs
May 26, 2026
running test runs after crash or forced stop
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
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
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
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' | ||
| }) | ||
| } |
…o upload, fix migration timestamp
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Dashboard pages were polling every 5s via
useAutoRefresh, causing periodic freezes. Replaces all polling with a push-based SSE model. Also fixes anEventSource is not definedSSR crash and several streaming-path bugs surfaced in review.Server
/api/stream— new global SSE endpoint; broadcastsrun-started,run-finished,run-submittedevents to all connected dashboard clients with a 15s heartbeatrun-events.ts— extendedRunEventBuswithpublishGlobal/subscribeGlobalon a separateglobalEmitter; added single-instance limitation docsstart.post.ts/finish.post.ts/submit.post.ts/upload.post.ts— each publishes aGlobalRunEventafter mutating run state so connected clients are notified immediatelyClient
useRunStream(refresh)— new composable replacinguseAutoRefresh; opens oneEventSource('/api/stream')per component, callsrefresh()on every message, closes on unmount; guarded byimport.meta.clientto prevent SSR crashpages/index.vue,projects/index.vue,projects/[id]/index.vue,layouts/default.vue,components/ProjectsMenu.vue— swappeduseAutoRefreshforuseRunStreamtest-runs/[id].vue— per-run live view; addedimport.meta.clientguard; handlesinitevent to seed initial state; deduplicates incoming test cases with an O(1)MapBug fixes
reporter/index.js—_uploadFilesForStreamingRunusedtestCase.indexwhich is only assigned in the non-streaminguploadWithFilespath, resulting in all trace form fields namedtrace_undefined; fixed by iterating withentries()and using the loop index directlyupload.post.ts— batch-upload path was not callingpublishGlobal, so uploaded runs never triggered a dashboard refresh_journal.json— migration 0007 had awhentimestamp earlier than 0006; corrected to maintain chronological order