Files
ArtPlayer/test/browser/screenshot.spec.js

215 lines
8.3 KiB
JavaScript

import { expect, test } from './fixtures.js'
async function setup(page, core, crossOrigin = false, control = false) {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
await page.evaluate(({ crossOrigin, control }) => {
const url = new URL('/test/pattern.mp4', location.href)
if (crossOrigin)
url.hostname = 'localhost'
window.art = new window.Artplayer({ container: '.player', url: url.href, muted: true, screenshot: control })
}, { crossOrigin, control })
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.evaluate(() => window.art.play())
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.evaluate(() => window.art.pause())
}
for (const core of ['published', 'candidate']) {
test(`${core}: captures decoded PNG pixels and consumer-owned Blob URLs`, async ({ page }) => {
await setup(page, core)
const result = await page.evaluate(async () => {
const art = window.art
const data = await art.getDataURL()
const url = await art.getBlobUrl()
const image = new Image()
image.src = data
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 pixels = context.getImageData(0, 0, image.width, image.height).data
const colors = new Set()
for (let index = 0; index < pixels.length; index += 4)
colors.add(`${pixels[index]},${pixels[index + 1]},${pixels[index + 2]}`)
const expected = [art.video.videoWidth, art.video.videoHeight]
const descriptors = ['getDataURL', 'getBlobUrl', 'screenshot'].map((name) => {
const d = Object.getOwnPropertyDescriptor(art, name)
return [d.writable, d.configurable, d.enumerable]
})
art.destroy()
const blob = await (await fetch(url)).blob()
URL.revokeObjectURL(url)
return { png: data.startsWith('data:image/png;base64,'), dimensions: [image.width, image.height], expected, colors: colors.size, blobType: blob.type, blobSize: blob.size, descriptors }
})
expect(result.png).toBe(true)
expect(result.dimensions).toEqual(result.expected)
expect(result.dimensions[0]).toBeGreaterThan(0)
expect(result.colors).toBeGreaterThan(10)
expect(result.blobType).toBe('image/png')
expect(result.blobSize).toBeGreaterThan(100)
expect(result.descriptors).toEqual(Array.from({ length: 3 }, () => [false, false, false]))
})
test(`${core}: screenshot preserves download filename, event data and Promise result`, async ({ page }) => {
await setup(page, core)
const downloadPromise = page.waitForEvent('download')
const result = await page.evaluate(async () => {
const art = window.art
const events = []
art.on('screenshot', value => events.push(value))
const screenshot = art.screenshot
const value = await screenshot('chosen.png')
art.destroy()
return { count: events.length, same: events[0] === value, png: value.startsWith('data:image/png;base64,') }
})
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('chosen.png.png')
expect(result).toEqual({ count: 1, same: true, png: true })
await download.delete()
})
test(`${core}: actual cross-origin video rejects capture with SecurityError and notice`, async ({ page }) => {
await setup(page, core, true)
const result = await page.evaluate(async () => {
const art = window.art
const errors = []
for (const name of ['getDataURL', 'getBlobUrl', 'screenshot']) {
try {
await art[name]()
errors.push('resolved')
}
catch (error) {
errors.push(error.name)
}
}
const result = { errors, notice: art.notice.show, text: art.template.$noticeInner.textContent.length }
art.destroy()
return result
})
expect(result.errors).toEqual(['SecurityError', 'SecurityError', 'SecurityError'])
expect(result.notice).toBe(true)
expect(result.text).toBeGreaterThan(0)
})
test(`${core}: source replacement does not download a stale captured frame`, async ({ page }) => {
await setup(page, core)
const result = await page.evaluate(async () => {
const art = window.art
let events = 0
let downloads = 0
const original = HTMLAnchorElement.prototype.click
HTMLAnchorElement.prototype.click = () => {
downloads++
}
art.on('screenshot', () => {
events++
})
try {
const promise = art.screenshot()
art.url = '/test/pattern.mp4?replacement=1'
const result = { png: (await promise).startsWith('data:image/png;base64,'), events, downloads }
art.destroy()
return result
}
finally {
HTMLAnchorElement.prototype.click = original
}
})
const oldEffects = core === 'published' ? 1 : 0
expect(result).toEqual({ png: true, events: oldEffects, downloads: oldEffects })
})
test(`${core}: screenshot toolbar rejection has exact historical error accounting`, async ({ page }, testInfo) => {
await setup(page, core, false, true)
await page.evaluate((core) => {
window.captureFailures = []
const failure = new Error('controlled screenshot draw failure')
const original = HTMLCanvasElement.prototype.toDataURL
window.restoreCapture = () => {
HTMLCanvasElement.prototype.toDataURL = original
}
HTMLCanvasElement.prototype.toDataURL = () => {
throw failure
}
if (core === 'published') {
window.addEventListener('unhandledrejection', (event) => {
if (event.reason === failure) {
window.captureFailures.push(event.reason.message)
event.preventDefault()
}
})
}
window.art.notice.show = false
}, core)
await page.locator('.art-control-screenshot').click()
await expect.poll(() => page.evaluate(() => window.art.template.$noticeInner.textContent)).toBe('controlled screenshot draw failure')
if (core === 'published')
await expect.poll(() => page.evaluate(() => window.captureFailures.length)).toBe(1)
const known = await page.evaluate(() => {
window.restoreCapture()
window.art.destroy()
return window.captureFailures
})
expect(known).toEqual(core === 'published' ? ['controlled screenshot draw failure'] : [])
await testInfo.attach('known-screenshot-rejections', { contentType: 'application/json', body: JSON.stringify(known) })
})
}
test('candidate: null toBlob callback rejects and reports without a page error', async ({ page }) => {
await setup(page, 'candidate')
expect(await page.evaluate(async () => {
const prototype = HTMLCanvasElement.prototype
const original = prototype.toBlob
prototype.toBlob = function (callback) {
queueMicrotask(() => callback(null))
}
try {
await window.art.getBlobUrl()
return 'resolved'
}
catch (error) {
return error.message
}
finally {
prototype.toBlob = original
window.art.destroy()
}
})).toBe('Unable to encode screenshot blob')
})
test('candidate: destroy during screenshot await suppresses the queued download and event', async ({ page }) => {
await setup(page, 'candidate')
expect(await page.evaluate(async () => {
const art = window.art
let events = 0
let downloads = 0
const original = HTMLAnchorElement.prototype.click
HTMLAnchorElement.prototype.click = () => {
downloads++
}
art.on('screenshot', () => {
events++
})
try {
const promise = art.screenshot()
art.destroy()
return { png: (await promise).startsWith('data:image/png;base64,'), events, downloads }
}
finally {
HTMLAnchorElement.prototype.click = original
}
})).toEqual({ png: true, events: 0, downloads: 0 })
})
test('candidate: cross-origin screenshot toolbar handles its internal rejected Promise', async ({ page }) => {
await setup(page, 'candidate', true, true)
await page.evaluate(() => {
window.art.notice.show = false
})
await page.locator('.art-control-screenshot').click()
await expect.poll(() => page.evaluate(() => window.art.notice.show)).toBe(true)
await page.evaluate(() => window.art.destroy())
})