Files
ArtPlayer/test/screenshot.test.js
T

166 lines
6.4 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const { screenshotMix, beginLifecycle, getScope, beginSource } = await loadModules({
screenshotMix: 'packages/artplayer/src/player/screenshotMix',
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
beginSource: { file: 'packages/artplayer/src/source/operation', name: 'beginSource' },
})
function fixture(t) {
const calls = []
const callbacks = []
const context = { drawImage: (...args) => calls.push(['draw', ...args]) }
const canvas = {
width: 0,
height: 0,
getContext: () => context,
toDataURL: type => `data:${type};base64,frame`,
toBlob: callback => callbacks.push(callback),
}
const anchor = { style: {}, click: () => calls.push(['download', anchor.download]), remove: () => calls.push(['remove']) }
const previous = Object.getOwnPropertyDescriptor(globalThis, 'document')
Object.defineProperty(globalThis, 'document', { configurable: true, value: {
createElement: tag => tag === 'canvas' ? canvas : anchor,
body: { appendChild: () => {} },
} })
t.after(() => previous ? Object.defineProperty(globalThis, 'document', previous) : delete globalThis.document)
const video = { videoWidth: 320, videoHeight: 180, currentTime: 62 }
const art = { template: { $video: video }, notice: { show: '' }, emit: (...args) => calls.push(args) }
beginLifecycle(art)
screenshotMix(art)
return { art, canvas, video, context, calls, callbacks, scope: getScope(art) }
}
test('screenshot methods retain immutable bound descriptors, PNG dimensions and filename', async (t) => {
const f = fixture(t)
for (const name of ['getDataURL', 'getBlobUrl', 'screenshot']) {
const descriptor = Object.getOwnPropertyDescriptor(f.art, name)
assert.equal(descriptor.enumerable, false)
assert.equal(descriptor.configurable, false)
assert.equal(descriptor.writable, false)
}
const screenshot = f.art.screenshot
assert.equal(await screenshot(), 'data:image/png;base64,frame')
assert.equal(f.canvas.width, 320)
assert.equal(f.canvas.height, 180)
assert.deepEqual(f.calls, [
['draw', f.video, 0, 0],
['download', 'artplayer_01:02.png'],
['remove'],
['screenshot', 'data:image/png;base64,frame'],
])
await screenshot('custom.png')
assert.deepEqual(f.calls.at(-3), ['download', 'custom.png.png'])
})
test('capture rejects the original drawing and security errors through both methods', async (t) => {
const f = fixture(t)
const failure = new Error('tainted canvas')
f.context.drawImage = () => {
throw failure
}
await assert.rejects(f.art.getDataURL(), error => error === failure)
await assert.rejects(f.art.getBlobUrl(), error => error === failure)
assert.equal(f.art.notice.show, failure)
assert.equal(f.callbacks.length, 0)
})
test('synchronous toBlob failure updates notice before returning its rejected Promise', async (t) => {
const f = fixture(t)
const failure = new Error('encoding failed synchronously')
f.canvas.toBlob = () => {
throw failure
}
const promise = f.art.getBlobUrl()
assert.equal(f.art.notice.show, failure)
await assert.rejects(promise, error => error === failure)
})
test('missing canvas context rejects both public captures', async (t) => {
const f = fixture(t)
f.canvas.getContext = () => null
await assert.rejects(f.art.getDataURL(), { name: 'TypeError', message: /context/ })
await assert.rejects(f.art.getBlobUrl(), { name: 'TypeError', message: /context/ })
})
test('throwing notice setter becomes a rejection even in an asynchronous callback', async (t) => {
const f = fixture(t)
const failure = new Error('consumer notice setter failed')
Object.defineProperty(f.art.notice, 'show', {
get: () => '',
set() {
throw failure
},
})
const promise = f.art.getBlobUrl()
const rejected = assert.rejects(promise, error => error === failure)
assert.doesNotThrow(() => f.callbacks[0](null))
await rejected
})
test('late capture rejection after destruction does not update notice', async (t) => {
const f = fixture(t)
const promise = f.art.getBlobUrl()
const rejected = assert.rejects(promise, /encode/)
f.scope.dispose()
f.callbacks[0](null)
await rejected
assert.equal(f.art.notice.show, '')
})
test('asynchronous Blob URL failures reject without escaping the canvas callback', async (t) => {
const f = fixture(t)
const failure = new Error('object URL allocation failed')
t.mock.method(URL, 'createObjectURL', () => {
throw failure
})
const promise = f.art.getBlobUrl()
const rejected = assert.rejects(promise, error => error === failure)
assert.doesNotThrow(() => f.callbacks[0](new Blob(['frame'])))
await rejected
assert.equal(f.art.notice.show, failure)
})
test('null Blob rejects without allocating a URL or leaving the promise pending', async (t) => {
const f = fixture(t)
const allocate = t.mock.method(URL, 'createObjectURL')
const promise = f.art.getBlobUrl()
const rejected = assert.rejects(promise, /encode|blob/i)
assert.doesNotThrow(() => f.callbacks[0](null))
await rejected
assert.equal(allocate.mock.callCount(), 0)
})
test('concurrent Blob captures settle independently and transfer URL ownership to consumers', async (t) => {
const f = fixture(t)
const revoke = t.mock.method(URL, 'revokeObjectURL', () => {})
t.mock.method(URL, 'createObjectURL', blob => `blob:${blob.size}`)
const first = f.art.getBlobUrl()
const second = f.art.getBlobUrl()
f.callbacks[1](new Blob(['bb']))
f.scope.dispose()
f.callbacks[0](new Blob(['a']))
assert.deepEqual(await Promise.all([first, second]), ['blob:1', 'blob:2'])
assert.equal(revoke.mock.callCount(), 0)
})
test('destruction during screenshot await preserves its result without download or event', async (t) => {
const f = fixture(t)
const promise = f.art.screenshot()
f.scope.dispose()
assert.equal(await promise, 'data:image/png;base64,frame')
assert.deepEqual(f.calls, [['draw', f.video, 0, 0]])
})
test('source replacement during screenshot await suppresses stale download and event', async (t) => {
const f = fixture(t)
const promise = f.art.screenshot()
beginSource(f.art)
assert.equal(await promise, 'data:image/png;base64,frame')
assert.deepEqual(f.calls, [['draw', f.video, 0, 0]])
})