diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index dd73e05df4..5392690413 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -40,11 +40,32 @@ jobs: - name: Run the client-side tests run: pnpm run --filter=client test + - name: Upload client test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: client-test-report + path: apps/client/test-output/vitest/html/ + retention-days: 30 + - name: Run the server-side tests run: pnpm run --filter=server test + - name: Upload server test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: server-test-report + path: apps/server/test-output/vitest/html/ + retention-days: 30 + + - name: Run CKEditor e2e tests + run: | + pnpm run --filter=ckeditor5-mermaid test + pnpm run --filter=ckeditor5-math test + - name: Run the rest of the tests - run: pnpm run --filter=\!client --filter=\!server test" + run: pnpm run --filter=\!client --filter=\!server --filter=\!ckeditor5-mermaid --filter=\!ckeditor5-math test build_docker: name: Build Docker image diff --git a/apps/client/vite.config.mts b/apps/client/vite.config.mts index 716ec8b912..29e4ee2080 100644 --- a/apps/client/vite.config.mts +++ b/apps/client/vite.config.mts @@ -120,7 +120,11 @@ export default defineConfig(() => ({ environment: "happy-dom", setupFiles: [ "./src/test/setup.ts" - ] + ], + reporters: [ + "verbose", + ["html", { outputFile: "./test-output/vitest/html/index.html" }] + ], }, commonjsOptions: { transformMixedEsModules: true, diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index d2d830ee06..3c5f4bdb72 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -86,8 +86,9 @@ export default async function buildApp() { app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt"))); app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png"))); - const sessionParser = (await import("./routes/session_parser.js")).default; + const { default: sessionParser, startSessionCleanup } = await import("./routes/session_parser.js"); app.use(sessionParser); + startSessionCleanup(); app.use(favicon(path.join(assetsDir, isDev ? "icon-dev.ico" : "icon.ico"))); if (openID.isOpenIDEnabled()) @@ -98,16 +99,16 @@ export default async function buildApp() { custom.register(app); error_handlers.register(app); - // triggers sync timer - await import("./services/sync.js"); + const { startSyncTimer } = await import("./services/sync.js"); + startSyncTimer(); - // triggers backup timer await import("./services/backup.js"); - // trigger consistency checks timer - await import("./services/consistency_checks.js"); + const { startConsistencyChecks } = await import("./services/consistency_checks.js"); + startConsistencyChecks(); - await import("./services/scheduler.js"); + const { startScheduler } = await import("./services/scheduler.js"); + startScheduler(); startScheduledCleanup(); diff --git a/apps/server/src/routes/login.spec.ts b/apps/server/src/routes/login.spec.ts index 0f210bfdc1..e6c7131ed0 100644 --- a/apps/server/src/routes/login.spec.ts +++ b/apps/server/src/routes/login.spec.ts @@ -2,7 +2,7 @@ import { dayjs } from "@triliumnext/commons"; import type { Application } from "express"; import { SessionData } from "express-session"; import supertest, { type Response } from "supertest"; -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import cls from "../services/cls.js"; import { type SQLiteSessionStore } from "./session_parser.js"; @@ -20,6 +20,10 @@ describe("Login Route test", () => { ({ sessionStore, CLEAN_UP_INTERVAL } = (await import("./session_parser.js"))); }); + afterAll(() => { + vi.useRealTimers(); + }); + it("should return the login page, when using a GET request", async () => { // RegExp for login page specific string in HTML diff --git a/apps/server/src/routes/session_parser.ts b/apps/server/src/routes/session_parser.ts index b630b09055..a52a9183aa 100644 --- a/apps/server/src/routes/session_parser.ts +++ b/apps/server/src/routes/session_parser.ts @@ -113,11 +113,13 @@ const sessionParser: express.RequestHandler = session({ store: sessionStore }); -setInterval(() => { - // Clean up expired sesions. - const now = Date.now(); - const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now); - console.log("Cleaning up expired sessions: ", result.changes); -}, CLEAN_UP_INTERVAL); +export function startSessionCleanup() { + setInterval(() => { + // Clean up expired sessions. + const now = Date.now(); + const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now); + console.log("Cleaning up expired sessions: ", result.changes); + }, CLEAN_UP_INTERVAL); +} export default sessionParser; diff --git a/apps/server/src/services/consistency_checks.ts b/apps/server/src/services/consistency_checks.ts index 7b4ba72adf..a92daf5b7e 100644 --- a/apps/server/src/services/consistency_checks.ts +++ b/apps/server/src/services/consistency_checks.ts @@ -953,12 +953,14 @@ function runEntityChangesChecks() { consistencyChecks.findEntityChangeIssues(); } -sqlInit.dbReady.then(() => { - setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000); +export function startConsistencyChecks() { + sqlInit.dbReady.then(() => { + setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000); - // kickoff checks soon after startup (to not block the initial load) - setTimeout(cls.wrap(runPeriodicChecks), 4 * 1000); -}); + // kickoff checks soon after startup (to not block the initial load) + setTimeout(cls.wrap(runPeriodicChecks), 4 * 1000); + }); +} export default { runOnDemandChecks, diff --git a/apps/server/src/services/import/single.spec.ts b/apps/server/src/services/import/single.spec.ts index e8a341a6b9..866a0874d6 100644 --- a/apps/server/src/services/import/single.spec.ts +++ b/apps/server/src/services/import/single.spec.ts @@ -24,6 +24,7 @@ async function testImport(fileName: string, mimetype: string) { const rootNote = becca.getNote("root"); if (!rootNote) { reject("Missing root note."); + return; } const importedNote = single.importSingleFile( diff --git a/apps/server/src/services/scheduler.ts b/apps/server/src/services/scheduler.ts index f44e3aa430..ef39e76009 100644 --- a/apps/server/src/services/scheduler.ts +++ b/apps/server/src/services/scheduler.ts @@ -35,39 +35,41 @@ function runNotesWithLabel(runAttrValue: string) { } } -// If the database is already initialized, we need to check the hidden subtree. Otherwise, hidden subtree -// is also checked before importing the demo.zip, so no need to do it again. -if (sqlInit.isDbInitialized()) { - console.log("Checking hidden subtree."); - sqlInit.dbReady.then(() => cls.init(() => hiddenSubtreeService.checkHiddenSubtree())); -} - -// Periodic checks. -sqlInit.dbReady.then(() => { - if (!process.env.TRILIUM_SAFE_MODE) { - setTimeout( - cls.wrap(() => runNotesWithLabel("backendStartup")), - 10 * 1000 - ); - - setInterval( - cls.wrap(() => runNotesWithLabel("hourly")), - 3600 * 1000 - ); - - setInterval( - cls.wrap(() => runNotesWithLabel("daily")), - 24 * 3600 * 1000 - ); - - setInterval( - cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()), - 7 * 3600 * 1000 - ); +export function startScheduler() { + // If the database is already initialized, we need to check the hidden subtree. Otherwise, hidden subtree + // is also checked before importing the demo.zip, so no need to do it again. + if (sqlInit.isDbInitialized()) { + console.log("Checking hidden subtree."); + sqlInit.dbReady.then(() => cls.init(() => hiddenSubtreeService.checkHiddenSubtree())); } - setInterval(() => checkProtectedSessionExpiration(), 30000); -}); + // Periodic checks. + sqlInit.dbReady.then(() => { + if (!process.env.TRILIUM_SAFE_MODE) { + setTimeout( + cls.wrap(() => runNotesWithLabel("backendStartup")), + 10 * 1000 + ); + + setInterval( + cls.wrap(() => runNotesWithLabel("hourly")), + 3600 * 1000 + ); + + setInterval( + cls.wrap(() => runNotesWithLabel("daily")), + 24 * 3600 * 1000 + ); + + setInterval( + cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()), + 7 * 3600 * 1000 + ); + } + + setInterval(() => checkProtectedSessionExpiration(), 30000); + }); +} function checkProtectedSessionExpiration() { const protectedSessionTimeout = options.getOptionInt("protectedSessionTimeout"); diff --git a/apps/server/src/services/sql_init.ts b/apps/server/src/services/sql_init.ts index 93452669fc..c6a084a72b 100644 --- a/apps/server/src/services/sql_init.ts +++ b/apps/server/src/services/sql_init.ts @@ -50,7 +50,7 @@ async function initDbConnection() { await migrationService.migrateIfNecessary(); - sql.execute('CREATE TEMP TABLE "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)'); + sql.execute('CREATE TEMP TABLE IF NOT EXISTS "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)'); sql.execute(` CREATE TABLE IF NOT EXISTS "user_data" diff --git a/apps/server/src/services/sync.ts b/apps/server/src/services/sync.ts index ef3bd6cba9..9b01a360bc 100644 --- a/apps/server/src/services/sync.ts +++ b/apps/server/src/services/sync.ts @@ -446,15 +446,17 @@ function getOutstandingPullCount() { return outstandingPullCount; } -becca_loader.beccaLoaded.then(() => { - setInterval(cls.wrap(sync), 60000); +export function startSyncTimer() { + becca_loader.beccaLoaded.then(() => { + setInterval(cls.wrap(sync), 60000); - // kickoff initial sync immediately, but should happen after initial consistency checks - setTimeout(cls.wrap(sync), 5000); + // kickoff initial sync immediately, but should happen after initial consistency checks + setTimeout(cls.wrap(sync), 5000); - // called just so ws.setLastSyncedPush() is called - getLastSyncedPush(); -}); + // called just so ws.setLastSyncedPush() is called + getLastSyncedPush(); + }); +} export default { sync, diff --git a/apps/server/src/share/routes.spec.ts b/apps/server/src/share/routes.spec.ts index a920a3620d..f4ce2c1736 100644 --- a/apps/server/src/share/routes.spec.ts +++ b/apps/server/src/share/routes.spec.ts @@ -1,6 +1,6 @@ import type { Application, NextFunction,Request, Response } from "express"; import supertest from "supertest"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { safeExtractMessageAndStackFromError } from "../services/utils.js"; @@ -23,6 +23,10 @@ describe("Share API test", () => { }); }); + afterAll(() => { + vi.useRealTimers(); + }); + beforeEach(() => { cannotSetHeadersCount = 0; }); diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index 8211ba8479..2fa10ff798 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -19,16 +19,18 @@ export default defineConfig(() => ({ exclude: [ "spec/build-checks/**", ], - hookTimeout: 20000, + hookTimeout: 20_000, + testTimeout: 40_000, reporters: [ - "verbose" + "verbose", + ["html", { outputFile: "./test-output/vitest/html/index.html" }] ], coverage: { reportsDirectory: './test-output/vitest/coverage', provider: 'v8' as const, reporter: [ "text", "html" ] }, - pool: "vmForks", - maxWorkers: 3 + pool: "forks", + maxWorkers: 6 }, }));