From 4dbc9d7be40637cd685c67fbd6f6f01fc05e51c0 Mon Sep 17 00:00:00 2001 From: Oleksandr_Halichenko Date: Tue, 9 Sep 2025 11:56:23 +0300 Subject: [PATCH 1/2] added electron steps --- .github/workflows/pull-request.yml | 21 +++++++++ CHANGELOG.md | 17 ++++++++ package-lock.json | 20 ++++----- package.json | 6 +-- src/QavajsPlaywrightElectronWorld.ts | 12 +++++- src/electron.ts | 48 +++++++++++++++++++++ test-e2e/apps/electron/main.js | 20 +++------ test-e2e/apps/electron/menuTemplate.js | 34 +++++++++++++++ test-e2e/apps/electron/newWindow.js | 14 ++++++ test-e2e/features/electron/electron.feature | 14 +++++- test-e2e/playwright.electron.config.ts | 7 ++- 11 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 src/electron.ts create mode 100644 test-e2e/apps/electron/menuTemplate.js create mode 100644 test-e2e/apps/electron/newWindow.js diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e7224cb..04d5351 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -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.xml' + fail_on_failure: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 015f6f1..8a5abf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 0e50c32..f7b3057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@qavajs/playwright", - "version": "3.3.0", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@qavajs/playwright", - "version": "3.3.0", + "version": "3.4.0", "license": "MIT", "dependencies": { "@playwright/test": "^1.55.0", @@ -15,8 +15,8 @@ }, "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" @@ -754,9 +754,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, "license": "MIT", "dependencies": { @@ -1354,9 +1354,9 @@ "license": "MIT" }, "node_modules/electron": { - "version": "37.3.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-37.3.1.tgz", - "integrity": "sha512-7DhktRLqhe6OJh/Bo75bTI0puUYEmIwSzMinocgO63mx3MVjtIn2tYMzLmAleNIlud2htkjpsMG2zT4PiTCloA==", + "version": "38.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-38.0.0.tgz", + "integrity": "sha512-egljptiPJqbL/oamFCEY+g3RNeONWTVxZSGeyLqzK8xq106JhzuxnhJZ3sxt4DzJFaofbGyGJA37Oe9d+gVzYw==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index 9a83ce7..45a5fe1 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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" diff --git a/src/QavajsPlaywrightElectronWorld.ts b/src/QavajsPlaywrightElectronWorld.ts index 8e3e36d..77664c3 100644 --- a/src/QavajsPlaywrightElectronWorld.ts +++ b/src/QavajsPlaywrightElectronWorld.ts @@ -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 @@ -22,7 +22,17 @@ const electron = test.extend({ 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; + } } \ No newline at end of file diff --git a/src/electron.ts b/src/electron.ts new file mode 100644 index 0000000..82bdcc6 --- /dev/null +++ b/src/electron.ts @@ -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() }); +}); \ No newline at end of file diff --git a/test-e2e/apps/electron/main.js b/test-e2e/apps/electron/main.js index 03fad56..b269178 100644 --- a/test-e2e/apps/electron/main.js +++ b/test-e2e/apps/electron/main.js @@ -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({ @@ -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() @@ -38,6 +29,9 @@ app.whenReady().then(() => { } event.returnValue = null }) + + const menu = Menu.buildFromTemplate(menuTemplate); + Menu.setApplicationMenu(menu); }) app.on('window-all-closed', function () { diff --git a/test-e2e/apps/electron/menuTemplate.js b/test-e2e/apps/electron/menuTemplate.js new file mode 100644 index 0000000..8b21de0 --- /dev/null +++ b/test-e2e/apps/electron/menuTemplate.js @@ -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', + } + ] + } +]; \ No newline at end of file diff --git a/test-e2e/apps/electron/newWindow.js b/test-e2e/apps/electron/newWindow.js new file mode 100644 index 0000000..e25a3b3 --- /dev/null +++ b/test-e2e/apps/electron/newWindow.js @@ -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') +} \ No newline at end of file diff --git a/test-e2e/features/electron/electron.feature b/test-e2e/features/electron/electron.feature index 5f8c524..9feb374 100644 --- a/test-e2e/features/electron/electron.feature +++ b/test-e2e/features/electron/electron.feature @@ -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' \ No newline at end of file + * 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' \ No newline at end of file diff --git a/test-e2e/playwright.electron.config.ts b/test-e2e/playwright.electron.config.ts index 2f52173..87c9190 100644 --- a/test-e2e/playwright.electron.config.ts +++ b/test-e2e/playwright.electron.config.ts @@ -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' + ], } }, } From cbd615c6e0c635d963bcade9537d8b17509e198b Mon Sep 17 00:00:00 2001 From: Oleksandr_Halichenko Date: Tue, 9 Sep 2025 11:59:02 +0300 Subject: [PATCH 2/2] updated electron test report path --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 04d5351..9222b15 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -47,5 +47,5 @@ jobs: uses: mikepenz/action-junit-report@v4 if: always() with: - report_paths: './test-e2e/report.xml' + report_paths: './test-e2e/report/report.xml' fail_on_failure: true