Files
ArtPlayer/test/pointer-events.test.js
T

162 lines
5.9 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 { setImmediate } from 'node:timers/promises'
import { loadModules } from './helpers/load.js'
const { clickInit, hoverInit, moveInit, Emitter, beginLifecycle, getScope } = await loadModules({
clickInit: 'packages/artplayer/src/events/clickInit',
hoverInit: 'packages/artplayer/src/events/hoverInit',
moveInit: 'packages/artplayer/src/events/moveInit',
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() {
const classes = new Set()
const $player = { classList: { add: name => classes.add(name), remove: name => classes.delete(name) } }
const $video = {}
const calls = []
const callbacks = new Map()
const art = Object.assign(new Emitter(), {
template: { $player, $video },
constructor: { DBCLICK_TIME: 300, DBCLICK_FULLSCREEN: true, MOBILE_CLICK_PLAY: false, MOBILE_DBCLICK_PLAY: true },
isFocus: false,
isInput: false,
isLock: false,
fullscreen: false,
toggle: () => calls.push('toggle'),
})
beginLifecycle(art)
const events = {
proxy(target, name, callback) {
callbacks.set(name, callback)
return () => callbacks.delete(name)
},
hover(target, enter, leave) {
callbacks.set('mouseenter', enter)
callbacks.set('mouseleave', leave)
},
}
clickInit(art, events)
hoverInit(art, events)
moveInit(art, events)
return { art, classes, calls, callbacks, dispose: () => getScope(art).dispose() }
}
test('click grouping keeps the inclusive threshold, third-click reset and live settings', (t) => {
const f = fixture()
let now = 0
t.mock.method(Date, 'now', () => now)
f.art.on('click', () => f.calls.push('click'))
f.art.on('dblclick', () => f.calls.push('dblclick'))
const click = f.callbacks.get('click')
click(new Event('click'))
now = 300
click(new Event('click'))
assert.equal(f.art.fullscreen, true)
now = 301
click(new Event('click'))
now = 602
click(new Event('click'))
f.art.constructor.DBCLICK_FULLSCREEN = false
click(new Event('click'))
assert.equal(f.art.fullscreen, true)
assert.deepEqual(f.calls, ['click', 'toggle', 'dblclick', 'click', 'toggle', 'click', 'toggle', 'dblclick'])
f.dispose()
})
for (const name of ['click', 'dblclick']) {
test(`destruction during ${name} stops the remaining playback or fullscreen action`, () => {
const f = fixture()
f.art.constructor.DBCLICK_TIME = 60000
f.art.on(name, f.dispose)
f.callbacks.get('click')(new Event('click'))
if (name === 'dblclick')
f.callbacks.get('click')(new Event('click'))
assert.equal(f.art.fullscreen, false)
assert.deepEqual(f.calls, name === 'click' ? [] : ['toggle'])
f.callbacks.get('click')(new Event('click'))
assert.deepEqual(f.calls, name === 'click' ? [] : ['toggle'])
})
}
test('click callback errors retain identity and stop the default action', () => {
const f = fixture()
const failure = new Error('caller failed')
const callback = () => {
throw failure
}
f.art.on('click', callback)
assert.throws(() => f.callbacks.get('click')(new Event('click')), error => error === failure)
assert.deepEqual(f.calls, [])
f.dispose()
})
test('click-triggered playback rejection remains handled internally', async () => {
const f = fixture()
f.art.toggle = () => Promise.reject(new DOMException('interrupted', 'AbortError'))
f.callbacks.get('click')(new Event('click'))
await setImmediate()
f.dispose()
})
test('nested clicks preserve the historical synchronous grouping and event identity', (t) => {
const f = fixture()
t.mock.method(Date, 'now', () => 100)
const first = new Event('click')
const nested = new Event('click')
f.art.on('click', (event) => {
assert.equal(event, first)
f.calls.push('click')
f.callbacks.get('click')(nested)
})
f.art.on('dblclick', event => f.calls.push(event === nested ? 'nested' : 'next'))
f.callbacks.get('click')(first)
assert.equal(f.art.fullscreen, true)
f.callbacks.get('click')(new Event('click'))
assert.equal(f.art.fullscreen, false)
assert.deepEqual(f.calls, ['click', 'nested', 'toggle', 'next'])
f.dispose()
})
test('focus flags retain input-only semantics and disposal removes only owned subscriptions', () => {
const f = fixture()
const events = []
f.art.on('focus', event => events.push(event))
const input = { tagName: 'INPUT' }
const event = { target: input, composedPath: () => [input, f.art.template.$player] }
f.art.emit('document:click', event)
assert.equal(f.art.isInput, true)
assert.equal(f.art.isFocus, true)
assert.equal(events[0], event)
input.tagName = 'TEXTAREA'
f.art.emit('document:contextmenu', event)
assert.equal(f.art.isInput, false)
let external = 0
f.art.on('document:click', () => external++)
f.dispose()
f.art.emit('document:click', { target: null, composedPath: () => [] })
assert.equal(f.art.isFocus, true)
assert.equal(external, 1)
assert.equal(events.length, 2)
})
test('hover and move preserve class-before-event ordering and reject stale callbacks', () => {
const f = fixture()
const states = []
const event = new Event('mousemove')
f.art.on('hover', (state, original) => states.push([state, original, f.classes.has('art-hover')]))
f.art.on('mousemove', original => states.push(['move', original]))
f.callbacks.get('mouseenter')(event)
f.callbacks.get('mousemove')(event)
f.callbacks.get('mouseleave')(event)
assert.deepEqual(states, [[true, event, true], ['move', event], [false, event, false]])
f.dispose()
f.callbacks.get('mouseenter')(event)
f.callbacks.get('mousemove')(event)
assert.equal(f.classes.size, 0)
assert.equal(states.length, 3)
})