mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-13 15:56:15 -08:00
refactor(core): CORE-16 type display modes and own fullscreen lifetimes
This commit is contained in:
1 parent
1b83081e8f
commit
88aef830df
81 files changed
+33897
-1503
No files matched your search
@@ -0,0 +1,285 @@
|
||||
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')
|
||||
window.miniEvents = []
|
||||
window.art.on('mini', value => window.miniEvents.push(value))
|
||||
const video = window.art.template.$video
|
||||
const parent = document.createElement('div')
|
||||
parent.id = 'original-video-parent'
|
||||
const next = document.createElement('span')
|
||||
next.id = 'original-video-next'
|
||||
video.before(parent)
|
||||
parent.append(video, next)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: mini retains first-entry and reuse DOM order and display`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
const first = popup.firstChild === art.template.$video
|
||||
const display = popup.style.display
|
||||
art.mini = true
|
||||
const result = { first, display, last: popup.lastChild === art.template.$video, reusedDisplay: popup.style.display }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ first: true, display: '', last: true, reusedDisplay: 'flex' })
|
||||
})
|
||||
|
||||
test(`${core}: mini restores the original video parent and sibling after repeated entry`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
art.mini = true
|
||||
art.mini = false
|
||||
const result = { restored: art.template.$video.parentElement.id === 'original-video-parent', next: art.template.$video.nextSibling?.id === 'original-video-next', reused: popup === art.template.$mini, hidden: popup.style.display, events: [...window.miniEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ restored: core === 'candidate', next: core === 'candidate', reused: true, hidden: 'none', events: [true, true, false] })
|
||||
})
|
||||
|
||||
for (const removeHtml of [false, true]) {
|
||||
test(`${core}: destroying active mini removeHtml=${removeHtml} removes the owned popup`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate((removeHtml) => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
const video = art.template.$video
|
||||
window.miniEvents.length = 0
|
||||
art.destroy(removeHtml)
|
||||
return { popupConnected: popup.isConnected, videoInPopup: popup.contains(video), events: window.miniEvents }
|
||||
}, removeHtml)).toEqual({ popupConnected: core === 'published', videoInPopup: core === 'published', events: [] })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test('candidate: hiding mini cancels drag and later document events cannot persist a hidden popup', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
const result = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
const original = { left: art.storage.get('left'), top: art.storage.get('top') }
|
||||
popup.dispatchEvent(new MouseEvent('mousedown', { button: 0, clientX: 20, clientY: 20 }))
|
||||
art.emit('document:mousemove', new MouseEvent('mousemove', { clientX: 40, clientY: 50 }))
|
||||
art.mini = false
|
||||
art.emit('document:mouseup', new MouseEvent('mouseup'))
|
||||
const result = { original, after: { left: art.storage.get('left'), top: art.storage.get('top') }, dragging: popup.classList.contains('art-mini-dragging'), transform: popup.style.transform }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result.after).toEqual(result.original)
|
||||
expect(result.dragging).toBe(false)
|
||||
expect(result.transform).toBe('')
|
||||
})
|
||||
|
||||
test('candidate: mini remains in the viewport on small screens', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 280, height: 160 })
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.art.storage.set('left', 9000)
|
||||
window.art.storage.set('top', 9000)
|
||||
window.art.mini = true
|
||||
})
|
||||
const rect = await page.locator('.art-mini-popup').boundingBox()
|
||||
expect(rect.x).toBeGreaterThanOrEqual(0)
|
||||
expect(rect.y).toBeGreaterThanOrEqual(0)
|
||||
expect(rect.x + rect.width).toBeLessThanOrEqual(280)
|
||||
expect(rect.y + rect.height).toBeLessThanOrEqual(160)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test('candidate: mini drag stores the actual position and playback buttons remain usable', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => window.art.mini = true)
|
||||
const popup = page.locator('.art-mini-popup')
|
||||
const rect = await popup.boundingBox()
|
||||
await page.mouse.move(rect.x + 30, rect.y + 20)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(rect.x - 30, rect.y - 20)
|
||||
await page.mouse.up()
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const rect = art.template.$mini.getBoundingClientRect()
|
||||
return Math.abs(art.storage.get('left') - rect.left) < 1 && Math.abs(art.storage.get('top') - rect.top) < 1
|
||||
})).toBe(true)
|
||||
await popup.locator('.art-icon-play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing && window.art.currentTime > 0)).toBe(true)
|
||||
await popup.locator('.art-icon-pause').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.paused)).toBe(true)
|
||||
await popup.locator('.art-mini-close').click()
|
||||
await expect(popup).toBeHidden()
|
||||
expect(await page.evaluate(() => window.art.template.$video.parentElement.id)).toBe('original-video-parent')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test('candidate: mini exit failure keeps the active video and can restore on retry', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const parent = art.template.$video.parentNode
|
||||
art.mini = true
|
||||
const insert = parent.insertBefore
|
||||
const failure = new Error('controlled mini restore failure')
|
||||
parent.insertBefore = () => {
|
||||
throw failure
|
||||
}
|
||||
let rejected = false
|
||||
try {
|
||||
art.mini = false
|
||||
}
|
||||
catch (error) { rejected = error === failure }
|
||||
const active = art.mini && art.template.$mini.contains(art.template.$video)
|
||||
parent.insertBefore = insert
|
||||
art.mini = false
|
||||
const result = { rejected, active, restored: art.template.$video.parentNode === parent }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ rejected: true, active: true, restored: true })
|
||||
})
|
||||
|
||||
test('candidate: mini reentry during restoration retains the new placement snapshot', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const parent = art.template.$video.parentNode
|
||||
const insert = parent.insertBefore
|
||||
art.mini = true
|
||||
parent.insertBefore = function (...args) {
|
||||
const result = insert.apply(this, args)
|
||||
parent.insertBefore = insert
|
||||
art.mini = true
|
||||
return result
|
||||
}
|
||||
art.mini = false
|
||||
const active = art.mini
|
||||
art.mini = false
|
||||
const result = { active, restored: art.template.$video.parentNode === parent, events: [...window.miniEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ active: true, restored: true, events: [true, true, false] })
|
||||
})
|
||||
|
||||
for (const action of ['hide', 'destroy', 'fail']) {
|
||||
test(`candidate: mini construction ${action} releases incomplete UI and restores the video`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate((action) => {
|
||||
const art = window.art
|
||||
const icons = art.icons
|
||||
const failure = new Error('controlled mini icon failure')
|
||||
art.icons = {
|
||||
get close() {
|
||||
if (action === 'hide')
|
||||
art.mini = false
|
||||
else if (action === 'destroy')
|
||||
art.destroy(false)
|
||||
else
|
||||
throw failure
|
||||
return icons.close
|
||||
},
|
||||
get play() { return icons.play },
|
||||
get pause() { return icons.pause },
|
||||
}
|
||||
let rejected = false
|
||||
try {
|
||||
art.mini = true
|
||||
}
|
||||
catch (error) {
|
||||
rejected = error === failure
|
||||
}
|
||||
const restored = art.template.$video.parentElement.id === 'original-video-parent'
|
||||
const active = art.mini
|
||||
let retry = null
|
||||
if (action !== 'destroy') {
|
||||
art.icons = icons
|
||||
art.mini = true
|
||||
retry = art.mini && document.querySelectorAll('.art-mini-popup').length === 1
|
||||
art.destroy()
|
||||
}
|
||||
return { restored, active, rejected, retry, remaining: document.querySelectorAll('.art-mini-popup').length }
|
||||
}, action)).toEqual({ restored: true, active: false, rejected: action === 'fail', retry: action === 'destroy' ? null : true, remaining: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: mini preserves a caller-owned popup on destruction', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const popup = document.createElement('div')
|
||||
popup.style.cssText = 'display: grid; width: 320px; height: 180px;'
|
||||
document.body.append(popup)
|
||||
art.template.$mini = popup
|
||||
art.mini = true
|
||||
art.destroy(false)
|
||||
const result = { connected: popup.isConnected, display: popup.style.display, restored: art.template.$video.parentElement.id === 'original-video-parent', empty: popup.childNodes.length === 0 }
|
||||
popup.remove()
|
||||
return result
|
||||
})).toEqual({ connected: true, display: 'grid', restored: true, empty: true })
|
||||
})
|
||||
|
||||
test('candidate: reentrant mini creation allocates only one owned popup', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const icons = art.icons
|
||||
let reentered = false
|
||||
art.icons = {
|
||||
get close() {
|
||||
if (!reentered) {
|
||||
reentered = true
|
||||
art.mini = true
|
||||
}
|
||||
return icons.close
|
||||
},
|
||||
get play() { return icons.play },
|
||||
get pause() { return icons.pause },
|
||||
}
|
||||
art.mini = true
|
||||
const result = { popups: document.querySelectorAll('.art-mini-popup').length, ownVideo: art.template.$mini.contains(art.template.$video), events: [...window.miniEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ popups: 1, ownVideo: true, events: [true] })
|
||||
})
|
||||
|
||||
test('candidate: mini switches to body web fullscreen and restores the original video placement', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
window.Artplayer.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.mini = true
|
||||
art.fullscreenWeb = true
|
||||
const switched = !art.mini && art.fullscreenWeb && art.template.$mini.style.display === 'none'
|
||||
art.fullscreenWeb = false
|
||||
const restored = art.template.$video.parentElement.id === 'original-video-parent'
|
||||
art.mini = true
|
||||
art.mini = false
|
||||
const events = [...window.miniEvents]
|
||||
art.destroy()
|
||||
return { switched, restored, events }
|
||||
})).toEqual({ switched: true, restored: true, events: [true, false, true, false] })
|
||||
})
|
||||
|
||||
test('candidate: destroying a mini drag removes its document subscriptions', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
art.template.$mini.dispatchEvent(new MouseEvent('mousedown', { button: 0, clientX: 10, clientY: 10 }))
|
||||
art.destroy(false)
|
||||
let writes = 0
|
||||
art.storage.set = () => writes++
|
||||
art.emit('document:mousemove', new MouseEvent('mousemove', { clientX: 100, clientY: 100 }))
|
||||
art.emit('document:mouseup', new MouseEvent('mouseup'))
|
||||
return writes
|
||||
})).toBe(0)
|
||||
})
|
||||
@@ -0,0 +1,251 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
const maps = [
|
||||
['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenchange'],
|
||||
['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitfullscreenchange'],
|
||||
['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitfullscreenchange'],
|
||||
['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozfullscreenchange'],
|
||||
['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'MSFullscreenChange'],
|
||||
]
|
||||
window.fullscreenAPI = maps.find(([, exit]) => exit in document)
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.art.template.$container.id = 'native-fullscreen-main'
|
||||
const container = document.createElement('div')
|
||||
Object.assign(container.style, { width: '320px', height: '180px' })
|
||||
document.body.append(container)
|
||||
window.other = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true })
|
||||
window.fullscreenEvents = [[], []]
|
||||
window.art.on('fullscreen', value => window.fullscreenEvents[0].push(value))
|
||||
window.other.on('fullscreen', value => window.fullscreenEvents[1].push(value))
|
||||
document.querySelector('#play').onclick = () => {
|
||||
window.art.fullscreen = true
|
||||
}
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady && window.other.isReady)).toBe(true)
|
||||
expect(await page.evaluate(() => Boolean(window.fullscreenAPI))).toBe(true)
|
||||
}
|
||||
|
||||
test('candidate: void exit waits for the owned video to leave and accepts another owner as completion', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const [, exitName, elementName, eventName] = window.fullscreenAPI
|
||||
const elementDescriptor = Object.getOwnPropertyDescriptor(document, elementName)
|
||||
const originalExit = document[exitName]
|
||||
let element = art.template.$video
|
||||
Object.defineProperty(document, elementName, { configurable: true, get: () => element })
|
||||
document.dispatchEvent(new Event(eventName))
|
||||
document[exitName] = () => undefined
|
||||
let settled = false
|
||||
const result = Object.getOwnPropertyDescriptor(art, 'fullscreen').set(false).then(() => settled = true)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const early = settled
|
||||
element = window.other.template.$player
|
||||
document.dispatchEvent(new Event(eventName))
|
||||
await result
|
||||
const completed = settled && !art.fullscreen && window.other.fullscreen
|
||||
element = null
|
||||
document.dispatchEvent(new Event(eventName))
|
||||
if (elementDescriptor)
|
||||
Object.defineProperty(document, elementName, elementDescriptor)
|
||||
else
|
||||
delete document[elementName]
|
||||
document[exitName] = originalExit
|
||||
art.destroy()
|
||||
window.other.destroy()
|
||||
return { early, completed }
|
||||
})).toEqual({ early: false, completed: true })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: native fullscreen belongs to its player and other players cannot exit it`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => document[window.fullscreenAPI[2]] === window.art.template.$player)).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.fullscreenEvents[0])).toEqual([true])
|
||||
expect(await page.evaluate(() => ({ own: window.art.fullscreen, other: window.other.fullscreen, className: window.other.template.$player.classList.contains('art-fullscreen'), otherEvents: window.fullscreenEvents[1] }))).toEqual({ own: true, other: core === 'published', className: core === 'published', otherEvents: core === 'published' ? [true] : [] })
|
||||
await page.evaluate(async () => {
|
||||
await Object.getOwnPropertyDescriptor(window.other, 'fullscreen').set.call(window.other, false)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => Boolean(document[window.fullscreenAPI[2]]))).toBe(core === 'candidate')
|
||||
await page.evaluate(async () => {
|
||||
if (document[window.fullscreenAPI[2]])
|
||||
await document[window.fullscreenAPI[1]]()
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test(`${core}: destroyed players stop receiving later native fullscreen events`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.evaluate(() => window.other.destroy(false))
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.fullscreenEvents[0])).toEqual([true])
|
||||
expect(await page.evaluate(() => ({ events: window.fullscreenEvents[1], className: window.other.template.$player.classList.contains('art-fullscreen') }))).toEqual({ events: core === 'published' ? [true] : [], className: core === 'published' })
|
||||
await page.evaluate(async () => {
|
||||
await document[window.fullscreenAPI[1]]()
|
||||
window.art.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test(`${core}: rejected native fullscreen request removes its temporary change listener`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const [request, , , change] = window.fullscreenAPI
|
||||
const add = document.addEventListener
|
||||
const remove = document.removeEventListener
|
||||
const active = new Set()
|
||||
document.addEventListener = function (name, callback, options) {
|
||||
if (name === change)
|
||||
active.add(callback)
|
||||
return add.call(this, name, callback, options)
|
||||
}
|
||||
document.removeEventListener = function (name, callback, options) {
|
||||
if (name === change)
|
||||
active.delete(callback)
|
||||
return remove.call(this, name, callback, options)
|
||||
}
|
||||
const failure = new Error('controlled fullscreen rejection')
|
||||
art.template.$player[request] = () => Promise.reject(failure)
|
||||
const rejected = await Object.getOwnPropertyDescriptor(art, 'fullscreen').set.call(art, true).then(() => false, error => error === failure)
|
||||
const result = { rejected, leaked: active.size }
|
||||
document.addEventListener = add
|
||||
document.removeEventListener = remove
|
||||
art.destroy()
|
||||
window.other.destroy()
|
||||
return result
|
||||
})).toEqual({ rejected: true, leaked: core === 'published' ? 1 : 0 })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: fullscreen assignment rejection shows notice without an unhandled rejection', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
const [request] = window.fullscreenAPI
|
||||
window.art.template.$player[request] = () => Promise.reject(new Error('controlled assignment denial'))
|
||||
window.art.fullscreen = true
|
||||
})
|
||||
await expect(page.locator('#native-fullscreen-main .art-notice')).toContainText('controlled assignment denial')
|
||||
await page.evaluate(() => {
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test('candidate: reentrant entry preserves real fullscreen UI and event order', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.fullscreenOrder = []
|
||||
window.art.on('fullscreen', (value) => {
|
||||
if (value) {
|
||||
window.fullscreenOrder.push(['fullscreen', window.art.template.$player.classList.contains('art-fullscreen')])
|
||||
window.art.fullscreen = true
|
||||
}
|
||||
})
|
||||
window.art.on('resize', () => {
|
||||
if (window.art.fullscreen)
|
||||
window.fullscreenOrder.push(['resize', window.art.template.$player.classList.contains('art-fullscreen')])
|
||||
})
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen && window.art.template.$player.classList.contains('art-fullscreen'))).toBe(true)
|
||||
expect(await page.evaluate(() => window.fullscreenOrder.slice(0, 2))).toEqual([['fullscreen', false], ['resize', true]])
|
||||
await page.evaluate(async () => {
|
||||
await document[window.fullscreenAPI[1]]()
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
for (const mode of ['promise-exit', 'promise-destroy', 'void-exit', 'void-destroy']) {
|
||||
test(`candidate: pending native entry ${mode} releases a late own entry`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(async (mode) => {
|
||||
const art = window.art
|
||||
const [request, exit, element, change] = window.fullscreenAPI
|
||||
const originalExit = document[exit]
|
||||
const originalElement = Object.getOwnPropertyDescriptor(document, element)
|
||||
let current = null
|
||||
let complete
|
||||
let exits = 0
|
||||
Object.defineProperty(document, element, { configurable: true, get: () => current })
|
||||
art.template.$player[request] = () => {
|
||||
if (mode.startsWith('promise'))
|
||||
return new Promise(resolve => complete = resolve)
|
||||
complete = () => document.dispatchEvent(new Event(change))
|
||||
}
|
||||
document[exit] = () => {
|
||||
exits++
|
||||
current = null
|
||||
document.dispatchEvent(new Event(change))
|
||||
return Promise.resolve()
|
||||
}
|
||||
const setter = Object.getOwnPropertyDescriptor(art, 'fullscreen').set
|
||||
const pending = setter.call(art, true)
|
||||
if (mode.endsWith('destroy'))
|
||||
art.destroy(false)
|
||||
else
|
||||
await setter.call(art, false)
|
||||
await pending
|
||||
current = art.template.$player
|
||||
complete()
|
||||
// Flush the request's native Promise observer and setter settlement chain.
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { exits, fullscreen: art.fullscreen, className: art.template.$player.classList.contains('art-fullscreen'), events: window.fullscreenEvents[0] }
|
||||
art.destroy()
|
||||
window.other.destroy()
|
||||
document[exit] = originalExit
|
||||
if (originalElement)
|
||||
Object.defineProperty(document, element, originalElement)
|
||||
else
|
||||
delete document[element]
|
||||
return result
|
||||
}, mode)).toEqual({ exits: 1, fullscreen: false, className: false, events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: cancelling an actual native request in the initiating click releases fullscreen', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.entrySettled = false
|
||||
document.querySelector('#play').onclick = () => {
|
||||
const setter = Object.getOwnPropertyDescriptor(window.art, 'fullscreen').set
|
||||
const entering = setter.call(window.art, true)
|
||||
window.art.fullscreen = false
|
||||
void entering.then(() => window.entrySettled = true)
|
||||
}
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.entrySettled && !document[window.fullscreenAPI[2]])).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test('candidate: rejected exit retains actual fullscreen state and can be retried', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$player.classList.contains('art-fullscreen'))).toBe(true)
|
||||
expect(await page.evaluate(async () => {
|
||||
const [, exit] = window.fullscreenAPI
|
||||
const originalExit = document[exit]
|
||||
const failure = new Error('controlled exit denial')
|
||||
document[exit] = () => Promise.reject(failure)
|
||||
const setter = Object.getOwnPropertyDescriptor(window.art, 'fullscreen').set
|
||||
const rejected = await setter.call(window.art, false).then(() => false, error => error === failure)
|
||||
const result = { rejected, fullscreen: window.art.fullscreen, className: window.art.template.$player.classList.contains('art-fullscreen') }
|
||||
document[exit] = originalExit
|
||||
await setter.call(window.art, false)
|
||||
return result
|
||||
})).toEqual({ rejected: true, fullscreen: true, className: true })
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen || window.art.template.$player.classList.contains('art-fullscreen'))).toBe(false)
|
||||
await page.evaluate(() => {
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.setViewportSize({ width: 400, height: 800 })
|
||||
await page.addInitScript(() => Object.defineProperty(navigator, 'userAgent', { configurable: true, value: 'ArtPlayer Android orientation fixture' }))
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.Artplayer.AUTO_ORIENTATION_TIME = 10
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, autoOrientation: true })
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
test('candidate: a cancelled rotation timer cannot rotate a later fullscreen session early', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.clock.install({ time: new Date('2026-09-11T00:00:00Z') })
|
||||
await page.clock.pauseAt(new Date('2026-09-11T00:00:01Z'))
|
||||
await page.evaluate(() => {
|
||||
window.Artplayer.AUTO_ORIENTATION_TIME = 1000
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await page.clock.runFor(700)
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = false
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await page.clock.runFor(400)
|
||||
expect(await page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(false)
|
||||
await page.clock.runFor(600)
|
||||
expect(await page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
for (const removeHtml of [false, true]) {
|
||||
test(`candidate: rotation destruction restores styles and cancels subscriptions removeHtml=${removeHtml}`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.template.$player.style.cssText = 'width:75%;height:80%;transform:scale(0.9);transform-origin:center;'
|
||||
window.beforeRotation = art.template.$player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(true)
|
||||
expect(await page.evaluate((remove) => {
|
||||
const art = window.art
|
||||
const player = art.template.$player
|
||||
art.destroy(remove)
|
||||
const style = player.getAttribute('style')
|
||||
art.emit('fullscreenWeb', true)
|
||||
art.emit('fullscreen', true)
|
||||
return { restored: style === window.beforeRotation, rotated: art.isRotate, state: art.plugins.autoOrientation.state, retained: player.isConnected, changed: style !== player.getAttribute('style') }
|
||||
}, removeHtml)).toEqual({ restored: true, rotated: false, state: false, retained: !removeHtml, changed: false })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: destroying a pending rotation removes its timer and produces no late resize', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.clock.install({ time: new Date('2026-09-11T00:00:00Z') })
|
||||
await page.clock.pauseAt(new Date('2026-09-11T00:00:01Z'))
|
||||
await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.constructor.AUTO_ORIENTATION_TIME = 1000
|
||||
art.fullscreenWeb = true
|
||||
art.destroy(false)
|
||||
window.lateRotations = []
|
||||
art.on('resize', () => window.lateRotations.push(art.isRotate))
|
||||
})
|
||||
await page.clock.runFor(2000)
|
||||
expect(await page.evaluate(() => ({ rotated: window.art.plugins.autoOrientation.state, events: window.lateRotations }))).toEqual({ rotated: false, events: [] })
|
||||
})
|
||||
|
||||
test('candidate: rotated playback supports ratio and flip changes before restoring normal layout', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(async () => {
|
||||
await window.art.play()
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.plugins.autoOrientation.state && window.art.currentTime > 0.1)).toBe(true)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.aspectRatio = '4:3'
|
||||
art.flip = 'horizontal'
|
||||
const video = art.template.$video
|
||||
const ratio = video.clientWidth / video.clientHeight
|
||||
const flipped = art.flip
|
||||
art.fullscreenWeb = false
|
||||
art.aspectRatio = 'default'
|
||||
art.flip = 'normal'
|
||||
const result = { ratioClose: Math.abs(ratio - 4 / 3) < 0.01, flipped, playing: art.playing, rotate: art.isRotate, cleared: video.style.width === '' && video.style.height === '' }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ ratioClose: true, flipped: 'horizontal', playing: true, rotate: false, cleared: true })
|
||||
})
|
||||
|
||||
test('candidate: repeated web fullscreen retains rotated dimensions and final styles', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(true)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.fullscreenWeb = true
|
||||
const result = { width: art.template.$player.style.width, height: art.template.$player.style.height, state: art.plugins.autoOrientation.state }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ width: '800px', height: '400px', state: true })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: rotated web fullscreen restores original inline geometry`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.template.$player.style.cssText = 'width:75%;height:80%;transform:scale(0.9);transform-origin:center;'
|
||||
window.beforeRotation = art.template.$player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isRotate)).toBe(true)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const rotated = { state: art.plugins.autoOrientation.state, width: art.template.$player.style.width, height: art.template.$player.style.height }
|
||||
art.fullscreenWeb = false
|
||||
const original = document.createElement('div')
|
||||
original.setAttribute('style', window.beforeRotation)
|
||||
const restored = ['width', 'height', 'transform', 'transform-origin'].every(name => art.template.$player.style.getPropertyValue(name) === original.style.getPropertyValue(name))
|
||||
const result = { rotated, restored, state: art.plugins.autoOrientation.state, isRotate: art.isRotate }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ rotated: { state: true, width: '800px', height: '400px' }, restored: core === 'candidate', state: false, isRotate: false })
|
||||
})
|
||||
|
||||
test(`${core}: late orientation lock after fullscreen exit does not reactivate the player`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const calls = []
|
||||
let resolveLock
|
||||
Object.defineProperty(screen, 'orientation', { configurable: true, value: {
|
||||
type: 'portrait-primary',
|
||||
lock(mode) {
|
||||
calls.push(mode)
|
||||
return new Promise((resolve) => {
|
||||
resolveLock = resolve
|
||||
})
|
||||
},
|
||||
unlock() {
|
||||
calls.push('unlock')
|
||||
},
|
||||
} })
|
||||
art.emit('fullscreen', true)
|
||||
art.emit('fullscreen', false)
|
||||
resolveLock()
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { requested: calls[0], unlocked: calls.includes('unlock'), active: art.template.$player.classList.contains('art-auto-orientation-fullscreen') }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ requested: 'landscape', unlocked: core === 'candidate', active: core !== 'candidate' })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(document, 'pictureInPictureEnabled', { configurable: true, value: false })
|
||||
const modes = new WeakMap()
|
||||
Object.defineProperties(HTMLVideoElement.prototype, {
|
||||
webkitSupportsPresentationMode: { configurable: true, writable: true, value: () => true },
|
||||
webkitPresentationMode: { configurable: true, get() { return modes.get(this) || 'inline' } },
|
||||
webkitSetPresentationMode: { configurable: true, writable: true, value(mode) {
|
||||
modes.set(this, mode)
|
||||
this.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
this.dispatchEvent(new Event(mode === 'picture-in-picture' ? 'enterpictureinpicture' : 'leavepictureinpicture'))
|
||||
} },
|
||||
})
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.pipEvents = []
|
||||
window.art.on('pip', value => window.pipEvents.push(value))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: WebKit PiP keeps boolean state and repeated synchronous setter events`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.pip = true
|
||||
art.pip = true
|
||||
const entered = art.pip
|
||||
art.pip = false
|
||||
const result = { entered, exited: art.pip, events: [...window.pipEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ entered: true, exited: false, events: [true, true, false] })
|
||||
})
|
||||
|
||||
test(`${core}: WebKit PiP follows native UI transitions`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const video = window.art.template.$video
|
||||
video.webkitSetPresentationMode('picture-in-picture')
|
||||
video.webkitSetPresentationMode('inline')
|
||||
const result = [...window.pipEvents]
|
||||
window.art.destroy()
|
||||
return result
|
||||
})).toEqual(core === 'candidate' ? [true, false] : [])
|
||||
})
|
||||
|
||||
test(`${core}: destroying WebKit PiP returns presentation to inline`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
window.art.pip = true
|
||||
window.pipEvents.length = 0
|
||||
window.art.destroy(false)
|
||||
return { mode: window.art.template.$video.webkitPresentationMode, events: window.pipEvents }
|
||||
})).toEqual({ mode: core === 'candidate' ? 'inline' : 'picture-in-picture', events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: WebKit PiP pending entry is cleaned up after destruction', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
const setMode = video.webkitSetPresentationMode.bind(video)
|
||||
video.webkitSetPresentationMode = () => {}
|
||||
art.pip = true
|
||||
art.destroy(false)
|
||||
video.webkitSetPresentationMode = setMode
|
||||
setMode('picture-in-picture')
|
||||
return { mode: video.webkitPresentationMode, events: window.pipEvents }
|
||||
})).toEqual({ mode: 'inline', events: [] })
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.addInitScript(() => {
|
||||
window.pipElement = null
|
||||
window.pipExits = 0
|
||||
Object.defineProperties(document, {
|
||||
pictureInPictureEnabled: { configurable: true, value: true },
|
||||
pictureInPictureElement: { configurable: true, get: () => window.pipElement },
|
||||
exitPictureInPicture: { configurable: true, writable: true, value() {
|
||||
window.pipExits++
|
||||
const previous = window.pipElement
|
||||
window.pipElement = null
|
||||
previous?.dispatchEvent(new Event('leavepictureinpicture'))
|
||||
return Promise.resolve()
|
||||
} },
|
||||
})
|
||||
Object.defineProperty(HTMLVideoElement.prototype, 'requestPictureInPicture', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value() {
|
||||
window.pipElement = this
|
||||
this.dispatchEvent(new Event('enterpictureinpicture'))
|
||||
return Promise.resolve({ width: 320, height: 180 })
|
||||
},
|
||||
})
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.pipEvents = []
|
||||
window.art.on('pip', value => window.pipEvents.push(value))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: native PiP preserves element/null getter, void setter and boolean events`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const setter = Object.getOwnPropertyDescriptor(art, 'pip').set
|
||||
const entered = setter.call(art, true)
|
||||
const ownElement = art.pip === art.template.$video
|
||||
const exited = setter.call(art, false)
|
||||
const result = { ownElement, empty: art.pip === null, entered: typeof entered, exited: typeof exited, events: [...window.pipEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ ownElement: true, empty: true, entered: 'undefined', exited: 'undefined', events: [true, false] })
|
||||
})
|
||||
|
||||
test(`${core}: a player cannot report or exit another video's PiP`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const other = document.createElement('video')
|
||||
window.pipElement = other
|
||||
const reported = window.art.pip === other
|
||||
window.art.pip = false
|
||||
const result = { reported, stillOther: window.pipElement === other, exits: window.pipExits }
|
||||
window.pipElement = null
|
||||
window.art.destroy()
|
||||
return result
|
||||
})).toEqual({ reported: core === 'published', stillOther: core === 'candidate', exits: core === 'published' ? 1 : 0 })
|
||||
})
|
||||
|
||||
test(`${core}: destroy releases active PiP without a late player event`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(async () => {
|
||||
window.art.pip = true
|
||||
await Promise.resolve()
|
||||
window.pipEvents.length = 0
|
||||
window.art.destroy(false)
|
||||
await Promise.resolve()
|
||||
return { active: Boolean(window.pipElement), events: window.pipEvents }
|
||||
})).toEqual({ active: core === 'published', events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
for (const action of ['cancel', 'destroy']) {
|
||||
test(`candidate: pending PiP ${action} releases late native entry`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(async (action) => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
let complete
|
||||
video.requestPictureInPicture = () => new Promise(resolve => complete = resolve)
|
||||
art.pip = true
|
||||
if (action === 'destroy')
|
||||
art.destroy(false)
|
||||
else
|
||||
art.pip = false
|
||||
window.pipElement = video
|
||||
video.dispatchEvent(new Event('enterpictureinpicture'))
|
||||
complete({ width: 320, height: 180 })
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { active: Boolean(window.pipElement), events: [...window.pipEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
}, action)).toEqual({ active: false, events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: rejected PiP assignment shows notice without an unhandled rejection', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.art.template.$video.requestPictureInPicture = () => Promise.reject(new Error('controlled PiP denial'))
|
||||
window.art.pip = true
|
||||
})
|
||||
await expect(page.locator('.art-notice')).toContainText('controlled PiP denial')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test('candidate: actual native PiP gesture and playback or explicit unsupported capability', async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=candidate&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(() => ({
|
||||
enabled: document.pictureInPictureEnabled === true,
|
||||
request: typeof window.art.template.$video.requestPictureInPicture === 'function',
|
||||
exit: typeof document.exitPictureInPicture === 'function',
|
||||
}))
|
||||
await testInfo.attach('native-pip-capability', { contentType: 'application/json', body: JSON.stringify(capability) })
|
||||
if (!capability.enabled || !capability.request || !capability.exit) {
|
||||
expect(await page.evaluate(() => window.art.pip)).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
return
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
const video = window.art.template.$video
|
||||
const request = video.requestPictureInPicture.bind(video)
|
||||
video.requestPictureInPicture = () => {
|
||||
const result = request()
|
||||
void result.then(pipWindow => window.actualPipWindow = { width: pipWindow.width, height: pipWindow.height })
|
||||
return result
|
||||
}
|
||||
document.querySelector('#play').onclick = () => {
|
||||
void window.art.play()
|
||||
window.art.pip = true
|
||||
}
|
||||
})
|
||||
await page.evaluate(() => window.art.mini = true)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.pip === window.art.template.$video)).toBe(true)
|
||||
expect(await page.evaluate(() => window.art.mini)).toBe(false)
|
||||
await expect.poll(() => page.evaluate(() => window.actualPipWindow?.width > 0 && window.art.currentTime > 0 && !window.art.template.$video.paused)).toBe(true)
|
||||
await testInfo.attach('native-pip-playback', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ window: window.actualPipWindow, currentTime: window.art.currentTime, events: window.events }))) })
|
||||
await page.evaluate(() => window.art.fullscreenWeb = true)
|
||||
await expect.poll(() => page.evaluate(() => document.pictureInPictureElement)).toBe(null)
|
||||
expect(await page.evaluate(() => window.art.fullscreenWeb)).toBe(true)
|
||||
await page.evaluate(() => window.art.fullscreenWeb = false)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.pip === window.art.template.$video)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('#play').onclick = () => window.art.fullscreen = true
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen && !document.pictureInPictureElement)).toBe(true)
|
||||
await page.evaluate(() => window.art.fullscreen = false)
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen)).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
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}: sizing methods remain detachable own properties and ratio parsing retains legacy coercion`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const descriptors = ['autoSize', 'autoHeight', 'aspectRatio', 'flip'].map((name) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(art, name)
|
||||
return { name, configurable: descriptor.configurable, enumerable: descriptor.enumerable, writable: descriptor.writable ?? null }
|
||||
})
|
||||
const { autoSize, autoHeight } = art
|
||||
const returns = [typeof Reflect.apply(autoSize, null, []), typeof Reflect.apply(autoHeight, {}, [])]
|
||||
art.template.$player.style.cssText = 'width:640px;height:360px;'
|
||||
const events = []
|
||||
art.on('aspectRatio', value => events.push(value))
|
||||
art.aspectRatio = ' 4 : 3 : ignored'
|
||||
const parsed = { value: art.aspectRatio, width: art.template.$video.style.width, height: art.template.$video.style.height }
|
||||
art.aspectRatio = ''
|
||||
art.aspectRatio = 'default'
|
||||
art.flip = 'custom'
|
||||
const custom = art.flip
|
||||
art.flip = ''
|
||||
const result = { descriptors, returns, parsed, events, custom, reset: art.flip }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({
|
||||
descriptors: ['autoSize', 'autoHeight', 'aspectRatio', 'flip'].map(name => ({ name, configurable: false, enumerable: false, writable: name.startsWith('auto') ? false : null })),
|
||||
returns: ['undefined', 'undefined'],
|
||||
parsed: { value: ' 4 : 3 : ignored', width: '480px', height: '100%' },
|
||||
events: [' 4 : 3 : ignored', 'default', 'default'],
|
||||
custom: 'custom',
|
||||
reset: 'normal',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: hidden containers defer sizing and recover after becoming visible', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $container, $video, $player } = art.template
|
||||
Object.defineProperties($video, { videoWidth: { configurable: true, value: 1920 }, videoHeight: { configurable: true, value: 1080 } })
|
||||
$container.style.cssText = 'display:none;width:640px;height:480px;'
|
||||
$player.style.cssText = 'width:80%;height:70%;'
|
||||
const events = []
|
||||
art.on('autoSize', value => events.push(['size', value]))
|
||||
art.on('autoHeight', value => events.push(['height', value]))
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
const hidden = { width: $player.style.width, height: $player.style.height, containerHeight: $container.style.height, count: events.length }
|
||||
$container.style.display = 'block'
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
const result = { hidden, events, height: $container.style.height }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ hidden: { width: '80%', height: '70%', containerHeight: '480px', count: 0 }, events: [['size', { width: 640, height: 360 }], ['height', 360]], height: '360px' })
|
||||
})
|
||||
|
||||
test('candidate: malformed ratios preserve layout while retaining notices, datasets and events', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $video } = art.template
|
||||
art.aspectRatio = '4:3'
|
||||
const before = $video.getAttribute('style')
|
||||
const values = []
|
||||
art.on('aspectRatio', value => values.push(value))
|
||||
const stable = ['bad', '0:0', '-1:2', '1:0', 'Infinity:2'].map((value) => {
|
||||
art.aspectRatio = value
|
||||
return $video.getAttribute('style') === before && art.aspectRatio === value && art.template.$notice.textContent.includes(value)
|
||||
})
|
||||
art.destroy()
|
||||
return { stable, values }
|
||||
})).toEqual({ stable: [true, true, true, true, true], values: ['bad', '0:0', '-1:2', '1:0', 'Infinity:2'] })
|
||||
})
|
||||
|
||||
test('candidate: retained display setters and sizing methods do not write or emit after destroy', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $container, $player, $video } = art.template
|
||||
art.destroy(false)
|
||||
const before = [$container.outerHTML, $player.outerHTML, $video.outerHTML]
|
||||
const events = []
|
||||
for (const name of ['autoSize', 'autoHeight', 'aspectRatio', 'flip'])
|
||||
art.on(name, () => events.push(name))
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
art.aspectRatio = '4:3'
|
||||
art.flip = 'horizontal'
|
||||
return { same: before.every((html, index) => html === [$container, $player, $video][index].outerHTML), events }
|
||||
})).toEqual({ same: true, events: [] })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: sizing preserves valid geometry, return values and emitted measurements`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $container, $player, $video } = art.template
|
||||
Object.defineProperties($video, { videoWidth: { configurable: true, value: 1920 }, videoHeight: { configurable: true, value: 1080 } })
|
||||
$container.style.cssText = 'width:640px;height:480px;'
|
||||
const sizes = []
|
||||
const heights = []
|
||||
art.on('autoSize', value => sizes.push(value))
|
||||
art.on('autoHeight', value => heights.push(value))
|
||||
const autoSize = art.autoSize()
|
||||
const styles = { width: $player.style.width, height: $player.style.height }
|
||||
const autoHeight = art.autoHeight()
|
||||
const result = { styles, sizes, heights, returns: [typeof autoSize, typeof autoHeight], containerHeight: $container.style.height }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ styles: { width: '100%', height: '75%' }, sizes: [{ width: 640, height: 360 }], heights: [360], returns: ['undefined', 'undefined'], containerHeight: '360px' })
|
||||
})
|
||||
|
||||
test(`${core}: aspect ratio and flip preserve dataset, notices and event order`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const events = []
|
||||
art.on('aspectRatio', value => events.push(['aspectRatio', value]))
|
||||
art.on('flip', value => events.push(['flip', value]))
|
||||
art.aspectRatio = '4:3'
|
||||
const video = art.template.$video
|
||||
const ratio = { width: video.style.width, height: video.style.height, margin: video.style.margin, value: art.aspectRatio }
|
||||
art.flip = 'horizontal'
|
||||
const flip = art.flip
|
||||
art.aspectRatio = 'default'
|
||||
art.flip = 'normal'
|
||||
const result = { ratio, flip, reset: { width: video.style.width, height: video.style.height, margin: video.style.margin, ratio: art.aspectRatio, flip: art.flip }, events }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ ratio: { width: '480px', height: '100%', margin: '0px auto', value: '4:3' }, flip: 'horizontal', reset: { width: '', height: '', margin: '', ratio: 'default', flip: 'normal' }, events: [['aspectRatio', '4:3'], ['flip', 'horizontal'], ['aspectRatio', 'default'], ['flip', 'normal']] })
|
||||
})
|
||||
|
||||
test(`${core}: unresolved media dimensions do not partially change layout or emit NaN height`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $video, $player } = art.template
|
||||
Object.defineProperties($video, { videoWidth: { configurable: true, value: 0 }, videoHeight: { configurable: true, value: 0 } })
|
||||
$player.style.width = '80%'
|
||||
$player.style.height = '70%'
|
||||
const events = []
|
||||
art.on('autoHeight', value => events.push(Number.isFinite(value)))
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
const result = { width: $player.style.width, height: $player.style.height, finiteHeightEvents: events }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ width: core === 'candidate' ? '80%' : '100%', height: '70%', finiteHeightEvents: core === 'candidate' ? [] : [false] })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.addInitScript(() => {
|
||||
// Exercise the video-only branch without claiming an iOS device test.
|
||||
for (const name of ['fullscreenEnabled', 'webkitFullscreenEnabled', 'webkitFullScreenEnabled', 'mozFullScreenEnabled', 'msFullscreenEnabled'])
|
||||
Object.defineProperty(document, name, { configurable: true, value: false })
|
||||
const state = new WeakMap()
|
||||
Object.defineProperties(HTMLVideoElement.prototype, {
|
||||
webkitSupportsFullscreen: { configurable: true, get: () => true },
|
||||
webkitDisplayingFullscreen: { configurable: true, get() { return state.get(this) === true } },
|
||||
webkitPresentationMode: { configurable: true, get() { return state.get(this) ? 'fullscreen' : 'inline' } },
|
||||
webkitEnterFullscreen: { configurable: true, writable: true, value() {
|
||||
state.set(this, true)
|
||||
this.dispatchEvent(new Event('webkitbeginfullscreen'))
|
||||
this.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
} },
|
||||
webkitExitFullscreen: { configurable: true, writable: true, value() {
|
||||
state.set(this, false)
|
||||
this.dispatchEvent(new Event('webkitendfullscreen'))
|
||||
this.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
} },
|
||||
})
|
||||
window.videoFullscreenState = state
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.modeEvents = []
|
||||
window.art.on('fullscreen', value => window.modeEvents.push(value))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: video-only fullscreen reads its native video state and deduplicates signals`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.fullscreen = true
|
||||
const entered = art.fullscreen
|
||||
art.emit('document:webkitfullscreenchange')
|
||||
art.fullscreen = false
|
||||
const result = { entered, exited: art.fullscreen, events: [...window.modeEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ entered: core === 'candidate', exited: false, events: core === 'candidate' ? [true, false] : [false] })
|
||||
})
|
||||
|
||||
test(`${core}: destroying video-only fullscreen releases native presentation`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.fullscreen = true
|
||||
window.modeEvents.length = 0
|
||||
art.destroy(false)
|
||||
return { native: art.template.$video.webkitDisplayingFullscreen, events: window.modeEvents }
|
||||
})).toEqual({ native: core === 'published', events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: cancelled video-only entry after destroy is released without late player events', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
video.webkitEnterFullscreen = () => {}
|
||||
art.fullscreen = true
|
||||
art.destroy(false)
|
||||
window.videoFullscreenState.set(video, true)
|
||||
video.dispatchEvent(new Event('webkitbeginfullscreen'))
|
||||
return { native: video.webkitDisplayingFullscreen, events: window.modeEvents }
|
||||
})).toEqual({ native: false, events: [] })
|
||||
})
|
||||
|
||||
test('candidate: video-only fullscreen preserves synchronous errors and supports exit retry', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
art.fullscreen = true
|
||||
const exit = video.webkitExitFullscreen
|
||||
const failure = new Error('controlled video exit denial')
|
||||
video.webkitExitFullscreen = () => {
|
||||
throw failure
|
||||
}
|
||||
let rejected = false
|
||||
try {
|
||||
art.fullscreen = false
|
||||
}
|
||||
catch (error) { rejected = error === failure }
|
||||
const active = art.fullscreen
|
||||
video.webkitExitFullscreen = exit
|
||||
art.fullscreen = false
|
||||
const result = { rejected, active, exited: art.fullscreen, events: window.modeEvents }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ rejected: true, active: true, exited: false, events: [true, false] })
|
||||
})
|
||||
@@ -0,0 +1,233 @@
|
||||
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 removeHtml of [false, true]) {
|
||||
for (const movedBeforeThrow of [false, true]) {
|
||||
test(`candidate: failed destroy restoration cannot orphan the player removeHtml=${removeHtml} movedBeforeThrow=${movedBeforeThrow}`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(({ removeHtml, movedBeforeThrow }) => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.fullscreenWeb = true
|
||||
const original = container.insertBefore
|
||||
const failure = new Error('destroy restore denied')
|
||||
container.insertBefore = function (node, anchor) {
|
||||
if (movedBeforeThrow)
|
||||
original.call(this, node, anchor)
|
||||
throw failure
|
||||
}
|
||||
let sameError = false
|
||||
try {
|
||||
art.destroy(removeHtml)
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
container.insertBefore = original
|
||||
return { sameError, orphan: player.isConnected && !container.contains(player), retained: container.contains(player), destroyed: art.isDestroy, registered: art.constructor.instances.includes(art) }
|
||||
}, { removeHtml, movedBeforeThrow })).toEqual({ sameError: true, orphan: false, retained: movedBeforeThrow && !removeHtml, destroyed: true, registered: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test('candidate: web fullscreen listener can exit without stale resize or overwritten restoration', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
const result = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const player = art.template.$player
|
||||
player.style.width = '75%'
|
||||
const style = player.style.cssText
|
||||
const events = []
|
||||
art.on('fullscreenWeb', (value) => {
|
||||
events.push(value)
|
||||
if (value)
|
||||
art.fullscreenWeb = false
|
||||
})
|
||||
art.on('resize', () => events.push('resize'))
|
||||
art.fullscreenWeb = true
|
||||
const result = { events, active: art.fullscreenWeb, restored: player.style.cssText === style }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ events: [true, false, 'resize'], active: false, restored: true })
|
||||
})
|
||||
|
||||
test('candidate: web fullscreen listener destroy restores ownership without post-destroy resize', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
const style = player.style.cssText
|
||||
const events = []
|
||||
art.on('fullscreenWeb', () => art.destroy(false))
|
||||
art.on('destroy', () => events.push('destroy'))
|
||||
art.on('resize', () => events.push('resize'))
|
||||
art.fullscreenWeb = true
|
||||
art.fullscreenWeb = true
|
||||
return { events, retained: container.contains(player), style: player.style.cssText === style, fullscreen: player.classList.contains('art-fullscreen-web') }
|
||||
})).toEqual({ events: ['destroy'], retained: true, style: true, fullscreen: false })
|
||||
})
|
||||
|
||||
test('candidate: a failed body move restores style and keeps its original error', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
const style = player.getAttribute('style')
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
const append = document.body.appendChild
|
||||
const failure = new Error('controlled body insertion failure')
|
||||
document.body.appendChild = function (node) {
|
||||
if (node === player)
|
||||
throw failure
|
||||
return append.call(this, node)
|
||||
}
|
||||
let sameError = false
|
||||
try {
|
||||
art.fullscreenWeb = true
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
document.body.appendChild = append
|
||||
const result = { sameError, retained: container.contains(player), style: player.getAttribute('style') === style, active: art.fullscreenWeb }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ sameError: true, retained: true, style: true, active: false })
|
||||
})
|
||||
|
||||
test('candidate: a failed web fullscreen exit can retry restoring the original placement', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
const style = player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
const insert = container.insertBefore
|
||||
const failure = new Error('controlled restore failure')
|
||||
container.insertBefore = () => {
|
||||
throw failure
|
||||
}
|
||||
let sameError = false
|
||||
try {
|
||||
art.fullscreenWeb = false
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
container.insertBefore = insert
|
||||
art.fullscreenWeb = false
|
||||
const result = { sameError, retained: container.contains(player), style: player.getAttribute('style') === style, active: art.fullscreenWeb }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ sameError: true, retained: true, style: true, active: false })
|
||||
})
|
||||
|
||||
test('candidate: reentry during web fullscreen restoration preserves the newer active mode', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
player.style.width = '65%'
|
||||
const style = player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
const insert = container.insertBefore
|
||||
container.insertBefore = function (node, anchor) {
|
||||
container.insertBefore = insert
|
||||
const result = insert.call(this, node, anchor)
|
||||
art.fullscreenWeb = true
|
||||
return result
|
||||
}
|
||||
art.fullscreenWeb = false
|
||||
const winner = art.fullscreenWeb && player.parentNode === document.body && player.style.width === '100%'
|
||||
art.fullscreenWeb = false
|
||||
const restored = container.contains(player) && player.getAttribute('style') === style
|
||||
art.destroy()
|
||||
return { winner, restored }
|
||||
})).toEqual({ winner: true, restored: true })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const initial of ['', 'width: 70%; height: 80%; color: red;']) {
|
||||
test(`${core}: web fullscreen restores initial style after repeated entry (${initial || 'empty'})`, async ({ page }, testInfo) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate((initial) => {
|
||||
const art = window.art
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = false
|
||||
const player = art.template.$player
|
||||
player.style.cssText = initial
|
||||
const before = player.style.cssText
|
||||
const events = []
|
||||
let restoredAtExit
|
||||
art.on('fullscreenWeb', (value) => {
|
||||
events.push(value)
|
||||
if (!value)
|
||||
restoredAtExit = player.style.cssText
|
||||
})
|
||||
art.fullscreenWeb = true
|
||||
art.fullscreenWeb = true
|
||||
art.fullscreenWeb = false
|
||||
const result = { restored: restoredAtExit === before, events, active: art.fullscreenWeb, dimensions: [player.style.width, player.style.height, player.style.color], styles: { before, restoredAtExit, after: player.style.cssText } }
|
||||
art.destroy()
|
||||
return result
|
||||
}, initial)
|
||||
const { styles, ...observed } = result
|
||||
await testInfo.attach('fullscreen-styles', { contentType: 'application/json', body: JSON.stringify(styles) })
|
||||
expect(observed).toEqual({ restored: core === 'candidate', events: [true, true, false], active: false, dimensions: core === 'candidate' ? (initial ? ['70%', '80%', 'red'] : ['', '', '']) : ['100%', '100%', initial ? 'red' : ''] })
|
||||
})
|
||||
}
|
||||
|
||||
for (const changeFlag of [false, true]) {
|
||||
test(`${core}: body fullscreen restores original parent and sibling when flag changes=${changeFlag}`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate((changeFlag) => {
|
||||
const art = window.art
|
||||
const player = art.template.$player
|
||||
const wrapper = document.createElement('div')
|
||||
const sibling = document.createElement('span')
|
||||
art.template.$container.append(wrapper)
|
||||
wrapper.append(player, sibling)
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.fullscreenWeb = true
|
||||
const entered = player.parentNode === document.body
|
||||
if (changeFlag)
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = false
|
||||
art.fullscreenWeb = false
|
||||
const result = { entered, parent: player.parentNode === wrapper, sibling: player.nextSibling === sibling, inBody: player.parentNode === document.body }
|
||||
art.destroy()
|
||||
player.remove()
|
||||
return result
|
||||
}, changeFlag)
|
||||
expect(result).toEqual({ entered: true, parent: core === 'candidate', sibling: core === 'candidate', inBody: core === 'published' && changeFlag })
|
||||
})
|
||||
}
|
||||
|
||||
for (const removeHtml of [false, true]) {
|
||||
test(`${core}: destroy while web fullscreen removeHtml=${removeHtml} restores DOM ownership`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate((removeHtml) => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
const style = player.style.cssText
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.fullscreenWeb = true
|
||||
art.destroy(removeHtml)
|
||||
const result = { orphan: player.parentNode === document.body, retained: container.contains(player), style: player.style.cssText === style, fullscreen: player.classList.contains('art-fullscreen-web') }
|
||||
player.remove()
|
||||
return result
|
||||
}, removeHtml)
|
||||
expect(result).toEqual(core === 'candidate'
|
||||
? { orphan: false, retained: !removeHtml, style: true, fullscreen: false }
|
||||
: { orphan: true, retained: false, style: false, fullscreen: true })
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user