mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 19:51:20 -08:00
refactor(core): CORE-17 type input events and own interaction lifetimes
This commit is contained in:
1 parent
88aef830df
commit
a842058136
72 files changed
+31156
-1219
No files matched your search
@@ -0,0 +1,223 @@
|
||||
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 } from './helpers/load.js'
|
||||
|
||||
const { resizeInit, viewInit, updateInit, inViewport, Emitter, beginLifecycle, getScope } = await loadModules({
|
||||
resizeInit: 'packages/artplayer/src/events/resizeInit',
|
||||
viewInit: 'packages/artplayer/src/events/viewInit',
|
||||
updateInit: 'packages/artplayer/src/events/updateInit',
|
||||
inViewport: { file: 'packages/artplayer/src/events/viewport', name: 'inViewport' },
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
|
||||
function fixture(t) {
|
||||
const timers = new Map()
|
||||
const frames = new Map()
|
||||
let id = 0
|
||||
t.mock.method(globalThis, 'setTimeout', (callback, delay) => {
|
||||
const key = ++id
|
||||
timers.set(key, { callback, delay })
|
||||
return key
|
||||
})
|
||||
t.mock.method(globalThis, 'clearTimeout', key => timers.delete(key))
|
||||
for (const [name, value] of Object.entries({
|
||||
requestAnimationFrame(callback) {
|
||||
const key = ++id
|
||||
frames.set(key, callback)
|
||||
return key
|
||||
},
|
||||
cancelAnimationFrame: key => frames.delete(key),
|
||||
})) {
|
||||
const before = Object.getOwnPropertyDescriptor(globalThis, name)
|
||||
Object.defineProperty(globalThis, name, { configurable: true, value })
|
||||
t.after(() => before ? Object.defineProperty(globalThis, name, before) : Reflect.deleteProperty(globalThis, name))
|
||||
}
|
||||
const orientation = new EventTarget()
|
||||
orientation.onchange = null
|
||||
const document = { defaultView: { innerHeight: 100, innerWidth: 200, screen: { orientation } }, documentElement: { clientHeight: 80, clientWidth: 160 } }
|
||||
const rect = { top: 0, left: 0, width: 20, height: 20 }
|
||||
const container = { ownerDocument: document, getBoundingClientRect: () => rect }
|
||||
const calls = []
|
||||
const art = Object.assign(new Emitter(), {
|
||||
template: { $player: { ownerDocument: document }, $container: container },
|
||||
constructor: { RESIZE_TIME: 10, SCROLL_TIME: 20, SCROLL_GAP: 0, USE_RAF: true },
|
||||
option: { autoSize: true, autoMini: true },
|
||||
aspectRatio: '16:9',
|
||||
state: 'standard',
|
||||
autoSize: () => calls.push('autoSize'),
|
||||
mini: false,
|
||||
playing: false,
|
||||
notice: { show: 'before' },
|
||||
})
|
||||
beginLifecycle(art)
|
||||
const scope = getScope(art)
|
||||
const registry = { proxy(target, name, callback) {
|
||||
target.addEventListener(name, callback)
|
||||
scope.add(() => {
|
||||
target.removeEventListener(name, callback)
|
||||
})
|
||||
} }
|
||||
const flushTimers = () => {
|
||||
const pending = [...timers.values()]
|
||||
timers.clear()
|
||||
for (const timer of pending)
|
||||
timer.callback()
|
||||
}
|
||||
const frame = () => {
|
||||
const [key, callback] = frames.entries().next().value
|
||||
frames.delete(key)
|
||||
callback(0)
|
||||
}
|
||||
return { art, scope, registry, calls, timers, frames, frame, flushTimers, orientation, rect, document }
|
||||
}
|
||||
|
||||
test('resize coalesces with live delay, preserves normal effects and cancels on destruction', (t) => {
|
||||
const f = fixture(t)
|
||||
resizeInit(f.art, f.registry)
|
||||
f.art.on('resize', () => f.calls.push('resize'))
|
||||
f.art.emit('window:resize', new Event('resize'))
|
||||
f.art.emit('window:orientationchange', new Event('orientationchange'))
|
||||
assert.equal(f.timers.size, 1)
|
||||
assert.equal([...f.timers.values()][0].delay, 10)
|
||||
f.art.constructor.RESIZE_TIME = 30
|
||||
f.art.emit('window:resize', new Event('resize'))
|
||||
assert.equal([...f.timers.values()][0].delay, 30)
|
||||
f.flushTimers()
|
||||
assert.deepEqual(f.calls, ['autoSize', 'resize'])
|
||||
assert.equal(f.art.aspectRatio, '16:9')
|
||||
assert.equal(f.art.notice.show, '')
|
||||
f.art.emit('window:resize', new Event('resize'))
|
||||
f.scope.dispose()
|
||||
assert.equal(f.timers.size, 0)
|
||||
f.art.emit('window:resize', new Event('resize'))
|
||||
assert.equal(f.timers.size, 0)
|
||||
})
|
||||
|
||||
test('orientation event capability does not depend on onchange being assigned', (t) => {
|
||||
const f = fixture(t)
|
||||
resizeInit(f.art, f.registry)
|
||||
f.orientation.dispatchEvent(new Event('change'))
|
||||
assert.equal(f.timers.size, 1)
|
||||
f.flushTimers()
|
||||
assert.deepEqual(f.calls, ['autoSize'])
|
||||
f.scope.dispose()
|
||||
f.orientation.dispatchEvent(new Event('change'))
|
||||
assert.equal(f.timers.size, 0)
|
||||
})
|
||||
|
||||
test('destroy inside auto-size prevents later ratio and notice writes', (t) => {
|
||||
const f = fixture(t)
|
||||
let ratioWrites = 0
|
||||
Object.defineProperty(f.art, 'aspectRatio', { get: () => '16:9', set: () => ratioWrites++ })
|
||||
f.art.autoSize = () => f.scope.dispose()
|
||||
resizeInit(f.art, f.registry)
|
||||
f.art.emit('resize')
|
||||
assert.equal(ratioWrites, 0)
|
||||
assert.equal(f.art.notice.show, 'before')
|
||||
})
|
||||
|
||||
test('view preserves leading-only throttle, initial delay and live gap while cleaning its timer', (t) => {
|
||||
const f = fixture(t)
|
||||
const seen = []
|
||||
viewInit(f.art)
|
||||
f.art.on('view', value => seen.push(value))
|
||||
f.art.constructor.SCROLL_TIME = 99
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
assert.deepEqual(seen, [true])
|
||||
assert.equal([...f.timers.values()][0].delay, 20)
|
||||
f.rect.top = 130
|
||||
f.flushTimers()
|
||||
assert.deepEqual(seen, [true])
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
assert.deepEqual(seen, [true, false])
|
||||
assert.equal(f.art.mini, true)
|
||||
f.flushTimers()
|
||||
f.art.constructor.SCROLL_GAP = 50
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
assert.deepEqual(seen, [true, false, true])
|
||||
f.scope.dispose()
|
||||
assert.equal(f.timers.size, 0)
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
assert.deepEqual(seen, [true, false, true])
|
||||
})
|
||||
|
||||
test('view callback failure remains synchronous and does not lock the throttle', (t) => {
|
||||
const f = fixture(t)
|
||||
viewInit(f.art)
|
||||
const error = new Error('view failed')
|
||||
f.art.once('view', () => {
|
||||
throw error
|
||||
})
|
||||
assert.throws(() => f.art.emit('window:scroll', new Event('scroll')), actual => actual === error)
|
||||
assert.equal(f.timers.size, 0)
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
assert.equal(f.timers.size, 1)
|
||||
f.scope.dispose()
|
||||
})
|
||||
|
||||
test('nested leading view dispatch keeps its timing and destroy prevents reset timers', (t) => {
|
||||
const f = fixture(t)
|
||||
viewInit(f.art)
|
||||
let views = 0
|
||||
f.art.on('view', () => views++)
|
||||
f.art.once('view', () => f.art.emit('window:scroll', new Event('scroll')))
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
assert.equal(views, 2)
|
||||
assert.equal(f.timers.size, 2)
|
||||
f.flushTimers()
|
||||
f.art.once('view', () => f.scope.dispose())
|
||||
f.art.emit('window:scroll', new Event('scroll'))
|
||||
assert.equal(f.timers.size, 0)
|
||||
})
|
||||
|
||||
test('RAF keeps the initial playing emission and one frame while cancelling reentrant destruction', (t) => {
|
||||
const f = fixture(t)
|
||||
let frames = 0
|
||||
f.art.playing = true
|
||||
f.art.on('raf', () => frames++)
|
||||
updateInit(f.art)
|
||||
assert.equal(frames, 1)
|
||||
assert.equal(f.frames.size, 1)
|
||||
f.art.playing = false
|
||||
f.frame()
|
||||
assert.equal(frames, 1)
|
||||
assert.equal(f.frames.size, 1)
|
||||
f.art.playing = true
|
||||
f.art.once('raf', () => f.scope.dispose())
|
||||
f.frame()
|
||||
assert.equal(frames, 2)
|
||||
assert.equal(f.frames.size, 0)
|
||||
})
|
||||
|
||||
test('RAF remains opt-in and explicit destroy notification can cancel an idle frame', (t) => {
|
||||
const f = fixture(t)
|
||||
f.art.constructor.USE_RAF = false
|
||||
updateInit(f.art)
|
||||
assert.equal(f.frames.size, 0)
|
||||
f.art.constructor.USE_RAF = true
|
||||
updateInit(f.art)
|
||||
assert.equal(f.frames.size, 1)
|
||||
f.art.emit('destroy')
|
||||
assert.equal(f.frames.size, 0)
|
||||
f.scope.dispose()
|
||||
})
|
||||
|
||||
test('viewport follows owner document changes and preserves edge/gap arithmetic', (t) => {
|
||||
const f = fixture(t)
|
||||
const container = f.art.template.$container
|
||||
f.rect.top = 150
|
||||
assert.equal(inViewport(container, 50), true)
|
||||
f.rect.top = 151
|
||||
assert.equal(inViewport(container, 50), false)
|
||||
container.ownerDocument = { defaultView: null, documentElement: { clientHeight: 200, clientWidth: 200 } }
|
||||
assert.equal(inViewport(container, 0), true)
|
||||
f.rect.left = 301
|
||||
assert.equal(inViewport(container, 50), false)
|
||||
f.rect.left = 300
|
||||
assert.equal(inViewport(container, 50), true)
|
||||
f.scope.dispose()
|
||||
})
|
||||
Reference in new issue
Block a user