@@ -14,7 +14,7 @@ import {
1414} from 'electron/main' ;
1515
1616import { expect } from 'chai' ;
17- import * as ws from 'ws' ;
17+ import WebSocketClient = require ( 'ws' ) ;
1818
1919import * as ChildProcess from 'node:child_process' ;
2020import { EventEmitter , once } from 'node:events' ;
@@ -773,6 +773,107 @@ describe('command line switches', () => {
773773 ) ;
774774 }
775775 } ) ;
776+
777+ it ( 'clears device metrics overrides when a client disconnects without detaching' , async function ( ) {
778+ // A client dying without clearing its overrides used to leave the page
779+ // pinned at the emulated size forever.
780+ const appPath = path . join ( fixturesPath , 'apps' , 'remote-debugging-emulation' ) ;
781+ appProcess = ChildProcess . spawn ( process . execPath , [ appPath , '--remote-debugging-port=0' ] ) ;
782+
783+ let stderr = '' ;
784+ const browserWsUrl = await new Promise < string > ( ( resolve , reject ) => {
785+ appProcess ! . stderr . on ( 'data' , ( data : Buffer ) => {
786+ stderr += data . toString ( ) ;
787+ const m = / D e v T o o l s l i s t e n i n g o n ( w s : \/ \/ \S + ) / . exec ( stderr ) ;
788+ if ( m ) {
789+ appProcess ! . stderr . removeAllListeners ( 'data' ) ;
790+ resolve ( m [ 1 ] ) ;
791+ }
792+ } ) ;
793+ appProcess ! . on ( 'exit' , ( ) =>
794+ reject ( new Error ( `Process exited before DevTools URL was found. stderr: ${ stderr } ` ) )
795+ ) ;
796+ } ) ;
797+
798+ type Client = {
799+ socket : WebSocketClient ;
800+ send ( method : string , params ?: unknown , sessionId ?: string ) : Promise < any > ;
801+ attachToPage ( ) : Promise < string > ;
802+ } ;
803+ const connectClient = async ( ) : Promise < Client > => {
804+ const socket = new WebSocketClient ( browserWsUrl ) ;
805+ await once ( socket , 'open' ) ;
806+ let nextId = 1 ;
807+ const pending = new Map < number , { resolve : ( result : any ) => void ; reject : ( error : Error ) => void } > ( ) ;
808+ socket . on ( 'message' , ( data : WebSocketClient . Data ) => {
809+ const message = JSON . parse ( data . toString ( ) ) ;
810+ const handler = message . id && pending . get ( message . id ) ;
811+ if ( handler ) {
812+ pending . delete ( message . id ) ;
813+ if ( message . error ) handler . reject ( new Error ( message . error . message ) ) ;
814+ else handler . resolve ( message . result ) ;
815+ }
816+ } ) ;
817+ const failPending = ( why : string ) => {
818+ for ( const handler of pending . values ( ) ) handler . reject ( new Error ( why ) ) ;
819+ pending . clear ( ) ;
820+ } ;
821+ socket . on ( 'error' , ( error : Error ) => failPending ( `websocket error: ${ error . message } ` ) ) ;
822+ socket . on ( 'close' , ( ) => failPending ( 'websocket closed' ) ) ;
823+ const send = ( method : string , params : unknown = { } , sessionId ?: string ) =>
824+ new Promise < any > ( ( resolve , reject ) => {
825+ const id = nextId ++ ;
826+ pending . set ( id , { resolve, reject } ) ;
827+ socket . send ( JSON . stringify ( { id, method, params, sessionId } ) ) ;
828+ } ) ;
829+ const attachToPage = async ( ) => {
830+ // The window may not exist yet when the DevTools server comes up.
831+ let page : any ;
832+ await waitUntil ( async ( ) => {
833+ const { targetInfos } = await send ( 'Target.getTargets' ) ;
834+ page = targetInfos . find ( ( target : any ) => target . type === 'page' ) ;
835+ return page !== undefined ;
836+ } ) ;
837+ const { sessionId } = await send ( 'Target.attachToTarget' , { targetId : page . targetId , flatten : true } ) ;
838+ return sessionId ;
839+ } ;
840+ return { socket, send, attachToPage } ;
841+ } ;
842+ const innerSize = async ( client : Client , sessionId : string ) => {
843+ const { result } = await client . send (
844+ 'Runtime.evaluate' ,
845+ {
846+ expression : 'window.innerWidth + "x" + window.innerHeight' ,
847+ returnByValue : true
848+ } ,
849+ sessionId
850+ ) ;
851+ return result . value ;
852+ } ;
853+
854+ const clientA = await connectClient ( ) ;
855+ const sessionA = await clientA . attachToPage ( ) ;
856+ const originalSize = await innerSize ( clientA , sessionA ) ;
857+ await clientA . send (
858+ 'Emulation.setDeviceMetricsOverride' ,
859+ {
860+ width : 800 ,
861+ height : 450 ,
862+ deviceScaleFactor : 0 ,
863+ mobile : false
864+ } ,
865+ sessionA
866+ ) ;
867+ expect ( await innerSize ( clientA , sessionA ) ) . to . equal ( '800x450' ) ;
868+
869+ // Drop the TCP connection like a killed client process would.
870+ ( clientA . socket as any ) . _socket . destroy ( ) ;
871+
872+ const clientB = await connectClient ( ) ;
873+ const sessionB = await clientB . attachToPage ( ) ;
874+ await waitUntil ( async ( ) => ( await innerSize ( clientB , sessionB ) ) === originalSize ) ;
875+ clientB . socket . close ( ) ;
876+ } ) ;
776877 } ) ;
777878
778879 describe ( '--trace-startup switch' , ( ) => {
@@ -3474,7 +3575,7 @@ describe('chromium features', () => {
34743575 const server = http . createServer ( ) ;
34753576 defer ( ( ) => server . close ( ) ) ;
34763577 const { port } = await listen ( server ) ;
3477- const wss = new ws . Server ( { server } ) ;
3578+ const wss = new WebSocketClient . Server ( { server } ) ;
34783579 const finished = new Promise < string | undefined > ( ( resolve , reject ) => {
34793580 wss . on ( 'error' , reject ) ;
34803581 wss . on ( 'connection' , ( ws , upgradeReq ) => {
0 commit comments