From f9819dbb748e4ea219af3ef54a694ded32fb6173 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 20 Jul 2026 10:44:53 +0200 Subject: [PATCH] test(e2e): deterministically wait for event stream in logout propagation test (#38535) The test raced the server-side SSE channel registration: a logout emitted before registration is silently dropped ([example flake](https://github.com/go-gitea/gitea/actions/runs/29699349255/job/88225654080)). The 500ms wait from https://github.com/go-gitea/gitea/pull/37403 only made this unlikely. The server now registers the channel before sending the initial response bytes, so an open connection implies registration. The shared worker forwards the built-in `open` event (replaying it to late-attaching ports via `EventSource.readyState`), the page exposes it as a `data-user-events-connected` attribute, and the test waits for that attribute instead of a fixed timeout. --- routers/web/events/events.go | 4 +++- tests/e2e/events.test.ts | 9 +++------ web_src/js/eventsource.sharedworker.ts | 5 +++++ web_src/js/modules/worker.ts | 3 +++ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/routers/web/events/events.go b/routers/web/events/events.go index 07375f77d50..2bbc326f087 100644 --- a/routers/web/events/events.go +++ b/routers/web/events/events.go @@ -38,7 +38,6 @@ func Events(ctx *context.Context) { // Listen to connection close and un-register messageChan notify := ctx.Done() - ctx.Resp.Flush() shutdownCtx := graceful.GetManager().ShutdownContext() @@ -57,11 +56,14 @@ func Events(ctx *context.Context) { } } + // send the initial response bytes only after registering messageChan, so a client whose + // connection is open can rely on receiving all subsequent events if _, err := ctx.Resp.Write([]byte("\n")); err != nil { log.Error("Unable to write to EventStream: %v", err) unregister() return } + ctx.Resp.Flush() timer := time.NewTicker(30 * time.Second) diff --git a/tests/e2e/events.test.ts b/tests/e2e/events.test.ts index 54b4774a5cc..2276e7e6187 100644 --- a/tests/e2e/events.test.ts +++ b/tests/e2e/events.test.ts @@ -66,12 +66,9 @@ test.describe('events', () => { // Verify page2 is logged in await expect(page2.getByRole('link', {name: 'Sign In'})).toBeHidden(); - // Give page2's SharedWorker time to register its SSE connection on the - // server — otherwise the logout event can race the connection and be - // silently dropped. See https://github.com/go-gitea/gitea/pull/37403 - // In the future, we can set an attribute to HTML page when the connection is established, - // then here we can just wait for that attribute (it should also work for the planned WebSocket SharedWorker) - await page2.waitForTimeout(500); // eslint-disable-line playwright/no-wait-for-timeout + // Wait until the server has registered page2's event stream, otherwise the logout + // event can race the connection and be silently dropped. + await expect(page2.locator('html[data-user-events-connected]')).toBeAttached(); // Logout from page1 — this sends a logout event to all tabs await page1.goto('/user/logout'); diff --git a/web_src/js/eventsource.sharedworker.ts b/web_src/js/eventsource.sharedworker.ts index 816cd7020a8..64da37c7038 100644 --- a/web_src/js/eventsource.sharedworker.ts +++ b/web_src/js/eventsource.sharedworker.ts @@ -26,6 +26,11 @@ class Source { type: 'status', message: `registered to ${this.url}`, }); + + // replay the "open" event to ports attaching to an already-open source + if (this.eventSource?.readyState === EventSource.OPEN) { + port.postMessage({type: 'open'}); + } } deregister(port: MessagePort) { diff --git a/web_src/js/modules/worker.ts b/web_src/js/modules/worker.ts index eb951d94e94..9d97e938efb 100644 --- a/web_src/js/modules/worker.ts +++ b/web_src/js/modules/worker.ts @@ -54,6 +54,9 @@ export class UserEventsSharedWorker { } else if (event.data.type === 'close') { this.sharedWorker.port.postMessage({type: 'close'}); this.sharedWorker.port.close(); + } else if (event.data.type === 'open') { + // e2e tests wait for this attribute to know events cannot be missed anymore + document.documentElement.setAttribute('data-user-events-connected', 'true'); } listener(event); });