fix(core): [CORE-25] read defaults without navigator

This commit is contained in:
Harvey Zhao committed 2026-09-13 13:53:49 +08:00
1 parent 4a299eb2d4
commit 3b016970f2
26 files changed
+916 -35

No files matched your search

+26
View File
@@ -1,5 +1,31 @@
import { expect, test } from './fixtures.js'
for (const locale of ['en-US', 'zh-CN']) {
test.describe(`default language ${locale}`, () => {
test.use({ locale })
for (const core of ['published', 'candidate']) {
test(`${core}: language defaults follow the browser and explicit language still overrides`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const container = document.createElement('div')
document.body.append(container)
const { Artplayer } = window
const initial = new Artplayer({ container })
const observed = { browser: navigator.language, defaults: Artplayer.option.lang, instance: initial.option.lang }
initial.destroy()
document.body.append(container)
const explicit = new Artplayer({ container, lang: 'fr' })
observed.explicit = explicit.option.lang
observed.defaultAfterOverride = Artplayer.option.lang
explicit.destroy()
return observed
})
expect(result).toEqual({ browser: locale, defaults: locale.toLowerCase(), instance: locale.toLowerCase(), explicit: 'fr', defaultAfterOverride: locale.toLowerCase() })
})
}
})
}
for (const core of ['published', 'candidate']) {
test(`${core}: expanded option input preserves historical JS forms`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
+49
View File
@@ -40,6 +40,55 @@ test('default values, keys and nested order match the published getter', () => {
assert.deepEqual(Object.keys(current.Artplayer.option), Object.keys(published.Artplayer.option))
})
test('candidate defaults are readable without navigator and keep independent option containers', () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
try {
for (const mode of ['absent', 'undefined', 'null']) {
assert(delete globalThis.navigator)
if (mode !== 'absent')
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: mode === 'null' ? null : undefined })
const first = current.Artplayer.option
const second = current.Artplayer.option
assert.equal(first.lang, undefined, mode)
assert(Object.hasOwn(first, 'lang'))
assert.notEqual(first, second)
assert.notEqual(first.subtitle, second.subtitle)
assert.deepEqual(first.plugins, [])
assert.equal(first.subtitle.onVttLoad('text'), 'text')
assert.throws(() => resolveOption({ container: '#player' }, first), /option.lang/)
assert.equal(resolveOption({ container: '#player', lang: 'en' }, first).lang, 'en')
}
}
finally {
if (descriptor)
Object.defineProperty(globalThis, 'navigator', descriptor)
else
delete globalThis.navigator
}
})
test('candidate defaults read the current browser language on each access without caching it', () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
try {
let language = 'EN-us'
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: {
get language() {
return language
},
} })
for (const value of ['EN-us', 'zh-CN', '']) {
language = value
assert.equal(current.Artplayer.option.lang, value.toLowerCase())
}
}
finally {
if (descriptor)
Object.defineProperty(globalThis, 'navigator', descriptor)
else
delete globalThis.navigator
}
})
for (const [name, resolve, Artplayer] of [['published', oldResolve, published.Artplayer], ['workspace', resolveOption, current.Artplayer]]) {
test(`resolved options preserve unknown fields, merge ownership and collection item identity: ${name}`, () => {
const callback = () => {}
+6 -5
View File
@@ -47,11 +47,12 @@ the actual child version. Reports and failure logs are `runtime-node-<version>.*
the same package output directory. Old reports without source/toolchain fields must
be rebuilt. CI restores canonical Node before running browser tools.
CORE-25 remains an explicit defect: both published and candidate static defaults throw
without navigator. The fixture tests that exact failure on every runtime, then uses an
identical controlled language for default-value comparison and restores the global.
`knownRuntimeBlockers=1` prevents strict release success; ordinary observation tests
passing do not mean the defect is fixed. See [Node evidence](../../refactor/changes/2026-09-13-CI-01-node-consumers.md).
CORE-25 fixed candidate static defaults without navigator. The fixture preserves the
published ReferenceError while requiring candidate main/legacy/ESM defaults to succeed,
then uses an identical controlled language for comparison and restores the global.
The fixture now reports zero known runtime blockers; it is still only core/chapter
acceptance, not full release readiness. See [the fix](../../refactor/changes/2026-09-13-CORE-25-defaults-ssr.md)
and the earlier [Node evidence](../../refactor/changes/2026-09-13-CI-01-node-consumers.md).
The optional core `artplayer/runtime` entry has eight additional strict consumer
groups: TS 5.1.6 and 5.9.3 each check Node10 CommonJS, NodeNext CJS/ESM and Bundler.
+15 -4
View File
@@ -189,10 +189,21 @@ const vm = require('node:vm');
const defaultNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
try {
assert(delete globalThis.navigator)
// CORE-25 tracks this reproduced published and candidate failure separately.
assert.throws(() => core.option, { name: 'ReferenceError', message: 'navigator is not defined' })
observations.defaultsWithoutNavigator = { name: 'ReferenceError', message: 'navigator is not defined', historical: !!expected.baseline, resolved: false }
check('SSR.defaults-missing-navigator-known-failure', true)
if (expected.baseline) {
assert.throws(() => core.option, { name: 'ReferenceError', message: 'navigator is not defined' })
observations.defaultsWithoutNavigator = { name: 'ReferenceError', message: 'navigator is not defined', historical: true, resolved: false }
check('SSR.defaults-missing-navigator-known-failure', true)
}
else {
const defaults = core.option
assert.equal(defaults.lang, undefined)
assert(Object.hasOwn(defaults, 'lang'))
assert.notEqual(core.option, defaults)
for (const Class of [esm.default, legacy.default, require('artplayer/legacy')])
assert.equal(Class.option.lang, undefined)
observations.defaultsWithoutNavigator = { historical: false, resolved: true, ownLang: true, langType: typeof defaults.lang }
check('SSR.defaults-without-navigator', true)
}
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { language: 'en-US' } })
observations.api = { static: shape(core), prototype: shape(core.prototype), emitter: shape(core.Emitter.prototype), factory: shape(chapter), defaults: JSON.parse(JSON.stringify(core.option, (_, value) => typeof value === 'function' ? '$function' : value)) }
check('API.defaults-controlled-language', observations.api.defaults.lang === 'en-us')
+5
View File
@@ -4,6 +4,11 @@ import resolveOption from '../../packages/artplayer/src/option/resolve'
const input: Option = { container: '#player', url: 'video.mp4', subtitle: { encoding: 'utf-8' } }
const resolved: ResolvedOption = resolveOption(input, createDefaults())
const defaultLanguage: string | undefined = createDefaults().lang
const validatedLanguage: string = resolved.lang.toLowerCase()
void [defaultLanguage, validatedLanguage]
// @ts-expect-error Unvalidated defaults may have no browser language.
createDefaults().lang.toLowerCase()
resolved.subtitle.onVttLoad(resolved.subtitle.url)
resolved.thumbnails.column.toFixed()
resolved.volume.toFixed()