mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 22:01:20 -08:00
refactor(core): CORE-19 type capture and preserve progress lifetimes
This commit is contained in:
1 parent
98a1800032
commit
ccf77c4ed2
49 files changed
+31570
-681
No files matched your search
@@ -0,0 +1,165 @@
|
||||
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]])
|
||||
})
|
||||
Reference in new issue
Block a user