mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 11:56:15 -08:00
256 lines
14 KiB
JavaScript
256 lines
14 KiB
JavaScript
import { expect, test } from './fixtures.js'
|
|
|
|
async function openChapter(page, chapter, chapters = []) {
|
|
await page.goto(`/test/player.html?core=published&chapter=${chapter}`)
|
|
await page.evaluate((chapters) => {
|
|
window.chapterInput = chapters
|
|
window.createPlayer('/test/pattern.mp4', { chapters })
|
|
}, chapters)
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
}
|
|
|
|
function segments(page) {
|
|
return page.locator('.art-chapter').evaluateAll(nodes => nodes.map(node => ({
|
|
start: node.dataset.start,
|
|
end: node.dataset.end,
|
|
title: node.dataset.title,
|
|
width: node.style.width,
|
|
})))
|
|
}
|
|
|
|
async function hover(page, percentage) {
|
|
const box = await page.locator('.art-control-progress-inner').boundingBox()
|
|
const x = Math.round(box.x + box.width * percentage)
|
|
await page.mouse.move(x, box.y + box.height / 2)
|
|
return (x - box.x) / box.width
|
|
}
|
|
|
|
for (const chapter of ['published', 'candidate']) {
|
|
test.describe(`${chapter} chapter on published core`, () => {
|
|
test('factory, registration, input normalization and safe title text', async ({ page }) => {
|
|
await openChapter(page, chapter, [{ start: 4, end: 6, title: ' Second ' }, { start: 1, end: 2, title: '<b>First</b>' }])
|
|
expect(await page.evaluate(() => {
|
|
const plugin = window.art.plugins.artplayerPluginChapter
|
|
return { factory: typeof window.artplayerPluginChapter(), name: plugin.name, keys: Object.keys(plugin).sort(), input: window.chapterInput }
|
|
})).toEqual({
|
|
factory: 'function',
|
|
name: 'artplayerPluginChapter',
|
|
keys: ['name', 'update'],
|
|
input: [{ start: 0, end: 1, title: '' }, { start: 1, end: 2, title: '<b>First</b>' }, { start: 2, end: 4, title: '' }, { start: 4, end: 6, title: ' Second ' }, { start: 6, end: 8, title: '' }],
|
|
})
|
|
expect(await segments(page)).toEqual([
|
|
{ start: '0', end: '1', title: '', width: '12.5%' },
|
|
{ start: '1', end: '2', title: '<b>First</b>', width: '12.5%' },
|
|
{ start: '2', end: '4', title: '', width: '25%' },
|
|
{ start: '4', end: '6', title: 'Second', width: '25%' },
|
|
{ start: '6', end: '8', title: '', width: '25%' },
|
|
])
|
|
await hover(page, 0.2)
|
|
await expect(page.locator('.art-chapter-title')).toHaveText('<b>First</b>')
|
|
await expect(page.locator('.art-chapter-title b')).toHaveCount(0)
|
|
await expect(page.locator('style#artplayer-plugin-chapter')).toHaveCount(1)
|
|
await page.evaluate(() => {
|
|
window.art.destroy()
|
|
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, plugins: [window.artplayerPluginChapter()] })
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
await expect(page.locator('.art-chapter')).toHaveCount(0)
|
|
})
|
|
|
|
test('progress bars, real hover, title edges and boundary seek', async ({ page }) => {
|
|
await openChapter(page, chapter, [{ start: 0, end: 2, title: 'First chapter' }, { start: 2, end: 6, title: 'Middle' }, { start: 6, end: 8, title: 'Last chapter' }])
|
|
for (const type of ['played', 'loaded']) {
|
|
const widths = await page.evaluate((type) => {
|
|
window.art.emit('setBar', type, 0.375)
|
|
return [...document.querySelectorAll(`.art-chapter .art-progress-${type}`)].map(node => node.style.width)
|
|
}, type)
|
|
expect(widths).toEqual(['100%', '25%', '0px'])
|
|
}
|
|
const percentage = await hover(page, 0.375)
|
|
const widths = await page.locator('.art-chapter .art-progress-hover').evaluateAll(nodes => nodes.map(node => Number.parseFloat(node.style.width)))
|
|
for (const [index, expected] of [100, (percentage * 8 - 2) / 4 * 100, 0].entries())
|
|
expect(widths[index]).toBeCloseTo(expected, 1)
|
|
const box = await page.locator('.art-control-progress-inner').boundingBox()
|
|
for (const [percentage, title] of [[0.01, 'First chapter'], [0.5, 'Middle'], [0.99, 'Last chapter']]) {
|
|
await page.mouse.move(box.x + box.width * percentage, box.y + box.height / 2)
|
|
await expect(page.locator('.art-chapter-title')).toHaveText(title)
|
|
await expect(page.locator('.art-video-player')).toHaveClass(/art-progress-hover/)
|
|
const layout = await page.locator('.art-chapter-title').evaluate(title => ({ left: Number.parseFloat(title.style.left), width: title.clientWidth, container: title.parentElement.clientWidth }))
|
|
expect(layout.left).toBeGreaterThanOrEqual(0)
|
|
expect(layout.left + layout.width).toBeLessThanOrEqual(layout.container)
|
|
}
|
|
await page.mouse.click(box.x + box.width / 4, box.y + box.height / 2)
|
|
await expect.poll(() => page.locator('video').evaluate(video => video.currentTime)).toBeCloseTo(2, 1)
|
|
expect(await page.evaluate(() => {
|
|
window.art.emit('setBar', 'played', 1)
|
|
return [...document.querySelectorAll('.art-chapter .art-progress-played')].map(node => node.style.width)
|
|
})).toEqual(['100%', '100%', '100%'])
|
|
})
|
|
|
|
test('sync update, Infinity, object identity, empty lists and invalid data', async ({ page }) => {
|
|
await openChapter(page, chapter)
|
|
await expect(page.locator('.art-chapter')).toHaveCount(0)
|
|
expect(await page.evaluate(() => {
|
|
const entry = { start: 0, end: Infinity, title: 'All' }
|
|
const chapters = [entry]
|
|
const result = window.art.plugins.artplayerPluginChapter.update({ chapters })
|
|
return { result, same: chapters[0] === entry, end: entry.end }
|
|
})).toEqual({ result: undefined, same: true, end: 8 })
|
|
await expect(page.locator('.art-chapter')).toHaveCount(1)
|
|
await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({}))
|
|
await expect(page.locator('.art-chapter')).toHaveCount(0)
|
|
await expect(page.locator('.art-video-player')).not.toHaveClass(/artplayer-plugin-chapter/)
|
|
const cases = [
|
|
[[{ start: '0', end: 2, title: 'Bad' }], 'TypeError', 'Illegal chapter data type'],
|
|
[[{ start: 0, end: 2, title: 12 }], 'TypeError', 'Illegal chapter data type'],
|
|
[[{ start: -1, end: 2, title: 'Bad' }], 'Error', 'Illegal chapter time point'],
|
|
[[{ start: 2, end: 2, title: 'Bad' }], 'Error', 'Illegal chapter time point'],
|
|
[[{ start: 0, end: 9, title: 'Bad' }], 'Error', 'Illegal chapter time point'],
|
|
[[{ start: 0, end: 4, title: 'A' }, { start: 3, end: 6, title: 'B' }], 'Error', 'Illegal chapter time point'],
|
|
]
|
|
for (const [chapters, name, message] of cases) {
|
|
expect(await page.evaluate((chapters) => {
|
|
const update = window.art.plugins.artplayerPluginChapter.update
|
|
update({ chapters: [{ start: 0, end: Infinity, title: 'Valid before failure' }] })
|
|
try {
|
|
update({ chapters })
|
|
}
|
|
catch (error) { return { name: error.name, message: error.message } }
|
|
}, chapters)).toEqual({ name, message })
|
|
await expect(page.locator('.art-chapter')).toHaveCount(0)
|
|
}
|
|
expect(await page.evaluate(() => {
|
|
try {
|
|
window.art.plugins.artplayerPluginChapter.update()
|
|
}
|
|
catch (error) { return error.name }
|
|
})).toBe('TypeError')
|
|
})
|
|
|
|
test('explicit source update and core-owned destruction', async ({ page }) => {
|
|
await openChapter(page, chapter, [{ start: 0, end: 8, title: 'Initial' }])
|
|
await page.evaluate(() => window.art.switchUrl('/assets/sample/video.mp4'))
|
|
expect(await page.locator('video').evaluate(video => video.duration)).toBeGreaterThan(90)
|
|
await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: 0, end: Infinity, title: 'New source' }] }))
|
|
expect(await page.locator('.art-chapter').evaluate(node => Number(node.dataset.end))).toBeGreaterThan(90)
|
|
await page.evaluate(() => window.art.destroy())
|
|
await expect(page.locator('.player')).toBeEmpty()
|
|
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
|
|
await expect(page.locator('style#artplayer-plugin-chapter')).toHaveCount(1)
|
|
})
|
|
})
|
|
}
|
|
|
|
test('published-only observations: blank title, NaN and once-only metadata are historical defects', async ({ page }, testInfo) => {
|
|
await openChapter(page, 'published', [{ start: 0, end: 2, title: 'Previous' }, { start: 4, end: 8, title: 'Next' }])
|
|
await hover(page, 0.1)
|
|
await hover(page, 0.375)
|
|
const staleTitle = await page.locator('.art-chapter-title').textContent()
|
|
expect(staleTitle).toBe('Previous')
|
|
await page.evaluate(() => window.art.switchUrl('/assets/sample/video.mp4'))
|
|
expect(await page.locator('.art-chapter').last().evaluate(node => node.dataset.end)).toBe('8')
|
|
await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: Number.NaN, end: 3, title: 'Invalid' }] }))
|
|
expect(await page.locator('.art-chapter').first().evaluate(node => node.dataset.start)).toBe('NaN')
|
|
await testInfo.attach('chapter-known-defects', { body: JSON.stringify({ staleTitle, endAfterSourceChange: 8, acceptsNaN: true }), contentType: 'application/json' })
|
|
})
|
|
|
|
test('candidate clears titles on blank chapters, zero hover and replacement', async ({ page }) => {
|
|
await openChapter(page, 'candidate', [{ start: 0, end: 2, title: 'Previous' }, { start: 4, end: 8, title: 'Next' }])
|
|
const title = page.locator('.art-chapter-title')
|
|
await hover(page, 0.1)
|
|
await expect(title).toHaveText('Previous')
|
|
await hover(page, 0.375)
|
|
await expect(title).toHaveText('')
|
|
await expect(title).toHaveCSS('visibility', 'hidden')
|
|
await hover(page, 0.75)
|
|
await expect(title).toHaveText('Next')
|
|
await hover(page, 0)
|
|
await expect(title).toHaveText('')
|
|
await hover(page, 0.1)
|
|
await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: 0, end: Infinity, title: 'Replacement' }] }))
|
|
await expect(title).toHaveText('')
|
|
await hover(page, 0.5)
|
|
await expect(title).toHaveText('Replacement')
|
|
await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({}))
|
|
await expect(title).toHaveText('')
|
|
})
|
|
|
|
test('candidate rejects NaN and can recover after an invalid update', async ({ page }) => {
|
|
await openChapter(page, 'candidate', [{ start: 0, end: 8, title: 'Valid' }])
|
|
expect(await page.evaluate(() => {
|
|
try {
|
|
window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: Number.NaN, end: 3, title: 'Invalid' }] })
|
|
}
|
|
catch (error) { return error.message }
|
|
})).toBe('Illegal chapter time point')
|
|
await expect(page.locator('.art-chapter')).toHaveCount(0)
|
|
await expect(page.locator('.art-video-player')).not.toHaveClass(/artplayer-plugin-chapter/)
|
|
await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: 0, end: Infinity, title: 'Recovered' }] }))
|
|
await expect(page.locator('.art-chapter')).toHaveCount(1)
|
|
await hover(page, 0.5)
|
|
await expect(page.locator('.art-chapter-title')).toHaveText('Recovered')
|
|
})
|
|
|
|
test('candidate releases only its listeners and DOM across multiple instances and early destruction', async ({ page }) => {
|
|
await openChapter(page, 'candidate', [{ start: 0, end: 8, title: 'First' }])
|
|
expect(await page.evaluate(() => {
|
|
const art = window.art
|
|
const plugin = art.plugins.artplayerPluginChapter
|
|
let observerCalls = 0
|
|
const observer = () => observerCalls++
|
|
art.on('setBar', observer)
|
|
const listenersBefore = art.e.setBar.length
|
|
const container = document.createElement('div')
|
|
container.className = 'second-player'
|
|
document.body.appendChild(container)
|
|
const other = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true, plugins: [window.artplayerPluginChapter()] })
|
|
other.plugins.artplayerPluginChapter.update({ chapters: [] })
|
|
art.destroy(false)
|
|
const entry = { start: 0, end: Infinity, title: 'Ignored' }
|
|
plugin.update({ chapters: [entry] })
|
|
const listenersAfter = art.e.setBar.length
|
|
art.emit('setBar', 'loaded', 0.5)
|
|
const result = {
|
|
ownDom: art.template.$player.querySelectorAll('.art-chapters, .art-chapter-title').length,
|
|
classRemoved: !art.template.$player.classList.contains('artplayer-plugin-chapter'),
|
|
listenersRemoved: listenersBefore - listenersAfter,
|
|
observerCalls,
|
|
inputUntouched: entry.end === Infinity,
|
|
otherDom: container.querySelectorAll('.art-chapters, .art-chapter-title').length,
|
|
otherAlive: !other.isDestroy,
|
|
}
|
|
other.destroy()
|
|
return result
|
|
})).toEqual({ ownDom: 0, classRemoved: true, listenersRemoved: 1, observerCalls: 1, inputUntouched: true, otherDom: 2, otherAlive: true })
|
|
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
|
|
await expect(page.locator('style#artplayer-plugin-chapter')).toHaveCount(1)
|
|
})
|
|
|
|
test('candidate ignores pending metadata initialization after immediate destroy', async ({ page }) => {
|
|
await page.goto('/test/player.html?core=published&chapter=candidate')
|
|
expect(await page.evaluate(() => {
|
|
const original = window.artplayerPluginChapter
|
|
let initialize
|
|
let plugin
|
|
const chapters = [{ start: 0, end: Infinity, title: 'Pending' }]
|
|
const art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, plugins: [(art) => {
|
|
const count = art.e['video:loadedmetadata']?.length || 0
|
|
plugin = original({ chapters })(art)
|
|
initialize = art.e['video:loadedmetadata'][count].fn._
|
|
return plugin
|
|
}] })
|
|
const before = art.e['video:loadedmetadata'].length
|
|
art.destroy()
|
|
const after = art.e['video:loadedmetadata']?.length || 0
|
|
initialize()
|
|
plugin.update({ chapters })
|
|
return { removed: before - after, untouched: chapters[0].end === Infinity, children: document.querySelector('.player').childElementCount }
|
|
})).toEqual({ removed: 1, untouched: true, children: 0 })
|
|
})
|
|
|
|
test('candidate deduplicates stylesheet injection while the document is loading', async ({ page, baseURL }) => {
|
|
await page.setContent(`<html><head><script src="${baseURL}/candidate/artplayer-plugin-chapter.js"></script><script src="${baseURL}/candidate/artplayer-plugin-chapter.js"></script></head><body></body></html>`)
|
|
await expect(page.locator('style#artplayer-plugin-chapter')).toHaveCount(1)
|
|
expect(await page.locator('style#artplayer-plugin-chapter').textContent()).toContain('.art-chapters')
|
|
})
|