@flamingo-stack/openframe-frontend-core 0.0.177 → 0.0.178
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-6LDN3CIY.js → chunk-AAX27BCR.js} +189 -348
- package/dist/chunk-AAX27BCR.js.map +1 -0
- package/dist/{chunk-WX7PT5C7.cjs → chunk-ALW3D72O.cjs} +61 -2
- package/dist/chunk-ALW3D72O.cjs.map +1 -0
- package/dist/{chunk-KB2N44BY.js → chunk-FMWHOUFE.js} +61 -2
- package/dist/chunk-FMWHOUFE.js.map +1 -0
- package/dist/{chunk-C6ZMI4UB.cjs → chunk-L4T24AN4.cjs} +113 -272
- package/dist/chunk-L4T24AN4.cjs.map +1 -0
- package/dist/components/features/index.cjs +3 -5
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +2 -4
- package/dist/components/features/video-player.d.ts +17 -20
- package/dist/components/features/video-player.d.ts.map +1 -1
- package/dist/components/features/youtube-embed.d.ts +18 -4
- package/dist/components/features/youtube-embed.d.ts.map +1 -1
- package/dist/components/index.cjs +3 -5
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +2 -4
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/use-near-viewport.d.ts +42 -0
- package/dist/hooks/use-near-viewport.d.ts.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/package.json +1 -1
- package/src/components/features/video-player.tsx +39 -176
- package/src/components/features/youtube-embed.tsx +107 -224
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-near-viewport.ts +118 -0
- package/dist/chunk-6LDN3CIY.js.map +0 -1
- package/dist/chunk-C6ZMI4UB.cjs.map +0 -1
- package/dist/chunk-KB2N44BY.js.map +0 -1
- package/dist/chunk-WX7PT5C7.cjs.map +0 -1
- package/src/components/features/__tests__/video-player.test.tsx +0 -142
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render } from '@testing-library/react'
|
|
2
|
-
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Regression test for the case-study video performance fix:
|
|
6
|
-
* plan: /Users/michaelassraf/.claude/plans/we-have-terrible-serious-radiant-turing.md
|
|
7
|
-
*
|
|
8
|
-
* Bug: VideoPlayer mounted two competing <video> elements on every render when no
|
|
9
|
-
* `poster` prop was passed — the hidden first-frame extractor (`useVideoFirstFramePoster`)
|
|
10
|
-
* raced ReactPlayer's own metadata fetch on the same URL, doubling bandwidth and
|
|
11
|
-
* stalling click-to-play on big MP4s.
|
|
12
|
-
*
|
|
13
|
-
* Fix: opt-in `lazyMount` prop that:
|
|
14
|
-
* 1. Skips `useVideoFirstFramePoster` (no hidden video)
|
|
15
|
-
* 2. Sets `preload="none"` on ReactPlayer's <video> until user clicks play
|
|
16
|
-
* 3. Synchronously calls .play() in the click handler for iOS user-activation safety
|
|
17
|
-
*
|
|
18
|
-
* Test strategy:
|
|
19
|
-
* - To detect "did the hidden poster <video> get created?", we tag the hook's
|
|
20
|
-
* unique signature — it sets `crossOrigin="anonymous"` on the element. Spy on
|
|
21
|
-
* `document.createElement` and count VIDEO elements that subsequently have
|
|
22
|
-
* `crossOrigin` set. React's own DOM render does NOT set crossOrigin on the
|
|
23
|
-
* mocked <video>, so this isolates the bug surface cleanly.
|
|
24
|
-
* - Mock `react-player` so we don't depend on its internal FilePlayer dynamic
|
|
25
|
-
* import (broken under JSDOM / Vitest's module loader).
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
// Mock react-player as a forwardRef component that exposes the same
|
|
29
|
-
// `getInternalPlayer()` surface used by VideoPlayer (line ~697 lazyMount sync play).
|
|
30
|
-
// Without this, `playerRef.current?.getInternalPlayer()` returns undefined under
|
|
31
|
-
// the test, the `instanceof HTMLVideoElement` guard fails, and the iOS user-
|
|
32
|
-
// activation path is silently never exercised by the test.
|
|
33
|
-
vi.mock('react-player', async () => {
|
|
34
|
-
const React = await import('react')
|
|
35
|
-
const RP = React.forwardRef<
|
|
36
|
-
{ getInternalPlayer: () => HTMLVideoElement | null },
|
|
37
|
-
{ url: string; config?: { file?: { attributes?: Record<string, unknown> } } }
|
|
38
|
-
>(function MockReactPlayer({ url, config }, ref) {
|
|
39
|
-
const videoRef = React.useRef<HTMLVideoElement | null>(null)
|
|
40
|
-
React.useImperativeHandle(ref, () => ({
|
|
41
|
-
getInternalPlayer: () => videoRef.current,
|
|
42
|
-
}))
|
|
43
|
-
const preload = (config?.file?.attributes?.preload as string) ?? 'metadata'
|
|
44
|
-
return <video ref={videoRef} src={url} preload={preload as 'none' | 'metadata' | 'auto'} />
|
|
45
|
-
})
|
|
46
|
-
return { default: RP }
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
beforeAll(() => {
|
|
50
|
-
// JSDOM doesn't implement these — react-player's internals (and the poster hook) call them.
|
|
51
|
-
Object.defineProperty(HTMLMediaElement.prototype, 'canPlayType', {
|
|
52
|
-
value: () => 'maybe',
|
|
53
|
-
writable: true,
|
|
54
|
-
configurable: true,
|
|
55
|
-
})
|
|
56
|
-
Object.defineProperty(HTMLMediaElement.prototype, 'load', {
|
|
57
|
-
value: () => undefined,
|
|
58
|
-
writable: true,
|
|
59
|
-
configurable: true,
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
import { VideoPlayer } from '../video-player'
|
|
64
|
-
|
|
65
|
-
const SAMPLE_MP4 = 'https://example.com/test.mp4'
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Detects the poster hook's signature via the `data-poster-extractor` attribute
|
|
69
|
-
* the hook sets on its hidden <video>. This is more durable than checking
|
|
70
|
-
* `crossOrigin === 'anonymous'` (any future code that happens to set crossOrigin
|
|
71
|
-
* on a freshly-created video would inflate the count and silently flake-pass).
|
|
72
|
-
*/
|
|
73
|
-
function spyForHiddenPosterVideos(): { count: () => number; restore: () => void } {
|
|
74
|
-
const original = document.createElement.bind(document)
|
|
75
|
-
const tracked: HTMLVideoElement[] = []
|
|
76
|
-
document.createElement = ((tag: string, opts?: ElementCreationOptions) => {
|
|
77
|
-
const el = original(tag, opts)
|
|
78
|
-
if (tag.toLowerCase() === 'video') tracked.push(el as HTMLVideoElement)
|
|
79
|
-
return el
|
|
80
|
-
}) as typeof document.createElement
|
|
81
|
-
return {
|
|
82
|
-
count: () => tracked.filter(v => v.getAttribute('data-poster-extractor') === 'true').length,
|
|
83
|
-
restore: () => { document.createElement = original },
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
describe('VideoPlayer lazyMount', () => {
|
|
88
|
-
let videoSpy: ReturnType<typeof spyForHiddenPosterVideos> | null = null
|
|
89
|
-
|
|
90
|
-
afterEach(() => {
|
|
91
|
-
videoSpy?.restore()
|
|
92
|
-
videoSpy = null
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('does NOT instantiate a hidden poster <video> when lazyMount=true (the actual fix)', () => {
|
|
96
|
-
videoSpy = spyForHiddenPosterVideos()
|
|
97
|
-
render(<VideoPlayer url={SAMPLE_MP4} lazyMount />)
|
|
98
|
-
expect(videoSpy.count()).toBe(0)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('renders ReactPlayer with preload="none" pre-click when lazyMount=true', () => {
|
|
102
|
-
const { container } = render(<VideoPlayer url={SAMPLE_MP4} lazyMount />)
|
|
103
|
-
const videos = container.querySelectorAll('video')
|
|
104
|
-
expect(videos.length).toBe(1)
|
|
105
|
-
expect(videos[0].getAttribute('preload')).toBe('none')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('preserves existing preload="metadata" + poster-hook behavior when lazyMount=false (default)', () => {
|
|
109
|
-
videoSpy = spyForHiddenPosterVideos()
|
|
110
|
-
const { container } = render(<VideoPlayer url={SAMPLE_MP4} />)
|
|
111
|
-
expect(container.querySelectorAll('video')[0].getAttribute('preload')).toBe('metadata')
|
|
112
|
-
// The poster hook DOES create one hidden <video> when no poster is provided.
|
|
113
|
-
// This is the legacy behavior we explicitly opted out of via lazyMount.
|
|
114
|
-
expect(videoSpy.count()).toBeGreaterThanOrEqual(1)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('skips the poster hook when an explicit poster is provided (existing gate, regression check)', () => {
|
|
118
|
-
videoSpy = spyForHiddenPosterVideos()
|
|
119
|
-
render(<VideoPlayer url={SAMPLE_MP4} poster="https://example.com/poster.jpg" />)
|
|
120
|
-
expect(videoSpy.count()).toBe(0)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('calls play() synchronously inside the click handler when lazyMount=true (iOS user-activation)', () => {
|
|
124
|
-
const playSpy = vi.fn().mockResolvedValue(undefined)
|
|
125
|
-
Object.defineProperty(HTMLMediaElement.prototype, 'play', {
|
|
126
|
-
value: playSpy,
|
|
127
|
-
writable: true,
|
|
128
|
-
configurable: true,
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const { container } = render(<VideoPlayer url={SAMPLE_MP4} lazyMount />)
|
|
132
|
-
// The play overlay div has `onClick={handlePlayClick}` and uses the
|
|
133
|
-
// 'group cursor-pointer' classes. Find by role/aria isn't reliable since
|
|
134
|
-
// the overlay is a styled div, so query by the unique class combo.
|
|
135
|
-
const overlay = container.querySelector('div.cursor-pointer.group') as HTMLElement | null
|
|
136
|
-
expect(overlay).toBeTruthy()
|
|
137
|
-
fireEvent.click(overlay!)
|
|
138
|
-
// play() must be called synchronously inside the click event task — if it
|
|
139
|
-
// were deferred (e.g., to a useEffect), iOS Safari would block playback.
|
|
140
|
-
expect(playSpy).toHaveBeenCalled()
|
|
141
|
-
})
|
|
142
|
-
})
|