diff --git a/.github/workflows/unit-ci.yml b/.github/workflows/unit-ci.yml new file mode 100644 index 0000000..ed7ea76 --- /dev/null +++ b/.github/workflows/unit-ci.yml @@ -0,0 +1,33 @@ +# Run Unit test without Harnesses +name: unit-ci + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install test dependencies (Playwright Chromium) + run: npm run test:install-dependencies + + - name: Run unit tests suite + run: npm run test:unit \ No newline at end of file diff --git a/package.json b/package.json index 1c430e2..ccf0c58 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "check": "npm run lint && npm run svelte-check", "unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/", "test": "vitest run", + "test:unit": "vitest run --config vitest.config.unit.ts", + "test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage", "test:install-playwright": "npx playwright install chromium", "test:install-dependencies": "npm run test:install-playwright", "test:coverage": "vitest run --coverage", diff --git a/vitest.config.common.ts b/vitest.config.common.ts new file mode 100644 index 0000000..4f8dd4a --- /dev/null +++ b/vitest.config.common.ts @@ -0,0 +1,110 @@ +import { defineConfig } from "vitest/config"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { sveltePreprocess } from "svelte-preprocess"; +import inlineWorkerPlugin from "esbuild-plugin-inline-worker"; +import path from "path"; +import { fileURLToPath } from "node:url"; +import fs from "node:fs"; +import { platform } from "node:process"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + ""); +const packageJson = JSON.parse(fs.readFileSync("./package.json") + ""); +const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + ""); +const prod = false; +const moduleAliasPlugin = { + name: "module-alias", + setup(build: any) { + build.onResolve({ filter: /.(dev)(.ts|)$/ }, (args: any) => { + // console.log(args.path); + if (prod) { + const prodTs = args.path.replace(".dev", ".prod"); + const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts"; + const realPath = path.join(args.resolveDir, statFile); + console.log(`Checking ${statFile}`); + if (fs.existsSync(realPath)) { + console.log(`Replaced ${args.path} with ${prodTs}`); + return { + path: realPath, + namespace: "file", + }; + } + } + return null; + }); + build.onResolve({ filter: /.(platform)(.ts|)$/ }, (args: any) => { + // console.log(args.path); + if (prod) { + const prodTs = args.path.replace(".platform", ".obsidian"); + const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts"; + const realPath = path.join(args.resolveDir, statFile); + console.log(`Checking ${statFile}`); + if (fs.existsSync(realPath)) { + console.log(`Replaced ${args.path} with ${prodTs}`); + return { + path: realPath, + namespace: "file", + }; + } + } + return null; + }); + }, +}; +const externals = [ + "obsidian", + "electron", + "crypto", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", +]; +const define = { + MANIFEST_VERSION: `"${manifestJson.version}"`, + PACKAGE_VERSION: `"${packageJson.version}"`, + UPDATE_INFO: `${updateInfo}`, + global: "globalThis", + hostPlatform: `"${platform}"`, +}; +export default defineConfig({ + plugins: [ + moduleAliasPlugin, + inlineWorkerPlugin({ + external: externals, + treeShaking: true, + }), + svelte({ + preprocess: sveltePreprocess(), + compilerOptions: { css: "injected", preserveComments: false }, + }), + ], + resolve: { + alias: { + obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"), + "@": path.resolve(__dirname, "./src"), + "@lib": path.resolve(__dirname, "./src/lib/src"), + src: path.resolve(__dirname, "./src"), + }, + }, + esbuild: { + define: define, + target: "es2018", + platform: "browser", + }, + // define, + server: { + headers: { + "Service-Worker-Allowed": "/", + }, + port: 5173, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts index 104c047..94e8352 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,180 +1,76 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig, mergeConfig } from "vitest/config"; import { playwright } from "@vitest/browser-playwright"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; -import { sveltePreprocess } from "svelte-preprocess"; -import inlineWorkerPlugin from "esbuild-plugin-inline-worker"; -import path from "path"; -import { fileURLToPath } from "node:url"; -import fs from "node:fs"; +import viteConfig from "./vitest.config.common"; import dotenv from "dotenv"; -import { platform } from "node:process"; - -import { acceptWebPeer, closeWebPeer, grantClipboardPermissions, openWebPeer } from "./test/lib/commands.ts"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - +import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands"; const defEnv = dotenv.config({ path: ".env" }).parsed; const testEnv = dotenv.config({ path: ".test.env" }).parsed; const env = Object.assign({}, defEnv, testEnv); const debuggerEnabled = env?.ENABLE_DEBUGGER === "true"; const enableUI = env?.ENABLE_UI === "true"; -// const livesyncLogsEnabled = env?.PRINT_LIVESYNC_LOGS === "true"; const headless = !debuggerEnabled && !enableUI; -const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + ""); -const packageJson = JSON.parse(fs.readFileSync("./package.json") + ""); -const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + ""); -const prod = false; -const moduleAliasPlugin = { - name: "module-alias", - setup(build: any) { - build.onResolve({ filter: /.(dev)(.ts|)$/ }, (args: any) => { - // console.log(args.path); - if (prod) { - const prodTs = args.path.replace(".dev", ".prod"); - const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts"; - const realPath = path.join(args.resolveDir, statFile); - console.log(`Checking ${statFile}`); - if (fs.existsSync(realPath)) { - console.log(`Replaced ${args.path} with ${prodTs}`); - return { - path: realPath, - namespace: "file", - }; - } - } - return null; - }); - build.onResolve({ filter: /.(platform)(.ts|)$/ }, (args: any) => { - // console.log(args.path); - if (prod) { - const prodTs = args.path.replace(".platform", ".obsidian"); - const statFile = prodTs.endsWith(".ts") ? prodTs : prodTs + ".ts"; - const realPath = path.join(args.resolveDir, statFile); - console.log(`Checking ${statFile}`); - if (fs.existsSync(realPath)) { - console.log(`Replaced ${args.path} with ${prodTs}`); - return { - path: realPath, - namespace: "file", - }; - } - } - return null; - }); - }, -}; -const externals = [ - "obsidian", - "electron", - "crypto", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", -]; -const define = { - MANIFEST_VERSION: `"${manifestJson.version}"`, - PACKAGE_VERSION: `"${packageJson.version}"`, - UPDATE_INFO: `${updateInfo}`, - global: "globalThis", - hostPlatform: `"${platform}"`, -}; -export default defineConfig({ - plugins: [ - moduleAliasPlugin, - inlineWorkerPlugin({ - external: externals, - treeShaking: true, - }), - svelte({ - preprocess: sveltePreprocess(), - compilerOptions: { css: "injected", preserveComments: false }, - }), - ], - resolve: { - alias: { - obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"), - "@": path.resolve(__dirname, "./src"), - "@lib": path.resolve(__dirname, "./src/lib/src"), - src: path.resolve(__dirname, "./src"), - }, - }, - esbuild: { - define: define, - target: "es2018", - platform: "browser", - }, - // define, - server: { - headers: { - "Service-Worker-Allowed": "/", - }, - port: 5173, - }, - test: { - env: env, - testTimeout: 40000, - hookTimeout: 50000, - fileParallelism: false, - isolate: true, - watch: false, - - // environment: "browser", - include: ["test/**/*.test.ts"], - coverage: { - include: ["src/**/*.ts", "src/lib/src/**/*.ts", "src/**/*.svelte"], - exclude: ["**/*.test.ts", "src/lib/**"], - provider: "v8", - reporter: ["text", "json", "html"], - // ignoreEmptyLines: true, - }, - browser: { - isolate: true, - commands: { - grantClipboardPermissions, - openWebPeer, - closeWebPeer, - acceptWebPeer, - }, - provider: playwright({ - launchOptions: { - args: ["--js-flags=--expose-gc"], - // chromiumSandbox: true, - }, - }), - enabled: true, - screenshotFailures: false, - instances: [ - { - execArgv: ["--js-flags=--expose-gc"], - browser: "chromium", - headless, - isolate: true, - inspector: debuggerEnabled - ? { - waitForDebugger: true, - enabled: true, - } - : undefined, - printConsoleTrace: debuggerEnabled, - onUnhandledError(error) { - // Ignore certain errors - const msg = error.message || ""; - if (msg.includes("Cannot create so many PeerConnections")) { - return false; - } - }, - }, - ], - headless, +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + env: env, + testTimeout: 40000, + hookTimeout: 50000, fileParallelism: false, - ui: debuggerEnabled || enableUI ? true : false, + isolate: true, + watch: false, + + // environment: "browser", + include: ["test/**/*.test.ts"], + coverage: { + include: ["src/**/*.ts", "src/lib/src/**/*.ts", "src/**/*.svelte"], + exclude: ["**/*.test.ts", "src/lib/**"], + provider: "v8", + reporter: ["text", "json", "html"], + // ignoreEmptyLines: true, + }, + browser: { + isolate: true, + commands: { + grantClipboardPermissions, + openWebPeer, + closeWebPeer, + acceptWebPeer, + }, + provider: playwright({ + launchOptions: { + args: ["--js-flags=--expose-gc"], + // chromiumSandbox: true, + }, + }), + enabled: true, + screenshotFailures: false, + instances: [ + { + execArgv: ["--js-flags=--expose-gc"], + browser: "chromium", + headless, + isolate: true, + inspector: debuggerEnabled + ? { + waitForDebugger: true, + enabled: true, + } + : undefined, + printConsoleTrace: debuggerEnabled, + onUnhandledError(error) { + // Ignore certain errors + const msg = error.message || ""; + if (msg.includes("Cannot create so many PeerConnections")) { + return false; + } + }, + }, + ], + headless, + fileParallelism: false, + ui: debuggerEnabled || enableUI ? true : false, + }, }, - }, -}); + }) +); diff --git a/vitest.config.unit.ts b/vitest.config.unit.ts new file mode 100644 index 0000000..119cb33 --- /dev/null +++ b/vitest.config.unit.ts @@ -0,0 +1,19 @@ +import { defineConfig, mergeConfig } from "vitest/config"; +import viteConfig from "./vitest.config.common"; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: "unit-tests", + include: ["**/*unit.test.ts"], + exclude: ["test/**"], + coverage: { + include: ["src/**/*.ts"], + exclude: ["**/*.test.ts", "src/lib/**/*.test.ts", "**/_*", "src/lib/apps", "src/lib/src/cli"], + provider: "v8", + reporter: ["text", "json", "html"], + }, + }, + }) +);