2020-08-10 21:22:57 -07:00
/ * *
* Copyright Microsoft Corporation . All rights reserved .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2020-08-16 19:19:52 -07:00
2022-03-25 15:05:50 -08:00
import { browserTest as it , expect } from '../config/browserTest' ;
2020-08-14 07:22:54 -07:00
import fs from 'fs' ;
import path from 'path' ;
2022-05-19 10:22:18 -06:00
import type { Page } from 'playwright-core' ;
2020-09-29 18:52:30 -07:00
import { spawnSync } from 'child_process' ;
2023-01-12 10:44:20 -08:00
import { PNG , jpegjs } from 'playwright-core/lib/utilsBundle' ;
2022-04-06 21:21:27 -08:00
import { registry } from '../../packages/playwright-core/lib/server' ;
2022-06-13 11:53:31 -07:00
import { rewriteErrorMessage } from '../../packages/playwright-core/lib/utils/stackTrace' ;
2023-05-05 15:12:18 -07:00
import { parseTraceRaw } from '../config/utils' ;
2020-09-29 18:52:30 -07:00
2021-10-27 18:58:13 +02:00
const ffmpeg = registry . findExecutable ( 'ffmpeg' ) ! . executablePath ( 'javascript' ) ;
2020-09-29 18:52:30 -07:00
export class VideoPlayer {
fileName : string ;
output : string ;
duration : number ;
frames : number ;
videoWidth : number ;
videoHeight : number ;
2023-10-17 20:52:41 +02:00
cache = new Map < number , any > ( ) ;
2020-09-29 18:52:30 -07:00
constructor ( fileName : string ) {
this . fileName = fileName ;
2020-10-23 10:33:58 -07:00
// Force output frame rate to 25 fps as otherwise it would produce one image per timebase unit
// which is 1 / (25 * 1000).
this . output = spawnSync ( ffmpeg , [ '-i' , this . fileName , '-r' , '25' , ` ${ this . fileName } -%03d.png ` ] ) . stderr . toString ( ) ;
2020-09-29 18:52:30 -07:00
const lines = this . output . split ( '\n' ) ;
let framesLine = lines . find ( l = > l . startsWith ( 'frame=' ) ) ! ;
2021-02-08 10:59:48 -08:00
if ( ! framesLine )
throw new Error ( ` No frame data in the output: \ n ${ this . output } ` ) ;
2020-09-29 18:52:30 -07:00
framesLine = framesLine . substring ( framesLine . lastIndexOf ( 'frame=' ) ) ;
const framesMatch = framesLine . match ( /frame=\s+(\d+)/ ) ;
const streamLine = lines . find ( l = > l . trim ( ) . startsWith ( 'Stream #0:0' ) ) ;
const resolutionMatch = streamLine . match ( /, (\d+)x(\d+),/ ) ;
const durationMatch = lines . find ( l = > l . trim ( ) . startsWith ( 'Duration' ) ) ! . match ( /Duration: (\d+):(\d\d):(\d\d.\d\d)/ ) ;
this . duration = ( ( ( parseInt ( durationMatch ! [ 1 ] , 10 ) * 60 ) + parseInt ( durationMatch ! [ 2 ] , 10 ) ) * 60 + parseFloat ( durationMatch ! [ 3 ] ) ) * 1000 ;
this . frames = parseInt ( framesMatch ! [ 1 ] , 10 ) ;
this . videoWidth = parseInt ( resolutionMatch ! [ 1 ] , 10 ) ;
this . videoHeight = parseInt ( resolutionMatch ! [ 2 ] , 10 ) ;
}
2020-08-10 21:22:57 -07:00
2023-10-17 20:52:41 +02:00
seekFirstNonEmptyFrame ( offset ? : { x : number , y : number } ) : any | undefined {
2020-09-29 18:52:30 -07:00
for ( let f = 1 ; f <= this . frames ; ++ f ) {
2022-06-13 11:53:31 -07:00
const frame = this . frame ( f , offset ) ;
2020-09-29 18:52:30 -07:00
let hasColor = false ;
for ( let i = 0 ; i < frame . data . length ; i += 4 ) {
if ( frame . data [ i + 0 ] < 230 || frame . data [ i + 1 ] < 230 || frame . data [ i + 2 ] < 230 ) {
hasColor = true ;
break ;
}
}
if ( hasColor )
return this . frame ( f , offset ) ;
}
}
2020-09-18 17:36:43 -07:00
2023-10-17 20:52:41 +02:00
seekLastFrame ( offset ? : { x : number , y : number } ) : any {
2020-09-29 18:52:30 -07:00
return this . frame ( this . frames , offset ) ;
}
2020-09-18 17:36:43 -07:00
2023-10-17 20:52:41 +02:00
frame ( frame : number , offset = { x : 10 , y : 10 } ) : any {
2020-09-29 18:52:30 -07:00
if ( ! this . cache . has ( frame ) ) {
const gap = '0' . repeat ( 3 - String ( frame ) . length ) ;
const buffer = fs . readFileSync ( ` ${ this . fileName } - ${ gap } ${ frame } .png ` ) ;
this . cache . set ( frame , PNG . sync . read ( buffer ) ) ;
}
const decoded = this . cache . get ( frame ) ;
const dst = new PNG ( { width : 10 , height : 10 } ) ;
PNG . bitblt ( decoded , dst , offset . x , offset . y , 10 , 10 , 0 , 0 ) ;
return dst ;
}
}
2020-09-14 15:04:44 -07:00
2020-08-12 10:18:41 -07:00
function almostRed ( r , g , b , alpha ) {
2021-01-05 20:31:50 -08:00
expect ( r ) . toBeGreaterThan ( 185 ) ;
expect ( g ) . toBeLessThan ( 70 ) ;
expect ( b ) . toBeLessThan ( 70 ) ;
2020-08-12 10:18:41 -07:00
expect ( alpha ) . toBe ( 255 ) ;
}
function almostBlack ( r , g , b , alpha ) {
2021-01-05 20:31:50 -08:00
expect ( r ) . toBeLessThan ( 70 ) ;
expect ( g ) . toBeLessThan ( 70 ) ;
expect ( b ) . toBeLessThan ( 70 ) ;
2020-08-12 10:18:41 -07:00
expect ( alpha ) . toBe ( 255 ) ;
}
2021-01-05 20:31:50 -08:00
function almostGray ( r , g , b , alpha ) {
2020-09-29 18:52:30 -07:00
expect ( r ) . toBeGreaterThan ( 70 ) ;
expect ( g ) . toBeGreaterThan ( 70 ) ;
expect ( b ) . toBeGreaterThan ( 70 ) ;
2021-01-05 20:31:50 -08:00
expect ( r ) . toBeLessThan ( 185 ) ;
expect ( g ) . toBeLessThan ( 185 ) ;
expect ( b ) . toBeLessThan ( 185 ) ;
2020-08-12 10:18:41 -07:00
expect ( alpha ) . toBe ( 255 ) ;
}
2020-09-29 18:52:30 -07:00
function expectAll ( pixels : Buffer , rgbaPredicate ) {
2020-08-28 04:20:29 -07:00
const checkPixel = i = > {
2020-08-12 10:18:41 -07:00
const r = pixels [ i ] ;
const g = pixels [ i + 1 ] ;
const b = pixels [ i + 2 ] ;
const alpha = pixels [ i + 3 ] ;
rgbaPredicate ( r , g , b , alpha ) ;
2020-08-28 04:20:29 -07:00
} ;
2020-08-12 10:18:41 -07:00
try {
2020-08-28 04:20:29 -07:00
for ( let i = 0 , n = pixels . length ; i < n ; i += 4 )
2020-08-12 10:18:41 -07:00
checkPixel ( i ) ;
2020-08-28 04:20:29 -07:00
} catch ( e ) {
2020-08-12 10:18:41 -07:00
// Log pixel values on failure.
2022-06-13 11:53:31 -07:00
rewriteErrorMessage ( e , e . message + ` \ n \ nActual pixels=[ ${ pixels . join ( ',' ) } ] ` ) ;
2020-08-12 10:18:41 -07:00
throw e ;
}
}
2020-09-29 18:52:30 -07:00
function findVideos ( videoDir : string ) {
const files = fs . readdirSync ( videoDir ) ;
2020-09-18 17:36:43 -07:00
return files . filter ( file = > file . endsWith ( 'webm' ) ) . map ( file = > path . join ( videoDir , file ) ) ;
}
2020-11-07 18:21:26 -08:00
function expectRedFrames ( videoFile : string , size : { width : number , height : number } ) {
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
expect ( duration ) . toBeGreaterThan ( 0 ) ;
expect ( videoPlayer . videoWidth ) . toBe ( size . width ) ;
expect ( videoPlayer . videoHeight ) . toBe ( size . height ) ;
{
const pixels = videoPlayer . seekLastFrame ( ) . data ;
expectAll ( pixels , almostRed ) ;
}
{
const pixels = videoPlayer . seekLastFrame ( { x : size.width - 20 , y : 0 } ) . data ;
expectAll ( pixels , almostRed ) ;
}
}
2021-04-05 09:18:56 -07:00
it . describe ( 'screencast' , ( ) = > {
2021-05-07 15:25:55 -07:00
it . slow ( ) ;
2023-10-04 22:56:42 -04:00
it . skip ( ( { mode } ) = > mode !== 'default' , 'video.path() is not available in remote mode' ) ;
2021-05-07 15:25:55 -07:00
2021-09-27 18:58:08 +02:00
it ( 'videoSize should require videosPath' , async ( { browser } ) = > {
2020-10-01 11:06:19 -07:00
const error = await browser . newContext ( { videoSize : { width : 100 , height : 100 } } ) . catch ( e = > e ) ;
expect ( error . message ) . toContain ( '"videoSize" option requires "videosPath" to be specified' ) ;
2020-09-18 17:36:43 -07:00
} ) ;
2023-04-26 20:11:02 +00:00
it ( 'should work with old options' , async ( { browser , browserName , trace , headless , isWindows } , testInfo ) = > {
2020-10-05 17:03:24 -07:00
const videosPath = testInfo . outputPath ( '' ) ;
2022-06-08 16:55:30 -08:00
// Firefox does not have a mobile variant and has a large minimum size (500 on windows and 450 elsewhere).
const size = browserName === 'firefox' ? { width : 500 , height : 400 } : { width : 320 , height : 240 } ;
2020-09-18 17:36:43 -07:00
const context = await browser . newContext ( {
2020-10-01 11:06:19 -07:00
videosPath ,
2020-09-29 18:52:30 -07:00
viewport : size ,
videoSize : size
2020-09-18 17:36:43 -07:00
} ) ;
2020-09-11 18:58:53 -07:00
const page = await context . newPage ( ) ;
2020-08-31 13:18:19 -07:00
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-09-18 17:36:43 -07:00
await context . close ( ) ;
2020-08-31 13:18:19 -07:00
2020-10-19 14:35:18 -07:00
const videoFile = await page . video ( ) . path ( ) ;
2020-11-07 18:21:26 -08:00
expectRedFrames ( videoFile , size ) ;
2020-11-02 19:42:05 -08:00
} ) ;
it ( 'should throw without recordVideo.dir' , async ( { browser } ) = > {
const error = await browser . newContext ( { recordVideo : { } as any } ) . catch ( e = > e ) ;
expect ( error . message ) . toContain ( 'recordVideo.dir: expected string, got undefined' ) ;
} ) ;
2023-04-26 20:11:02 +00:00
it ( 'should capture static page' , async ( { browser , browserName , trace , headless , isWindows } , testInfo ) = > {
2022-06-08 16:55:30 -08:00
// Firefox does not have a mobile variant and has a large minimum size (500 on windows and 450 elsewhere).
const size = browserName === 'firefox' ? { width : 500 , height : 400 } : { width : 320 , height : 240 } ;
2020-11-02 19:42:05 -08:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-11-02 19:42:05 -08:00
await context . close ( ) ;
const videoFile = await page . video ( ) . path ( ) ;
2020-11-07 18:21:26 -08:00
expectRedFrames ( videoFile , size ) ;
2020-08-31 13:18:19 -07:00
} ) ;
2024-06-19 10:03:58 -07:00
it ( 'should continue recording main page after popup closes' , async ( { browser , browserName } , testInfo ) = > {
2024-06-18 18:29:41 +01:00
it . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/30837' } ) ;
// Firefox does not have a mobile variant and has a large minimum size (500 on windows and 450 elsewhere).
const size = browserName === 'firefox' ? { width : 500 , height : 400 } : { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
await page . setContent ( '<a target=_blank href="about:blank">clickme</a>' ) ;
const [ popup ] = await Promise . all ( [
page . waitForEvent ( 'popup' ) ,
await page . click ( 'a' ) ,
] ) ;
await popup . close ( ) ;
await page . evaluate ( ( ) = > {
document . body . textContent = '' ; // remove link
document . body . style . backgroundColor = 'red' ;
} ) ;
await waitForRafs ( page , 100 ) ;
await context . close ( ) ;
const videoFile = await page . video ( ) . path ( ) ;
expectRedFrames ( videoFile , size ) ;
} ) ;
2021-09-27 18:58:08 +02:00
it ( 'should expose video path' , async ( { browser } , testInfo ) = > {
2020-10-13 22:15:51 -07:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : videosPath ,
size
} ,
2020-10-13 22:15:51 -07:00
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
const path = await page . video ( ) ! . path ( ) ;
expect ( path ) . toContain ( videosPath ) ;
2020-10-14 14:10:35 -07:00
await context . close ( ) ;
expect ( fs . existsSync ( path ) ) . toBeTruthy ( ) ;
2020-10-14 15:09:36 -07:00
} ) ;
2022-06-08 14:16:09 -07:00
it ( 'saveAs should throw when no video frames' , async ( { browser } , testInfo ) = > {
2021-03-31 10:38:05 -07:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
recordVideo : {
dir : videosPath ,
size
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
const [ popup ] = await Promise . all ( [
page . context ( ) . waitForEvent ( 'page' ) ,
page . evaluate ( ( ) = > {
const win = window . open ( 'about:blank' ) ;
win . close ( ) ;
} ) ,
] ) ;
await page . close ( ) ;
const saveAsPath = testInfo . outputPath ( 'my-video.webm' ) ;
const error = await popup . video ( ) . saveAs ( saveAsPath ) . catch ( e = > e ) ;
2021-09-24 15:05:20 -07:00
// WebKit pauses renderer before win.close() and actually writes something,
// and other browsers are sometimes fast as well.
if ( ! fs . existsSync ( saveAsPath ) )
2021-03-31 10:38:05 -07:00
expect ( error . message ) . toContain ( 'Page did not produce any video frames' ) ;
2022-06-08 14:16:09 -07:00
await context . close ( ) ;
2021-03-31 10:38:05 -07:00
} ) ;
2021-09-27 18:58:08 +02:00
it ( 'should delete video' , async ( { browser } , testInfo ) = > {
2021-03-31 10:38:05 -07:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
recordVideo : {
dir : videosPath ,
size
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
const deletePromise = page . video ( ) . delete ( ) ;
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-03-31 10:38:05 -07:00
await context . close ( ) ;
const videoPath = await page . video ( ) . path ( ) ;
await deletePromise ;
expect ( fs . existsSync ( videoPath ) ) . toBeFalsy ( ) ;
} ) ;
2021-09-27 18:58:08 +02:00
it ( 'should expose video path blank page' , async ( { browser } , testInfo ) = > {
2020-10-14 15:09:36 -07:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : videosPath ,
size
} ,
2020-10-14 15:09:36 -07:00
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
const path = await page . video ( ) ! . path ( ) ;
expect ( path ) . toContain ( videosPath ) ;
await context . close ( ) ;
expect ( fs . existsSync ( path ) ) . toBeTruthy ( ) ;
} ) ;
2023-04-03 16:28:35 +00:00
it ( 'should work with weird screen resolution' , async ( { browser } , testInfo ) = > {
it . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/22069' } ) ;
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 1904 , height : 609 } ;
const context = await browser . newContext ( {
recordVideo : {
dir : videosPath ,
size
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
const path = await page . video ( ) ! . path ( ) ;
expect ( path ) . toContain ( videosPath ) ;
await context . close ( ) ;
expect ( fs . existsSync ( path ) ) . toBeTruthy ( ) ;
} ) ;
2023-09-15 10:26:20 -07:00
it ( 'should work with relative path for recordVideo.dir' , async ( { browser } , testInfo ) = > {
it . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/27086' } ) ;
const videosPath = path . relative ( process . cwd ( ) , testInfo . outputPath ( '' ) ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
recordVideo : {
dir : videosPath ,
size
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
const videoPath = await page . video ( ) ! . path ( ) ;
await context . close ( ) ;
expect ( fs . existsSync ( videoPath ) ) . toBeTruthy ( ) ;
} ) ;
2021-09-27 18:58:08 +02:00
it ( 'should expose video path blank popup' , async ( { browser } , testInfo ) = > {
2020-10-14 15:09:36 -07:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : videosPath ,
size
} ,
2020-10-14 15:09:36 -07:00
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
const [ popup ] = await Promise . all ( [
page . waitForEvent ( 'popup' ) ,
page . evaluate ( 'window.open("about:blank")' )
] ) ;
const path = await popup . video ( ) ! . path ( ) ;
expect ( path ) . toContain ( videosPath ) ;
await context . close ( ) ;
expect ( fs . existsSync ( path ) ) . toBeTruthy ( ) ;
2020-10-13 22:15:51 -07:00
} ) ;
2022-02-10 10:45:18 -07:00
it ( 'should capture navigation' , async ( { browser , browserName , server , trace } , testInfo ) = > {
2020-09-18 17:36:43 -07:00
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size : { width : 1280 , height : 720 }
} ,
2020-09-18 17:36:43 -07:00
} ) ;
2020-09-11 18:58:53 -07:00
const page = await context . newPage ( ) ;
2020-08-28 13:53:47 -07:00
await page . goto ( server . PREFIX + '/background-color.html#rgb(0,0,0)' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-08-28 13:53:47 -07:00
await page . goto ( server . CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-09-18 17:36:43 -07:00
await context . close ( ) ;
2020-08-28 13:53:47 -07:00
2020-10-19 14:35:18 -07:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-29 18:52:30 -07:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
2020-08-28 13:53:47 -07:00
expect ( duration ) . toBeGreaterThan ( 0 ) ;
{
2020-09-29 18:52:30 -07:00
const pixels = videoPlayer . seekFirstNonEmptyFrame ( ) . data ;
2020-08-28 13:53:47 -07:00
expectAll ( pixels , almostBlack ) ;
}
2020-08-24 17:23:54 -07:00
2020-08-28 13:53:47 -07:00
{
2020-09-29 18:52:30 -07:00
const pixels = videoPlayer . seekLastFrame ( ) . data ;
2021-01-05 20:31:50 -08:00
expectAll ( pixels , almostGray ) ;
2020-08-28 13:53:47 -07:00
}
} ) ;
2022-02-10 10:45:18 -07:00
it ( 'should capture css transformation' , async ( { browser , server , headless , browserName , platform , trace } , testInfo ) = > {
2021-05-13 10:22:23 -07:00
it . fixme ( ! headless , 'Fails on headed' ) ;
it . fixme ( browserName === 'webkit' && platform === 'win32' ) ;
2021-04-05 09:18:56 -07:00
2022-06-01 14:21:00 -07:00
const size = { width : 600 , height : 400 } ;
2020-09-02 10:40:50 -07:00
// Set viewport equal to screencast frame size to avoid scaling.
2020-09-18 17:36:43 -07:00
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
2020-09-18 17:36:43 -07:00
viewport : size ,
} ) ;
2020-09-11 18:58:53 -07:00
const page = await context . newPage ( ) ;
2020-08-28 13:53:47 -07:00
await page . goto ( server . PREFIX + '/rotate-z.html' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-09-18 17:36:43 -07:00
await context . close ( ) ;
2020-08-28 13:53:47 -07:00
2020-10-19 14:35:18 -07:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-29 18:52:30 -07:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
2020-08-28 13:53:47 -07:00
expect ( duration ) . toBeGreaterThan ( 0 ) ;
{
2020-09-29 18:52:30 -07:00
const pixels = videoPlayer . seekLastFrame ( { x : 95 , y : 45 } ) . data ;
2020-08-28 13:53:47 -07:00
expectAll ( pixels , almostRed ) ;
}
} ) ;
2022-02-10 10:45:18 -07:00
it ( 'should work for popups' , async ( { browser , server , browserName , trace } , testInfo ) = > {
2022-06-01 14:21:00 -07:00
it . fixme ( browserName === 'firefox' , 'https://github.com/microsoft/playwright/issues/14557' ) ;
2020-10-05 17:03:24 -07:00
const videosPath = testInfo . outputPath ( '' ) ;
2022-06-01 14:21:00 -07:00
const size = { width : 600 , height : 400 } ;
2020-09-18 17:36:43 -07:00
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : videosPath ,
2020-11-07 18:21:26 -08:00
size ,
2020-11-02 19:42:05 -08:00
} ,
2020-11-07 18:21:26 -08:00
viewport : size ,
2020-09-18 17:36:43 -07:00
} ) ;
2020-08-31 15:21:02 -07:00
2020-09-08 17:01:00 -07:00
const page = await context . newPage ( ) ;
2020-09-04 22:37:38 -07:00
await page . goto ( server . EMPTY_PAGE ) ;
2020-11-07 18:21:26 -08:00
const [ popup ] = await Promise . all ( [
2020-09-18 17:36:43 -07:00
page . waitForEvent ( 'popup' ) ,
page . evaluate ( ( ) = > { window . open ( 'about:blank' ) ; } ) ,
2020-08-28 13:53:47 -07:00
] ) ;
2020-11-07 18:21:26 -08:00
await popup . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 10:22:18 -06:00
await Promise . all ( [
waitForRafs ( page , 100 ) ,
waitForRafs ( popup , 100 ) ,
] ) ;
2020-09-18 17:36:43 -07:00
await context . close ( ) ;
2020-11-07 18:21:26 -08:00
const pageVideoFile = await page . video ( ) . path ( ) ;
const popupVideoFile = await popup . video ( ) . path ( ) ;
expect ( pageVideoFile ) . not . toEqual ( popupVideoFile ) ;
expectRedFrames ( popupVideoFile , size ) ;
2020-10-01 11:06:19 -07:00
const videoFiles = findVideos ( videosPath ) ;
2020-09-18 17:36:43 -07:00
expect ( videoFiles . length ) . toBe ( 2 ) ;
2020-08-28 13:53:47 -07:00
} ) ;
2020-09-02 13:59:15 -07:00
2024-11-14 12:20:44 +00:00
it ( 'should scale frames down to the requested size ' , async ( { browser , browserName , server , headless , isHeadlessShell } , testInfo ) = > {
2024-11-04 03:56:00 -08:00
it . fixme ( ! headless , 'Fails on headed' ) ;
2024-11-13 10:52:28 +00:00
it . fixme ( browserName === 'chromium' && ! isHeadlessShell , 'Chromium (but not headless shell) has a min width issue' ) ;
2021-04-05 09:18:56 -07:00
2020-09-11 18:58:53 -07:00
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
// Set size to 1/2 of the viewport.
size : { width : 320 , height : 240 } ,
} ,
2021-09-27 18:58:08 +02:00
viewport : { width : 640 , height : 480 } ,
2020-09-11 18:58:53 -07:00
} ) ;
const page = await context . newPage ( ) ;
2020-09-02 13:59:15 -07:00
await page . goto ( server . PREFIX + '/checkerboard.html' ) ;
// Update the picture to ensure enough frames are generated.
await page . $eval ( '.container' , container = > {
container . firstElementChild . classList . remove ( 'red' ) ;
} ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-09-02 13:59:15 -07:00
await page . $eval ( '.container' , container = > {
container . firstElementChild . classList . add ( 'red' ) ;
} ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-09-18 17:36:43 -07:00
await context . close ( ) ;
2020-09-02 13:59:15 -07:00
2020-10-19 14:35:18 -07:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-29 18:52:30 -07:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
2020-09-02 13:59:15 -07:00
expect ( duration ) . toBeGreaterThan ( 0 ) ;
{
2021-09-27 18:58:08 +02:00
const pixels = videoPlayer . seekLastFrame ( { x : 0 , y : 0 } ) . data ;
2020-09-02 13:59:15 -07:00
expectAll ( pixels , almostRed ) ;
}
{
2021-09-27 18:58:08 +02:00
const pixels = videoPlayer . seekLastFrame ( { x : 300 , y : 0 } ) . data ;
2021-01-05 20:31:50 -08:00
expectAll ( pixels , almostGray ) ;
2020-09-02 13:59:15 -07:00
}
{
2021-09-27 18:58:08 +02:00
const pixels = videoPlayer . seekLastFrame ( { x : 0 , y : 200 } ) . data ;
2021-01-05 20:31:50 -08:00
expectAll ( pixels , almostGray ) ;
2020-09-02 13:59:15 -07:00
}
{
2021-09-27 18:58:08 +02:00
const pixels = videoPlayer . seekLastFrame ( { x : 300 , y : 200 } ) . data ;
2020-09-02 13:59:15 -07:00
expectAll ( pixels , almostRed ) ;
}
} ) ;
2020-09-11 15:14:31 -07:00
2021-09-27 18:58:08 +02:00
it ( 'should use viewport scaled down to fit into 800x800 as default size' , async ( { browser } , testInfo ) = > {
const size = { width : 1600 , height : 1200 } ;
2020-09-18 17:36:43 -07:00
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
2020-09-18 17:36:43 -07:00
viewport : size ,
} ) ;
2020-09-11 15:14:31 -07:00
2020-10-19 14:35:18 -07:00
const page = await context . newPage ( ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-09-18 17:36:43 -07:00
await context . close ( ) ;
2020-09-11 15:14:31 -07:00
2020-10-19 14:35:18 -07:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-29 18:52:30 -07:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
2021-02-08 10:59:48 -08:00
expect ( videoPlayer . videoWidth ) . toBe ( 800 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 600 ) ;
2020-09-11 15:14:31 -07:00
} ) ;
2021-04-05 09:18:56 -07:00
it ( 'should be 800x450 by default' , async ( { browser } , testInfo ) = > {
2020-09-18 17:36:43 -07:00
const context = await browser . newContext ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
2020-09-18 17:36:43 -07:00
} ) ;
2020-09-11 15:14:31 -07:00
2020-10-19 14:35:18 -07:00
const page = await context . newPage ( ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-09-18 17:36:43 -07:00
await context . close ( ) ;
2020-09-11 15:14:31 -07:00
2020-10-19 14:35:18 -07:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-29 18:52:30 -07:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
2021-02-08 10:59:48 -08:00
expect ( videoPlayer . videoWidth ) . toBe ( 800 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 450 ) ;
} ) ;
2021-05-13 10:22:23 -07:00
it ( 'should be 800x600 with null viewport' , async ( { browser , headless , browserName } , testInfo ) = > {
it . fixme ( browserName === 'firefox' && headless , 'Fails in headless on bots' ) ;
2021-04-05 09:18:56 -07:00
2021-02-08 10:59:48 -08:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
viewport : null
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-02-08 10:59:48 -08:00
await context . close ( ) ;
const videoFile = await page . video ( ) . path ( ) ;
const videoPlayer = new VideoPlayer ( videoFile ) ;
expect ( videoPlayer . videoWidth ) . toBe ( 800 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 600 ) ;
2020-09-11 15:14:31 -07:00
} ) ;
2020-10-01 11:06:19 -07:00
2023-03-09 16:56:29 -08:00
it ( 'should capture static page in persistent context @smoke' , async ( { launchPersistent , browserName , trace , isMac } , testInfo ) = > {
it . skip ( browserName === 'webkit' && isMac && process . arch === 'arm64' , 'Is only failing on self-hosted github actions runner on M1 mac; not reproducible locally' ) ;
2022-06-01 14:21:00 -07:00
const size = { width : 600 , height : 400 } ;
2020-10-01 11:06:19 -07:00
const { context , page } = await launchPersistent ( {
2020-11-02 19:42:05 -08:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
2020-10-01 11:06:19 -07:00
viewport : size ,
} ) ;
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2020-10-01 11:06:19 -07:00
await context . close ( ) ;
2020-10-19 14:35:18 -07:00
const videoFile = await page . video ( ) . path ( ) ;
2020-10-01 11:06:19 -07:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
expect ( duration ) . toBeGreaterThan ( 0 ) ;
2022-06-01 14:21:00 -07:00
expect ( videoPlayer . videoWidth ) . toBe ( 600 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 400 ) ;
2020-10-01 11:06:19 -07:00
{
const pixels = videoPlayer . seekLastFrame ( ) . data ;
expectAll ( pixels , almostRed ) ;
}
} ) ;
2021-02-10 13:37:27 -08:00
2021-10-28 07:31:30 -08:00
it ( 'should emulate an iphone' , async ( { contextFactory , playwright , browserName } , testInfo ) = > {
2021-04-05 09:18:56 -07:00
it . skip ( browserName === 'firefox' , 'isMobile is not supported in Firefox' ) ;
2021-02-10 13:37:27 -08:00
const device = playwright . devices [ 'iPhone 6' ] ;
const context = await contextFactory ( {
. . . device ,
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-02-10 13:37:27 -08:00
await context . close ( ) ;
const videoFile = await page . video ( ) . path ( ) ;
const videoPlayer = new VideoPlayer ( videoFile ) ;
expect ( videoPlayer . videoWidth ) . toBe ( 374 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 666 ) ;
} ) ;
2021-04-08 14:01:05 -07:00
2021-10-28 07:31:30 -08:00
it ( 'should throw on browser close' , async ( { browserType } , testInfo ) = > {
2021-04-08 14:01:05 -07:00
const size = { width : 320 , height : 240 } ;
2021-10-27 18:00:06 -08:00
const browser = await browserType . launch ( ) ;
2021-04-08 14:01:05 -07:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-04-08 14:01:05 -07:00
await browser . close ( ) ;
const file = testInfo . outputPath ( 'saved-video-' ) ;
const saveResult = await page . video ( ) . saveAs ( file ) . catch ( e = > e ) ;
expect ( saveResult . message ) . toContain ( 'browser has been closed' ) ;
} ) ;
2021-04-08 18:56:09 -07:00
2021-10-28 07:31:30 -08:00
it ( 'should throw if browser dies' , async ( { browserType } , testInfo ) = > {
2021-04-08 18:56:09 -07:00
const size = { width : 320 , height : 240 } ;
2021-10-27 18:00:06 -08:00
const browser = await browserType . launch ( ) ;
2021-04-08 18:56:09 -07:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-04-08 18:56:09 -07:00
await ( browser as any ) . _channel . killForTests ( ) ;
const file = testInfo . outputPath ( 'saved-video-' ) ;
const saveResult = await page . video ( ) . saveAs ( file ) . catch ( e = > e ) ;
expect ( saveResult . message ) . toContain ( 'rowser has been closed' ) ;
} ) ;
2021-05-19 05:29:39 +00:00
2021-10-28 07:31:30 -08:00
it ( 'should wait for video to finish if page was closed' , async ( { browserType } , testInfo ) = > {
2021-05-19 05:29:39 +00:00
const size = { width : 320 , height : 240 } ;
2021-10-27 18:00:06 -08:00
const browser = await browserType . launch ( ) ;
2021-05-19 05:29:39 +00:00
const videoDir = testInfo . outputPath ( '' ) ;
const context = await browser . newContext ( {
recordVideo : {
dir : videoDir ,
size ,
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-05-19 05:29:39 +00:00
await page . close ( ) ;
await context . close ( ) ;
await browser . close ( ) ;
const videoFiles = findVideos ( videoDir ) ;
expect ( videoFiles . length ) . toBe ( 1 ) ;
const videoPlayer = new VideoPlayer ( videoFiles [ 0 ] ) ;
expect ( videoPlayer . videoWidth ) . toBe ( 320 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 240 ) ;
} ) ;
2021-10-28 07:31:30 -08:00
it ( 'should not create video for internal pages' , async ( { browser , server } , testInfo ) = > {
2021-05-29 01:20:49 +00:00
it . fixme ( true , 'https://github.com/microsoft/playwright/issues/6743' ) ;
server . setRoute ( '/empty.html' , ( req , res ) = > {
res . setHeader ( 'Set-Cookie' , 'name=value' ) ;
res . end ( ) ;
} ) ;
const videoDir = testInfo . outputPath ( '' ) ;
const context = await browser . newContext ( {
recordVideo : {
dir : videoDir
}
} ) ;
const page = await context . newPage ( ) ;
await page . goto ( server . EMPTY_PAGE ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-05-29 01:20:49 +00:00
const cookies = await context . cookies ( ) ;
expect ( cookies . length ) . toBe ( 1 ) ;
await context . storageState ( ) ;
await context . close ( ) ;
const files = fs . readdirSync ( videoDir ) ;
expect ( files . length ) . toBe ( 1 ) ;
} ) ;
2021-12-08 18:13:12 -08:00
2024-11-14 12:20:44 +00:00
it ( 'should capture full viewport' , async ( { browserType , browserName , isWindows , headless , isHeadlessShell } , testInfo ) = > {
2023-04-18 09:21:20 -07:00
it . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/22411' } ) ;
2024-11-13 10:52:28 +00:00
it . fixme ( browserName === 'chromium' && ! isHeadlessShell , 'The square is not on the video' ) ;
2022-06-08 16:55:30 -08:00
it . fixme ( browserName === 'firefox' && isWindows , 'https://github.com/microsoft/playwright/issues/14405' ) ;
2022-06-01 14:21:00 -07:00
const size = { width : 600 , height : 400 } ;
2022-05-25 08:45:22 -07:00
const browser = await browserType . launch ( ) ;
const videoDir = testInfo . outputPath ( '' ) ;
const context = await browser . newContext ( {
viewport : size ,
recordVideo : {
dir : videoDir ,
size ,
} ,
} ) ;
const page = await context . newPage ( ) ;
await page . setContent ( ` <div style='margin: 0; background: red; position: fixed; right:0; bottom:0; width: 30; height: 30;'></div> ` ) ;
await waitForRafs ( page , 100 ) ;
await page . close ( ) ;
await context . close ( ) ;
await browser . close ( ) ;
const videoFiles = findVideos ( videoDir ) ;
expect ( videoFiles . length ) . toBe ( 1 ) ;
const videoPlayer = new VideoPlayer ( videoFiles [ 0 ] ) ;
expect ( videoPlayer . videoWidth ) . toBe ( size . width ) ;
expect ( videoPlayer . videoHeight ) . toBe ( size . height ) ;
// Bottom right corner should be part of the red border.
2022-06-13 11:53:31 -07:00
// However, headed browsers on mac have rounded corners, so offset by 10.
const pixels = videoPlayer . seekLastFrame ( { x : size.width - 20 , y : size.height - 20 } ) . data ;
2022-05-25 08:45:22 -07:00
expectAll ( pixels , almostRed ) ;
} ) ;
2024-11-14 12:20:44 +00:00
it ( 'should capture full viewport on hidpi' , async ( { browserType , browserName , headless , isWindows , isLinux , isHeadlessShell } , testInfo ) = > {
2023-04-18 09:21:20 -07:00
it . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/22411' } ) ;
2024-11-13 10:52:28 +00:00
it . fixme ( browserName === 'chromium' && ! isHeadlessShell , 'The square is not on the video' ) ;
2022-06-08 16:55:30 -08:00
it . fixme ( browserName === 'firefox' && isWindows , 'https://github.com/microsoft/playwright/issues/14405' ) ;
2023-09-01 17:26:23 -07:00
it . fixme ( browserName === 'webkit' && isLinux && ! headless , 'https://github.com/microsoft/playwright/issues/22617' ) ;
2022-06-01 14:21:00 -07:00
const size = { width : 600 , height : 400 } ;
2021-12-08 18:13:12 -08:00
const browser = await browserType . launch ( ) ;
const videoDir = testInfo . outputPath ( '' ) ;
const context = await browser . newContext ( {
viewport : size ,
deviceScaleFactor : 3 ,
recordVideo : {
dir : videoDir ,
size ,
} ,
} ) ;
const page = await context . newPage ( ) ;
await page . setContent ( ` <div style='margin: 0; background: red; position: fixed; right:0; bottom:0; width: 30; height: 30;'></div> ` ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-12-08 18:13:12 -08:00
await page . close ( ) ;
await context . close ( ) ;
await browser . close ( ) ;
const videoFiles = findVideos ( videoDir ) ;
expect ( videoFiles . length ) . toBe ( 1 ) ;
const videoPlayer = new VideoPlayer ( videoFiles [ 0 ] ) ;
expect ( videoPlayer . videoWidth ) . toBe ( size . width ) ;
expect ( videoPlayer . videoHeight ) . toBe ( size . height ) ;
// Bottom right corner should be part of the red border.
2022-06-13 11:53:31 -07:00
// However, headed browsers on mac have rounded corners, so offset by 10.
const pixels = videoPlayer . seekLastFrame ( { x : size.width - 20 , y : size.height - 20 } ) . data ;
2021-12-08 18:13:12 -08:00
expectAll ( pixels , almostRed ) ;
} ) ;
2023-01-12 10:44:20 -08:00
2024-11-14 12:20:44 +00:00
it ( 'should work with video+trace' , async ( { browser , trace , headless , browserName , isHeadlessShell } , testInfo ) = > {
2023-01-12 10:44:20 -08:00
it . skip ( trace === 'on' ) ;
2024-11-04 03:56:00 -08:00
it . fixme ( ! headless , 'different trace screencast image size on all browsers' ) ;
2024-11-13 10:52:28 +00:00
it . fixme ( browserName === 'chromium' && ! isHeadlessShell , 'different trace screencast image size' ) ;
2023-01-12 10:44:20 -08:00
const size = { width : 500 , height : 400 } ;
2024-09-17 15:32:30 +02:00
const traceFile = testInfo . outputPath ( 'trace.zip' ) ;
2023-01-12 10:44:20 -08:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size
} ,
viewport : size ,
} ) ;
await context . tracing . start ( { screenshots : true } ) ;
const page = await context . newPage ( ) ;
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
await waitForRafs ( page , 100 ) ;
await context . tracing . stop ( { path : traceFile } ) ;
await context . close ( ) ;
const videoFile = await page . video ( ) . path ( ) ;
expectRedFrames ( videoFile , size ) ;
2023-05-05 15:12:18 -07:00
const { events , resources } = await parseTraceRaw ( traceFile ) ;
2023-01-12 10:44:20 -08:00
const frame = events . filter ( e = > e . type === 'screencast-frame' ) . pop ( ) ;
const buffer = resources . get ( 'resources/' + frame . sha1 ) ;
const image = jpegjs . decode ( buffer ) ;
expect ( image . width ) . toBe ( size . width ) ;
expect ( image . height ) . toBe ( size . height ) ;
const offset = size . width * size . height / 2 * 4 + size . width * 4 / 2 ; // Center should be red.
almostRed ( image . data . readUInt8 ( offset ) , image . data . readUInt8 ( offset + 1 ) , image . data . readUInt8 ( offset + 2 ) , image . data . readUInt8 ( offset + 3 ) ) ;
} ) ;
2021-10-14 10:41:03 -07:00
} ) ;
2021-05-29 01:20:49 +00:00
2021-10-14 10:41:03 -07:00
it ( 'should saveAs video' , async ( { browser } , testInfo ) = > {
it . slow ( ) ;
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
recordVideo : {
dir : videosPath ,
size
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 10:22:18 -06:00
await waitForRafs ( page , 100 ) ;
2021-10-14 10:41:03 -07:00
await context . close ( ) ;
const saveAsPath = testInfo . outputPath ( 'my-video.webm' ) ;
await page . video ( ) . saveAs ( saveAsPath ) ;
expect ( fs . existsSync ( saveAsPath ) ) . toBeTruthy ( ) ;
2020-09-18 17:36:43 -07:00
} ) ;
2022-05-19 10:22:18 -06:00
async function waitForRafs ( page : Page , count : number ) : Promise < void > {
await page . evaluate ( count = > new Promise < void > ( resolve = > {
const onRaf = ( ) = > {
-- count ;
if ( ! count )
resolve ( ) ;
else
2024-05-31 14:44:26 -07:00
window . builtinRequestAnimationFrame ( onRaf ) ;
2022-05-19 10:22:18 -06:00
} ;
2024-05-31 14:44:26 -07:00
window . builtinRequestAnimationFrame ( onRaf ) ;
2022-05-19 10:22:18 -06:00
} ) , count ) ;
}