11import { test , expect , Page } from '@playwright/test' ;
22import { login , USERS } from '../fixtures/test-fixtures' ;
33import { TimelinePage } from '../pages/TimelinePage' ;
4+ import { serveRecordingMedia } from '../fixtures/recording-media' ;
45
56type Segment = { id : number ; stream : string ; start_timestamp : number ; end_timestamp : number } ;
67
@@ -268,7 +269,34 @@ test.describe('Timeline boundary flows @ui @timeline', () => {
268269 await expect ( timelinePage . nextRecordingButton ) . toBeDisabled ( ) ;
269270 } ) ;
270271
271- test ( 'waits for playback preparation and cancels polling when switching recordings' , async ( { page } ) => {
272+ test ( 'plays and seeks the original recording without preparing a copy' , async ( { page } ) => {
273+ const stream = 'front_door' ;
274+ const date = '2026-03-08' ;
275+ const segments : Segment [ ] = [
276+ { id : 320 , stream, start_timestamp : localTimestamp ( date , '11:00:00' ) , end_timestamp : localTimestamp ( date , '11:10:00' ) }
277+ ] ;
278+ await mockTimelineApis ( page , stream , segments ) ;
279+ const playbackRequests : string [ ] = [ ] ;
280+ await page . route ( '**/api/recordings/play/*' , route => {
281+ playbackRequests . push ( route . request ( ) . url ( ) ) ;
282+ return serveRecordingMedia ( route ) ;
283+ } ) ;
284+ await page . goto ( `/timeline.html?stream=${ stream } &date=${ date } &time=11:02:00` , { waitUntil : 'domcontentloaded' } ) ;
285+ const timelinePage = new TimelinePage ( page ) ;
286+ await expect . poll ( ( ) => timelinePage . videoPlayer . evaluate ( ( video : HTMLVideoElement ) => video . videoWidth ) ) . toBe ( 64 ) ;
287+ await expect . poll ( ( ) => timelinePage . videoPlayer . evaluate ( ( video : HTMLVideoElement ) => Math . round ( video . currentTime ) ) ) . toBe ( 120 ) ;
288+ await timelinePage . videoPlayer . evaluate ( async ( video : HTMLVideoElement ) => {
289+ video . muted = true ;
290+ await video . play ( ) ;
291+ } ) ;
292+ await expect . poll ( ( ) => timelinePage . videoPlayer . evaluate ( ( video : HTMLVideoElement ) => video . currentTime ) ) . toBeGreaterThan ( 120 ) ;
293+ expect ( playbackRequests . length ) . toBeGreaterThan ( 0 ) ;
294+ expect ( playbackRequests . every ( url => ! new URL ( url ) . searchParams . has ( 'prepare' ) &&
295+ ! new URL ( url ) . searchParams . has ( 'transcode' ) ) ) . toBe ( true ) ;
296+ await expect ( timelinePage . videoContainer . getByRole ( 'status' ) ) . toHaveCount ( 0 ) ;
297+ } ) ;
298+
299+ test ( 'prepares only rejected recordings and cancels fallback when switching recordings' , async ( { page } ) => {
272300 const stream = 'front_door' ;
273301 const date = '2026-03-08' ;
274302 const segments : Segment [ ] = [
@@ -278,7 +306,7 @@ test.describe('Timeline boundary flows @ui @timeline', () => {
278306 ] ;
279307 await mockTimelineApis ( page , stream , segments ) ;
280308 const preparations : number [ ] = [ ] ;
281- const mediaRequests : number [ ] = [ ] ;
309+ const mediaRequests : Array < { id : number ; transcode : boolean } > = [ ] ;
282310 let firstReady = false ;
283311 await page . route ( '**/api/recordings/play/*' , route => {
284312 const url = new URL ( route . request ( ) . url ( ) ) ;
@@ -292,22 +320,28 @@ test.describe('Timeline boundary flows @ui @timeline', () => {
292320 json : { status : ready ? 'ready' : 'preparing' }
293321 } ) ;
294322 }
295- mediaRequests . push ( id ) ;
296- return route . fulfill ( { status : 204 , body : '' } ) ;
323+ const transcode = url . searchParams . get ( 'transcode' ) === '1' ;
324+ mediaRequests . push ( { id, transcode } ) ;
325+ if ( transcode || id === 323 ) return serveRecordingMedia ( route ) ;
326+ return route . fulfill ( {
327+ contentType : 'video/mp4' ,
328+ body : Buffer . from ( 'unsupported video' )
329+ } ) ;
297330 } ) ;
298331 await page . goto ( `/timeline.html?stream=${ stream } &date=${ date } &time=11:00:00` , { waitUntil : 'domcontentloaded' } ) ;
299332
300333 const timelinePage = new TimelinePage ( page ) ;
301334 const status = timelinePage . videoContainer . getByRole ( 'status' ) ;
302- await expect ( status ) . toHaveText ( 'Preparing recording for playback …' ) ;
335+ await expect ( status ) . toHaveText ( 'Preparing a compatible version for this browser …' ) ;
303336 await expect ( timelinePage . videoPlayer ) . not . toHaveAttribute ( 'src' ) ;
304- expect ( mediaRequests ) . toEqual ( [ ] ) ;
337+ expect ( mediaRequests ) . toEqual ( [ { id : 321 , transcode : false } ] ) ;
305338 firstReady = true ;
306339 await expect ( timelinePage . videoPlayer ) . toHaveAttribute ( 'src' , / \/ a p i \/ r e c o r d i n g s \/ p l a y \/ 3 2 1 (?: \? | $ ) / ) ;
340+ await expect . poll ( ( ) => timelinePage . videoPlayer . evaluate ( ( video : HTMLVideoElement ) => video . videoWidth ) ) . toBe ( 64 ) ;
307341 expect ( preparations . filter ( id => id === 321 ) . length ) . toBeGreaterThanOrEqual ( 2 ) ;
308342
309343 await timelinePage . nextRecordingButton . click ( ) ;
310- await expect ( status ) . toHaveText ( 'Preparing recording for playback …' ) ;
344+ await expect ( status ) . toHaveText ( 'Preparing a compatible version for this browser …' ) ;
311345 await expect ( timelinePage . videoPlayer ) . not . toHaveAttribute ( 'src' ) ;
312346 await timelinePage . nextRecordingButton . click ( ) ;
313347 await expect ( timelinePage . videoPlayer ) . toHaveAttribute ( 'src' , / \/ a p i \/ r e c o r d i n g s \/ p l a y \/ 3 2 3 (?: \? | $ ) / ) ;
@@ -316,7 +350,11 @@ test.describe('Timeline boundary flows @ui @timeline', () => {
316350 // Wait beyond Retry-After to catch polling or media loads from the old clip.
317351 await page . waitForTimeout ( 1200 ) ;
318352 expect ( preparations . filter ( id => id === 322 ) ) . toHaveLength ( abandonedRequests ) ;
319- expect ( mediaRequests ) . toEqual ( [ 321 , 323 ] ) ;
353+ expect ( mediaRequests ) . toEqual ( [
354+ { id : 321 , transcode : false } , { id : 321 , transcode : true } ,
355+ { id : 322 , transcode : false } , { id : 323 , transcode : false }
356+ ] ) ;
357+ expect ( preparations ) . not . toContain ( 323 ) ;
320358 await expect ( timelinePage . videoPlayer ) . toHaveAttribute ( 'src' , / \/ a p i \/ r e c o r d i n g s \/ p l a y \/ 3 2 3 (?: \? | $ ) / ) ;
321359 } ) ;
322360
0 commit comments