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) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-03-28 14:10:51 +01:00
committed by GitHub
parent b136a66d12
commit 7492251e7e
7 changed files with 30 additions and 9 deletions

View File

@@ -28,8 +28,10 @@ var (
CfgProvider ConfigProvider CfgProvider ConfigProvider
IsWindows bool IsWindows bool
// IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing // IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
// TODO: this is only a temporary solution, we should make the test code more reliable // * 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 IsInTesting = false
) )
@@ -57,6 +59,10 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
return currentUser, runUser == currentUser return currentUser, runUser == currentUser
} }
func IsInE2eTesting() bool {
return os.Getenv("GITEA_TEST_E2E") == "true"
}
// PrepareAppDataPath creates app data directory if necessary // PrepareAppDataPath creates app data directory if necessary
func PrepareAppDataPath() error { func PrepareAppDataPath() error {
// FIXME: There are too many calls to MkdirAll in old code. It is incorrect. // FIXME: There are too many calls to MkdirAll in old code. It is incorrect.

View File

@@ -1740,7 +1740,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/swagger.v1.json", SwaggerV1Json) m.Get("/swagger.v1.json", SwaggerV1Json)
} }
if !setting.IsProd { if !setting.IsProd || setting.IsInE2eTesting() {
m.Group("/devtest", func() { m.Group("/devtest", func() {
m.Any("", devtest.List) m.Any("", devtest.List)
m.Any("/fetch-action-test", devtest.FetchActionTest) m.Any("/fetch-action-test", devtest.FetchActionTest)

View File

@@ -3,7 +3,7 @@
<div class="tw-grid tw-grid-cols-3 tw-gap-4"> <div class="tw-grid tw-grid-cols-3 tw-gap-4">
<div> <div>
<h2>Relative (auto)</h2> <h2>Relative (auto)</h2>
<div>now: <relative-time datetime="{{.TimeNow.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div> <div>now: <relative-time data-testid="relative-time-now" datetime="{{.TimeNow.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div>
<div>3m ago: <relative-time datetime="{{.TimePast3m.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div> <div>3m ago: <relative-time datetime="{{.TimePast3m.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div>
<div>3h ago: <relative-time datetime="{{.TimePast3h.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div> <div>3h ago: <relative-time datetime="{{.TimePast3h.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div>
<div>1d ago: <relative-time datetime="{{.TimePast1d.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div> <div>1d ago: <relative-time datetime="{{.TimePast1d.Format "2006-01-02T15:04:05Z07:00"}}"></relative-time></div>

View File

@@ -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);
});

View File

@@ -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(); 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) { export async function logout(page: Page) {
await page.context().clearCookies(); // workaround issues related to fomantic dropdown await page.context().clearCookies(); // workaround issues related to fomantic dropdown
await page.goto('/'); await page.goto('/');

View File

@@ -43,6 +43,7 @@ LEVEL = Warn
EOF EOF
export GITEA_WORK_DIR="$WORK_DIR" export GITEA_WORK_DIR="$WORK_DIR"
export GITEA_TEST_E2E=true
# Start Gitea server # Start Gitea server
echo "Starting Gitea server on port $FREE_PORT (workdir: $WORK_DIR)..." echo "Starting Gitea server on port $FREE_PORT (workdir: $WORK_DIR)..."

View File

@@ -259,12 +259,12 @@ class RelativeTime extends HTMLElement {
get #lang(): string { get #lang(): string {
const lang = this.closest('[lang]')?.getAttribute('lang'); const lang = this.closest('[lang]')?.getAttribute('lang');
if (!lang) return navigator.language; if (lang) {
try { try {
return new Intl.Locale(lang).toString(); return new Intl.Locale(lang).toString();
} catch { } catch { /* invalid locale, fall through */ }
return navigator.language;
} }
return navigator.language ?? 'en';
} }
get second(): 'numeric' | '2-digit' | undefined { get second(): 'numeric' | '2-digit' | undefined {