Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat): detect app binary path for electron builder and forge #222

Merged
merged 7 commits into from
Oct 11, 2023
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
67 changes: 23 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ Enables cross-platform E2E testing of Electron apps via the extensive WebdriverI

Spiritual successor to [Spectron](https://github.com/electron-userland/spectron) ([RIP](https://github.com/electron-userland/spectron/issues/1045)).

### Features

Using this service makes testing Electron applications much easier as it takes care of the following:

- 🚗 auto-setup of required Chromedriver
- 📦 finds path to your bundled Electron application (if [Electron Forge](https://www.electronforge.io/) or [Electron Builder](https://www.electron.build/) is used)
- 🧩 enables ability to access Electron APIs within your tests
- 🕵️ allows to mock Electron APIs

## Installation

```bash
Expand All @@ -26,7 +35,7 @@ If you prefer to manage Chromedriver yourself you can install it directly or via

#### Service Managed

If you are not specifying a Chromedriver binary then the service will download and use the appropriate version for your app's Electron version. The Electron version of your app is determined by the version of Electron in your `package.json`, however you may want to override this behaviour - for instance, if the app you are testing is in a different repo from the tests. You can specify the Electron version manually by setting the `browserVersion` capability, as shown in the example configuration below.
If you are not specifying a Chromedriver binary then the service will download and use the appropriate version for your app's Electron version. The Electron version of your app is determined by the version of `electron` or `electron-nighly` in your `package.json`, however you may want to override this behaviour - for instance, if the app you are testing is in a different repo from the tests. You can specify the Electron version manually by setting the `browserVersion` capability, as shown in the example configuration below.

## Example Configuration

Expand All @@ -42,58 +51,28 @@ export const config = {
outputDir: 'logs',
// ...
services: ['electron'],
capabilities: [
{
'browserName': 'electron',
'wdio:electronServiceOptions': {
appBinaryPath: path.resolve(__dirname, 'dist', 'myElectronApplication.exe'),
},
},
],
capabilities: [{
'browserName': 'electron'
}],
// ...
};
```

If you are building your app using [`electron-builder`](https://www.electron.build/), your configuration might resemble the following:

```js
// wdio.conf.js
import url from 'node:url';
import path from 'node:path';
import fs from 'node:fs/promises';
import { getBinaryPath } from 'wdio-electron-service/utils';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const pkg = JSON.parse(await fs.readFile('./package.json'));
The service will attempt to find the path to your bundled Electron application if you use [Electron Forge](https://www.electronforge.io/) or [Electron Builder](https://www.electron.build/) as bundler. You can provide a custom path to the binary via custom service capabilities, e.g.:

export const config = {
outputDir: 'logs',
// ...
services: ['electron'],
capabilities: [
{
'browserName': 'electron',
'browserVersion': '26.2.2', // optional override
'wdio:electronServiceOptions': {
// Use `getBinaryPath` to point to the right binary, e.g. given your `productName` is "myElectronApplication"
// it would set the binary depending on your OS to:
//
// Linux: ./dist/linux-unpacked/myElectronApplication
// MacOS: ./dist/mac-arm64/myElectronApplication.app/Contents/MacOS/myElectronApplication
// Windows: ./win-unpacked/myElectronApplication.exe
appBinaryPath: getBinaryPath(__dirname, pkg.build.productName),
appArgs: ['foo', 'bar=baz'],
},
},
],
// ...
};
```ts
capabilities: [{
'browserName': 'electron',
'wdio:electronServiceOptions': {
appBinaryPath: './path/to/bundled/electron/app.exe',
appArgs: ['foo', 'bar=baz'],
},
}],
```

### API Configuration

If you wish to use the Electron APIs then you will need to import (or require) the preload and main scripts in your app.
To import 3rd-party packages (node_modules) in your `preload.js`, you have to disable sandboxing in your `BrowserWindow` config.
If you wish to use the Electron APIs then you will need to import (or require) the preload and main scripts in your app. To import 3rd-party packages (node_modules) in your `preload.js`, you have to disable sandboxing in your `BrowserWindow` config.

It is not recommended to disable sandbox mode in production; to control this behaviour you can set the `NODE_ENV` environment variable when executing WDIO:

Expand Down
1 change: 0 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export default [
},
rules: {
...eslint.configs.recommended.rules,
'no-nested-ternary': 'error',
},
},
// Node & Electron main process files and scripts
Expand Down
10 changes: 0 additions & 10 deletions example-cjs/wdio.conf.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import path from 'node:path';
import fs from 'node:fs';

import { getBinaryPath } from 'wdio-electron-service/utils';

const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
const {
build: { productName },
} = packageJson;

process.env.TEST = 'true';

Expand All @@ -15,9 +7,7 @@ exports.config = {
capabilities: [
{
'browserName': 'electron',
'browserVersion': '27.0.0',
'wdio:electronServiceOptions': {
appBinaryPath: getBinaryPath(__dirname, productName),
appArgs: ['foo', 'bar=baz'],
},
},
Expand Down
8 changes: 6 additions & 2 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"types": ["node", "@wdio/globals/types"],
"typeRoots": ["./node_modules", "./node_modules/@types", "./../node_modules/@types", "../@types"],
"typeRoots": [
"./node_modules",
"./node_modules/@types",
"./../node_modules/@types",
"../@types"
],
"outDir": "dist"
},
"exclude": ["node_modules"],
Expand Down
9 changes: 0 additions & 9 deletions example/wdio.conf.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
/// <reference types="../@types/wdio-electron-service/utils.d.ts" />
import fs from 'node:fs';
import url from 'node:url';
import path from 'node:path';

import { getBinaryPath } from 'wdio-electron-service/utils';

const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
const {
build: { productName },
} = packageJson;

process.env.TEST = 'true';

Expand All @@ -18,9 +11,7 @@ export const config = {
capabilities: [
{
'browserName': 'electron',
'browserVersion': '28.0.0-nightly.20231009',
'wdio:electronServiceOptions': {
appBinaryPath: getBinaryPath(__dirname, productName),
appArgs: ['foo', 'bar=baz'],
},
},
Expand Down
4 changes: 0 additions & 4 deletions src/esm/constants.ts

This file was deleted.

81 changes: 79 additions & 2 deletions src/launcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import util from 'node:util';

import findVersions from 'find-versions';
import { readPackageUp, type NormalizedReadResult } from 'read-pkg-up';
import { SevereServiceError } from 'webdriverio';
import type { Services, Options, Capabilities } from '@wdio/types';

import log from './log.js';
import { getBinaryPath } from './utils.js';
import { getChromeOptions, getChromedriverOptions, getElectronCapabilities } from './capabilities.js';
import { getChromiumVersion } from './versions.js';
import type { ElectronServiceOptions } from './types.js';

const APP_NOT_FOUND_ERROR =
'Could not find Electron app at %s build with %s!\n' +
'If the application is not compiled, please do so before running your tests, via `%s`.\n' +
'Otherwise if the application is compiled at a different location, please specify the `appBinaryPath` option in your capabilities.';

export default class ElectronLaunchService implements Services.ServiceInstance {
#globalOptions: ElectronServiceOptions;
#projectRoot: string;
Expand All @@ -28,7 +38,11 @@ export default class ElectronLaunchService implements Services.ServiceInstance {
({ packageJson: { dependencies: {}, devDependencies: {} } } as NormalizedReadResult);

const { dependencies, devDependencies } = pkg.packageJson;
const pkgElectronVersion = dependencies?.electron || devDependencies?.electron;
const pkgElectronVersion =
dependencies?.electron ||
devDependencies?.electron ||
dependencies?.['electron-nightly'] ||
devDependencies?.['electron-nightly'];
const localElectronVersion = pkgElectronVersion ? findVersions(pkgElectronVersion, { loose: true })[0] : undefined;

if (!caps.length) {
Expand All @@ -43,7 +57,10 @@ export default class ElectronLaunchService implements Services.ServiceInstance {
const chromiumVersion = await getChromiumVersion(electronVersion);
log.debug(`found Electron v${electronVersion} with Chromedriver v${chromiumVersion}`);

const { appBinaryPath, appArgs } = Object.assign({}, this.#globalOptions, cap['wdio:electronServiceOptions']);
let { appBinaryPath, appArgs } = Object.assign({}, this.#globalOptions, cap['wdio:electronServiceOptions']);
if (!appBinaryPath) {
appBinaryPath = await detectBinaryPath(pkg);
}

const invalidPathOpts = appBinaryPath === undefined;
if (invalidPathOpts) {
Expand Down Expand Up @@ -82,3 +99,63 @@ export default class ElectronLaunchService implements Services.ServiceInstance {
});
}
}

/**
* detect the path to the Electron app binary
* @param pkg result of `readPackageUp`
* @param p process object (used for testing purposes)
* @returns path to the Electron app binary
*/
export async function detectBinaryPath(pkg: NormalizedReadResult, p = process) {
const appName: string = pkg.packageJson.build?.productName || pkg.packageJson.name;
if (!appName) {
return undefined;
}

const isForgeSetup = Boolean(
pkg.packageJson.config?.forge || Object.keys(pkg.packageJson.devDependencies || {}).includes('@electron-forge/cli'),
);
if (isForgeSetup) {
/**
* Electron Forge always bundles into an `out` directory, until this PR is merged:
* https://github.com/electron/forge/pull/2714
*/
const outDir = path.join(path.dirname(pkg.path), 'out', `${appName}-${p.platform}-${p.arch}`);
const appPath =
p.platform === 'darwin'
? path.join(outDir, `${appName}.app`, 'Contents', 'MacOS', appName)
: p.platform === 'win32'
? path.join(outDir, `${appName}.exe`)
: path.join(outDir, appName);
const appExists = await fs.access(appPath).then(
() => true,
() => false,
);
if (!appExists) {
throw new SevereServiceError(
util.format(APP_NOT_FOUND_ERROR, appPath, 'Electron Forge', 'npx electron-forge make'),
);
}
return appPath;
}

const isElectronBuilderSetup = Boolean(
pkg.packageJson.build?.appId || Object.keys(pkg.packageJson.devDependencies || {}).includes('electron-builder'),
);
if (isElectronBuilderSetup) {
const distDirName = pkg.packageJson.build?.directories?.output || 'dist';
const appPath = getBinaryPath(path.dirname(pkg.path), appName, distDirName, p);
const appExists = await fs.access(appPath).then(
() => true,
() => false,
);
if (!appExists) {
throw new SevereServiceError(
util.format(APP_NOT_FOUND_ERROR, appPath, 'Electron Builder', 'npx electron-builder build'),
);
}
return appPath;
}

return undefined;
}
12 changes: 5 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import path from 'node:path';

export function getBinaryPath(appPath: string, appName: string, distDirName = 'dist') {
export function getBinaryPath(appPath: string, appName: string, distDirName = 'dist', p = process) {
const SupportedPlatform = {
darwin: 'darwin',
linux: 'linux',
win32: 'win32',
};
const { platform, arch } = process;

if (!Object.values(SupportedPlatform).includes(platform)) {
throw new Error(`Unsupported platform: ${platform}`);
if (!Object.values(SupportedPlatform).includes(p.platform)) {
throw new Error(`Unsupported platform: ${p.platform}`);
}

const pathMap = {
darwin: path.join(arch === 'arm64' ? 'mac-arm64' : 'mac', `${appName}.app`, 'Contents', 'MacOS', appName),
darwin: path.join(p.arch === 'arm64' ? 'mac-arm64' : 'mac', `${appName}.app`, 'Contents', 'MacOS', appName),
linux: path.join('linux-unpacked', appName),
win32: path.join('win-unpacked', `${appName}.exe`),
};

const electronPath = pathMap[platform as keyof typeof SupportedPlatform];
const electronPath = pathMap[p.platform as keyof typeof SupportedPlatform];

return path.join(appPath, distDirName, electronPath);
}
Loading