mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(thumbnail): [PKG-TOOL-THUMB-05] verify generated sheets across core versions
This commit is contained in:
1 parent
fb78328c43
commit
8aa99ef03f
13 files changed
+15502
-9
No files matched your search
@@ -535,6 +535,18 @@ The first immediate-pixel WebKit failure remains in the refactor evidence.
|
||||
This is native desktop media playback, not physical audibility or full devices.
|
||||
`document-site.spec.js` also checks generated guide links and Run Code parameters.
|
||||
|
||||
## Standalone thumbnail sheets
|
||||
|
||||
The standalone tool's `thumbnail-core.spec.js` generates actual PNG sheets from
|
||||
a selected local MP4 and displays them in core 3.5.31, 5.4.0 and the candidate.
|
||||
It checks two generations, real hover, unobscured screenshot pixels and exact
|
||||
Blob URL ownership. Historical grid defects are asserted separately from the
|
||||
correct candidate layout. Tool main can be selected with
|
||||
`ARTPLAYER_THUMBNAIL_ARTIFACT`. Windows WebKit's HTTP-success/Blob-error-4 branch
|
||||
is explicitly a capability control, not successful extraction. Full evidence:
|
||||
`refactor/baselines/thumbnail-core-validation.json`; run
|
||||
`yarn test:browser:source thumbnail-core.spec.js --workers=1`.
|
||||
|
||||
## Auto-thumbnail delayed registration
|
||||
|
||||
Auto-thumbnail's `auto-thumbnail-registration.spec.js` separately checks direct
|
||||
|
||||
@@ -95,6 +95,11 @@ 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 })
|
||||
const thumbnailCore = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/thumbnail-core.json'), 'utf8')).release
|
||||
const thumbnailCoreMember = `package/${thumbnailCore.manifest.main}`
|
||||
const thumbnailCoreBytes = readMember(await ensureArchive(thumbnailCore), thumbnailCoreMember)
|
||||
assert.equal(hash(thumbnailCoreBytes), thumbnailCore.files[thumbnailCoreMember], 'Historical Thumbnail core differs from frozen archive')
|
||||
add('/published-3.5.31/artplayer.js', thumbnailCoreBytes, { kind: 'npm-release', version: thumbnailCore.version, integrity: thumbnailCore.integrity, member: thumbnailCoreMember })
|
||||
for (const release of JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/multiple-subtitles-cores.json'), 'utf8')).releases) {
|
||||
const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
|
||||
const bytes = readMember(await ensureArchive(release), member)
|
||||
@@ -199,7 +204,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.2', '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')
|
||||
assert(['candidate', 'published', 'published-3.5.31', 'published-4.5.5', 'published-4.5.9', 'published-5.1.2', '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,172 @@
|
||||
import { Buffer } from 'node:buffer'
|
||||
import fs from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { PNG } from 'pngjs'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { thumbnailCandidate, thumbnailHistorical } from '../helpers/thumbnail.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const sample = fileURLToPath(new URL('./media/thumbnail-pattern.mp4', import.meta.url))
|
||||
const candidate = await thumbnailCandidate()
|
||||
const implementations = [
|
||||
...thumbnailHistorical().filter(item => item.name !== 'workspace.legacy.js'),
|
||||
{ ...candidate, lifecycle: true },
|
||||
{ ...candidate, name: `${candidate.name}-workspace-policy`, compatibility: 'workspace-4.4', lifecycle: true },
|
||||
]
|
||||
|
||||
for (const core of ['published-3.5.31', 'published', 'candidate']) {
|
||||
for (const implementation of implementations) {
|
||||
test(`Thumbnail ${implementation.name} sheets display and refresh in ${core} core`, async ({ page, browserName }, testInfo) => {
|
||||
test.setTimeout(45000)
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: implementation.code })
|
||||
const native = await page.evaluate(async () => {
|
||||
const bytes = await (await fetch('/test/thumbnail-pattern.mp4')).blob()
|
||||
const source = URL.createObjectURL(bytes)
|
||||
const result = {}
|
||||
for (const [transport, url] of [['http', '/test/thumbnail-pattern.mp4'], ['blob', source]]) {
|
||||
const video = document.createElement('video')
|
||||
video.muted = true
|
||||
document.body.append(video)
|
||||
result[transport] = await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('Native media control did not settle')), 5000)
|
||||
video.onloadeddata = () => {
|
||||
clearTimeout(timer)
|
||||
resolve({ loaded: true, width: video.videoWidth })
|
||||
}
|
||||
video.onerror = () => {
|
||||
clearTimeout(timer)
|
||||
resolve({ loaded: false, code: video.error.code })
|
||||
}
|
||||
video.src = url
|
||||
})
|
||||
video.removeAttribute('src')
|
||||
video.load()
|
||||
video.remove()
|
||||
}
|
||||
URL.revokeObjectURL(source)
|
||||
return result
|
||||
})
|
||||
const delay = implementation.legacy ? 300 : 20
|
||||
await page.evaluate(({ compatibility, delay }) => {
|
||||
const input = document.createElement('input')
|
||||
input.id = 'thumbnail-file'
|
||||
input.type = 'file'
|
||||
document.body.append(input)
|
||||
window.createdThumbnailUrls = []
|
||||
window.revokedThumbnailUrls = []
|
||||
const create = URL.createObjectURL.bind(URL)
|
||||
const revoke = URL.revokeObjectURL.bind(URL)
|
||||
URL.createObjectURL = (blob) => {
|
||||
const url = create(blob)
|
||||
window.createdThumbnailUrls.push(url)
|
||||
return url
|
||||
}
|
||||
URL.revokeObjectURL = (url) => {
|
||||
window.revokedThumbnailUrls.push(url)
|
||||
revoke(url)
|
||||
}
|
||||
window.tool = new window.ArtplayerToolThumbnail({ fileInput: input, number: 12, column: 4, width: 160, height: 90, delay, compatibility })
|
||||
window.toolErrors = []
|
||||
window.tool.on('error', error => window.toolErrors.push(String(error)))
|
||||
}, { compatibility: implementation.compatibility, delay })
|
||||
await page.locator('#thumbnail-file').setInputFiles(sample)
|
||||
await testInfo.attach('thumbnail-core-inputs', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, delay, toolSha256: hash(implementation.code), mediaSha256: hash(fs.readFileSync(sample)), native, originalToolArchiveAvailable: false }) })
|
||||
if (!native.blob.loaded) {
|
||||
expect(browserName).toBe('webkit')
|
||||
expect(native).toEqual({ http: { loaded: true, width: 320 }, blob: { loaded: false, code: 4 } })
|
||||
await expect.poll(() => page.evaluate(() => window.tool.video.error?.code)).toBe(4)
|
||||
// Native error state can be set before the queued error event is dispatched.
|
||||
await expect.poll(() => page.evaluate(() => window.toolErrors.length)).toBe(implementation.lifecycle ? 1 : 0)
|
||||
await page.evaluate(() => window.tool.destroy())
|
||||
testInfo.annotations.push({ type: 'capability-control', description: 'Native HTTP works; native Blob and tool Blob fail with media error 4. This case does not validate extraction or core thumbnail display.' })
|
||||
return
|
||||
}
|
||||
await expect.poll(() => page.evaluate(() => window.tool.video.readyState)).toBeGreaterThanOrEqual(2)
|
||||
const generations = []
|
||||
for (let generation = 0; generation < 2; generation++) {
|
||||
const sheet = await page.evaluate(async () => {
|
||||
await window.tool.start()
|
||||
const image = new Image()
|
||||
image.src = window.tool.thumbnailUrl
|
||||
await image.decode()
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
const context = canvas.getContext('2d')
|
||||
context.drawImage(image, 0, 0)
|
||||
const cells = Array.from({ length: 12 }, (_, index) => [...context.getImageData(index % 4 * 160 + 80, Math.floor(index / 4) * 90 + 45, 1, 1).data])
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: window.tool.videoUrl,
|
||||
muted: true,
|
||||
thumbnails: { url: window.tool.thumbnailUrl, number: 12, column: 4, width: 160, height: 90, scale: 1 },
|
||||
})
|
||||
return { url: window.tool.thumbnailUrl, width: image.width, height: image.height, cells, png: canvas.toDataURL('image/png').split(',')[1] }
|
||||
})
|
||||
const sheetPng = PNG.sync.read(Buffer.from(sheet.png, 'base64'))
|
||||
await testInfo.attach(`sheet-${generation}`, { contentType: 'image/png', body: Buffer.from(sheet.png, 'base64') })
|
||||
delete sheet.png
|
||||
expect([sheet.width, sheet.height]).toEqual([640, 300])
|
||||
// Independent FFmpeg decoding confirms all twelve sample centers are blue.
|
||||
for (const pixel of sheet.cells)
|
||||
expect(pixel[2] - Math.max(pixel[0], pixel[1])).toBeGreaterThan(80)
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.readyState)).toBeGreaterThanOrEqual(2)
|
||||
const progress = page.locator('.art-control-progress')
|
||||
const preview = page.locator('.art-control-thumbnails')
|
||||
const bounds = await progress.boundingBox()
|
||||
const observed = []
|
||||
// The old core's first-cell/row-boundary defect is independently characterized.
|
||||
for (const [index, oldIndex] of [[1, 1], [4, 3], [5, 5], [0, 5], [11, 11]]) {
|
||||
const x = bounds.x + bounds.width * (index + 0.5) / 12
|
||||
const y = bounds.y + bounds.height / 2
|
||||
await page.mouse.move(x, y)
|
||||
await expect.poll(async () => {
|
||||
await page.mouse.move(x, y)
|
||||
return preview.evaluate(element => element.style.backgroundImage)
|
||||
}).toContain('blob:')
|
||||
const cell = core === 'candidate' ? index : oldIndex
|
||||
await expect(preview).toHaveCSS('background-position', `${-(cell % 4) * 160}px ${-Math.floor(cell / 4) * 90}px`)
|
||||
await expect(preview).toBeVisible()
|
||||
let screenshot
|
||||
await expect.poll(async () => {
|
||||
screenshot = await preview.screenshot()
|
||||
const png = PNG.sync.read(screenshot)
|
||||
expect([png.width, png.height]).toEqual([160, 90])
|
||||
let difference = 0
|
||||
let channels = 0
|
||||
// The progress time tooltip covers the lower part of the preview.
|
||||
// Compare the unobscured interior, excluding rounded border pixels.
|
||||
for (let py = 8; py < 60; py++) {
|
||||
for (let px = 8; px < 152; px++) {
|
||||
const actual = (py * png.width + px) * 4
|
||||
const expected = ((Math.floor(cell / 4) * 90 + py) * sheetPng.width + cell % 4 * 160 + px) * 4
|
||||
for (let channel = 0; channel < 3; channel++) {
|
||||
difference += Math.abs(png.data[actual + channel] - sheetPng.data[expected + channel])
|
||||
channels++
|
||||
}
|
||||
}
|
||||
}
|
||||
return difference / channels
|
||||
}).toBeLessThanOrEqual(3)
|
||||
observed.push({ requestedIndex: index, displayedIndex: cell })
|
||||
await testInfo.attach(`sheet-${generation}-cell-${index}`, { contentType: 'image/png', body: screenshot })
|
||||
}
|
||||
const beforeDestroy = await page.evaluate(() => window.revokedThumbnailUrls.slice())
|
||||
await page.evaluate(() => window.art.destroy(true))
|
||||
expect(await page.evaluate(() => window.revokedThumbnailUrls)).toEqual(beforeDestroy)
|
||||
expect(await page.evaluate(() => window.tool.video.isConnected)).toBe(true)
|
||||
generations.push({ ...sheet, observed })
|
||||
}
|
||||
expect(generations[0].url).not.toBe(generations[1].url)
|
||||
const cleanup = await page.evaluate(() => {
|
||||
window.tool.destroy()
|
||||
return { created: window.createdThumbnailUrls, revoked: window.revokedThumbnailUrls, videoConnected: window.tool.video.isConnected, players: window.Artplayer.instances.length, errors: window.toolErrors }
|
||||
})
|
||||
expect(cleanup.revoked.filter(url => url === '')).toHaveLength(core === 'published' ? 2 : 0)
|
||||
expect([...cleanup.created].sort()).toEqual(cleanup.revoked.filter(url => url !== '').sort())
|
||||
expect(cleanup).toMatchObject({ videoConnected: false, players: 0, errors: [] })
|
||||
await testInfo.attach('thumbnail-core-results', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, generations, cleanup, scope: 'Native local file extraction, real hover and screenshot pixels on two generations; historical core defects asserted separately; no physical device claim' }) })
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user