chore(nx/server): set up e2e

This commit is contained in:
Elian Doran
2025-04-28 22:58:00 +03:00
parent 546bb52abe
commit e18613148b
33 changed files with 40 additions and 172 deletions

View File

@@ -1,8 +0,0 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('/');
// Expect h1 to contain a substring.
expect(await page.locator('h1').innerText()).toContain('Welcome');
});

View File

@@ -1,22 +0,0 @@
{
"jsc": {
"target": "es2017",
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"transform": {
"decoratorMetadata": true,
"legacyDecorator": true
},
"keepClassNames": true,
"externalHelpers": true,
"loose": true
},
"module": {
"type": "es6"
},
"sourceMaps": true,
"exclude": []
}

View File

@@ -1,5 +0,0 @@
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig
];

View File

@@ -1,24 +0,0 @@
/* eslint-disable */
import { readFileSync } from 'fs';
// Reading the SWC compilation config for the spec files
const swcJestConfig = JSON.parse(
readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8')
);
// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
swcJestConfig.swcrc = false;
export default {
displayName: '@triliumnext/desktop-e2e',
preset: '../../jest.preset.js',
globalSetup: '<rootDir>/src/support/global-setup.ts',
globalTeardown: '<rootDir>/src/support/global-teardown.ts',
setupFiles: ['<rootDir>/src/support/test-setup.ts'],
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: 'test-output/jest/coverage',
};

View File

@@ -1,25 +0,0 @@
{
"name": "@triliumnext/desktop-e2e",
"version": "0.0.1",
"private": true,
"nx": {
"implicitDependencies": [
"@triliumnext/desktop"
],
"targets": {
"e2e": {
"executor": "@nx/jest:jest",
"outputs": [
"{projectRoot}/test-output/jest/coverage"
],
"options": {
"jestConfig": "apps/desktop-e2e/jest.config.ts",
"passWithNoTests": true
},
"dependsOn": [
"@triliumnext/desktop:build"
]
}
}
}
}

View File

@@ -1,10 +0,0 @@
import axios from 'axios';
describe('GET /', () => {
it('should return a message', async () => {
const res = await axios.get(`/`);
expect(res.status).toBe(200);
expect(res.data).toEqual({ message: 'Hello API' });
});
})

View File

@@ -1,11 +0,0 @@
/* eslint-disable */
var __TEARDOWN_MESSAGE__: string;
module.exports = async function() {
// Start services that that the app needs to run (e.g. database, docker-compose, etc.).
console.log('\nSetting up...\n');
// Hint: Use `globalThis` to pass variables to global teardown.
globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
};

View File

@@ -1,7 +0,0 @@
/* eslint-disable */
module.exports = async function() {
// Put clean up logic here (e.g. stopping services, docker-compose, etc.).
// Hint: `globalThis` is shared between setup and teardown.
console.log(globalThis.__TEARDOWN_MESSAGE__);
};

View File

@@ -1,9 +0,0 @@
/* eslint-disable */
import axios from 'axios';
module.exports = async function() {
// Configure axios for tests to use.
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ?? '3000';
axios.defaults.baseURL = `http://${host}:${port}`;
};

View File

@@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "out-tsc/@triliumnext/desktop-e2e",
"esModuleInterop": true,
"noUnusedLocals": false,
"noImplicitAny": false
},
"include": [
"jest.config.ts",
"src/**/*.ts"
],
"references": []
}

3
apps/server-e2e/.env Normal file
View File

@@ -0,0 +1,3 @@
TRILIUM_INTEGRATION_TEST=memory
TRILIUM_PORT=8082
TRILIUM_DATA_DIR=apps/server/spec/db

View File

@@ -1,10 +1,11 @@
{
"name": "@triliumnext/client-e2e",
"name": "@triliumnext/server-e2e",
"version": "0.0.1",
"private": true,
"nx": {
"implicitDependencies": [
"@triliumnext/client"
"@triliumnext/client",
"@triliumnext/server"
]
}
}

View File

@@ -3,7 +3,8 @@ import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
const port = process.env['TRILIUM_PORT'];
const baseURL = process.env['BASE_URL'] || `http://localhost:${port}`;
/**
* Read environment variables from file.
@@ -24,8 +25,8 @@ export default defineConfig({
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx run @triliumnext/client:serve-static',
url: 'http://localhost:4200',
command: 'pnpm server:start-prod',
url: baseURL,
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot
},
@@ -35,16 +36,16 @@ export default defineConfig({
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',

View File

@@ -0,0 +1,64 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test("Help popup", async ({ page, context }) => {
page.setDefaultTimeout(15_000);
const app = new App(page, context);
await app.goto();
const popupPromise = page.waitForEvent("popup");
await app.currentNoteSplit.press("Shift+F1");
await page.getByRole("link", { name: "online" }).click();
const popup = await popupPromise;
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/");
});
test("Complete help in search", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.launcherBar.locator(".bx-search").first().click();
await app.currentNoteSplit.locator(".search-settings .bx-help-circle").click();
const popupPromise = page.waitForEvent("popup");
await page.getByRole("link", { name: "complete help on search syntax" }).click();
const popup = await popupPromise;
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
});
test("In-app-help works in English", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.currentNoteSplit.press("F1");
const title = "User Guide";
await expect(app.noteTreeHoistedNote).toContainText(title);
await expect(app.currentNoteSplitTitle).toHaveValue(title);
app.noteTree.getByText("Troubleshooting").click();
await expect(app.currentNoteSplitTitle).toHaveValue("Troubleshooting");
await app.currentNoteSplitContent.locator("p").first().waitFor({ state: "visible" });
expect(await app.currentNoteSplitContent.locator("p").count()).toBeGreaterThan(10);
});
test("In-app-help works in other languages", async ({ page, context }) => {
const app = new App(page, context);
try {
await app.goto();
await app.setOption("locale", "cn");
await app.goto();
await app.currentNoteSplit.press("F1");
const title = "用户指南";
await expect(app.noteTreeHoistedNote).toContainText(title);
await expect(app.currentNoteSplitTitle).toHaveValue(title);
app.noteTree.getByText("Troubleshooting").click();
await expect(app.currentNoteSplitTitle).toHaveValue("Troubleshooting");
await app.currentNoteSplitContent.locator("p").first().waitFor({ state: "visible" });
expect(await app.currentNoteSplitContent.locator("p").count()).toBeGreaterThan(10);
} finally {
// Ensure English is set after each locale change to avoid any leaks to other tests.
await app.setOption("locale", "en");
}
});

View File

@@ -0,0 +1,61 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test.afterEach(async ({ page, context }) => {
const app = new App(page, context);
// Ensure English is set after each locale change to avoid any leaks to other tests.
await app.setOption("locale", "en");
});
test("Displays translation on desktop", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await expect(page.locator("#left-pane .quick-search input")).toHaveAttribute("placeholder", "Quick search");
});
test("Displays translation on mobile", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ isMobile: true });
await expect(page.locator("#mobile-sidebar-wrapper .quick-search input")).toHaveAttribute("placeholder", "Quick search");
});
test("Displays translations in Settings", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToSettings();
await app.noteTree.getByText("Language & Region").click();
await expect(app.currentNoteSplit).toContainText("Localization");
await expect(app.currentNoteSplit).toContainText("Language");
});
test("User can change language from settings", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToSettings();
await app.noteTree.getByText("Language & Region").click();
// Check that the default value (English) is set.
await expect(app.currentNoteSplit).toContainText("First day of the week");
const languageCombobox = app.currentNoteSplit.getByRole("combobox").first();
await expect(languageCombobox).toHaveValue("en");
// Select Chinese and ensure the translation is set.
await languageCombobox.selectOption("cn");
// Press the refresh button.
await app.currentNoteSplit.getByRole("button", { name: "Restart the application" }).click();
await expect(app.currentNoteSplit).toContainText("一周的第一天", { timeout: 15000 });
await expect(languageCombobox).toHaveValue("cn");
// Select English again.
await languageCombobox.selectOption("en");
await expect(app.currentNoteSplit).toContainText("Language", { timeout: 15000 });
await expect(languageCombobox).toHaveValue("en");
});

View File

@@ -0,0 +1,133 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
const NOTE_TITLE = "Trilium Integration Test DB";
test("Can drag tabs around", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
// [1]: Trilium Integration Test DB note
await app.closeAllTabs();
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
await expect(app.getActiveTab()).toContainText(NOTE_TITLE);
// [1] [2] [3]
await app.addNewTab();
await app.addNewTab();
let tab = app.getTab(0);
// Drag the first tab at the end
await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 } });
tab = app.getTab(2);
await expect(tab).toContainText(NOTE_TITLE);
// Drag the tab to the left
await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 } });
await expect(app.getTab(0)).toContainText(NOTE_TITLE);
});
test("Can drag tab to new window", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
const tab = app.getTab(0);
await expect(tab).toContainText(NOTE_TITLE);
const popupPromise = page.waitForEvent("popup");
const tabPos = await tab.boundingBox();
if (tabPos) {
const x = tabPos.x + tabPos.width / 2;
const y = tabPos.y + tabPos.height / 2;
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.move(x, y + tabPos.height + 100, { steps: 5 });
await page.mouse.up();
} else {
test.fail(true, "Unable to determine tab position");
}
// Wait for the popup to show
const popup = await popupPromise;
const popupApp = new App(popup, context);
await expect(popupApp.getActiveTab()).toHaveText(NOTE_TITLE);
});
test("Tabs are restored in right order", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
// Open three tabs.
await app.closeAllTabs();
await app.goToNoteInNewTab("Code notes");
await app.addNewTab();
await app.goToNoteInNewTab("Text notes");
await app.addNewTab();
await app.goToNoteInNewTab("Mermaid");
// Select the mid one.
await app.getTab(1).click();
// Refresh the page and check the order.
await app.goto( { preserveTabs: true });
await expect(app.getTab(0)).toContainText("Code notes");
await expect(app.getTab(1)).toContainText("Text notes");
await expect(app.getTab(2)).toContainText("Mermaid");
// Check the note tree has the right active node.
await expect(app.noteTreeActiveNote).toContainText("Text notes");
});
test("Empty tabs are cleared out", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
// Open three tabs.
await app.closeAllTabs();
await app.addNewTab();
await app.goToNoteInNewTab("Code notes");
await app.addNewTab();
await app.addNewTab();
// Refresh the page and check the order.
await app.goto({ preserveTabs: true });
// Expect no empty tabs.
expect(await app.tabBar.locator(".note-tab-wrapper").count()).toBe(1);
});
test("Search works when dismissing a tab", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Table of contents");
await app.openAndClickNoteActionMenu("Search in note");
await expect(app.findAndReplaceWidget).toBeVisible();
app.findAndReplaceWidget.locator(".find-widget-close-button").click();
await app.addNewTab();
await app.goToNoteInNewTab("Sample mindmap");
await app.getTab(0).click();
await app.openAndClickNoteActionMenu("Search in note");
await expect(app.findAndReplaceWidget).toBeVisible();
});
test("New tab displays workspaces", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
const workspaceNotesEl = app.currentNoteSplitContent.locator(".workspace-notes");
await expect(workspaceNotesEl).toBeVisible();
expect(workspaceNotesEl).toContainText("Personal");
expect(workspaceNotesEl).toContainText("Work");
await expect(workspaceNotesEl.locator(".bx.bxs-user")).toBeVisible();
await expect(workspaceNotesEl.locator(".bx.bx-briefcase-alt")).toBeVisible();
await app.closeAllTabs();
});

View File

@@ -0,0 +1,47 @@
import { test, expect, Page } from "@playwright/test";
import App from "../support/app";
test("Displays lint warnings for backend script", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Backend script with lint warnings");
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
// Expect two warning signs in the gutter.
await expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2);
// Hover over hello
await codeEditor.getByText("hello").first().hover();
await expectTooltip(page, "'hello' is defined but never used.");
// Hover over world
await codeEditor.getByText("world").first().hover();
await expectTooltip(page, "'world' is defined but never used.");
});
test("Displays lint errors for backend script", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Backend script with lint errors");
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
// Expect two warning signs in the gutter.
const errorMarker = codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-error");
await expect(errorMarker).toHaveCount(1);
// Hover over hello
await errorMarker.hover();
await expectTooltip(page, "Parsing error: Unexpected token world");
});
async function expectTooltip(page: Page, tooltip: string) {
await expect(
page.locator(".CodeMirror-lint-tooltip:visible", {
hasText: tooltip
})
).toBeVisible();
}

View File

@@ -0,0 +1,69 @@
import { test, expect, Page, BrowserContext } from "@playwright/test";
import App from "../support/app";
test("renders ELK flowchart", async ({ page, context }) => {
await testAriaSnapshot({
page,
context,
noteTitle: "Flowchart ELK on",
snapshot: `
- document:
- paragraph: A
- paragraph: B
- paragraph: C
- paragraph: Guarantee
- paragraph: User attributes
- paragraph: Master data
- paragraph: Exchange Rate
- paragraph: Profit Centers
- paragraph: Vendor Partners
- paragraph: Work Situation
- paragraph: Customer
- paragraph: Profit Centers
- paragraph: Guarantee
- text: Interfaces for B
`
});
});
test("renders standard flowchart", async ({ page, context }) => {
await testAriaSnapshot({
page,
context,
noteTitle: "Flowchart ELK off",
snapshot: `
- document:
- paragraph: Guarantee
- paragraph: User attributes
- paragraph: Master data
- paragraph: Exchange Rate
- paragraph: Profit Centers
- paragraph: Vendor Partners
- paragraph: Work Situation
- paragraph: Customer
- paragraph: Profit Centers
- paragraph: Guarantee
- paragraph: A
- paragraph: B
- paragraph: C
- text: Interfaces for B
`
});
});
interface AriaTestOpts {
page: Page;
context: BrowserContext;
noteTitle: string;
snapshot: string;
}
async function testAriaSnapshot({ page, context, noteTitle, snapshot }: AriaTestOpts) {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab(noteTitle);
const svgData = app.currentNoteSplit.locator(".render-container svg");
await expect(svgData).toBeVisible();
await expect(svgData).toMatchAriaSnapshot(snapshot);
}

View File

@@ -0,0 +1,22 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
test("displays simple map", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Sample mindmap");
await expect(app.currentNoteSplit).toContainText("Hello world");
await expect(app.currentNoteSplit).toContainText("1");
await expect(app.currentNoteSplit).toContainText("1a");
});
test("displays note settings", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Sample mindmap");
await app.currentNoteSplit.getByText("Hello world").click({ force: true });
const nodeMenu = app.currentNoteSplit.locator(".node-menu");
await expect(nodeMenu).toBeVisible();
});

View File

@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
test("renders global map", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.launcherBar.locator(".launcher-button.bx-map-alt").click();
await expect(app.currentNoteSplit.locator(".force-graph-container canvas")).toBeVisible();
});

View File

@@ -0,0 +1,84 @@
import { test, expect, Page } from "@playwright/test";
import App from "../support/app";
test("Table of contents is displayed", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Table of contents");
await expect(app.sidebar).toContainText("Table of Contents");
const rootList = app.sidebar.locator(".toc-widget > span > ol");
// Heading 1.1
// Heading 1.1
// Heading 1.2
// Heading 2
// Heading 2.1
// Heading 2.2
// Heading 2.2.1
// Heading 2.2.1.1
// Heading 2.2.11.1
await expect(rootList.locator("> li")).toHaveCount(2);
await expect(rootList.locator("> li").first()).toHaveText("Heading 1");
await expect(rootList.locator("> ol").first().locator("> li").first()).toHaveText("Heading 1.1");
await expect(rootList.locator("> ol").first().locator("> li").nth(1)).toHaveText("Heading 1.2");
// Heading 2 has a Katex equation, check if it's rendered.
await expect(rootList.locator("> li").nth(1)).toContainText("Heading 2");
await expect(rootList.locator("> li").nth(1).locator(".katex")).toBeAttached();
await expect(rootList.locator("> ol")).toHaveCount(2);
await expect(rootList.locator("> ol").nth(1).locator("> li")).toHaveCount(2);
await expect(rootList.locator("> ol").nth(1).locator("> ol")).toHaveCount(1);
await expect(rootList.locator("> ol").nth(1).locator("> ol > ol")).toHaveCount(1);
await expect(rootList.locator("> ol").nth(1).locator("> ol > ol > ol")).toHaveCount(1);
});
test("Highlights list is displayed", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Highlights list");
await expect(app.sidebar).toContainText("Highlights List");
const rootList = app.sidebar.locator(".highlights-list ol");
let index = 0;
for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) {
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
}
});
test("Displays math popup", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Empty text");
const noteContent = app.currentNoteSplit.locator(".note-detail-editable-text-editor");
await noteContent.fill("Hello world");
await noteContent.press("ControlOrMeta+M");
const mathForm = page.locator(".ck-math-form");
await expect(mathForm).toBeVisible();
const input = mathForm.locator(".ck-input").first();
await input.click();
await input.fill("e=mc^2");
await page.waitForTimeout(100);
const preview = page.locator('[id^="math-preview"]');
await preview.waitFor({
state: 'visible',
timeout: 5000
});
await page.waitForFunction((): boolean => {
const preview = document.querySelector('[id^="math-preview"]');
if (!preview) return false;
const katex = preview.querySelector('.katex');
return !!katex && window.getComputedStyle(preview).display !== 'none';
}, { timeout: 5000 });
await expect(preview.locator('.katex')).toBeVisible();
await expect(preview).toMatchAriaSnapshot("- math: e = m c 2");
});

View File

@@ -0,0 +1,18 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test("Goes to share root", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "/share" });
const noteTitle = "Shared notes";
await expect(page).toHaveTitle(noteTitle);
await expect(page.locator("h1")).toHaveText(noteTitle);
});
test("Goes to parent share root page", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "/share/bKMn5EFv9KS2" });
await expect(page.locator("h1")).toHaveText("Child note");
await page.locator("#parentLink a").click();
await page.waitForURL("/share/");
});

View File

@@ -0,0 +1,157 @@
import { expect, Locator, Page } from "@playwright/test";
import type { BrowserContext } from "@playwright/test";
interface GotoOpts {
url?: string;
isMobile?: boolean;
preserveTabs?: boolean;
}
const BASE_URL = "http://127.0.0.1:8082";
export default class App {
readonly page: Page;
readonly context: BrowserContext;
readonly tabBar: Locator;
readonly noteTree: Locator;
readonly noteTreeActiveNote: Locator;
readonly noteTreeHoistedNote: Locator;
readonly launcherBar: Locator;
readonly currentNoteSplit: Locator;
readonly currentNoteSplitTitle: Locator;
readonly currentNoteSplitContent: Locator;
readonly sidebar: Locator;
constructor(page: Page, context: BrowserContext) {
this.page = page;
this.context = context;
this.tabBar = page.locator(".tab-row-widget-container");
this.noteTree = page.locator(".tree-wrapper");
this.noteTreeActiveNote = this.noteTree.locator(".fancytree-node.fancytree-active");
this.noteTreeHoistedNote = this.noteTree.locator(".fancytree-node", { has: page.locator(".unhoist-button") });
this.launcherBar = page.locator("#launcher-container");
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)");
this.currentNoteSplitTitle = this.currentNoteSplit.locator(".note-title");
this.currentNoteSplitContent = this.currentNoteSplit.locator(".note-detail-printable.visible");
this.sidebar = page.locator("#right-pane");
}
async goto({ url, isMobile, preserveTabs }: GotoOpts = {}) {
await this.context.addCookies([
{
url: BASE_URL,
name: "trilium-device",
value: isMobile ? "mobile" : "desktop"
}
]);
if (!url) {
url = "/";
}
await this.page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
// Wait for the page to load.
if (url === "/") {
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
if (!preserveTabs) {
await this.closeAllTabs();
}
}
}
async goToNoteInNewTab(noteTitle: string) {
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
await autocomplete.fill(noteTitle);
await expect(this.currentNoteSplit.locator(".note-detail-empty-results")).toContainText(noteTitle);
await autocomplete.press("ArrowDown");
await autocomplete.press("Enter");
}
async goToSettings() {
await this.page.locator(".launcher-button.bx-cog").click();
}
getTab(tabIndex: number) {
return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex);
}
getActiveTab() {
return this.tabBar.locator(".note-tab[active]");
}
/**
* Closes all the tabs in the client by issuing a command.
*/
async closeAllTabs() {
await this.triggerCommand("closeAllTabs");
// Page in Playwright is not updated somehow, need to click on the tab to make sure it's rendered
await this.getTab(0).click();
}
/**
* Adds a new tab by cliking on the + button near the tab bar.
*/
async addNewTab() {
await this.page.locator('[data-trigger-command="openNewTab"]').click();
}
/**
* Looks for a given title in the note tree and clicks on it. Useful for selecting option pages in settings in a similar fashion as the user.
* @param title the title of the note to click, as displayed in the note tree.
*/
async clickNoteOnNoteTreeByTitle(title: string) {
await this.noteTree.getByText(title).click();
}
/**
* Opens the note context menu by clicking on it, looks for the item with the given text and clicks it.
*
* Assertions are put in place to make sure the menu is open and closed after the click.
* @param itemToFind the text of the item to find in the menu.
*/
async openAndClickNoteActionMenu(itemToFind: string) {
const noteActionsButton = this.currentNoteSplit.locator(".note-actions");
await noteActionsButton.click();
const dropdownMenu = noteActionsButton.locator(".dropdown-menu");
await this.page.waitForTimeout(100);
await expect(dropdownMenu).toBeVisible();
dropdownMenu.getByText(itemToFind).click();
await expect(dropdownMenu).not.toBeVisible();
}
/**
* Obtains the locator to the find and replace widget, if it's being displayed.
*/
get findAndReplaceWidget() {
return this.page.locator(".component.visible.find-replace-widget");
}
/**
* Executes any Trilium command on the client.
* @param command the command to send.
*/
async triggerCommand(command: string) {
await this.page.evaluate(async (command: string) => {
await (window as any).glob.appContext.triggerCommand(command);
}, command);
}
async setOption(key: string, value: string) {
const csrfToken = await this.page.evaluate(() => {
return (window as any).glob.csrfToken;
});
expect(csrfToken).toBeTruthy();
await expect(
await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, {
headers: {
"x-csrf-token": csrfToken
}
})
).toBeOK();
}
}

View File

@@ -0,0 +1,2 @@
TRILIUM_ENV=production
TRILIUM_DATA_DIR=./apps/server/data

View File

@@ -150,6 +150,10 @@
"dependsOn": [ "build" ],
"command": "bash apps/server/scripts/build-server.sh"
},
"start-prod": {
"dependsOn": [ "build" ],
"command": "node apps/server/dist/main.js"
},
"docker-build": {
"dependsOn": [
"build"

View File

@@ -54,7 +54,7 @@ class IndexService {
return;
}
const aiEnabled = await options.getOptionBool('aiEnabled');
const aiEnabled = options.getOptionOrNull('aiEnabled') === "true";
if (!aiEnabled) {
log.info("Index service: AI features disabled, skipping initialization");
return;