Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions docs/playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Testing with Playwright

# Testing with Playwright <Badge text="Since 2.5" type="warning"/>

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.

Expand Down Expand Up @@ -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 }) => {
Expand All @@ -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.
Expand Down
73 changes: 56 additions & 17 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
/**
Expand All @@ -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.
Expand All @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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',
},
];
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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();
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions test/data/electron/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
13 changes: 13 additions & 0 deletions test/data/electron/index.js
Original file line number Diff line number Diff line change
@@ -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);
74 changes: 74 additions & 0 deletions test/helper/Playwright_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});
});