test: add comprehensive frontend tests for components, stores, and utils
Full test coverage for the frontend implementation using Vitest and Testing Library. Tests are organized by concern with shared fixtures. Component tests: - AppShell.test.tsx: Navigation tabs, view switching, batch mode overlay - FocusCard.test.tsx: Rendering, action buttons, keyboard shortcuts, empty state - QueueView.test.tsx: Item display, focus promotion, empty state - QueueItem.test.tsx: Type badges, click handling - QueueSummary.test.tsx: Count display by type - QuickCapture.test.tsx: Modal behavior, form submission, error states - BatchMode.test.tsx: Progress tracking, item advancement, completion - App.test.tsx: Updated for AppShell integration Store tests: - focus-store.test.ts: Item management, act(), setFocus(), reorderQueue() - nav-store.test.ts: View switching - capture-store.test.ts: Open/close, submission states - batch-store.test.ts: Batch lifecycle, status tracking, derived counts Library tests: - types.test.ts: Type guards, staleness computation - transform.test.ts: Lore data transformation, priority ordering - format.test.ts: IID formatting for MRs vs issues E2E tests (app.spec.ts): - Navigation flow - Focus card interactions - Queue management - Quick capture flow Test infrastructure: - fixtures.ts: makeFocusItem() factory - tauri-plugin-shell.ts: Mock for @tauri-apps/plugin-shell - Updated tauri-api.ts mock with new commands - vitest.config.ts: Path aliases, jsdom environment - playwright.config.ts: Removed webServer (run separately) - package.json: Added @tauri-apps/plugin-shell dependency Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
20
tests/lib/format.test.ts
Normal file
20
tests/lib/format.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { formatIid } from "@/lib/format";
|
||||
|
||||
describe("formatIid", () => {
|
||||
it("formats mr_review with ! prefix", () => {
|
||||
expect(formatIid("mr_review", 847)).toBe("!847");
|
||||
});
|
||||
|
||||
it("formats mr_authored with ! prefix", () => {
|
||||
expect(formatIid("mr_authored", 123)).toBe("!123");
|
||||
});
|
||||
|
||||
it("formats issue with # prefix", () => {
|
||||
expect(formatIid("issue", 42)).toBe("#42");
|
||||
});
|
||||
|
||||
it("formats manual task with # prefix", () => {
|
||||
expect(formatIid("manual", 1)).toBe("#1");
|
||||
});
|
||||
});
|
||||
119
tests/lib/transform.test.ts
Normal file
119
tests/lib/transform.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { transformLoreData } from "@/lib/transform";
|
||||
|
||||
describe("transformLoreData", () => {
|
||||
it("returns empty array for empty data", () => {
|
||||
const result = transformLoreData({
|
||||
open_issues: [],
|
||||
open_mrs_authored: [],
|
||||
reviewing_mrs: [],
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("puts reviews first, then issues, then authored MRs", () => {
|
||||
const result = transformLoreData({
|
||||
open_issues: [
|
||||
{
|
||||
iid: 42,
|
||||
title: "Bug fix",
|
||||
project: "g/p",
|
||||
web_url: "https://gitlab.com/g/p/-/issues/42",
|
||||
},
|
||||
],
|
||||
open_mrs_authored: [
|
||||
{
|
||||
iid: 200,
|
||||
title: "My feature",
|
||||
project: "g/p",
|
||||
web_url: "https://gitlab.com/g/p/-/merge_requests/200",
|
||||
},
|
||||
],
|
||||
reviewing_mrs: [
|
||||
{
|
||||
iid: 100,
|
||||
title: "Review this",
|
||||
project: "g/p",
|
||||
web_url: "https://gitlab.com/g/p/-/merge_requests/100",
|
||||
author_username: "alice",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].type).toBe("mr_review");
|
||||
expect(result[0].requestedBy).toBe("alice");
|
||||
expect(result[1].type).toBe("issue");
|
||||
expect(result[2].type).toBe("mr_authored");
|
||||
});
|
||||
|
||||
it("generates correct IDs for each type", () => {
|
||||
const result = transformLoreData({
|
||||
open_issues: [
|
||||
{
|
||||
iid: 42,
|
||||
title: "Issue",
|
||||
project: "group/repo",
|
||||
web_url: "https://x.com",
|
||||
},
|
||||
],
|
||||
open_mrs_authored: [
|
||||
{
|
||||
iid: 200,
|
||||
title: "MR",
|
||||
project: "group/repo",
|
||||
web_url: "https://x.com",
|
||||
},
|
||||
],
|
||||
reviewing_mrs: [
|
||||
{
|
||||
iid: 100,
|
||||
title: "Review",
|
||||
project: "group/repo",
|
||||
web_url: "https://x.com",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result[0].id).toBe("mr_review:group/repo:100");
|
||||
expect(result[1].id).toBe("issue:group/repo:42");
|
||||
expect(result[2].id).toBe("mr_authored:group/repo:200");
|
||||
});
|
||||
|
||||
it("preserves updated_at_iso from lore data", () => {
|
||||
const result = transformLoreData({
|
||||
open_issues: [],
|
||||
open_mrs_authored: [],
|
||||
reviewing_mrs: [
|
||||
{
|
||||
iid: 1,
|
||||
title: "T",
|
||||
project: "g/p",
|
||||
web_url: "https://x.com",
|
||||
updated_at_iso: "2026-02-25T10:00:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result[0].updatedAt).toBe("2026-02-25T10:00:00Z");
|
||||
});
|
||||
|
||||
it("handles missing optional fields gracefully", () => {
|
||||
const result = transformLoreData({
|
||||
open_issues: [
|
||||
{
|
||||
iid: 1,
|
||||
title: "T",
|
||||
project: "g/p",
|
||||
web_url: "https://x.com",
|
||||
},
|
||||
],
|
||||
open_mrs_authored: [],
|
||||
reviewing_mrs: [],
|
||||
});
|
||||
|
||||
expect(result[0].updatedAt).toBeNull();
|
||||
expect(result[0].contextQuote).toBeNull();
|
||||
expect(result[0].requestedBy).toBeNull();
|
||||
});
|
||||
});
|
||||
59
tests/lib/types.test.ts
Normal file
59
tests/lib/types.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { computeStaleness, isMcError } from "@/lib/types";
|
||||
|
||||
describe("computeStaleness", () => {
|
||||
it("returns 'normal' for null timestamp", () => {
|
||||
expect(computeStaleness(null)).toBe("normal");
|
||||
});
|
||||
|
||||
it("returns 'fresh' for items less than 1 day old", () => {
|
||||
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
|
||||
expect(computeStaleness(twoHoursAgo)).toBe("fresh");
|
||||
});
|
||||
|
||||
it("returns 'normal' for items 1-2 days old", () => {
|
||||
const thirtyHoursAgo = new Date(
|
||||
Date.now() - 30 * 60 * 60 * 1000
|
||||
).toISOString();
|
||||
expect(computeStaleness(thirtyHoursAgo)).toBe("normal");
|
||||
});
|
||||
|
||||
it("returns 'amber' for items 3-6 days old", () => {
|
||||
const fourDaysAgo = new Date(
|
||||
Date.now() - 4 * 24 * 60 * 60 * 1000
|
||||
).toISOString();
|
||||
expect(computeStaleness(fourDaysAgo)).toBe("amber");
|
||||
});
|
||||
|
||||
it("returns 'urgent' for items 7+ days old", () => {
|
||||
const tenDaysAgo = new Date(
|
||||
Date.now() - 10 * 24 * 60 * 60 * 1000
|
||||
).toISOString();
|
||||
expect(computeStaleness(tenDaysAgo)).toBe("urgent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isMcError", () => {
|
||||
it("returns true for valid McError objects", () => {
|
||||
const error = {
|
||||
code: "LORE_UNAVAILABLE",
|
||||
message: "lore CLI not found",
|
||||
recoverable: true,
|
||||
};
|
||||
expect(isMcError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for plain strings", () => {
|
||||
expect(isMcError("some error")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for null", () => {
|
||||
expect(isMcError(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for objects missing required fields", () => {
|
||||
expect(isMcError({ code: "TEST" })).toBe(false);
|
||||
expect(isMcError({ message: "test" })).toBe(false);
|
||||
expect(isMcError({ recoverable: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user