e2e: make tests reusable for standalone

This commit is contained in:
Elian Doran
2026-04-19 11:32:52 +03:00
parent 7c53fe56be
commit 417228ebde
36 changed files with 148 additions and 79 deletions

View File

@@ -154,7 +154,7 @@ pnpm desktop:build # Build desktop application
### Test Organization
- **Server tests** (`apps/server/spec/`): Must run sequentially (shared database state)
- **Client tests** (`apps/client/src/`): Can run in parallel
- **E2E tests** (`apps/server-e2e/`): Use Playwright for integration testing
- **E2E tests** (`packages/trilium-e2e/`): Shared Playwright tests, run via `pnpm --filter server e2e` or `pnpm --filter client-standalone e2e`
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
**Pattern**: When adding new API endpoints, add tests in `spec/etapi/` following existing patterns (see `search.spec.ts`).

View File

@@ -82,7 +82,7 @@ jobs:
require-healthy: true
- name: Run Playwright tests
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm --filter=server-e2e e2e
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm --filter=server e2e
- name: Upload Playwright trace
if: failure()

View File

@@ -73,14 +73,14 @@ jobs:
sleep 10
- name: Server end-to-end tests
run: pnpm --filter server-e2e e2e
run: pnpm --filter server e2e
- name: Upload test report
if: failure()
uses: actions/upload-artifact@v7
with:
name: e2e report ${{ matrix.arch }}
path: apps/server-e2e/test-output
path: apps/server/test-output
- name: Kill the server
if: always()

View File

@@ -59,7 +59,6 @@ apps/
desktop/ # Electron (bundles server + client)
client-standalone/ # Standalone client (WASM + service workers, no Node.js)
standalone-desktop/ # Standalone desktop variant
server-e2e/ # Playwright E2E tests for server
web-clipper/ # Browser extension
website/ # Project website
db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/
@@ -67,6 +66,7 @@ apps/
packages/
trilium-core/ # Core business logic: entities, services, SQL, sync
commons/ # Shared interfaces and utilities
trilium-e2e/ # Shared Playwright E2E tests
ckeditor5/ # Custom rich text editor bundle
codemirror/ # Code editor integration
highlightjs/ # Syntax highlighting
@@ -248,7 +248,7 @@ Use `note.getOwnedAttribute()` for direct, `note.getAttribute()` for inherited.
- **Server tests** (`apps/server/spec/`): Vitest, must run sequentially (shared DB), forks pool, max 6 workers
- **Client tests** (`apps/client/src/`): Vitest with happy-dom environment, can run in parallel
- **E2E tests** (`apps/server-e2e/`): Playwright, Chromium, server started automatically on port 8082
- **E2E tests** (`packages/trilium-e2e/`): Shared Playwright tests, run via `pnpm --filter server e2e` or `pnpm --filter client-standalone e2e`
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
## Documentation

View File

@@ -9,7 +9,8 @@
"dev": "vite dev",
"test": "vitest",
"start-prod": "pnpm build && pnpm vite preview --port 8888",
"coverage": "vitest --coverage"
"coverage": "vitest --coverage",
"e2e": "pnpm build && playwright test"
},
"dependencies": {
"@excalidraw/excalidraw": "0.18.0",

View File

@@ -0,0 +1,16 @@
import { createBaseConfig } from "../../packages/trilium-e2e/src/base-config";
const port = process.env["TRILIUM_PORT"] ?? "8082";
const baseURL = process.env["BASE_URL"] || `http://127.0.0.1:${port}`;
export default createBaseConfig({
appDir: __dirname,
projectName: "standalone",
webServer: !process.env.TRILIUM_DOCKER ? {
command: "pnpm vite preview --port " + port,
url: baseURL,
reuseExistingServer: !process.env.CI,
cwd: __dirname,
timeout: 5 * 60 * 1000
} : undefined,
});

View File

@@ -1,44 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
import { join } from 'path';
// For CI, you may want to set BASE_URL to the deployed application.
const port = process.env['TRILIUM_PORT'] ?? "8082";
const baseURL = process.env['BASE_URL'] || `http://127.0.0.1:${port}`;
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "src",
reporter: [["list"], ["html", { outputFolder: "test-output" }]],
outputDir: "test-output",
retries: 3,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: !process.env.TRILIUM_DOCKER ? {
command: 'pnpm start-prod-no-dir',
url: baseURL,
reuseExistingServer: !process.env.CI,
cwd: join(__dirname, "../server"),
env: {
TRILIUM_DATA_DIR: "spec/db",
TRILIUM_PORT: port,
TRILIUM_INTEGRATION_TEST: "memory"
},
timeout: 5 * 60 * 1000
} : undefined,
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
}
]
});

View File

@@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test";
import App from "./support/app";
import App, { getBaseUrl } from "../../../packages/trilium-e2e/src/support/app";
const BASE_URL = "http://127.0.0.1:8082";
const BASE_URL = getBaseUrl();
/**
* E2E tests for exact search functionality using the leading "=" operator.

View File

@@ -1,5 +1,5 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
import App from "../../../packages/trilium-e2e/src/support/app";
test("Goes to share root", async ({ page, context }) => {
const app = new App(page, context);

View File

@@ -16,6 +16,7 @@
"test-build": "vitest --config vitest.build.config.mts",
"start-prod": "cross-env TRILIUM_DATA_DIR=data pnpm start-prod-no-dir",
"start-prod-no-dir": "pnpm build && cross-env TRILIUM_ENV=production TRILIUM_PORT=8082 node dist/main.cjs",
"e2e": "playwright test",
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular",
"docker-build-debian": "pnpm build && docker build . -t triliumnext-debian -f Dockerfile",
"docker-build-alpine": "pnpm build && docker build . -t triliumnext-alpine -f Dockerfile.alpine",

View File

@@ -0,0 +1,22 @@
import { createBaseConfig } from "../../packages/trilium-e2e/src/base-config";
const port = process.env["TRILIUM_PORT"] ?? "8082";
const baseURL = process.env["BASE_URL"] || `http://127.0.0.1:${port}`;
export default createBaseConfig({
appDir: __dirname,
localTestDir: "e2e",
projectName: "server",
webServer: !process.env.TRILIUM_DOCKER ? {
command: "pnpm start-prod-no-dir",
url: baseURL,
reuseExistingServer: !process.env.CI,
cwd: __dirname,
env: {
TRILIUM_DATA_DIR: "spec/db",
TRILIUM_PORT: port,
TRILIUM_INTEGRATION_TEST: "memory"
},
timeout: 5 * 60 * 1000
} : undefined,
});

View File

@@ -26,11 +26,13 @@ apps/
│ └── src/**/*.spec.ts # Server tests
├── client/
│ └── src/**/*.spec.ts # Client tests
── server-e2e/
│ └── tests/**/*.spec.ts # E2E tests
── server/
│ └── e2e/**/*.spec.ts # Server-specific E2E tests
└── desktop/
└── e2e
└── tests/**/*.spec.ts # E2E tests
└── e2e/**/*.spec.ts # Desktop E2E tests
packages/
└── trilium-e2e/
└── src/**/*.spec.ts # Shared E2E tests
```
## Running tests

View File

@@ -9,7 +9,12 @@
* Playwright with Electron
* Tests some basic functionality such as creating a new document.
These can be found in `apps/server-e2e` and `apps/desktop/e2e`.
Shared E2E tests live in `packages/trilium-e2e/`. Server-specific tests are in `apps/server/e2e/`, desktop tests in `apps/desktop/e2e/`.
Run E2E tests via:
- `pnpm --filter server e2e` (server)
- `pnpm --filter client-standalone e2e` (standalone)
- `pnpm --filter desktop e2e` (desktop/Electron)
## First-time run

View File

@@ -81,7 +81,9 @@ const mainConfig = [
const playwrightConfig = {
files: [
"apps/server-e2e/src/**/*.spec.ts",
"packages/trilium-e2e/src/**/*.spec.ts",
"apps/server/e2e/**/*.spec.ts",
"apps/client-standalone/e2e/**/*.spec.ts",
"apps/desktop/e2e/**/*.spec.ts"
],
plugins: { playwright },

View File

@@ -177,7 +177,7 @@
"apps/dump-db"
"apps/edit-docs"
"apps/server"
"apps/server-e2e"
"packages/trilium-e2e"
];
desktopItems = lib.optionals (app == "desktop") [

View File

@@ -1,10 +1,8 @@
{
"name": "@triliumnext/server-e2e",
"name": "@triliumnext/trilium-e2e",
"version": "0.0.1",
"private": true,
"scripts": {
"e2e": "playwright test"
},
"scripts": {},
"devDependencies": {
"dotenv": "17.4.1"
}

View File

@@ -0,0 +1,63 @@
import { defineConfig, devices, type PlaywrightTestConfig } from "@playwright/test";
import { join } from "path";
interface BaseConfigOptions {
/**
* The directory of the calling app (i.e. `__dirname` from the app's playwright.config.ts).
*/
appDir: string;
/**
* Optional local test directory for app-specific tests (relative to appDir).
* If provided, a second project is added for app-specific tests.
*/
localTestDir?: string;
/**
* The name for the app-specific test project (e.g. "server", "standalone").
*/
projectName: string;
/**
* Optional webServer configuration to start the app before tests.
*/
webServer?: PlaywrightTestConfig["webServer"];
}
/**
* Creates a base Playwright configuration that includes the shared trilium-e2e
* tests and optionally app-specific tests.
*/
export function createBaseConfig({ appDir, localTestDir, projectName, webServer }: BaseConfigOptions) {
const port = process.env["TRILIUM_PORT"] ?? "8082";
const baseURL = process.env["BASE_URL"] || `http://127.0.0.1:${port}`;
const sharedTestDir = join(__dirname);
const projects: PlaywrightTestConfig["projects"] = [
{
name: `${projectName}-shared`,
testDir: sharedTestDir,
use: { ...devices["Desktop Chrome"] },
}
];
if (localTestDir) {
projects.push({
name: projectName,
testDir: join(appDir, localTestDir),
use: { ...devices["Desktop Chrome"] },
});
}
return defineConfig({
reporter: [["list"], ["html", { outputFolder: join(appDir, "test-output") }]],
outputDir: join(appDir, "test-output"),
retries: 3,
use: {
baseURL,
trace: "on-first-retry",
},
webServer,
projects,
});
}

View File

@@ -4,7 +4,7 @@ import App from "./support/app";
test("Can duplicate note with broken links", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({
url: "http://localhost:8082/#root/Q5abPvymDH6C/2VammGGdG6Ie"
url: "/#root/Q5abPvymDH6C/2VammGGdG6Ie"
});
await app.noteTree.getByText("Note map").first().click({ button: "right" });

View File

@@ -4,21 +4,21 @@ import App from "./support/app";
test("Native Title Bar not displayed on web", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsAppearance" });
await app.goto({ url: "/#root/_hidden/_options/_optionsAppearance" });
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Theme" })).toBeVisible();
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
});
test("Tray settings not displayed on web", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsOther" });
await app.goto({ url: "/#root/_hidden/_options/_optionsOther" });
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Tray" })).toBeHidden();
});
test("Spellcheck settings not displayed on web", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck" });
await app.goto({ url: "/#root/_hidden/_options/_optionsSpellcheck" });
await expect(app.currentNoteSplitContent.getByText("These options apply only for desktop builds")).toBeVisible();
await expect(app.currentNoteSplitContent.getByText("Check spelling")).toBeHidden();
});

View File

@@ -7,7 +7,10 @@ interface GotoOpts {
preserveTabs?: boolean;
}
const BASE_URL = "http://127.0.0.1:8082";
export function getBaseUrl(): string {
const port = process.env["TRILIUM_PORT"] ?? "8082";
return process.env["BASE_URL"] || `http://127.0.0.1:${port}`;
}
interface DropdownLocator extends Locator {
selectOptionByText: (text: string) => Promise<void>;
@@ -48,7 +51,7 @@ export default class App {
await this.context.addCookies([
{
url: BASE_URL,
url: getBaseUrl(),
name: "trilium-device",
value: isMobile ? "mobile" : "desktop"
}
@@ -165,7 +168,7 @@ export default class App {
expect(csrfToken).toBeTruthy();
await expect(
await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, {
await this.page.request.put(`${getBaseUrl()}/api/options/${key}/${value}`, {
headers: {
"x-csrf-token": csrfToken
}

12
pnpm-lock.yaml generated
View File

@@ -1070,12 +1070,6 @@ importers:
specifier: 3.3.0
version: 3.3.0
apps/server-e2e:
devDependencies:
dotenv:
specifier: 17.4.1
version: 17.4.1
apps/web-clipper:
dependencies:
cash-dom:
@@ -1702,6 +1696,12 @@ importers:
specifier: 2.16.1
version: 2.16.1
packages/trilium-e2e:
devDependencies:
dotenv:
specifier: 17.4.1
version: 17.4.1
packages/turndown-plugin-gfm:
devDependencies:
happy-dom:

View File

@@ -13,7 +13,7 @@
"path": "./apps/server"
},
{
"path": "./apps/server-e2e"
"path": "./packages/trilium-e2e"
},
{
"path": "./apps/client"