mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-15 09:06:16 -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,125 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core, raf = false) {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate((raf) => {
|
||||
window.Artplayer.SCROLL_TIME = 24731
|
||||
window.Artplayer.RESIZE_TIME = 0
|
||||
window.Artplayer.USE_RAF = raf
|
||||
const orientation = new EventTarget()
|
||||
orientation.onchange = null
|
||||
Object.defineProperty(window.screen, 'orientation', { configurable: true, value: orientation })
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
}, raf)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: actual animation frames follow playback and stop after destruction inside raf`, async ({ page }) => {
|
||||
await setup(page, core, true)
|
||||
await page.evaluate(() => {
|
||||
window.frameCount = 0
|
||||
window.art.on('raf', () => window.frameCount++)
|
||||
return window.art.play()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.frameCount)).toBeGreaterThan(2)
|
||||
await page.evaluate(() => window.art.pause())
|
||||
const paused = await page.evaluate(() => window.frameCount)
|
||||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))
|
||||
expect(await page.evaluate(() => window.frameCount)).toBe(paused)
|
||||
await page.evaluate(() => {
|
||||
window.art.once('raf', () => window.art.destroy())
|
||||
return window.art.play()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isDestroy)).toBe(true)
|
||||
const destroyed = await page.evaluate(() => window.frameCount)
|
||||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))
|
||||
expect(await page.evaluate(() => window.frameCount)).toBe(destroyed)
|
||||
})
|
||||
|
||||
test(`${core}: Events keeps its public fields, prototype and detached native methods`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const events = art.events
|
||||
const target = document.createElement('div')
|
||||
let hits = 0
|
||||
const { proxy, hover } = events
|
||||
const dispose = proxy(target, 'example', () => hits++)
|
||||
hover(target, () => hits++, () => hits++)
|
||||
target.dispatchEvent(new Event('example'))
|
||||
target.dispatchEvent(new Event('mouseenter'))
|
||||
target.dispatchEvent(new Event('mouseleave'))
|
||||
events.remove(dispose)
|
||||
target.dispatchEvent(new Event('example'))
|
||||
const result = { fields: Object.keys(events).sort(), prototype: Object.getOwnPropertyNames(Object.getPrototypeOf(events)).sort(), hits }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ fields: ['bindGlobalEvents', 'destroyEvents', 'hover', 'proxy'], prototype: ['constructor', 'destroy', 'hover', 'proxy', 'remove'], hits: 3 })
|
||||
})
|
||||
|
||||
test(`${core}: screen orientation dispatch works when onchange has no assigned handler`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
let resized = 0
|
||||
art.on('resize', () => resized++)
|
||||
window.screen.orientation.dispatchEvent(new Event('change'))
|
||||
await new Promise(resolve => setTimeout(resolve, 30))
|
||||
art.destroy()
|
||||
return resized
|
||||
})).toBe(core === 'candidate' ? 1 : 0)
|
||||
})
|
||||
|
||||
test(`${core}: destruction releases the scroll throttle reset timer`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const set = window.setTimeout
|
||||
const clear = window.clearTimeout
|
||||
const pending = new Set()
|
||||
window.setTimeout = function (callback, delay, ...args) {
|
||||
const timer = set.call(this, callback, delay, ...args)
|
||||
if (delay === 24731)
|
||||
pending.add(timer)
|
||||
return timer
|
||||
}
|
||||
window.clearTimeout = function (timer) {
|
||||
pending.delete(timer)
|
||||
return clear.call(this, timer)
|
||||
}
|
||||
try {
|
||||
art.emit('window:scroll', new Event('scroll'))
|
||||
const before = pending.size
|
||||
art.destroy()
|
||||
return { before, after: pending.size }
|
||||
}
|
||||
finally {
|
||||
for (const timer of pending)
|
||||
clear.call(window, timer)
|
||||
window.setTimeout = set
|
||||
window.clearTimeout = clear
|
||||
}
|
||||
})).toEqual({ before: 1, after: core === 'candidate' ? 0 : 1 })
|
||||
})
|
||||
|
||||
test(`${core}: view visibility uses the container's current iframe viewport`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const frame = document.createElement('iframe')
|
||||
frame.style.cssText = 'width:200px;height:100px'
|
||||
document.body.append(frame)
|
||||
const container = art.template.$container
|
||||
container.style.cssText = 'position:absolute;top:250px;left:0;width:200px;height:100px'
|
||||
frame.contentDocument.body.append(container)
|
||||
art.events.bindGlobalEvents()
|
||||
const seen = []
|
||||
art.on('view', visible => seen.push(visible))
|
||||
frame.contentWindow.dispatchEvent(new Event('scroll'))
|
||||
art.destroy()
|
||||
frame.remove()
|
||||
return seen
|
||||
})).toEqual([core === 'published'])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core, primeMedia = true) {
|
||||
await page.addInitScript(() => Object.defineProperty(navigator, 'userAgent', { configurable: true, value: 'ArtPlayer Android gesture fixture' }))
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.events = []
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, gesture: true })
|
||||
window.touch = (target, type, points) => {
|
||||
const event = new Event(type, { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(event, 'touches', { value: points.map(([pageX, pageY, identifier = 1]) => ({ pageX, pageY, clientX: pageX, clientY: pageY, identifier })) })
|
||||
target.dispatchEvent(event)
|
||||
return event
|
||||
}
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
if (!primeMedia)
|
||||
return
|
||||
await page.evaluate(() => window.art.play())
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime > 0.1)).toBe(true)
|
||||
await page.evaluate(() => window.art.pause())
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.paused && !window.art.template.$video.seeking)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.currentTime = 2
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => !window.art.template.$video.seeking && Math.abs(window.art.currentTime - 2) < 0.1)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: a gesture before first play still issues the requested seek`, async ({ page }) => {
|
||||
await setup(page, core, false)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const start = art.currentTime
|
||||
const expected = start + art.duration * 0.1 * art.constructor.TOUCH_MOVE_RATIO
|
||||
const requests = []
|
||||
art.on('seek', (time, requested) => requests.push(requested))
|
||||
window.touch(art.template.$video, 'touchstart', [[100, 100]])
|
||||
window.touch(art.template.$video, 'touchmove', [[100 + art.width * 0.1, 100]])
|
||||
art.destroy()
|
||||
return { count: requests.length, valid: Math.abs(requests[0] - expected) < 0.000001 }
|
||||
})).toEqual({ count: 1, valid: true })
|
||||
})
|
||||
|
||||
test(`${core}: progress touch keeps absolute position and bar-before-seek ordering`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const progress = art.template.$progress
|
||||
const rect = progress.getBoundingClientRect()
|
||||
const calls = []
|
||||
art.on('setBar', (kind, value, event) => {
|
||||
if (event?.type === 'touchstart')
|
||||
calls.push(['bar', value, event])
|
||||
})
|
||||
art.on('seek', (time, requested) => calls.push(['seek', requested]))
|
||||
const event = window.touch(progress, 'touchstart', [[rect.left + progress.clientWidth / 2, rect.top]])
|
||||
window.touch(document, 'touchend', [])
|
||||
return { order: calls.map(item => item[0]), percentage: calls[0][1], identity: calls[0][2] === event, requested: calls[1][1], expected: art.duration / 2 }
|
||||
})
|
||||
expect(result.order).toEqual(['bar', 'seek'])
|
||||
expect(result.percentage).toBeCloseTo(0.5, 6)
|
||||
expect(result.identity).toBe(true)
|
||||
expect(result.requested).toBeCloseTo(result.expected, 6)
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeCloseTo(result.expected, 1)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core}: destroy during absolute progress touch stops the following seek`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const progress = art.template.$progress
|
||||
const rect = progress.getBoundingClientRect()
|
||||
let seeks = 0
|
||||
art.on('seek', () => seeks++)
|
||||
art.on('setBar', (kind, value, event) => {
|
||||
if (event?.type === 'touchstart')
|
||||
art.destroy()
|
||||
})
|
||||
window.touch(progress, 'touchstart', [[rect.left + progress.clientWidth / 2, rect.top]])
|
||||
return { seeks, destroyed: art.isDestroy }
|
||||
})).toEqual({ seeks: core === 'candidate' ? 0 : 1, destroyed: true })
|
||||
})
|
||||
|
||||
test(`${core}: normal touch drag keeps seek/bar ordering and original touch identity`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
const calls = []
|
||||
const start = art.currentTime
|
||||
const expected = start + art.duration * 0.2 * art.constructor.TOUCH_MOVE_RATIO
|
||||
art.on('seek', (time, requested) => calls.push(['seek', requested]))
|
||||
art.on('setBar', (kind, percentage, event) => {
|
||||
if (event?.type === 'touchmove')
|
||||
calls.push(['bar', kind, percentage, event])
|
||||
})
|
||||
window.touch(video, 'touchstart', [[100, 100]])
|
||||
const event = window.touch(video, 'touchmove', [[100 + art.width * 0.2, 100]])
|
||||
window.touch(document, 'touchend', [])
|
||||
return { expected, order: calls.map(item => item[0]), requested: calls[0][1], percentage: calls[1][2], identity: calls[1][3] === event }
|
||||
})
|
||||
expect(result.order).toEqual(['seek', 'bar'])
|
||||
expect(result.identity).toBe(true)
|
||||
expect(result.requested).toBeCloseTo(result.expected, 6)
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeCloseTo(result.expected, 1)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
for (const cause of ['cancel', 'lock', 'multitouch', 'identifier']) {
|
||||
test(`${core}: ${cause} invalidates an in-progress gesture`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate((cause) => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
const seeks = []
|
||||
art.on('seek', (time, requested) => seeks.push(requested))
|
||||
window.touch(video, 'touchstart', [[100, 100]])
|
||||
window.touch(video, 'touchmove', [[100 + art.width * 0.1, 100]])
|
||||
if (cause === 'cancel')
|
||||
window.touch(video, 'touchcancel', [])
|
||||
if (cause === 'lock')
|
||||
art.isLock = true
|
||||
if (cause === 'multitouch')
|
||||
window.touch(video, 'touchmove', [[150, 100], [200, 100, 2]])
|
||||
window.touch(video, 'touchmove', [[100 + art.width * 0.3, 100, cause === 'identifier' ? 2 : 1]])
|
||||
art.destroy()
|
||||
return seeks.length
|
||||
}, cause)).toBe(core === 'candidate' ? 1 : 2)
|
||||
})
|
||||
}
|
||||
|
||||
test(`${core}: destroy from a touch seek callback prevents the later progress notification`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
let bars = 0
|
||||
art.on('seek', () => art.destroy())
|
||||
art.on('setBar', (kind, value, event) => {
|
||||
if (event?.type === 'touchmove')
|
||||
bars++
|
||||
})
|
||||
window.touch(video, 'touchstart', [[100, 100]])
|
||||
window.touch(video, 'touchmove', [[100 + art.width * 0.1, 100]])
|
||||
return { bars, destroyed: art.isDestroy }
|
||||
})).toEqual({ bars: core === 'candidate' ? 0 : 1, destroyed: true })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
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)
|
||||
}
|
||||
|
||||
for (const action of ['rebind', 'destroy']) {
|
||||
test(`candidate: ${action} during native global registration cancels the interrupted binding`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate((action) => {
|
||||
const art = window.art
|
||||
const frames = [document.createElement('iframe'), document.createElement('iframe')]
|
||||
frames.forEach(frame => document.body.append(frame))
|
||||
const [interrupted, newest] = frames
|
||||
const target = interrupted.contentDocument
|
||||
const original = target.addEventListener
|
||||
target.addEventListener = function (name, ...args) {
|
||||
if (name === 'click') {
|
||||
if (action === 'destroy')
|
||||
art.destroy()
|
||||
else
|
||||
art.events.bindGlobalEvents({ document: newest.contentDocument, window: newest.contentWindow })
|
||||
}
|
||||
return original.call(this, name, ...args)
|
||||
}
|
||||
const received = []
|
||||
art.on('document:mouseup', event => received.push(event.target === newest.contentDocument ? 'newest' : 'stale'))
|
||||
art.events.bindGlobalEvents({ document: target, window: interrupted.contentWindow })
|
||||
document.dispatchEvent(new Event('mouseup'))
|
||||
for (const frame of frames)
|
||||
frame.contentDocument.dispatchEvent(new frame.contentWindow.Event('mouseup'))
|
||||
if (!art.isDestroy)
|
||||
art.destroy()
|
||||
target.addEventListener = original
|
||||
for (const frame of frames)
|
||||
frame.remove()
|
||||
return { received, remaining: art.events.destroyEvents.size }
|
||||
}, action)).toEqual({ received: action === 'rebind' ? ['newest'] : [], remaining: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: global rebinding forwards original events once and releases the previous document`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const frame = document.createElement('iframe')
|
||||
document.body.append(frame)
|
||||
const next = frame.contentDocument
|
||||
const events = []
|
||||
art.on('document:mouseup', event => events.push(event))
|
||||
const first = new Event('mouseup')
|
||||
document.dispatchEvent(first)
|
||||
const returned = art.events.bindGlobalEvents({ document: next, window: frame.contentWindow })
|
||||
art.events.bindGlobalEvents({ document: next, window: frame.contentWindow })
|
||||
document.dispatchEvent(new Event('mouseup'))
|
||||
const second = new frame.contentWindow.Event('mouseup')
|
||||
next.dispatchEvent(second)
|
||||
art.destroy()
|
||||
next.dispatchEvent(new frame.contentWindow.Event('mouseup'))
|
||||
frame.remove()
|
||||
return { count: events.length, identity: events[0] === first && events[1] === second, returned: typeof returned }
|
||||
})).toEqual({ count: 2, identity: true, returned: 'undefined' })
|
||||
})
|
||||
|
||||
test(`${core}: failed global rebinding keeps the previous document and removes partial new listeners`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const frame = document.createElement('iframe')
|
||||
document.body.append(frame)
|
||||
const next = frame.contentDocument
|
||||
const original = next.addEventListener
|
||||
const failure = new Error('document listener denied')
|
||||
next.addEventListener = function (name, ...args) {
|
||||
if (name === 'keydown')
|
||||
throw failure
|
||||
return original.call(this, name, ...args)
|
||||
}
|
||||
const received = []
|
||||
art.on('document:mouseup', event => received.push(event.target === document ? 'old' : 'new'))
|
||||
let sameError = false
|
||||
try {
|
||||
art.events.bindGlobalEvents({ document: next, window: frame.contentWindow })
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
next.addEventListener = original
|
||||
document.dispatchEvent(new Event('mouseup'))
|
||||
next.dispatchEvent(new frame.contentWindow.Event('mouseup'))
|
||||
art.destroy()
|
||||
frame.remove()
|
||||
return { received, sameError }
|
||||
})).toEqual({ received: core === 'candidate' ? ['old'] : ['new'], sameError: true })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
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)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: mobile keeps keyboard initialization opt-in through the existing init method`, async ({ page }) => {
|
||||
await page.addInitScript(() => Object.defineProperty(navigator, 'userAgent', { configurable: true, value: 'ArtPlayer Android keyboard fixture' }))
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const initial = Object.keys(art.hotkey.keys)
|
||||
let hits = 0
|
||||
art.hotkey.add('KeyK', () => hits++)
|
||||
art.isFocus = true
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyK' }))
|
||||
const before = hits
|
||||
art.hotkey.init()
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyK' }))
|
||||
const result = { initial, before, after: hits }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ initial: [], before: 0, after: 1 })
|
||||
})
|
||||
|
||||
test(`${core}: prototype-named hotkeys do not crash or replace the keys prototype`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const prototype = Object.getPrototypeOf(art.hotkey.keys)
|
||||
const errors = []
|
||||
let hits = 0
|
||||
art.isFocus = true
|
||||
for (const key of ['__proto__', 'constructor', 'toString']) {
|
||||
try {
|
||||
const callback = () => hits++
|
||||
art.hotkey.add(key, callback)
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { code: key, cancelable: true }))
|
||||
art.hotkey.remove(key, callback)
|
||||
}
|
||||
catch {
|
||||
errors.push(key)
|
||||
}
|
||||
}
|
||||
const stable = Object.getPrototypeOf(art.hotkey.keys) === prototype
|
||||
art.destroy()
|
||||
return { errors, hits, stable }
|
||||
})).toEqual({ errors: core === 'candidate' ? [] : ['__proto__', 'constructor', 'toString'], hits: core === 'candidate' ? 3 : 0, stable: true })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: native keyboard play/pause, seek, volume and web-fullscreen Escape retain their behavior', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.art.isFocus = true
|
||||
window.nativeHotkeys = []
|
||||
window.art.on('hotkey', event => window.nativeHotkeys.push(event.code))
|
||||
})
|
||||
await page.keyboard.press('Space')
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing && window.art.currentTime > 0.1)).toBe(true)
|
||||
await page.keyboard.press('Space')
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.paused && !window.art.template.$video.seeking)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.currentTime = 1
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => !window.art.template.$video.seeking && Math.abs(window.art.currentTime - 1) < 0.1)).toBe(true)
|
||||
await page.keyboard.press('ArrowRight')
|
||||
expect(await page.evaluate(() => window.nativeHotkeys.at(-1))).toBe('ArrowRight')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(5)
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.seeking)).toBe(false)
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeLessThan(2)
|
||||
const volume = await page.evaluate(() => {
|
||||
window.art.volume = 0.5
|
||||
return window.art.volume
|
||||
})
|
||||
await page.keyboard.press('ArrowUp')
|
||||
expect(await page.evaluate(() => window.art.volume)).toBeGreaterThan(volume)
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await page.keyboard.press('Escape')
|
||||
expect(await page.evaluate(() => window.art.fullscreenWeb)).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
for (const kind of ['inherited', 'plaintext', 'select']) {
|
||||
test(`candidate: ${kind} editing and IME composition do not activate player shortcuts`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate((kind) => {
|
||||
const art = window.art
|
||||
const root = document.createElement('div')
|
||||
root.innerHTML = kind === 'select' ? '<select><option>one</option></select>' : `<div contenteditable="${kind === 'plaintext' ? 'plaintext-only' : 'true'}"><span tabindex="0">edit</span></div>`
|
||||
art.template.$player.append(root)
|
||||
const target = root.querySelector('span,select')
|
||||
target.focus()
|
||||
art.isFocus = true
|
||||
let hits = 0
|
||||
const generic = []
|
||||
art.hotkey.add('KeyK', () => hits++)
|
||||
art.on('keydown', value => generic.push(value.code))
|
||||
const key = new KeyboardEvent('keydown', { code: 'KeyK', cancelable: true, bubbles: true })
|
||||
target.dispatchEvent(key)
|
||||
target.blur()
|
||||
const composition = new KeyboardEvent('keydown', { code: 'KeyK', cancelable: true, bubbles: true, isComposing: true })
|
||||
document.dispatchEvent(composition)
|
||||
art.destroy()
|
||||
return { hits, generic, prevented: [key.defaultPrevented, composition.defaultPrevented] }
|
||||
}, kind)).toEqual({ hits: 0, generic: ['KeyK', 'KeyK'], prevented: [false, false] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: destroying during a key callback stops remaining keys and notifications', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const calls = []
|
||||
art.isFocus = true
|
||||
art.hotkey.add('KeyK', () => {
|
||||
calls.push('first')
|
||||
art.destroy(false)
|
||||
})
|
||||
art.hotkey.add('KeyK', () => calls.push('second'))
|
||||
art.on('hotkey', () => calls.push('hotkey'))
|
||||
art.on('keydown', () => calls.push('keydown'))
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyK' }))
|
||||
art.hotkey.init()
|
||||
art.emit('document:keydown', new KeyboardEvent('keydown', { code: 'KeyK' }))
|
||||
return calls
|
||||
})).toEqual(['first'])
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: hotkey registration retains chain, array identity, receiver and event order`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.isFocus = true
|
||||
const calls = []
|
||||
const event = new KeyboardEvent('keydown', { code: 'KeyK', bubbles: true, cancelable: true })
|
||||
function callback(value) {
|
||||
calls.push(['callback', this === art, value === event])
|
||||
}
|
||||
const add = art.hotkey.add('KeyK', callback)
|
||||
const list = art.hotkey.keys.KeyK
|
||||
art.hotkey.add('KeyK', callback)
|
||||
art.on('hotkey', value => calls.push(['hotkey', value === event]))
|
||||
art.on('keydown', value => calls.push(['keydown', value === event]))
|
||||
document.dispatchEvent(event)
|
||||
const count = list.length
|
||||
const remove = art.hotkey.remove('KeyK', callback)
|
||||
const result = { chain: add === art.hotkey && remove === art.hotkey, count, retainedArray: list.length === 0, missing: !Object.hasOwn(art.hotkey.keys, 'KeyK'), keysPrototype: Object.getPrototypeOf(art.hotkey.keys) === Object.prototype, prevented: event.defaultPrevented, calls }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ chain: true, count: 1, retainedArray: true, missing: true, keysPrototype: true, prevented: true, calls: [['callback', true, true], ['hotkey', true], ['keydown', true]] })
|
||||
})
|
||||
|
||||
test(`${core}: rebound document input retains typing instead of triggering a hotkey`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.evaluate(() => {
|
||||
const frame = document.createElement('iframe')
|
||||
frame.id = 'keyboard-frame'
|
||||
document.body.append(frame)
|
||||
const doc = frame.contentDocument
|
||||
doc.body.innerHTML = '<input id="editor">'
|
||||
window.art.events.bindGlobalEvents({ document: doc, window: frame.contentWindow })
|
||||
window.hits = 0
|
||||
window.art.hotkey.add('KeyK', () => window.hits++)
|
||||
window.art.isFocus = true
|
||||
})
|
||||
const editor = page.frameLocator('#keyboard-frame').locator('#editor')
|
||||
await editor.focus()
|
||||
await editor.press('k')
|
||||
expect(await page.evaluate(() => window.hits)).toBe(core === 'candidate' ? 0 : 1)
|
||||
await expect(editor).toHaveValue(core === 'candidate' ? 'k' : '')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core}: shadow-root input retains typing instead of triggering a hotkey`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.evaluate(() => {
|
||||
const host = document.createElement('div')
|
||||
host.id = 'shadow-editor'
|
||||
window.art.template.$player.append(host)
|
||||
host.attachShadow({ mode: 'open' }).innerHTML = '<input id="editor">'
|
||||
window.hits = 0
|
||||
window.art.hotkey.add('KeyK', () => window.hits++)
|
||||
window.art.isFocus = true
|
||||
})
|
||||
const editor = page.locator('#shadow-editor input')
|
||||
await editor.focus()
|
||||
await editor.press('k')
|
||||
expect(await page.evaluate(() => window.hits)).toBe(core === 'candidate' ? 0 : 1)
|
||||
await expect(editor).toHaveValue(core === 'candidate' ? 'k' : '')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core}: repeated hotkey init does not multiply the keyboard subscription`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.isFocus = true
|
||||
let hits = 0
|
||||
art.hotkey.add('KeyK', () => hits++)
|
||||
art.hotkey.init()
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyK', bubbles: true, cancelable: true }))
|
||||
art.destroy()
|
||||
return hits
|
||||
})).toBe(core === 'candidate' ? 1 : 2)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, mobile = false) {
|
||||
if (mobile)
|
||||
await page.addInitScript(() => Object.defineProperty(navigator, 'userAgent', { configurable: true, value: 'ArtPlayer Android closeout fixture' }))
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => {
|
||||
window.Artplayer.SCROLL_TIME = 0
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, gesture: true })
|
||||
window.touch = (target, name, x, y) => {
|
||||
const event = new Event(name, { bubbles: true })
|
||||
Object.defineProperty(event, 'touches', { value: [{ identifier: 1, pageX: x, pageY: y, clientX: x, clientY: y }] })
|
||||
target.dispatchEvent(event)
|
||||
}
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
test('candidate closeout: restoring a removed default hotkey keeps identity and real playback', async ({ page }) => {
|
||||
await setup(page)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const original = art.hotkey.keys.Space[0]
|
||||
art.hotkey.remove('Space', original)
|
||||
const absent = !art.hotkey.keys.Space
|
||||
art.hotkey.init()
|
||||
art.hotkey.init()
|
||||
art.isFocus = true
|
||||
return { absent, count: art.hotkey.keys.Space.length, identity: art.hotkey.keys.Space[0] === original }
|
||||
})).toEqual({ absent: true, count: 1, identity: true })
|
||||
await page.keyboard.press('Space')
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test('candidate closeout: a failed native removal remains retryable and releases its record', async ({ page }) => {
|
||||
await setup(page)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
const remove = target.removeEventListener
|
||||
const warning = console.warn
|
||||
const error = new Error('temporary removal failure')
|
||||
let deny = true
|
||||
let warnings = 0
|
||||
let hits = 0
|
||||
console.warn = (message, actual) => {
|
||||
if (actual === error)
|
||||
warnings++
|
||||
}
|
||||
target.removeEventListener = function (...args) {
|
||||
if (deny)
|
||||
throw error
|
||||
return remove.apply(this, args)
|
||||
}
|
||||
try {
|
||||
const dispose = art.events.proxy(target, 'example', () => hits++)
|
||||
art.events.remove(dispose)
|
||||
const retained = art.events.destroyEvents.has(dispose)
|
||||
target.dispatchEvent(new Event('example'))
|
||||
deny = false
|
||||
art.events.remove(dispose)
|
||||
target.dispatchEvent(new Event('example'))
|
||||
const released = !art.events.destroyEvents.has(dispose)
|
||||
art.destroy()
|
||||
return { retained, released, warnings, hits }
|
||||
}
|
||||
finally {
|
||||
console.warn = warning
|
||||
target.removeEventListener = remove
|
||||
}
|
||||
})).toEqual({ retained: true, released: true, warnings: 1, hits: 1 })
|
||||
})
|
||||
|
||||
test('candidate closeout: destruction before native add returns cannot leave a late listener', async ({ page }) => {
|
||||
await setup(page)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
const add = target.addEventListener
|
||||
let hits = 0
|
||||
target.addEventListener = function (...args) {
|
||||
art.destroy()
|
||||
return add.apply(this, args)
|
||||
}
|
||||
art.events.proxy(target, 'example', () => hits++)
|
||||
target.dispatchEvent(new Event('example'))
|
||||
return { hits, remaining: art.events.destroyEvents.size, destroyed: art.isDestroy }
|
||||
})).toEqual({ hits: 0, remaining: 0, destroyed: true })
|
||||
})
|
||||
|
||||
test('candidate closeout: window registration failure restores the previous global binding', async ({ page }) => {
|
||||
await setup(page)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const frame = document.createElement('iframe')
|
||||
document.body.append(frame)
|
||||
const next = frame.contentWindow
|
||||
const add = next.addEventListener
|
||||
const failure = new Error('window registration failed after add')
|
||||
next.addEventListener = function (name, ...args) {
|
||||
add.call(this, name, ...args)
|
||||
if (name === 'scroll')
|
||||
throw failure
|
||||
}
|
||||
const received = []
|
||||
art.on('document:mouseup', event => received.push(event.target === document ? 'old-document' : 'new-document'))
|
||||
art.on('window:scroll', event => received.push(event.target === window ? 'old-window' : 'new-window'))
|
||||
let sameError = false
|
||||
try {
|
||||
art.events.bindGlobalEvents({ document: next.document, window: next })
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
next.addEventListener = add
|
||||
next.document.dispatchEvent(new next.Event('mouseup'))
|
||||
next.dispatchEvent(new next.Event('scroll'))
|
||||
document.dispatchEvent(new Event('mouseup'))
|
||||
window.dispatchEvent(new Event('scroll'))
|
||||
art.destroy()
|
||||
frame.remove()
|
||||
return { sameError, received }
|
||||
})).toEqual({ sameError: true, received: ['old-document', 'old-window'] })
|
||||
})
|
||||
|
||||
for (const cause of ['source', 'rotation', 'geometry']) {
|
||||
test(`candidate closeout: ${cause} changes cancel the old gesture in the installed player`, async ({ page }) => {
|
||||
await setup(page, true)
|
||||
expect(await page.evaluate((cause) => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
let seeks = 0
|
||||
art.on('seek', () => seeks++)
|
||||
window.touch(video, 'touchstart', 100, 100)
|
||||
if (cause === 'source')
|
||||
art.url = '/test/pattern.mp4?closeout=source'
|
||||
if (cause === 'rotation')
|
||||
art.isRotate = true
|
||||
if (cause === 'geometry') {
|
||||
art.template.$player.style.setProperty('width', '0px', 'important')
|
||||
art.template.$player.style.setProperty('min-width', '0px', 'important')
|
||||
}
|
||||
const before = seeks
|
||||
const width = art.width
|
||||
window.touch(video, 'touchmove', cause === 'rotation' ? 100 : 140, cause === 'rotation' ? 140 : 100)
|
||||
art.destroy()
|
||||
return { newSeeks: seeks - before, zeroWidth: cause !== 'geometry' || width === 0 }
|
||||
}, cause)).toEqual({ newSeeks: 0, zeroWidth: true })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate closeout: adopted viewport retains exact gap boundaries', async ({ page }) => {
|
||||
await setup(page)
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const frame = document.createElement('iframe')
|
||||
frame.style.cssText = 'height:100px;width:200px'
|
||||
document.body.append(frame)
|
||||
const container = art.template.$container
|
||||
container.style.cssText = 'position:absolute;top:150px;left:0;height:20px;width:20px'
|
||||
frame.contentDocument.body.append(container)
|
||||
art.events.bindGlobalEvents()
|
||||
art.constructor.SCROLL_GAP = 50
|
||||
const states = []
|
||||
art.on('view', value => states.push(value))
|
||||
frame.contentWindow.dispatchEvent(new Event('scroll'))
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
container.style.top = '151px'
|
||||
frame.contentWindow.dispatchEvent(new Event('scroll'))
|
||||
art.destroy()
|
||||
frame.remove()
|
||||
return states
|
||||
})).toEqual([true, false])
|
||||
})
|
||||
@@ -0,0 +1,151 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
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)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: null options preserve the native default and callbacks can be removed directly`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
let hits = 0
|
||||
let failed = false
|
||||
const callback = () => hits++
|
||||
try {
|
||||
const remove = art.events.proxy(target, 'example', callback, null)
|
||||
target.dispatchEvent(new Event('example'))
|
||||
remove()
|
||||
target.dispatchEvent(new Event('example'))
|
||||
}
|
||||
catch {
|
||||
failed = true
|
||||
}
|
||||
art.destroy()
|
||||
return { hits, failed }
|
||||
})).toEqual({ hits: 1, failed: false })
|
||||
})
|
||||
|
||||
test(`${core}: once and abort use native event identity and permit subsequent registration`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
const seen = []
|
||||
const event = new Event('example')
|
||||
const callback = function (received) {
|
||||
seen.push(this === target && received === event)
|
||||
}
|
||||
const once = art.events.proxy(target, 'example', callback, { once: true })
|
||||
target.dispatchEvent(event)
|
||||
target.dispatchEvent(event)
|
||||
once()
|
||||
const controller = new AbortController()
|
||||
const aborted = art.events.proxy(target, 'example', callback, { signal: controller.signal })
|
||||
target.dispatchEvent(event)
|
||||
controller.abort()
|
||||
target.dispatchEvent(event)
|
||||
aborted()
|
||||
const skipped = art.events.proxy(target, 'example', callback, { signal: controller.signal })
|
||||
target.dispatchEvent(event)
|
||||
skipped()
|
||||
art.events.proxy(target, 'example', callback)
|
||||
target.dispatchEvent(event)
|
||||
art.destroy()
|
||||
target.dispatchEvent(event)
|
||||
return seen
|
||||
})).toEqual([true, true, true])
|
||||
})
|
||||
|
||||
test(`${core}: direct event disposer preserves native callback identity and releases bookkeeping`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
let hits = 0
|
||||
const callback = () => hits++
|
||||
const before = art.events.destroyEvents.size
|
||||
const remove = art.events.proxy(target, 'example', callback)
|
||||
target.removeEventListener('example', callback)
|
||||
target.dispatchEvent(new Event('example'))
|
||||
const returned = remove()
|
||||
const result = { hits, returned: typeof returned, remaining: art.events.destroyEvents.size - before }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ hits: 0, returned: 'undefined', remaining: core === 'candidate' ? 0 : 1 })
|
||||
})
|
||||
|
||||
test(`${core}: changing capture options after registration cannot prevent cleanup`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
let hits = 0
|
||||
const options = { capture: true }
|
||||
const remove = art.events.proxy(target, 'example', () => hits++, options)
|
||||
options.capture = false
|
||||
remove()
|
||||
target.dispatchEvent(new Event('example'))
|
||||
art.destroy()
|
||||
return hits
|
||||
})).toBe(core === 'candidate' ? 0 : 1)
|
||||
})
|
||||
|
||||
test(`${core}: partially failing array registration rolls back the listeners it created`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
const original = target.addEventListener
|
||||
const failure = new Error('second registration denied')
|
||||
target.addEventListener = function (name, ...args) {
|
||||
if (name === 'second')
|
||||
throw failure
|
||||
return original.call(this, name, ...args)
|
||||
}
|
||||
let hits = 0
|
||||
let sameError = false
|
||||
const before = art.events.destroyEvents.size
|
||||
try {
|
||||
art.events.proxy(target, ['first', 'second'], () => hits++)
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
target.addEventListener = original
|
||||
target.dispatchEvent(new Event('first'))
|
||||
const result = { hits, sameError, remaining: art.events.destroyEvents.size - before }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ hits: core === 'candidate' ? 0 : 1, sameError: true, remaining: core === 'candidate' ? 0 : 1 })
|
||||
})
|
||||
|
||||
test(`${core}: registration throwing after native add does not leave an untracked listener`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const target = document.createElement('div')
|
||||
const original = target.addEventListener
|
||||
const failure = new Error('after native add')
|
||||
target.addEventListener = function (...args) {
|
||||
original.apply(this, args)
|
||||
throw failure
|
||||
}
|
||||
let hits = 0
|
||||
let sameError = false
|
||||
try {
|
||||
art.events.proxy(target, 'example', () => hits++)
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
target.addEventListener = original
|
||||
target.dispatchEvent(new Event('example'))
|
||||
art.destroy()
|
||||
return { hits, sameError }
|
||||
})).toEqual({ hits: core === 'candidate' ? 0 : 1, sameError: true })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core, mobile = false) {
|
||||
if (mobile)
|
||||
await page.addInitScript(() => Object.defineProperty(navigator, 'userAgent', { configurable: true, value: 'ArtPlayer Android pointer fixture' }))
|
||||
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)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: real mouse input focuses the player, hovers and toggles actual playback`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.evaluate(() => {
|
||||
window.art.constructor.DBCLICK_FULLSCREEN = false
|
||||
window.art.constructor.DBCLICK_TIME = 0
|
||||
window.pointerTrusted = []
|
||||
window.art.on('click', event => window.pointerTrusted.push(event.isTrusted))
|
||||
})
|
||||
const video = page.locator('.art-video')
|
||||
await video.click({ position: { x: 100, y: 100 } })
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
|
||||
expect(await page.evaluate(() => ({ focus: window.art.isFocus, hover: window.art.template.$player.classList.contains('art-hover') }))).toEqual({ focus: true, hover: true })
|
||||
await video.click({ position: { x: 100, y: 100 } })
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.paused)).toBe(true)
|
||||
expect(await page.evaluate(() => window.pointerTrusted)).toEqual([true, true])
|
||||
await page.mouse.move(1, 1)
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$player.classList.contains('art-hover'))).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core}: click pairs preserve callback identity and playback ordering`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.constructor.DBCLICK_TIME = 60000
|
||||
art.constructor.DBCLICK_FULLSCREEN = false
|
||||
const calls = []
|
||||
const events = [new MouseEvent('click'), new MouseEvent('click'), new MouseEvent('click')]
|
||||
let current
|
||||
art.template.$video.play = () => {
|
||||
calls.push('play')
|
||||
return Promise.resolve()
|
||||
}
|
||||
for (const name of ['click', 'dblclick']) {
|
||||
art.on(name, function (event) {
|
||||
calls.push(`${name}:${event === current}:${this === art}`)
|
||||
}, art)
|
||||
}
|
||||
for (const event of events) {
|
||||
current = event
|
||||
art.template.$video.dispatchEvent(event)
|
||||
}
|
||||
art.destroy()
|
||||
return calls
|
||||
})).toEqual(['click:true:true', 'play', 'dblclick:true:true', 'click:true:true', 'play'])
|
||||
})
|
||||
|
||||
for (const name of ['click', 'dblclick']) {
|
||||
test(`${core}: destruction in ${name} stops its subsequent default action`, async ({ page }) => {
|
||||
await setup(page, core, name === 'dblclick')
|
||||
expect(await page.evaluate((name) => {
|
||||
const art = window.art
|
||||
art.constructor.DBCLICK_TIME = 60000
|
||||
let plays = 0
|
||||
art.template.$video.play = () => {
|
||||
plays++
|
||||
return Promise.resolve()
|
||||
}
|
||||
art.on(name, () => art.destroy())
|
||||
const video = art.template.$video
|
||||
video.dispatchEvent(new MouseEvent('click'))
|
||||
if (name === 'dblclick')
|
||||
video.dispatchEvent(new MouseEvent('click'))
|
||||
return { plays, destroyed: art.isDestroy }
|
||||
}, name)).toEqual({ plays: core === 'published' ? 1 : 0, destroyed: true })
|
||||
})
|
||||
}
|
||||
|
||||
test(`${core}: focus, hover and move preserve flags, classes and original events`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player } = art.template
|
||||
const input = document.createElement('input')
|
||||
$player.append(input)
|
||||
const calls = []
|
||||
let expected
|
||||
for (const name of ['focus', 'blur', 'mousemove'])
|
||||
art.on(name, event => calls.push([name, event === expected, art.isFocus, art.isInput]))
|
||||
art.on('hover', (state, event) => calls.push(['hover', state, event === expected, $player.classList.contains('art-hover')]))
|
||||
expected = new MouseEvent('click', { bubbles: true })
|
||||
input.dispatchEvent(expected)
|
||||
expected = new MouseEvent('contextmenu', { bubbles: true })
|
||||
document.body.dispatchEvent(expected)
|
||||
expected = new MouseEvent('mouseenter')
|
||||
$player.dispatchEvent(expected)
|
||||
expected = new MouseEvent('mousemove')
|
||||
$player.dispatchEvent(expected)
|
||||
expected = new MouseEvent('mouseleave')
|
||||
$player.dispatchEvent(expected)
|
||||
art.destroy()
|
||||
return calls
|
||||
})).toEqual([
|
||||
['focus', true, true, true],
|
||||
['blur', true, false, false],
|
||||
['hover', true, true, true],
|
||||
['mousemove', true, false, false],
|
||||
['hover', false, true, false],
|
||||
])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user