generated from nhcarrigan/template
feat: major feature additions and improvements #135
@@ -30,6 +30,47 @@ Example commit command:
|
||||
git commit --author="Hikari <hikari@nhcarrigan.com>" --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!
|
||||
|
||||
@@ -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}`;
|
||||
</svg>
|
||||
<span>Clipboard</span>
|
||||
</button>
|
||||
|
||||
<SystemClock />
|
||||
</div>
|
||||
|
||||
<div class="input-row">
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
let currentTime = $state("");
|
||||
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
|
||||
// Format date as "1 January 2026"
|
||||
const day = now.getDate();
|
||||
const month = now.toLocaleString("en-GB", { month: "long" });
|
||||
const year = now.getFullYear();
|
||||
|
||||
// Format time as HH:MM:SS (24-hour)
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||
|
||||
currentTime = `${day} ${month} ${year}, ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
// Update immediately on mount
|
||||
updateTime();
|
||||
|
||||
// Update every second
|
||||
const interval = setInterval(updateTime, 1000);
|
||||
|
||||
// Cleanup on component destroy
|
||||
$effect(() => {
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="system-clock">
|
||||
<svg
|
||||
class="clock-icon"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span class="clock-text">{currentTime}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.system-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--font-mono, monospace);
|
||||
transition: all 0.2s;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.system-clock:hover {
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.clock-icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.clock-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user