2020-08-11 07:22:57 +03: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-17 05:19:52 +03:00
2022-03-26 02:05:50 +03:00
import { browserTest as it , expect } from '../config/browserTest' ;
2020-08-14 17:22:54 +03:00
import fs from 'fs' ;
import path from 'path' ;
2022-05-19 19:22:18 +03:00
import type { Page } from 'playwright-core' ;
2020-09-30 04:52:30 +03:00
import { spawnSync } from 'child_process' ;
2023-01-12 21:44:20 +03:00
import { PNG , jpegjs } from 'playwright-core/lib/utilsBundle' ;
2022-04-07 08:21:27 +03:00
import { registry } from '../../packages/playwright-core/lib/server' ;
2022-06-13 21:53:31 +03:00
import { rewriteErrorMessage } from '../../packages/playwright-core/lib/utils/stackTrace' ;
2023-05-06 01:12:18 +03:00
import { parseTraceRaw } from '../config/utils' ;
2020-09-30 04:52:30 +03:00
2021-10-27 19:58:13 +03:00
const ffmpeg = registry . findExecutable ( 'ffmpeg' ) ! . executablePath ( 'javascript' ) ;
2020-09-30 04:52:30 +03:00
export class VideoPlayer {
fileName : string ;
output : string ;
duration : number ;
frames : number ;
videoWidth : number ;
videoHeight : number ;
cache = new Map < number , PNG > ( ) ;
constructor ( fileName : string ) {
this . fileName = fileName ;
2020-10-23 20:33:58 +03: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-30 04:52:30 +03:00
const lines = this . output . split ( '\n' ) ;
let framesLine = lines . find ( l = > l . startsWith ( 'frame=' ) ) ! ;
2021-02-08 21:59:48 +03:00
if ( ! framesLine )
throw new Error ( ` No frame data in the output: \ n ${ this . output } ` ) ;
2020-09-30 04:52:30 +03: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-11 07:22:57 +03:00
2022-06-13 21:53:31 +03:00
seekFirstNonEmptyFrame ( offset ? : { x : number , y : number } ) : PNG | undefined {
2020-09-30 04:52:30 +03:00
for ( let f = 1 ; f <= this . frames ; ++ f ) {
2022-06-13 21:53:31 +03:00
const frame = this . frame ( f , offset ) ;
2020-09-30 04:52:30 +03: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-19 03:36:43 +03:00
2020-09-30 04:52:30 +03:00
seekLastFrame ( offset ? : { x : number , y : number } ) : PNG {
return this . frame ( this . frames , offset ) ;
}
2020-09-19 03:36:43 +03:00
2020-09-30 04:52:30 +03:00
frame ( frame : number , offset = { x : 10 , y : 10 } ) : PNG {
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-15 01:04:44 +03:00
2020-08-12 20:18:41 +03:00
function almostRed ( r , g , b , alpha ) {
2021-01-06 07:31:50 +03:00
expect ( r ) . toBeGreaterThan ( 185 ) ;
expect ( g ) . toBeLessThan ( 70 ) ;
expect ( b ) . toBeLessThan ( 70 ) ;
2020-08-12 20:18:41 +03:00
expect ( alpha ) . toBe ( 255 ) ;
}
function almostBlack ( r , g , b , alpha ) {
2021-01-06 07:31:50 +03:00
expect ( r ) . toBeLessThan ( 70 ) ;
expect ( g ) . toBeLessThan ( 70 ) ;
expect ( b ) . toBeLessThan ( 70 ) ;
2020-08-12 20:18:41 +03:00
expect ( alpha ) . toBe ( 255 ) ;
}
2021-01-06 07:31:50 +03:00
function almostGray ( r , g , b , alpha ) {
2020-09-30 04:52:30 +03:00
expect ( r ) . toBeGreaterThan ( 70 ) ;
expect ( g ) . toBeGreaterThan ( 70 ) ;
expect ( b ) . toBeGreaterThan ( 70 ) ;
2021-01-06 07:31:50 +03:00
expect ( r ) . toBeLessThan ( 185 ) ;
expect ( g ) . toBeLessThan ( 185 ) ;
expect ( b ) . toBeLessThan ( 185 ) ;
2020-08-12 20:18:41 +03:00
expect ( alpha ) . toBe ( 255 ) ;
}
2020-09-30 04:52:30 +03:00
function expectAll ( pixels : Buffer , rgbaPredicate ) {
2020-08-28 14:20:29 +03:00
const checkPixel = i = > {
2020-08-12 20:18:41 +03: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 14:20:29 +03:00
} ;
2020-08-12 20:18:41 +03:00
try {
2020-08-28 14:20:29 +03:00
for ( let i = 0 , n = pixels . length ; i < n ; i += 4 )
2020-08-12 20:18:41 +03:00
checkPixel ( i ) ;
2020-08-28 14:20:29 +03:00
} catch ( e ) {
2020-08-12 20:18:41 +03:00
// Log pixel values on failure.
2022-06-13 21:53:31 +03:00
rewriteErrorMessage ( e , e . message + ` \ n \ nActual pixels=[ ${ pixels . join ( ',' ) } ] ` ) ;
2020-08-12 20:18:41 +03:00
throw e ;
}
}
2020-09-30 04:52:30 +03:00
function findVideos ( videoDir : string ) {
const files = fs . readdirSync ( videoDir ) ;
2020-09-19 03:36:43 +03:00
return files . filter ( file = > file . endsWith ( 'webm' ) ) . map ( file = > path . join ( videoDir , file ) ) ;
}
2020-11-08 05:21:26 +03: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 19:18:56 +03:00
it . describe ( 'screencast' , ( ) = > {
2021-05-08 01:25:55 +03:00
it . slow ( ) ;
2022-07-06 19:20:07 +03:00
it . skip ( ( { mode } ) = > mode !== 'default' , 'video.path() is not avaialble in remote mode' ) ;
2021-05-08 01:25:55 +03:00
2021-09-27 19:58:08 +03:00
it ( 'videoSize should require videosPath' , async ( { browser } ) = > {
2020-10-01 21:06:19 +03: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-19 03:36:43 +03:00
} ) ;
2023-04-26 23:11:02 +03:00
it ( 'should work with old options' , async ( { browser , browserName , trace , headless , isWindows } , testInfo ) = > {
2020-10-06 03:03:24 +03:00
const videosPath = testInfo . outputPath ( '' ) ;
2022-06-09 03:55:30 +03: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-19 03:36:43 +03:00
const context = await browser . newContext ( {
2020-10-01 21:06:19 +03:00
videosPath ,
2020-09-30 04:52:30 +03:00
viewport : size ,
videoSize : size
2020-09-19 03:36:43 +03:00
} ) ;
2020-09-12 04:58:53 +03:00
const page = await context . newPage ( ) ;
2020-08-31 23:18:19 +03:00
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-09-19 03:36:43 +03:00
await context . close ( ) ;
2020-08-31 23:18:19 +03:00
2020-10-20 00:35:18 +03:00
const videoFile = await page . video ( ) . path ( ) ;
2020-11-08 05:21:26 +03:00
expectRedFrames ( videoFile , size ) ;
2020-11-03 06:42:05 +03: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 23:11:02 +03:00
it ( 'should capture static page' , async ( { browser , browserName , trace , headless , isWindows } , testInfo ) = > {
2022-06-09 03:55:30 +03: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-03 06:42:05 +03: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 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-11-03 06:42:05 +03:00
await context . close ( ) ;
const videoFile = await page . video ( ) . path ( ) ;
2020-11-08 05:21:26 +03:00
expectRedFrames ( videoFile , size ) ;
2020-08-31 23:18:19 +03:00
} ) ;
2021-09-27 19:58:08 +03:00
it ( 'should expose video path' , async ( { browser } , testInfo ) = > {
2020-10-14 08:15:51 +03:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : videosPath ,
size
} ,
2020-10-14 08:15:51 +03: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-15 00:10:35 +03:00
await context . close ( ) ;
expect ( fs . existsSync ( path ) ) . toBeTruthy ( ) ;
2020-10-15 01:09:36 +03:00
} ) ;
2022-06-09 00:16:09 +03:00
it ( 'saveAs should throw when no video frames' , async ( { browser } , testInfo ) = > {
2021-03-31 20:38:05 +03: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-25 01:05:20 +03: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 20:38:05 +03:00
expect ( error . message ) . toContain ( 'Page did not produce any video frames' ) ;
2022-06-09 00:16:09 +03:00
await context . close ( ) ;
2021-03-31 20:38:05 +03:00
} ) ;
2021-09-27 19:58:08 +03:00
it ( 'should delete video' , async ( { browser } , testInfo ) = > {
2021-03-31 20:38:05 +03: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 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-03-31 20:38:05 +03:00
await context . close ( ) ;
const videoPath = await page . video ( ) . path ( ) ;
await deletePromise ;
expect ( fs . existsSync ( videoPath ) ) . toBeFalsy ( ) ;
} ) ;
2021-09-27 19:58:08 +03:00
it ( 'should expose video path blank page' , async ( { browser } , testInfo ) = > {
2020-10-15 01:09:36 +03:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : videosPath ,
size
} ,
2020-10-15 01:09:36 +03: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 19:28:35 +03: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 ( ) ;
} ) ;
2021-09-27 19:58:08 +03:00
it ( 'should expose video path blank popup' , async ( { browser } , testInfo ) = > {
2020-10-15 01:09:36 +03:00
const videosPath = testInfo . outputPath ( '' ) ;
const size = { width : 320 , height : 240 } ;
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : videosPath ,
size
} ,
2020-10-15 01:09:36 +03: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-14 08:15:51 +03:00
} ) ;
2022-02-10 20:45:18 +03:00
it ( 'should capture navigation' , async ( { browser , browserName , server , trace } , testInfo ) = > {
2020-09-19 03:36:43 +03:00
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size : { width : 1280 , height : 720 }
} ,
2020-09-19 03:36:43 +03:00
} ) ;
2020-09-12 04:58:53 +03:00
const page = await context . newPage ( ) ;
2020-08-28 23:53:47 +03:00
await page . goto ( server . PREFIX + '/background-color.html#rgb(0,0,0)' ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-08-28 23:53:47 +03:00
await page . goto ( server . CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)' ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-09-19 03:36:43 +03:00
await context . close ( ) ;
2020-08-28 23:53:47 +03:00
2020-10-20 00:35:18 +03:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-30 04:52:30 +03:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
2020-08-28 23:53:47 +03:00
expect ( duration ) . toBeGreaterThan ( 0 ) ;
{
2020-09-30 04:52:30 +03:00
const pixels = videoPlayer . seekFirstNonEmptyFrame ( ) . data ;
2020-08-28 23:53:47 +03:00
expectAll ( pixels , almostBlack ) ;
}
2020-08-25 03:23:54 +03:00
2020-08-28 23:53:47 +03:00
{
2020-09-30 04:52:30 +03:00
const pixels = videoPlayer . seekLastFrame ( ) . data ;
2021-01-06 07:31:50 +03:00
expectAll ( pixels , almostGray ) ;
2020-08-28 23:53:47 +03:00
}
} ) ;
2022-02-10 20:45:18 +03:00
it ( 'should capture css transformation' , async ( { browser , server , headless , browserName , platform , trace } , testInfo ) = > {
2021-05-13 20:22:23 +03:00
it . fixme ( ! headless , 'Fails on headed' ) ;
it . fixme ( browserName === 'webkit' && platform === 'win32' ) ;
2021-04-05 19:18:56 +03:00
2022-06-02 00:21:00 +03:00
const size = { width : 600 , height : 400 } ;
2020-09-02 20:40:50 +03:00
// Set viewport equal to screencast frame size to avoid scaling.
2020-09-19 03:36:43 +03:00
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
2020-09-19 03:36:43 +03:00
viewport : size ,
} ) ;
2020-09-12 04:58:53 +03:00
const page = await context . newPage ( ) ;
2020-08-28 23:53:47 +03:00
await page . goto ( server . PREFIX + '/rotate-z.html' ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-09-19 03:36:43 +03:00
await context . close ( ) ;
2020-08-28 23:53:47 +03:00
2020-10-20 00:35:18 +03:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-30 04:52:30 +03:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
2020-08-28 23:53:47 +03:00
expect ( duration ) . toBeGreaterThan ( 0 ) ;
{
2020-09-30 04:52:30 +03:00
const pixels = videoPlayer . seekLastFrame ( { x : 95 , y : 45 } ) . data ;
2020-08-28 23:53:47 +03:00
expectAll ( pixels , almostRed ) ;
}
} ) ;
2022-02-10 20:45:18 +03:00
it ( 'should work for popups' , async ( { browser , server , browserName , trace } , testInfo ) = > {
2022-06-02 00:21:00 +03:00
it . fixme ( browserName === 'firefox' , 'https://github.com/microsoft/playwright/issues/14557' ) ;
2020-10-06 03:03:24 +03:00
const videosPath = testInfo . outputPath ( '' ) ;
2022-06-02 00:21:00 +03:00
const size = { width : 600 , height : 400 } ;
2020-09-19 03:36:43 +03:00
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : videosPath ,
2020-11-08 05:21:26 +03:00
size ,
2020-11-03 06:42:05 +03:00
} ,
2020-11-08 05:21:26 +03:00
viewport : size ,
2020-09-19 03:36:43 +03:00
} ) ;
2020-09-01 01:21:02 +03:00
2020-09-09 03:01:00 +03:00
const page = await context . newPage ( ) ;
2020-09-05 08:37:38 +03:00
await page . goto ( server . EMPTY_PAGE ) ;
2020-11-08 05:21:26 +03:00
const [ popup ] = await Promise . all ( [
2020-09-19 03:36:43 +03:00
page . waitForEvent ( 'popup' ) ,
page . evaluate ( ( ) = > { window . open ( 'about:blank' ) ; } ) ,
2020-08-28 23:53:47 +03:00
] ) ;
2020-11-08 05:21:26 +03:00
await popup . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 19:22:18 +03:00
await Promise . all ( [
waitForRafs ( page , 100 ) ,
waitForRafs ( popup , 100 ) ,
] ) ;
2020-09-19 03:36:43 +03:00
await context . close ( ) ;
2020-11-08 05:21:26 +03:00
const pageVideoFile = await page . video ( ) . path ( ) ;
const popupVideoFile = await popup . video ( ) . path ( ) ;
expect ( pageVideoFile ) . not . toEqual ( popupVideoFile ) ;
expectRedFrames ( popupVideoFile , size ) ;
2020-10-01 21:06:19 +03:00
const videoFiles = findVideos ( videosPath ) ;
2020-09-19 03:36:43 +03:00
expect ( videoFiles . length ) . toBe ( 2 ) ;
2020-08-28 23:53:47 +03:00
} ) ;
2020-09-02 23:59:15 +03:00
2022-02-10 20:45:18 +03:00
it ( 'should scale frames down to the requested size ' , async ( { browser , browserName , server , headless , trace } , testInfo ) = > {
2021-05-13 20:22:23 +03:00
it . fixme ( ! headless , 'Fails on headed' ) ;
2021-04-05 19:18:56 +03:00
2020-09-12 04:58:53 +03:00
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
// Set size to 1/2 of the viewport.
size : { width : 320 , height : 240 } ,
} ,
2021-09-27 19:58:08 +03:00
viewport : { width : 640 , height : 480 } ,
2020-09-12 04:58:53 +03:00
} ) ;
const page = await context . newPage ( ) ;
2020-09-02 23:59:15 +03: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 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-09-02 23:59:15 +03:00
await page . $eval ( '.container' , container = > {
container . firstElementChild . classList . add ( 'red' ) ;
} ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-09-19 03:36:43 +03:00
await context . close ( ) ;
2020-09-02 23:59:15 +03:00
2020-10-20 00:35:18 +03:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-30 04:52:30 +03:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
2020-09-02 23:59:15 +03:00
expect ( duration ) . toBeGreaterThan ( 0 ) ;
{
2021-09-27 19:58:08 +03:00
const pixels = videoPlayer . seekLastFrame ( { x : 0 , y : 0 } ) . data ;
2020-09-02 23:59:15 +03:00
expectAll ( pixels , almostRed ) ;
}
{
2021-09-27 19:58:08 +03:00
const pixels = videoPlayer . seekLastFrame ( { x : 300 , y : 0 } ) . data ;
2021-01-06 07:31:50 +03:00
expectAll ( pixels , almostGray ) ;
2020-09-02 23:59:15 +03:00
}
{
2021-09-27 19:58:08 +03:00
const pixels = videoPlayer . seekLastFrame ( { x : 0 , y : 200 } ) . data ;
2021-01-06 07:31:50 +03:00
expectAll ( pixels , almostGray ) ;
2020-09-02 23:59:15 +03:00
}
{
2021-09-27 19:58:08 +03:00
const pixels = videoPlayer . seekLastFrame ( { x : 300 , y : 200 } ) . data ;
2020-09-02 23:59:15 +03:00
expectAll ( pixels , almostRed ) ;
}
} ) ;
2020-09-12 01:14:31 +03:00
2021-09-27 19:58:08 +03: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-19 03:36:43 +03:00
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
2020-09-19 03:36:43 +03:00
viewport : size ,
} ) ;
2020-09-12 01:14:31 +03:00
2020-10-20 00:35:18 +03:00
const page = await context . newPage ( ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-09-19 03:36:43 +03:00
await context . close ( ) ;
2020-09-12 01:14:31 +03:00
2020-10-20 00:35:18 +03:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-30 04:52:30 +03:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
2021-02-08 21:59:48 +03:00
expect ( videoPlayer . videoWidth ) . toBe ( 800 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 600 ) ;
2020-09-12 01:14:31 +03:00
} ) ;
2021-04-05 19:18:56 +03:00
it ( 'should be 800x450 by default' , async ( { browser } , testInfo ) = > {
2020-09-19 03:36:43 +03:00
const context = await browser . newContext ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
2020-09-19 03:36:43 +03:00
} ) ;
2020-09-12 01:14:31 +03:00
2020-10-20 00:35:18 +03:00
const page = await context . newPage ( ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-09-19 03:36:43 +03:00
await context . close ( ) ;
2020-09-12 01:14:31 +03:00
2020-10-20 00:35:18 +03:00
const videoFile = await page . video ( ) . path ( ) ;
2020-09-30 04:52:30 +03:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
2021-02-08 21:59:48 +03:00
expect ( videoPlayer . videoWidth ) . toBe ( 800 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 450 ) ;
} ) ;
2021-05-13 20:22:23 +03: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 19:18:56 +03:00
2021-02-08 21:59:48 +03:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
viewport : null
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-02-08 21:59:48 +03: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-12 01:14:31 +03:00
} ) ;
2020-10-01 21:06:19 +03:00
2023-03-10 03:56:29 +03: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-02 00:21:00 +03:00
const size = { width : 600 , height : 400 } ;
2020-10-01 21:06:19 +03:00
const { context , page } = await launchPersistent ( {
2020-11-03 06:42:05 +03:00
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
2020-10-01 21:06:19 +03:00
viewport : size ,
} ) ;
await page . evaluate ( ( ) = > document . body . style . backgroundColor = 'red' ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2020-10-01 21:06:19 +03:00
await context . close ( ) ;
2020-10-20 00:35:18 +03:00
const videoFile = await page . video ( ) . path ( ) ;
2020-10-01 21:06:19 +03:00
const videoPlayer = new VideoPlayer ( videoFile ) ;
const duration = videoPlayer . duration ;
expect ( duration ) . toBeGreaterThan ( 0 ) ;
2022-06-02 00:21:00 +03:00
expect ( videoPlayer . videoWidth ) . toBe ( 600 ) ;
expect ( videoPlayer . videoHeight ) . toBe ( 400 ) ;
2020-10-01 21:06:19 +03:00
{
const pixels = videoPlayer . seekLastFrame ( ) . data ;
expectAll ( pixels , almostRed ) ;
}
} ) ;
2021-02-11 00:37:27 +03:00
2021-10-28 18:31:30 +03:00
it ( 'should emulate an iphone' , async ( { contextFactory , playwright , browserName } , testInfo ) = > {
2021-04-05 19:18:56 +03:00
it . skip ( browserName === 'firefox' , 'isMobile is not supported in Firefox' ) ;
2021-02-11 00:37:27 +03:00
const device = playwright . devices [ 'iPhone 6' ] ;
const context = await contextFactory ( {
. . . device ,
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
} ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-02-11 00:37:27 +03: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-09 00:01:05 +03:00
2021-10-28 18:31:30 +03:00
it ( 'should throw on browser close' , async ( { browserType } , testInfo ) = > {
2021-04-09 00:01:05 +03:00
const size = { width : 320 , height : 240 } ;
2021-10-28 05:00:06 +03:00
const browser = await browserType . launch ( ) ;
2021-04-09 00:01:05 +03:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-04-09 00:01:05 +03: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-09 04:56:09 +03:00
2021-10-28 18:31:30 +03:00
it ( 'should throw if browser dies' , async ( { browserType } , testInfo ) = > {
2021-04-09 04:56:09 +03:00
const size = { width : 320 , height : 240 } ;
2021-10-28 05:00:06 +03:00
const browser = await browserType . launch ( ) ;
2021-04-09 04:56:09 +03:00
const context = await browser . newContext ( {
recordVideo : {
dir : testInfo.outputPath ( '' ) ,
size ,
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-04-09 04:56:09 +03: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 08:29:39 +03:00
2021-10-28 18:31:30 +03:00
it ( 'should wait for video to finish if page was closed' , async ( { browserType } , testInfo ) = > {
2021-05-19 08:29:39 +03:00
const size = { width : 320 , height : 240 } ;
2021-10-28 05:00:06 +03:00
const browser = await browserType . launch ( ) ;
2021-05-19 08:29:39 +03:00
const videoDir = testInfo . outputPath ( '' ) ;
const context = await browser . newContext ( {
recordVideo : {
dir : videoDir ,
size ,
} ,
viewport : size ,
} ) ;
const page = await context . newPage ( ) ;
2022-05-19 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-05-19 08:29:39 +03: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 18:31:30 +03:00
it ( 'should not create video for internal pages' , async ( { browser , server } , testInfo ) = > {
2021-05-29 04:20:49 +03: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 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-05-29 04:20:49 +03: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-09 05:13:12 +03:00
2023-05-23 22:04:44 +03:00
it ( 'should capture full viewport' , async ( { browserType , browserName , headless , isWindows } , testInfo ) = > {
2023-04-18 19:21:20 +03:00
it . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/22411' } ) ;
2023-06-02 21:40:12 +03:00
it . fixme ( browserName === 'chromium' && ( ! headless || ! ! process . env . PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW ) , 'The square is not on the video' ) ;
2022-06-09 03:55:30 +03:00
it . fixme ( browserName === 'firefox' && isWindows , 'https://github.com/microsoft/playwright/issues/14405' ) ;
2022-06-02 00:21:00 +03:00
const size = { width : 600 , height : 400 } ;
2022-05-25 18:45:22 +03: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 21:53:31 +03: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 18:45:22 +03:00
expectAll ( pixels , almostRed ) ;
} ) ;
2023-04-26 23:11:02 +03:00
it ( 'should capture full viewport on hidpi' , async ( { browserType , browserName , headless , isWindows , isLinux } , testInfo ) = > {
2023-04-18 19:21:20 +03:00
it . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/22411' } ) ;
2023-06-02 21:40:12 +03:00
it . fixme ( browserName === 'chromium' && ( ! headless || ! ! process . env . PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW ) , 'The square is not on the video' ) ;
2022-06-09 03:55:30 +03:00
it . fixme ( browserName === 'firefox' && isWindows , 'https://github.com/microsoft/playwright/issues/14405' ) ;
2023-09-02 03:26:23 +03:00
it . fixme ( browserName === 'webkit' && isLinux && ! headless , 'https://github.com/microsoft/playwright/issues/22617' ) ;
2022-06-02 00:21:00 +03:00
const size = { width : 600 , height : 400 } ;
2021-12-09 05:13:12 +03: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 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-12-09 05:13:12 +03: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 21:53:31 +03: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-09 05:13:12 +03:00
expectAll ( pixels , almostRed ) ;
} ) ;
2023-01-12 21:44:20 +03:00
2023-01-24 02:26:05 +03:00
it ( 'should work with video+trace' , async ( { browser , trace , headless } , testInfo ) = > {
2023-01-12 21:44:20 +03:00
it . skip ( trace === 'on' ) ;
2023-06-02 21:40:12 +03:00
it . fixme ( ! headless || ! ! process . env . PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW , 'different trace screencast image size on all browsers' ) ;
2023-01-12 21:44:20 +03:00
const size = { width : 500 , height : 400 } ;
const traceFile = testInfo . outputPath ( 'trace.zip' ) ;
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-06 01:12:18 +03:00
const { events , resources } = await parseTraceRaw ( traceFile ) ;
2023-01-12 21:44:20 +03: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 20:41:03 +03:00
} ) ;
2021-05-29 04:20:49 +03:00
2021-10-14 20:41:03 +03: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 19:22:18 +03:00
await waitForRafs ( page , 100 ) ;
2021-10-14 20:41:03 +03:00
await context . close ( ) ;
const saveAsPath = testInfo . outputPath ( 'my-video.webm' ) ;
await page . video ( ) . saveAs ( saveAsPath ) ;
expect ( fs . existsSync ( saveAsPath ) ) . toBeTruthy ( ) ;
2020-09-19 03:36:43 +03:00
} ) ;
2022-05-19 19:22:18 +03: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
requestAnimationFrame ( onRaf ) ;
} ;
requestAnimationFrame ( onRaf ) ;
} ) , count ) ;
}