Merge pull request #8372 from streetwriters/fix/web-tests

This commit is contained in:
Abdullah Atta
2025-07-21 09:54:05 +05:00
22 changed files with 1847 additions and 597 deletions

View File

@@ -10,6 +10,12 @@ on:
# re-run workflow if workflow file changes # re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml" - ".github/workflows/core.tests.yml"
pull_request: pull_request:
branches:
- "master"
paths:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
types: types:
- "ready_for_review" - "ready_for_review"
- "opened" - "opened"

View File

@@ -10,6 +10,12 @@ on:
# re-run workflow if workflow file changes # re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml" - ".github/workflows/desktop.tests.yml"
pull_request: pull_request:
branches:
- "master"
paths:
- "app/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
jobs: jobs:
build: build:

View File

@@ -10,6 +10,12 @@ on:
# re-run workflow if workflow file changes # re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml" - ".github/workflows/editor.tests.yml"
pull_request: pull_request:
branches:
- "master"
paths:
- "packages/editor/**"
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
types: types:
- "ready_for_review" - "ready_for_review"
- "opened" - "opened"

View File

@@ -10,6 +10,12 @@ on:
# re-run workflow if workflow file changes # re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml" - ".github/workflows/web.tests.yml"
pull_request: pull_request:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml"
types: types:
- "ready_for_review" - "ready_for_review"
- "opened" - "opened"

View File

@@ -17,230 +17,226 @@ 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 } from "vitest"; import { testCleanup, test } from "./test-override.js";
import { harness } from "./utils.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";
import { describe } from "vitest";
test("update starts downloading if version is outdated", async (t) => { test("update starts downloading if version is outdated", async ({
await harness( ctx: { page },
t, expect,
async ({ page }) => { onTestFinished
await page.waitForSelector("#authForm"); }) => {
onTestFinished(testCleanup);
t.expect( await page.waitForSelector("#authForm");
await page.getByRole("button", { name: "Create account" }).isVisible() expect(
).toBe(true); await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page await page
.getByRole("button", { name: "Skip & go directly to the app" }) .getByRole("button", { name: "Skip & go directly to the app" })
.click(); .click();
await skipDialog(page); await skipDialog(page);
await page.waitForSelector(".ProseMirror"); await page.waitForSelector(".ProseMirror");
await page await page
.locator(".theme-scope-statusBar") .locator(".theme-scope-statusBar")
.getByRole("button", { name: /updating/i }) .getByRole("button", { name: /updating/i })
.waitFor({ state: "attached" }); .waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
}); });
test("update is only shown if version is outdated and auto updates are disabled", async (t) => { test("update is only shown if version is outdated and auto updates are disabled", async ({
await harness( ctx,
t, expect,
async (ctx) => { onTestFinished
await ctx.app.close(); }) => {
await writeFile( onTestFinished(testCleanup);
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
);
await ctx.relaunch(); await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
);
const { page } = ctx; await ctx.relaunch();
await page.waitForSelector("#authForm"); const { page } = ctx;
t.expect( await page.waitForSelector("#authForm");
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page expect(
.getByRole("button", { name: "Skip & go directly to the app" }) await page.getByRole("button", { name: "Create account" }).isVisible()
.click(); ).toBe(true);
await skipDialog(page); await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror"); await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.waitFor({ state: "attached" });
});
describe("update to stable if it is newer", () => {
test.scoped({ options: { version: "3.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
expect(gt(version, "3.0.0-beta.0")).toBe(true);
});
});
describe("update is not available if it latest stable version is older", () => {
test.scoped({ options: { version: "99.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
expect(
await page await page
.locator(".theme-scope-statusBar") .locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i }) .getByRole("button", { name: /available/i })
.waitFor({ state: "attached" }); .isHidden()
}, ).toBe(true);
{ version: "3.0.0" } });
);
}); });
test("update to stable if it is newer", async (t) => { describe("downgrade to stable on switching to stable release track", () => {
await harness( test.scoped({ options: { version: "99.0.0-beta.0" } });
t, test("test", async ({ ctx, expect, onTestFinished }) => {
async (ctx) => { onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch(); await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
const { page } = ctx; await ctx.relaunch();
await page.waitForSelector("#authForm"); const { page } = ctx;
t.expect( await page.waitForSelector("#authForm");
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page expect(
.getByRole("button", { name: "Skip & go directly to the app" }) await page.getByRole("button", { name: "Create account" }).isVisible()
.click(); ).toBe(true);
await skipDialog(page); await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror"); await skipDialog(page);
const updateButton = page await page.waitForSelector(".ProseMirror");
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(gt(version, "3.0.0-beta.0")).toBe(true);
},
{ version: "3.0.0-beta.0" }
);
});
test("update is not available if it latest stable version is older", async (t) => { await page
await harness( .locator(".theme-scope-statusBar")
t, .getByRole("button", { name: /checking for updates/i })
async (ctx) => { .waitFor({ state: "hidden" });
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch(); const updateButton = page
.locator(".theme-scope-statusBar")
const { page } = ctx; .getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
await page.waitForSelector("#authForm"); const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect( expect(lt(version, "99.0.0-beta.0")).toBe(true);
await page.getByRole("button", { name: "Create account" }).isVisible() });
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
t.expect(
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.isHidden()
).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
});
test("downgrade to stable on switching to stable release track", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(lt(version, "99.0.0-beta.0")).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
}); });
async function skipDialog(page: Page) { async function skipDialog(page: Page) {
await page try {
.waitForSelector(".ReactModal__Content", { const dialog = page.locator(".ReactModal__Content");
timeout: 1000 const positiveButton = dialog.locator(
}) "button[data-role='positive-button']"
.catch(() => {}) );
.then(async () => { const negativeButton = dialog.locator(
const positiveButton = page.locator( "button[data-role='negative-button']"
"button[data-role='positive-button']" );
); if (await positiveButton.isVisible())
const negativeButton = page.locator( await positiveButton.click({ timeout: 1000 });
"button[data-role='negative-button']" else if (await negativeButton.isVisible())
); await negativeButton.click({ timeout: 1000 });
if (await positiveButton.isVisible()) await positiveButton.click(); } catch (e) {
else if (await negativeButton.isVisible()) await negativeButton.click(); // ignore error
}); }
} }

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,22 +17,24 @@ 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 } from "vitest"; import { testCleanup, test } from "./test-override.js";
import { harness } from "./utils.js";
import assert from "assert";
test("make sure app loads", async (t) => { test("make sure app loads", async ({
await harness(t, async ({ page }) => { ctx: { page },
await page.waitForSelector("#authForm"); expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
assert.ok( await page.waitForSelector("#authForm");
await page.getByRole("button", { name: "Create account" }).isVisible()
);
await page expect(
.getByRole("button", { name: "Skip & go directly to the app" }) await page.getByRole("button", { name: "Create account" }).isVisible()
.click(); ).toBe(true);
await page.waitForSelector(".ProseMirror"); await page
}); .getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
}); });

View File

@@ -0,0 +1,64 @@
/*
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,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
await rm(ctx.outputDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
}

View File

@@ -18,146 +18,235 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { execSync } from "child_process"; import { execSync } from "child_process";
import { mkdir } from "fs/promises"; import { cp, readFile, writeFile } from "fs/promises";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import path 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 { existsSync } from "fs";
import { TaskContext } from "vitest";
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;
userDataDir: string;
outputDir: string;
relaunch: () => Promise<void>; relaunch: () => Promise<void>;
} }
interface TestOptions { export interface TestOptions {
version: string; version: string;
} }
export async function harness( export interface Fixtures {
t: TaskContext, options: TestOptions;
cb: (ctx: AppContext) => Promise<void>, ctx: AppContext;
options?: TestOptions
) {
const ctx = await buildAndLaunchApp(options);
t.onTestFinished(async (result) => {
if (result.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(t.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
});
await cb(ctx);
} }
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> { export async function buildAndLaunchApp(
const productName = makeid(10); options?: TestOptions
const executablePath = await buildApp({ ...options, productName }); ): Promise<AppContext> {
const { app, page, configPath } = await launchApp(executablePath); const productName = `notesnooktest${makeid(10)}`;
const outputDir = path.join("test-artifacts", `${productName}-output`);
const executablePath = await copyBuild({
...options,
outputDir
});
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName
);
const ctx: AppContext = { const ctx: AppContext = {
app, app,
page, page,
configPath, configPath,
userDataDir,
outputDir,
relaunch: async () => { relaunch: async () => {
const { app, page, configPath } = await launchApp(executablePath); const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName
);
ctx.app = app; ctx.app = app;
ctx.page = page; ctx.page = page;
ctx.userDataDir = userDataDir;
ctx.configPath = configPath; ctx.configPath = configPath;
} }
}; };
return ctx; return ctx;
} }
async function launchApp(executablePath: string) { async function launchApp(executablePath: string, packageName: string) {
const userDataDir = resolve(
__dirname,
"..",
"test-artifacts",
"user_data_dirs",
packageName
);
const app = await electron.launch({ const app = await electron.launch({
executablePath, executablePath,
args: IS_DEBUG ? [] : ["--hidden"], args: IS_DEBUG ? [] : ["--hidden"],
env: env: {
process.platform === "linux" ...(process.platform === "linux"
? { ? {
...(process.env as Record<string, string>), ...(process.env as Record<string, string>),
APPIMAGE: "true" APPIMAGE: "true"
} }
: (process.env as Record<string, string>) : (process.env as Record<string, string>)),
CUSTOM_USER_DATA_DIR: userDataDir
}
}); });
const page = await app.firstWindow(); const page = await app.firstWindow();
const userDataDirectory = await app.evaluate((a) => { const configPath = path.join(userDataDir, "UserData", "config.json");
return a.app.getPath("userData");
});
const configPath = path.join(userDataDirectory, "config.json");
return { return {
app, app,
page, page,
configPath configPath,
userDataDir
}; };
} }
async function buildApp({ let MAX_RETRIES = 3;
export async function buildApp(version?: string) {
if (!existsSync(SOURCE_DIR)) {
const args = [
"electron-builder",
"--dir",
`--${process.arch}`,
`--config electron-builder.config.js`,
`--c.extraMetadata.productName=${productName}`,
`--c.compression=store`,
"--publish=never"
];
if (version) args.push(`--c.extraMetadata.version=${version}`);
try {
execSync(`npx ${args.join(" ")}`, {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: SOURCE_DIR
}
});
} catch (e) {
if (--MAX_RETRIES) {
console.log("retrying...");
return await buildApp(version);
} else throw e;
}
}
}
async function copyBuild({
version, version,
productName outputDir
}: { }: {
version?: string; version?: string;
productName: string; outputDir: string;
}) { }) {
const buildRoot = path.join("test-artifacts", `${productName}-build`); return process.platform === "win32"
const output = path.join("test-artifacts", `${productName}-output`); ? await makeBuildCopyWindows(outputDir, productName, version)
execSync(`npm run release -- --root ${buildRoot} --skip-tsc-build`, { : process.platform === "darwin"
stdio: IS_DEBUG ? "inherit" : "ignore" ? await makeBuildCopyMacOS(outputDir, productName, version)
}); : await makeBuildCopyLinux(outputDir, productName, version);
}
const args = [ async function makeBuildCopyLinux(
`--config electron-builder.config.js`, outputDir: string,
`--c.extraMetadata.productName=${productName}`, productName: string,
"--publish=never" version?: string
]; ) {
if (version) args.push(`--c.extraMetadata.version=${version}`); const platformDir =
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
execSync(`npx electron-builder --dir --${process.arch} ${args.join(" ")}`, { const appDir = await makeBuildCopy(
stdio: IS_DEBUG ? "inherit" : "ignore", outputDir,
env: { platformDir,
...process.env, "resources",
NOTESNOOK_STAGING: "true", version
NN_BUILD_ROOT: buildRoot, );
NN_PRODUCT_NAME: productName, return resolve(
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: output
}
});
return path.join(
__dirname, __dirname,
"..", "..",
output, appDir,
process.platform === "linux" productName.toLowerCase().replace(/\s+/g, "-")
? process.arch === "arm64"
? "linux-arm64-unpacked"
: "linux-unpacked"
: process.platform === "darwin"
? process.arch === "arm64"
? `mac-arm64/${productName}.app/Contents/MacOS/`
: `mac/${productName}.app/Contents/MacOS/`
: "win-unpacked",
process.platform === "win32" ? `${productName}.exe` : productName
); );
} }
async function makeBuildCopyWindows(
outputDir: string,
productName: string,
version?: string
) {
const platformDir =
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
"resources",
version
);
return resolve(__dirname, "..", appDir, `${productName}.exe`);
}
async function makeBuildCopyMacOS(
outputDir: string,
productName: string,
version?: string
) {
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
join(`${productName}.app`, "Contents", "Resources"),
version
);
return resolve(
__dirname,
"..",
appDir,
`${productName}.app`,
"Contents",
"MacOS",
productName
);
}
async function makeBuildCopy(
outputDir: string,
platformDir: string,
resourcesDir: string,
version?: string
) {
const appDir = outputDir;
await cp(join(SOURCE_DIR, platformDir), outputDir, {
recursive: true,
preserveTimestamps: true,
verbatimSymlinks: true,
dereference: false,
force: true
});
const packageJsonPath = join(appDir, resourcesDir, "app", "package.json");
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
if (version) {
packageJson.version = version;
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
return appDir;
}
function makeid(length: number) { function makeid(length: number) {
let result = ""; let result = "";
const characters = const characters =

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,9 @@
"main": "./dist/cjs/index.js", "main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js", "module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts", "types": "./dist/types/index.d.ts",
"sideEffects": false, "sideEffects": [
"src/overrides.ts"
],
"exports": { "exports": {
".": { ".": {
"require": { "require": {
@@ -55,7 +57,7 @@
"slugify": "1.6.6", "slugify": "1.6.6",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"undici": "^7.8.0", "undici": "^7.8.0",
"vitest": "2.1.8" "vitest": "^3.2.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"dmg-license": "^1.0.11" "dmg-license": "^1.0.11"

View File

@@ -17,6 +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 "./overrides";
import { app, BrowserWindow, nativeTheme, shell } from "electron"; import { app, BrowserWindow, nativeTheme, shell } from "electron";
import { isDevelopment } from "./utils"; import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol"; import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";

View File

@@ -0,0 +1,35 @@
/*
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 { app } from "electron";
import path from "path";
if (process.env.CUSTOM_USER_DATA_DIR) {
app.setPath(
"appData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
);
app.setPath(
"userData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
);
app.setPath(
"documents",
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
);
}

View File

@@ -21,11 +21,13 @@ import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
test: { test: {
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000, testTimeout: 120 * 1000,
hookTimeout: 120 * 1000,
sequence: { sequence: {
concurrent: true, concurrent: true,
shuffle: true shuffle: true
}, },
globalSetup: "./__tests__/global-setup.ts",
dir: "./__tests__/", dir: "./__tests__/",
exclude: [ exclude: [
"**/node_modules/**", "**/node_modules/**",

View File

@@ -1 +1 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -1 +1 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -1 +1 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -162,9 +162,9 @@ export class SettingsViewModel {
await appLockSwitch.click(); await appLockSwitch.click();
await fillPasswordDialog(this.page, userPassword); await fillPasswordDialog(this.page, userPassword);
await this.page.waitForTimeout(100); await this.page.waitForTimeout(500);
await fillConfirmPasswordDialog(this.page, appLockPassword); await fillConfirmPasswordDialog(this.page, appLockPassword);
await this.page.waitForTimeout(100); await this.page.waitForTimeout(500);
} }
async disableAppLock(appLockPassword: string) { async disableAppLock(appLockPassword: string) {

View File

@@ -91,6 +91,7 @@ export async function fillReminderDialog(
} }
await confirmDialog(dialog); await confirmDialog(dialog);
await dialog.waitFor({ state: "hidden" });
} }
export async function fillItemDialog(page: Page, item: Item) { export async function fillItemDialog(page: Page, item: Item) {
@@ -133,7 +134,6 @@ export async function fillConfirmPasswordDialog(page: Page, password: string) {
export async function confirmDialog(dialog: Locator) { export async function confirmDialog(dialog: Locator) {
const dialogConfirm = dialog.locator(getTestId("dialog-yes")); const dialogConfirm = dialog.locator(getTestId("dialog-yes"));
await dialogConfirm.click(); await dialogConfirm.click();
// await dialogConfirm.waitFor({ state: "detached" });
} }
export async function denyDialog(page: Page) { export async function denyDialog(page: Page) {

View File

@@ -53,16 +53,17 @@ test("adding a one-time reminder before current time should not be possible", as
await app.goto(); await app.goto();
const reminders = await app.goToReminders(); const reminders = await app.goToReminders();
await reminders.createReminder({ const result = await Promise.race([
...ONE_TIME_REMINDER, reminders.createReminder({
date: 0 ...ONE_TIME_REMINDER,
}); date: 0
}),
expect( app.toasts.waitForToast(
await app.toasts.waitForToast(
"Reminder time cannot be earlier than the current time." "Reminder time cannot be earlier than the current time."
) )
).toBeTruthy(); ]);
expect(result).toBeTruthy();
}); });
for (const recurringMode of ["Daily", "Weekly", "Monthly"] as const) { for (const recurringMode of ["Daily", "Weekly", "Monthly"] as const) {

View File

@@ -455,6 +455,7 @@ test("if note is active in multiple tabs, moving the note to trash should close
title: "Note 1" title: "Note 1"
}); });
await note?.contextMenu.openInNewTab(); await note?.contextMenu.openInNewTab();
await page.waitForTimeout(1000);
await note?.contextMenu.moveToTrash(); await note?.contextMenu.moveToTrash();

View File

@@ -5,7 +5,8 @@
"outputs": [ "outputs": [
"{projectRoot}/build", "{projectRoot}/build",
"{projectRoot}/dist", "{projectRoot}/dist",
"{projectRoot}/languages" "{projectRoot}/languages",
"{projectRoot}/src/extensions/code-block/languages"
], ],
"cache": true "cache": true
}, },