mirror of
https://github.com/zadam/trilium.git
synced 2025-11-04 20:36:13 +01:00
chore(nx/server): set up e2e
This commit is contained in:
@@ -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');
|
||||
});
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import baseConfig from "../../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...baseConfig
|
||||
];
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
});
|
||||
})
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
@@ -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__);
|
||||
};
|
||||
@@ -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}`;
|
||||
};
|
||||
@@ -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
3
apps/server-e2e/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
TRILIUM_INTEGRATION_TEST=memory
|
||||
TRILIUM_PORT=8082
|
||||
TRILIUM_DATA_DIR=apps/server/spec/db
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
64
apps/server-e2e/src/help.spec.ts
Normal file
64
apps/server-e2e/src/help.spec.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
61
apps/server-e2e/src/i18n.spec.ts
Normal file
61
apps/server-e2e/src/i18n.spec.ts
Normal 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");
|
||||
});
|
||||
133
apps/server-e2e/src/layout/tab_bar.spec.ts
Normal file
133
apps/server-e2e/src/layout/tab_bar.spec.ts
Normal 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();
|
||||
});
|
||||
47
apps/server-e2e/src/note_types/code.spec.ts
Normal file
47
apps/server-e2e/src/note_types/code.spec.ts
Normal 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();
|
||||
}
|
||||
69
apps/server-e2e/src/note_types/mermaid.spec.ts
Normal file
69
apps/server-e2e/src/note_types/mermaid.spec.ts
Normal 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);
|
||||
}
|
||||
22
apps/server-e2e/src/note_types/mindmap.spec.ts
Normal file
22
apps/server-e2e/src/note_types/mindmap.spec.ts
Normal 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();
|
||||
});
|
||||
9
apps/server-e2e/src/note_types/note_map.spec.ts
Normal file
9
apps/server-e2e/src/note_types/note_map.spec.ts
Normal 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();
|
||||
});
|
||||
84
apps/server-e2e/src/note_types/text.spec.ts
Normal file
84
apps/server-e2e/src/note_types/text.spec.ts
Normal 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");
|
||||
});
|
||||
18
apps/server-e2e/src/shared_notes.spec.ts
Normal file
18
apps/server-e2e/src/shared_notes.spec.ts
Normal 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/");
|
||||
});
|
||||
157
apps/server-e2e/src/support/app.ts
Normal file
157
apps/server-e2e/src/support/app.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
2
apps/server/.start-prod.env
Normal file
2
apps/server/.start-prod.env
Normal file
@@ -0,0 +1,2 @@
|
||||
TRILIUM_ENV=production
|
||||
TRILIUM_DATA_DIR=./apps/server/data
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user