fix(react): [EX-01] verify installed consumers and clean up callback failures

This commit is contained in:
Harvey Zhao committed 2026-09-14 05:17:51 +08:00
1 parent 3e39fc745f
commit e0d785662d
25 files changed
+3539 -98

No files matched your search

+8
View File
@@ -2,6 +2,14 @@
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
`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
is in `test/react/`; unique reports and frozen consumer locks are retained under
`refactor/.cache/react-consumer-*`. The `--before` control intentionally fails at
the recorded old wrapper's callback-exception leak. See the example README for
the compatibility contract and the capabilities covered by other package tasks.
`yarn test:auto-thumbnail-types` checks the preserved npm 1.1.0 declaration,
accurate `/runtime` Promise types, exact rejected consumer lines, and generated
editor globals. It also runs in `test:auto-thumbnail` and `test:baseline`.
+21
View File
@@ -0,0 +1,21 @@
import type Artplayer from 'artplayer'
import type { Option } from 'artplayer'
import type { ComponentProps } from 'react'
import Player from './src/Player'
const option: Partial<Option> = { url: '/pattern.mp4' }
const props: ComponentProps<typeof Player> = {
option,
getInstance(art: Artplayer) { art.pause() },
style: { width: 640 },
className: 'consumer-container',
onClick(event) { event.currentTarget.dataset.clicked = 'yes' },
}
export const valid = <Player {...props} />
export const optional = <Player option={{}} />
// @ts-expect-error The historical callback receives a player, not a media element.
export const wrongCallback = <Player option={option} getInstance={(video: HTMLVideoElement) => video.pause()} />
// @ts-expect-error Native div attributes retain their original types.
export const wrongStyle = <Player option={option} style="width: 640px" />
// @ts-expect-error Player options still enforce field types.
export const wrongOption = <Player option={{ url: 123 }} />
+79
View File
@@ -0,0 +1,79 @@
import type { ReactNode } from 'react'
import Artplayer from 'artplayer'
import { Component, StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import Player from './src/Player'
const records: { art: Artplayer, destroyed: number }[] = []
const errors: { message: string, same: boolean }[] = []
const container = document.getElementById('root')
if (!container)
throw new Error('Missing React fixture container')
const root = createRoot(container)
const option = { url: '/pattern.mp4', muted: true }
const changedOption = { ...option, volume: 0.2 }
const failure = new Error('react-consumer-callback-failure')
function capture(art: Artplayer) {
const record = { art, destroyed: 0 }
records.push(record)
art.on('destroy', () => record.destroyed++)
}
function changedCapture(art: Artplayer) {
capture(art)
}
function fail(art: Artplayer) {
capture(art)
throw failure
}
function failWithCleanupError(art: Artplayer) {
const destroy = art.destroy.bind(art)
art.destroy = (...args) => {
destroy(...args)
throw new Error('secondary-cleanup-error')
}
fail(art)
}
class Boundary extends Component<{ children: ReactNode }, { failed: boolean }> {
state = { failed: false }
static getDerivedStateFromError() { return { failed: true } }
componentDidCatch(error: Error) { errors.push({ message: error.message, same: error === failure }) }
render() { return this.state.failed ? <div id="caught">caught</div> : this.props.children }
}
function render(mode = 'normal') {
root.render(
<StrictMode>
<Boundary key={mode.startsWith('throw') ? mode : 'healthy'}>
{mode !== 'empty' && (
<Player
key="primary"
id="player"
className="consumer-container"
data-consumer="preserved"
style={{ width: 640, height: 360 }}
option={mode === 'option' ? changedOption : option}
getInstance={mode === 'throw-cleanup' ? failWithCleanupError : mode === 'throw' ? fail : mode === 'callback' ? changedCapture : mode === 'optional' ? undefined : capture}
/>
)}
{mode === 'siblings' && <Player key="sibling" id="sibling" style={{ width: 320, height: 180 }} option={option} getInstance={capture} />}
</Boundary>
</StrictMode>,
)
}
const probe = {
render,
snapshot: () => ({
active: Artplayer.instances.length,
created: records.length,
records: records.map(({ art, destroyed }) => ({ id: art.id, isDestroy: art.isDestroy, destroyed })),
plugins: Artplayer.instances.map(art => Object.keys(art.plugins)),
errors,
videos: document.querySelectorAll('video').length,
players: document.querySelectorAll('.art-video-player').length,
}),
current: () => Artplayer.instances[0],
}
declare global {
interface Window { probe: typeof probe }
}
window.probe = probe
render()