From 3e2ea31131538d75e26584a8134b747267f514e8 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Thu, 13 Aug 2026 10:53:42 -0400 Subject: [PATCH] Fix video playback initialization and click-triggered controls --- .../ui/editor/canvas/CanvasMediaElement.tsx | 2 +- .../state/mediaPlaceholderReplacement.ts | 2 +- .../ui/editor/state/use-local-media-import.ts | 2 +- .../ui/editor/CanvasWorkspace.media.test.tsx | 24 ++++++++ .../editor/EditorShell.media-import.test.tsx | 2 +- tests/e2e/editor/video-media-controls.spec.ts | 4 +- tests/e2e/public-deck/media-playback.spec.ts | 60 ++++++++++++++++++- 7 files changed, 88 insertions(+), 8 deletions(-) diff --git a/apps/editor/src/ui/editor/canvas/CanvasMediaElement.tsx b/apps/editor/src/ui/editor/canvas/CanvasMediaElement.tsx index c510f6f8..c1c7fe02 100644 --- a/apps/editor/src/ui/editor/canvas/CanvasMediaElement.tsx +++ b/apps/editor/src/ui/editor/canvas/CanvasMediaElement.tsx @@ -170,7 +170,7 @@ function CanvasVideoElement({ playVideo(video); return; } - if (animationState.hidden || element.startOnClick || animationState.mediaActionPending) { + if (animationState.hidden || animationState.mediaActionPending) { stopReversePlayback(); video.pause(); video.currentTime = Math.max(0, element.trimStartSeconds); diff --git a/apps/editor/src/ui/editor/state/mediaPlaceholderReplacement.ts b/apps/editor/src/ui/editor/state/mediaPlaceholderReplacement.ts index dd94adfc..ac14119d 100644 --- a/apps/editor/src/ui/editor/state/mediaPlaceholderReplacement.ts +++ b/apps/editor/src/ui/editor/state/mediaPlaceholderReplacement.ts @@ -114,7 +114,7 @@ function createVideoElement(input: { controls: true, muted: true, autoplayInPreview: true, - playing: true, + playing: false, trimStartSeconds: 0, repeatMode: 'loop', ...(input.durationSeconds !== undefined diff --git a/apps/editor/src/ui/editor/state/use-local-media-import.ts b/apps/editor/src/ui/editor/state/use-local-media-import.ts index 6f62ef12..9ad7de54 100644 --- a/apps/editor/src/ui/editor/state/use-local-media-import.ts +++ b/apps/editor/src/ui/editor/state/use-local-media-import.ts @@ -215,7 +215,7 @@ export function useLocalMediaImport({ controls: true, muted: true, autoplayInPreview: true, - playing: true, + playing: false, trimStartSeconds: 0, ...(videoDurationSeconds !== undefined ? { diff --git a/apps/editor/tests/unit/ui/editor/CanvasWorkspace.media.test.tsx b/apps/editor/tests/unit/ui/editor/CanvasWorkspace.media.test.tsx index 5653b60d..65ccfdc8 100644 --- a/apps/editor/tests/unit/ui/editor/CanvasWorkspace.media.test.tsx +++ b/apps/editor/tests/unit/ui/editor/CanvasWorkspace.media.test.tsx @@ -167,6 +167,30 @@ describe('CanvasWorkspace media elements', () => { await waitFor(() => expect(playSpy).toHaveBeenCalled()); expect(video.style.opacity).toBe('1'); + const pauseCallsAfterPlayback = pauseSpy.mock.calls.length; + + rerender( + , + ); + + expect(pauseSpy).toHaveBeenCalledTimes(pauseCallsAfterPlayback); playSpy.mockRestore(); pauseSpy.mockRestore(); }); diff --git a/apps/editor/tests/unit/ui/editor/EditorShell.media-import.test.tsx b/apps/editor/tests/unit/ui/editor/EditorShell.media-import.test.tsx index 08666cb9..a6178930 100644 --- a/apps/editor/tests/unit/ui/editor/EditorShell.media-import.test.tsx +++ b/apps/editor/tests/unit/ui/editor/EditorShell.media-import.test.tsx @@ -78,7 +78,7 @@ describe('EditorShell media import workflows', () => { ); expect(importedVideo).toMatchObject({ autoplayInPreview: true, - playing: true, + playing: false, type: 'video', }); expect(savedProject?.assets[importedVideo?.assetId ?? '']?.name).toBe('toolbar-video.mp4'); diff --git a/tests/e2e/editor/video-media-controls.spec.ts b/tests/e2e/editor/video-media-controls.spec.ts index e9e16a76..3f8fa8cd 100644 --- a/tests/e2e/editor/video-media-controls.spec.ts +++ b/tests/e2e/editor/video-media-controls.spec.ts @@ -21,11 +21,11 @@ test.describe('editor video media controls journey', () => { await editor.openTool('Design'); const movieTabs = page.getByRole('tablist', { name: 'Movie inspector sections' }); await movieTabs.getByRole('tab', { name: 'Movie' }).click(); - await expect(page.getByRole('button', { name: 'Pause movie' })).toBeVisible(); - await page.getByRole('button', { name: 'Pause movie' }).click(); await expect(page.getByRole('button', { name: 'Play movie' })).toBeVisible(); await page.getByRole('button', { name: 'Play movie' }).click(); await expect(page.getByRole('button', { name: 'Pause movie' })).toBeVisible(); + await page.getByRole('button', { name: 'Pause movie' }).click(); + await expect(page.getByRole('button', { name: 'Play movie' })).toBeVisible(); await page.getByLabel('Selected video volume').fill('25'); await expect(page.getByLabel('Selected video volume')).toHaveValue('25'); diff --git a/tests/e2e/public-deck/media-playback.spec.ts b/tests/e2e/public-deck/media-playback.spec.ts index e9b5c6b1..4ad3b2f3 100644 --- a/tests/e2e/public-deck/media-playback.spec.ts +++ b/tests/e2e/public-deck/media-playback.spec.ts @@ -30,6 +30,13 @@ test.describe('public deck media playback journey', () => { objectUrl: 'http://localhost/e2e-public-video.mp4', type: 'video', }; + payload.project.assets['asset-public-click-video'] = { + id: 'asset-public-click-video', + mimeType: 'video/mp4', + name: 'Click-start public fixture', + objectUrl: 'http://localhost/e2e-public-video.mp4', + type: 'video', + }; payload.project.assets['asset-public-gif'] = { id: 'asset-public-gif', mimeType: 'image/gif', @@ -62,6 +69,31 @@ test.describe('public deck media playback journey', () => { x: 600, y: 420, }; + payload.project.elements['video-public-click'] = { + assetId: 'asset-public-click-video', + autoplayInPreview: true, + controls: true, + height: 202, + id: 'video-public-click', + locked: false, + loop: false, + muted: true, + opacity: 1, + playAcrossSlides: false, + playbackPositionSeconds: 0, + playing: false, + posterFrameSeconds: 0, + repeatMode: 'none', + rotation: 0, + startOnClick: true, + trimStartSeconds: 0, + type: 'video', + visible: true, + volume: 0.5, + width: 360, + x: 120, + y: 120, + }; payload.project.elements['gif-public'] = { assetId: 'asset-public-gif', height: 180, @@ -104,7 +136,7 @@ test.describe('public deck media playback journey', () => { }, }; payload.project.pages[0].elementIds.push('image-public'); - payload.project.pages[1].elementIds.push('video-public', 'gif-public'); + payload.project.pages[1].elementIds.push('video-public', 'video-public-click', 'gif-public'); payload.project.pages[1].transition = { delayMs: 100, effect: 'fade' }; payload.project.pages[1].animationBuilds = [ { @@ -116,6 +148,15 @@ test.describe('public deck media playback journey', () => { mediaAction: 'play', trigger: 'after-transition', }, + { + delayMs: 0, + durationMs: 0, + effect: 'reveal', + elementId: 'video-public-click', + id: 'video-public-click-play', + mediaAction: 'play', + trigger: 'on-click', + }, ]; await page.route('**/e2e-share-with-video.json', async (route) => { @@ -163,17 +204,32 @@ test.describe('public deck media playback journey', () => { const video = page.locator('video[aria-label="Big Buck Bunny public fixture"]'); await expect(video).toBeVisible(); await expect(video).toHaveAttribute('src', /e2e-public-video\.mp4/); + const clickVideo = page.locator('video[aria-label="Click-start public fixture"]'); + await expect(clickVideo).toBeVisible(); + await expect(clickVideo).toHaveAttribute('src', /e2e-public-video\.mp4/); const gif = page.locator('img[aria-label="Public animated loop"]'); await expect(gif).toBeVisible(); await expect(gif).toHaveAttribute('src', /e2e-public-loop\.png/); await expect(page.getByTestId('slide-canvas-frame')).toHaveAttribute( 'data-animation-preview-phase', - 'complete', + 'waiting', ); await expect.poll(() => video.evaluate((node: HTMLVideoElement) => node.paused)).toBe(false); const playbackTime = await video.evaluate((node: HTMLVideoElement) => node.currentTime); await expect .poll(() => video.evaluate((node: HTMLVideoElement) => node.currentTime)) .toBeGreaterThan(playbackTime); + await expect(clickVideo.evaluate((node: HTMLVideoElement) => node.paused)).resolves.toBe(true); + + await page.locator('canvas').click({ position: { x: 400, y: 220 } }); + await expect(page.getByTestId('slide-canvas-frame')).toHaveAttribute( + 'data-animation-preview-phase', + 'complete', + ); + await expect.poll(() => clickVideo.evaluate((node: HTMLVideoElement) => node.paused)).toBe(false); + const clickPlaybackTime = await clickVideo.evaluate((node: HTMLVideoElement) => node.currentTime); + await expect + .poll(() => clickVideo.evaluate((node: HTMLVideoElement) => node.currentTime)) + .toBeGreaterThan(clickPlaybackTime); }); });