mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 17:46:17 -08:00
test(vtt-thumbnail): [PKG-VTT-THUMB-05] verify historical core and fullscreen combinations
This commit is contained in:
1 parent
4fa5c68be9
commit
07d5bfef2e
14 files changed
+2984
-10
No files matched your search
@@ -245,3 +245,14 @@ when diagnosing a timeout: the historical failure includes a single ~10-second
|
||||
evaluate returning an already-recorded restart, not seven seconds of fast empty
|
||||
reads. These observations do not expand the timeout or waive the open timing and
|
||||
physical-device acceptance in PKG-CHAPTER-05.
|
||||
|
||||
VTT combination tests verify actual preview screenshot pixels, Chapter coexistence,
|
||||
native/web fullscreen, source switching and cleanup across five recorded core
|
||||
inputs. The 5.1.6 server route verifies its frozen npm archive/member fingerprints.
|
||||
Historical 1.0.1 has a distinct valid 5.1.6 pair and explicit later-core name-conflict
|
||||
tests; expected historical failures are not compatibility passes. Later 1.0.x
|
||||
compiled bundles receive their documented compact-arrow fixture, while ordinary
|
||||
syntax failures remain in the historical parser tests. Mobile cases use an Android
|
||||
UA and synthetic DOM touches through the real core handlers, not trusted hardware
|
||||
input. Fullscreen mouse placement first waits for the progress control to be stable;
|
||||
the earlier hidden-preview screenshot timeout remains in the checkpoint evidence.
|
||||
@@ -79,6 +79,11 @@ const asrCoreMember = `package/${asrCore.manifest.main.replace(/^\.\//, '')}`
|
||||
const asrCoreBytes = readMember(await ensureArchive(asrCore), asrCoreMember)
|
||||
assert.equal(hash(asrCoreBytes), asrCore.files[asrCoreMember], 'Adjacent stable ASR core differs from frozen release')
|
||||
add('/published-5.3.0/artplayer.js', asrCoreBytes, { kind: 'npm-release', version: asrCore.version, integrity: asrCore.integrity, member: asrCoreMember })
|
||||
const vttCore = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/vtt-thumbnail-core.json'), 'utf8')).release
|
||||
const vttCoreMember = `package/${vttCore.manifest.main.replace(/^\.\//, '')}`
|
||||
const vttCoreBytes = readMember(await ensureArchive(vttCore), vttCoreMember)
|
||||
assert.equal(hash(vttCoreBytes), vttCore.files[vttCoreMember], 'Historical VTT core differs from frozen archive')
|
||||
add('/published-5.1.6/artplayer.js', vttCoreBytes, { kind: 'npm-release', version: vttCore.version, integrity: vttCore.integrity, member: vttCoreMember })
|
||||
add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/media/audio-tone.m4a')), { kind: 'generated-media', file: 'test/browser/media/audio-tone.m4a' })
|
||||
add('/test/thumbnail-pattern.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-pattern.mp4')), { kind: 'generated-media', file: 'test/browser/media/thumbnail-pattern.mp4' })
|
||||
add('/test/auto-thumbnail-timeline.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/auto-thumbnail-timeline.mp4')), { kind: 'generated-media', file: 'test/browser/media/auto-thumbnail-timeline.mp4' })
|
||||
@@ -165,7 +170,7 @@ const server = http.createServer((req, res) => {
|
||||
if (url.pathname === '/test/player.html') {
|
||||
const core = url.searchParams.get('core') || 'candidate'
|
||||
const chapter = url.searchParams.get('chapter') || 'candidate'
|
||||
assert(['candidate', 'published', 'published-4.5.5', 'published-4.5.9', 'published-5.1.7', 'published-5.3.0', 'published-5.3.1-beta.1'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
|
||||
assert(['candidate', 'published', 'published-4.5.5', 'published-4.5.9', 'published-5.1.6', 'published-5.1.7', 'published-5.3.0', 'published-5.3.1-beta.1'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
|
||||
const html = fs.readFileSync(path.join(workspace, 'test/browser/player.html'), 'utf8')
|
||||
.replace('__CORE__', core)
|
||||
.replace('__CHAPTER__', chapter)
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { PNG } from 'pngjs'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { acceptedVttText, vttThumbnailCandidate, vttThumbnailHistorical } from '../helpers/vtt-thumbnail.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const candidate = await vttThumbnailCandidate()
|
||||
const historical = (await vttThumbnailHistorical()).filter(item => /^published-.*-main$/.test(item.name))
|
||||
const cores = ['published-5.1.6', 'published-5.1.7', 'published-5.3.0', 'published', 'candidate']
|
||||
const cues = 'WEBVTT\n\n00:00.000 --> 00:04.000\nsprite.svg#xywh=0,0,80,45\n\n00:04.000 --> 00:08.000\nsprite.svg#xywh=80,0,80,45\n'
|
||||
const sprite = '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="45"><path fill="red" d="M0 0h80v45H0z"/><path fill="blue" d="M80 0h80v45H80z"/></svg>'
|
||||
|
||||
function centerPixel(bytes) {
|
||||
const { width, height, data } = PNG.sync.read(bytes)
|
||||
const index = (Math.floor(height / 2) * width + Math.floor(width / 2)) * 4
|
||||
return [...data.subarray(index, index + 4)]
|
||||
}
|
||||
|
||||
async function install(page, core, implementation, testInfo) {
|
||||
let requests = 0
|
||||
await page.route('**/test/vtt-combination/cues.vtt', (route) => {
|
||||
requests++
|
||||
return route.fulfill({ contentType: 'text/vtt', body: acceptedVttText(implementation, cues) })
|
||||
})
|
||||
await page.route('**/test/vtt-combination/sprite.svg', route => route.fulfill({ contentType: 'image/svg+xml', body: sprite }))
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.vttFactory = module.exports.default || module.exports; })();` })
|
||||
await page.evaluate(async () => {
|
||||
window.createPlayer('/test/pattern.mp4', true)
|
||||
await window.art.plugins.add(window.vttFactory({ vtt: '/test/vtt-combination/cues.vtt' }))
|
||||
window.vttControl = window.art.controls['vtt-thumbnail'] || window.art.controls.thumbnails
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await testInfo.attach('vtt-combination-inputs', {
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ core, implementation: implementation.name, sha256: hash(implementation.code), cueFormat: /^published-1\.0\./.test(implementation.name) ? 'historical compact arrow; ordinary-space failures retained separately' : 'standard spaced arrow', mobile: 'desktop unless test declares an emulated user agent; no physical-device inference' }),
|
||||
})
|
||||
return () => requests
|
||||
}
|
||||
|
||||
async function hover(page, percentage) {
|
||||
const progress = page.locator('.art-control-progress')
|
||||
await progress.hover()
|
||||
const bounds = await progress.boundingBox()
|
||||
await page.mouse.move(bounds.x + bounds.width * percentage, bounds.y + bounds.height / 2)
|
||||
}
|
||||
|
||||
for (const core of cores) {
|
||||
test(`VTT candidate / ${core}: actual sprite pixels, chapter, native/web fullscreen and source switch`, async ({ page }, testInfo) => {
|
||||
const requests = await install(page, core, candidate, testInfo)
|
||||
try {
|
||||
const preview = page.locator('.art-control-vtt-thumbnail')
|
||||
const progress = page.locator('.art-control-progress')
|
||||
const bounds = await progress.boundingBox()
|
||||
await page.mouse.move(bounds.x + bounds.width * 0.25, bounds.y + bounds.height / 2)
|
||||
await expect(preview).toHaveCSS('background-position', '0px 0px')
|
||||
await expect(preview).toBeVisible()
|
||||
const red = await preview.screenshot()
|
||||
expect(centerPixel(red)).toEqual([255, 0, 0, 255])
|
||||
await testInfo.attach('vtt-red-cell', { contentType: 'image/png', body: red })
|
||||
await page.mouse.move(bounds.x + bounds.width * 0.75, bounds.y + bounds.height / 2)
|
||||
await expect(preview).toHaveCSS('background-position', '-80px 0px')
|
||||
const blue = await preview.screenshot()
|
||||
expect(centerPixel(blue)).toEqual([0, 0, 255, 255])
|
||||
await testInfo.attach('vtt-blue-cell', { contentType: 'image/png', body: blue })
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.click('#pause')
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreenWeb)).toBe(true)
|
||||
await hover(page, 0.25)
|
||||
await expect(preview).toHaveCSS('background-position', '0px 0px')
|
||||
expect(centerPixel(await preview.screenshot())).toEqual([255, 0, 0, 255])
|
||||
await page.evaluate(async () => {
|
||||
window.art.fullscreenWeb = false
|
||||
await window.art.switchUrl('/test/pattern.mp4?second')
|
||||
})
|
||||
await hover(page, 0.75)
|
||||
await expect(preview).toHaveCSS('background-position', '-80px 0px')
|
||||
expect(await page.evaluate(() => window.vttControl === window.art.controls['vtt-thumbnail'])).toBe(true)
|
||||
expect(requests()).toBe(1)
|
||||
expect(await page.evaluate(() => Boolean(window.art.plugins.artplayerPluginChapter))).toBe(true)
|
||||
await expect(page.locator('.art-chapter')).toHaveCount(1)
|
||||
await page.evaluate(() => {
|
||||
window.vttFullscreen = []
|
||||
window.art.on('fullscreen', value => window.vttFullscreen.push(value))
|
||||
document.querySelector('#play').onclick = () => {
|
||||
window.art.fullscreen = true
|
||||
}
|
||||
})
|
||||
expect(await page.evaluate(() => Boolean(document.fullscreenEnabled || document.webkitFullscreenEnabled))).toBe(true)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => (document.fullscreenElement || document.webkitFullscreenElement) === window.art.template.$player)).toBe(true)
|
||||
await hover(page, 0.75)
|
||||
await expect(preview).toBeVisible()
|
||||
await expect(preview).toHaveCSS('background-position', '-80px 0px')
|
||||
expect(centerPixel(await preview.screenshot())).toEqual([0, 0, 255, 255])
|
||||
expect(await page.evaluate(() => (document.fullscreenElement || document.webkitFullscreenElement).contains(window.vttControl))).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreen = false
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => Boolean(document.fullscreenElement || document.webkitFullscreenElement))).toBe(false)
|
||||
await expect.poll(() => page.evaluate(() => window.vttFullscreen)).toEqual([true, false])
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await expect(preview).toHaveCount(0)
|
||||
}
|
||||
finally {
|
||||
await page.evaluate(() => {
|
||||
if (!window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const implementation of historical) {
|
||||
const core = implementation.profile === 'old-control' ? 'published-5.1.6' : 'candidate'
|
||||
test(`VTT ${implementation.name} / ${core}: legal old calls preserve preview and control identity`, async ({ page }, testInfo) => {
|
||||
const requests = await install(page, core, implementation, testInfo)
|
||||
try {
|
||||
const name = implementation.profile === 'old-control' ? 'thumbnails' : 'vtt-thumbnail'
|
||||
const preview = page.locator(`.art-control-${name}`)
|
||||
const bounds = await page.locator('.art-control-progress').boundingBox()
|
||||
await page.mouse.move(bounds.x + bounds.width * 0.75, bounds.y + bounds.height / 2)
|
||||
await expect(preview).toHaveCSS('background-position', '-80px 0px')
|
||||
expect(centerPixel(await preview.screenshot())).toEqual([0, 0, 255, 255])
|
||||
expect(await page.evaluate(name => window.art.controls[name] === window.vttControl, name)).toBe(true)
|
||||
await page.evaluate(async () => {
|
||||
await window.art.switchUrl('/test/pattern.mp4?old-plugin')
|
||||
})
|
||||
await hover(page, 0.25)
|
||||
await expect(preview).toHaveCSS('background-position', '0px 0px')
|
||||
expect(requests()).toBe(1)
|
||||
}
|
||||
finally { await page.evaluate(() => window.art.destroy()) }
|
||||
})
|
||||
}
|
||||
|
||||
for (const core of ['published-5.1.7', 'published', 'candidate']) {
|
||||
test(`VTT 1.0.1 / ${core}: pre-existing reserved thumbnails name conflict is retained as historical evidence`, async ({ page }, testInfo) => {
|
||||
const implementation = historical.find(item => item.version === '1.0.1')
|
||||
await page.route('**/test/vtt-conflict.vtt', route => route.fulfill({ contentType: 'text/vtt', body: acceptedVttText(implementation, cues) }))
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.vttFactory = module.exports.default || module.exports; })();` })
|
||||
const state = await page.evaluate(async () => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
const original = window.art.controls.thumbnails
|
||||
try {
|
||||
await window.art.plugins.add(window.vttFactory({ vtt: '/test/vtt-conflict.vtt' }))
|
||||
return { error: null }
|
||||
}
|
||||
catch (error) {
|
||||
return { error: error.message, sameReservedControl: original === window.art.controls.thumbnails }
|
||||
}
|
||||
finally { window.art.destroy() }
|
||||
})
|
||||
expect(state.error).toContain('Can\'t add an existing [thumbnails] to the [control]')
|
||||
expect(state.sameReservedControl).toBe(true)
|
||||
await testInfo.attach('vtt-historical-conflict', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, sha256: hash(implementation.code), state, scope: 'Existing incompatibility, not successful old-plugin/new-core acceptance and not a newly introduced refactor regression' }) })
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('VTT mobile event integration', () => {
|
||||
test.use({ userAgent: 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36', hasTouch: true })
|
||||
for (const core of cores) {
|
||||
test(`VTT candidate / ${core}: emulated mobile drag routes through core progress and releases hiding timer`, async ({ page }, testInfo) => {
|
||||
await install(page, core, candidate, testInfo)
|
||||
const sample = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const progress = art.template.$progress
|
||||
const box = progress.getBoundingClientRect()
|
||||
const events = []
|
||||
art.on('setBar', (type, percentage, event) => {
|
||||
if (event)
|
||||
events.push({ type, percentage, nativeType: event.type })
|
||||
})
|
||||
function touch(type, percentage) {
|
||||
const x = box.left + box.width * percentage
|
||||
const y = box.top + box.height / 2
|
||||
const point = { identifier: 1, target: progress, clientX: x, clientY: y, pageX: x + scrollX, pageY: y + scrollY }
|
||||
const event = new Event(type, { bubbles: true, cancelable: true })
|
||||
Object.defineProperties(event, { touches: { value: type === 'touchend' ? [] : [point] }, changedTouches: { value: [point] } })
|
||||
progress.dispatchEvent(event)
|
||||
}
|
||||
touch('touchstart', 0.25)
|
||||
touch('touchmove', 0.75)
|
||||
touch('touchend', 0.75)
|
||||
const control = art.controls['vtt-thumbnail']
|
||||
return { mobile: window.Artplayer.utils.isMobile, events, position: control.style.backgroundPosition, display: control.style.display, time: art.currentTime, duration: art.duration }
|
||||
})
|
||||
expect(sample.mobile).toBe(true)
|
||||
expect(sample.events.some(event => event.nativeType === 'touchmove' && event.type === 'played')).toBe(true)
|
||||
expect(sample.display).toBe('flex')
|
||||
expect(sample.position).toBe('-80px 0px')
|
||||
expect(sample.time / sample.duration).toBeGreaterThan(0.5)
|
||||
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCSS('display', 'none')
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(0)
|
||||
await testInfo.attach('vtt-mobile-event-integration', { contentType: 'application/json', body: JSON.stringify({ core, sample, scope: 'Android user agent and synthetic DOM touch payloads through actual core handlers; native media seek and real hide timer. Not trusted OS touch, real Android, or iOS Safari acceptance.' }) })
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user