test(dash): [PKG-DASH-02] cover released contracts and real menu interactions

This commit is contained in:
Harvey Zhao committed 2026-09-12 04:42:16 +08:00
1 parent 3d4c636101
commit 762cc63431
12 files changed
+2580 -15

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
let publishedCode
let sourceCode
let evidence
test.beforeAll(async () => {
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-control-release.json', import.meta.url)))
const member = 'package/dist/artplayer-plugin-dash-control.js'
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
publishedCode = bytes.toString()
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Controlled DASH source tests do not represent installed SDK/package acceptance')
sourceCode = process.env.ARTPLAYER_DASH_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DASH_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
evidence = { published: release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', scope: 'Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation.' }
})
test.afterEach(async ({ page }, testInfo) => {
const state = await page.evaluate(() => ({
calls: window.dashCalls,
ready: window.art?.isReady,
time: window.art?.currentTime,
videoError: window.art?.video.error?.code || null,
quality: window.art?.controls['dash-quality']?.textContent,
audio: window.art?.controls['dash-audio']?.textContent,
settingRows: [...document.querySelectorAll('.art-setting-panel.art-current .art-setting-item')].map(row => ({ text: row.textContent, label: row.querySelector('.art-setting-item-left-text')?.textContent })),
})).catch(error => ({ error: error.message }))
await testInfo.attach('controlled-dash-state', { contentType: 'application/json', body: JSON.stringify(state) })
if (!page.isClosed())
await page.evaluate(() => window.art?.destroy())
})
async function openDash(page, core, plugin, testInfo) {
await testInfo.attach('controlled-dash-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
await page.evaluate((sdk) => {
window.dashCalls = []
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
muted: true,
setting: true,
plugins: [(art) => {
const levels = sdk === 4
? [{ qualityIndex: 0, height: 360 }, { qualityIndex: 1, height: 720 }]
: [{ id: 'low', height: 360 }, { id: 'high', height: 720 }]
const tracks = [{ id: 'en', lang: 'en' }, { id: 'fr', lang: 'fr' }]
window.dashState = { levels, tracks, track: tracks[0], current: levels[0], quality: 0, settings: { streaming: { abr: { autoSwitchBitrate: { video: true } } } } }
const state = window.dashState
art.dash = {
getVideoElement: () => art.template.$video,
getSettings: () => state.settings,
updateSettings(update) {
state.settings.streaming.abr.autoSwitchBitrate.video = update.streaming.abr.autoSwitchBitrate.video
window.dashCalls.push(['auto', state.settings.streaming.abr.autoSwitchBitrate.video])
},
getTracksFor: () => state.tracks,
getCurrentTrackFor: () => state.track,
setCurrentTrack(track) {
state.track = track
window.dashCalls.push(['track', track.id, tracks.includes(track)])
},
}
if (sdk === 4) {
Object.assign(art.dash, {
getBitrateInfoListFor: () => levels,
getQualityFor: () => state.quality,
setQualityFor(type, index) {
state.quality = index
window.dashCalls.push(['qualityIndex', type, index])
},
})
}
else {
Object.assign(art.dash, {
getRepresentationsByType: () => levels,
getCurrentRepresentationForType: () => state.current,
setRepresentationForTypeById(type, id) {
state.current = levels.find(level => level.id === id)
window.dashCalls.push(['representationId', type, id])
},
})
}
return window.artplayerPluginDashControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })(art)
}],
})
document.querySelector('#play').onclick = () => window.art.play()
document.querySelector('#pause').onclick = () => window.art.pause()
}, plugin === 'published' ? 4 : 5)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await expect(page.locator('.art-control-dash-quality .art-selector-value')).toHaveText('Auto')
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('en')
}
for (const core of ['published', 'candidate']) {
for (const plugin of ['published', 'candidate']) {
for (const surface of ['control', 'setting']) {
for (const type of ['quality', 'audio']) {
test(`${core} core + ${plugin} DASH: controlled SDK ${type} selection through real ${surface} DOM`, async ({ page }, testInfo) => {
await openDash(page, core, plugin, testInfo)
const text = type === 'quality' ? '720p' : 'fr'
if (surface === 'control') {
const control = page.locator(`.art-control-dash-${type}`)
await control.hover()
await expect(control.locator('.art-selector-list')).toHaveCSS('opacity', '1')
await control.locator('.art-selector-item').filter({ hasText: new RegExp(`^${text}$`) }).click()
}
else {
await page.locator('.art-control-setting').click()
await page.locator(`[data-name="dash-${type}"]`).click()
await page.locator('.art-setting-panel.art-current .art-setting-item-left-text').filter({ hasText: new RegExp(`^${text}$`) }).click()
}
const expected = type === 'quality'
? [['auto', false], plugin === 'published' ? ['qualityIndex', 'video', 1] : ['representationId', 'video', 'high']]
: [['track', 'fr', true]]
await expect.poll(() => page.evaluate(() => window.dashCalls)).toEqual(expected)
await expect(page.locator(`.art-control-dash-${type} .art-selector-value`)).toHaveText(text)
expect(await page.evaluate(type => window.art.setting.find(`dash-${type}`).tooltip, type)).toBe(text)
await testInfo.attach('controlled-dash-selection', { contentType: 'image/png', body: await page.screenshot() })
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.2)
expect(await page.evaluate(() => window.art.video.error)).toBeNull()
})
}
}
}
}
+263
View File
@@ -0,0 +1,263 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Controlled plugin contracts use Node's built-in runner.
import test from 'node:test'
import { bothMenus, dashHost, dashImplementations } from './helpers/dash-control.js'
const implementations = await dashImplementations()
const abr = video => ({ streaming: { abr: { autoSwitchBitrate: { video } } } })
for (const { name, factory, sdk } of implementations) {
test(`${name}: optional factory, deferred SDK binding and synchronous update shape`, () => {
const host = dashHost(sdk)
delete host.art.dash
const result = factory()(host.art)
assert.deepEqual(Object.keys(result).sort(), ['name', 'update'])
assert.equal(result.name, 'artplayerPluginDashControl')
assert.equal(host.calls.length, 0)
host.art.dash = host.dash
assert.equal(result.update(), undefined)
assert.equal(host.controls.size, 0)
assert.equal(host.settings.size, 0)
})
test(`${name}: ready/restart render named surfaces and preserve default Auto/audio behavior`, () => {
const host = dashHost(sdk)
factory(bothMenus())(host.art)
assert.equal(host.controls.size, 0)
host.art.emit('ready')
assert.deepEqual([...host.controls.keys()], ['dash-quality', 'dash-audio'])
assert.deepEqual([...host.settings.keys()], ['dash-quality', 'dash-audio'])
const quality = host.controls.get('dash-quality')
const audio = host.controls.get('dash-audio')
assert.equal(quality.html, 'Auto')
assert.deepEqual(Array.from(quality.selector, item => item.html), ['1080p', '720p', '360p', 'Auto'])
assert.deepEqual(Array.from(quality.selector).filter(item => item.default).map(item => item.value), ['auto'])
assert.equal(audio.html, 'en')
assert.deepEqual(Array.from(audio.selector, item => item.value), host.state.tracks)
assert.equal(audio.selector[0].default, true)
assert.equal(audio.selector.length, 2, 'Audio has no synthetic Auto item')
for (const key of ['dash-quality', 'dash-audio']) {
assert.equal(host.controls.get(key).position, 'right')
assert.equal(host.controls.get(key).style.padding, '0 10px')
assert.equal(host.settings.get(key).width, 200)
assert.match(host.settings.get(key).icon, /<svg/)
}
host.calls.length = 0
host.art.emit('restart', 'new.mpd')
assert.deepEqual(host.calls.map(call => call[0]), ['controls.update', 'setting.update', 'controls.update', 'setting.update'])
})
test(`${name}: selectors preserve SDK/check identities, notice order and sync return values`, () => {
for (const surface of ['controls', 'settings']) {
const host = dashHost(sdk)
factory(bothMenus())(host.art).update()
const menu = host[surface].get('dash-quality')
const item = menu.selector[0]
host.calls.length = 0
assert.equal(menu.onSelect(item), '1080p')
assert.deepEqual(host.calls, [
['updateSettings', abr(false)],
sdk === 4 ? ['setQualityFor', 'video', 2] : ['setRepresentationForTypeById', 'video', 'top'],
['notice', 'Quality: 1080p'],
['controls.check', item],
['setting.check', item],
])
assert.equal(host.state.settings.streaming.abr.autoSwitchBitrate.audio, false)
assert.equal(host.state.settings.streaming.abr.minBitrate.video, -1)
host.calls.length = 0
const auto = menu.selector.at(-1)
assert.equal(menu.onSelect(auto), 'Auto')
assert.deepEqual(host.calls, [['updateSettings', abr(true)], ['notice', 'Quality: Auto'], ['controls.check', auto], ['setting.check', auto]])
const audio = host[surface].get('dash-audio')
const track = audio.selector[1]
host.calls.length = 0
assert.equal(audio.onSelect(track), 'fr')
assert.equal(host.state.currentTrack, host.state.tracks[1])
assert.deepEqual(host.calls, [['setCurrentTrack', host.state.tracks[1]], ['notice', 'Audio: fr'], ['controls.check', track], ['setting.check', track]])
}
})
test(`${name}: getName sees original objects, one argument and no receiver binding`, () => {
const host = dashHost(sdk)
host.state.settings.streaming.abr.autoSwitchBitrate.video = false
const qualityCalls = []
const audioCalls = []
const options = bothMenus()
options.quality.getName = function (...args) {
qualityCalls.push([this, args])
return `Q${args[0].height}`
}
options.audio.getName = function (...args) {
audioCalls.push([this, args])
return `A${args[0].lang}`
}
factory(options)(host.art).update()
assert.deepEqual(qualityCalls.map(([receiver, args]) => [receiver, args.length, args[0]]), [host.state.levels[1], ...host.state.levels].map(item => [undefined, 1, item]))
assert.deepEqual(audioCalls.map(([receiver, args]) => [receiver, args.length, args[0]]), [host.state.tracks[0], ...host.state.tracks].map(item => [undefined, 1, item]))
assert.equal(host.controls.get('dash-quality').html, 'Q720')
assert.equal(host.controls.get('dash-audio').html, 'Aen')
})
test(`${name}: labels deduplicate without mutating caller SDK arrays`, () => {
const host = dashHost(sdk)
host.state.levels[1].height = 360
host.state.tracks[1].lang = 'en'
const levels = [...host.state.levels]
const tracks = [...host.state.tracks]
const before = JSON.stringify({ levels, tracks })
factory(bothMenus())(host.art).update()
assert.deepEqual(Array.from(host.controls.get('dash-quality').selector, item => item.html), ['1080p', '360p', 'Auto'])
assert.equal(host.controls.get('dash-audio').selector.length, 1)
assert.equal(host.controls.get('dash-audio').selector[0].value, tracks[0])
assert.equal(JSON.stringify({ levels: host.state.levels, tracks: host.state.tracks }), before)
assert.deepEqual(host.state.levels, levels)
})
test(`${name}: explicit quality keys survive noncontiguous SDK metadata`, () => {
const host = dashHost(sdk)
host.state.levels.forEach((level, index) => {
level[sdk === 4 ? 'qualityIndex' : 'absoluteIndex'] = 10 + index * 20
})
factory(bothMenus())(host.art).update()
const menu = host.controls.get('dash-quality')
const item = menu.selector.find(item => item.html === '720p')
host.calls.length = 0
menu.onSelect(item)
assert.deepEqual(host.calls[1], sdk === 4 ? ['setQualityFor', 'video', 30] : ['setRepresentationForTypeById', 'video', 'high'])
})
test(`${name}: empty topology has no UI and later ready data can render`, () => {
const host = dashHost(sdk)
const { levels, tracks } = host.state
host.state.levels = []
host.state.tracks = null
const plugin = factory(bothMenus())(host.art)
plugin.update()
assert.equal(host.calls.length, 0)
host.state.levels = levels
host.state.tracks = tracks
host.art.emit('ready')
assert.equal(host.controls.size, 2)
})
test(`${name}: truthy label defaults and per-surface enablement remain unchanged`, () => {
const host = dashHost(sdk)
factory({ quality: { control: true, title: '', auto: '' }, audio: { setting: true, title: '' } })(host.art).update()
assert.deepEqual([...host.controls.keys()], ['dash-quality'])
assert.deepEqual([...host.settings.keys()], ['dash-audio'])
assert.equal(host.controls.get('dash-quality').html, 'Auto')
assert.equal(host.settings.get('dash-audio').html, 'Audio')
host.calls.length = 0
const audio = host.settings.get('dash-audio')
audio.onSelect(audio.selector[1])
assert.equal(host.calls.some(call => call[0] === 'controls.check'), false)
})
test(`${name}: missing and wrong media ownership fails synchronously`, () => {
const host = dashHost(sdk)
const plugin = factory(bothMenus())(host.art)
host.state.video = {}
assert.throws(plugin.update, { message: 'Cannot find instance of DASH from "art.dash"' })
assert.equal(host.controls.size, 0)
delete host.art.dash
assert.throws(plugin.update, { name: 'TypeError' })
host.art.dash = host.dash
host.state.video = host.video
plugin.update()
assert.equal(host.controls.size, 2)
})
test(`${name}: caller getName and SDK setter failures preserve error identity`, () => {
const host = dashHost(sdk)
const failure = new Error('caller failure')
const options = bothMenus()
options.quality.getName = () => {
throw failure
}
const plugin = factory(options)(host.art)
assert.throws(plugin.update, error => error === failure)
assert.equal(host.controls.size, 0)
delete options.quality.getName
plugin.update()
host.dash[sdk === 4 ? 'setQualityFor' : 'setRepresentationForTypeById'] = () => {
throw failure
}
host.calls.length = 0
const menu = host.controls.get('dash-quality')
assert.throws(() => menu.onSelect(menu.selector[0]), error => error === failure)
assert.deepEqual(host.calls, [['updateSettings', abr(false)]])
})
test(`${name}: SDK replacement and multiple hosts retain independent selections`, () => {
const left = dashHost(sdk)
const right = dashHost(sdk)
factory(bothMenus())(left.art).update()
factory(bothMenus())(right.art).update()
right.state.video = left.video
left.art.dash = right.dash
left.art.emit('restart')
left.calls.length = 0
right.calls.length = 0
const menu = left.controls.get('dash-audio')
menu.onSelect(menu.selector[1])
assert.equal(right.state.currentTrack, right.state.tracks[1])
assert.equal(left.state.currentTrack, left.state.tracks[0])
assert.equal(left.calls.some(call => call[0] === 'setCurrentTrack'), false)
assert.equal(right.calls[0][0], 'setCurrentTrack')
})
}
// These historical factories stay frozen when the live source is repaired.
for (const { name, factory, sdk } of implementations.filter(item => item.name.startsWith('published') || item.name === 'pre-refactor-v5')) {
test(`${name} observation DASH-LIFE-01: empty/disabled surfaces remain registered`, () => {
const host = dashHost(sdk)
const options = bothMenus()
const plugin = factory(options)(host.art)
plugin.update()
const previous = host.controls.get('dash-quality')
host.state.levels = []
host.state.tracks = []
plugin.update()
assert.equal(host.controls.get('dash-quality'), previous)
options.quality.control = options.quality.setting = false
options.audio.control = options.audio.setting = false
plugin.update()
assert.equal(host.controls.size, 2)
assert.equal(host.settings.size, 2)
})
test(`${name} observation DASH-LIFE-01: retained callbacks outlive destruction and SDK replacement`, () => {
const host = dashHost(sdk)
const plugin = factory(bothMenus())(host.art)
plugin.update()
const menu = host.controls.get('dash-quality')
const next = dashHost(sdk)
next.state.video = host.video
host.art.dash = next.dash
plugin.update()
host.art.destroy()
host.calls.length = 0
menu.onSelect(menu.selector[0])
assert.equal(host.calls[1][0], sdk === 4 ? 'setQualityFor' : 'setRepresentationForTypeById')
assert.equal(host.listeners.get('ready').size, 1)
assert.equal(host.listeners.get('restart').size, 1)
host.calls.length = 0
plugin.update()
assert.equal(host.calls.filter(call => call[0].endsWith('.update')).length, 4)
assert.equal(host.calls.some(call => call[0] === 'dash.destroy'), false)
})
test(`${name} observation DASH-STATE-01: cloned current track and duplicate current label lose highlighting`, () => {
const host = dashHost(sdk)
host.state.currentTrack = { ...host.state.tracks[1] }
host.state.settings.streaming.abr.autoSwitchBitrate.video = false
host.state.levels[1].height = 360
factory(bothMenus())(host.art).update()
assert.equal(host.controls.get('dash-audio').html, 'fr')
assert.equal(Array.from(host.controls.get('dash-audio').selector).filter(item => item.default).length, 0)
assert.equal(host.controls.get('dash-quality').html, '360p')
assert.equal(Array.from(host.controls.get('dash-quality').selector).filter(item => item.default).length, 0)
})
}
test('pre-refactor v5 observation: numeric zero representation ID loses manual highlighting', () => {
const { factory } = implementations.find(item => item.name === 'pre-refactor-v5')
const host = dashHost(5)
host.state.levels[1].id = 0
host.state.settings.streaming.abr.autoSwitchBitrate.video = false
factory(bothMenus())(host.art).update()
const menu = host.controls.get('dash-quality')
assert.equal(menu.html, '720p')
assert.equal(Array.from(menu.selector).filter(item => item.default).length, 0)
})
test('frozen SDK generations demonstrate the two incompatible quality method surfaces', () => {
const published = implementations.find(item => item.name === 'published-main').factory
const historical = implementations.find(item => item.name === 'pre-refactor-v5').factory
assert.throws(() => published(bothMenus())(dashHost(5).art).update(), /getBitrateInfoListFor/)
assert.throws(() => historical(bothMenus())(dashHost(4).art).update(), /getRepresentationsByType/)
})
+194
View File
@@ -0,0 +1,194 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import vm from 'node:vm'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { loadPackage } from './load.js'
async function factoryFromBytes(bytes, module) {
if (module)
return (await import(`data:text/javascript;base64,${Buffer.from(bytes).toString('base64')}`)).default
const context = { module: { exports: {} } }
context.exports = context.module.exports
vm.runInNewContext(bytes.toString(), context, { timeout: 5000 })
assert.equal(typeof context.module.exports, 'function')
return context.module.exports
}
export async function dashImplementations() {
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-control-release.json', import.meta.url)))
const { release } = baseline
const archive = await ensureArchive(release)
const implementations = [{ name: 'source', sdk: 5, factory: (await loadPackage(release.name)).default }]
const historicalFile = `packages/${release.name}/dist/artplayer-plugin-dash-control.js`
const historical = execFileSync('git', ['show', `${baseline.sourceCommit}:${historicalFile}`])
assert.equal(hash(historical.toString().replace(/\r\n/g, '\n')), baseline.source[historicalFile])
implementations.push({ name: 'pre-refactor-v5', sdk: 5, factory: await factoryFromBytes(historical, false) })
for (const format of ['main', 'legacy', 'module']) {
const member = `package/${release.manifest[format].replace(/^\.\//, '')}`
const bytes = readMember(archive, member)
assert.equal(hash(bytes), release.files[member])
implementations.push({ name: `published-${format}`, sdk: 4, factory: await factoryFromBytes(bytes, format === 'module') })
}
for (const filename of (process.env.ARTPLAYER_TEST_DASH || '').split(path.delimiter).filter(Boolean)) {
const bytes = fs.readFileSync(filename)
implementations.push({ name: `artifact-${path.basename(filename)}`, sdk: 5, factory: await factoryFromBytes(bytes, filename.endsWith('.mjs')) })
}
return implementations
}
// A controlled SDK surface and registry. It does not decode media or simulate ABR.
export function dashHost(version = 5) {
const calls = []
const controls = new Map()
const settings = new Map()
const listeners = new Map()
const video = {}
const levels = [
{ id: 'low', qualityIndex: 0, height: 360, bitrateInKbit: 400 },
{ id: 'high', qualityIndex: 1, height: 720, bitrateInKbit: 1200 },
{ id: 'top', qualityIndex: 2, height: 1080, bitrateInKbit: 2400 },
]
for (const level of levels) {
if (version === 4)
delete level.id
else
delete level.qualityIndex
}
const tracks = [{ id: 'en', lang: 'en', index: 0 }, { id: 'fr', lang: 'fr', index: 1 }]
const state = {
video,
levels,
tracks,
currentRepresentation: levels[1],
currentQuality: 1,
currentTrack: tracks[0],
settings: { streaming: { abr: { autoSwitchBitrate: { video: true, audio: false }, minBitrate: { video: -1 } } } },
}
const art = {
template: { $video: video },
isDestroy: false,
constructor: { utils: { errorHandle(condition, message) {
if (!condition)
throw new Error(message)
} } },
notice: {},
on(name, callback) {
if (!listeners.has(name))
listeners.set(name, new Set())
listeners.get(name).add(callback)
return art
},
off(name, callback) {
listeners.get(name)?.delete(callback)
return art
},
emit(name, ...args) {
for (const callback of [...(listeners.get(name) || [])])
callback(...args)
return art
},
destroy() {
art.isDestroy = true
art.emit('destroy')
},
}
let notice = ''
Object.defineProperty(art.notice, 'show', {
get: () => notice,
set(value) {
notice = value
calls.push(['notice', value])
},
})
for (const [name, entries] of [['controls', controls], ['setting', settings]]) {
art[name] = {
update(option) {
entries.set(option.name, option)
calls.push([`${name}.update`, option.name])
},
remove(key) {
entries.delete(key)
calls.push([`${name}.remove`, key])
},
check(item) {
calls.push([`${name}.check`, item])
},
}
}
const dash = {
getVideoElement() {
assert.equal(this, dash)
return state.video
},
getSettings() {
assert.equal(this, dash)
return state.settings
},
updateSettings(update) {
assert.equal(this, dash)
calls.push(['updateSettings', JSON.parse(JSON.stringify(update))])
Object.assign(state.settings.streaming.abr.autoSwitchBitrate, update.streaming.abr.autoSwitchBitrate)
},
getTracksFor(type) {
assert.equal(this, dash)
assert.equal(type, 'audio')
return state.tracks
},
getCurrentTrackFor(type) {
assert.equal(this, dash)
assert.equal(type, 'audio')
return state.currentTrack
},
setCurrentTrack(track) {
assert.equal(this, dash)
state.currentTrack = track
calls.push(['setCurrentTrack', track])
},
destroy() {
calls.push(['dash.destroy'])
},
}
if (version === 4) {
Object.assign(dash, {
getBitrateInfoListFor(type) {
assert.equal(this, dash)
assert.equal(type, 'video')
return state.levels
},
getQualityFor(type) {
assert.equal(this, dash)
assert.equal(type, 'video')
return state.currentQuality
},
setQualityFor(type, index) {
assert.equal(this, dash)
state.currentQuality = index
calls.push(['setQualityFor', type, index])
},
})
}
else {
Object.assign(dash, {
getRepresentationsByType(type) {
assert.equal(this, dash)
assert.equal(type, 'video')
return state.levels
},
getCurrentRepresentationForType(type) {
assert.equal(this, dash)
assert.equal(type, 'video')
return state.currentRepresentation
},
setRepresentationForTypeById(type, id) {
assert.equal(this, dash)
state.currentRepresentation = state.levels?.find(level => level.id === id)
calls.push(['setRepresentationForTypeById', type, id])
},
})
}
art.dash = dash
return { art, dash, state, calls, controls, settings, listeners, video }
}
export const bothMenus = () => ({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })