fix(iframe): [PKG-IFRAME-05] checkpoint player and editor integration

This commit is contained in:
Harvey Zhao committed 2026-09-13 02:17:44 +08:00
1 parent 047e2df945
commit b35183c7af
15 files changed
+2547 -14

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs'
import { iframeCandidate } from '../helpers/iframe.js'
import { expect, test } from './fixtures.js'
const candidate = await iframeCandidate()
test('Iframe docs editor Run releases the previous tool and its pending requests', async ({ page }, testInfo) => {
await page.route('https://pagead2.googlesyndication.com/**', route => route.fulfill({ contentType: 'text/javascript', body: '' }))
await page.route('**/uncompiled/artplayer-tool-iframe/index.js', route => route.fulfill({
contentType: 'text/javascript',
body: `${candidate.code}\nif (window === window.top) {
window.demoTools = [];
window.ArtplayerToolIframe = class extends window.ArtplayerToolIframe {
constructor(option) { super(option); window.demoTools.push(this); }
};
}`,
}))
await page.goto('/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe')
await expect.poll(() => page.evaluate(() => window.demoTools?.length)).toBe(1)
const firstFrame = page.frames().find(frame => frame.url().endsWith('/iframe.html'))
await expect.poll(() => firstFrame.evaluate(() => window.Artplayer?.instances[0]?.isReady)).toBe(true)
await page.evaluate(() => {
window.demoTools[0].commit(() => {
const art = window.Artplayer.instances[0]
art.fullscreenWeb = true
})
})
await expect(page.locator('iframe')).toHaveClass('fullscreenWeb')
await firstFrame.locator('.art-video-player').hover()
await firstFrame.locator('.art-control-fullscreenWeb').click()
await expect(page.locator('iframe')).not.toHaveClass('fullscreenWeb')
await page.evaluate(() => {
window.pendingSettlement = 'pending'
window.demoTools[0].commit((resolve) => {
window.finishPendingDemo = () => resolve(1)
}).then(() => window.pendingSettlement = 'resolved', () => window.pendingSettlement = 'rejected')
})
await page.locator('.run').click()
await expect.poll(() => page.evaluate(() => window.demoTools.length)).toBe(2)
await expect.poll(() => page.evaluate(() => window.demoTools[0].destroyed)).toBe(true)
await expect.poll(() => page.evaluate(() => window.pendingSettlement)).toBe('rejected')
await expect(page.locator('iframe')).not.toHaveClass('fullscreenWeb')
const secondFrame = page.frames().find(frame => frame.url().endsWith('/iframe.html'))
await expect.poll(() => secondFrame.evaluate(() => window.Artplayer?.instances[0]?.isReady)).toBe(true)
await page.locator('.run').click()
await expect.poll(() => page.evaluate(() => window.demoTools.length)).toBe(3)
expect(await page.evaluate(() => window.demoTools.map(tool => tool.destroyed))).toEqual([true, true, false])
await page.evaluate(() => {
const model = window.monaco.editor.getModels().find(model => model.getLanguageId() === 'javascript')
model.setValue(`
document.querySelector('.artplayer-app').innerHTML = '<div class="first"></div><div class="second"></div>';
window.parentPlayers = ['.first', '.second'].map(container => new Artplayer({ container, url: '/test/pattern.mp4' }));
`)
const url = new URL(location.href)
url.searchParams.delete('example')
history.replaceState(null, '', url)
})
await page.locator('.run').click()
await expect.poll(() => page.evaluate(() => window.Artplayer.instances.length)).toBe(2)
expect(await page.evaluate(() => window.demoTools.every(tool => tool.destroyed))).toBe(true)
await page.evaluate(() => {
window.previousPlayers = window.parentPlayers
window.monaco.editor.getModels().find(model => model.getLanguageId() === 'javascript').setValue('window.exampleFinished = true')
})
await page.locator('.run').click()
await expect.poll(() => page.evaluate(() => window.exampleFinished)).toBe(true)
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
expect(await page.evaluate(() => window.previousPlayers.every(art => art.isDestroy))).toBe(true)
await testInfo.attach('iframe-editor', { contentType: 'application/json', body: JSON.stringify({ candidateSha256: hash(candidate.code), files: Object.fromEntries(['docs/index.html', 'docs/iframe.html', 'docs/assets/example/iframe.js', 'docs/assets/js/common.js'].map(file => [file, hash(fs.readFileSync(file))])), scope: 'Actual docs index, local Monaco and Run button. Only the iframe constructor is observed through a test subclass; external advertising script is stubbed in this test only. No physical-device/BFCache claim.' }) })
await page.evaluate(() => window.demoTools.forEach(tool => tool.destroy()))
})
+135
View File
@@ -0,0 +1,135 @@
import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs'
import { iframeCandidate, iframeHistorical } from '../helpers/iframe.js'
import { expect, test } from './fixtures.js'
const candidate = await iframeCandidate()
const published = (await iframeHistorical()).find(item => item.name === 'published-artplayer-plugin-iframe.js')
const demo = fs.readFileSync('docs/assets/example/iframe.js', 'utf8')
const childHtml = fs.readFileSync('docs/iframe.html', 'utf8')
const bridges = [[candidate, candidate], [published, candidate], [candidate, published], [published, published]]
for (const core of ['published-4.5.9', 'published', 'candidate']) {
for (const relation of ['same-origin', 'cross-origin']) {
for (const [parentTool, childTool] of bridges) {
test(`Iframe player ${core}/${relation}/${parentTool.name}/${childTool.name}`, async ({ page, request }, testInfo) => {
const coreResponse = await request.get(`/${core}/artplayer.js`)
expect(coreResponse.ok()).toBe(true)
const coreBytes = await coreResponse.body()
await page.addInitScript(() => {
window.mediaEvents = []
window.documentWitness = Math.random().toString(36)
for (const name of ['loadedmetadata', 'loadeddata', 'canplay', 'playing', 'pause', 'seeking', 'seeked', 'emptied', 'error']) {
addEventListener(name, (event) => {
if (event.target instanceof HTMLVideoElement)
window.mediaEvents.push({ type: name, source: event.target.currentSrc, time: event.target.currentTime, error: event.target.error?.code })
}, true)
}
})
const bridge = implementation => `${implementation.code}\n${implementation === published ? 'window.ArtplayerToolIframe = window.ArtplayerPluginIframe;' : ''}`
await page.route('**/uncompiled/artplayer-tool-iframe/index.js', route => route.fulfill({ contentType: 'text/javascript', body: bridge(childTool) }))
await page.route('**/uncompiled/artplayer/index.js', route => route.fulfill({ contentType: 'text/javascript', body: coreBytes }))
await page.goto('/test/player.html?core=published')
await page.addStyleTag({ url: '/assets/css/style.css' })
await page.evaluate(() => {
document.body.innerHTML = '<div class="artplayer-app" style="width:640px;height:360px"></div>'
window.messages = []
})
await page.addScriptTag({ content: bridge(parentTool) })
const parentOrigin = new URL(page.url()).origin
const childOrigin = relation === 'cross-origin' ? parentOrigin.replace('127.0.0.1', 'localhost') : parentOrigin
const url = `${childOrigin}/iframe.html`
const adaptedDemo = demo.replace('url: \'/iframe.html\'', `url: '${url}'`)
expect(adaptedDemo).not.toBe(demo)
await page.addScriptTag({ content: `${adaptedDemo}\nwindow.demoTool = iframe; window.demoFrame = $iframe; const originalCallback = iframe.messageCallback; iframe.message(function (packet) { originalCallback.call(this, packet); window.messages.push(packet); });` })
let child
try {
await expect.poll(() => page.frames().some(frame => frame.url() === url)).toBe(true)
child = page.frames().find(frame => frame.url() === url)
await expect.poll(() => child.evaluate(() => {
const art = window.Artplayer?.instances[0]
return Boolean(art && art.video.readyState >= 2 && art.video.videoWidth > 0)
})).toBe(true)
const initial = await page.evaluate(() => window.demoTool.commit(() => {
const art = window.Artplayer.instances[0]
art.muted = true
return { version: window.Artplayer.version, width: art.video.videoWidth, height: art.video.videoHeight, duration: art.duration, instances: window.Artplayer.instances.length, source: art.video.currentSrc }
}))
expect(initial.instances).toBe(1)
expect(initial.source).toContain('/assets/sample/video.mp4')
expect(initial.width).toBeGreaterThan(0)
const play = await page.evaluate(() => window.demoTool.commit((resolve) => {
window.Artplayer.instances[0].play().then(() => {
const result = { playing: !window.Artplayer.instances[0].video.paused }
resolve(result)
}).catch((error) => {
const result = { error: error.message }
resolve(result)
})
}))
expect(play).toEqual({ playing: true })
await expect.poll(() => child.evaluate(() => window.Artplayer.instances[0].currentTime)).toBeGreaterThan(0.15)
await child.locator('.art-video-player').hover()
await child.locator('.art-control-fullscreenWeb').click()
await expect(page.locator('iframe')).toHaveClass('fullscreenWeb')
const fullscreen = await page.locator('iframe').boundingBox()
expect(fullscreen.width).toBe(page.viewportSize().width)
expect(fullscreen.height).toBe(page.viewportSize().height)
await child.locator('.art-video-player').hover()
await child.locator('.art-control-fullscreenWeb').click()
await expect(page.locator('iframe')).not.toHaveClass('fullscreenWeb')
expect(await page.evaluate(() => window.messages.filter(packet => packet.type === 'fullscreenWeb').map(packet => packet.data))).toEqual([true, false])
await page.evaluate(() => window.demoTool.commit(() => {
const art = window.Artplayer.instances[0]
art.pause()
art.currentTime = 1
art.playbackRate = 1.25
}))
await expect.poll(() => child.evaluate(() => window.Artplayer.instances[0].video.seeking)).toBe(false)
const seek = await child.evaluate(() => ({ time: window.Artplayer.instances[0].currentTime, rate: window.Artplayer.instances[0].video.playbackRate, paused: window.Artplayer.instances[0].video.paused }))
expect(seek.time).toBeCloseTo(1, 1)
expect(seek.rate).toBe(1.25)
expect(seek.paused).toBe(true)
const switched = await page.evaluate(() => window.demoTool.commit((resolve) => {
const art = window.Artplayer.instances[0]
art.switchUrl('/test/pattern.mp4').then(() => {
const result = { source: art.video.currentSrc, width: art.video.videoWidth }
resolve(result)
}).catch((error) => {
const result = { error: error.message }
resolve(result)
})
}))
expect(switched.error).toBeUndefined()
expect(switched.source).toContain('/test/pattern.mp4')
await expect.poll(() => child.evaluate(() => window.Artplayer.instances[0].video.readyState >= 2)).toBe(true)
const events = await child.evaluate(() => window.mediaEvents)
expect(events.some(event => event.type === 'playing')).toBe(true)
expect(events.some(event => event.type === 'seeked')).toBe(true)
expect(events.filter(event => event.type === 'error')).toEqual([])
const destroyed = await page.evaluate(() => window.demoTool.commit(() => {
window.Artplayer.instances[0].destroy()
return { instances: window.Artplayer.instances.length, players: document.querySelectorAll('.art-video-player').length }
}))
expect(destroyed).toEqual({ instances: 0, players: 0 })
const afterDestroy = await page.evaluate(() => window.demoTool.commit(() => {
return 17
}))
expect(afterDestroy).toBe(17)
await testInfo.attach('iframe-player', { contentType: 'application/json', body: JSON.stringify({ core, coreSha256: hash(coreBytes), parent: { name: parentTool.name, sha256: hash(parentTool.code) }, child: { name: childTool.name, sha256: hash(childTool.code) }, relation, initial, seek, switched, destroyed, events, demoSha256: hash(demo), adaptedDemoSha256: hash(adaptedDemo), childHtmlSha256: hash(childHtml), scope: 'Actual docs iframe.html, parent example and CSS in a minimal parent harness; only source URL and explicit test-only old global-name aliases are adapted. Actual media plays/seeks/switches, fullscreenWeb control sends messages and both core/tool lifetimes are tested independently. No complete Monaco/device/BFCache claim.' }) })
}
finally {
if (child && !child.isDetached()) {
await child.evaluate(() => {
for (const art of [...(window.Artplayer?.instances || [])]) art.destroy()
})
}
await page.evaluate(() => {
window.demoTool?.destroy()
window.demoFrame?.remove()
})
}
})
}
}
}
+6 -1
View File
@@ -69,6 +69,11 @@ const vastCoreMember = `package/${vastCore.manifest.main}`
const vastCoreBytes = readMember(await ensureArchive(vastCore), vastCoreMember)
assert.equal(hash(vastCoreBytes), vastCore.files[vastCoreMember], 'Historical VAST core differs from frozen release')
add('/published-5.1.7/artplayer.js', vastCoreBytes, { kind: 'npm-release', version: vastCore.version, integrity: vastCore.integrity, member: vastCoreMember })
const iframeCore = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/iframe-core.json'), 'utf8')).release
const iframeCoreMember = `package/${iframeCore.manifest.main}`
const iframeCoreBytes = readMember(await ensureArchive(iframeCore), iframeCoreMember)
assert.equal(hash(iframeCoreBytes), iframeCore.files[iframeCoreMember], 'Historical Iframe core differs from frozen release')
add('/published-4.5.9/artplayer.js', iframeCoreBytes, { kind: 'npm-release', version: iframeCore.version, integrity: iframeCore.integrity, member: iframeCoreMember })
add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/media/audio-tone.m4a')), { kind: 'generated-media', file: 'test/browser/media/audio-tone.m4a' })
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
@@ -138,7 +143,7 @@ const server = http.createServer((req, res) => {
if (url.pathname === '/test/player.html') {
const core = url.searchParams.get('core') || 'candidate'
const chapter = url.searchParams.get('chapter') || 'candidate'
assert(['candidate', 'published', 'published-4.5.5', 'published-5.1.7'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
assert(['candidate', 'published', 'published-4.5.5', 'published-4.5.9', 'published-5.1.7'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
const html = fs.readFileSync(path.join(workspace, 'test/browser/player.html'), 'utf8')
.replace('__CORE__', core)
.replace('__CHAPTER__', chapter)