desktop: build app only once during tests

This commit is contained in:
Abdullah Atta
2025-07-21 09:19:53 +05:00
parent 70fd038207
commit 578bc6e546
6 changed files with 113 additions and 68 deletions

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { test, testCleanup } from "./utils.js"; import { testCleanup, test } from "./test-override.js";
import { writeFile } from "fs/promises"; import { writeFile } from "fs/promises";
import { Page } from "playwright"; import { Page } from "playwright";
import { gt, lt } from "semver"; import { gt, lt } from "semver";

View File

@@ -0,0 +1,24 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { buildApp } from "./utils";
export default async function setup() {
await buildApp();
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { testCleanup, test } from "./utils.js"; import { testCleanup, test } from "./test-override.js";
test("make sure app loads", async ({ test("make sure app loads", async ({
ctx: { page }, ctx: { page },

View File

@@ -0,0 +1,50 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test as vitestTest, TestContext } from "vitest";
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
import { mkdir, rm } from "fs/promises";
import path from "path";
import slugify from "slugify";
export const test = vitestTest.extend<Fixtures>({
options: { version: "3.0.0" } as TestOptions,
ctx: async ({ options }, use) => {
const ctx = await buildAndLaunchApp(options);
await use(ctx);
}
});
export async function testCleanup(context: TestContext) {
const ctx = (context.task.context as unknown as Fixtures).ctx;
if (context.task.result?.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(context.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
await rm(ctx.userDataDir, { force: true, recursive: true });
await rm(ctx.outputDir, { force: true, recursive: true });
}

View File

@@ -18,19 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { execSync } from "child_process"; import { execSync } from "child_process";
import { cp, mkdir, readFile, rm, writeFile } from "fs/promises"; import { cp, readFile, writeFile } from "fs/promises";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import path, { join, resolve } from "path"; import path, { join, resolve } from "path";
import { _electron as electron } from "playwright"; import { _electron as electron } from "playwright";
import slugify from "slugify";
import { test as vitestTest, TestContext } from "vitest";
import { existsSync } from "fs"; import { existsSync } from "fs";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true"; const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
const productName = `NotesnookTestHarness`;
const SOURCE_DIR = resolve("output", productName);
interface AppContext { export interface AppContext {
app: import("playwright").ElectronApplication; app: import("playwright").ElectronApplication;
page: import("playwright").Page; page: import("playwright").Page;
configPath: string; configPath: string;
@@ -39,45 +39,21 @@ interface AppContext {
relaunch: () => Promise<void>; relaunch: () => Promise<void>;
} }
interface TestOptions { export interface TestOptions {
version: string; version: string;
} }
interface Fixtures { export interface Fixtures {
options: TestOptions; options: TestOptions;
ctx: AppContext; ctx: AppContext;
} }
export const test = vitestTest.extend<Fixtures>({ export async function buildAndLaunchApp(
options: { version: "3.0.0" } as TestOptions, options?: TestOptions
ctx: async ({ options }, use) => { ): Promise<AppContext> {
const ctx = await buildAndLaunchApp(options);
await use(ctx);
}
});
export async function testCleanup(context: TestContext) {
const ctx = (context.task.context as unknown as Fixtures).ctx;
if (context.task.result?.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(context.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
await rm(ctx.userDataDir, { force: true, recursive: true });
await rm(ctx.outputDir, { force: true, recursive: true });
}
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
const productName = `notesnooktest${makeid(10)}`; const productName = `notesnooktest${makeid(10)}`;
const outputDir = path.join("test-artifacts", `${productName}-output`); const outputDir = path.join("test-artifacts", `${productName}-output`);
const executablePath = await buildApp({ const executablePath = await copyBuild({
...options, ...options,
outputDir outputDir
}); });
@@ -139,16 +115,8 @@ async function launchApp(executablePath: string, packageName: string) {
} }
let MAX_RETRIES = 3; let MAX_RETRIES = 3;
async function buildApp({ export async function buildApp(version?: string) {
version, if (!existsSync(SOURCE_DIR)) {
outputDir
}: {
version?: string;
outputDir: string;
}) {
const productName = `NotesnookTestHarness`;
const sourceDir = resolve("output", productName);
if (!existsSync(sourceDir)) {
const args = [ const args = [
"electron-builder", "electron-builder",
"--dir", "--dir",
@@ -167,33 +135,40 @@ async function buildApp({
NOTESNOOK_STAGING: "true", NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName, NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`, NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: sourceDir NN_OUTPUT_DIR: SOURCE_DIR
} }
}); });
} catch (e) { } catch (e) {
if (--MAX_RETRIES) { if (--MAX_RETRIES) {
console.log("retrying..."); console.log("retrying...");
return await buildApp({ outputDir, version }); return await buildApp(version);
} else throw e; } else throw e;
} }
} }
return process.platform === "win32"
? await copyBuildWindows(sourceDir, outputDir, productName, version)
: process.platform === "darwin"
? await copyBuildMacOS(sourceDir, outputDir, productName, version)
: await copyBuildLinux(sourceDir, outputDir, productName, version);
} }
async function copyBuildLinux( async function copyBuild({
sourceDir: string, version,
outputDir
}: {
version?: string;
outputDir: string;
}) {
return process.platform === "win32"
? await makeBuildCopyWindows(outputDir, productName, version)
: process.platform === "darwin"
? await makeBuildCopyMacOS(outputDir, productName, version)
: await makeBuildCopyLinux(outputDir, productName, version);
}
async function makeBuildCopyLinux(
outputDir: string, outputDir: string,
productName: string, productName: string,
version?: string version?: string
) { ) {
const platformDir = const platformDir =
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked"; process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
const appDir = await copyBuild( const appDir = await makeBuildCopy(
sourceDir,
outputDir, outputDir,
platformDir, platformDir,
"resources", "resources",
@@ -207,16 +182,14 @@ async function copyBuildLinux(
); );
} }
async function copyBuildWindows( async function makeBuildCopyWindows(
sourceDir: string,
outputDir: string, outputDir: string,
productName: string, productName: string,
version?: string version?: string
) { ) {
const platformDir = const platformDir =
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked"; process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
const appDir = await copyBuild( const appDir = await makeBuildCopy(
sourceDir,
outputDir, outputDir,
platformDir, platformDir,
"resources", "resources",
@@ -225,15 +198,13 @@ async function copyBuildWindows(
return resolve(__dirname, "..", appDir, `${productName}.exe`); return resolve(__dirname, "..", appDir, `${productName}.exe`);
} }
async function copyBuildMacOS( async function makeBuildCopyMacOS(
sourceDir: string,
outputDir: string, outputDir: string,
productName: string, productName: string,
version?: string version?: string
) { ) {
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac"; const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
const appDir = await copyBuild( const appDir = await makeBuildCopy(
sourceDir,
outputDir, outputDir,
platformDir, platformDir,
join(`${productName}.app`, "Contents", "Resources"), join(`${productName}.app`, "Contents", "Resources"),
@@ -250,15 +221,14 @@ async function copyBuildMacOS(
); );
} }
async function copyBuild( async function makeBuildCopy(
sourceDir: string,
outputDir: string, outputDir: string,
platformDir: string, platformDir: string,
resourcesDir: string, resourcesDir: string,
version?: string version?: string
) { ) {
const appDir = outputDir; const appDir = outputDir;
await cp(join(sourceDir, platformDir), outputDir, { await cp(join(SOURCE_DIR, platformDir), outputDir, {
recursive: true, recursive: true,
preserveTimestamps: true, preserveTimestamps: true,
verbatimSymlinks: true, verbatimSymlinks: true,

View File

@@ -26,6 +26,7 @@ export default defineConfig({
concurrent: true, concurrent: true,
shuffle: true shuffle: true
}, },
globalSetup: "./__tests__/global-setup.ts",
dir: "./__tests__/", dir: "./__tests__/",
exclude: [ exclude: [
"**/node_modules/**", "**/node_modules/**",