Files
ArtPlayer/test/browser/chapter-combinations.spec.js
T

222 lines
13 KiB
JavaScript

import process from 'node:process'
import { devices } from '@playwright/test'
import { hoverProgress } from './chapter-hover.ts'
import { installChapterTiming, readChapterTiming } from './chapter-timing.js'
import { expect, test } from './fixtures.js'
async function openChapter(page, core, chapter) {
await page.goto(`/test/player.html?core=${core}&chapter=${chapter}`)
await page.evaluate(() => {
document.querySelector('.player').style.cssText = 'width:min(640px,100%);height:300px'
window.chapterEvents = []
window.chapterMediaEvents = []
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4?quality=B',
muted: true,
fullscreen: true,
fullscreenWeb: true,
miniProgressBar: true,
quality: [
{ html: 'Quality A', url: '/test/pattern.mp4?quality=A' },
{ html: 'Quality B', url: '/test/pattern.mp4?quality=B', default: true },
],
thumbnails: { url: '/test/thumbnail-grid.svg', number: 100, column: 10, width: 100, height: 60 },
plugins: [window.artplayerPluginChapter({ chapters: [
{ start: 0, end: 2, title: 'Opening' },
{ start: 2, end: 6, title: 'Middle' },
{ start: 6, end: 8, title: 'Ending' },
] })],
})
for (const name of ['restart', 'seek', 'fullscreen', 'fullscreenWeb'])
window.art.on(name, value => window.chapterEvents.push({ name, value, at: performance.now() }))
for (const name of ['loadedmetadata', 'loadeddata', 'canplay', 'seeking', 'seeked', 'playing', 'pause', 'error'])
window.art.video.addEventListener(name, () => window.chapterMediaEvents.push({ name, at: performance.now(), time: window.art.currentTime, source: window.art.video.currentSrc, paused: window.art.video.paused, readyState: window.art.video.readyState }))
document.querySelector('#play').onclick = () => window.art.play()
document.querySelector('#pause').onclick = () => window.art.pause()
})
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await expect(page.locator('.art-chapter')).toHaveCount(3)
if (process.env.ARTPLAYER_CHAPTER_TIMING_DIAGNOSTICS === '1')
await installChapterTiming(page)
}
async function hoverChapter(page, percentage, text) {
const { box, x } = await hoverProgress(page, percentage)
await expect(page.locator('.art-chapter-title')).toHaveText(text)
await expect(page.locator('.art-chapter-title')).toHaveCSS('opacity', '1')
await expect(page.locator('.art-control-thumbnails')).toBeVisible()
await expect.poll(() => page.evaluate(() => window.art.controls.thumbnails.style.backgroundImage)).not.toBe('')
const layout = await page.evaluate(() => {
const rect = (element) => {
const box = element.getBoundingClientRect()
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom }
}
return {
title: rect(document.querySelector('.art-chapter-title')),
thumbnail: rect(window.art.controls.thumbnails),
progress: rect(document.querySelector('.art-control-progress-inner')),
}
})
expect(layout.title.left).toBeGreaterThanOrEqual(layout.progress.left - 1)
expect(layout.title.right).toBeLessThanOrEqual(layout.progress.right + 1)
expect(layout.thumbnail.bottom).toBeLessThanOrEqual(layout.title.top)
return { box, x, layout }
}
async function waitForChapterRestart(page, testInfo) {
const calls = []
try {
await expect.poll(async () => {
const started = Date.now()
const state = await page.evaluate(() => ({
at: performance.now(),
timeOrigin: performance.timeOrigin,
events: window.chapterEvents.filter(event => event.name === 'restart').map(event => event.value),
}))
calls.push({ started, elapsed: Date.now() - started, ...state })
return state.events
}).toEqual(['/test/pattern.mp4?quality=A'])
}
finally {
await testInfo.attach('chapter-restart-observations', { contentType: 'application/json', body: JSON.stringify(calls) })
}
}
test.afterEach(async ({ page }, testInfo) => {
if (process.env.ARTPLAYER_CHAPTER_TIMING_DIAGNOSTICS === '1')
await readChapterTiming(page, testInfo)
const state = await page.evaluate(() => ({
at: performance.now(),
timeOrigin: performance.timeOrigin,
qualitySelectedAt: window.chapterQualitySelectedAt,
events: window.chapterEvents,
nativeEvents: window.chapterMediaEvents,
time: window.art?.currentTime,
paused: window.art?.video.paused,
mediaError: window.art?.video.error?.code || null,
source: window.art?.video.currentSrc,
readyState: window.art?.video.readyState,
mobileBranch: window.Artplayer?.utils.isMobile,
maxTouchPoints: navigator.maxTouchPoints,
chapters: [...document.querySelectorAll('.art-chapter')].map(node => ({ ...node.dataset })),
})).catch(error => ({ error: error.message }))
await testInfo.attach('chapter-combination-state', { contentType: 'application/json', body: JSON.stringify(state) })
if (!page.isClosed())
await page.evaluate(() => window.art?.destroy())
})
for (const core of ['published', 'candidate']) {
for (const chapter of ['published', 'candidate']) {
test(`${core} core + ${chapter} chapter: long titles retain text within a narrow player`, async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 })
await openChapter(page, core, chapter)
const fullTitle = 'A long chapter title with complete metadata retained for consumers. '.repeat(4)
await page.evaluate(fullTitle => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: 0, end: Infinity, title: fullTitle }] }), fullTitle)
const box = await page.locator('.art-control-progress-inner').boundingBox()
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
await expect(page.locator('.art-chapter-title')).toHaveText(fullTitle.trim())
await expect(page.locator('.art-chapter-title')).toHaveCSS('opacity', '1')
const layout = await page.locator('.art-chapter-title').evaluate((element) => {
const title = element.getBoundingClientRect()
const progress = element.parentElement.getBoundingClientRect()
return { left: title.left, right: title.right, width: title.width, progressLeft: progress.left, progressRight: progress.right, text: element.textContent, dataset: element.parentElement.querySelector('.art-chapter').dataset.title }
})
await testInfo.attach('long-chapter-layout', { contentType: 'application/json', body: JSON.stringify(layout) })
await testInfo.attach('long-chapter-title', { contentType: 'image/png', body: await page.screenshot() })
expect(layout.text).toBe(fullTitle.trim())
expect(layout.dataset).toBe(fullTitle.trim())
expect(layout.left).toBeGreaterThanOrEqual(layout.progressLeft - 1)
if (chapter === 'published')
expect(layout.right).toBeGreaterThan(layout.progressRight)
else
expect(layout.right).toBeLessThanOrEqual(layout.progressRight + 1)
})
test(`${core} core + ${chapter} chapter: quality switch preserves chapter/thumbnail interactions`, async ({ page, browserName }, testInfo) => {
await openChapter(page, core, chapter)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.2)
await page.locator('#pause').click()
const { box, x } = await hoverChapter(page, 0.375, 'Middle')
await page.mouse.click(x, box.y + box.height / 2)
await expect.poll(() => page.evaluate(() => !window.art.video.seeking)).toBe(true)
const time = await page.evaluate(() => window.art.currentTime)
expect(time).toBeCloseTo(3, 1)
await expect.poll(() => page.locator('.art-chapter .art-progress-played').nth(1).evaluate(node => Number.parseFloat(node.style.width))).toBeCloseTo(25, 0)
expect(await page.locator('.art-chapter .art-progress-played').evaluateAll(nodes => [nodes[0].style.width, nodes[2].style.width])).toEqual(['100%', '0px'])
await page.locator('.art-control-quality').hover()
await expect(page.locator('.art-control-quality .art-selector-list')).toHaveCSS('opacity', '1')
await page.evaluate(() => window.chapterQualitySelectedAt = performance.now())
await page.locator('.art-control-quality .art-selector-item').filter({ hasText: 'Quality A' }).click()
await expect.poll(() => page.evaluate(() => window.art.url.includes('quality=A'))).toBe(true)
await waitForChapterRestart(page, testInfo)
await expect.poll(() => page.evaluate(() => !window.art.video.seeking && window.art.video.readyState >= 2)).toBe(true)
const restoredTime = await page.evaluate(() => window.art.currentTime)
const historicalReset = core === 'published' && browserName === 'webkit'
if (historicalReset)
expect(restoredTime).toBeLessThan(0.05)
else
expect(restoredTime).toBeCloseTo(time, 1)
await testInfo.attach('quality-position', { contentType: 'application/json', body: JSON.stringify({ before: time, restoredTime, historicalReset }) })
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
await expect(page.locator('.art-control-quality .art-selector-value')).toHaveText('Quality A')
await page.locator('#pause').click()
await expect(page.locator('.art-control-quality .art-selector-list')).toHaveCSS('opacity', '0')
await hoverChapter(page, 0.85, 'Ending')
await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: 0, end: Infinity, title: 'Updated after quality' }] }))
await hoverChapter(page, 0.5, 'Updated after quality')
await testInfo.attach('chapter-quality-thumbnail', { contentType: 'image/png', body: await page.locator('.art-video-player').screenshot() })
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(restoredTime + 0.2)
expect(await page.evaluate(() => window.art.video.error)).toBeNull()
})
for (const mode of ['fullscreenWeb', 'fullscreen']) {
test(`${core} core + ${chapter} chapter: ${mode} keeps hover geometry and exits cleanly`, async ({ page }, testInfo) => {
await openChapter(page, core, chapter)
if (mode === 'fullscreenWeb')
await page.evaluate(() => { window.Artplayer.FULLSCREEN_WEB_IN_BODY = true })
const control = page.locator(`.art-control-${mode}`)
const before = await page.locator('.art-control-progress-inner').boundingBox()
await control.click()
await expect.poll(() => page.evaluate(mode => window.art[mode], mode)).toBe(true)
const { box, layout } = await hoverChapter(page, 0.5, 'Middle')
expect(box.width).toBeGreaterThanOrEqual(before.width)
await testInfo.attach(`chapter-${mode}`, { contentType: 'image/png', body: await page.screenshot() })
await testInfo.attach('chapter-fullscreen-layout', { contentType: 'application/json', body: JSON.stringify(layout) })
await control.click()
await expect.poll(() => page.evaluate(mode => window.art[mode], mode)).toBe(false)
expect(await page.evaluate(() => window.art.template.$container.contains(window.art.template.$player))).toBe(true)
await hoverChapter(page, 0.99, 'Ending')
expect(await page.evaluate(mode => window.chapterEvents.filter(event => event.name === mode).map(event => event.value), mode)).toEqual([true, false])
})
}
test.describe(`${core} core + ${chapter} chapter touch emulation`, () => {
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, userAgent: devices['iPhone 13'].userAgent })
test('trusted taps seek through chapters before and after web fullscreen', async ({ page }, testInfo) => {
await openChapter(page, core, chapter)
expect(await page.evaluate(() => window.Artplayer.utils.isMobile)).toBe(true)
await page.evaluate(() => {
window.chapterTouches = []
window.art.template.$progress.addEventListener('touchstart', event => window.chapterTouches.push({ trusted: event.isTrusted, touches: event.touches.length }))
})
for (const fullscreen of [false, true, false]) {
if (await page.evaluate(() => window.art.fullscreenWeb) !== fullscreen)
await page.locator('.art-control-fullscreenWeb').tap()
await expect.poll(() => page.evaluate(() => window.art.fullscreenWeb)).toBe(fullscreen)
const progress = await page.locator('.art-control-progress-inner').boundingBox()
await page.touchscreen.tap(progress.x + progress.width * 0.375, progress.y + progress.height / 2)
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeCloseTo(3, 1)
await expect.poll(() => page.evaluate(() => !window.art.video.seeking)).toBe(true)
await expect(page.locator('.art-chapter')).toHaveCount(3)
}
expect(await page.evaluate(() => window.chapterTouches)).toEqual(Array.from({ length: 3 }, () => ({ trusted: true, touches: 1 })))
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await testInfo.attach('chapter-touch-emulation', { contentType: 'image/png', body: await page.screenshot() })
})
})
}
}