diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d37bf0b4b..0f4ddddad 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -25,5 +25,14 @@ if ! bun run build; then exit 1 fi +# Run unit tests +echo "Running bun test..." +if ! bun run test; then + echo "" + echo "Error: Unit tests failed! Please fix failing tests before committing." + echo "Run 'cd frontend && bun run test' to see the failures." + exit 1 +fi + echo "All checks passed! Proceeding with commit..." -exit 0 \ No newline at end of file +exit 0 diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml new file mode 100644 index 000000000..cd8c0f98c --- /dev/null +++ b/.github/workflows/frontend-tests.yml @@ -0,0 +1,48 @@ +name: Frontend Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test-frontend: + runs-on: ubuntu-latest-8-cores + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.2.2 + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + frontend/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + working-directory: ./frontend + run: bun install + + - name: Format check + working-directory: ./frontend + run: bun run format:check + + - name: Lint + working-directory: ./frontend + run: bun run lint + + - name: Typecheck + working-directory: ./frontend + run: bun run typecheck + + - name: Unit tests + working-directory: ./frontend + run: bun run test diff --git a/frontend/package.json b/frontend/package.json index 8192be360..ed50be807 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,9 @@ "build": "tsc -b && vite build", "build:fast": "vite build", "lint": "eslint './src/**/*.{ts,tsx}'", + "typecheck": "tsc -b", + "test": "bun test", + "test:coverage": "bun test --coverage", "preview": "vite preview", "add": "bunx shadcn-ui@latest add", "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", diff --git a/frontend/src/lib/test/der-loader.ts b/frontend/src/lib/test/der-loader.ts new file mode 100644 index 000000000..923765c93 --- /dev/null +++ b/frontend/src/lib/test/der-loader.ts @@ -0,0 +1,35 @@ +type BunPluginApi = { + plugin: (options: { + name: string; + setup: (build: { + onLoad: ( + options: { filter: RegExp }, + callback: (args: { + path: string; + }) => { contents: string; loader?: string } | Promise<{ contents: string; loader?: string }> + ) => void; + }) => void; + }) => void; + file: (path: string) => { arrayBuffer: () => Promise }; +}; + +const bun = (globalThis as unknown as { Bun?: BunPluginApi }).Bun; + +if (bun) { + bun.plugin({ + name: "der-loader", + setup(build) { + build.onLoad({ filter: /\.der$/ }, async (args) => { + const buffer = await bun.file(args.path).arrayBuffer(); + const bytes = new Uint8Array(buffer); + + return { + contents: `export default new Uint8Array([${Array.from(bytes).join(",")}]);`, + loader: "js" + }; + }); + } + }); +} + +export {}; diff --git a/frontend/src/lib/test/preload.ts b/frontend/src/lib/test/preload.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/frontend/src/lib/test/preload.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/utils/markdown.test.ts b/frontend/src/utils/markdown.test.ts new file mode 100644 index 000000000..278bcdadd --- /dev/null +++ b/frontend/src/utils/markdown.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "bun:test"; + +import { truncateMarkdownPreservingLinks } from "./markdown"; + +describe("truncateMarkdownPreservingLinks", () => { + it("returns the original text when already within maxLength", () => { + expect(truncateMarkdownPreservingLinks("hello", 10)).toBe("hello"); + }); + + it("safely truncates when there are no links", () => { + expect(truncateMarkdownPreservingLinks("abcdef", 3)).toBe("abc..."); + }); + + it("converts a plain URL into a markdown link when truncation would cut it", () => { + const before = "Check this "; + const url = "https://example.com/very/long/path"; + const after = " for more"; + const text = `${before}${url}${after}`; + + const maxLength = before.length + 12; + expect(maxLength).toBeGreaterThan(before.length); + expect(maxLength).toBeLessThan(before.length + url.length); + + const result = truncateMarkdownPreservingLinks(text, maxLength); + expect(result).toBe(`${before}[${url.slice(0, 12)}...](${url})`); + }); + + it("does not convert a URL that is already inside a markdown link", () => { + const before = "Go to "; + const link = "[site](https://example.com/very/long/path)"; + const after = " now"; + const text = `${before}${link}${after}`; + + const maxLength = before.length + 5; + const result = truncateMarkdownPreservingLinks(text, maxLength); + + expect(result).toBe("Go to..."); + }); + + it("truncates before a markdown link if truncation would break it", () => { + const before = "Start "; + const link = "[docs](https://example.com/docs)"; + const after = " end"; + const text = `${before}${link}${after}`; + + const maxLength = before.length + 1; + const result = truncateMarkdownPreservingLinks(text, maxLength); + + expect(result).toBe("Start..."); + }); + + it("does not convert a URL when truncation happens exactly at the URL boundary", () => { + const before = "Before "; + const url = "https://example.com/very/long/path"; + const after = " after"; + const text = `${before}${url}${after}`; + + expect(truncateMarkdownPreservingLinks(text, before.length)).toBe(`${before}...`); + expect(truncateMarkdownPreservingLinks(text, before.length + url.length)).toBe( + `${before}${url}...` + ); + }); + + it("converts the correct plain URL when multiple links exist", () => { + const md = "See [x](https://x.test) and "; + const url = "https://example.com/very/long/path"; + const after = " for details"; + const text = `${md}${url}${after}`; + + const maxLength = md.length + 8; + const result = truncateMarkdownPreservingLinks(text, maxLength); + + expect(result).toBe(`${md}[${url.slice(0, 8)}...](${url})`); + }); +}); diff --git a/setup-hooks.sh b/setup-hooks.sh index 2d02abb07..51752b642 100755 --- a/setup-hooks.sh +++ b/setup-hooks.sh @@ -16,4 +16,5 @@ git config core.hooksPath .githooks echo "✅ Git hooks configured successfully!" echo "The pre-commit hook will now:" echo " 1. Check code formatting with 'bun run format:check'" -echo " 2. Run 'bun run build' to ensure the project builds" \ No newline at end of file +echo " 2. Run 'bun run build' to ensure the project builds" +echo " 3. Run 'bun run test' to ensure unit tests pass"