2022-04-21 02:03:49 +00:00
import { once } from 'events' ;
import * as fs from 'fs' ;
import * as path from 'path' ;
import { Shell } from 'electron' ;
import {
_electron ,
ConsoleMessage ,
Dialog ,
ElectronApplication ,
Page ,
} from 'playwright' ;
2022-07-30 17:15:30 +00:00
import { getTempDir , isLinux } from './helpers/helpers' ;
2022-04-21 02:03:49 +00:00
import { NativefierOptions } from '../shared/src/options/model' ;
const INJECT_DIR = path . join ( __dirname , '..' , 'app' , 'inject' ) ;
const log = console ;
function sleep ( milliseconds : number ) : Promise < void > {
return new Promise ( ( resolve ) = > {
setTimeout ( resolve , milliseconds ) ;
} ) ;
}
2022-07-24 00:05:48 +00:00
/ * *
* Debugging this ? Run your playwright tests in debug mode :
* DEBUG = 'pw:browser*' npm run test :playwright
* /
2022-04-21 02:03:49 +00:00
describe ( 'Application launch' , ( ) = > {
jest . setTimeout ( 60000 ) ;
let app : ElectronApplication ;
let appClosed = true ;
const appMainJSPath = path . join ( __dirname , '..' , 'app' , 'lib' , 'main.js' ) ;
const DEFAULT_CONFIG : NativefierOptions = {
targetUrl : 'https://npmjs.com' ,
} ;
const logFileDir = getTempDir ( 'playwright' ) ;
const metaOrAlt = process . platform === 'darwin' ? 'Meta' : 'Alt' ;
const metaOrCtrl = process . platform === 'darwin' ? 'Meta' : 'Control' ;
const spawnApp = async (
playwrightConfig : NativefierOptions = { . . . DEFAULT_CONFIG } ,
awaitFirstWindow = true ,
preventNavigation = false ,
) : Promise < Page | undefined > = > {
const consoleListener = ( consoleMessage : ConsoleMessage ) : void = > {
const consoleMethods : Record < string , ( ...args : unknown [ ] ) = > unknown > = {
debug : log.debug.bind ( console ) ,
error : log.error.bind ( console ) ,
info : log.info.bind ( console ) ,
log : log.log.bind ( console ) ,
trace : log.trace.bind ( console ) ,
warn : log.warn.bind ( console ) ,
} ;
Promise . all ( consoleMessage . args ( ) . map ( ( x ) = > x . jsonValue ( ) ) )
. then ( ( args ) = > {
if ( consoleMessage . type ( ) in consoleMethods ) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
consoleMethods [ consoleMessage . type ( ) ] ( 'window.console' , args ) ;
} else {
log . log ( 'window.console' , args ) ;
}
} )
. catch ( ( ) = > log . log ( 'window.console' , consoleMessage ) ) ;
} ;
app = await _electron . launch ( {
2022-07-30 17:15:30 +00:00
// Workaround for the following errors in some linux distros:
// pw:browser [pid=24716][err] [24718:0100/000000.660708:ERROR:zygote_linux.cc(650)] write: Broken pipe (32) +16ms
// pw:browser [pid=24719][err] [24719:0725/114519.722060:FATAL:setuid_sandbox_host.cc(157)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /home/parallels/Dev/nativefier/node_modules/electron/dist/chrome-sandbox is owned by root and has mode 4755. +61ms
args : isLinux ( )
? [ '--no-sandbox' , '--disable-setuid-sandbox' , appMainJSPath ]
: [ appMainJSPath ] ,
2022-04-21 02:03:49 +00:00
env : {
LOG_FILE_DIR : logFileDir ,
PLAYWRIGHT_TEST : '1' ,
2022-07-30 17:15:30 +00:00
PLAYWRIGHT_CONFIG : JSON.stringify ( {
. . . playwrightConfig ,
// disableGpu and process.env.DISPLAY forwarding solve the following errors on Linux:
// pw:browser [pid=286188][err] [286188:0724/102939.938248:ERROR:ozone_platform_x11.cc(248)] Missing X server or $DISPLAY +77ms
// pw:browser [pid=286188][err] [286188:0724/102939.938299:ERROR:env.cc(225)] The platform failed to initialize. Exiting. +2ms
disableGpu : isLinux ( ) ? true : undefined ,
processEnvs :
isLinux ( ) && process . env . DISPLAY
? JSON . stringify ( { DISPLAY : process.env.DISPLAY } )
: undefined ,
} as NativefierOptions ) ,
2022-04-21 02:03:49 +00:00
USE_LOG_FILE : '1' ,
VERBOSE : '1' ,
} ,
2022-04-25 14:11:11 +00:00
timeout : 60000 ,
2022-04-21 02:03:49 +00:00
} ) ;
app . on ( 'window' , ( page : Page ) = > {
page . on ( 'console' , consoleListener ) ;
if ( preventNavigation ) {
// Prevent page navigation so we can have a reliable test
page
. route ( '*' , ( route ) : void = > {
log . info ( ` Preventing route: ${ route . request ( ) . url ( ) } ` ) ;
route . abort ( ) . catch ( ( error ) = > {
log . error ( 'ERROR' , error ) ;
} ) ;
} )
. catch ( ( error ) = > {
log . error ( 'ERROR' , error ) ;
} ) ;
}
} ) ;
app . on ( 'close' , ( ) = > ( appClosed = true ) ) ;
appClosed = false ;
if ( ! awaitFirstWindow ) {
return undefined ;
}
const window = await app . firstWindow ( ) ;
// Wait for our initial page to finish loading, otherwise some tests will break
let waited = 0 ;
while (
window . url ( ) === 'about:blank' &&
playwrightConfig . targetUrl !== 'about:blank' &&
waited < 2000
) {
waited += 100 ;
await sleep ( 100 ) ;
}
return window ;
} ;
beforeEach ( ( ) = > {
nukeInjects ( ) ;
nukeLogs ( logFileDir ) ;
} ) ;
afterEach ( async ( ) = > {
if ( app && ! appClosed ) {
await app . close ( ) ;
}
if ( process . env . DEBUG ) {
showLogs ( logFileDir ) ;
}
} ) ;
test ( 'shows an initial window' , async ( ) = > {
const mainWindow = ( await spawnApp ( ) ) as Page ;
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
expect ( app . windows ( ) ) . toHaveLength ( 1 ) ;
expect ( await mainWindow . title ( ) ) . toBe ( 'npm' ) ;
} ) ;
test ( 'can inject some CSS' , async ( ) = > {
const fuschia = 'rgb(255, 0, 255)' ;
createInject (
'inject.css' ,
` * { background-color: ${ fuschia } !important; } ` ,
) ;
const mainWindow = ( await spawnApp ( ) ) as Page ;
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
const headerStyle = await mainWindow . $eval ( 'header' , ( el ) = >
window . getComputedStyle ( el ) ,
) ;
expect ( headerStyle . backgroundColor ) . toBe ( fuschia ) ;
2023-03-24 18:16:49 +00:00
await mainWindow . click ( '#nav-pricing-link' ) ;
2022-04-21 02:03:49 +00:00
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
const headerStylePostNavigate = await mainWindow . $eval ( 'header' , ( el ) = >
window . getComputedStyle ( el ) ,
) ;
expect ( headerStylePostNavigate . backgroundColor ) . toBe ( fuschia ) ;
} ) ;
test ( 'can inject some JS' , async ( ) = > {
const alertMsg = 'hello world from inject' ;
createInject (
'inject.js' ,
` setTimeout(() => {alert(" ${ alertMsg } "); }, 5000); ` , // Buy ourselves 5 seconds to get the dialog handler setup
) ;
const mainWindow = ( await spawnApp (
{ . . . DEFAULT_CONFIG } ,
true ,
true ,
) ) as Page ;
const [ dialogPromise ] = ( await once (
mainWindow ,
'dialog' ,
) ) as unknown as Promise < Dialog > [ ] ;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const dialog : Dialog = await dialogPromise ;
await dialog . dismiss ( ) ;
expect ( dialog . message ( ) ) . toBe ( alertMsg ) ;
} ) ;
test ( 'can open internal links' , async ( ) = > {
const mainWindow = ( await spawnApp ( ) ) as Page ;
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
2023-03-24 18:16:49 +00:00
await mainWindow . click ( '#nav-pricing-link' ) ;
2022-04-21 02:03:49 +00:00
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
expect ( app . windows ( ) ) . toHaveLength ( 1 ) ;
} ) ;
test ( 'tries to open external links' , async ( ) = > {
const mainWindow = ( await spawnApp ( ) ) as Page ;
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
// Install the mock first
await app . evaluate ( ( { shell } : { shell : Shell } ) = > {
// @ts-expect-error injecting into shell so that this promise
// can be accessed outside of this anonymous function's scope
// Not my favorite thing to do, but I could not find another way
process . openExternalPromise = new Promise ( ( resolve ) = > {
shell . openExternal = async ( url : string ) : Promise < void > = > {
resolve ( url ) ;
return Promise . resolve ( ) ;
} ;
} ) ;
} ) ;
// Click, but don't await it - Playwright waits for stuff that does not happen when Electron does openExternal.
mainWindow
. click ( '#footer > div:nth-child(2) > ul > li:nth-child(2) > a' )
. catch ( ( err : unknown ) = > {
expect ( err ) . toBeUndefined ( ) ;
} ) ;
// Go pull out our value returned by our hacky global promise
const openExternalUrl = await app . evaluate ( 'process.openExternalPromise' ) ;
expect ( openExternalUrl ) . not . toBe ( 'https://www.npmjs.com/' ) ;
expect ( openExternalUrl ) . not . toBe ( DEFAULT_CONFIG . targetUrl ) ;
} ) ;
// Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts.
// Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved.
test . skip ( 'keyboard shortcuts: zoom' , async ( ) = > {
const mainWindow = ( await spawnApp ( ) ) as Page ;
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
const defaultZoom : number | undefined = await app . evaluate (
( { BrowserWindow } ) = >
BrowserWindow . getFocusedWindow ( ) ? . webContents ? . zoomFactor ,
) ;
expect ( defaultZoom ) . toBeDefined ( ) ;
await mainWindow . keyboard . press ( ` ${ metaOrCtrl } +Equal ` ) ;
const postZoomIn = await app . evaluate (
( { BrowserWindow } ) : number | undefined = >
BrowserWindow . getFocusedWindow ( ) ? . webContents ? . zoomFactor ,
) ;
expect ( postZoomIn ) . toBeGreaterThan ( defaultZoom as number ) ;
await mainWindow . keyboard . press ( ` ${ metaOrCtrl } +0 ` ) ;
const postZoomReset = await app . evaluate (
( { BrowserWindow } ) : number | undefined = >
BrowserWindow . getFocusedWindow ( ) ? . webContents ? . zoomFactor ,
) ;
expect ( postZoomReset ) . toEqual ( defaultZoom ) ;
await mainWindow . keyboard . press ( ` ${ metaOrCtrl } +Minus ` ) ;
const postZoomOut : number | undefined = await app . evaluate (
( { BrowserWindow } ) = >
BrowserWindow . getFocusedWindow ( ) ? . webContents ? . zoomFactor ,
) ;
expect ( postZoomOut ) . toBeLessThan ( defaultZoom as number ) ;
} ) ;
// Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts.
// Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved.
test . skip ( 'keyboard shortcuts: back and forward' , async ( ) = > {
const mainWindow = ( await spawnApp ( ) ) as Page ;
await mainWindow . waitForLoadState ( 'domcontentloaded' ) ;
await Promise . all ( [
2023-03-24 18:16:49 +00:00
mainWindow . click ( '#nav-pricing-link' ) ,
2022-04-21 02:03:49 +00:00
mainWindow . waitForNavigation ( { waitUntil : 'domcontentloaded' } ) ,
] ) ;
// Go back
// console.log(`${metaOrAlt}+ArrowLeft`);
await mainWindow . keyboard . press ( ` ${ metaOrAlt } +ArrowLeft ` ) ;
await mainWindow . waitForNavigation ( { waitUntil : 'domcontentloaded' } ) ;
const backUrl = await mainWindow . evaluate ( ( ) = > window . location . href ) ;
expect ( backUrl ) . toBe ( DEFAULT_CONFIG . targetUrl ) ;
// Go forward
// console.log(`${metaOrAlt}+ArrowRight`);
await mainWindow . keyboard . press ( ` ${ metaOrAlt } +ArrowRight ` ) ;
await mainWindow . waitForNavigation ( { waitUntil : 'domcontentloaded' } ) ;
const forwardUrl = await mainWindow . evaluate ( ( ) = > window . location . href ) ;
expect ( forwardUrl ) . not . toBe ( DEFAULT_CONFIG . targetUrl ) ;
} ) ;
test ( 'no errors thrown in console' , async ( ) = > {
await spawnApp ( { . . . DEFAULT_CONFIG } , false ) ;
const mainWindow = await app . firstWindow ( ) ;
mainWindow . addListener ( 'console' , ( consoleMessage : ConsoleMessage ) = > {
try {
expect ( consoleMessage . type ( ) ) . not . toBe ( 'error' ) ;
} catch {
// Do it this way so we'll see the whole message, not just
// expect('error').not.toBe('error')
// which isn't particularly useful
expect ( {
message : 'console.error called unexpectedly with' ,
2023-08-03 17:35:19 +00:00
consoleMessage : { . . . consoleMessage } ,
2022-04-21 02:03:49 +00:00
} ) . toBeUndefined ( ) ;
}
} ) ;
// Give the app 5 seconds to spin up and ensure no errors happened
await new Promise ( ( resolve ) = > setTimeout ( resolve , 5000 ) ) ;
} ) ;
test ( 'basic auth' , async ( ) = > {
const mainWindow = ( await spawnApp ( {
2023-08-03 17:35:19 +00:00
targetUrl : 'https://authenticationtest.com/HTTPAuth/' ,
basicAuthUsername : 'user' ,
basicAuthPassword : 'pass' ,
2022-04-21 02:03:49 +00:00
} ) ) as Page ;
await mainWindow . waitForLoadState ( 'networkidle' ) ;
const documentText = await mainWindow . evaluate < string > (
'document.documentElement.innerText' ,
) ;
2023-08-03 17:35:19 +00:00
expect ( documentText ) . toContain ( 'Success' ) ;
2022-04-21 02:03:49 +00:00
2023-08-03 17:35:19 +00:00
expect ( documentText ) . not . toContain ( 'Failure' ) ;
} ) ;
test ( 'basic auth - bad login' , async ( ) = > {
const mainWindow = ( await spawnApp ( {
targetUrl : 'https://authenticationtest.com/HTTPAuth/' ,
basicAuthUsername : 'userbad' ,
basicAuthPassword : 'passbad' ,
} ) ) as Page ;
await mainWindow . waitForLoadState ( 'networkidle' ) ;
const documentText = await mainWindow . evaluate < string > (
'document.documentElement.innerText' ,
) ;
expect ( documentText ) . not . toContain ( 'Success' ) ;
expect ( documentText ) . toContain ( 'Failure' ) ;
2022-04-21 02:03:49 +00:00
} ) ;
test ( 'basic auth without pre-providing' , async ( ) = > {
const mainWindow = ( await spawnApp ( {
2023-08-03 17:35:19 +00:00
targetUrl : 'https://authenticationtest.com/HTTPAuth/' ,
2022-04-21 02:03:49 +00:00
} ) ) as Page ;
await mainWindow . waitForLoadState ( 'load' ) ;
// Give the app a few seconds to open the login window
await new Promise ( ( resolve ) = > setTimeout ( resolve , 5000 ) ) ;
const appWindows = app . windows ( ) ;
expect ( appWindows ) . toHaveLength ( 2 ) ;
const loginWindow = appWindows . filter ( ( x ) = > x !== mainWindow ) [ 0 ] ;
await loginWindow . waitForLoadState ( 'domcontentloaded' ) ;
2022-07-30 17:15:30 +00:00
await loginWindow . waitForLoadState ( 'load' ) ;
2022-04-21 02:03:49 +00:00
const usernameField = await loginWindow . $ ( '#username-input' ) ;
expect ( usernameField ) . not . toBeNull ( ) ;
2023-08-03 17:35:19 +00:00
await usernameField ? . fill ( 'user' ) ;
2022-04-21 02:03:49 +00:00
const passwordField = await loginWindow . $ ( '#password-input' ) ;
expect ( passwordField ) . not . toBeNull ( ) ;
2023-08-03 17:35:19 +00:00
await passwordField ? . fill ( 'pass' ) ;
2022-04-21 02:03:49 +00:00
const submitButton = await loginWindow . $ ( '#submit-form-button' ) ;
expect ( submitButton ) . not . toBeNull ( ) ;
2022-07-30 17:15:30 +00:00
// "Why is this here?" you may be asking yourself.
// Because for some reason, on some linux boxes,
// the click function will not work until this is done.
// Why? I do not have access to the dark incantation
// that would allow me to know such information.
log . log ( { submitButton } ) ;
2022-04-21 02:03:49 +00:00
await submitButton ? . click ( ) ;
2022-07-30 17:15:30 +00:00
await mainWindow . waitForEvent ( 'load' ) ;
2022-04-21 02:03:49 +00:00
const documentText = await mainWindow . evaluate < string > (
'document.documentElement.innerText' ,
) ;
2023-08-03 17:35:19 +00:00
expect ( documentText ) . toContain ( 'Success' ) ;
2022-04-21 02:03:49 +00:00
2023-08-03 17:35:19 +00:00
expect ( documentText ) . not . toContain ( 'Failure' ) ;
2022-04-21 02:03:49 +00:00
} ) ;
} ) ;
function createInject ( filename : string , contents : string ) : void {
fs . writeFileSync ( path . join ( INJECT_DIR , filename ) , contents ) ;
}
function nukeInjects ( ) : void {
if ( ! fs . existsSync ( INJECT_DIR ) ) {
return ;
}
const injected = fs
. readdirSync ( INJECT_DIR )
. filter ( ( x ) = > x !== '_placeholder' ) ;
injected . forEach ( ( x ) = > fs . unlinkSync ( path . join ( INJECT_DIR , x ) ) ) ;
}
function nukeLogs ( logFileDir : string ) : void {
const logs = fs . readdirSync ( logFileDir ) . filter ( ( x ) = > x . endsWith ( '.log' ) ) ;
logs . forEach ( ( x ) = > fs . unlinkSync ( path . join ( logFileDir , x ) ) ) ;
}
function showLogs ( logFileDir : string ) : void {
const logs = fs . readdirSync ( logFileDir ) . filter ( ( x ) = > x . endsWith ( '.log' ) ) ;
for ( const logFile of logs ) {
log . log ( fs . readFileSync ( path . join ( logFileDir , logFile ) ) . toString ( ) ) ;
}
}