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

[chore] add simple windows e2e tests #327

Closed
wants to merge 18 commits into from
Closed
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
48 changes: 48 additions & 0 deletions .github/workflows/e2e-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name : e2e-tests

on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
workflow_dispatch:

jobs:
e2e-tests-windows:
runs-on: windows-latest

steps:
- name: Cleanup files
run: cmd /r dir

- name: Checkout
uses: actions/checkout@v3

- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Install Playwright deps
run: npx playwright install --with-deps

- name: Build
run: npm run build

- name: Package
run: npm exec -c 'electron-builder build --publish "never" --win --x64'

- name: Run e2e tests
run: npm run e2e-test-ci

- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-output
path: test-results/

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ npm-debug.log.*
*.css.d.ts
*.sass.d.ts
*.scss.d.ts

test-results
243 changes: 243 additions & 0 deletions e2e-tests/helpers/electron.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/**
* Code taken from https://github.com/kubeshop/monokle/blob/main/tests/electronHelpers.ts
* don't tunch unless you know what you're doing
*/

import { Page } from "@playwright/test";
import path from "path";
import {ElectronApplication, _electron as electron} from 'playwright-core';
import { pause } from "shared/helpers/promise.helpers";
import log from "electron-log";
import * as fs from 'fs';
import * as ASAR from 'asar';

export const TEST_OUTPUT_DIR = 'test-results';

export async function startApp(): Promise<StartAppResponse> {
const latestBuild = findLatestBuild();
const appInfo = parseElectronApp(latestBuild);
const electronApp = await electron.launch({
args: [appInfo.main],
env: {
...process.env,
E2E_BUILD: "true",
},
executablePath: appInfo.executable,
recordVideo: {
dir: getRecordingPath(appInfo.platform),
size: {
width: 1200,
height: 800,
},
},
});

const appWindow = await electronApp.firstWindow();

if (!appWindow) {
throw new Error('Unable to get main window');
}

await pause(3000);

appWindow.on('console', log.info);
appWindow.screenshot({path: path.join(TEST_OUTPUT_DIR, "initial-screen.png")});

return {appWindow, appInfo, electronApp};
}

export function findLatestBuild(): string {
const rootDir = path.resolve('./');
const outDir = path.join(rootDir, 'release', 'build');
const builds = fs.readdirSync(outDir);
const platforms = ['win32', 'win', 'windows', 'darwin', 'mac', 'macos', 'osx', 'linux', 'ubuntu'];

const latestBuild = builds
.map(fileName => {
// make sure it's a directory with "-" delimited platform in its name
const stats = fs.statSync(path.join(outDir, fileName));
const isBuild = fileName.toLocaleLowerCase().split('-').some(part => platforms.includes(part));

if(!stats.isDirectory() || !isBuild){ return undefined; }

return {
name: fileName,
time: fs.statSync(path.join(outDir, fileName)).mtimeMs,
};
})
.sort((a, b) => b.time - a.time)
.map(file => file?.name)[0];

if (!latestBuild) {
throw new Error('No build found in out directory');
}

return path.join(outDir, latestBuild);
}

/**
* Given a directory containing an Electron app build,
* return the path to the app's executable and the path to the app's main file.
*/
export function parseElectronApp(buildDir: string): ElectronAppInfo {
log.info(`Parsing Electron app in ${buildDir}`);

let platform: string | undefined;

if (buildDir.endsWith('.app')) {
buildDir = path.dirname(buildDir);
platform = 'darwin';
}
else if (buildDir.endsWith('.exe')) {
buildDir = path.dirname(buildDir);
platform = 'win32';
}

const baseName = path.basename(buildDir).toLowerCase();
if (!platform) {
// parse the directory name to figure out the platform
if (baseName.includes('win')) {
platform = 'win32';
}
if (baseName.includes('linux') || baseName.includes('ubuntu') || baseName.includes('debian')) {
platform = 'linux';
}
if (baseName.includes('darwin') || baseName.includes('mac') || baseName.includes('osx')) {
platform = 'darwin';
}
}

if (!platform) {
throw new Error(`Platform not found in directory name: ${baseName}`);
}

let arch: Architecture;
if (baseName.includes('x32') || baseName.includes('i386')) {
arch = 'x32';
}
if (baseName.includes('x64')) {
arch = 'x64';
}
if (baseName.includes('arm64')) {
arch = 'arm64';
}

let executable: string;
let main: string;
let name: string;
let asar: boolean;
let resourcesDir: string;

if (platform === 'darwin') {
// MacOS Structure
// <buildDir>/
// <appName>.app/
// Contents/
// MacOS/
// <appName> (executable)
// Info.plist
// PkgInfo
// Resources/
// electron.icns
// file.icns
// app.asar (asar bundle) - or -
// app
// package.json
// (your app structure)

const list = fs.readdirSync(buildDir);
const appBundle = list.find(fileName => {
return fileName.endsWith('.app');
});

const appDir = path.join(buildDir, appBundle, 'Contents', 'MacOS');
const appName = fs.readdirSync(appDir)[0];
executable = path.join(appDir, appName);

resourcesDir = path.join(buildDir, appBundle, 'Contents', 'Resources');
const resourcesList = fs.readdirSync(resourcesDir);
asar = resourcesList.includes('app.asar');

let packageJson: {main: string; name: string};
if (asar) {
const asarPath = path.join(resourcesDir, 'app.asar');
packageJson = JSON.parse(ASAR.extractFile(asarPath, 'package.json').toString('utf8'));
main = path.join(asarPath, packageJson.main);
} else {
packageJson = JSON.parse(fs.readFileSync(path.join(resourcesDir, 'app', 'package.json'), 'utf8'));
main = path.join(resourcesDir, 'app', packageJson.main);
}
name = packageJson.name;
}
else if (platform === 'win32') {
// Windows Structure
// <buildDir>/
// <appName>.exe (executable)
// resources/
// app.asar (asar bundle) - or -
// app
// package.json
// (your app structure)

const list = fs.readdirSync(buildDir);
const exe = list.find(fileName => {
return fileName.endsWith('.exe');
});

executable = path.join(buildDir, exe);

resourcesDir = path.join(buildDir, 'resources');
const resourcesList = fs.readdirSync(resourcesDir);
asar = resourcesList.includes('app.asar');

let packageJson: {main: string; name: string};

if (asar) {
const asarPath = path.join(resourcesDir, 'app.asar');
packageJson = JSON.parse(ASAR.extractFile(asarPath, 'package.json').toString('utf8'));
main = path.join(asarPath, packageJson.main);
} else {
packageJson = JSON.parse(fs.readFileSync(path.join(resourcesDir, 'app', 'package.json'), 'utf8'));
main = path.join(resourcesDir, 'app', packageJson.main);
}
name = packageJson.name;
}
else {
/** @todo add support for linux */
throw new Error(`Platform not supported: ${platform}`);
}

return { executable, main, asar, name, platform, resourcesDir, arch };
}

export function getRecordingPath(...paths: string[]): string {
return path.join(TEST_OUTPUT_DIR, ...paths);
}

export function getMainWindow(windows: Page[]): Page {
const mainWindow = windows.find(w => w.url().includes('index.html'));
return mainWindow;
}

type Architecture = 'x64' | 'x32' | 'arm64';
export interface ElectronAppInfo {
/** Path to the app's executable file */
executable: string;
/** Path to the app's main (JS) file */
main: string;
/** Name of the app */
name: string;
/** Resources directory */
resourcesDir: string;
/** True if the app is using asar */
asar: boolean;
/** OS platform */
platform: 'darwin' | 'win32' | 'linux';
arch: Architecture;
}

interface StartAppResponse {
electronApp: ElectronApplication;
appWindow: Page;
appInfo: ElectronAppInfo;
}
55 changes: 55 additions & 0 deletions e2e-tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {Page} from 'playwright';
import { test, expect } from '@playwright/test';
import { getRecordingPath, startApp } from './helpers/electron.helpers';
import { pause } from 'shared/helpers/promise.helpers';
import { MainWindow } from './models/main-window.class';
import { AddVersionPanel } from './models/add-version-panel.class';


let appWindow: Page;
let mainWindow: MainWindow;
let addVersionPanel: AddVersionPanel;

test.beforeAll(async () => {
const startAppResponse = await startApp();
appWindow = startAppResponse.appWindow;
mainWindow = new MainWindow(appWindow);
addVersionPanel = new AddVersionPanel(appWindow);
});

test.beforeEach(async () => {
await pause(1000);
await mainWindow.clickAddVersionButton();
});

test.afterEach(async () => {
await pause(1000);
});

test('should be able to select then unselect a bs version', async () => {
const versionManifest = "8948172000430595334";
await addVersionPanel.selectYear("2021");
await pause(1000);

await addVersionPanel.clickVersion(versionManifest); // select version 0.12.2
await pause(1000);

expect(await addVersionPanel.downloadVersionButton.isVisible()).toBeTruthy();

await addVersionPanel.clickVersion(versionManifest); // deselect version 0.12.2
await pause(3000);

expect(await addVersionPanel.downloadVersionButton.isVisible()).toBeFalsy();

// sadly we can't test if the download works or not :( (Steam ids, dot-net, etc)
});

test('should be able to download a map then delete it', async () => {
// TODO: implement this test
});

test.afterAll(async () => {
await pause(3000);
await appWindow.screenshot({path: getRecordingPath("final-screen.png")});
await appWindow.close();
});
26 changes: 26 additions & 0 deletions e2e-tests/models/add-version-panel.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Page, Locator } from "playwright";
import { MainWindow } from "./main-window.class";

export class AddVersionPanel extends MainWindow {

private readonly _yearsTabBar: Locator;
private readonly _downloadVersionButton: Locator;

constructor(page: Page) {
super(page);

this._yearsTabBar = page.locator("#version-years-tab-bar");
this._downloadVersionButton = page.locator("#download-version-btn");
}

selectYear(year: string): Promise<void>{
return this._yearsTabBar.getByText(year).click();
}

clickVersion(manifest: string): Promise<void>{
return this._page.locator(`#version-item-${manifest}`).click();
}

get downloadVersionButton(): Locator { return this._downloadVersionButton; }

}
Loading
Loading