mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 05:41:20 -08:00
refactor(danmuku): [PKG-DANMUKU-05] own renderer settings and heatmap resources
This commit is contained in:
1 parent
12332df221
commit
053d211fb0
38 files changed
+65630
-2420
No files matched your search
@@ -0,0 +1,291 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from '../helpers/load.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
let candidate
|
||||
test.beforeAll(async () => {
|
||||
candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
|
||||
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
|
||||
: await compilePackage('artplayer-plugin-danmuku', 'umd')
|
||||
})
|
||||
|
||||
async function setup(page, core, scenario, testInfo) {
|
||||
const origin = new URL(testInfo.project.use.baseURL).origin
|
||||
await page.route('**/*', (route) => {
|
||||
return new URL(route.request().url()).origin === origin ? route.continue() : route.abort()
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: candidate })
|
||||
await page.evaluate(({ scenario }) => {
|
||||
window.resourcePlayers = []
|
||||
window.resourcePlugins = []
|
||||
window.resourceOwners = []
|
||||
window.resourceMessages = []
|
||||
window.resourceSends = []
|
||||
window.resourceErrors = []
|
||||
window.resourceWorkers = []
|
||||
window.resourceTimers = new Set()
|
||||
const nativeSetTimeout = window.setTimeout.bind(window)
|
||||
const nativeClearTimeout = window.clearTimeout.bind(window)
|
||||
window.setTimeout = (callback, delay, ...args) => {
|
||||
const id = nativeSetTimeout(() => {
|
||||
window.resourceTimers.delete(id)
|
||||
callback(...args)
|
||||
}, delay)
|
||||
if (window.captureNextResourceTimer && delay === 1000) {
|
||||
window.resourceTimers.add(id)
|
||||
window.captureNextResourceTimer = false
|
||||
}
|
||||
return id
|
||||
}
|
||||
window.clearTimeout = (id) => {
|
||||
window.resourceTimers.delete(id)
|
||||
nativeClearTimeout(id)
|
||||
}
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.record = { terminated: 0 }
|
||||
window.resourceWorkers.push(this.record)
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.record.terminated++
|
||||
super.terminate()
|
||||
}
|
||||
}
|
||||
const originalError = console.error
|
||||
console.error = (...args) => {
|
||||
window.resourceErrors.push(args.map(arg => arg?.message || String(arg)))
|
||||
originalError(...args)
|
||||
}
|
||||
window.sendGate = new Promise((resolve, reject) => {
|
||||
window.resolveSend = resolve
|
||||
window.rejectSend = reject
|
||||
})
|
||||
window.createResourcePlayer = (index, extra = {}) => {
|
||||
const container = index === 0 ? document.querySelector('.player') : document.body.appendChild(document.createElement('div'))
|
||||
container.style.cssText = 'width:640px;height:360px'
|
||||
const mount = document.body.appendChild(document.createElement('div'))
|
||||
mount.id = `resource-mount-${index}`
|
||||
const art = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true, fullscreenWeb: true, setting: true })
|
||||
window.resourcePlayers[index] = art
|
||||
art.on('artplayerPluginDanmuku:visible', item => window.resourceMessages.push(item.text))
|
||||
const option = {
|
||||
danmuku: [],
|
||||
mount,
|
||||
heatmap: scenario === 'heatmap' ? { sampling: 0 } : false,
|
||||
lockTime: 10,
|
||||
margin: [10, 10],
|
||||
fontSize: 20,
|
||||
speed: 2,
|
||||
beforeEmit(danmu) {
|
||||
window.resourceSends.push({ index, text: danmu.text, receiver: this === window.resourcePlugins[index].option })
|
||||
// Capture the lock scheduled in this send's await continuation, not
|
||||
// unrelated browser/core one-second timers while typing or clicking.
|
||||
if (window.observeResourceTimers)
|
||||
window.captureNextResourceTimer = true
|
||||
return scenario === 'pending' || scenario === 'reject' ? window.sendGate : true
|
||||
},
|
||||
...extra,
|
||||
}
|
||||
art.plugins.add(window.artplayerPluginDanmuku(option))
|
||||
const plugin = art.plugins.artplayerPluginDanmuku
|
||||
window.resourcePlugins[index] = plugin
|
||||
window.resourceOwners[index] = plugin.show()
|
||||
return art
|
||||
}
|
||||
window.art = window.createResourcePlayer(0)
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
}, { scenario })
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await testInfo.attach('danmuku-resources-inputs', { contentType: 'application/json', body: JSON.stringify({ core, scenario, artifact: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', sha256: hash(candidate), native: 'video, RAF, DOM, Worker; observe the first one-second timer after beforeEmit in the synchronous send continuation, no fake timer clock' }) })
|
||||
}
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await testInfo.attach('danmuku-resources-page', { contentType: 'image/png', body: await page.screenshot() })
|
||||
const evidence = await page.evaluate(() => {
|
||||
for (const art of window.resourcePlayers || []) {
|
||||
if (!art.isDestroy)
|
||||
art.destroy(false)
|
||||
}
|
||||
return { workers: window.resourceWorkers, sends: window.resourceSends, messages: window.resourceMessages, errors: window.resourceErrors, retainedSettings: document.querySelectorAll('[id^="resource-mount-"] .artplayer-plugin-danmuku').length, timers: window.resourceTimers?.size }
|
||||
})
|
||||
await testInfo.attach('danmuku-resources-evidence', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
expect(evidence.retainedSettings).toBe(0)
|
||||
for (const worker of evidence.workers || []) expect(worker.terminated).toBe(1)
|
||||
})
|
||||
|
||||
async function renderResourceRow(page, text) {
|
||||
await page.evaluate(async (text) => {
|
||||
const owner = await window.resourcePlugins[0].emit({ text, time: 0.2, mode: 1 })
|
||||
// Resource tests start with an eligible row, then require a real Worker and
|
||||
// rendered DOM. Native timestamp delivery is covered by the scheduler spec.
|
||||
owner.setState(owner.queue[owner.queue.length - 1], 'ready')
|
||||
}, text)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.resourceMessages)).toContain(text)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: shared setting mount preserves the surviving instance and later user changes`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'normal', testInfo)
|
||||
const result = await page.evaluate(() => {
|
||||
const mount = document.querySelector('#resource-mount-0')
|
||||
window.createResourcePlayer(1, { mount, theme: 'light' })
|
||||
window.art.destroy(false)
|
||||
const survivor = { panels: mount.querySelectorAll('.artplayer-plugin-danmuku').length, theme: mount.dataset.danmukuTheme }
|
||||
mount.dataset.danmukuTheme = 'user-value'
|
||||
window.resourcePlayers[1].destroy(false)
|
||||
return { survivor, theme: mount.dataset.danmukuTheme, ownedKeys: Object.keys(mount.dataset).filter(key => key.startsWith('danmuku') && key !== 'danmukuTheme'), panels: mount.querySelectorAll('.artplayer-plugin-danmuku').length }
|
||||
})
|
||||
expect(result).toEqual({ survivor: { panels: 1, theme: 'light' }, theme: 'user-value', ownedKeys: [], panels: 0 })
|
||||
})
|
||||
|
||||
test(`${core}: invalid setting mount rolls back plugin resources while preserving the original error`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'normal', testInfo)
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.body.appendChild(document.createElement('div'))
|
||||
container.style.cssText = 'width:640px;height:360px'
|
||||
const art = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true })
|
||||
window.resourcePlayers.push(art)
|
||||
art.template.$controlsCenter.style.setProperty('display', 'grid', 'important')
|
||||
const display = art.template.$controlsCenter.style.display
|
||||
let error
|
||||
try {
|
||||
art.plugins.add(window.artplayerPluginDanmuku({ danmuku: [], mount: '#missing-setting-mount' }))
|
||||
}
|
||||
catch (failure) {
|
||||
error = failure.message
|
||||
}
|
||||
return { error, panels: container.querySelectorAll('.artplayer-plugin-danmuku').length, worker: window.resourceWorkers.at(-1), display: art.template.$controlsCenter.style.display, priority: art.template.$controlsCenter.style.getPropertyPriority('display'), previousDisplay: display }
|
||||
})
|
||||
expect(result.error).toContain('Can not find the mount point: #missing-setting-mount')
|
||||
expect(result.panels).toBe(0)
|
||||
expect(result.worker.terminated).toBe(1)
|
||||
expect(result.display).toBe(result.previousDisplay)
|
||||
expect(result.priority).toBe('important')
|
||||
})
|
||||
|
||||
test(`${core}: destroy cancels pending setting send without clearing retained input or starting a lock`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'pending', testInfo)
|
||||
const mount = page.locator('#resource-mount-0')
|
||||
await mount.locator('.apd-input').fill('PENDING')
|
||||
await mount.locator('.apd-send').click()
|
||||
expect(await page.evaluate(() => window.resourceSends)).toEqual([{ index: 0, text: 'PENDING', receiver: true }])
|
||||
const result = await page.evaluate(async () => {
|
||||
const input = document.querySelector('#resource-mount-0 .apd-input')
|
||||
const send = document.querySelector('#resource-mount-0 .apd-send')
|
||||
window.art.destroy(false)
|
||||
window.resolveSend(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 30))
|
||||
return { value: input.value, locked: send.classList.contains('apd-lock'), queue: window.resourceOwners[0].queue.map(item => item.text), connected: input.isConnected }
|
||||
})
|
||||
expect(result).toEqual({ value: 'PENDING', locked: false, queue: [], connected: false })
|
||||
expect(await page.evaluate(() => window.resourceErrors)).toEqual([])
|
||||
})
|
||||
|
||||
test(`${core}: destroy clears active send timer and leaves another mounted instance usable`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'normal', testInfo)
|
||||
await page.evaluate(() => window.observeResourceTimers = true)
|
||||
await page.locator('#resource-mount-0 .apd-input').fill('LOCK')
|
||||
await page.locator('#resource-mount-0 .apd-send').click()
|
||||
await expect(page.locator('#resource-mount-0 .apd-send')).toHaveClass(/apd-lock/u)
|
||||
expect(await page.evaluate(() => window.resourceTimers.size)).toBe(1)
|
||||
await page.evaluate(() => {
|
||||
window.observeResourceTimers = false
|
||||
window.createResourcePlayer(1)
|
||||
window.art.destroy(false)
|
||||
})
|
||||
expect(await page.evaluate(() => window.resourceTimers.size)).toBe(0)
|
||||
await expect(page.locator('#resource-mount-0 .artplayer-plugin-danmuku')).toHaveCount(0)
|
||||
await page.locator('#resource-mount-1 .apd-input').fill('SECOND')
|
||||
await page.locator('#resource-mount-1 .apd-send').click()
|
||||
await expect.poll(() => page.evaluate(() => window.resourceOwners[1].queue.map(row => row.text))).toEqual(['SECOND'])
|
||||
expect(await page.locator('style#artplayer-plugin-danmuku').count()).toBe(1)
|
||||
})
|
||||
|
||||
test(`${core}: rejected setting callback retains input and original console error without an unhandled rejection`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'reject', testInfo)
|
||||
await page.locator('#resource-mount-0 .apd-input').fill('REJECT')
|
||||
await page.locator('#resource-mount-0 .apd-send').click()
|
||||
await page.evaluate(() => window.rejectSend(new Error('controlled setting rejection')))
|
||||
await expect.poll(() => page.evaluate(() => window.resourceErrors)).toEqual([['Error emitting danmuku:', 'controlled setting rejection']])
|
||||
await expect(page.locator('#resource-mount-0 .apd-input')).toHaveValue('REJECT')
|
||||
await expect(page.locator('#resource-mount-0 .apd-send')).not.toHaveClass(/apd-lock/u)
|
||||
expect(await page.evaluate(() => window.resourceOwners[0].queue.length)).toBe(0)
|
||||
})
|
||||
|
||||
test(`${core}: destroy false releases actual renderer nodes while retaining the player shell`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'normal', testInfo)
|
||||
await renderResourceRow(page, 'VISIBLE')
|
||||
const result = await page.evaluate(() => {
|
||||
const layer = window.art.template.$danmuku
|
||||
const node = [...layer.children].find(item => item.textContent === 'VISIBLE')
|
||||
const owner = window.resourceOwners[0]
|
||||
window.art.destroy(false)
|
||||
return { connected: node.isConnected, nodes: layer.children.length, refs: owner.$refs.length, dangling: owner.queue.filter(item => item.$ref).length, shell: !!window.art.template.$player }
|
||||
})
|
||||
expect(result).toEqual({ connected: false, nodes: 0, refs: 0, dangling: 0, shell: true })
|
||||
})
|
||||
|
||||
test(`${core}: replacement load removes a moved renderer node and preserves unrelated body content`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'normal', testInfo)
|
||||
await renderResourceRow(page, 'MOVED')
|
||||
const result = await page.evaluate(async () => {
|
||||
window.art.pause()
|
||||
const layer = window.art.template.$danmuku
|
||||
const node = [...layer.children].find(item => item.textContent === 'MOVED')
|
||||
const foreign = document.createElement('aside')
|
||||
document.body.append(node, foreign)
|
||||
await window.resourcePlugins[0].load()
|
||||
const replacement = { connected: node.isConnected, nodes: layer.childElementCount, foreign: foreign.isConnected, refs: window.resourceOwners[0].$refs.length }
|
||||
window.art.destroy(false)
|
||||
return { replacement, foreignAfterDestroy: foreign.isConnected }
|
||||
})
|
||||
expect(result).toEqual({ replacement: { connected: false, nodes: 0, foreign: true, refs: 0 }, foreignAfterDestroy: true })
|
||||
})
|
||||
|
||||
test(`${core}: narrow heatmap invalid sampling stays responsive and two instances have independent gradients`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, 'heatmap', testInfo)
|
||||
// A second evaluation has fresh module counters but shares this document.
|
||||
await page.addScriptTag({ content: candidate })
|
||||
await page.evaluate(() => {
|
||||
window.createResourcePlayer(1)
|
||||
const first = window.art.controls.heatmap
|
||||
first.style.width = '99px'
|
||||
window.art.emit('resize')
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.resourcePlayers[1].isReady)).toBe(true)
|
||||
await expect.poll(() => page.locator('.art-control-heatmap svg').count()).toBe(2)
|
||||
const result = await page.evaluate(() => {
|
||||
const first = window.resourcePlayers[0].controls.heatmap
|
||||
const second = window.resourcePlayers[1].controls.heatmap
|
||||
const gradient = node => node.querySelector('linearGradient').id
|
||||
window.resourcePlayers[0].emit('artplayerPluginDanmuku:points', [[0, 0], [49, 40], [99, 10]])
|
||||
window.resourcePlayers[1].emit('artplayerPluginDanmuku:points', [[0, 20], [300, 0], [640, 35]])
|
||||
window.resourcePlayers[0].emit('setBar', 'played', 0.25)
|
||||
window.resourcePlayers[1].emit('setBar', 'played', 0.75)
|
||||
return {
|
||||
ids: [gradient(first), gradient(second)],
|
||||
fills: [first.querySelector('path').getAttribute('fill'), second.querySelector('path').getAttribute('fill')],
|
||||
offsets: [first.querySelector('#heatmap-start').getAttribute('offset'), second.querySelector('#heatmap-start').getAttribute('offset')],
|
||||
validPaths: [first, second].every(node => !/NaN|Infinity/.test(node.querySelector('path').getAttribute('d'))),
|
||||
}
|
||||
})
|
||||
expect(new Set(result.ids).size).toBe(2)
|
||||
expect(result.ids[0]).toBe('heatmap-solids')
|
||||
expect(result.fills).toEqual(result.ids.map(id => `url(#${id})`))
|
||||
expect(result.offsets).toEqual(['25%', '75%'])
|
||||
expect(result.validPaths).toBe(true)
|
||||
await testInfo.attach('danmuku-resources-heatmap', { contentType: 'application/json', body: JSON.stringify(result) })
|
||||
await testInfo.attach('danmuku-heatmap-live-page', { contentType: 'image/png', body: await page.screenshot({ fullPage: true }) })
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await expect(page.locator('.art-control-heatmap')).toHaveCount(1)
|
||||
await page.evaluate(() => window.resourcePlayers[1].emit('resize'))
|
||||
await expect(page.locator('.art-control-heatmap svg')).toHaveCount(1)
|
||||
})
|
||||
}
|
||||
@@ -133,6 +133,15 @@ async function geometry(page) {
|
||||
|
||||
const visibleIds = page => page.evaluate(() => [...new Set(window.danmukuEvidence.events.filter(item => item.event === 'visible' && item.id).map(item => item.id))])
|
||||
|
||||
async function queueReady(page, item) {
|
||||
await page.evaluate(async (item) => {
|
||||
const owner = await window.danmukuPlugins[0].emit(item)
|
||||
// These cases verify cancellation/recovery after eligibility. Seed that
|
||||
// precondition; the separate timing/geometry cases still use the media clock.
|
||||
owner.setState(owner.queue[owner.queue.length - 1], 'ready')
|
||||
}, item)
|
||||
}
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await testInfo.attach('danmuku-layout', { contentType: 'application/json', body: JSON.stringify(await geometry(page)) })
|
||||
await testInfo.attach('danmuku-native-page', { contentType: 'image/png', body: await page.screenshot() })
|
||||
@@ -158,6 +167,7 @@ for (const core of ['published', 'candidate']) {
|
||||
|
||||
test(`${label}: accepted NaN timestamp keeps the historical current-time fallback`, async ({ page }, testInfo) => {
|
||||
const external = await setup(page, core, format, 'modes', testInfo)
|
||||
await page.evaluate(() => window.art.playbackRate = 0.25)
|
||||
const numeric = await page.evaluate(async () => {
|
||||
const input = { id: 'nan-time', text: 'NAN FALLBACK', mode: 1, time: Number.NaN }
|
||||
const expected = window.art.currentTime + 0.5
|
||||
@@ -176,7 +186,7 @@ for (const core of ['published', 'candidate']) {
|
||||
for (const action of ['destroy', 'stop']) {
|
||||
test(`${label}: failed recovery respects synchronous ${action} from the error listener`, async ({ page }, testInfo) => {
|
||||
const external = await setup(page, core, format, 'modes', testInfo)
|
||||
await page.evaluate(() => window.danmukuPlugins[0].emit({ id: 'recovery-row', text: 'RECOVERY', mode: 1, time: 0.5 }))
|
||||
await queueReady(page, { id: 'recovery-row', text: 'RECOVERY', mode: 1, time: 0.5 })
|
||||
await page.click('#play')
|
||||
await expect.poll(() => visibleIds(page)).toContain('recovery-row')
|
||||
const result = await page.evaluate((action) => {
|
||||
@@ -216,7 +226,10 @@ for (const core of ['published', 'candidate']) {
|
||||
for (const action of ['pause', 'reset', 'seek', 'destroy']) {
|
||||
test(`${label}: native ${action} cancels pending visibility without blocking later work`, async ({ page }, testInfo) => {
|
||||
const external = await setup(page, core, format, 'modes', testInfo)
|
||||
await page.evaluate(() => window.danmukuPlugins[0].emit({ id: 'obsolete-gate', text: 'GATED', mode: 1, time: 0.5 }))
|
||||
await page.evaluate(async () => {
|
||||
window.art.playbackRate = 0.25
|
||||
await window.danmukuPlugins[0].emit({ id: 'obsolete-gate', text: 'GATED', mode: 1, time: 0.5 })
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.danmukuEvidence.gates.some(item => item.id === 'obsolete-gate'))).toBe(true)
|
||||
if (action === 'pause') {
|
||||
@@ -237,7 +250,7 @@ for (const core of ['published', 'candidate']) {
|
||||
await page.evaluate(() => window.danmukuPlugins[0].load())
|
||||
}
|
||||
if (action !== 'destroy') {
|
||||
await page.evaluate(() => window.danmukuPlugins[0].emit({ id: 'fresh-after-cancel', text: 'FRESH', mode: 1, time: window.art.currentTime + 0.5 }))
|
||||
await queueReady(page, { id: 'fresh-after-cancel', text: 'FRESH', mode: 1 })
|
||||
if (action === 'pause')
|
||||
await page.click('#play')
|
||||
// The old user Promise is still unresolved. A cancelled frame must
|
||||
@@ -325,7 +338,21 @@ for (const core of ['published', 'candidate']) {
|
||||
test(`${label}: native pause seek rate and public reset/load retain historical pool semantics`, async ({ page }, testInfo) => {
|
||||
const external = await setup(page, core, format, 'playback', testInfo)
|
||||
await page.evaluate(async () => {
|
||||
await window.danmukuPlugins[0].emit({ id: 'moving', text: 'MOVING', mode: 0, time: 0.5 })
|
||||
const owner = await window.danmukuPlugins[0].emit({ id: 'moving', text: 'MOVING', mode: 0, time: 0.5 })
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), 'readys')
|
||||
window.danmukuEvidence.eligibility = []
|
||||
Object.defineProperty(owner, 'readys', {
|
||||
configurable: true,
|
||||
get() {
|
||||
const before = owner.art.currentTime
|
||||
const rows = descriptor.get.call(owner)
|
||||
const samples = window.danmukuEvidence.eligibility
|
||||
if (samples.length < 2000) {
|
||||
samples.push({ wall: performance.now(), before, after: owner.art.currentTime, playing: owner.art.playing, paused: owner.art.template.$video.paused, seeking: owner.art.template.$video.seeking, rate: owner.art.playbackRate, stop: owner.isStop, generation: owner.scheduler.generation, fault: owner.scheduler.fault, ready: rows.map(row => row.id), queue: owner.queue.map(row => ({ id: row.id, time: row.time, state: row.$state })) })
|
||||
}
|
||||
return rows
|
||||
},
|
||||
})
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => visibleIds(page)).toContain('moving')
|
||||
@@ -431,8 +458,8 @@ for (const core of ['published', 'candidate']) {
|
||||
await expect.poll(() => page.evaluate(() => window.danmukuPlayers[1].isReady)).toBe(true)
|
||||
await page.evaluate(() => window.danmukuPlayers[0].destroy())
|
||||
expect(await page.evaluate(() => window.danmukuEvidence.workers.map(worker => worker.terminated))).toEqual([1, 0])
|
||||
// The released plugin does not dispose external Setting DOM; retain this observation.
|
||||
await expect(mount.locator('.artplayer-plugin-danmuku')).toHaveCount(1)
|
||||
// The candidate owns external Setting DOM and removes it on destruction.
|
||||
await expect(mount.locator('.artplayer-plugin-danmuku')).toHaveCount(0)
|
||||
await page.locator('#danmuku-mount-1 .apd-input').fill('SECOND')
|
||||
await page.locator('#danmuku-mount-1 .apd-send').click()
|
||||
await page.click('#play-second-danmuku')
|
||||
|
||||
Reference in new issue
Block a user