/** * 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. */ import type { BrowserWindow } from 'electron'; import path from 'path'; import fs from 'fs'; import { electronTest as test, expect, selectors } from './electronTest'; import type { ConsoleMessage } from 'playwright'; test('should fire close event via ElectronApplication.close();', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app.js'); const events = []; electronApp.on('close', () => events.push('application(close)')); electronApp.context().on('close', () => events.push('context(close)')); electronApp.process().on('exit', () => events.push('process(exit)')); await electronApp.close(); // Close one more time - this should be a noop. await electronApp.close(); events.sort(); // we don't care about the order expect(events).toEqual(['application(close)', 'context(close)', 'process(exit)']); // Give it some time to fire more events - there should not be any. await new Promise(f => setTimeout(f, 1000)); expect(events).toEqual(['application(close)', 'context(close)', 'process(exit)']); }); test('should fire close event via BrowserContext.close()', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app.js'); const events = []; electronApp.on('close', () => events.push('application(close)')); electronApp.context().on('close', () => events.push('context(close)')); electronApp.process().on('exit', () => events.push('process(exit)')); await electronApp.context().close(); // Close one more time - this should be a noop. await electronApp.context().close(); events.sort(); // we don't care about the order expect(events).toEqual(['application(close)', 'context(close)', 'process(exit)']); // Give it some time to fire more events - there should not be any. await new Promise(f => setTimeout(f, 1000)); expect(events).toEqual(['application(close)', 'context(close)', 'process(exit)']); }); test('should fire close event when the app quits itself', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app.js'); const events = []; electronApp.on('close', () => events.push('application(close)')); electronApp.context().on('close', () => events.push('context(close)')); electronApp.process().on('exit', () => events.push('process(exit)')); { const waitForAppClose = new Promise(f => electronApp.on('close', f)); await electronApp.evaluate(({ app }) => app.quit()); await waitForAppClose; } await expect.poll(() => [...events].sort()).toEqual(['application(close)', 'context(close)', 'process(exit)']); // Give it some time to fire more events - there should not be any. await new Promise(f => setTimeout(f, 1000)); expect([...events].sort()).toEqual(['application(close)', 'context(close)', 'process(exit)']); }); test('should fire console events', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app.js'); const messages = []; electronApp.on('console', message => messages.push({ type: message.type(), text: message.text() })); await electronApp.evaluate(() => { console.log('its type log'); console.debug('its type debug'); console.info('its type info'); console.error('its type error'); console.warn('its type warn'); }); await electronApp.close(); expect(messages).toEqual([ { type: 'log', text: 'its type log' }, { type: 'debug', text: 'its type debug' }, { type: 'info', text: 'its type info' }, { type: 'error', text: 'its type error' }, { type: 'warning', text: 'its type warn' }, ]); }); test('should fire console events with handles and complex objects', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app.js'); const messages: ConsoleMessage[] = []; electronApp.on('console', message => messages.push(message)); await electronApp.evaluate(() => { globalThis.complexObject = [{ a: 1, b: 2, c: 3 }, { a: 4, b: 5, c: 6 }, { a: 7, b: 8, c: 9 }]; console.log(globalThis.complexObject[0], globalThis.complexObject[1], globalThis.complexObject[2]); }); expect(messages.length).toBe(1); const message = messages[0]; expect(message.text()).toBe('{a: 1, b: 2, c: 3} {a: 4, b: 5, c: 6} {a: 7, b: 8, c: 9}'); expect(message.args().length).toBe(3); expect(await message.args()[0].jsonValue()).toEqual({ a: 1, b: 2, c: 3 }); expect(await message.args()[0].evaluate(part => part === globalThis.complexObject[0])).toBeTruthy(); expect(await message.args()[1].jsonValue()).toEqual({ a: 4, b: 5, c: 6 }); expect(await message.args()[1].evaluate(part => part === globalThis.complexObject[1])).toBeTruthy(); expect(await message.args()[2].jsonValue()).toEqual({ a: 7, b: 8, c: 9 }); expect(await message.args()[2].evaluate(part => part === globalThis.complexObject[2])).toBeTruthy(); await electronApp.close(); }); test('should dispatch ready event', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app-ready-event.js'); const events = await electronApp.evaluate(() => globalThis.__playwrightLog); expect(events).toEqual([ 'isReady == false', 'will-finish-launching fired', 'ready fired', 'whenReady resolved', 'isReady == true', ]); }); test('should script application', async ({ launchElectronApp }) => { const app = await launchElectronApp('electron-app.js'); const appPath = await app.evaluate(async ({ app }) => app.getAppPath()); expect(appPath).toBe(path.resolve(__dirname)); }); test('should preserve args', async ({ launchElectronApp, isMac }) => { const electronApp = await launchElectronApp('electron-app-args.js', ['foo', 'bar', '& <>^|\\"']); const argv = await electronApp.evaluate(async () => globalThis.argv); const electronPath = isMac ? path.join('dist', 'Electron.app') : path.join('dist', 'electron'); expect(argv).toEqual([expect.stringContaining(electronPath), expect.stringContaining(path.join('electron', 'electron-app-args.js')), 'foo', 'bar', '& <>^|\\"']); }); test('should return windows', async ({ launchElectronApp, newWindow }) => { const app = await launchElectronApp('electron-app.js'); const page = await newWindow(app); expect(app.windows()).toEqual([page]); }); test('should evaluate handle', async ({ launchElectronApp }) => { const app = await launchElectronApp('electron-app.js'); const appHandle = await app.evaluateHandle(({ app }) => app); expect(await app.evaluate(({ app }, appHandle) => app === appHandle, appHandle)).toBeTruthy(); }); test('should infer evaluate types', async ({ launchElectronApp }) => { const app = await launchElectronApp('electron-app.js'); // No-arg evaluate, returning a primitive. const appPath: string = await app.evaluate(({ app }) => app.getAppPath()); expect(typeof appPath).toBe('string'); // Arg evaluate with explicit typing on both sides. const sum: number = await app.evaluate(({}, { a, b }) => a + b, { a: 1, b: 2 }); expect(sum).toBe(3); // Async evaluate returning an object literal. const info = await app.evaluate(async ({ app }) => ({ name: app.getName(), version: app.getVersion() })); const name: string = info.name; const version: string = info.version; expect(typeof name).toBe('string'); expect(typeof version).toBe('string'); // evaluateHandle returns JSHandle; jsonValue() preserves R. const handle = await app.evaluateHandle(({ app }) => ({ path: app.getAppPath() })); const jsonValue = await handle.jsonValue(); const path: string = jsonValue.path; expect(typeof path).toBe('string'); // Passing a JSHandle as an arg unwraps it on the worker side. const numberHandle = await app.evaluateHandle(() => 42); const doubled: number = await app.evaluate(({}, n) => n * 2, numberHandle); expect(doubled).toBe(84); }); test('should register custom selector engine', async ({ launchElectronApp, newWindow, server }) => { const app = await launchElectronApp('electron-app.js'); const page = await newWindow(app); const tag = `electron-tag-${test.info().workerIndex}`; await selectors.register(tag, `({ query(root, selector) { return root.querySelector(selector); }, queryAll(root, selector) { return Array.from(root.querySelectorAll(selector)); }, })`); await page.goto(server.EMPTY_PAGE); await page.setContent('
'); expect(await page.$eval(`${tag}=DIV`, e => e.nodeName)).toBe('DIV'); expect(await page.$$eval(`${tag}=DIV`, es => es.length)).toBe(2); }); test('should route network', async ({ launchElectronApp, newWindow }) => { const app = await launchElectronApp('electron-app.js'); const page = await newWindow(app); await app.context().route('**/empty.html', async (route, request) => { await route.fulfill({ status: 200, contentType: 'text/html', body: 'Hello World', }); }); await page.goto('https://localhost:1000/empty.html'); expect(await page.title()).toBe('Hello World'); }); test('should support init script', async ({ launchElectronApp, newWindow }) => { const app = await launchElectronApp('electron-app.js'); await app.context().addInitScript('window.magic = 42;'); const page = await newWindow(app); await page.goto('data:text/html,'); expect(await page.evaluate(() => window['copy'])).toBe(42); }); test('should expose function', async ({ launchElectronApp, newWindow }) => { const app = await launchElectronApp('electron-app.js'); await app.context().exposeFunction('add', (a, b) => a + b); const page = await newWindow(app); await page.goto('data:text/html,'); expect(await page.evaluate(() => window['result'])).toBe(42); }); test('should wait for first window', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app-args.js'); await electronApp.evaluate(({ BrowserWindow }) => { const window = new BrowserWindow({ width: 800, height: 600 }); void window.loadURL('data:text/html,Hello World!'); }); const window = await electronApp.firstWindow(); expect(await window.title()).toBe('Hello World!'); }); test('should have a clipboard instance', async ({ launchElectronApp }) => { const app = await launchElectronApp('electron-app.js'); const clipboardContentToWrite = 'Hello from Playwright'; await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), clipboardContentToWrite); const clipboardContentRead = await app.evaluate(async ({ clipboard }) => clipboard.readText()); expect(clipboardContentRead).toEqual(clipboardContentToWrite); }); test('should test app that opens window fast', async ({ launchElectronApp }) => { await launchElectronApp('electron-window-app.js'); }); test('should return browser window', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-window-app.js'); const page = await electronApp.firstWindow(); const bwHandle = await electronApp.browserWindow(page); expect(await bwHandle.evaluate((bw: BrowserWindow) => bw.title)).toBe('Electron'); }); test('should set timezone via process.env.TZ', async ({ launchElectronApp }) => { // Migrated from the removed timezoneId launch option, see packages/playwright-electron/README.md. const app = await launchElectronApp('electron-options-app.js', [], { env: { PWTEST_OPTION_TZ: 'Europe/London' } }); const page = await app.firstWindow(); expect(await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone)).toBe('Europe/London'); }); test('should ignore https errors via --ignore-certificate-errors switch', async ({ launchElectronApp, httpsServer }) => { // Migrated from the removed ignoreHTTPSErrors launch option, see packages/playwright-electron/README.md. const app = await launchElectronApp('electron-options-app.js', [], { env: { PWTEST_OPTION_IGNORE_HTTPS_ERRORS: '1' } }); const page = await app.firstWindow(); const response = await page.goto(httpsServer.EMPTY_PAGE); expect(response.status()).toBe(200); }); test('should create page for browser view', async ({ launchElectronApp }) => { const app = await launchElectronApp('electron-window-app.js'); await app.firstWindow(); await app.evaluate(async electron => { const window = electron.BrowserWindow.getAllWindows()[0]; const view = new electron.BrowserView(); window.addBrowserView(view); await view.webContents.loadURL('about:blank'); view.setBounds({ x: 0, y: 0, width: 256, height: 256 }); }); await expect.poll(() => app.windows().length).toBe(2); }); test('should return same browser window for browser view pages', async ({ launchElectronApp }) => { const app = await launchElectronApp('electron-window-app.js'); await app.firstWindow(); await app.evaluate(async electron => { const window = electron.BrowserWindow.getAllWindows()[0]; const view = new electron.BrowserView(); window.addBrowserView(view); await view.webContents.loadURL('about:blank'); view.setBounds({ x: 0, y: 0, width: 256, height: 256 }); }); await expect.poll(() => app.windows().length).toBe(2); const [firstWindowId, secondWindowId] = await Promise.all( app.windows().map(async page => { const bwHandle = await app.browserWindow(page); const id = await bwHandle.evaluate((bw: BrowserWindow) => bw.id); return id; }) ); expect(firstWindowId).toEqual(secondWindowId); }); test('should record video via page.screencast.start', async ({ launchElectronApp }, testInfo) => { const app = await launchElectronApp('electron-window-app.js'); const page = await app.firstWindow(); const videoPath = testInfo.outputPath('video.webm'); await page.screencast.start({ path: videoPath }); await page.setContent(``); await page.waitForTimeout(1000); await page.screencast.stop(); await app.close(); expect(fs.statSync(videoPath).size).toBeGreaterThan(0); }); test('should record har via context.tracing.startHar', async ({ launchElectronApp, server }, testInfo) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30747' }); const app = await launchElectronApp('electron-window-app.js'); const harPath = testInfo.outputPath('har.zip'); await app.context().tracing.startHar(harPath); const page = await app.firstWindow(); await page.goto(server.EMPTY_PAGE); await app.context().tracing.stopHar(); await app.close(); expect(fs.existsSync(harPath)).toBeTruthy(); expect(fs.statSync(harPath).size).toBeGreaterThan(0); }); test('should be able to get the first window when with a delayed navigation', async ({ launchElectronApp }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/17765' }); const app = await launchElectronApp('electron-window-app-delayed-loadURL.js'); const page = await app.firstWindow(); await expect(page).toHaveURL('data:text/html,

Foobar

'); await expect(page.locator('h1')).toHaveText('Foobar'); }); test('should detach debugger on app-initiated exit', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app.js'); const closePromise = new Promise(f => electronApp.process().on('close', f)); await electronApp.evaluate(({ app }) => { app.quit(); }); await closePromise; }); test('should run pre-ready apis', async ({ launchElectronApp }) => { await launchElectronApp('electron-app-pre-ready.js'); }); test('should resolve app path for folder apps', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('.'); const appPath = await electronApp.evaluate(async ({ app }) => app.getAppPath()); expect(appPath).toBe(path.resolve(__dirname)); }); test('should return app name / version from manifest', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('.'); const data = await electronApp.evaluate(async ({ app }) => { return { name: app.getName(), version: app.getVersion(), }; }); expect(data).toEqual({ name: 'my-electron-app', version: '1.0.0' }); }); test('should report downloads', async ({ launchElectronApp, electronMajorVersion, server }) => { test.skip(electronMajorVersion < 30, 'Depends on https://github.com/electron/electron/pull/41718'); server.setRoute('/download', (req, res) => { res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Disposition', 'attachment'); res.end(`Hello world`); }); const app = await launchElectronApp('electron-window-app.js', [], { acceptDownloads: true }); const window = await app.firstWindow(); await window.setContent(`download`); const [download] = await Promise.all([ window.waitForEvent('download'), window.click('a') ]); const path = await download.path(); expect(fs.existsSync(path)).toBeTruthy(); expect(fs.readFileSync(path).toString()).toBe('Hello world'); await app.close(); });