mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-27 09:41:20 -08:00
refactor(types): CORE-07 reconcile public declarations and preserve legacy consumers
This commit is contained in:
1 parent
ee859e4804
commit
f9707bad71
34 files changed
+2806
-85
No files matched your search
@@ -0,0 +1,97 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: plugin, toggle, setting and static runtime return contracts`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
Object.assign(container.style, { width: '640px', height: '360px' })
|
||||
document.body.append(container)
|
||||
window.art = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true, setting: true })
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const values = await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const sync = art.plugins.add(() => ({ name: 'syncContract' }))
|
||||
const promise = art.plugins.add(async () => ({ name: 'asyncContract' }))
|
||||
const before = art.plugins.asyncContract === undefined
|
||||
const asyncResult = await promise
|
||||
const failure = new Error('plugin rejection contract')
|
||||
const rejected = await art.plugins.add(() => Promise.reject(failure)).catch(error => error === failure)
|
||||
const item = { name: 'returnContract', html: 'Initial' }
|
||||
const added = art.setting.add(item)
|
||||
const updated = art.setting.update({ name: 'returnContract', html: 'Updated' })
|
||||
const identity = art.setting.find('returnContract') === item
|
||||
const html = item.$html.textContent
|
||||
const removed = art.setting.remove('returnContract')
|
||||
const missing = art.setting.find('returnContract')
|
||||
art.notice.show = 'Visible notice'
|
||||
const notice = art.notice.show
|
||||
const emitter = new art.constructor.Emitter('ignored')
|
||||
let received
|
||||
const ctx = { value: 2 }
|
||||
const chained = emitter.on('contract', function (payload) {
|
||||
received = this.value + payload
|
||||
}, ctx).emit('contract', 3) === emitter
|
||||
return {
|
||||
syncRegistry: sync === art.plugins,
|
||||
syncThen: typeof sync.then,
|
||||
asyncPromise: promise instanceof Promise,
|
||||
asyncPending: before,
|
||||
asyncRegistry: asyncResult === art.plugins,
|
||||
rejected,
|
||||
addedItem: added === item,
|
||||
updatedItem: updated === item,
|
||||
foundItem: identity,
|
||||
html,
|
||||
removedType: typeof removed,
|
||||
missing,
|
||||
notice,
|
||||
chained,
|
||||
received,
|
||||
}
|
||||
})
|
||||
expect(values).toEqual({ syncRegistry: true, syncThen: 'undefined', asyncPromise: true, asyncPending: true, asyncRegistry: true, rejected: true, addedItem: true, updatedItem: true, foundItem: true, html: 'Updated', removedType: 'undefined', missing: null, notice: true, chained: true, received: 5 })
|
||||
expect(await page.evaluate(async () => {
|
||||
const result = window.art.toggle()
|
||||
const promise = result instanceof Promise
|
||||
await result
|
||||
return promise
|
||||
})).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
|
||||
expect(await page.evaluate(() => typeof window.art.toggle())).toBe('undefined')
|
||||
expect(await page.evaluate(() => window.art.playing)).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core}: native subtitle update events carry cue arrays`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
Object.assign(container.style, { width: '640px', height: '360px' })
|
||||
document.body.append(container)
|
||||
const art = window.art = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true, subtitle: { url: '/test/declaration-cues.vtt' } })
|
||||
window.cueEvents = []
|
||||
for (const name of ['subtitleBeforeUpdate', 'subtitleAfterUpdate']) {
|
||||
art.on(name, (cues) => {
|
||||
window.cueEvents.push({ name, array: Array.isArray(cues), length: cues.length, native: cues[0] instanceof VTTCue, same: cues[0] === art.subtitle.textTrack.cues[0], text: cues[0]?.text })
|
||||
})
|
||||
}
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady && window.art.subtitle.cues.length === 1)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.seek = 1
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.cueEvents.some(event => event.name === 'subtitleAfterUpdate'))).toBe(true)
|
||||
const events = await page.evaluate(() => window.cueEvents)
|
||||
for (const name of ['subtitleBeforeUpdate', 'subtitleAfterUpdate'])
|
||||
expect(events.find(event => event.name === name)).toEqual({ name, array: true, length: 1, native: true, same: true, text: 'Declaration cue' })
|
||||
expect(await page.evaluate(() => window.art.template.$subtitle.textContent.trim())).toBe('Declaration cue')
|
||||
await page.evaluate(() => {
|
||||
const url = window.art.template.$track.src
|
||||
window.art.destroy()
|
||||
// Explicit test teardown; subtitle URL ownership is still tracked by CORE-15.
|
||||
URL.revokeObjectURL(url)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
WEBVTT
|
||||
|
||||
00:00:00.000 --> 00:00:04.000
|
||||
Declaration cue
|
||||
@@ -1,17 +1,25 @@
|
||||
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 }) => {
|
||||
test(`${core}: expanded option input preserves historical JS forms`, 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 }
|
||||
const art = new window.Artplayer({
|
||||
container,
|
||||
controls: [{ name: 'numeric', html: 42, position: 'left' }],
|
||||
layers: [{ name: 'numeric', html: 43 }],
|
||||
contextmenu: [{ name: 'numeric', html: 44 }],
|
||||
})
|
||||
const result = { url: art.option.url, html: art.controls.numeric.textContent, layer: art.layers.numeric.textContent, menu: art.contextmenu.numeric.textContent }
|
||||
art.controls.update({ name: 'numeric', html: 45, position: 'left' })
|
||||
result.updated = art.controls.numeric.textContent
|
||||
result.added = art.layers.add({ name: 'added', html: 46 }).textContent
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ url: '', html: '42' })
|
||||
expect(result).toEqual({ url: '', html: '42', layer: '43', menu: '44', updated: '45', added: '46' })
|
||||
})
|
||||
|
||||
test(`${core}: configuration errors occur before mounting and preserve allocation timing`, async ({ page }) => {
|
||||
|
||||
@@ -60,6 +60,7 @@ for (const extension of ['mp4', 'webm']) {
|
||||
}
|
||||
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
|
||||
add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' })
|
||||
function send(req, res, bytes, filename) {
|
||||
res.setHeader('Content-Type', mime[path.extname(filename)] || 'application/octet-stream')
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Emitter, Option, OptionInput, SubtitleUpdateEvents } from 'artplayer'
|
||||
import Artplayer from 'artplayer'
|
||||
|
||||
const historical: Option = { container: '#player', url: '' }
|
||||
const requiredUrl: string = historical.url
|
||||
const art = new Artplayer(historical)
|
||||
const instanceUrl: string = art.option.url
|
||||
const defaultUrl: string = Artplayer.option.url
|
||||
const input: OptionInput = {
|
||||
container: '#player',
|
||||
controls: [{ html: 42, position: 'left' }],
|
||||
layers: [{ html: 43 }],
|
||||
contextmenu: [{ html: 44 }],
|
||||
}
|
||||
void new Artplayer(input, function (instance) {
|
||||
const receiverId: number = this.id
|
||||
const argument: Artplayer = instance
|
||||
void [receiverId, argument]
|
||||
})
|
||||
void new Artplayer({ container: '#player' })
|
||||
art.controls.update({ name: 'numeric', html: 45, position: 'left' })
|
||||
art.layers.add(() => ({ html: 46 }))
|
||||
// @ts-expect-error URL must still be a string when provided.
|
||||
void new Artplayer({ container: '#player', url: 42 })
|
||||
// @ts-expect-error Numeric HTML does not relax other component fields.
|
||||
void new Artplayer({ container: '#player', controls: [{ html: 42, position: 42 }] })
|
||||
// @ts-expect-error An arbitrary object is not renderable HTML.
|
||||
void new Artplayer({ container: '#player', layers: [{ html: {} }] })
|
||||
const unescaped: string = Artplayer.utils.unescape('&')
|
||||
const error: Error = new Artplayer.utils.ArtPlayerError('message')
|
||||
const timer: Promise<void> = Artplayer.utils.sleep()
|
||||
const target = { value: 1 }
|
||||
const identity: typeof target = Artplayer.utils.def(target, Symbol('key'), { value: 2 })
|
||||
// Existing string-key return assignments remain valid during reconciliation.
|
||||
const oldDef: void = Artplayer.utils.def(target, 'key', { value: 2 })
|
||||
void [requiredUrl, instanceUrl, defaultUrl, unescaped, error, timer, identity, oldDef]
|
||||
|
||||
const emitter: Emitter<{ ready: [number] }> = new Artplayer.Emitter<{ ready: [number] }>('historically-ignored')
|
||||
emitter.on('ready', function (value) {
|
||||
const id: number = this.id
|
||||
const payload: number = value
|
||||
void [id, payload]
|
||||
}, { id: 1 }).once('ready', () => {}).emit('ready', 1).off('ready')
|
||||
// @ts-expect-error Typed event maps reject wrong payloads.
|
||||
emitter.emit('ready', 'bad')
|
||||
// @ts-expect-error Typed event maps reject unknown names.
|
||||
emitter.on('missing', () => {})
|
||||
const open = new Artplayer.Emitter()
|
||||
open.on(Symbol.for('event'), (...values) => values.length).emit(Symbol.for('event'), 1, 'two')
|
||||
const before = (cues: SubtitleUpdateEvents['subtitleBeforeUpdate'][0]) => cues.map(cue => cue.text)
|
||||
art.on('subtitleBeforeUpdate', before).once('subtitleAfterUpdate', before)
|
||||
art.emit('subtitleBeforeUpdate', []).off('subtitleBeforeUpdate', before)
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Option } from 'artplayer'
|
||||
import Artplayer from 'artplayer'
|
||||
|
||||
// These legal historical declarations expose known runtime mismatches.
|
||||
// Keep them compiling until a compatible, reviewed resolution exists.
|
||||
const option: Option = { container: '#player', url: '' }
|
||||
const requiredUrl: string = option.url
|
||||
const art = new Artplayer(option)
|
||||
const registration: Promise<Artplayer['plugins']> = art.plugins.add(() => ({ name: 'legacy' }))
|
||||
const toggle: void = art.toggle()
|
||||
const debounced: number = Artplayer.utils.debounce(() => 1, 10, {})()
|
||||
const throttled: number = Artplayer.utils.throttle(() => 1, 10)()
|
||||
const descriptor: void = Artplayer.utils.def({}, 'value', { value: 1 })
|
||||
const updated: Artplayer['setting'] = art.setting.update({ name: 'entry', html: 'Entry' })
|
||||
const removed: Artplayer['setting'] = art.setting.remove('entry')
|
||||
art.on('subtitleBeforeUpdate', (cue) => {
|
||||
const historicalCue: VTTCue = cue
|
||||
void historicalCue
|
||||
})
|
||||
void [requiredUrl, registration, toggle, debounced, throttled, descriptor, updated, removed]
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Events } from 'artplayer'
|
||||
import type { Events, Emitter as PublicEmitter } from 'artplayer'
|
||||
import Emitter from '../../packages/artplayer/src/utils/emitter'
|
||||
|
||||
const symbol = Symbol('message')
|
||||
@@ -12,6 +12,7 @@ class TypedEmitter extends Emitter<Messages> {
|
||||
count = 0
|
||||
}
|
||||
const events = new TypedEmitter()
|
||||
const publicShape: PublicEmitter<Messages> = events
|
||||
const context = { total: 0 }
|
||||
const chain: TypedEmitter = events.on('value', function (count, label) {
|
||||
this.total += count
|
||||
@@ -39,4 +40,4 @@ events.on('ready', contextListener, { total: 'zero' })
|
||||
function contextListener(this: { total: number }) {
|
||||
return this.total
|
||||
}
|
||||
void chain
|
||||
void [chain, publicShape]
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Option, ResolvedOption } from '../../packages/artplayer/src/option/types'
|
||||
import type { Option, OptionInput, ResolvedOption } from '../../packages/artplayer/src/option/types'
|
||||
import createDefaults from '../../packages/artplayer/src/option/defaults'
|
||||
import resolveOption from '../../packages/artplayer/src/option/resolve'
|
||||
|
||||
@@ -25,9 +25,8 @@ 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]
|
||||
const omittedUrl: OptionInput = { container: '#player' }
|
||||
const numericHtml: NonNullable<OptionInput['controls']>[number] = { html: 42, position: 'left' }
|
||||
const expanded = resolveOption({ ...omittedUrl, controls: [numericHtml] }, createDefaults())
|
||||
const defaultUrl: string = expanded.url
|
||||
void [omittedUrl, numericHtml, defaultUrl]
|
||||
Reference in new issue
Block a user