Files
ArtPlayer/test/fast-forward.test.js

230 lines
6.6 KiB
JavaScript

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 { fastForward, Emitter, beginLifecycle, getScope, beginSource } = await loadModules({
fastForward: 'packages/artplayer/src/plugins/fastForward',
Emitter: 'packages/artplayer/src/utils/emitter',
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
beginSource: { file: 'packages/artplayer/src/source/operation', name: 'beginSource' },
})
function fixture(t) {
let id = 0
const timers = new Map()
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))
const classes = new Set()
const video = new EventTarget()
const art = Object.assign(new Emitter(), {
template: { $video: video, $player: { classList: {
add: value => classes.add(value),
remove: value => classes.delete(value),
contains: value => classes.has(value),
} } },
constructor: { FAST_FORWARD_TIME: 100, FAST_FORWARD_VALUE: 3 },
playing: true,
isLock: false,
playbackRate: 1.5,
proxy(target, name, callback) {
target.addEventListener(name, callback)
const remove = () => target.removeEventListener(name, callback)
getScope(art).add(() => {
remove()
})
return remove
},
})
beginLifecycle(art)
beginSource(art)
const plugin = fastForward(art)
const start = (count = 1) => {
const event = new Event('touchstart')
Object.defineProperty(event, 'touches', { value: Array.from({ length: count }, (_, identifier) => ({ identifier })) })
video.dispatchEvent(event)
}
const flush = () => {
const callbacks = [...timers.values()]
timers.clear()
for (const { callback } of callbacks)
callback()
}
return { art, plugin, start, flush, timers, video, scope: getScope(art) }
}
test('fastForward preserves name, state, live constants and previous playback rate', (t) => {
const f = fixture(t)
assert.deepEqual(Object.keys(f.plugin), ['name', 'state'])
assert.equal(f.plugin.name, 'fastForward')
assert.equal(f.plugin.state, false)
f.art.constructor.FAST_FORWARD_TIME = 200
f.start()
assert.equal([...f.timers.values()][0].delay, 200)
f.art.constructor.FAST_FORWARD_VALUE = 4
f.flush()
assert.equal(f.art.playbackRate, 4)
assert.equal(f.plugin.state, true)
f.art.emit('document:touchend', new Event('touchend'))
assert.equal(f.art.playbackRate, 1.5)
assert.equal(f.plugin.state, false)
f.scope.dispose()
})
test('fastForward only schedules a single playing unlocked touch', (t) => {
const f = fixture(t)
f.start(2)
f.art.playing = false
f.start()
f.art.playing = true
f.art.isLock = true
f.start()
assert.equal(f.timers.size, 0)
f.scope.dispose()
})
test('repeated touch starts do not leave timers or overwrite the original rate', (t) => {
const f = fixture(t)
f.start()
f.start()
assert.equal(f.timers.size, 1)
f.flush()
f.start(2)
assert.equal(f.art.playbackRate, 1.5)
assert.equal(f.plugin.state, false)
f.scope.dispose()
})
for (const event of ['document:touchmove', 'document:touchend', 'document:touchcancel', 'video:pause', 'destroy']) {
test(`fastForward cancels pending and active presses on ${event}`, (t) => {
const f = fixture(t)
f.start()
const stale = [...f.timers.values()][0].callback
f.art.emit(event, new Event(event))
assert.equal(f.timers.size, 0)
stale()
assert.equal(f.art.playbackRate, 1.5)
f.start()
f.flush()
assert.equal(f.plugin.state, true)
f.art.emit(event, new Event(event))
assert.equal(f.art.playbackRate, 1.5)
assert.equal(f.plugin.state, false)
f.scope.dispose()
})
}
test('native touchcancel, lock, source replacement and destruction release the press', (t) => {
const f = fixture(t)
f.start()
f.video.dispatchEvent(new Event('touchcancel'))
assert.equal(f.timers.size, 0)
f.start()
f.flush()
f.art.isLock = true
f.art.emit('lock', true)
assert.equal(f.art.playbackRate, 1.5)
f.art.isLock = false
f.start()
f.flush()
beginSource(f.art)
assert.equal(f.art.playbackRate, 1.5)
assert.equal(f.plugin.state, false)
f.start()
f.flush()
f.scope.dispose()
assert.equal(f.art.playbackRate, 1.5)
assert.equal(f.plugin.state, false)
f.start()
f.art.emit('lock', false)
assert.equal(f.timers.size, 0)
})
test('destruction inside the playback rate setter cannot leave an active class', (t) => {
const f = fixture(t)
let rate = 1.5
Object.defineProperty(f.art, 'playbackRate', {
get: () => rate,
set(value) {
rate = value
if (value === 3)
f.scope.dispose()
},
})
f.start()
f.flush()
assert.equal(rate, 1.5)
assert.equal(f.plugin.state, false)
assert.equal(f.timers.size, 0)
})
test('failed rate assignments release press state and preserve the thrown value', (t) => {
const f = fixture(t)
const failure = new Error('rate refused')
Object.defineProperty(f.art, 'playbackRate', {
get: () => 1.5,
set(value) {
if (value === 3)
throw failure
},
})
f.start()
assert.throws(f.flush, error => error === failure)
assert.equal(f.plugin.state, false)
assert.equal(f.timers.size, 0)
f.scope.dispose()
})
test('source replacement cancels pending work and callback-time flags are rechecked', (t) => {
const f = fixture(t)
f.start()
const stale = [...f.timers.values()][0].callback
beginSource(f.art)
assert.equal(f.timers.size, 0)
stale()
assert.equal(f.art.playbackRate, 1.5)
f.start()
f.art.playing = false
f.flush()
assert.equal(f.plugin.state, false)
f.art.playing = true
f.start()
f.art.isLock = true
f.flush()
assert.equal(f.art.playbackRate, 1.5)
assert.equal(f.plugin.state, false)
f.scope.dispose()
})
test('a new press started during rate restoration supersedes the outer restart', (t) => {
const f = fixture(t)
f.start()
f.flush()
let rate = 3
let reentered = false
Object.defineProperty(f.art, 'playbackRate', {
get: () => rate,
set(value) {
rate = value
if (value === 1.5 && !reentered) {
reentered = true
f.start()
f.flush()
}
},
})
f.start()
assert.equal(f.timers.size, 0)
assert.equal(f.plugin.state, true)
assert.equal(rate, 3)
f.art.emit('document:touchend', new Event('touchend'))
assert.equal(rate, 1.5)
assert.equal(f.plugin.state, false)
f.scope.dispose()
})