Compare commits

...

18 Commits

Author SHA1 Message Date
MathieuG-P c140d30e90 [chore] npm i 2024-01-12 22:10:18 +01:00
MathieuG-P a6826cc899 [chore] add missing scripts and asar package 2024-01-07 20:23:15 +01:00
MathieuG-P 1266da74d8 Merge branch 'master' into chore/e2e-tests 2024-01-07 20:18:20 +01:00
MathieuG-P bb05b14236 [chore] bypass udate properly + tweak first test 2023-10-08 17:12:55 +02:00
MathieuG-P 4f475d4224 [chore] tweak first test 2023-10-08 16:48:04 +02:00
MathieuG-P e55c146824 [chore] add install playwright deps 2023-10-08 16:27:40 +02:00
MathieuG-P 696eec5f2b [chore] test bypass auto update 2023-09-25 13:54:03 +02:00
MathieuG-P 2699b4a7cd [chore] nb retries to 1 2023-09-25 13:27:20 +02:00
MathieuG-P 36749a39cc [chore] update package-locks 2023-09-25 13:27:02 +02:00
MathieuG-P e13c641ba3 Merge branch 'master' into chore/e2e-tests 2023-09-25 13:06:54 +02:00
MathieuG-P 5385797e06 [chore] replace checkout by upload-artifact 2023-09-25 12:53:55 +02:00
MathieuG-P b13ee858b6 [chore] restaure auto updater 2023-09-24 19:11:42 +02:00
MathieuG-P 7c792d6520 [chore] try e2e tests with no auto-update 2023-09-24 18:13:09 +02:00
MathieuG-P d03ae069ca Update e2e-tests.yaml 2023-09-24 17:45:36 +02:00
MathieuG-P 3bf2d8814f Update e2e-tests.yaml 2023-09-24 17:33:07 +02:00
MathieuG-P 0e9f1fc8a1 [chore] reduce nb retries 2023-09-24 17:31:38 +02:00
MathieuG-P d3e65f9f92 [chore] fix "a step cannot have both the uses and run keys" 2023-09-24 17:10:06 +02:00
MathieuG-P 5909ac4373 [chore] add simple windows e2e tests 2023-09-24 16:49:29 +02:00
22 changed files with 690 additions and 20 deletions
+48
View File
@@ -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
View File
@@ -27,3 +27,5 @@ npm-debug.log.*
*.css.d.ts
*.sass.d.ts
*.scss.d.ts
test-results
+243
View File
@@ -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
View File
@@ -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();
});
@@ -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; }
}
+36
View File
@@ -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<void> {
return this._bsmLogo.click();
}
public async clickAddVersionButton(): Promise<void> {
return this._addVersionButton.click();
}
public async clickSettingsButton(): Promise<void> {
return this._settingsButton.click();
}
public async clickSharedContentButton(): Promise<void> {
return this._sharedContentButton.click();
}
}
+226 -1
View File
@@ -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",
+9 -2
View File
@@ -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",
+18
View File
@@ -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,
});
+2 -2
View File
@@ -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": {
+3 -2
View File
@@ -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);
}
@@ -23,7 +23,7 @@ export const AvailableVersionItem = memo(function AvailableVersionItem({version,
const formatedDate = useConstant(() => dateFormat(+version.ReleaseDate * 1000, "ddd. d mmm yyyy"));
return (
<motion.li className="group relative w-72 h-60 active:scale-[.98]" onClick={onClick} onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)}>
<motion.li id={`version-item-${version.BSManifest}`} className="group relative w-72 h-60 active:scale-[.98]" onClick={onClick} onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)}>
<GlowEffect visible={hovered || selected} className="absolute" />
<div className={`relative flex flex-col overflow-hidden rounded-md w-72 h-60 cursor-pointer group-hover:shadow-none duration-300 bg-light-main-color-2 dark:bg-main-color-2 ${!selected && "shadow-lg shadow-gray-900"}`}>
{version.recommended && (
@@ -29,7 +29,7 @@ export function AvailableVersionsSlide({ versions }: Props) {
}
return (
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap px-3.5 py-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
<ol id={`version-slide-${versions}`} className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap px-3.5 py-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
{getVersions().map(version => (
<AvailableVersionItem key={version.BSManifest} version={version} selected={equal(version, context.selectedVersion)} onClick={() => setSelectedVersion(version)}/>
))}
@@ -26,7 +26,7 @@ export function AvailableVersionsSlider() {
return (
<div className="w-full h-fit max-h-full flex flex-col items-center grow min-h-0 gap-3">
<TabNavBar tabIndex={yearIndex} tabsText={availableYears} onTabChange={setSelectedYear} />
<TabNavBar id="version-years-tab-bar" tabIndex={yearIndex} tabsText={availableYears} onTabChange={setSelectedYear} />
<ol className="w-full min-h-0 flex transition-transform duration-300" style={{ transform: `translate(${-(yearIndex * 100)}%, 0)` }}>
{availableYears.map(year => (
<AvailableVersionsSlide key={year} versions={getVersionOfYear(year)} />
@@ -31,7 +31,7 @@ export const BsManagerIcon = memo(({ className }: { className?: string }) => {
};
return (
<motion.div className="cursor-pointer" whileTap={{ rotate: -10 }} variants={transitions} animate={playing && bpm > 0 ? "playing" : "idle"} onClick={clickAction}>
<motion.div id="bsm-logo" className="cursor-pointer" whileTap={{ rotate: -10 }} variants={transitions} animate={playing && bpm > 0 ? "playing" : "idle"} onClick={clickAction}>
<svg className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<g fill={firstColor}>
<path d="M626.75,485.91l20.82,351.94a35.21,35.21,0,0,1-15.49,31.36L487.37,965.35l-.06,0A35.56,35.56,0,0,1,476,970.12a34.85,34.85,0,0,1-23.51-2.87L136.58,809.9a115.45,115.45,0,0,1-63.75-96.49L52,361.46a35.26,35.26,0,0,1,15.23-31.21l145.05-96.36a35.72,35.72,0,0,1,11.27-4.7,34.87,34.87,0,0,1,23.51,2.87L563,389.42a115.41,115.41,0,0,1,63.75,96.49Z" />
@@ -7,13 +7,14 @@ type Props = {
progress?: number;
isActive?: boolean;
onCancel?: (e: React.MouseEvent) => void;
id?: string;
};
export function NavBarItem({ progress, isDownloading, children, isActive, onCancel }: Props) {
export function NavBarItem({ progress, isDownloading, children, isActive, onCancel, id }: Props) {
const { firstColor, secondColor } = useThemeColor();
return (
<li className={`outline-none relative p-[1px] overflow-hidden rounded-xl flex justify-center items-center mb-1 ${isDownloading && "nav-item-download"} active:translate-y-[1px]`}>
<li id={id} className={`outline-none relative p-[1px] overflow-hidden rounded-xl flex justify-center items-center mb-1 ${isDownloading && "nav-item-download"} active:translate-y-[1px]`}>
{isDownloading && <div className="download-progress absolute top-0 w-full h-full" style={{ transform: `translate(${-(100 - progress)}%, 0)`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})` }} />}
<div className={`wrapper z-[1] px-1 py-[3px] w-full rounded-xl ${isDownloading && "bg-white dark:bg-black"} ${!isDownloading && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"} ${isActive && !isDownloading && "bg-light-main-color-3 dark:bg-main-color-3"}`}>
{children}
@@ -16,7 +16,7 @@ export function SharedNavBarItem() {
const color = useThemeColor("first-color");
return (
<NavBarItem isActive={route === "/shared"}>
<NavBarItem id="shared-contents-btn" isActive={route === "/shared"}>
<Tippy content={t("nav-bar.shared.tooltip")} className="font-bold !bg-neutral-900" placement="right-end" arrow={false} duration={[100, 0]} animation="shift-away-subtle">
<Link to="shared" className="w-full flex items-center justify-start content-center max-w-full h-[30px]">
<BsmIcon className="w-[19px] h-[19px] mr-[5px] shrink-0 brightness-125" icon="link" style={{ color }} />
@@ -48,12 +48,12 @@ export function NavBar() {
<NavBarSpliter />
<div className="w-full pb-2 flex flex-col items-center content-center justify-start gap-1">
<Tippy placement="right" content={t("nav-bar.add-version")} className="!bg-neutral-900" arrow={false}>
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="blah">
<Link id="add-version-btn" className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="blah">
<BsmIcon icon="add" className="text-blue-500 h-[34px]" style={{ color }} />
</Link>
</Tippy>
<Tippy placement="right" content={t("nav-bar.settings")} className="!bg-neutral-900" arrow={false}>
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="settings">
<Link id="settings-btn" className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="settings">
<BsmIcon icon="settings" className="text-blue-500 h-7" style={{ color }} />
</Link>
</Tippy>
@@ -9,6 +9,7 @@ import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
type BsmButtonType = "primary" | "secondary" | "success" | "cancel" | "error";
type Props = {
id?: string;
className?: string;
style?: CSSProperties;
imgClassName?: string;
@@ -29,7 +30,7 @@ type Props = {
textClassName?: string;
};
export function BsmButton({ className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }: Props) {
export function BsmButton({ id, className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }: Props) {
const t = useTranslation();
const { firstColor, secondColor } = useThemeColor();
const ref = useRef(null);
@@ -72,7 +73,7 @@ export function BsmButton({ className, style, imgClassName, iconClassName, icon,
const handleClick = (e: MouseEvent<HTMLDivElement>) => !disabled && onClick?.(e);
return (
<div ref={ref} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled ? "brightness-75 cursor-not-allowed" : "cursor-pointer"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
<div id={id} ref={ref} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled ? "brightness-75 cursor-not-allowed" : "cursor-pointer"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
{image && <BsmImage image={image} className={imgClassName} />}
{icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{ color: iconColor || textColor }} />}
{text &&
@@ -4,6 +4,7 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { LaserSlider } from "./laser-slider.component";
type Props = {
id?: string,
tabIndex: number;
tabsText: string[];
onTabChange: (index: number) => void;
@@ -22,7 +23,7 @@ export function TabNavBar(props: Props) {
};
return (
<nav className={`relative h-8 shrink-0 cursor-pointer rounded-md overflow-hidden shadow-md shadow-black bg-light-main-color-2 dark:bg-main-color-2 ${props.className}`}>
<nav id={props.id} className={`relative h-8 shrink-0 cursor-pointer rounded-md overflow-hidden shadow-md shadow-black bg-light-main-color-2 dark:bg-main-color-2 ${props.className}`}>
<LaserSlider className="absolute w-full h-1 bottom-0" mode="horizontal" color={secondColor} nbSteps={props.tabsText.length} step={currentIndex} />
<ul className="grid" style={{ gridTemplateColumns: `repeat(${props.tabsText.length}, minmax(0, 1fr))` }}>
{props.tabsText.map((text, index) =>
@@ -56,7 +56,7 @@ export function AvailableVersionsList() {
<AnimatePresence>
{selectedVersion && !downloading && (
<motion.div initial={{ y: "150%" }} animate={{ y: "0%" }} exit={{ y: "150%" }} className="absolute bottom-5" onClick={startDownload}>
<BsmButton text="misc.download" className="relative text-gray-800 dark:text-gray-100 rounded-md text-3xl font-bold italic tracking-wide px-3 pb-2 pt-1 shadow-md shadow-black" />
<BsmButton id="download-version-btn" text="misc.download" className="relative text-gray-800 dark:text-gray-100 rounded-md text-3xl font-bold italic tracking-wide px-3 pb-2 pt-1 shadow-md shadow-black" />
</motion.div>
)}
</AnimatePresence>
+6
View File
@@ -19,3 +19,9 @@ export async function allSettled<T>(promises: Promise<T>[], options?: AllSettled
return acc;
}, []);
}
export async function pause(ms: number): Promise<void>{
return new Promise((resolve) => {
setTimeout(() => resolve(), ms);
});
}