mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 17:01:20 -08:00
refactor(core): CORE-05 separate typed defaults and resolved options
This commit is contained in:
1 parent
4eec8112fa
commit
97d320bd74
27 files changed
+2278
-240
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: historical JS option forms remain accepted despite declaration gaps`, 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 art = new window.Artplayer({ container, controls: [{ name: 'numeric', html: 42, position: 'left' }] })
|
||||
const result = { url: art.option.url, html: art.controls.numeric.textContent }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ url: '', html: '42' })
|
||||
})
|
||||
|
||||
test(`${core}: configuration errors occur before mounting and preserve allocation timing`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const { Artplayer } = window
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = '<span>before</span>'
|
||||
document.body.append(container)
|
||||
let proxyCalls = 0
|
||||
let error
|
||||
try {
|
||||
void new Artplayer({ container, url: '', volume: 'loud', proxy() {
|
||||
proxyCalls++
|
||||
return document.createElement('video')
|
||||
} })
|
||||
}
|
||||
catch (actual) { error = { name: actual.name, message: actual.message } }
|
||||
const untouched = {
|
||||
html: container.innerHTML,
|
||||
id: container.getAttribute('data-art-id'),
|
||||
proxyCalls,
|
||||
count: Artplayer.instances.length,
|
||||
}
|
||||
const art = new Artplayer({ container, url: '' })
|
||||
const allocatedId = art.id
|
||||
art.destroy()
|
||||
return { error, untouched, allocatedId }
|
||||
})
|
||||
expect(result).toEqual({ error: { name: 'Error', message: '[Type Error]: \'option.volume\' require \'number\' type, but got \'string\'' }, untouched: { html: '<span>before</span>', id: null, proxyCalls: 0, count: 0 }, allocatedId: 2 })
|
||||
})
|
||||
|
||||
test(`${core}: container identity, partial defaults, custom fields and normal callbacks`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { Artplayer } = window
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const extension = { custom: true }
|
||||
let customCalls = 0
|
||||
let callbackShape = false
|
||||
let readyShape = false
|
||||
const customType = function (video, url, art) {
|
||||
customCalls++
|
||||
callbackShape = this === art && video === art.video
|
||||
video.src = url
|
||||
}
|
||||
const input = { container, url: '/test/pattern.mp4', subtitle: { style: { color: 'red' } }, customType: { mp4: customType }, extension, muted: true }
|
||||
let art
|
||||
await new Promise((resolve) => {
|
||||
art = new Artplayer(input, function (player) {
|
||||
readyShape = this === player && player === art
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const defaults = Artplayer.option
|
||||
const result = {
|
||||
sameContainer: art.option.container === container,
|
||||
sameExtension: art.option.extension === extension,
|
||||
customCallback: art.option.customType.mp4 === customType,
|
||||
customCalls,
|
||||
callbackShape,
|
||||
readyShape,
|
||||
encoding: art.option.subtitle.encoding,
|
||||
color: art.option.subtitle.style.color,
|
||||
columns: art.option.thumbnails.column,
|
||||
inputSubtitleKeys: Object.keys(input.subtitle),
|
||||
freshDefaults: defaults !== Artplayer.option,
|
||||
preload: Artplayer.option.moreVideoAttr.preload,
|
||||
expectedPreload: Artplayer.utils.isSafari ? 'auto' : 'metadata',
|
||||
}
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
const { preload, expectedPreload, ...behavior } = result
|
||||
expect(preload).toBe(expectedPreload)
|
||||
expect(behavior).toEqual({ sameContainer: true, sameExtension: true, customCallback: true, customCalls: 1, callbackShape: true, readyShape: true, encoding: 'utf-8', color: 'red', columns: 10, inputSubtitleKeys: ['style'], freshDefaults: true })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { loadModules, loadPackage, loadPublishedCore } from './helpers/load.js'
|
||||
|
||||
const { resolveOption } = await loadModules({ resolveOption: 'packages/artplayer/src/option/resolve' })
|
||||
const published = await loadPublishedCore({ navigator: { language: navigator.language, userAgent: navigator.userAgent } })
|
||||
const current = {
|
||||
name: 'workspace',
|
||||
Artplayer: (await loadPackage('artplayer')).default,
|
||||
}
|
||||
function oldResolve(input, defaults) {
|
||||
const merged = published.Artplayer.utils.mergeDeep(defaults, input)
|
||||
merged.container = input.container
|
||||
return published.Artplayer.validator(merged, published.Artplayer.scheme)
|
||||
}
|
||||
|
||||
for (const { name, Artplayer } of [published, current]) {
|
||||
test(`option defaults are fresh, keep all fields and callback shape: ${name}`, () => {
|
||||
const first = Artplayer.option
|
||||
const second = Artplayer.option
|
||||
assert.notEqual(first, second)
|
||||
for (const key of ['plugins', 'layers', 'controls', 'settings', 'quality', 'highlight', 'contextmenu', 'thumbnails', 'subtitle', 'moreVideoAttr', 'icons', 'i18n', 'customType', 'cssVar'])
|
||||
assert.notEqual(first[key], second[key], key)
|
||||
assert.notEqual(first.subtitle.style, second.subtitle.style)
|
||||
assert.notEqual(first.subtitle.onVttLoad, second.subtitle.onVttLoad)
|
||||
assert.equal(first.subtitle.onVttLoad(' text '), ' text ')
|
||||
assert.equal(first.subtitle.onVttLoad.length, 1)
|
||||
assert.equal(first.subtitle.onVttLoad.name, 'onVttLoad')
|
||||
assert(Object.hasOwn(first, 'proxy'))
|
||||
assert.equal(first.proxy, undefined)
|
||||
assert.equal(first.lang, navigator.language.toLowerCase())
|
||||
assert.equal(Artplayer.scheme.layers[0], Artplayer.scheme.settings[0])
|
||||
assert.equal(Artplayer.scheme.layers[0], Artplayer.scheme.contextmenu[0])
|
||||
})
|
||||
}
|
||||
|
||||
test('default values, keys and nested order match the published getter', () => {
|
||||
assert.equal(JSON.stringify(current.Artplayer.option), JSON.stringify(published.Artplayer.option))
|
||||
assert.deepEqual(Object.keys(current.Artplayer.option), Object.keys(published.Artplayer.option))
|
||||
})
|
||||
|
||||
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 = () => {}
|
||||
const item = { html: 'Quality', url: 'a.mp4' }
|
||||
const extension = { value: 1 }
|
||||
const input = { container: '#player', url: 'video.mp4', subtitle: { style: { color: 'red' } }, thumbnails: { url: 'sprite.jpg' }, quality: [item], customType: { mp4: callback }, extension }
|
||||
const defaults = Artplayer.option
|
||||
const option = resolve(input, defaults)
|
||||
assert.equal(option.container, input.container)
|
||||
assert.notEqual(option, input)
|
||||
assert.notEqual(option.subtitle, input.subtitle)
|
||||
assert.notEqual(option.subtitle.style, input.subtitle.style)
|
||||
assert.equal(option.subtitle.style.color, 'red')
|
||||
assert.equal(option.subtitle.encoding, 'utf-8')
|
||||
assert.equal(option.thumbnails.column, 10)
|
||||
assert.notEqual(option.quality, input.quality)
|
||||
assert.equal(option.quality[0], item)
|
||||
assert.equal(option.customType.mp4, callback)
|
||||
assert.equal(option.extension, extension)
|
||||
assert.deepEqual(Object.keys(input.subtitle), ['style'])
|
||||
assert.deepEqual(Object.keys(defaults.subtitle.style), [])
|
||||
})
|
||||
|
||||
test(`getter reads and inherited container restoration retain their order: ${name}`, () => {
|
||||
const reads = []
|
||||
const input = {
|
||||
get container() {
|
||||
reads.push('container')
|
||||
return '#player'
|
||||
},
|
||||
get url() {
|
||||
reads.push('url')
|
||||
return 'video.mp4'
|
||||
},
|
||||
get volume() {
|
||||
reads.push('volume')
|
||||
return 0.5
|
||||
},
|
||||
}
|
||||
const option = resolve(input, Artplayer.option)
|
||||
assert.deepEqual(reads, ['container', 'url', 'volume', 'container'])
|
||||
assert.equal(option.volume, 0.5)
|
||||
const inherited = Object.create({ container: '#inherited', url: 'ignored.mp4' })
|
||||
inherited.customField = 'own'
|
||||
const result = resolve(inherited, Artplayer.option)
|
||||
assert.equal(result.container, '#inherited')
|
||||
assert.equal(result.url, '')
|
||||
assert.equal(result.customField, 'own')
|
||||
})
|
||||
}
|
||||
|
||||
test('invalid JS input keeps the published validation path, first error and thrown kind', () => {
|
||||
const cases = [
|
||||
{ volume: undefined },
|
||||
{ volume: 'loud', muted: 'yes' },
|
||||
{ subtitle: { encoding: undefined } },
|
||||
{ thumbnails: { number: 'many' } },
|
||||
{ plugins: [42] },
|
||||
{ controls: [{ html: 'Button', position: 'middle' }] },
|
||||
{ quality: [{ html: 'HD', url: 42 }] },
|
||||
{ proxy: null },
|
||||
{ moreVideoAttr: [] },
|
||||
]
|
||||
for (const patch of cases) {
|
||||
const input = { container: '#player', url: 'video.mp4', ...patch }
|
||||
const errors = []
|
||||
for (const [resolve, Artplayer] of [[oldResolve, published.Artplayer], [resolveOption, current.Artplayer]]) {
|
||||
assert.throws(() => resolve(input, Artplayer.option), (error) => {
|
||||
errors.push({ name: error.name, message: error.message })
|
||||
return true
|
||||
})
|
||||
}
|
||||
assert.deepEqual(errors[0], errors[1])
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Option, ResolvedOption } from '../../packages/artplayer/src/option/types'
|
||||
import createDefaults from '../../packages/artplayer/src/option/defaults'
|
||||
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())
|
||||
resolved.subtitle.onVttLoad(resolved.subtitle.url)
|
||||
resolved.thumbnails.column.toFixed()
|
||||
resolved.volume.toFixed()
|
||||
resolved.plugins.push(function (art) {
|
||||
return { name: 'typed', receiverId: this.id, argumentId: art.id }
|
||||
})
|
||||
resolved.proxy = undefined
|
||||
const extended = resolveOption({ container: '#player', url: '', application: { token: 1 } }, createDefaults())
|
||||
const token: number = extended.application.token
|
||||
void token
|
||||
// @ts-expect-error Input remains optional before resolution.
|
||||
input.subtitle.onVttLoad('text')
|
||||
// @ts-expect-error Known fields cannot be widened by generic extension inference.
|
||||
resolveOption({ container: '#player', url: '', volume: 'loud' }, createDefaults())
|
||||
// @ts-expect-error Resolved nested defaults are required.
|
||||
resolved.thumbnails.column = undefined
|
||||
// @ts-expect-error Existing callback return types remain checked.
|
||||
resolved.subtitle.onVttLoad = () => 42
|
||||
// @ts-expect-error Application fields retain their own type.
|
||||
extended.application.token = 'invalid'
|
||||
|
||||
// Historical public declaration gaps; CORE-07 must replace these with positive checks.
|
||||
// @ts-expect-error Runtime accepts an omitted URL, but the current public Option requires it.
|
||||
const omittedUrl: Option = { container: '#player' }
|
||||
// @ts-expect-error Runtime accepts numeric control HTML, but the current public type excludes it.
|
||||
const numericHtml: NonNullable<Option['controls']>[number] = { html: 42, position: 'left' }
|
||||
void [omittedUrl, numericHtml]
|
||||
Reference in new issue
Block a user