diff --git a/docs/playwright.md b/docs/playwright.md index da145a752..1b666dd1c 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -5,7 +5,7 @@ title: Testing with Playwright # Testing with Playwright -Playwright is a Node library to automate the [Chromium](https://www.chromium.org/Home), [WebKit](https://webkit.org/) and [Firefox](https://www.mozilla.org/en-US/firefox/new/) browsers with a single API. It enables **cross-browser** web automation that is **ever-green**, **capable**, **reliable** and **fast**. +Playwright is a Node library to automate the [Chromium](https://www.chromium.org/Home), [WebKit](https://webkit.org/) and [Firefox](https://www.mozilla.org/en-US/firefox/new/) browsers as well as [Electron](https://www.electronjs.org/) apps with a single API. It enables **cross-browser** web automation that is **ever-green**, **capable**, **reliable** and **fast**. Playwright was built similarly to [Puppeteer](https://github.com/puppeteer/puppeteer), using its API and so is very different in usage. However, Playwright has cross browser support with better design for test automaiton. @@ -202,7 +202,7 @@ CodeceptJS allows you to implement custom actions like `I.createTodo` or use **P ## Multi Session Testing -TO launch additional browser context (or incognito window) use `session` command. +To launch additional browser context (or incognito window) use `session` command. ```js Scenario('I try to open this site as anonymous user', ({ I }) => { @@ -217,6 +217,53 @@ Scenario('I try to open this site as anonymous user', ({ I }) => { > ℹ Learn more about [multi-session testing](/basics/#multiple-sessions) +## Electron Testing + +CodeceptJS allows you to make use of [Playwright's Electron flavor](https://github.com/microsoft/playwright/blob/master/packages/playwright-electron/README.md). +To use this functionality, all you need to do is set the browser to `electron` in the CodeceptJS configuration file and, according to the [Playwright BrowserType API](https://playwright.dev/docs/api/class-browsertype/#browsertypelaunchoptions), set the launch options to point to your Electron application. + +`main.js` - main Electron application file +```js +const { app, BrowserWindow } = require("electron"); + +function createWindow() { + const window = new BrowserWindow({ width: 800, height: 600 }); + window.loadURL("https://example.com"); +} + +app.whenReady().then(createWindow); +``` + +`codecept.conf.js` - CodeceptJS configuration file +```js +const path = require("path"); + +exports.config = { + helpers: { + Playwright: { + browser: "electron", + electron: { + executablePath: require("electron"), + args: [path.join(__dirname, "main.js")], + }, + }, + }, + // rest of config +} +``` + +### Headless Mode + +With Electron, headless mode must be set when creating the window. Therefore, CodeceptJS's `show` configuration parameter will not work. However, you can set it in the `main.js` file as shown below: + +```js +function createWindow() { + const window = new BrowserWindow({ width: 800, height: 600, show: false }); + window.loadURL("https://example.com"); +} +``` + + ## Device Emulation Playwright can emulate browsers of mobile devices. Instead of paying for expensive devices for mobile tests you can adjust Playwright settings so it could emulate mobile browsers on iPhone, Samsung Galaxy, etc. diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 5c8c41af4..9c4c25ecd 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -35,7 +35,7 @@ let defaultSelectorEnginesInitialized = false; const popupStore = new Popup(); const consoleLogStore = new Console(); -const availableBrowsers = ['chromium', 'webkit', 'firefox']; +const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']; const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine'); /** @@ -58,7 +58,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright * This helper should be configured in codecept.json or codecept.conf.js * * * `url`: base url of website to be tested - * * `browser`: a browser to test on, either: `chromium`, `firefox`, `webkit`. Default: chromium. + * * `browser`: a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium. * * `show`: (optional, default: false) - show browser window. * * `restart`: (optional, default: true) - restart browser between tests. * * `disableScreenshots`: (optional, default: false) - don't save screenshot on failure. @@ -77,6 +77,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright * * `userAgent`: (optional) user-agent string. * * `manualStart`: (optional, default: false) - do not start browser before a test, start it manually inside a helper with `this.helpers["Playwright"]._startBrowser()`. * * `chromium`: (optional) pass additional chromium options + * * `electron`: (optional) pass additional electron options * * #### Example #1: Wait for 0 network connections. * @@ -207,6 +208,8 @@ class Playwright extends Helper { this.isAuthenticated = false; this.sessionPages = {}; this.activeSessionName = ''; + this.isElectron = false; + this.electronSessions = []; // override defaults with config this._setConfig(config); @@ -258,6 +261,7 @@ class Playwright extends Helper { ...this._getOptionsForBrowser(config), }; this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint; + this.isElectron = this.options.browser === 'electron'; this.userDataDir = this.playwrightOptions.userDataDir; popupStore.defaultAction = this.options.defaultPopupAction; } @@ -270,7 +274,7 @@ class Playwright extends Helper { }, { name: 'browser', - message: 'Browser in which testing will be performed. Possible options: chromium, firefox or webkit', + message: 'Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron', default: 'chromium', }, ]; @@ -322,6 +326,12 @@ class Playwright extends Helper { async _after() { if (!this.isRunning) return; + if (this.isElectron) { + this.browser.close(); + this.electronSessions.forEach(session => session.close()); + return; + } + // close other sessions try { const contexts = await this.browser.contexts(); @@ -377,12 +387,22 @@ class Playwright extends Helper { this.debugSection('New Context', config ? JSON.stringify(config) : 'opened'); this.activeSessionName = sessionName; - const bc = await this.browser.newContext(config); - const page = await bc.newPage(); + let browserContext; + let page; + if (this.isElectron) { + const browser = await playwright._electron.launch(this.playwrightOptions); + this.electronSessions.push(browser); + browserContext = browser.context(); + page = await browser.firstWindow(); + } else { + browserContext = await this.browser.newContext(config); + page = await browserContext.newPage(); + } + targetCreatedHandler.call(this, page); this._setPage(page); // Create a new page inside context. - return bc; + return browserContext; }, stop: async () => { // is closed by _after @@ -553,7 +573,9 @@ class Playwright extends Helper { } async _startBrowser() { - if (this.isRemoteBrowser) { + if (this.isElectron) { + this.browser = await playwright._electron.launch(this.playwrightOptions); + } else if (this.isRemoteBrowser) { try { this.browser = await playwright[this.options.browser].connect(this.playwrightOptions.browserWSEndpoint); } catch (err) { @@ -562,13 +584,9 @@ class Playwright extends Helper { } throw err; } - } - - if (!this.isRemoteBrowser && this.userDataDir) { + } else if (this.userDataDir) { this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions); - } - - if (!this.isRemoteBrowser && !this.userDataDir) { + } else { this.browser = await playwright[this.options.browser].launch(this.playwrightOptions); } @@ -577,15 +595,21 @@ class Playwright extends Helper { this.debugSection('Url', target.url()); }); - if (this.userDataDir) { + if (this.isElectron) { + this.browserContext = this.browser.context(); + } else if (this.userDataDir) { this.browserContext = this.browser; } else { this.browserContext = await this.browser.newContext({ ignoreHTTPSErrors: this.options.ignoreHTTPSErrors, acceptDownloads: true, ...this.options.emulate });// Adding the HTTPSError ignore in the context so that we can ignore those errors } - const existingPages = await this.browserContext.pages(); - - const mainPage = existingPages[0] || await this.browserContext.newPage(); + let mainPage; + if (this.isElectron) { + mainPage = await this.browser.firstWindow(); + } else { + const existingPages = await this.browserContext.pages(); + mainPage = existingPages[0] || await this.browserContext.newPage(); + } targetCreatedHandler.call(this, mainPage); await this._setPage(mainPage); @@ -656,6 +680,9 @@ class Playwright extends Helper { * {{> amOnPage }} */ async amOnPage(url) { + if (this.isElectron) { + throw new Error('Cannot open pages inside an Electron container'); + } if (!(/^\w+\:\/\//.test(url))) { url = this.options.url + url; } @@ -907,6 +934,9 @@ class Playwright extends Helper { * @param {number} [num=1] */ async switchToNextTab(num = 1) { + if (this.isElectron) { + throw new Error('Cannot switch tabs inside an Electron container'); + } const pages = await this.browserContext.pages(); const index = pages.indexOf(this.page); @@ -930,6 +960,9 @@ class Playwright extends Helper { * @param {number} [num=1] */ async switchToPreviousTab(num = 1) { + if (this.isElectron) { + throw new Error('Cannot switch tabs inside an Electron container'); + } const pages = await this.browserContext.pages(); const index = pages.indexOf(this.page); this.withinLocator = null; @@ -951,6 +984,9 @@ class Playwright extends Helper { * ``` */ async closeCurrentTab() { + if (this.isElectron) { + throw new Error('Cannot close current tab inside an Electron container'); + } const oldPage = this.page; await this.switchToPreviousTab(); await oldPage.close(); @@ -989,6 +1025,9 @@ class Playwright extends Helper { * ``` */ async openNewTab(options) { + if (this.isElectron) { + throw new Error('Cannot open new tabs inside an Electron container'); + } await this._setPage(await this.browserContext.newPage(options)); return this._waitForAction(); } diff --git a/package.json b/package.json index 0d25dce0a..ec8e0c582 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "contributor-faces": "^1.0.3", "documentation": "^12.3.0", "dtslint": "^3.6.12", + "electron": "^12.0.0", "eslint": "^6.8.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-import": "^2.22.1", @@ -120,7 +121,7 @@ "mocha-parallel-tests": "^2.3.0", "nightmare": "^3.0.2", "nodemon": "^1.19.4", - "playwright": "^1.6.2", + "playwright": "^1.9.1", "protractor": "^5.4.4", "puppeteer": "^4.0.0", "qrcode-terminal": "^0.12.0", diff --git a/test/data/electron/index.html b/test/data/electron/index.html new file mode 100644 index 000000000..5a8f46c75 --- /dev/null +++ b/test/data/electron/index.html @@ -0,0 +1,10 @@ + + + + + Hello World! + + +

Hello World!

+ + \ No newline at end of file diff --git a/test/data/electron/index.js b/test/data/electron/index.js new file mode 100644 index 000000000..cc56d244f --- /dev/null +++ b/test/data/electron/index.js @@ -0,0 +1,13 @@ +const { app, BrowserWindow } = require('electron'); + +function createWindow() { + const window = new BrowserWindow({ + width: 500, + height: 700, + show: false, + }); + + window.loadFile('index.html'); +} + +app.whenReady().then(createWindow); diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 32efbe4b9..9a8dd84d3 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -906,3 +906,77 @@ describe('Playwright - PERSISTENT', () => { assert.equal(I._getType(), 'BrowserContext'); }); }); + +describe('Playwright - Electron', () => { + before(() => { + global.codecept_dir = path.join(__dirname, '/../data'); + + I = new Playwright({ + waitForTimeout: 5000, + waitForAction: 500, + restart: true, + browser: 'electron', + electron: { + executablePath: require('electron'), + args: [path.join(codecept_dir, '/electron/')], + }, + }); + I._init(); + return I._beforeSuite(); + }); + + describe('#amOnPage', () => { + it('should throw an error', async () => { + try { + await I.amOnPage('/'); + throw Error('It should never get this far'); + } catch (e) { + e.message.should.include('Cannot open pages inside an Electron container'); + } + }); + }); + + describe('#openNewTab', () => { + it('should throw an error', async () => { + try { + await I.openNewTab(); + throw Error('It should never get this far'); + } catch (e) { + e.message.should.include('Cannot open new tabs inside an Electron container'); + } + }); + }); + + describe('#switchToNextTab', () => { + it('should throw an error', async () => { + try { + await I.switchToNextTab(); + throw Error('It should never get this far'); + } catch (e) { + e.message.should.include('Cannot switch tabs inside an Electron container'); + } + }); + }); + + describe('#switchToPreviousTab', () => { + it('should throw an error', async () => { + try { + await I.switchToNextTab(); + throw Error('It should never get this far'); + } catch (e) { + e.message.should.include('Cannot switch tabs inside an Electron container'); + } + }); + }); + + describe('#closeCurrentTab', () => { + it('should throw an error', async () => { + try { + await I.closeCurrentTab(); + throw Error('It should never get this far'); + } catch (e) { + e.message.should.include('Cannot close current tab inside an Electron container'); + } + }); + }); +});