diff --git a/src/components/YouTubePlayer.test.tsx b/src/components/YouTubePlayer.test.tsx
index 68189e7..1c6faee 100644
--- a/src/components/YouTubePlayer.test.tsx
+++ b/src/components/YouTubePlayer.test.tsx
@@ -172,4 +172,65 @@ describe('YouTubePlayer', () => {
// Check destroy was called
expect(mockPlayerInstance.destroy).toHaveBeenCalled()
})
+
+ it('shows error when YouTube API script fails to load', async () => {
+ // Remove YT so the component tries to load the script
+ ;(window as unknown as { YT: unknown }).YT = undefined
+
+ render()
+
+ // The component should have created a script tag - fire error on it
+ await waitFor(() => {
+ const script = document.getElementById('youtube-iframe-api')
+ expect(script).not.toBeNull()
+ })
+
+ const script = document.getElementById('youtube-iframe-api')!
+ act(() => {
+ script.dispatchEvent(new Event('error'))
+ })
+
+ await waitFor(() => {
+ expect(screen.getByText('Failed to load YouTube player')).toBeInTheDocument()
+ })
+
+ // Clean up the script tag for other tests
+ script.remove()
+ })
+
+ it('allows retry after script load failure', async () => {
+ // First: fail the load
+ ;(window as unknown as { YT: unknown }).YT = undefined
+
+ const { unmount } = render()
+
+ await waitFor(() => {
+ const script = document.getElementById('youtube-iframe-api')
+ expect(script).not.toBeNull()
+ })
+
+ const script = document.getElementById('youtube-iframe-api')!
+ act(() => {
+ script.dispatchEvent(new Event('error'))
+ })
+
+ await waitFor(() => {
+ expect(screen.getByText('Failed to load YouTube player')).toBeInTheDocument()
+ })
+
+ unmount()
+ script.remove()
+
+ // Second: retry with YT available should succeed
+ ;(window as unknown as { YT: unknown }).YT = {
+ Player: MockYTPlayer,
+ PlayerState: { UNSTARTED: -1, ENDED: 0, PLAYING: 1, PAUSED: 2, BUFFERING: 3, CUED: 5 },
+ }
+
+ render()
+
+ await waitFor(() => {
+ expect(MockYTPlayer).toHaveBeenCalled()
+ })
+ })
})
diff --git a/src/components/YouTubePlayer.tsx b/src/components/YouTubePlayer.tsx
index b389be9..a3aa97b 100644
--- a/src/components/YouTubePlayer.tsx
+++ b/src/components/YouTubePlayer.tsx
@@ -83,6 +83,8 @@ interface YouTubePlayerProps {
let apiLoadPromise: Promise | null = null
+const API_LOAD_TIMEOUT_MS = 10000
+
function loadYouTubeAPI(): Promise {
if (apiLoadPromise) return apiLoadPromise
@@ -90,34 +92,53 @@ function loadYouTubeAPI(): Promise {
return Promise.resolve()
}
- apiLoadPromise = new Promise((resolve) => {
+ apiLoadPromise = new Promise((resolve, reject) => {
if (typeof window === 'undefined') {
resolve()
return
}
+ const timeout = setTimeout(() => {
+ apiLoadPromise = null
+ reject(new Error('YouTube API load timed out'))
+ }, API_LOAD_TIMEOUT_MS)
+
+ const onLoad = () => {
+ clearTimeout(timeout)
+ resolve()
+ }
+
+ const onError = () => {
+ clearTimeout(timeout)
+ apiLoadPromise = null
+ reject(new Error('Failed to load YouTube API script'))
+ }
+
const existingScript = document.getElementById('youtube-iframe-api')
if (existingScript) {
if (window.YT && window.YT.Player) {
+ clearTimeout(timeout)
resolve()
} else {
const originalCallback = window.onYouTubeIframeAPIReady
window.onYouTubeIframeAPIReady = () => {
originalCallback?.()
- resolve()
+ onLoad()
}
+ existingScript.addEventListener('error', onError)
}
return
}
window.onYouTubeIframeAPIReady = () => {
- resolve()
+ onLoad()
}
const script = document.createElement('script')
script.id = 'youtube-iframe-api'
script.src = 'https://www.youtube.com/iframe_api'
script.async = true
+ script.addEventListener('error', onError)
document.body.appendChild(script)
})