fix(locale): gate browser detection on window and tolerate a missing languages list

Node >= 21 exposes a global `navigator` reporting the machine's own language,
so gating detection on `navigator` let a non-browser boot of the client tree
resolve to `en` instead of the documented fallback; `window` is the browser
test. `navigator.languages` is spec-required but absent on some embedders and
older WebViews, where spreading it would throw at boot, so the walk tolerates
its absence and `navigator.language` covers that host.

The per-spec pin boilerplate collapses into one suite-level
`usePinnedBrowserLanguages('zh-CN')`, which owns the rationale in
dsh-client-test-runtime, and the English-browser e2e scenario now clears the
console warnings channel too — its page has no closing inventory spec.
This commit is contained in:
creatixchu
2026-07-31 15:49:59 +08:00
parent cb754a0319
commit e5563ae433
19 changed files with 84 additions and 76 deletions

View File

@@ -38,7 +38,7 @@ export { TestWorkspaces } from './workspaces.ts'
export { conversationSnapshot, workspaceListState } from './fixtures.ts'
export type { SessionBehaviorOverrides, SessionFixture, Stabilizer } from './fixtures.ts'
export { makeTranslate } from './translate.ts'
export { pinBrowserLanguages } from './locale-env.ts'
export { usePinnedBrowserLanguages } from './locale-env.ts'
/** Erased register face for the internal root call (the public declare seam holds the typing). */
type ErasedRegister = (options: object, component: unknown) => () => void

View File

@@ -5,21 +5,25 @@
* the product's Chinese copy states the browser it assumes instead of
* inheriting the machine's.
*/
import { afterEach, beforeEach } from 'vitest'
/**
* Override `navigator.languages`/`navigator.language` for the current spec.
* Pin `navigator.languages`/`navigator.language` for every test in the
* calling file (or describe block), restoring the environment's own values
* afterwards. Call at suite level, like the other vitest hooks.
* @param primary - most preferred BCP 47 tag; also becomes `navigator.language`.
* @param rest - further tags in preference order.
* @returns restore function handing the properties back to the environment.
*/
export function pinBrowserLanguages(primary: string, ...rest: string[]): () => void {
Object.defineProperty(navigator, 'languages', { value: [primary, ...rest], configurable: true })
Object.defineProperty(navigator, 'language', { value: primary, configurable: true })
return () => {
export function usePinnedBrowserLanguages(primary: string, ...rest: string[]): void {
beforeEach(() => {
Object.defineProperty(navigator, 'languages', { value: [primary, ...rest], configurable: true })
Object.defineProperty(navigator, 'language', { value: primary, configurable: true })
})
afterEach(() => {
// Deleting the own properties uncovers the environment's own accessors
// again (Navigator declares both readonly, hence the erased receiver).
const own = navigator as unknown as Record<string, unknown>
delete own.languages
delete own.language
}
})
}