feat(vue): [EX-02] verify installed consumers and preserve lifecycle contracts

This commit is contained in:
Harvey Zhao committed 2026-09-14 06:02:13 +08:00
1 parent 4df9689364
commit d62ab13a35
29 files changed
+2326 -82

No files matched your search

+9
View File
@@ -2,6 +2,15 @@
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
`yarn test:vue-consumer` checks the actual Vue example with packed core/Danmuku/
Document PiP in an outside-workspace install. It also installs its pinned Vue
compiler there, checks all declaration paths for isolation, and runs development
and production builds in three engines. `test/vue/` contains strict SFC/type
fixtures and a separate plain-JS consumer. Reports under
`refactor/.cache/vue-consumer-*` cover updates, remount, KeepAlive, captured errors,
native media and real Worker cleanup. `--before` substitutes the recorded old
wrapper and must preserve the same lifecycle behavior; it is not expected to fail.
`yarn test:react-consumer` installs packed core/Danmuku/Document PiP outside the
workspace and checks the actual React example's strict TSX, development and
production builds, and native lifecycle/playback in three engines. Its fixture
+20
View File
@@ -0,0 +1,20 @@
<script setup>
import { ref } from 'vue'
import Player from './src/Player.vue'
const shown = ref(true)
const option = { url: '/pattern.mp4', muted: true }
const playerStyle = { width: '640px', height: '360px' }
// Plain JavaScript keeps the existing event and prop syntax.
function getInstance(art) {
document.body.dataset.instance = String(art.id)
art.on('destroy', () => document.body.dataset.destroyed = 'yes')
}
</script>
<template>
<button id="toggle" @click="shown = !shown">
Toggle player
</button>
<Player v-if="shown" id="legacy-player" data-consumer="preserved" :option="option" :style="playerStyle" @get-instance="getInstance" />
</template>
+99
View File
@@ -0,0 +1,99 @@
<script setup lang="ts">
import type { Option } from 'artplayer'
import Artplayer from 'artplayer'
import { nextTick, onErrorCaptured, reactive, ref, shallowRef } from 'vue'
import Player from './src/Player.vue'
const shown = ref(true)
const sibling = ref(false)
const bare = ref(false)
const cached = ref(false)
const key = ref(0)
const player = shallowRef<InstanceType<typeof Player> | null>(null)
const option = shallowRef<Partial<Option>>(reactive({ url: '/pattern.mp4', muted: true }))
const records: { art: Artplayer, destroyed: number }[] = []
const errors: { message: string, same: boolean }[] = []
const failure = new Error('vue-consumer-listener-failure')
let callbackMode = 'normal'
function capture(art: Artplayer) {
const record = { art, destroyed: 0 }
records.push(record)
art.on('destroy', () => record.destroyed++)
if (callbackMode === 'throw')
throw failure
if (callbackMode === 'unmount')
shown.value = false
}
onErrorCaptured((error) => {
errors.push({ message: error.message, same: error === failure })
return false
})
const probe = {
async act(action: string) {
if (action === 'mutate')
option.value.url = '/pattern.mp4?mutation'
if (action === 'replace')
option.value = { url: '/pattern.mp4?replacement', muted: true }
if (action === 'key')
key.value++
if (action === 'siblings')
sibling.value = true
if (action === 'remove-sibling')
sibling.value = false
if (action === 'hide')
shown.value = false
if (action === 'show')
shown.value = true
if (action === 'bare') {
shown.value = false
bare.value = true
}
if (action === 'remove-bare')
bare.value = false
if (action === 'cache') {
shown.value = false
cached.value = true
}
if (action === 'deactivate')
cached.value = false
if (action === 'activate')
cached.value = true
if (action === 'throw' || action === 'unmount') {
callbackMode = action
key.value++
shown.value = true
}
if (action === 'normal')
callbackMode = 'normal'
if (action === 'cancel') {
shown.value = true
shown.value = false
}
await nextTick()
},
current: () => Artplayer.instances.find(art => art.template.$container.classList.contains('primary-player')) || Artplayer.instances[0],
snapshot: () => ({
active: Artplayer.instances.length,
created: records.length,
records: records.map(({ art, destroyed }) => ({ id: art.id, isDestroy: art.isDestroy, destroyed })),
errors,
exposesArt: Boolean(player.value && 'art' in player.value),
componentRef: Boolean(player.value?.$el),
players: document.querySelectorAll('.art-video-player').length,
videos: document.querySelectorAll('video').length,
}),
}
declare global {
interface Window { vueProbe: typeof probe, unmountVue: () => void }
}
window.vueProbe = probe
</script>
<template>
<Player v-if="shown" :key="key" ref="player" class="primary-player" :style="{ width: '640px', height: '360px' }" :option="option" @get-instance="capture" />
<Player v-if="sibling" :style="{ width: '320px', height: '180px' }" :option="option" @get-instance="capture" />
<Player v-if="bare" :style="{ width: '320px', height: '180px' }" :option="option" />
<KeepAlive>
<Player v-if="cached" :style="{ width: '320px', height: '180px' }" :option="option" @get-instance="capture" />
</KeepAlive>
</template>
+18
View File
@@ -0,0 +1,18 @@
import type Artplayer from 'artplayer'
import type { Option } from 'artplayer'
import type Player from './src/Player.vue'
type Props = InstanceType<typeof Player>['$props']
const option: Partial<Option> = { url: '/pattern.mp4' }
export const valid: Props = { option, onGetInstance: (art: Artplayer) => art.pause() }
export const optional: Props = { option: {} }
// Preserve the historical array-form emits type; do not silently narrow its listener shape.
export const oldListener: Props = { option, onGetInstance: (...args: unknown[]) => args.length }
// @ts-expect-error Option fields retain the original public types.
export const wrongOption: Props = { option: { url: 123 } }
// @ts-expect-error The wrapper requires an option object.
export const missingOption: Props = {}
declare const component: InstanceType<typeof Player>
export const publicProps: Props = component.$props
// @ts-expect-error Script setup does not expose its private player ref.
export const privateRef = component.art
+6
View File
@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import Lifecycle from './Lifecycle.vue'
const app = createApp(Lifecycle)
window.unmountVue = () => app.unmount()
app.mount('#root')
+4
View File
@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import LegacyConsumer from './LegacyConsumer.vue'
createApp(LegacyConsumer).mount('#root')