mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-17 01:56:16 -08:00
refactor(docs): [SITE-SMOKE-01] verify example readiness and cleanup
This commit is contained in:
1 parent
d74ecc43cc
commit
1a5440a67f
25 files changed
+3510
-4770
No files matched your search
@@ -1,5 +1,13 @@
|
||||
# Browser regression entry
|
||||
|
||||
`docs-smoke.spec.js` verifies the documentation runner in three engines with
|
||||
actual core/media, controlled failures and three selected generated snippets.
|
||||
It checks pending readiness beyond the former 100ms timer, returned/unhandled
|
||||
rejections, ready/destroy errors, missing scripts, deadlines, frame/global/timer
|
||||
cleanup and storage restoration. It does not execute all 233 documentation cases
|
||||
or prove remote SDKs/features. DPIP-MEDIA-01's exact WebKit intrinsic/layout size
|
||||
pairs remain explicit observations; this suite does not close that risk.
|
||||
|
||||
`yarn test:browser test/browser/ads.spec.js` tests actual Ads 1.0.6 and candidate code
|
||||
with published 4.5.5/5.4.1 and candidate cores using local image/video media. It covers actual
|
||||
decoded pixels, countdown, skipping, content restoration, 404 and source replacement.
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
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)
|
||||
})
|
||||
Reference in new issue
Block a user