2021-02-24 23:22:34 +01:00
/ * *
* Copyright ( c ) Microsoft Corporation .
*
* 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 .
* /
2024-08-05 14:42:29 +02:00
import { escapeHTMLAttribute , escapeHTML } from '@isomorphic/stringUtils' ;
2024-05-08 20:08:40 +02:00
import type { FrameSnapshot , NodeNameAttributesChildNodesSnapshot , NodeSnapshot , RenderedFrameSnapshot , ResourceSnapshot , SubtreeReferenceSnapshot } from '@trace/snapshot' ;
2024-10-22 14:12:25 +02:00
import type { PageEntry } from '../types/entries' ;
2024-10-23 19:25:16 +02:00
import type { LRUCache } from './lruCache' ;
2024-10-22 14:12:25 +02:00
function findClosest < T > ( items : T [ ] , metric : ( v : T ) = > number , target : number ) {
return items . find ( ( item , index ) = > {
if ( index === items . length - 1 )
return true ;
const next = items [ index + 1 ] ;
return Math . abs ( metric ( item ) - target ) < Math . abs ( metric ( next ) - target ) ;
} ) ;
}
2024-05-08 20:08:40 +02:00
function isNodeNameAttributesChildNodesSnapshot ( n : NodeSnapshot ) : n is NodeNameAttributesChildNodesSnapshot {
return Array . isArray ( n ) && typeof n [ 0 ] === 'string' ;
}
function isSubtreeReferenceSnapshot ( n : NodeSnapshot ) : n is SubtreeReferenceSnapshot {
return Array . isArray ( n ) && Array . isArray ( n [ 0 ] ) ;
}
2021-02-24 23:22:34 +01:00
2021-02-25 18:33:32 +01:00
export class SnapshotRenderer {
2024-10-23 19:25:16 +02:00
private _htmlCache : LRUCache < SnapshotRenderer , string > ;
2021-02-25 18:33:32 +01:00
private _snapshots : FrameSnapshot [ ] ;
2021-02-24 23:22:34 +01:00
private _index : number ;
2021-03-09 04:49:57 +01:00
readonly snapshotName : string | undefined ;
2023-03-22 17:32:21 +01:00
private _resources : ResourceSnapshot [ ] ;
2021-08-11 06:23:31 +02:00
private _snapshot : FrameSnapshot ;
2023-03-16 06:33:40 +01:00
private _callId : string ;
2024-10-22 14:12:25 +02:00
private _screencastFrames : PageEntry [ 'screencastFrames' ] ;
2021-02-24 23:22:34 +01:00
2024-10-23 19:25:16 +02:00
constructor ( htmlCache : LRUCache < SnapshotRenderer , string > , resources : ResourceSnapshot [ ] , snapshots : FrameSnapshot [ ] , screencastFrames : PageEntry [ 'screencastFrames' ] , index : number ) {
this . _htmlCache = htmlCache ;
2021-08-10 21:08:19 +02:00
this . _resources = resources ;
2021-02-25 18:33:32 +01:00
this . _snapshots = snapshots ;
2021-02-24 23:22:34 +01:00
this . _index = index ;
2021-08-11 06:23:31 +02:00
this . _snapshot = snapshots [ index ] ;
2023-03-16 06:33:40 +01:00
this . _callId = snapshots [ index ] . callId ;
2024-10-22 14:12:25 +02:00
this . _screencastFrames = screencastFrames ;
2021-03-09 04:49:57 +01:00
this . snapshotName = snapshots [ index ] . snapshotName ;
}
snapshot ( ) : FrameSnapshot {
return this . _snapshots [ this . _index ] ;
2021-02-24 23:22:34 +01:00
}
2021-08-11 02:06:14 +02:00
viewport ( ) : { width : number , height : number } {
return this . _snapshots [ this . _index ] . viewport ;
}
2024-10-22 14:12:25 +02:00
closestScreenshot ( ) : string | undefined {
const { wallTime , timestamp } = this . snapshot ( ) ;
const closestFrame = ( wallTime && this . _screencastFrames [ 0 ] ? . frameSwapWallTime )
? findClosest ( this . _screencastFrames , frame = > frame . frameSwapWallTime ! , wallTime )
: findClosest ( this . _screencastFrames , frame = > frame . timestamp , timestamp ) ;
return closestFrame ? . sha1 ;
}
2021-02-25 18:33:32 +01:00
render ( ) : RenderedFrameSnapshot {
2024-09-04 09:57:15 +02:00
const result : string [ ] = [ ] ;
const visit = ( n : NodeSnapshot , snapshotIndex : number , parentTag : string | undefined , parentAttrs : [ string , string ] [ ] | undefined ) = > {
2021-02-24 23:22:34 +01:00
// Text node.
2022-04-06 01:10:12 +02:00
if ( typeof n === 'string' ) {
// Best-effort Electron support: rewrite custom protocol in url() links in stylesheets.
// Old snapshotter was sending lower-case.
if ( parentTag === 'STYLE' || parentTag === 'style' )
2024-09-04 09:57:15 +02:00
result . push ( rewriteURLsInStyleSheetForCustomProtocol ( n ) ) ;
else
result . push ( escapeHTML ( n ) ) ;
return ;
2022-04-06 01:10:12 +02:00
}
2021-02-24 23:22:34 +01:00
2024-09-04 09:57:15 +02:00
if ( isSubtreeReferenceSnapshot ( n ) ) {
// Node reference.
const referenceIndex = snapshotIndex - n [ 0 ] [ 0 ] ;
if ( referenceIndex >= 0 && referenceIndex <= snapshotIndex ) {
const nodes = snapshotNodes ( this . _snapshots [ referenceIndex ] ) ;
const nodeIndex = n [ 0 ] [ 1 ] ;
if ( nodeIndex >= 0 && nodeIndex < nodes . length )
return visit ( nodes [ nodeIndex ] , referenceIndex , parentTag , parentAttrs ) ;
}
} else if ( isNodeNameAttributesChildNodesSnapshot ( n ) ) {
const [ name , nodeAttrs , . . . children ] = n ;
// Element node.
// Note that <noscript> will not be rendered by default in the trace viewer, because
// JS is enabled. So rename it to <x-noscript>.
const nodeName = name === 'NOSCRIPT' ? 'X-NOSCRIPT' : name ;
const attrs = Object . entries ( nodeAttrs || { } ) ;
result . push ( '<' , nodeName ) ;
const kCurrentSrcAttribute = '__playwright_current_src__' ;
const isFrame = nodeName === 'IFRAME' || nodeName === 'FRAME' ;
const isAnchor = nodeName === 'A' ;
const isImg = nodeName === 'IMG' ;
const isImgWithCurrentSrc = isImg && attrs . some ( a = > a [ 0 ] === kCurrentSrcAttribute ) ;
const isSourceInsidePictureWithCurrentSrc = nodeName === 'SOURCE' && parentTag === 'PICTURE' && parentAttrs ? . some ( a = > a [ 0 ] === kCurrentSrcAttribute ) ;
for ( const [ attr , value ] of attrs ) {
let attrName = attr ;
if ( isFrame && attr . toLowerCase ( ) === 'src' ) {
// Never set relative URLs as <iframe src> - they start fetching frames immediately.
attrName = '__playwright_src__' ;
}
if ( isImg && attr === kCurrentSrcAttribute ) {
// Render currentSrc for images, so that trace viewer does not accidentally
// resolve srcset to a different source.
attrName = 'src' ;
2021-02-24 23:22:34 +01:00
}
2024-09-04 09:57:15 +02:00
if ( [ 'src' , 'srcset' ] . includes ( attr . toLowerCase ( ) ) && ( isImgWithCurrentSrc || isSourceInsidePictureWithCurrentSrc ) ) {
// Disable actual <img src>, <img srcset>, <source src> and <source srcset> if
// we will be using the currentSrc instead.
attrName = '_' + attrName ;
2021-11-23 20:36:18 +01:00
}
2024-09-04 09:57:15 +02:00
let attrValue = value ;
if ( isAnchor && attr . toLowerCase ( ) === 'href' )
attrValue = 'link://' + value ;
else if ( attr . toLowerCase ( ) === 'href' || attr . toLowerCase ( ) === 'src' || attr === kCurrentSrcAttribute )
attrValue = rewriteURLForCustomProtocol ( value ) ;
result . push ( ' ' , attrName , '="' , escapeHTMLAttribute ( attrValue ) , '"' ) ;
2021-02-24 23:22:34 +01:00
}
2024-09-04 09:57:15 +02:00
result . push ( '>' ) ;
for ( const child of children )
visit ( child , snapshotIndex , nodeName , attrs ) ;
if ( ! autoClosing . has ( nodeName ) )
result . push ( '</' , nodeName , '>' ) ;
return ;
} else {
// Why are we here? Let's not throw, just in case.
return ;
2021-02-24 23:22:34 +01:00
}
} ;
2021-08-11 06:23:31 +02:00
const snapshot = this . _snapshot ;
2024-10-23 19:25:16 +02:00
const html = this . _htmlCache . getOrCompute ( this , ( ) = > {
2024-09-04 09:57:15 +02:00
visit ( snapshot . html , this . _index , undefined , undefined ) ;
const prefix = snapshot . doctype ? ` <!DOCTYPE ${ snapshot . doctype } > ` : '' ;
2024-10-23 19:25:16 +02:00
const html = prefix + [
// Hide the document in order to prevent flickering. We will unhide once script has processed shadow.
2024-09-04 09:57:15 +02:00
'<style>*,*::before,*::after { visibility: hidden }</style>' ,
` <script> ${ snapshotScript ( this . _callId , this . snapshotName ) } </script> `
2024-10-23 19:25:16 +02:00
] . join ( '' ) + result . join ( '' ) ;
return { value : html , size : html.length } ;
2024-09-04 09:57:15 +02:00
} ) ;
2021-02-24 23:22:34 +01:00
2021-08-11 06:23:31 +02:00
return { html , pageId : snapshot.pageId , frameId : snapshot.frameId , index : this._index } ;
}
2023-07-11 05:04:48 +02:00
resourceByUrl ( url : string , method : string ) : ResourceSnapshot | undefined {
2021-08-11 06:23:31 +02:00
const snapshot = this . _snapshot ;
2023-07-27 17:06:00 +02:00
let sameFrameResource : ResourceSnapshot | undefined ;
let otherFrameResource : ResourceSnapshot | undefined ;
2021-08-11 06:23:31 +02:00
2021-08-10 21:08:19 +02:00
for ( const resource of this . _resources ) {
2023-07-11 05:04:48 +02:00
// Only use resources that received response before the snapshot.
// Note that both snapshot time and request time are taken in the same Node process.
2022-06-22 23:44:12 +02:00
if ( typeof resource . _monotonicTime === 'number' && resource . _monotonicTime >= snapshot . timestamp )
2021-08-10 21:08:19 +02:00
break ;
2023-07-27 17:06:00 +02:00
if ( resource . response . status === 304 ) {
// "Not Modified" responses are issued when browser requests the same resource
// multiple times, meanwhile indicating that it has the response cached.
//
// When rendering the snapshot, browser most likely will not have the resource cached,
// so we should respond with the real content instead, picking the last response that
// is not 304.
2021-08-10 21:08:19 +02:00
continue ;
2023-07-27 17:06:00 +02:00
}
2023-07-11 05:04:48 +02:00
if ( resource . request . url === url && resource . request . method === method ) {
2023-06-17 15:58:16 +02:00
// Pick the last resource with matching url - most likely it was used
// at the time of snapshot, not the earlier aborted resource with the same url.
2023-07-27 17:06:00 +02:00
if ( resource . _frameref === snapshot . frameId )
sameFrameResource = resource ;
else
otherFrameResource = resource ;
2021-08-11 06:23:31 +02:00
}
2021-02-25 03:38:04 +01:00
}
2021-08-11 06:23:31 +02:00
2023-07-27 17:06:00 +02:00
// First try locating exact resource belonging to this frame,
// then fall back to resource with this URL to account for memory cache.
let result = sameFrameResource ? ? otherFrameResource ;
2023-07-11 05:04:48 +02:00
if ( result && method . toUpperCase ( ) === 'GET' ) {
2021-08-11 06:23:31 +02:00
// Patch override if necessary.
for ( const o of snapshot . resourceOverrides ) {
if ( url === o . url && o . sha1 ) {
2021-08-24 22:17:58 +02:00
result = {
. . . result ,
response : {
. . . result . response ,
content : {
. . . result . response . content ,
_sha1 : o.sha1 ,
}
} ,
} ;
2021-08-11 06:23:31 +02:00
break ;
}
}
2021-02-25 03:38:04 +01:00
}
2021-08-11 06:23:31 +02:00
return result ;
2021-02-24 23:22:34 +01:00
}
}
const autoClosing = new Set ( [ 'AREA' , 'BASE' , 'BR' , 'COL' , 'COMMAND' , 'EMBED' , 'HR' , 'IMG' , 'INPUT' , 'KEYGEN' , 'LINK' , 'MENUITEM' , 'META' , 'PARAM' , 'SOURCE' , 'TRACK' , 'WBR' ] ) ;
2021-02-25 18:33:32 +01:00
function snapshotNodes ( snapshot : FrameSnapshot ) : NodeSnapshot [ ] {
2021-02-24 23:22:34 +01:00
if ( ! ( snapshot as any ) . _nodes ) {
2021-02-25 18:33:32 +01:00
const nodes : NodeSnapshot [ ] = [ ] ;
const visit = ( n : NodeSnapshot ) = > {
2021-02-24 23:22:34 +01:00
if ( typeof n === 'string' ) {
nodes . push ( n ) ;
2024-05-08 20:08:40 +02:00
} else if ( isNodeNameAttributesChildNodesSnapshot ( n ) ) {
const [ , , . . . children ] = n ;
for ( const child of children )
visit ( child ) ;
2021-02-24 23:22:34 +01:00
nodes . push ( n ) ;
}
} ;
visit ( snapshot . html ) ;
( snapshot as any ) . _nodes = nodes ;
}
return ( snapshot as any ) . _nodes ;
}
2021-02-25 17:25:52 +01:00
2023-08-31 21:46:49 +02:00
function snapshotScript ( . . . targetIds : ( string | undefined ) [ ] ) {
function applyPlaywrightAttributes ( unwrapPopoutUrl : ( url : string ) = > string , . . . targetIds : ( string | undefined ) [ ] ) {
2024-10-22 14:12:25 +02:00
const isUnderTest = new URLSearchParams ( location . search ) . has ( 'isUnderTest' ) ;
2024-02-06 17:27:45 +01:00
const kPointerWarningTitle = 'Recorded click position in absolute coordinates did not' +
' match the center of the clicked element. This is likely due to a difference between' +
' the test runner and the trace viewer operating systems.' ;
2021-02-25 17:25:52 +01:00
const scrollTops : Element [ ] = [ ] ;
const scrollLefts : Element [ ] = [ ] ;
2023-09-01 01:52:54 +02:00
const targetElements : Element [ ] = [ ] ;
2024-10-22 14:12:25 +02:00
const canvasElements : HTMLCanvasElement [ ] = [ ] ;
2021-02-25 17:25:52 +01:00
const visit = ( root : Document | ShadowRoot ) = > {
// Collect all scrolled elements for later use.
2022-03-22 02:51:48 +01:00
for ( const e of root . querySelectorAll ( ` [__playwright_scroll_top_] ` ) )
2021-02-25 17:25:52 +01:00
scrollTops . push ( e ) ;
2022-03-22 02:51:48 +01:00
for ( const e of root . querySelectorAll ( ` [__playwright_scroll_left_] ` ) )
2021-02-25 17:25:52 +01:00
scrollLefts . push ( e ) ;
2022-03-22 02:51:48 +01:00
for ( const element of root . querySelectorAll ( ` [__playwright_value_] ` ) ) {
2023-12-01 18:38:50 +01:00
const inputElement = element as HTMLInputElement | HTMLTextAreaElement ;
if ( inputElement . type !== 'file' )
inputElement . value = inputElement . getAttribute ( '__playwright_value_' ) ! ;
2022-03-22 02:51:48 +01:00
element . removeAttribute ( '__playwright_value_' ) ;
}
for ( const element of root . querySelectorAll ( ` [__playwright_checked_] ` ) ) {
( element as HTMLInputElement ) . checked = element . getAttribute ( '__playwright_checked_' ) === 'true' ;
element . removeAttribute ( '__playwright_checked_' ) ;
}
for ( const element of root . querySelectorAll ( ` [__playwright_selected_] ` ) ) {
( element as HTMLOptionElement ) . selected = element . getAttribute ( '__playwright_selected_' ) === 'true' ;
element . removeAttribute ( '__playwright_selected_' ) ;
}
2023-08-31 21:46:49 +02:00
for ( const targetId of targetIds ) {
for ( const target of root . querySelectorAll ( ` [__playwright_target__=" ${ targetId } "] ` ) ) {
const style = ( target as HTMLElement ) . style ;
style . outline = '2px solid #006ab1' ;
style . backgroundColor = '#6fa8dc7f' ;
2023-09-01 01:52:54 +02:00
targetElements . push ( target ) ;
2023-08-31 21:46:49 +02:00
}
}
2021-12-17 19:43:19 +01:00
for ( const iframe of root . querySelectorAll ( 'iframe, frame' ) ) {
2021-11-23 20:36:18 +01:00
const src = iframe . getAttribute ( '__playwright_src__' ) ;
2021-03-09 04:49:57 +01:00
if ( ! src ) {
2021-03-11 20:22:59 +01:00
iframe . setAttribute ( 'src' , 'data:text/html,<body style="background: #ddd"></body>' ) ;
2021-03-09 04:49:57 +01:00
} else {
2023-12-07 15:27:49 +01:00
// Retain query parameters to inherit name=, time=, pointX=, pointY= and other values from parent.
2023-01-31 04:07:52 +01:00
const url = new URL ( unwrapPopoutUrl ( window . location . href ) ) ;
2021-11-23 20:36:18 +01:00
// We can be loading iframe from within iframe, reset base to be absolute.
const index = url . pathname . lastIndexOf ( '/snapshot/' ) ;
if ( index !== - 1 )
url . pathname = url . pathname . substring ( 0 , index + 1 ) ;
url . pathname += src . substring ( 1 ) ;
2021-10-20 00:36:17 +02:00
iframe . setAttribute ( 'src' , url . toString ( ) ) ;
2021-03-09 04:49:57 +01:00
}
2021-02-25 17:25:52 +01:00
}
2023-02-23 06:53:27 +01:00
{
const body = root . querySelector ( ` body[__playwright_custom_elements__] ` ) ;
if ( body && window . customElements ) {
const customElements = ( body . getAttribute ( '__playwright_custom_elements__' ) || '' ) . split ( ',' ) ;
for ( const elementName of customElements )
window . customElements . define ( elementName , class extends HTMLElement { } ) ;
}
}
2022-03-22 02:51:48 +01:00
for ( const element of root . querySelectorAll ( ` template[__playwright_shadow_root_] ` ) ) {
2021-02-25 17:25:52 +01:00
const template = element as HTMLTemplateElement ;
const shadowRoot = template . parentElement ! . attachShadow ( { mode : 'open' } ) ;
shadowRoot . appendChild ( template . content ) ;
template . remove ( ) ;
visit ( shadowRoot ) ;
}
2021-06-17 18:41:29 +02:00
if ( 'adoptedStyleSheets' in ( root as any ) ) {
const adoptedSheets : CSSStyleSheet [ ] = [ . . . ( root as any ) . adoptedStyleSheets ] ;
2022-03-22 02:51:48 +01:00
for ( const element of root . querySelectorAll ( ` template[__playwright_style_sheet_] ` ) ) {
2021-06-17 18:41:29 +02:00
const template = element as HTMLTemplateElement ;
const sheet = new CSSStyleSheet ( ) ;
2022-03-22 02:51:48 +01:00
( sheet as any ) . replaceSync ( template . getAttribute ( '__playwright_style_sheet_' ) ) ;
2021-06-17 18:41:29 +02:00
adoptedSheets . push ( sheet ) ;
}
( root as any ) . adoptedStyleSheets = adoptedSheets ;
2021-06-23 11:08:35 +02:00
}
2024-10-22 14:12:25 +02:00
canvasElements . push ( . . . root . querySelectorAll ( 'canvas' ) ) ;
2021-02-25 17:25:52 +01:00
} ;
const onLoad = ( ) = > {
window . removeEventListener ( 'load' , onLoad ) ;
for ( const element of scrollTops ) {
2022-03-22 02:51:48 +01:00
element . scrollTop = + element . getAttribute ( '__playwright_scroll_top_' ) ! ;
element . removeAttribute ( '__playwright_scroll_top_' ) ;
2021-02-25 17:25:52 +01:00
}
for ( const element of scrollLefts ) {
2022-03-22 02:51:48 +01:00
element . scrollLeft = + element . getAttribute ( '__playwright_scroll_left_' ) ! ;
element . removeAttribute ( '__playwright_scroll_left_' ) ;
2021-02-25 17:25:52 +01:00
}
2021-10-13 06:21:06 +02:00
2023-09-01 01:52:54 +02:00
document . styleSheets [ 0 ] . disabled = true ;
2021-10-13 06:21:06 +02:00
const search = new URL ( window . location . href ) . searchParams ;
2024-10-22 14:12:25 +02:00
const isTopFrame = window . location . pathname . match ( /\/page@[a-z0-9]+$/ ) ;
2023-12-07 15:27:49 +01:00
if ( search . get ( 'pointX' ) && search . get ( 'pointY' ) ) {
const pointX = + search . get ( 'pointX' ) ! ;
const pointY = + search . get ( 'pointY' ) ! ;
2024-09-11 12:04:03 +02:00
const hasInputTarget = search . has ( 'hasInputTarget' ) ;
2023-12-07 15:27:49 +01:00
const hasTargetElements = targetElements . length > 0 ;
const roots = document . documentElement ? [ document . documentElement ] : [ ] ;
for ( const target of ( hasTargetElements ? targetElements : roots ) ) {
2023-09-01 01:52:54 +02:00
const pointElement = document . createElement ( 'x-pw-pointer' ) ;
pointElement . style . position = 'fixed' ;
pointElement . style . backgroundColor = '#f44336' ;
pointElement . style . width = '20px' ;
pointElement . style . height = '20px' ;
pointElement . style . borderRadius = '10px' ;
pointElement . style . margin = '-10px 0 0 -10px' ;
2023-11-01 23:56:49 +01:00
pointElement . style . zIndex = '2147483646' ;
2024-02-06 17:27:45 +01:00
pointElement . style . display = 'flex' ;
pointElement . style . alignItems = 'center' ;
pointElement . style . justifyContent = 'center' ;
2023-12-07 15:27:49 +01:00
if ( hasTargetElements ) {
// Sometimes there are layout discrepancies between recording and rendering, e.g. fonts,
// that may place the point at the wrong place. To avoid confusion, we just show the
// point in the middle of the target element.
const box = target . getBoundingClientRect ( ) ;
const centerX = ( box . left + box . width / 2 ) ;
const centerY = ( box . top + box . height / 2 ) ;
pointElement . style . left = centerX + 'px' ;
pointElement . style . top = centerY + 'px' ;
2024-02-06 17:27:45 +01:00
// "Warning symbol" indicates that action point is not 100% correct.
2024-09-11 12:04:03 +02:00
// Note that action point is relative to the top frame, so we can only compare in the top frame.
if ( isTopFrame && ( Math . abs ( centerX - pointX ) >= 10 || Math . abs ( centerY - pointY ) >= 10 ) ) {
2024-02-06 17:27:45 +01:00
const warningElement = document . createElement ( 'x-pw-pointer-warning' ) ;
warningElement . textContent = '⚠' ;
warningElement . style . fontSize = '19px' ;
warningElement . style . color = 'white' ;
warningElement . style . marginTop = '-3.5px' ;
warningElement . style . userSelect = 'none' ;
pointElement . appendChild ( warningElement ) ;
pointElement . setAttribute ( 'title' , kPointerWarningTitle ) ;
}
2024-09-11 12:04:03 +02:00
document . documentElement . appendChild ( pointElement ) ;
} else if ( isTopFrame && ! hasInputTarget ) {
2023-12-07 15:27:49 +01:00
// For actions without a target element, e.g. page.mouse.move(),
2024-09-11 12:04:03 +02:00
// show the point at the recorded location, which is relative to the top frame.
2023-12-07 15:27:49 +01:00
pointElement . style . left = pointX + 'px' ;
pointElement . style . top = pointY + 'px' ;
2024-09-11 12:04:03 +02:00
document . documentElement . appendChild ( pointElement ) ;
2023-12-07 15:27:49 +01:00
}
2023-09-01 01:52:54 +02:00
}
2021-10-13 06:21:06 +02:00
}
2024-10-22 14:12:25 +02:00
if ( canvasElements . length > 0 ) {
function drawCheckerboard ( context : CanvasRenderingContext2D , canvas : HTMLCanvasElement ) {
function createCheckerboardPattern() {
const pattern = document . createElement ( 'canvas' ) ;
pattern . width = pattern . width / Math . floor ( pattern . width / 24 ) ;
pattern . height = pattern . height / Math . floor ( pattern . height / 24 ) ;
const context = pattern . getContext ( '2d' ) ! ;
context . fillStyle = 'lightgray' ;
context . fillRect ( 0 , 0 , pattern . width , pattern . height ) ;
context . fillStyle = 'white' ;
context . fillRect ( 0 , 0 , pattern . width / 2 , pattern . height / 2 ) ;
context . fillRect ( pattern . width / 2 , pattern . height / 2 , pattern . width , pattern . height ) ;
return context . createPattern ( pattern , 'repeat' ) ! ;
}
context . fillStyle = createCheckerboardPattern ( ) ;
context . fillRect ( 0 , 0 , canvas . width , canvas . height ) ;
}
if ( ! isTopFrame ) {
for ( const canvas of canvasElements ) {
const context = canvas . getContext ( '2d' ) ! ;
drawCheckerboard ( context , canvas ) ;
canvas . title = ` Playwright displays canvas contents on a best-effort basis. It doesn't support canvas elements inside an iframe yet. If this impacts your workflow, please open an issue so we can prioritize. ` ;
}
return ;
}
const img = new Image ( ) ;
img . onload = ( ) = > {
for ( const canvas of canvasElements ) {
const context = canvas . getContext ( '2d' ) ! ;
const boundingRect = canvas . getBoundingClientRect ( ) ;
const xStart = boundingRect . left / window . innerWidth ;
const yStart = boundingRect . top / window . innerHeight ;
const xEnd = boundingRect . right / window . innerWidth ;
const yEnd = boundingRect . bottom / window . innerHeight ;
const partiallyUncaptured = xEnd > 1 || yEnd > 1 ;
const fullyUncaptured = xStart > 1 || yStart > 1 ;
if ( fullyUncaptured ) {
canvas . title = ` Playwright couldn't capture canvas contents because it's located outside the viewport. ` ;
continue ;
}
drawCheckerboard ( context , canvas ) ;
context . drawImage ( img , xStart * img . width , yStart * img . height , ( xEnd - xStart ) * img . width , ( yEnd - yStart ) * img . height , 0 , 0 , canvas . width , canvas . height ) ;
if ( isUnderTest )
// eslint-disable-next-line no-console
console . log ( ` canvas drawn: ` , JSON . stringify ( [ xStart , yStart , xEnd , yEnd ] . map ( v = > Math . floor ( v * 100 ) ) ) ) ;
if ( partiallyUncaptured )
canvas . title = ` Playwright couldn't capture full canvas contents because it's located partially outside the viewport. ` ;
else
canvas . title = ` Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution. ` ;
}
} ;
img . onerror = ( ) = > {
for ( const canvas of canvasElements ) {
const context = canvas . getContext ( '2d' ) ! ;
drawCheckerboard ( context , canvas ) ;
canvas . title = ` Playwright couldn't show canvas contents because the screenshot failed to load. ` ;
}
} ;
img . src = location . href . replace ( '/snapshot' , '/closest-screenshot' ) ;
}
2021-02-25 17:25:52 +01:00
} ;
2021-12-17 19:43:19 +01:00
const onDOMContentLoaded = ( ) = > visit ( document ) ;
2021-02-25 17:25:52 +01:00
window . addEventListener ( 'load' , onLoad ) ;
2021-12-17 19:43:19 +01:00
window . addEventListener ( 'DOMContentLoaded' , onDOMContentLoaded ) ;
2021-02-25 17:25:52 +01:00
}
2023-08-31 21:46:49 +02:00
return ` \ n( ${ applyPlaywrightAttributes . toString ( ) } )( ${ unwrapPopoutUrl . toString ( ) } ${ targetIds . map ( id = > ` , " ${ id } " ` ) . join ( '' ) } ) ` ;
2021-02-25 17:25:52 +01:00
}
2022-04-05 05:56:04 +02:00
2022-04-06 01:10:12 +02:00
/ * *
* Best - effort Electron support : rewrite custom protocol in DOM .
* vscode - file : //vscode-app/ -> https://pw-vscode-file--vscode-app/
* /
2022-08-18 20:12:33 +02:00
const schemas = [ 'about:' , 'blob:' , 'data:' , 'file:' , 'ftp:' , 'http:' , 'https:' , 'mailto:' , 'sftp:' , 'ws:' , 'wss:' ] ;
2022-04-05 05:56:04 +02:00
const kLegacyBlobPrefix = 'http://playwright.bloburl/#' ;
export function rewriteURLForCustomProtocol ( href : string ) : string {
// Legacy support, we used to prepend this to blobs, strip it away.
if ( href . startsWith ( kLegacyBlobPrefix ) )
href = href . substring ( kLegacyBlobPrefix . length ) ;
try {
const url = new URL ( href ) ;
// Sanitize URL.
2022-06-02 21:25:59 +02:00
if ( url . protocol === 'javascript:' || url . protocol === 'vbscript:' )
2022-04-05 05:56:04 +02:00
return 'javascript:void(0)' ;
// Pass through if possible.
const isBlob = url . protocol === 'blob:' ;
2024-06-10 11:44:52 +02:00
const isFile = url . protocol === 'file:' ;
if ( ! isBlob && ! isFile && schemas . includes ( url . protocol ) )
2022-04-05 05:56:04 +02:00
return href ;
2024-06-10 11:44:52 +02:00
// Rewrite blob, file and custom schemas.
2022-04-05 05:56:04 +02:00
const prefix = 'pw-' + url . protocol . slice ( 0 , url . protocol . length - 1 ) ;
2024-06-10 11:44:52 +02:00
if ( ! isFile )
url . protocol = 'https:' ;
2022-04-06 01:10:12 +02:00
url . hostname = url . hostname ? ` ${ prefix } -- ${ url . hostname } ` : prefix ;
2024-06-10 11:44:52 +02:00
if ( isFile ) {
// File URIs can only have their protocol changed after the hostname
// is set. (For all other URIs, we must set the protocol first.)
url . protocol = 'https:' ;
}
2022-04-05 05:56:04 +02:00
return url . toString ( ) ;
} catch {
return href ;
}
}
2022-04-06 01:10:12 +02:00
/ * *
* Best - effort Electron support : rewrite custom protocol in inline stylesheets .
* vscode - file : //vscode-app/ -> https://pw-vscode-file--vscode-app/
* /
const urlInCSSRegex = /url\(['"]?([\w-]+:)\/\//ig ;
function rewriteURLsInStyleSheetForCustomProtocol ( text : string ) : string {
return text . replace ( urlInCSSRegex , ( match : string , protocol : string ) = > {
const isBlob = protocol === 'blob:' ;
2024-06-10 11:44:52 +02:00
const isFile = protocol === 'file:' ;
if ( ! isBlob && ! isFile && schemas . includes ( protocol ) )
2022-04-06 01:10:12 +02:00
return match ;
return match . replace ( protocol + '//' , ` https://pw- ${ protocol . slice ( 0 , - 1 ) } -- ` ) ;
} ) ;
}
2023-01-31 04:07:52 +01:00
2023-06-21 21:10:50 +02:00
// <base>/snapshot.html?r=<snapshotUrl> is used for "pop out snapshot" feature.
2023-01-31 04:07:52 +01:00
export function unwrapPopoutUrl ( url : string ) {
const u = new URL ( url ) ;
2023-06-21 21:10:50 +02:00
if ( u . pathname . endsWith ( '/snapshot.html' ) )
2023-01-31 04:07:52 +01:00
return u . searchParams . get ( 'r' ) ! ;
return url ;
}