@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.
Files changed (42) hide show
  1. package/dist/{chunk-6LDN3CIY.js → chunk-AAX27BCR.js} +189 -348
  2. package/dist/chunk-AAX27BCR.js.map +1 -0
  3. package/dist/{chunk-WX7PT5C7.cjs → chunk-ALW3D72O.cjs} +61 -2
  4. package/dist/chunk-ALW3D72O.cjs.map +1 -0
  5. package/dist/{chunk-KB2N44BY.js → chunk-FMWHOUFE.js} +61 -2
  6. package/dist/chunk-FMWHOUFE.js.map +1 -0
  7. package/dist/{chunk-C6ZMI4UB.cjs → chunk-L4T24AN4.cjs} +113 -272
  8. package/dist/chunk-L4T24AN4.cjs.map +1 -0
  9. package/dist/components/features/index.cjs +3 -5
  10. package/dist/components/features/index.cjs.map +1 -1
  11. package/dist/components/features/index.js +2 -4
  12. package/dist/components/features/video-player.d.ts +17 -20
  13. package/dist/components/features/video-player.d.ts.map +1 -1
  14. package/dist/components/features/youtube-embed.d.ts +18 -4
  15. package/dist/components/features/youtube-embed.d.ts.map +1 -1
  16. package/dist/components/index.cjs +3 -5
  17. package/dist/components/index.cjs.map +1 -1
  18. package/dist/components/index.js +2 -4
  19. package/dist/components/navigation/index.cjs +3 -3
  20. package/dist/components/navigation/index.js +2 -2
  21. package/dist/components/ui/index.cjs +3 -3
  22. package/dist/components/ui/index.js +2 -2
  23. package/dist/hooks/index.cjs +4 -2
  24. package/dist/hooks/index.cjs.map +1 -1
  25. package/dist/hooks/index.d.ts +1 -0
  26. package/dist/hooks/index.d.ts.map +1 -1
  27. package/dist/hooks/index.js +3 -1
  28. package/dist/hooks/use-near-viewport.d.ts +42 -0
  29. package/dist/hooks/use-near-viewport.d.ts.map +1 -0
  30. package/dist/index.cjs +3 -3
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +1 -1
  34. package/src/components/features/video-player.tsx +39 -176
  35. package/src/components/features/youtube-embed.tsx +107 -224
  36. package/src/hooks/index.ts +3 -0
  37. package/src/hooks/use-near-viewport.ts +118 -0
  38. package/dist/chunk-6LDN3CIY.js.map +0 -1
  39. package/dist/chunk-C6ZMI4UB.cjs.map +0 -1
  40. package/dist/chunk-KB2N44BY.js.map +0 -1
  41. package/dist/chunk-WX7PT5C7.cjs.map +0 -1
  42. 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
- })