From 9cf7ea8a90bd94f4d253e4dc53b6bfe6ebbeeba6 Mon Sep 17 00:00:00 2001 From: Epid Date: Thu, 2 Apr 2026 02:40:11 +0300 Subject: [PATCH] fix(stopwatch): show icon dynamically in tabs loaded before stopwatch starts The stopwatch navbar icon and popup were only rendered by the server when a stopwatch was already active at page load. If a tab was opened before the stopwatch started, `initStopwatch()` found no `.active-stopwatch` element in the DOM, returned early, and never registered a SharedWorker listener. As a result the WebSocket push from the stopwatch notifier had nowhere to land and the icon never appeared. Fix by always rendering both the icon anchor and the popup skeleton in the navbar (hidden with `tw-hidden` when no stopwatch is active). `initStopwatch()` can now set up the SharedWorker in every tab, and `updateStopwatchData` can call `showElem`/`hideElem` as stopwatch state changes arrive in real time. Also add `onShow` to `createTippy` so the popup content is re-cloned from the (JS-updated) original each time the tooltip opens, keeping it current even when the stopwatch was started after page load. Add a new e2e test (`stopwatch appears via real-time push`) that verifies the icon appears after `apiStartStopwatch` is called with the page already loaded. --- templates/base/head_navbar.tmpl | 46 +++++++++++++-------------- templates/base/head_navbar_icons.tmpl | 4 +-- tests/e2e/events.test.ts | 24 +++++++++++++- web_src/js/features/stopwatch.ts | 5 +++ 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 447f78565e..5b8593ec43 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -152,31 +152,29 @@ {{$activeStopwatch := and .PageGlobalData (call .PageGlobalData.GetActiveStopwatch)}} - {{if $activeStopwatch}} -
-
- - {{svg "octicon-issue-opened" 16}} - {{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}} - -
-
- -
-
- -
-
+ {{template "base/head_banner"}} diff --git a/templates/base/head_navbar_icons.tmpl b/templates/base/head_navbar_icons.tmpl index 89b02389fc..e0c997b88a 100644 --- a/templates/base/head_navbar_icons.tmpl +++ b/templates/base/head_navbar_icons.tmpl @@ -3,14 +3,12 @@ {{if and $data $data.IsSigned}}{{/* data may not exist, for example: rendering 503 page before the PageGlobalData middleware */}} {{- $activeStopwatch := call $data.GetActiveStopwatch -}} {{- $notificationUnreadCount := call $data.GetNotificationUnreadCount -}} - {{if $activeStopwatch}} - +
{{svg "octicon-stopwatch"}}
- {{end}}
{{svg "octicon-bell"}} diff --git a/tests/e2e/events.test.ts b/tests/e2e/events.test.ts index 61f1a3c881..bab3430945 100644 --- a/tests/e2e/events.test.ts +++ b/tests/e2e/events.test.ts @@ -29,7 +29,7 @@ test.describe('events', () => { await Promise.all([apiDeleteUser(request, commenter), apiDeleteUser(request, owner)]); }); - test('stopwatch', async ({page, request}) => { + test('stopwatch visible at page load', async ({page, request}) => { const name = `ev-sw-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; const headers = apiUserHeaders(name); @@ -51,6 +51,28 @@ test.describe('events', () => { await apiDeleteUser(request, name); }); + test('stopwatch appears via real-time push', async ({page, request}) => { + const name = `ev-sw-push-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const headers = apiUserHeaders(name); + + await apiCreateUser(request, name); + await apiCreateRepo(request, {name, headers}); + await apiCreateIssue(request, name, name, {title: 'events stopwatch push test', headers}); + + // Login before starting stopwatch — page loads without active stopwatch + await loginUser(page, name); + + const stopwatch = page.locator('.active-stopwatch.not-mobile'); + await expect(stopwatch).toBeHidden(); + + // Start stopwatch after page is loaded — icon should appear via WebSocket push + await apiStartStopwatch(request, name, name, 1, {headers}); + await expect(stopwatch).toBeVisible({timeout: 15000}); + + // Cleanup + await apiDeleteUser(request, name); + }); + test('logout propagation', async ({browser, request}) => { const name = `ev-logout-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index 6fa8fbbdf3..75f6290735 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -34,6 +34,11 @@ export function initStopwatch() { interactive: true, hideOnClick: true, theme: 'default', + onShow(instance) { + // Re-clone so the tooltip always reflects the latest stopwatch state, + // even when the icon became visible via a real-time WebSocket push. + instance.setContent(stopwatchPopup.cloneNode(true) as Element); + }, }); }