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
21 changes: 21 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,24 @@ jobs:
with:
report_paths: './test-e2e/report/report.xml'
fail_on_failure: true

test-electron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm run build
- name: setup virtual display
run: |
export DISPLAY=:99
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24" npm run test:e2e:electron
- name: junit report (electron)
uses: mikepenz/action-junit-report@v4
if: always()
with:
report_paths: './test-e2e/report/report.xml'
fail_on_failure: true
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
:pencil: - chore
:microscope: - experimental

## [3.4.0]
- :rocket: added `electron` world property to interact with main electron process
- :rocket: added step to interact with electron app menu
```gherkin
When I click 'Test > Open Page' electron menu
```

- :rocket: added capability to execute script on electron main process
```gherkin
Scenario: evaluate script on main process
When I execute '$js(async ({ app }) => app.showAboutPanel())' script on electron app

Scenario: evaluate script on main process and save result to memory
When I execute '$js(async ({ app }) => app.getAppPath())' script on electron app and save result as 'appPath'
Then I expect '$appPath' memory value to contain 'test-e2e/apps/electron'
```

## [3.3.0]
- :rocket: added `to satisfy` validation to verify user-defined expectation provided as predicate
```Gherkin
Expand Down
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@qavajs/playwright",
"version": "3.3.0",
"version": "3.4.0",
"description": "steps to interact with playwright",
"main": "./index.js",
"scripts": {
Expand All @@ -26,8 +26,8 @@
"homepage": "https://github.com/qavajs/playwright#readme",
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.3.0",
"electron": "^37.3.1",
"@types/node": "^24.3.1",
"electron": "^38.0.0",
"express": "^5.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
Expand Down
12 changes: 11 additions & 1 deletion src/QavajsPlaywrightElectronWorld.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QavajsPlaywrightWorld } from './QavajsPlaywrightWorld';
import { _electron, ElectronApplication, test, WorkerInfo } from '@playwright/test';
import { _electron, Browser, BrowserContext, ElectronApplication, Page, test, WorkerInfo } from '@playwright/test';

type ElectronFixture = {
browser: ElectronApplication
Expand All @@ -22,7 +22,17 @@ const electron = test.extend<ElectronFixture>({

export class QavajsPlaywrightElectronWorld extends QavajsPlaywrightWorld {
test = electron;
electron!: ElectronApplication;
context!: BrowserContext;
page!: Page;
constructor(options: any) {
super(options);
}

init = ({ browser, context, page }: { browser: ElectronApplication, context: BrowserContext, page: Page } ) => {
this.browser = browser as unknown as Browser;
this.electron = browser;
this.context = context;
this.page = page;
}
}
48 changes: 48 additions & 0 deletions src/electron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { When } from '@qavajs/playwright-runner-adapter';
import { MemoryValue } from './types';
import { QavajsPlaywrightElectronWorld } from './QavajsPlaywrightElectronWorld';

/**
* Execute client function on electron process and save result into memory
* @param {string} functionKey - memory key of function
* @param {string} memoryKey - memory key to store result
* @example I execute '$fn' function and save result as 'result' // fn is function reference
* @example I execute '$js(async ({ app }) => app.getAppPath())' function and save result as 'scroll'
*/
When('I execute {value} function/script on electron app', async function (this: QavajsPlaywrightElectronWorld, fn: MemoryValue) {
await this.electron.evaluate(await fn.value());
});

/**
* Execute client function on electron process and save result into memory
* @param {string} functionKey - memory key of function
* @param {string} memoryKey - memory key to store result
* @example I execute '$fn' function and save result as 'result' // fn is function reference
* @example I execute '$js(async ({ app }) => app.getAppPath())' function on electron app and save result as 'result'
*/
When('I execute {value} function/script on electron app and save result as {value}', async function (this: QavajsPlaywrightElectronWorld, fn: MemoryValue, memoryKey: MemoryValue) {
memoryKey.set(await this.electron.evaluate(await fn.value()));
});

/**
* Click electron menu
* @param {string} menuPath - menu path
* @example I click 'File > Edit' electron menu
*/
When('I click {value} electron menu', async function (this: QavajsPlaywrightElectronWorld, menu: MemoryValue) {
await this.electron.evaluate(async ({ Menu }: { Menu: any }, { menuPath }: { menuPath: string }) => {
const path = menuPath.split(/\s*>\s*/) as string[];
const menu = Menu.getApplicationMenu();
if (!menu) throw new Error('Menu is not set');
const firstMenu = path.shift() as string;
const findItemPredicate = (item: string) => (menu: any) => menu.label === item || menu.role === item;
let currentMenu = menu.items.find(findItemPredicate(firstMenu));
if (!currentMenu) throw new Error(`Menu '${firstMenu}' is not found`);
for (const pathItem of path) {
if (!currentMenu?.submenu) throw new Error(`Menu '${pathItem}' does not have submenu`);
currentMenu = currentMenu.submenu.items.find(findItemPredicate(pathItem));
if (!currentMenu) throw new Error(`Menu '${pathItem}' is not found`);
}
currentMenu.click()
}, { menuPath: await menu.value() });
});
20 changes: 7 additions & 13 deletions test-e2e/apps/electron/main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
const path = require('node:path')
const menuTemplate = require('./menuTemplate');
const handleOpenNewWindow = require('./newWindow');

function createWindow () {
const mainWindow = new BrowserWindow({
Expand All @@ -10,20 +12,9 @@ function createWindow () {
}
})

mainWindow.loadFile('index.html')
return mainWindow.loadFile('index.html')
}

function handleOpenNewWindow() {
const newWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

newWindow.loadFile('newWindow.html')
}
app.whenReady().then(() => {
createWindow()

Expand All @@ -38,6 +29,9 @@ app.whenReady().then(() => {
}
event.returnValue = null
})

const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
})

app.on('window-all-closed', function () {
Expand Down
34 changes: 34 additions & 0 deletions test-e2e/apps/electron/menuTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const handleOpenNewWindow = require("./newWindow");

module.exports = [
{
label: 'Default',
submenu: [
{
label: 'Open Page',
click: () => {
handleOpenNewWindow()
}
}
]
},
{
label: 'Test',
submenu: [
{
label: 'Open Page',
click: () => {
handleOpenNewWindow()
}
}
]
},
{
label: 'Help',
submenu: [
{
role: 'about',
}
]
}
];
14 changes: 14 additions & 0 deletions test-e2e/apps/electron/newWindow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { BrowserWindow } = require('electron')
const path = require('node:path')

module.exports = function handleOpenNewWindow() {
const newWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

newWindow.loadFile('newWindow.html')
}
14 changes: 13 additions & 1 deletion test-e2e/features/electron/electron.feature
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,16 @@ Feature: electron
* I click 'Open New Window Electron Button'
* I switch to 'qavajs electron app new window' window
* I click 'Close Current Window Electron Button'
* I expect current url to contain 'newWindow.html'
* I expect current url to contain 'newWindow.html'

Scenario: evaluate script on main process
* I execute '$js(async ({ app }) => app.showAboutPanel())' script on electron app

Scenario: evaluate script on main process and save result to memory
* I execute '$js(async ({ app }) => app.getAppPath())' script on electron app and save result as 'appPath'
* I expect '$appPath' to contain 'test-e2e/apps/electron'

Scenario: open menu
* I click 'Test > Open Page' electron menu
* I switch to 'qavajs electron app new window' window
* I click 'Close Current Window Electron Button'
7 changes: 6 additions & 1 deletion test-e2e/playwright.electron.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export default defineConfig({
name: 'electron',
use: {
launchOptions: {
args: ['test-e2e/apps/electron/main.js'],
args: [
'test-e2e/apps/electron/main.js',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu'
],
}
},
}
Expand Down