mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
213 lines
7.6 KiB
JavaScript
213 lines
7.6 KiB
JavaScript
import fs from 'node:fs'
|
|
import { build } from 'esbuild'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
const compiled = await build({
|
|
entryPoints: ['scripts/docs-smoke/runtime.ts'],
|
|
bundle: true,
|
|
write: false,
|
|
format: 'iife',
|
|
globalName: 'DocumentationSmoke',
|
|
target: 'es2020',
|
|
logLevel: 'silent',
|
|
})
|
|
const runtime = compiled.outputFiles[0].text
|
|
const core = '/candidate/artplayer.js'
|
|
const example = code => ({ id: 'fixture.md:1', file: 'fixture.md', line: 1, code })
|
|
const player = 'var art = new Artplayer({ container: \'.artplayer-app\', url: \'/test/pattern.mp4\', muted: true });'
|
|
const documented = JSON.parse(fs.readFileSync('docs/test/examples.json', 'utf8'))
|
|
.examples
|
|
.filter(item => item.file === 'start/option.md')
|
|
.slice(0, 3)
|
|
|
|
function expectDimensions(media, browserName) {
|
|
// DPIP-MEDIA-01: Windows WebKit can report either intrinsic or styled dimensions.
|
|
// Keep exact observed pairs; readiness smoke does not close intrinsic-size compatibility.
|
|
const sizes = browserName === 'webkit' ? [[320, 180], [640, 360]] : [[320, 180]]
|
|
expect(sizes).toContainEqual([media.width, media.height])
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/test/player.html')
|
|
await page.addScriptTag({ content: runtime })
|
|
})
|
|
|
|
test('readiness smoke uses actual core/media and releases each frame without touching a parent player', async ({
|
|
page,
|
|
browserName,
|
|
}, testInfo) => {
|
|
await page.evaluate(() => {
|
|
window.createPlayer('/test/pattern.mp4')
|
|
localStorage.setItem('smoke-existing', 'preserved')
|
|
sessionStorage.setItem('smoke-session', 'preserved')
|
|
})
|
|
for (let index = 0; index < 3; index++) {
|
|
const result = await page.evaluate(
|
|
async ({ item, core }) => {
|
|
return window.DocumentationSmoke.runExample(item, [core])
|
|
},
|
|
{
|
|
item: example(
|
|
`${player}\nlocalStorage.clear(); sessionStorage.clear(); localStorage.setItem('smoke-leak', 'bad');`,
|
|
),
|
|
core,
|
|
},
|
|
)
|
|
expect(result.instances).toBe(1)
|
|
expect(result.ready).toBe(1)
|
|
expectDimensions(result.media[0], browserName)
|
|
expect(result.media[0].duration).toBeGreaterThan(0)
|
|
await testInfo.attach(`readiness-${index}`, { body: JSON.stringify(result), contentType: 'application/json' })
|
|
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
|
|
}
|
|
expect(
|
|
await page.evaluate(() => ({
|
|
players: window.Artplayer.instances.length,
|
|
kept: localStorage.getItem('smoke-existing'),
|
|
session: sessionStorage.getItem('smoke-session'),
|
|
leak: localStorage.getItem('smoke-leak'),
|
|
})),
|
|
).toEqual({ players: 1, kept: 'preserved', session: 'preserved', leak: null })
|
|
await page.evaluate(() => window.art.destroy())
|
|
})
|
|
|
|
test('smoke remains pending beyond 100ms until real media becomes ready', async ({ page }) => {
|
|
let release
|
|
const gate = new Promise((resolve) => {
|
|
release = resolve
|
|
})
|
|
await page.route('**/test/pattern.mp4', async (route) => {
|
|
await gate
|
|
await route.continue()
|
|
})
|
|
try {
|
|
await page.evaluate(
|
|
({ item, core }) => {
|
|
window.smokeState = { done: false }
|
|
window.smokePromise = window.DocumentationSmoke.runExample(item, [core]).then((value) => {
|
|
window.smokeState = { done: true, value }
|
|
})
|
|
window.oldCompletion = new Promise(resolve => setTimeout(resolve, 100))
|
|
},
|
|
{ item: example(player), core },
|
|
)
|
|
await page.evaluate(() => window.oldCompletion)
|
|
expect(await page.evaluate(() => window.smokeState.done)).toBe(false)
|
|
release()
|
|
await page.evaluate(() => window.smokePromise)
|
|
expect(await page.evaluate(() => window.smokeState.value.ready)).toBe(1)
|
|
}
|
|
finally {
|
|
release()
|
|
}
|
|
})
|
|
|
|
test('generated real documentation cases run with controlled native media', async ({ page, browserName }) => {
|
|
await page.route('**/assets/sample/video.mp4', route =>
|
|
route.fulfill({ contentType: 'video/mp4', body: fs.readFileSync('test/browser/media/pattern.mp4') }))
|
|
for (const item of documented) {
|
|
const result = await page.evaluate(({ item, core }) => window.DocumentationSmoke.runExample(item, [core]), {
|
|
item,
|
|
core,
|
|
})
|
|
expect(result.ready).toBe(1)
|
|
expectDimensions(result.media[0], browserName)
|
|
}
|
|
})
|
|
|
|
test('parallel calls are rejected while a controlled case is still active', async ({ page }) => {
|
|
const state = await page.evaluate(
|
|
async ({ item, core }) => {
|
|
const first = window.DocumentationSmoke.runExample(item, [core])
|
|
let rejected
|
|
try {
|
|
await window.DocumentationSmoke.runExample(item, [core])
|
|
}
|
|
catch (error) {
|
|
rejected = String(error)
|
|
}
|
|
await first
|
|
return rejected
|
|
},
|
|
{ item: example(player), core },
|
|
)
|
|
expect(state).toContain('must run sequentially')
|
|
})
|
|
|
|
for (const [name, code, message] of [
|
|
['synchronous error', `${player}\nthrow new Error('sync fixture');`, 'sync fixture'],
|
|
['returned rejection', `${player}\nPromise.reject(new Error('returned fixture'));`, 'returned fixture'],
|
|
['unhandled rejection', `${player}\nPromise.reject(new Error('unhandled fixture')); void 0;`, 'unhandled fixture'],
|
|
[
|
|
'readiness callback error',
|
|
`${player}\nart.on('ready', () => { throw new Error('ready fixture'); });`,
|
|
'ready fixture',
|
|
],
|
|
['cleanup error', `${player}\nart.on('destroy', () => { throw new Error('destroy fixture'); });`, 'destroy fixture'],
|
|
]) {
|
|
test(`smoke reports ${name} and removes its frame`, async ({ page }) => {
|
|
const result = await page.evaluate(
|
|
async ({ item, core }) => {
|
|
try {
|
|
await window.DocumentationSmoke.runExample(item, [core])
|
|
return 'unexpected success'
|
|
}
|
|
catch (error) {
|
|
return String(error)
|
|
}
|
|
},
|
|
{ item: example(code), core },
|
|
)
|
|
expect(result).toContain(message)
|
|
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
|
|
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
|
|
})
|
|
}
|
|
|
|
test('missing script and never-ready examples fail instead of becoming green after a delay', async ({ page }) => {
|
|
await page.route('**/missing-smoke.js', route => route.fulfill({ status: 404, body: 'missing fixture' }))
|
|
const missing = await page.evaluate(async (item) => {
|
|
try {
|
|
await window.DocumentationSmoke.runExample(item, ['/missing-smoke.js'])
|
|
return 'unexpected success'
|
|
}
|
|
catch (error) {
|
|
return String(error)
|
|
}
|
|
}, example('void 0;'))
|
|
expect(missing).toContain('Failed to load')
|
|
const timeout = await page.evaluate(
|
|
async ({ item, core }) => {
|
|
try {
|
|
await window.DocumentationSmoke.runExample(item, [core], 1000)
|
|
return 'unexpected success'
|
|
}
|
|
catch (error) {
|
|
return String(error)
|
|
}
|
|
},
|
|
{ item: example('new Artplayer({ container: \'.artplayer-app\', url: \'\' });'), core },
|
|
)
|
|
expect(timeout).toContain('readiness timeout')
|
|
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
|
|
})
|
|
|
|
test('frame-owned timers and globals do not survive into the next example', async ({ page }) => {
|
|
await page.evaluate(
|
|
async ({ first, second, core }) => {
|
|
await window.DocumentationSmoke.runExample(first, [core])
|
|
await window.DocumentationSmoke.runExample(second, [core])
|
|
},
|
|
{
|
|
first: example(
|
|
`${player}\nwindow.smokeLeak = 1; setTimeout(() => { throw new Error('late frame callback'); }, 700);`,
|
|
),
|
|
second: example(
|
|
`${player}\nif (window.smokeLeak) throw new Error('global leaked');\nnew Promise(resolve => setTimeout(resolve, 900));`,
|
|
),
|
|
core,
|
|
},
|
|
)
|
|
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
|
|
})
|