diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml new file mode 100644 index 00000000..9f137529 --- /dev/null +++ b/.github/workflows/e2e-tests.yaml @@ -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/ + diff --git a/.gitignore b/.gitignore index da134307..97f74bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ npm-debug.log.* *.css.d.ts *.sass.d.ts *.scss.d.ts + +test-results diff --git a/e2e-tests/helpers/electron.helpers.ts b/e2e-tests/helpers/electron.helpers.ts new file mode 100644 index 00000000..0b6363bb --- /dev/null +++ b/e2e-tests/helpers/electron.helpers.ts @@ -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 { + 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 + // / + // .app/ + // Contents/ + // MacOS/ + // (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 + // / + // .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; +} \ No newline at end of file diff --git a/e2e-tests/main.test.ts b/e2e-tests/main.test.ts new file mode 100644 index 00000000..fab98a51 --- /dev/null +++ b/e2e-tests/main.test.ts @@ -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(); +}); \ No newline at end of file diff --git a/e2e-tests/models/add-version-panel.class.ts b/e2e-tests/models/add-version-panel.class.ts new file mode 100644 index 00000000..43a40abe --- /dev/null +++ b/e2e-tests/models/add-version-panel.class.ts @@ -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{ + return this._yearsTabBar.getByText(year).click(); + } + + clickVersion(manifest: string): Promise{ + return this._page.locator(`#version-item-${manifest}`).click(); + } + + get downloadVersionButton(): Locator { return this._downloadVersionButton; } + +} \ No newline at end of file diff --git a/e2e-tests/models/main-window.class.ts b/e2e-tests/models/main-window.class.ts new file mode 100644 index 00000000..6e1a6ad7 --- /dev/null +++ b/e2e-tests/models/main-window.class.ts @@ -0,0 +1,36 @@ +import { Page, Locator } from "playwright" + +export class MainWindow { + + protected readonly _page: Page; + + private readonly _bsmLogo: Locator; + private readonly _addVersionButton: Locator; + private readonly _settingsButton: Locator; + private readonly _sharedContentButton: Locator; + + constructor(page: Page) { + this._page = page; + + this._bsmLogo = page.locator("#bsm-logo"); + this._addVersionButton = page.locator("#add-version-btn"); + this._settingsButton = page.locator("#settings-btn"); + this._sharedContentButton = page.locator("#shared-contents-btn"); + } + public async clickBsmLogo(): Promise { + return this._bsmLogo.click(); + } + + public async clickAddVersionButton(): Promise { + return this._addVersionButton.click(); + } + + public async clickSettingsButton(): Promise { + return this._settingsButton.click(); + } + + public async clickSharedContentButton(): Promise { + return this._sharedContentButton.click(); + } + +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8fda678d..d9900f31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ }, "devDependencies": { "@electron/rebuild": "^3.2.13", + "@playwright/test": "^1.38.1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.5", "@teamsupercell/typings-for-css-modules-loader": "^2.5.1", "@testing-library/jest-dom": "^5.16.5", @@ -81,6 +82,7 @@ "@types/webpack-env": "^1.18.0", "@typescript-eslint/eslint-plugin": "^5.34.0", "@typescript-eslint/parser": "^5.34.0", + "asar": "^3.2.0", "autoprefixer": "^10.4.8", "browserslist-config-erb": "^0.0.3", "chalk": "^4.1.2", @@ -114,6 +116,8 @@ "lint-staged": "^12.5.0", "mini-css-extract-plugin": "^2.6.1", "opencollective-postinstall": "^2.0.3", + "playwright": "^1.38.1", + "playwright-core": "^1.38.1", "postcss": "^8.4.16", "postcss-loader": "^6.2.1", "prettier": "^2.7.1", @@ -137,7 +141,8 @@ "webpack-bundle-analyzer": "^4.6.1", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.10.0", - "webpack-merge": "^5.8.0" + "webpack-merge": "^5.8.0", + "xvfb-maybe": "^0.2.1" } }, "node_modules/@adobe/css-tools": { @@ -2015,6 +2020,21 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@playwright/test": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "dev": true, + "dependencies": { + "playwright": "1.38.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz", @@ -4123,6 +4143,48 @@ "node": ">=0.10.0" } }, + "node_modules/asar": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", + "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", + "dev": true, + "dependencies": { + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + }, + "optionalDependencies": { + "@types/glob": "^7.1.1" + } + }, + "node_modules/asar/node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -14451,6 +14513,36 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "dev": true, + "dependencies": { + "playwright-core": "1.38.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -19081,6 +19173,46 @@ "node": ">=0.4" } }, + "node_modules/xvfb-maybe": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xvfb-maybe/-/xvfb-maybe-0.2.1.tgz", + "integrity": "sha512-9IyRz3l6Qyhl6LvnGRF5jMPB4oBEepQnuzvVAFTynP6ACLLSevqigICJ9d/+ofl29m2daeaVBChnPYUnaeJ7yA==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "which": "^1.2.4" + }, + "bin": { + "xvfb-maybe": "src/xvfb-maybe.js" + } + }, + "node_modules/xvfb-maybe/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/xvfb-maybe/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/xvfb-maybe/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -20675,6 +20807,15 @@ "rimraf": "^3.0.2" } }, + "@playwright/test": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "dev": true, + "requires": { + "playwright": "1.38.1" + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz", @@ -22400,6 +22541,38 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==" }, + "asar": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "dependencies": { + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "optional": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + } + } + }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -30236,6 +30409,22 @@ "find-up": "^3.0.0" } }, + "playwright": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.38.1" + } + }, + "playwright-core": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "dev": true + }, "plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -33600,6 +33789,42 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "xvfb-maybe": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xvfb-maybe/-/xvfb-maybe-0.2.1.tgz", + "integrity": "sha512-9IyRz3l6Qyhl6LvnGRF5jMPB4oBEepQnuzvVAFTynP6ACLLSevqigICJ9d/+ofl29m2daeaVBChnPYUnaeJ7yA==", + "dev": true, + "requires": { + "debug": "^2.2.0", + "which": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3e8e3c40..3331da81 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", "test": "jest", - "publish": "npm run build && electron-builder -c.win.certificateSha1=842a817a51e2a1d360fcd62f54bf5f9193e919e1 --publish always --win --x64" + "publish": "npm run build && electron-builder -c.win.certificateSha1=842a817a51e2a1d360fcd62f54bf5f9193e919e1 --publish always --win --x64", + "e2e-test": "npm run package && xvfb-maybe npx playwright test --workers=1", + "e2e-test-ci": "xvfb-maybe npx playwright test --workers=1" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -149,6 +151,7 @@ }, "devDependencies": { "@electron/rebuild": "^3.2.13", + "@playwright/test": "^1.38.1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.5", "@teamsupercell/typings-for-css-modules-loader": "^2.5.1", "@testing-library/jest-dom": "^5.16.5", @@ -175,6 +178,7 @@ "@types/webpack-env": "^1.18.0", "@typescript-eslint/eslint-plugin": "^5.34.0", "@typescript-eslint/parser": "^5.34.0", + "asar": "^3.2.0", "autoprefixer": "^10.4.8", "browserslist-config-erb": "^0.0.3", "chalk": "^4.1.2", @@ -208,6 +212,8 @@ "lint-staged": "^12.5.0", "mini-css-extract-plugin": "^2.6.1", "opencollective-postinstall": "^2.0.3", + "playwright": "^1.38.1", + "playwright-core": "^1.38.1", "postcss": "^8.4.16", "postcss-loader": "^6.2.1", "prettier": "^2.7.1", @@ -231,7 +237,8 @@ "webpack-bundle-analyzer": "^4.6.1", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.10.0", - "webpack-merge": "^5.8.0" + "webpack-merge": "^5.8.0", + "xvfb-maybe": "^0.2.1" }, "dependencies": { "@electron/fuses": "^1.6.2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..31412ebe --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e-tests", + testMatch: "**/*.test.ts", + timeout: 200000, + fullyParallel: true, + retries: 1, +}); diff --git a/release/app/package-lock.json b/release/app/package-lock.json index a784f062..76622776 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "bs-manager", - "version": "1.4.2", + "version": "1.4.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bs-manager", - "version": "1.4.2", + "version": "1.4.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/main/main.ts b/src/main/main.ts index 6cf82c74..22f889d1 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -23,6 +23,7 @@ import { LivShortcut } from "./services/liv/liv-shortcut.service"; import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service"; const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true"; +const isE2E = process.env.E2E_BUILD === "true"; log.transports.file.level = "info"; log.transports.file.resolvePath = () => { @@ -54,7 +55,7 @@ const installExtensions = async () => { .catch(log.error); }; -const createWindow = async (window: AppWindow = "launcher.html") => { +const createWindow = async (window: AppWindow) => { if (isDebug) { await installExtensions(); } @@ -98,7 +99,7 @@ if (!gotTheLock) { const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg)); if (!deepLink) { - createWindow(); + createWindow(!isE2E ? "launcher.html" : "index.html"); } else { DeepLinkService.getInstance().dispatchLinkOpened(deepLink); } diff --git a/src/renderer/components/available-versions/available-version-item.component.tsx b/src/renderer/components/available-versions/available-version-item.component.tsx index 605c054d..f0f25930 100644 --- a/src/renderer/components/available-versions/available-version-item.component.tsx +++ b/src/renderer/components/available-versions/available-version-item.component.tsx @@ -23,7 +23,7 @@ export const AvailableVersionItem = memo(function AvailableVersionItem({version, const formatedDate = useConstant(() => dateFormat(+version.ReleaseDate * 1000, "ddd. d mmm yyyy")); return ( - setHovered(true)} onHoverEnd={() => setHovered(false)}> + setHovered(true)} onHoverEnd={() => setHovered(false)}>
{version.recommended && ( diff --git a/src/renderer/components/available-versions/available-versions-slide.component.tsx b/src/renderer/components/available-versions/available-versions-slide.component.tsx index 89f9b3c4..b6318764 100644 --- a/src/renderer/components/available-versions/available-versions-slide.component.tsx +++ b/src/renderer/components/available-versions/available-versions-slide.component.tsx @@ -29,7 +29,7 @@ export function AvailableVersionsSlide({ versions }: Props) { } return ( -
    +
      {getVersions().map(version => ( setSelectedVersion(version)}/> ))} diff --git a/src/renderer/components/available-versions/available-versions-slider.component.tsx b/src/renderer/components/available-versions/available-versions-slider.component.tsx index 02f45b9f..b256b626 100644 --- a/src/renderer/components/available-versions/available-versions-slider.component.tsx +++ b/src/renderer/components/available-versions/available-versions-slider.component.tsx @@ -26,7 +26,7 @@ export function AvailableVersionsSlider() { return (
      - +
        {availableYears.map(year => ( diff --git a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx index b4e1355d..0e2eef96 100644 --- a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx +++ b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx @@ -31,7 +31,7 @@ export const BsManagerIcon = memo(({ className }: { className?: string }) => { }; return ( - 0 ? "playing" : "idle"} onClick={clickAction}> +