From 7492251e7e09e5526bd7569bddde22a7f0d624b7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 28 Mar 2026 14:10:51 +0100 Subject: [PATCH] Fix relative-time RangeError (#37021) `navigator.language` can be `undefined` in headless browsers (e.g. Playwright Firefox), causing `RangeError: invalid language tag: "undefined"` in `Intl.DateTimeFormat` within the `relative-time` web component. Also adds an e2e test that verifies `relative-time` renders correctly and a shared `assertNoJsError` helper. Bug is als present in https://github.com/github/relative-time-element but (incorrectly) masked there. Fixes: https://github.com/go-gitea/gitea/issues/25324 --------- Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- modules/setting/setting.go | 10 ++++++++-- routers/web/web.go | 2 +- templates/devtest/relative-time.tmpl | 2 +- tests/e2e/relative-time.test.ts | 10 ++++++++++ tests/e2e/utils.ts | 4 ++++ tools/test-e2e.sh | 1 + web_src/js/webcomponents/relative-time.ts | 10 +++++----- 7 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 tests/e2e/relative-time.test.ts diff --git a/modules/setting/setting.go b/modules/setting/setting.go index f2b6274edc..2918ef11a1 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -28,8 +28,10 @@ var ( CfgProvider ConfigProvider IsWindows bool - // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing - // TODO: this is only a temporary solution, we should make the test code more reliable + // IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for: + // * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable) + // * Panic in dev or testing mode to make the problem more obvious and easier to debug + // * Mock some functions or options to make testing easier (eg: session store, time, URL detection, etc.) IsInTesting = false ) @@ -57,6 +59,10 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) { return currentUser, runUser == currentUser } +func IsInE2eTesting() bool { + return os.Getenv("GITEA_TEST_E2E") == "true" +} + // PrepareAppDataPath creates app data directory if necessary func PrepareAppDataPath() error { // FIXME: There are too many calls to MkdirAll in old code. It is incorrect. diff --git a/routers/web/web.go b/routers/web/web.go index 75cc437b43..dec5f73b01 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1740,7 +1740,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/swagger.v1.json", SwaggerV1Json) } - if !setting.IsProd { + if !setting.IsProd || setting.IsInE2eTesting() { m.Group("/devtest", func() { m.Any("", devtest.List) m.Any("/fetch-action-test", devtest.FetchActionTest) diff --git a/templates/devtest/relative-time.tmpl b/templates/devtest/relative-time.tmpl index ff2485ac01..041ce49f09 100644 --- a/templates/devtest/relative-time.tmpl +++ b/templates/devtest/relative-time.tmpl @@ -3,7 +3,7 @@

Relative (auto)

-
now:
+
now:
3m ago:
3h ago:
1d ago:
diff --git a/tests/e2e/relative-time.test.ts b/tests/e2e/relative-time.test.ts new file mode 100644 index 0000000000..a22fad0e4a --- /dev/null +++ b/tests/e2e/relative-time.test.ts @@ -0,0 +1,10 @@ +import {test, expect} from '@playwright/test'; +import {assertNoJsError} from './utils.ts'; + +test('relative-time renders without errors', async ({page}) => { + await page.goto('/devtest/relative-time'); + const relativeTime = page.getByTestId('relative-time-now'); + await expect(relativeTime).toHaveAttribute('data-tooltip-content', /.+/); + await expect(relativeTime).toHaveText('now'); + await assertNoJsError(page); +}); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index aded858600..1c3b33be90 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -104,6 +104,10 @@ export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, pass await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden(); } +export async function assertNoJsError(page: Page) { + await expect(page.locator('.js-global-error')).toHaveCount(0); +} + export async function logout(page: Page) { await page.context().clearCookies(); // workaround issues related to fomantic dropdown await page.goto('/'); diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh index 1ee513c109..e11c788fdf 100755 --- a/tools/test-e2e.sh +++ b/tools/test-e2e.sh @@ -43,6 +43,7 @@ LEVEL = Warn EOF export GITEA_WORK_DIR="$WORK_DIR" +export GITEA_TEST_E2E=true # Start Gitea server echo "Starting Gitea server on port $FREE_PORT (workdir: $WORK_DIR)..." diff --git a/web_src/js/webcomponents/relative-time.ts b/web_src/js/webcomponents/relative-time.ts index fdff909c2f..8a5d988733 100644 --- a/web_src/js/webcomponents/relative-time.ts +++ b/web_src/js/webcomponents/relative-time.ts @@ -259,12 +259,12 @@ class RelativeTime extends HTMLElement { get #lang(): string { const lang = this.closest('[lang]')?.getAttribute('lang'); - if (!lang) return navigator.language; - try { - return new Intl.Locale(lang).toString(); - } catch { - return navigator.language; + if (lang) { + try { + return new Intl.Locale(lang).toString(); + } catch { /* invalid locale, fall through */ } } + return navigator.language ?? 'en'; } get second(): 'numeric' | '2-digit' | undefined {