mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:39:41 -08:00
222 lines
13 KiB
JavaScript
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() })
|
|
})
|
|
})
|
|
}
|
|
}
|