diff --git a/CLAUDE.md b/CLAUDE.md index 1fcc3fa..08d21e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,47 @@ Example commit command: git commit --author="Hikari " --no-gpg-sign -m "your commit message" ``` +## Testing Requirements + +All new features, fixes, and significant changes should include tests whenever possible: + +- **Frontend tests**: Use Vitest with `@testing-library/svelte` for component tests +- **Test files**: Place test files next to the code they test with `.test.ts` or `.spec.ts` extension +- **Run tests**: Use `pnpm test` to run all tests, or `pnpm test:watch` for watch mode +- **Coverage**: Run `pnpm test:coverage` to generate coverage reports +- **Rust tests**: Use `pnpm test:backend` for Rust/Tauri backend tests + +### Testing Guidelines + +- Write tests for utility functions, stores, and business logic +- For Svelte 5 components, focus on testing the underlying logic functions +- Use descriptive test names that explain what behaviour is being tested +- Include edge cases and error conditions in test coverage +- Mock Tauri APIs using the patterns in `vitest.setup.ts` + +### Example Test Structure + +```typescript +import { describe, it, expect } from "vitest"; + +describe("FeatureName", () => { + it("handles the normal case correctly", () => { + // Arrange + const input = "test data"; + + // Act + const result = functionUnderTest(input); + + // Assert + expect(result).toBe("expected output"); + }); + + it("handles edge cases gracefully", () => { + // Test edge cases... + }); +}); +``` + ## Project Context Hikari Desktop is a Tauri-based desktop application that wraps Claude Code with a visual anime character (Hikari) who appears on screen. This is a personal project where Hikari can sign her work and act as herself! diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 0e57896..d2b2b8e 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -17,6 +17,7 @@ } from "$lib/stores/historyRestore"; import MessageModeSelector from "$lib/components/MessageModeSelector.svelte"; import SlashCommandMenu from "$lib/components/SlashCommandMenu.svelte"; + import SystemClock from "$lib/components/SystemClock.svelte"; import { getCurrentMode } from "$lib/stores/messageMode"; import { formatMessageWithMode } from "$lib/types/messageMode"; import { @@ -914,6 +915,8 @@ User: ${formattedMessage}`; Clipboard + +
diff --git a/src/lib/components/SystemClock.svelte b/src/lib/components/SystemClock.svelte new file mode 100644 index 0000000..0059276 --- /dev/null +++ b/src/lib/components/SystemClock.svelte @@ -0,0 +1,81 @@ + + +
+ + + + + {currentTime} +
+ + diff --git a/src/lib/components/SystemClock.test.ts b/src/lib/components/SystemClock.test.ts new file mode 100644 index 0000000..8b8aa8c --- /dev/null +++ b/src/lib/components/SystemClock.test.ts @@ -0,0 +1,149 @@ +/** + * SystemClock Component Tests + * + * Note: This file tests the time formatting logic used by the SystemClock component. + * Full component rendering tests are challenging with Svelte 5 + @testing-library/svelte + * due to SSR/CSR compatibility issues. The component itself is simple and visually + * testable - it displays the current date and time, updating every second. + * + * What this component does: + * - Displays date in British format: "7 February 2026" + * - Displays time in 24-hour format: "14:35:42" + * - Updates every second via setInterval + * - Cleans up interval on unmount via $effect + * + * Manual testing checklist: + * - [ ] Clock appears above the Send button + * - [ ] Time updates every second + * - [ ] Date format is "DD Month YYYY" + * - [ ] Time format is "HH:MM:SS" (24-hour) + * - [ ] Hover effect works (border turns accent colour) + */ + +import { describe, it, expect } from "vitest"; + +// Helper function that mirrors the component's formatting logic +function formatDateTime(date: Date): string { + const day = date.getDate(); + const month = date.toLocaleString("en-GB", { month: "long" }); + const year = date.getFullYear(); + + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const seconds = String(date.getSeconds()).padStart(2, "0"); + + return `${day} ${month} ${year}, ${hours}:${minutes}:${seconds}`; +} + +describe("SystemClock date/time formatting", () => { + it("formats date in British format (DD Month YYYY)", () => { + // Use local timezone (not UTC) since the component uses local time + const date = new Date(2026, 1, 7, 14, 35, 42); // Feb 7, 2026 14:35:42 local + const formatted = formatDateTime(date); + + expect(formatted).toContain("7 February 2026"); + }); + + it("formats time in 24-hour format (HH:MM:SS)", () => { + const date = new Date(2026, 1, 7, 14, 35, 42); + const formatted = formatDateTime(date); + + // Should have the pattern HH:MM:SS + expect(formatted).toMatch(/\d{2}:\d{2}:\d{2}/); + expect(formatted).toContain("14:35:42"); + }); + + it("combines date and time with comma separator", () => { + const date = new Date(2026, 1, 7, 14, 35, 42); + const formatted = formatDateTime(date); + + expect(formatted).toBe("7 February 2026, 14:35:42"); + }); + + it("pads single-digit hours, minutes, and seconds with zeros", () => { + const date = new Date(2026, 1, 7, 3, 5, 8); + const formatted = formatDateTime(date); + + // Should have leading zeros: 03:05:08, not 3:5:8 + expect(formatted).toContain("03:05:08"); + }); + + it("handles different months correctly", () => { + const date = new Date(2026, 11, 25, 12, 0, 0); // December is month 11 + const formatted = formatDateTime(date); + + expect(formatted).toContain("25 December 2026"); + }); + + it("handles year changes correctly", () => { + const date = new Date(2027, 0, 1, 0, 0, 0); // January is month 0 + const formatted = formatDateTime(date); + + expect(formatted).toContain("1 January 2027"); + expect(formatted).toContain("00:00:00"); + }); + + it("handles midnight correctly", () => { + const date = new Date(2026, 1, 7, 0, 0, 0); + const formatted = formatDateTime(date); + + expect(formatted).toContain("00:00:00"); + }); + + it("handles noon correctly", () => { + const date = new Date(2026, 1, 7, 12, 0, 0); + const formatted = formatDateTime(date); + + // 24-hour format, so noon is 12:00:00, not 00:00:00 + expect(formatted).toContain("12:00:00"); + }); + + it("handles end of day correctly", () => { + const date = new Date(2026, 1, 7, 23, 59, 59); + const formatted = formatDateTime(date); + + expect(formatted).toContain("23:59:59"); + }); + + it("handles month boundaries correctly", () => { + // Last day of January + const jan31 = new Date(2026, 0, 31, 23, 59, 59); + expect(formatDateTime(jan31)).toContain("31 January 2026"); + + // First day of February + const feb1 = new Date(2026, 1, 1, 0, 0, 0); + expect(formatDateTime(feb1)).toContain("1 February 2026"); + }); + + it("handles leap year February correctly", () => { + // 2024 is a leap year + const feb29 = new Date(2024, 1, 29, 12, 0, 0); + const formatted = formatDateTime(feb29); + + expect(formatted).toContain("29 February 2024"); + }); + + it("handles all 12 months correctly", () => { + const months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + months.forEach((month, index) => { + const date = new Date(2026, index, 15, 12, 0, 0); + const formatted = formatDateTime(date); + + expect(formatted).toContain(month); + }); + }); +});