mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:39:41 -08:00
262 lines
13 KiB
JavaScript
262 lines
13 KiB
JavaScript
import process from 'node:process'
|
|
import { hash } from '../../refactor/scripts/releases.mjs'
|
|
import { browserCandidate } from '../helpers/browser-candidate.js'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
let provenance
|
|
let candidate
|
|
test.beforeAll(async () => {
|
|
const input = await browserCandidate('artplayer-plugin-chromecast', process.env.ARTPLAYER_CHROMECAST_ARTIFACT)
|
|
candidate = input.code
|
|
provenance = input.provenance
|
|
})
|
|
|
|
async function setup(page, core, testInfo, loaded = true) {
|
|
const external = []
|
|
await page.route('**/*', (route) => {
|
|
const url = new URL(route.request().url())
|
|
const local = ['127.0.0.1', 'localhost'].includes(url.hostname)
|
|
const localBlob = url.protocol === 'blob:' && url.origin === new URL(testInfo.project.use.baseURL).origin
|
|
if (!local && !localBlob) {
|
|
external.push(url.href)
|
|
return route.abort()
|
|
}
|
|
return route.continue()
|
|
})
|
|
await page.goto(`/test/player.html?core=${core}`)
|
|
await page.addScriptTag({ content: candidate })
|
|
await page.evaluate(async (loaded) => {
|
|
const listeners = new Map()
|
|
const stub = window.castStub = {
|
|
requests: [],
|
|
loads: [],
|
|
options: [],
|
|
callbacks: [[], []],
|
|
starts: [0, 0],
|
|
errors: [[], []],
|
|
current: null,
|
|
deferredRequest: false,
|
|
previousCalls: [],
|
|
emit(type, event) { for (const listener of [...(listeners.get(type) || [])]) listener(event) },
|
|
count() { return [...listeners.values()].reduce((sum, set) => sum + set.size, 0) },
|
|
sessionState(value) {
|
|
this.emit('session', { sessionState: value, session: this.current })
|
|
},
|
|
}
|
|
const session = {
|
|
loadMedia(request) {
|
|
return new Promise((resolve, reject) => stub.loads.push({ request, resolve, reject }))
|
|
},
|
|
}
|
|
const context = {
|
|
setOptions(option) { stub.options.push(option) },
|
|
getCurrentSession() { return stub.current },
|
|
addEventListener(type, listener) {
|
|
if (!listeners.has(type))
|
|
listeners.set(type, new Set())
|
|
listeners.get(type).add(listener)
|
|
},
|
|
removeEventListener(type, listener) { listeners.get(type)?.delete(listener) },
|
|
requestSession() {
|
|
return new Promise((resolve) => {
|
|
const accept = () => {
|
|
stub.current = session
|
|
stub.sessionState('SESSION_STARTED')
|
|
// The real SDK resolves without returning the CastSession.
|
|
resolve(undefined)
|
|
}
|
|
stub.requests.push({ accept })
|
|
if (!stub.deferredRequest)
|
|
accept()
|
|
})
|
|
},
|
|
}
|
|
window.installCastStub = () => {
|
|
window.cast = { framework: {
|
|
CastContext: { getInstance: () => context },
|
|
CastContextEventType: { SESSION_STATE_CHANGED: 'session', CAST_STATE_CHANGED: 'availability' },
|
|
SessionState: Object.fromEntries(['NO_SESSION', 'SESSION_STARTING', 'SESSION_STARTED', 'SESSION_RESUMED', 'SESSION_ENDING', 'SESSION_ENDED', 'SESSION_START_FAILED'].map(name => [name, name])),
|
|
CastState: Object.fromEntries(['NO_DEVICES_AVAILABLE', 'NOT_CONNECTED', 'CONNECTING', 'CONNECTED'].map(name => [name, name])),
|
|
} }
|
|
window.chrome ||= {}
|
|
window.chrome.cast = {
|
|
AutoJoinPolicy: { ORIGIN_SCOPED: 'origin' },
|
|
media: {
|
|
DEFAULT_MEDIA_RECEIVER_APP_ID: 'controlled-receiver',
|
|
MediaInfo: class { constructor(contentId, contentType) { Object.assign(this, { contentId, contentType }) } },
|
|
LoadRequest: class { constructor(media) { this.media = media } },
|
|
},
|
|
}
|
|
}
|
|
if (loaded)
|
|
window.installCastStub()
|
|
window.previousCastCallback = (...args) => stub.previousCalls.push(args)
|
|
window.__onGCastApiAvailable = window.previousCastCallback
|
|
window.castPlayers = []
|
|
window.registrationBefore = []
|
|
for (let index = 0; index < 2; index++) {
|
|
const container = index === 0 ? document.querySelector('.player') : document.body.appendChild(document.createElement('div'))
|
|
container.id = `cast-player-${index}`
|
|
container.style.cssText = 'width:480px;height:270px'
|
|
const art = new window.Artplayer({ container, url: `/test/pattern.mp4?player=${index}`, muted: true, mutex: false })
|
|
window.castPlayers.push(art)
|
|
const registration = art.plugins.add(window.artplayerPluginChromecast({
|
|
sdk: '/test/controlled-cast-sdk.js',
|
|
onStateChange: state => stub.callbacks[index].push(state),
|
|
onCastStart: () => stub.starts[index]++,
|
|
onError: error => stub.errors[index].push(String(error)),
|
|
}))
|
|
window.registrationBefore.push({ promise: registration instanceof Promise, absent: !art.plugins.artplayerPluginChromecast })
|
|
await registration
|
|
art.controls.show = true
|
|
}
|
|
}, loaded)
|
|
await expect.poll(() => page.evaluate(() => window.castPlayers.every(art => art.isReady))).toBe(true)
|
|
await testInfo.attach('chromecast-controlled-browser-inputs', { contentType: 'application/json', body: JSON.stringify({ core, provenance, sha256: hash(candidate), sdk: 'controlled stub; no receiver, discovery, real SDK or Cast media playback acceptance', native: 'ArtPlayer core, DOM controls, browser click dispatch and local video' }) })
|
|
return external
|
|
}
|
|
|
|
const button = (page, index) => page.locator(`#cast-player-${index} .art-control-chromecast`)
|
|
const icon = (page, index) => page.locator(`#cast-player-${index} .art-icon-cast`)
|
|
const loads = page => page.evaluate(() => window.castStub.loads.length)
|
|
|
|
test.afterEach(async ({ page }, testInfo) => {
|
|
await page.evaluate(() => {
|
|
for (const art of window.castPlayers || []) {
|
|
if (!art.isDestroy)
|
|
art.destroy()
|
|
}
|
|
})
|
|
await testInfo.attach('chromecast-controlled-browser-state', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ version: window.Artplayer?.version, listeners: window.castStub?.count(), starts: window.castStub?.starts, errors: window.castStub?.errors, callbacks: window.castStub?.callbacks, controls: document.querySelectorAll('.art-control-chromecast').length }))) })
|
|
expect(await page.evaluate(() => window.castStub?.count() ?? 0)).toBe(0)
|
|
})
|
|
|
|
for (const core of ['published-5.3.0', 'published', 'candidate']) {
|
|
test(`${core}: Cast async registration and native clicks update the owning control`, async ({ page }, testInfo) => {
|
|
const external = await setup(page, core, testInfo)
|
|
expect(await page.evaluate(() => window.registrationBefore)).toEqual([{ promise: true, absent: true }, { promise: true, absent: true }])
|
|
await expect(button(page, 0)).toHaveCount(1)
|
|
await expect(button(page, 1)).toHaveCount(1)
|
|
await button(page, 1).click()
|
|
await expect.poll(() => loads(page)).toBe(1)
|
|
await expect(icon(page, 1)).toHaveCSS('color', 'rgb(255, 0, 0)')
|
|
expect(await icon(page, 0).evaluate(node => node.style.color)).toBe('')
|
|
expect(await page.evaluate(() => window.castStub.loads[0].request.media)).toEqual({ contentId: '/test/pattern.mp4?player=1', contentType: 'video/mp4' })
|
|
await button(page, 1).click()
|
|
expect(await loads(page)).toBe(1)
|
|
await page.evaluate(() => window.castStub.loads[0].resolve(undefined))
|
|
await expect.poll(() => page.evaluate(() => window.castStub.starts)).toEqual([0, 1])
|
|
await button(page, 0).click()
|
|
await expect.poll(() => loads(page)).toBe(2)
|
|
expect(await page.evaluate(() => window.castStub.requests.length)).toBe(1)
|
|
await page.evaluate(() => {
|
|
window.castStub.sessionState('SESSION_RESUMED')
|
|
window.castStub.loads[1].resolve(undefined)
|
|
})
|
|
await expect(icon(page, 0)).toHaveCSS('color', 'rgb(255, 0, 0)')
|
|
await expect.poll(() => page.evaluate(() => window.castStub.starts)).toEqual([1, 1])
|
|
expect(await page.evaluate(() => [window.castStub.options.length, window.castStub.count()])).toEqual([1, 4])
|
|
expect(external).toEqual([])
|
|
})
|
|
|
|
test(`${core}: concurrent Cast controls share one local SDK load and release a destroyed waiter`, async ({ page }, testInfo) => {
|
|
let sdkRoute
|
|
let finish
|
|
const held = new Promise(resolve => finish = resolve)
|
|
const external = await setup(page, core, testInfo, false)
|
|
const requests = []
|
|
await page.route('**/test/controlled-cast-sdk.js', async (route) => {
|
|
requests.push(route.request().url())
|
|
sdkRoute = route
|
|
await held
|
|
await route.fulfill({ contentType: 'text/javascript', body: 'window.installCastStub(); window.__onGCastApiAvailable(true);' })
|
|
})
|
|
try {
|
|
await button(page, 0).click()
|
|
await button(page, 1).click()
|
|
await expect.poll(() => Boolean(sdkRoute)).toBe(true)
|
|
expect(requests).toHaveLength(1)
|
|
expect(await page.locator('script[src="/test/controlled-cast-sdk.js"]').count()).toBe(1)
|
|
await page.evaluate(() => window.castPlayers[0].destroy())
|
|
finish()
|
|
await expect.poll(() => loads(page)).toBe(1)
|
|
expect(await page.evaluate(() => [window.castStub.options.length, window.castStub.count(), window.__onGCastApiAvailable === window.previousCastCallback])).toEqual([1, 2, true])
|
|
expect(await page.evaluate(() => window.castStub.previousCalls)).toEqual([[true, undefined]])
|
|
await page.evaluate(() => window.castStub.loads[0].resolve(undefined))
|
|
await expect.poll(() => page.evaluate(() => window.castStub.starts)).toEqual([0, 1])
|
|
expect(await page.evaluate(() => window.castStub.callbacks[0])).toEqual([])
|
|
expect(external).toEqual([])
|
|
}
|
|
finally { finish() }
|
|
})
|
|
|
|
test(`${core}: Cast terminal states clear native controls and suppress stale media success`, async ({ page }, testInfo) => {
|
|
const external = await setup(page, core, testInfo)
|
|
for (const [index, state] of ['SESSION_ENDED', 'SESSION_START_FAILED', 'NO_SESSION'].entries()) {
|
|
await button(page, 1).click()
|
|
await expect.poll(() => loads(page)).toBe(index + 1)
|
|
await expect(icon(page, 1)).toHaveCSS('color', 'rgb(255, 0, 0)')
|
|
await page.evaluate(() => window.castStub.sessionState('SESSION_ENDING'))
|
|
await expect(icon(page, 1)).toHaveCSS('color', 'rgb(255, 165, 0)')
|
|
await page.evaluate((state) => {
|
|
window.castStub.current = null
|
|
window.castStub.sessionState(state)
|
|
}, state)
|
|
await expect(icon(page, 1)).toHaveCSS('color', 'rgb(255, 255, 255)')
|
|
expect(await page.evaluate(() => {
|
|
const plugin = window.castPlayers[1].plugins.artplayerPluginChromecast
|
|
return [plugin.getCastState(), plugin.isCasting()]
|
|
})).toEqual([state, false])
|
|
await page.evaluate(index => window.castStub.loads[index].resolve(undefined), index)
|
|
expect(await page.evaluate(() => window.castStub.starts)).toEqual([0, 0])
|
|
}
|
|
await button(page, 1).click()
|
|
await expect.poll(() => loads(page)).toBe(4)
|
|
await page.evaluate(() => window.castStub.loads[3].resolve(undefined))
|
|
await expect.poll(() => page.evaluate(() => window.castStub.starts)).toEqual([0, 1])
|
|
expect(external).toEqual([])
|
|
})
|
|
|
|
test(`${core}: destroy unbinds Cast listeners during pending session and media operations`, async ({ page }, testInfo) => {
|
|
const external = await setup(page, core, testInfo)
|
|
await page.evaluate(() => window.castStub.deferredRequest = true)
|
|
await button(page, 0).click()
|
|
await expect.poll(() => page.evaluate(() => window.castStub.requests.length)).toBe(1)
|
|
expect(await page.evaluate(() => window.castStub.count())).toBe(2)
|
|
await page.evaluate(() => {
|
|
window.castPlayers[0].destroy()
|
|
window.castStub.requests[0].accept()
|
|
})
|
|
expect(await loads(page)).toBe(0)
|
|
expect(await page.evaluate(() => [window.castStub.count(), window.castPlayers[0].plugins.artplayerPluginChromecast.isCasting()])).toEqual([0, false])
|
|
await button(page, 1).click()
|
|
await expect.poll(() => loads(page)).toBe(1)
|
|
await page.evaluate(() => {
|
|
window.castPlayers[1].destroy()
|
|
window.castStub.loads[0].resolve(undefined)
|
|
window.castStub.sessionState('SESSION_STARTED')
|
|
})
|
|
expect(await page.evaluate(() => [window.castStub.count(), window.castStub.starts, window.castStub.callbacks])).toEqual([0, [0, 0], [[], []]])
|
|
await expect(button(page, 0)).toHaveCount(0)
|
|
await expect(button(page, 1)).toHaveCount(0)
|
|
expect(external).toEqual([])
|
|
})
|
|
|
|
test(`${core}: native Cast click reports media rejection once and permits retry`, async ({ page }, testInfo) => {
|
|
const external = await setup(page, core, testInfo)
|
|
await button(page, 1).click()
|
|
await expect.poll(() => loads(page)).toBe(1)
|
|
await page.evaluate(() => window.castStub.loads[0].reject(new Error('Controlled media load failure')))
|
|
await expect.poll(() => page.evaluate(() => window.castStub.errors)).toEqual([[], ['Error: Controlled media load failure']])
|
|
await expect(page.locator('#cast-player-1 .art-notice')).toContainText('Error casting media')
|
|
expect(await page.evaluate(() => window.castStub.starts)).toEqual([0, 0])
|
|
await button(page, 1).click()
|
|
await expect.poll(() => loads(page)).toBe(2)
|
|
await page.evaluate(() => window.castStub.loads[1].resolve(undefined))
|
|
await expect.poll(() => page.evaluate(() => window.castStub.starts)).toEqual([0, 1])
|
|
await expect(page.locator('#cast-player-1 .art-notice')).toContainText('Casting started')
|
|
expect(await page.evaluate(() => window.castStub.errors)).toEqual([[], ['Error: Controlled media load failure']])
|
|
expect(external).toEqual([])
|
|
})
|
|
}
|