mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 22:01:20 -08:00
refactor(core): CORE-12 type template resources and verify bundled notices
This commit is contained in:
1 parent
b8039ad1f1
commit
9eb7c945ca
101 files changed
+7363
-664
No files matched your search
@@ -0,0 +1,139 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: native fullscreen uses a real gesture when the engine supports it`, async ({ page }, testInfo) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const capability = await page.evaluate(() => ({
|
||||
native: Boolean(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled),
|
||||
video: Boolean(window.art.video.webkitSupportsFullscreen),
|
||||
}))
|
||||
await testInfo.attach('native-fullscreen-capability', { contentType: 'application/json', body: JSON.stringify(capability) })
|
||||
if (!capability.native) {
|
||||
expect(await page.evaluate(() => Boolean(document.fullscreenElement || document.webkitFullscreenElement))).toBe(false)
|
||||
return
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
window.fullscreenEvents = []
|
||||
window.art.on('fullscreen', value => window.fullscreenEvents.push(value))
|
||||
document.querySelector('#play').onclick = () => {
|
||||
window.art.fullscreen = true
|
||||
}
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => {
|
||||
const art = window.art
|
||||
return art.fullscreen && (document.fullscreenElement || document.webkitFullscreenElement) === art.template.$player && art.template.$player.classList.contains('art-fullscreen')
|
||||
})).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreen = false
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => !window.art.fullscreen && !window.art.template.$player.classList.contains('art-fullscreen'))).toBe(true)
|
||||
expect(await page.evaluate(() => window.fullscreenEvents)).toEqual([true, false])
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core}: SSR mounting preserves nodes, listeners, selectors and style ownership`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.querySelector('.player')
|
||||
container.innerHTML = window.Artplayer.html
|
||||
const original = Array.from(container.querySelectorAll('*'))
|
||||
const video = container.querySelector('video')
|
||||
const player = container.firstElementChild
|
||||
let clicks = 0
|
||||
player.addEventListener('contract', () => clicks++)
|
||||
const style = document.getElementById('artplayer-style')
|
||||
const art = new window.Artplayer({ container, url: '', useSSR: true, backdrop: true })
|
||||
const { query } = art.template
|
||||
player.dispatchEvent(new Event('contract'))
|
||||
const mounted = {
|
||||
retained: original.every(node => container.contains(node)),
|
||||
media: art.video === video,
|
||||
query: query('.art-video') === video && query('.absent') === null,
|
||||
backdrop: player.classList.contains('art-backdrop'),
|
||||
track: art.template.$track === video.querySelector('track'),
|
||||
clicks,
|
||||
style: style.textContent === window.Artplayer.STYLE,
|
||||
}
|
||||
art.destroy(false)
|
||||
return { ...mounted, retainedOnDestroy: container.contains(player), destroyed: player.classList.contains('art-destroy'), styleRetained: document.getElementById('artplayer-style') === style }
|
||||
})
|
||||
expect(result).toEqual({ retained: true, media: true, query: true, backdrop: true, track: true, clicks: 1, style: true, retainedOnDestroy: true, destroyed: true, styleRetained: true })
|
||||
})
|
||||
|
||||
test(`${core}: proxy replacement preserves callback and node identity`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.querySelector('.player')
|
||||
container.innerHTML = window.Artplayer.html
|
||||
const track = container.querySelector('track')
|
||||
const originalVideo = container.querySelector('video')
|
||||
const video = document.createElement('video')
|
||||
video.className = 'before'
|
||||
let callback
|
||||
let context
|
||||
let count
|
||||
const art = new window.Artplayer({ container, url: '', useSSR: true, proxy(art) {
|
||||
callback = art
|
||||
context = this
|
||||
count = arguments.length
|
||||
return video
|
||||
} })
|
||||
const result = { callback: callback === art && context === art, count, media: art.video === video, className: video.className, retainedTrack: art.template.$track === track, detachedTrack: !container.contains(track), detachedVideo: !container.contains(originalVideo) }
|
||||
art.destroy()
|
||||
return { ...result, empty: container.innerHTML === '' }
|
||||
})
|
||||
expect(result).toEqual({ callback: true, count: 1, media: true, className: 'art-video', retainedTrack: true, detachedTrack: true, detachedVideo: true, empty: true })
|
||||
})
|
||||
|
||||
test(`${core}: icon access preserves descriptors and moves custom DOM into fresh wrappers`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const node = document.createElement('b')
|
||||
node.textContent = 'custom'
|
||||
const art = new window.Artplayer({ container: '.player', url: '', icons: { custom: node, markup: '<em>markup</em>' } })
|
||||
const first = art.icons.custom
|
||||
const second = art.icons.custom
|
||||
const builtin = art.icons.play
|
||||
const descriptor = Object.getOwnPropertyDescriptor(art.icons, 'custom')
|
||||
const result = { tag: second.tagName, fresh: first !== second, moved: first.childNodes.length === 0 && second.firstChild === node, classes: second.className, markup: art.icons.markup.innerHTML, builtins: builtin !== art.icons.play, flags: [descriptor.enumerable, descriptor.configurable, descriptor.set === undefined], alias: window['artplayer-i18n-zh-cn'].Play }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ tag: 'I', fresh: true, moved: true, classes: 'art-icon art-icon-custom', markup: '<em>markup</em>', builtins: true, flags: [false, false, true], alias: '播放' })
|
||||
})
|
||||
|
||||
test(`${core}: i18n prototype fallback difference and custom keys are explicit`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const art = new window.Artplayer({ container: '.player', url: '', lang: 'en' })
|
||||
const inherited = typeof art.i18n.get('constructor')
|
||||
art.i18n.update({ en: { constructor: 'custom', Play: '' } })
|
||||
const result = { inherited, custom: art.i18n.get('constructor'), empty: art.i18n.get('Play'), missing: art.i18n.get('Not translated') }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ inherited: core === 'candidate' ? 'string' : 'function', custom: 'custom', empty: 'Play', missing: 'Not translated' })
|
||||
})
|
||||
|
||||
test(`${core}: fullscreen web retains DOM and CSS hooks through entry and exit`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const art = new window.Artplayer({ container: '.player', url: '' })
|
||||
const player = art.template.$player
|
||||
const parent = player.parentNode
|
||||
const events = []
|
||||
art.on('fullscreenWeb', value => events.push(value))
|
||||
art.fullscreenWeb = true
|
||||
const entered = art.fullscreenWeb && player.classList.contains('art-fullscreen-web')
|
||||
art.fullscreenWeb = false
|
||||
const exited = !art.fullscreenWeb && !player.classList.contains('art-fullscreen-web')
|
||||
const identity = art.template.$player === player && player.parentNode === parent
|
||||
art.destroy()
|
||||
return { entered, exited, identity, events }
|
||||
})
|
||||
expect(result).toEqual({ entered: true, exited: true, identity: true, events: [true, false] })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { checkCoreVendors } from '../refactor/scripts/core-vendor.mjs'
|
||||
|
||||
test('copied core libraries match fixed references plus reviewed local adaptations', async () => {
|
||||
const evidence = await checkCoreVendors()
|
||||
assert.equal(evidence.sources.length, 4)
|
||||
})
|
||||
|
||||
const methods = [
|
||||
['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange'],
|
||||
['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange'],
|
||||
['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange'],
|
||||
['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange'],
|
||||
['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange'],
|
||||
]
|
||||
const source = fs.readFileSync(new URL('../packages/artplayer/src/libs/screenfull.js', import.meta.url), 'utf8').replace('export default screenfull', 'globalThis.result = screenfull')
|
||||
|
||||
for (const [request, exit, element, enabled, change] of methods) {
|
||||
test(`screenfull preserves ${request} mapping, options, promise completion and listeners`, async () => {
|
||||
const listeners = new Map()
|
||||
const options = { navigationUI: 'hide' }
|
||||
const document = {
|
||||
[element]: null,
|
||||
[enabled]: true,
|
||||
[exit]() {
|
||||
this[element] = null
|
||||
listeners.get(change)?.()
|
||||
return Promise.resolve()
|
||||
},
|
||||
addEventListener(name, callback) { listeners.set(name, callback) },
|
||||
removeEventListener(name, callback) {
|
||||
if (listeners.get(name) === callback)
|
||||
listeners.delete(name)
|
||||
},
|
||||
}
|
||||
document.documentElement = {
|
||||
[request](actual) {
|
||||
assert.equal(actual, options)
|
||||
document[element] = this
|
||||
listeners.get(change)?.()
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
const context = { document, Promise }
|
||||
vm.runInNewContext(source, context)
|
||||
const screenfull = context.result
|
||||
assert.equal(screenfull.isEnabled, true)
|
||||
assert.equal(screenfull.element, null)
|
||||
assert.equal(await screenfull.request(undefined, options), undefined)
|
||||
assert.equal(screenfull.element, document.documentElement)
|
||||
assert.equal(screenfull.isFullscreen, true)
|
||||
assert.equal(listeners.size, 0)
|
||||
assert.equal(await screenfull.toggle(), undefined)
|
||||
assert.equal(screenfull.isFullscreen, false)
|
||||
assert.equal(listeners.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('screenfull import remains inert without document and retains local disabled surface', () => {
|
||||
const context = {}
|
||||
vm.runInNewContext(source, context)
|
||||
assert.equal(typeof context.result.request, 'function')
|
||||
assert.equal(context.result.raw, false)
|
||||
const browser = { document: {} }
|
||||
vm.runInNewContext(source, browser)
|
||||
assert.equal(browser.result.isEnabled, false)
|
||||
assert.equal(browser.result.element, undefined)
|
||||
})
|
||||
@@ -63,14 +63,25 @@ const vm = require('node:vm');
|
||||
check('SSR.constructor-browser-only', observations.constructorErrors.length === 6)
|
||||
observations.languages = []
|
||||
const languages = fs.readdirSync(path.join(root, 'node_modules/artplayer/dist/i18n')).filter(file => file.endsWith('.mjs')).map(file => file.slice(0, -4)).sort()
|
||||
assert.deepEqual(languages, ['ar', 'cs', 'es', 'fa', 'fr', 'id', 'pl', 'ru', 'tr', 'vi', 'zh-tw'])
|
||||
assert.deepEqual(fs.readdirSync(path.join(root, 'node_modules/artplayer/dist/i18n')).sort(), languages.flatMap(name => [`${name}.js`, `${name}.mjs`]).sort())
|
||||
for (const name of languages) {
|
||||
const cjs = require(`artplayer/i18n/${name}`)
|
||||
const module = await import(`artplayer/i18n/${name}`)
|
||||
globalThis.window = {}
|
||||
let module
|
||||
try {
|
||||
module = await import(`artplayer/i18n/${name}`)
|
||||
assert.equal(globalThis.window[`artplayer-i18n-${name}`], module.default)
|
||||
}
|
||||
finally {
|
||||
delete globalThis.window
|
||||
}
|
||||
assert.deepEqual(module.default, cjs)
|
||||
const globalName = `artplayerI18n${name.replace(/(^|-)([a-z])/g, (_, _prefix, char) => char.toUpperCase())}`
|
||||
const context = vm.createContext({})
|
||||
const context = vm.createContext({ window: {} })
|
||||
vm.runInContext(load(`artplayer/dist/i18n/${name}.js`), context, { timeout: 2000 })
|
||||
assert.equal(JSON.stringify(context[globalName]), JSON.stringify(cjs))
|
||||
assert.equal(context.window[`artplayer-i18n-${name}`], context[globalName])
|
||||
observations.languages.push({ name, globalName, keys: Object.keys(cjs).sort(), play: cjs.Play })
|
||||
}
|
||||
check('DIST.i18n-cjs-esm-global', languages.length > 0)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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 { I18n, html } = await loadModules({
|
||||
I18n: 'packages/artplayer/src/i18n/index',
|
||||
html: 'packages/artplayer/src/template/html',
|
||||
})
|
||||
|
||||
test('language selection, extension and fallback preserve case and truthy semantics', () => {
|
||||
const host = { option: { lang: 'ZH-CN', i18n: { 'zh-cn': { Play: 'custom' }, 'extension': { Play: 'extension', Empty: '' } } } }
|
||||
const i18n = new I18n(host)
|
||||
assert.equal(i18n.get('Play'), 'custom')
|
||||
assert.equal(i18n.get('Pause'), '暂停')
|
||||
assert.equal(i18n.get('Missing'), 'Missing')
|
||||
host.option.lang = 'EXTENSION'
|
||||
assert.equal(i18n.update({ extension: { Pause: 'pause' } }), undefined)
|
||||
assert.equal(i18n.get('Play'), 'extension')
|
||||
assert.equal(i18n.get('Pause'), 'pause')
|
||||
assert.equal(i18n.get('Empty'), 'Empty')
|
||||
assert.deepEqual(host.option.i18n.extension, { Play: 'extension', Empty: '' })
|
||||
host.option.lang = 'UNKNOWN'
|
||||
i18n.init()
|
||||
assert.equal(i18n.get('Play'), 'Play')
|
||||
})
|
||||
|
||||
test('prototype names fall back as strings and explicit own translations remain usable', () => {
|
||||
const host = { option: { lang: 'constructor', i18n: {} } }
|
||||
const i18n = new I18n(host)
|
||||
for (const key of ['constructor', 'toString', '__proto__'])
|
||||
assert.equal(i18n.get(key), key)
|
||||
i18n.update(JSON.parse('{"constructor":{"constructor":"ctor","toString":"string","__proto__":"proto"}}'))
|
||||
assert.equal(i18n.get('constructor'), 'ctor')
|
||||
assert.equal(i18n.get('toString'), 'string')
|
||||
assert.equal(i18n.get('__proto__'), 'proto')
|
||||
assert.equal(Object.getPrototypeOf(i18n.languages), Object.prototype)
|
||||
assert.equal(Object.getPrototypeOf(i18n.language), Object.prototype)
|
||||
})
|
||||
|
||||
test('language updates remain isolated between instances', () => {
|
||||
const host = { option: { lang: 'zh-cn', i18n: {} } }
|
||||
const first = new I18n(host)
|
||||
const second = new I18n(host)
|
||||
first.update({ 'zh-cn': { Play: 'first' } })
|
||||
assert.equal(first.get('Play'), 'first')
|
||||
assert.equal(second.get('Play'), '播放')
|
||||
})
|
||||
|
||||
test('SSR imports expose exact historical template and retain browser-only construction', async () => {
|
||||
const { Artplayer: published } = await loadPublishedCore()
|
||||
const { default: candidate } = await loadPackage('artplayer')
|
||||
assert.equal(html, candidate.html)
|
||||
assert.equal(candidate.html.replace(candidate.version, '<version>'), published.html.replace(published.version, '<version>'))
|
||||
for (const Artplayer of [published, candidate]) {
|
||||
assert.throws(() => new Artplayer({ container: '#player', url: '' }), {
|
||||
message: 'Artplayer can only be used in the browser environment',
|
||||
})
|
||||
assert.equal(Artplayer.instances.length, 0)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { TemplateHost } from '../../packages/artplayer/src/template/types'
|
||||
import I18n from '../../packages/artplayer/src/i18n'
|
||||
import Template from '../../packages/artplayer/src/template'
|
||||
|
||||
interface Host extends TemplateHost<Host> {
|
||||
custom: string
|
||||
}
|
||||
declare const host: Host
|
||||
host.option.proxy = function (art) {
|
||||
const same = this satisfies Host
|
||||
const label: string = art.custom + same.custom
|
||||
return document.createElement(label ? 'canvas' : 'video')
|
||||
}
|
||||
const template = new Template(host)
|
||||
const node: HTMLVideoElement | HTMLCanvasElement | null = template.$video
|
||||
const track: HTMLTrackElement | null = template.query<HTMLTrackElement>('track')
|
||||
// @ts-expect-error A bare canvas is not a complete native video.
|
||||
const video: HTMLVideoElement = template.$video
|
||||
const i18n = new I18n({ option: { lang: 'custom', i18n: { custom: { Feature: 'feature' } } } })
|
||||
const message: string = i18n.get('Feature')
|
||||
// @ts-expect-error A message dictionary is not a numeric table.
|
||||
i18n.update({ custom: { Feature: 42 } })
|
||||
void [node, track, video, message]
|
||||
Reference in new issue
Block a user