generated from nhcarrigan/template
test: add component logic tests and clean up eslint directives
Adds mirror-function tests for five Svelte components (HighlightedText, CliVersion, AchievementNotification, StatusBar, ConversationTabs) and removes stale eslint-disable comments from existing store test files.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* AchievementNotification Component Tests
|
||||
*
|
||||
* Tests the rarity classification and colour mapping logic used by the
|
||||
* AchievementNotification component.
|
||||
*
|
||||
* What this component does:
|
||||
* - Listens for "achievement:unlocked" Tauri events
|
||||
* - Queues and displays achievement notifications one at a time
|
||||
* - Each notification shows the achievement's name, icon, description, and rarity
|
||||
* - A gradient border and badge colour correspond to the achievement's rarity
|
||||
*
|
||||
* Manual testing checklist:
|
||||
* - [ ] Achievement notification slides in from the right
|
||||
* - [ ] Notification auto-dismisses after 5 seconds
|
||||
* - [ ] Dismiss button works immediately
|
||||
* - [ ] Multiple achievements queue and display sequentially
|
||||
* - [ ] Legendary achievements have a yellow-orange gradient
|
||||
* - [ ] Epic achievements have a purple-pink gradient
|
||||
* - [ ] Rare achievements have a blue-indigo gradient
|
||||
* - [ ] Common achievements have a green-emerald gradient
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
function getAchievementRarity(id: string): string {
|
||||
if (id === "TokenMaster") return "legendary";
|
||||
if (["CodeMachine", "Unstoppable"].includes(id)) return "epic";
|
||||
if (
|
||||
[
|
||||
"BlossomingCoder",
|
||||
"CodeWizard",
|
||||
"MasterBuilder",
|
||||
"EnduranceChamp",
|
||||
"DeepDive",
|
||||
"CreativeCoder",
|
||||
].includes(id)
|
||||
)
|
||||
return "rare";
|
||||
return "common";
|
||||
}
|
||||
|
||||
function getRarityColor(rarity: string): string {
|
||||
switch (rarity) {
|
||||
case "legendary":
|
||||
return "from-yellow-400 to-orange-500";
|
||||
case "epic":
|
||||
return "from-purple-400 to-pink-500";
|
||||
case "rare":
|
||||
return "from-blue-400 to-indigo-500";
|
||||
default:
|
||||
return "from-green-400 to-emerald-500";
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
describe("getAchievementRarity", () => {
|
||||
describe("legendary tier", () => {
|
||||
it("classifies TokenMaster as legendary", () => {
|
||||
expect(getAchievementRarity("TokenMaster")).toBe("legendary");
|
||||
});
|
||||
});
|
||||
|
||||
describe("epic tier", () => {
|
||||
it("classifies CodeMachine as epic", () => {
|
||||
expect(getAchievementRarity("CodeMachine")).toBe("epic");
|
||||
});
|
||||
|
||||
it("classifies Unstoppable as epic", () => {
|
||||
expect(getAchievementRarity("Unstoppable")).toBe("epic");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rare tier", () => {
|
||||
it("classifies BlossomingCoder as rare", () => {
|
||||
expect(getAchievementRarity("BlossomingCoder")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies CodeWizard as rare", () => {
|
||||
expect(getAchievementRarity("CodeWizard")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies MasterBuilder as rare", () => {
|
||||
expect(getAchievementRarity("MasterBuilder")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies EnduranceChamp as rare", () => {
|
||||
expect(getAchievementRarity("EnduranceChamp")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies DeepDive as rare", () => {
|
||||
expect(getAchievementRarity("DeepDive")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies CreativeCoder as rare", () => {
|
||||
expect(getAchievementRarity("CreativeCoder")).toBe("rare");
|
||||
});
|
||||
});
|
||||
|
||||
describe("common tier", () => {
|
||||
it("classifies unknown IDs as common", () => {
|
||||
expect(getAchievementRarity("FirstChat")).toBe("common");
|
||||
expect(getAchievementRarity("SomeNewAchievement")).toBe("common");
|
||||
expect(getAchievementRarity("")).toBe("common");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRarityColor", () => {
|
||||
it("returns yellow-to-orange gradient for legendary", () => {
|
||||
expect(getRarityColor("legendary")).toBe("from-yellow-400 to-orange-500");
|
||||
});
|
||||
|
||||
it("returns purple-to-pink gradient for epic", () => {
|
||||
expect(getRarityColor("epic")).toBe("from-purple-400 to-pink-500");
|
||||
});
|
||||
|
||||
it("returns blue-to-indigo gradient for rare", () => {
|
||||
expect(getRarityColor("rare")).toBe("from-blue-400 to-indigo-500");
|
||||
});
|
||||
|
||||
it("returns green-to-emerald gradient for common", () => {
|
||||
expect(getRarityColor("common")).toBe("from-green-400 to-emerald-500");
|
||||
});
|
||||
|
||||
it("falls back to green-to-emerald gradient for unknown rarities", () => {
|
||||
expect(getRarityColor("mythic")).toBe("from-green-400 to-emerald-500");
|
||||
expect(getRarityColor("")).toBe("from-green-400 to-emerald-500");
|
||||
});
|
||||
|
||||
describe("end-to-end rarity pipeline", () => {
|
||||
it("produces the correct colour for a legendary achievement", () => {
|
||||
const color = getRarityColor(getAchievementRarity("TokenMaster"));
|
||||
expect(color).toBe("from-yellow-400 to-orange-500");
|
||||
});
|
||||
|
||||
it("produces the correct colour for an epic achievement", () => {
|
||||
const color = getRarityColor(getAchievementRarity("CodeMachine"));
|
||||
expect(color).toBe("from-purple-400 to-pink-500");
|
||||
});
|
||||
|
||||
it("produces the correct colour for a rare achievement", () => {
|
||||
const color = getRarityColor(getAchievementRarity("CodeWizard"));
|
||||
expect(color).toBe("from-blue-400 to-indigo-500");
|
||||
});
|
||||
|
||||
it("produces the correct colour for a common achievement", () => {
|
||||
const color = getRarityColor(getAchievementRarity("FirstChat"));
|
||||
expect(color).toBe("from-green-400 to-emerald-500");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* CliVersion Component Tests
|
||||
*
|
||||
* Tests the version comparison logic used by the CliVersion component,
|
||||
* which compares the installed CLI version against the supported version.
|
||||
*
|
||||
* What this component does:
|
||||
* - Displays the installed Claude CLI version
|
||||
* - Displays the highest audited/supported CLI version
|
||||
* - Shows a warning when the installed version is ahead of or behind supported
|
||||
*
|
||||
* Manual testing checklist:
|
||||
* - [ ] Installed version is fetched and displayed on mount
|
||||
* - [ ] "current" badge shows in green when versions match
|
||||
* - [ ] "ahead" badge shows in amber when installed is newer than supported
|
||||
* - [ ] "behind" badge shows in red when installed is older than supported
|
||||
* - [ ] Warning message appears for "ahead" and "behind" states
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
const SUPPORTED_CLI_VERSION = "2.1.53";
|
||||
|
||||
function compareVersions(a: string, b: string): number {
|
||||
const aParts = a.split(".").map(Number);
|
||||
const bParts = b.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
||||
const aVal = aParts[i] ?? 0;
|
||||
const bVal = bParts[i] ?? 0;
|
||||
if (aVal > bVal) return 1;
|
||||
if (aVal < bVal) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
describe("SUPPORTED_CLI_VERSION", () => {
|
||||
it("is defined and non-empty", () => {
|
||||
expect(SUPPORTED_CLI_VERSION).toBeTruthy();
|
||||
});
|
||||
|
||||
it("matches the expected audited version", () => {
|
||||
expect(SUPPORTED_CLI_VERSION).toBe("2.1.53");
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareVersions", () => {
|
||||
describe("equal versions", () => {
|
||||
it("returns 0 for identical versions", () => {
|
||||
expect(compareVersions("1.0.0", "1.0.0")).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 for the supported CLI version against itself", () => {
|
||||
expect(compareVersions(SUPPORTED_CLI_VERSION, SUPPORTED_CLI_VERSION)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 for 0.0.0 vs 0.0.0", () => {
|
||||
expect(compareVersions("0.0.0", "0.0.0")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("major version differences", () => {
|
||||
it("returns 1 when a has a higher major version", () => {
|
||||
expect(compareVersions("2.0.0", "1.0.0")).toBe(1);
|
||||
});
|
||||
|
||||
it("returns -1 when a has a lower major version", () => {
|
||||
expect(compareVersions("1.0.0", "2.0.0")).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("minor version differences", () => {
|
||||
it("returns 1 when a has a higher minor version", () => {
|
||||
expect(compareVersions("1.2.0", "1.1.0")).toBe(1);
|
||||
});
|
||||
|
||||
it("returns -1 when a has a lower minor version", () => {
|
||||
expect(compareVersions("1.1.0", "1.2.0")).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("patch version differences", () => {
|
||||
it("returns 1 when a has a higher patch version", () => {
|
||||
expect(compareVersions("1.0.2", "1.0.1")).toBe(1);
|
||||
});
|
||||
|
||||
it("returns -1 when a has a lower patch version", () => {
|
||||
expect(compareVersions("1.0.1", "1.0.2")).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("major version takes precedence", () => {
|
||||
it("returns 1 when a has a higher major but lower minor", () => {
|
||||
expect(compareVersions("2.0.0", "1.9.9")).toBe(1);
|
||||
});
|
||||
|
||||
it("returns -1 when a has a lower major but higher minor", () => {
|
||||
expect(compareVersions("1.9.9", "2.0.0")).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unequal segment counts", () => {
|
||||
it("treats missing segments as 0 (a shorter than b)", () => {
|
||||
expect(compareVersions("1.0", "1.0.0")).toBe(0);
|
||||
});
|
||||
|
||||
it("treats missing segments as 0 (a longer than b)", () => {
|
||||
expect(compareVersions("1.0.0", "1.0")).toBe(0);
|
||||
});
|
||||
|
||||
it("correctly compares when a has an extra non-zero segment", () => {
|
||||
expect(compareVersions("1.0.1", "1.0")).toBe(1);
|
||||
});
|
||||
|
||||
it("correctly compares when b has an extra non-zero segment", () => {
|
||||
expect(compareVersions("1.0", "1.0.1")).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("supported CLI version comparisons", () => {
|
||||
it("returns 1 for a version ahead of supported", () => {
|
||||
expect(compareVersions("2.2.0", SUPPORTED_CLI_VERSION)).toBe(1);
|
||||
});
|
||||
|
||||
it("returns -1 for a version behind supported", () => {
|
||||
expect(compareVersions("2.1.0", SUPPORTED_CLI_VERSION)).toBe(-1);
|
||||
});
|
||||
|
||||
it("returns 0 for exactly the supported version", () => {
|
||||
expect(compareVersions("2.1.53", SUPPORTED_CLI_VERSION)).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* ConversationTabs Component Tests
|
||||
*
|
||||
* Tests the connection status colour mapping and unread message detection
|
||||
* logic used by the ConversationTabs component.
|
||||
*
|
||||
* What this component does:
|
||||
* - Displays one tab per conversation
|
||||
* - Each tab shows a coloured dot for its connection state
|
||||
* - Inactive tabs with new messages show an animated blue dot badge
|
||||
* - Tabs can be renamed by double-clicking
|
||||
* - Tabs can be reordered by drag-and-drop
|
||||
* - New tabs created with Ctrl+T, closed with Ctrl+W
|
||||
*
|
||||
* Manual testing checklist:
|
||||
* - [ ] Connected tabs show a green dot
|
||||
* - [ ] Connecting tabs show a yellow dot
|
||||
* - [ ] Disconnected tabs show a red dot
|
||||
* - [ ] Active tab never shows the unread badge
|
||||
* - [ ] Inactive tab shows blue pulsing dot when it receives new messages
|
||||
* - [ ] Switching to a tab clears the unread indicator
|
||||
* - [ ] Double-clicking a tab name enables inline editing
|
||||
* - [ ] Tabs can be dragged to reorder
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
type ConnectionStatus = "connected" | "connecting" | "disconnected";
|
||||
|
||||
function getConnectionStatusColor(status: ConnectionStatus | string): string {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return "bg-green-500";
|
||||
case "connecting":
|
||||
return "bg-yellow-500";
|
||||
case "disconnected":
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
}
|
||||
}
|
||||
|
||||
function hasUnreadMessages(
|
||||
id: string,
|
||||
conversationLineCount: number,
|
||||
activeConversationId: string | null,
|
||||
lastSeenMessageCount: Map<string, number>
|
||||
): boolean {
|
||||
if (id === activeConversationId) return false;
|
||||
const lastSeen = lastSeenMessageCount.get(id) ?? 0;
|
||||
return conversationLineCount > lastSeen;
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
describe("getConnectionStatusColor", () => {
|
||||
it("returns green for connected status", () => {
|
||||
expect(getConnectionStatusColor("connected")).toBe("bg-green-500");
|
||||
});
|
||||
|
||||
it("returns yellow for connecting status", () => {
|
||||
expect(getConnectionStatusColor("connecting")).toBe("bg-yellow-500");
|
||||
});
|
||||
|
||||
it("returns red for disconnected status", () => {
|
||||
expect(getConnectionStatusColor("disconnected")).toBe("bg-red-500");
|
||||
});
|
||||
|
||||
it("returns grey for unknown status (fallback)", () => {
|
||||
expect(getConnectionStatusColor("error")).toBe("bg-gray-500");
|
||||
expect(getConnectionStatusColor("")).toBe("bg-gray-500");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasUnreadMessages", () => {
|
||||
it("returns false for the active conversation regardless of message count", () => {
|
||||
const lastSeen = new Map([["tab-1", 0]]);
|
||||
expect(hasUnreadMessages("tab-1", 10, "tab-1", lastSeen)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when an inactive tab has more messages than last seen", () => {
|
||||
const lastSeen = new Map([["tab-1", 5]]);
|
||||
expect(hasUnreadMessages("tab-1", 10, "tab-2", lastSeen)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when an inactive tab has no new messages", () => {
|
||||
const lastSeen = new Map([["tab-1", 10]]);
|
||||
expect(hasUnreadMessages("tab-1", 10, "tab-2", lastSeen)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when an inactive tab has fewer messages than last seen", () => {
|
||||
const lastSeen = new Map([["tab-1", 15]]);
|
||||
expect(hasUnreadMessages("tab-1", 10, "tab-2", lastSeen)).toBe(false);
|
||||
});
|
||||
|
||||
it("treats a tab with no last-seen record as having 0 messages seen", () => {
|
||||
const lastSeen = new Map<string, number>();
|
||||
// Tab has 1 message but no entry in lastSeen → treated as 0 seen → unread
|
||||
expect(hasUnreadMessages("tab-1", 1, "tab-2", lastSeen)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for an untracked tab with 0 messages", () => {
|
||||
const lastSeen = new Map<string, number>();
|
||||
expect(hasUnreadMessages("tab-1", 0, "tab-2", lastSeen)).toBe(false);
|
||||
});
|
||||
|
||||
it("handles null activeConversationId (no active tab)", () => {
|
||||
const lastSeen = new Map([["tab-1", 3]]);
|
||||
expect(hasUnreadMessages("tab-1", 10, null, lastSeen)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* HighlightedText Component Tests
|
||||
*
|
||||
* Tests the text-splitting logic used by the HighlightedText component,
|
||||
* which highlights search query matches within a string.
|
||||
*
|
||||
* What this component does:
|
||||
* - Splits text into an array of {text, isMatch} parts
|
||||
* - Matches are case-insensitive
|
||||
* - Special regex characters in the query are escaped
|
||||
* - Non-matching text is preserved verbatim around matches
|
||||
*
|
||||
* Manual testing checklist:
|
||||
* - [ ] Matching text is highlighted (yellow background) in the terminal
|
||||
* - [ ] Highlighting is case-insensitive
|
||||
* - [ ] Multiple matches on the same line all get highlighted
|
||||
* - [ ] Non-matching text renders normally alongside matches
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
interface TextPart {
|
||||
text: string;
|
||||
isMatch: boolean;
|
||||
}
|
||||
|
||||
function getHighlightedParts(text: string, query: string): TextPart[] {
|
||||
if (!query) {
|
||||
return [{ text, isMatch: false }];
|
||||
}
|
||||
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(`(${escapedQuery})`, "gi");
|
||||
const parts: TextPart[] = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({
|
||||
text: text.slice(lastIndex, match.index),
|
||||
isMatch: false,
|
||||
});
|
||||
}
|
||||
|
||||
parts.push({
|
||||
text: match[1],
|
||||
isMatch: true,
|
||||
});
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({
|
||||
text: text.slice(lastIndex),
|
||||
isMatch: false,
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
describe("getHighlightedParts", () => {
|
||||
describe("empty query", () => {
|
||||
it("returns the whole text as a single non-match when query is empty string", () => {
|
||||
const result = getHighlightedParts("hello world", "");
|
||||
expect(result).toEqual([{ text: "hello world", isMatch: false }]);
|
||||
});
|
||||
|
||||
it("returns an empty non-match part when both text and query are empty", () => {
|
||||
const result = getHighlightedParts("", "");
|
||||
expect(result).toEqual([{ text: "", isMatch: false }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no match", () => {
|
||||
it("returns the whole text as a single non-match when query is not found", () => {
|
||||
const result = getHighlightedParts("hello world", "xyz");
|
||||
expect(result).toEqual([{ text: "hello world", isMatch: false }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("single match", () => {
|
||||
it("returns three parts for a match in the middle", () => {
|
||||
const result = getHighlightedParts("hello world foo", "world");
|
||||
expect(result).toEqual([
|
||||
{ text: "hello ", isMatch: false },
|
||||
{ text: "world", isMatch: true },
|
||||
{ text: " foo", isMatch: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns two parts for a match at the start", () => {
|
||||
const result = getHighlightedParts("hello world", "hello");
|
||||
expect(result).toEqual([
|
||||
{ text: "hello", isMatch: true },
|
||||
{ text: " world", isMatch: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns two parts for a match at the end", () => {
|
||||
const result = getHighlightedParts("hello world", "world");
|
||||
expect(result).toEqual([
|
||||
{ text: "hello ", isMatch: false },
|
||||
{ text: "world", isMatch: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns a single match part when the entire text matches", () => {
|
||||
const result = getHighlightedParts("hello", "hello");
|
||||
expect(result).toEqual([{ text: "hello", isMatch: true }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple matches", () => {
|
||||
it("returns interleaved match and non-match parts for multiple occurrences", () => {
|
||||
const result = getHighlightedParts("foo bar foo", "foo");
|
||||
expect(result).toEqual([
|
||||
{ text: "foo", isMatch: true },
|
||||
{ text: " bar ", isMatch: false },
|
||||
{ text: "foo", isMatch: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles adjacent matches correctly", () => {
|
||||
const result = getHighlightedParts("aaa", "a");
|
||||
expect(result).toEqual([
|
||||
{ text: "a", isMatch: true },
|
||||
{ text: "a", isMatch: true },
|
||||
{ text: "a", isMatch: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("case-insensitive matching", () => {
|
||||
it("matches uppercase query against lowercase text", () => {
|
||||
const result = getHighlightedParts("hello world", "WORLD");
|
||||
expect(result).toEqual([
|
||||
{ text: "hello ", isMatch: false },
|
||||
{ text: "world", isMatch: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("matches lowercase query against uppercase text", () => {
|
||||
const result = getHighlightedParts("HELLO WORLD", "hello");
|
||||
expect(result).toEqual([
|
||||
{ text: "HELLO", isMatch: true },
|
||||
{ text: " WORLD", isMatch: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves the original casing of the matched text", () => {
|
||||
const result = getHighlightedParts("Hello World", "hello");
|
||||
const matchPart = result.find((p) => p.isMatch);
|
||||
expect(matchPart?.text).toBe("Hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("special regex character escaping", () => {
|
||||
it("treats a dot in the query as a literal dot, not a wildcard", () => {
|
||||
const result = getHighlightedParts("v1.2.3 v123", "1.2");
|
||||
const matchParts = result.filter((p) => p.isMatch);
|
||||
expect(matchParts).toHaveLength(1);
|
||||
expect(matchParts[0].text).toBe("1.2");
|
||||
});
|
||||
|
||||
it("handles a query with parentheses", () => {
|
||||
const result = getHighlightedParts("fn(args)", "(args)");
|
||||
expect(result).toEqual([
|
||||
{ text: "fn", isMatch: false },
|
||||
{ text: "(args)", isMatch: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles a query with a plus sign", () => {
|
||||
const result = getHighlightedParts("a+b=c", "+");
|
||||
expect(result).toEqual([
|
||||
{ text: "a", isMatch: false },
|
||||
{ text: "+", isMatch: true },
|
||||
{ text: "b=c", isMatch: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles a query with a question mark", () => {
|
||||
const result = getHighlightedParts("is it true?", "?");
|
||||
expect(result).toEqual([
|
||||
{ text: "is it true", isMatch: false },
|
||||
{ text: "?", isMatch: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* StatusBar Component Tests
|
||||
*
|
||||
* Tests the connection status colour and text helpers used by the
|
||||
* StatusBar component to display the current Claude connection state.
|
||||
*
|
||||
* What this component does:
|
||||
* - Shows a coloured indicator dot for the connection state
|
||||
* - Shows a text label for the connection state
|
||||
* - Provides connect/disconnect buttons
|
||||
* - Contains the working directory input and browse button
|
||||
* - Houses all toolbar action buttons (settings, stats, panels, etc.)
|
||||
*
|
||||
* Manual testing checklist:
|
||||
* - [ ] Green dot and "Connected" label when Claude is running
|
||||
* - [ ] Animated yellow dot and "Connecting..." label whilst connecting
|
||||
* - [ ] Red dot and "Error" label on connection error
|
||||
* - [ ] Grey dot and "Disconnected" label when not connected
|
||||
* - [ ] Directory input is hidden when connected, visible when disconnected
|
||||
* - [ ] Connect button transitions to Disconnect button on connection
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
type ConnectionStatus = "connected" | "connecting" | "disconnected" | "error";
|
||||
|
||||
function getStatusColor(connectionStatus: ConnectionStatus): string {
|
||||
switch (connectionStatus) {
|
||||
case "connected":
|
||||
return "bg-green-500";
|
||||
case "connecting":
|
||||
return "bg-yellow-500 animate-pulse";
|
||||
case "error":
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusText(connectionStatus: ConnectionStatus): string {
|
||||
switch (connectionStatus) {
|
||||
case "connected":
|
||||
return "Connected";
|
||||
case "connecting":
|
||||
return "Connecting...";
|
||||
case "error":
|
||||
return "Error";
|
||||
default:
|
||||
return "Disconnected";
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
describe("getStatusColor", () => {
|
||||
it("returns green for connected status", () => {
|
||||
expect(getStatusColor("connected")).toBe("bg-green-500");
|
||||
});
|
||||
|
||||
it("returns animated yellow for connecting status", () => {
|
||||
expect(getStatusColor("connecting")).toBe("bg-yellow-500 animate-pulse");
|
||||
});
|
||||
|
||||
it("returns red for error status", () => {
|
||||
expect(getStatusColor("error")).toBe("bg-red-500");
|
||||
});
|
||||
|
||||
it("returns grey for disconnected status", () => {
|
||||
expect(getStatusColor("disconnected")).toBe("bg-gray-500");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatusText", () => {
|
||||
it("returns 'Connected' for connected status", () => {
|
||||
expect(getStatusText("connected")).toBe("Connected");
|
||||
});
|
||||
|
||||
it("returns 'Connecting...' for connecting status", () => {
|
||||
expect(getStatusText("connecting")).toBe("Connecting...");
|
||||
});
|
||||
|
||||
it("returns 'Error' for error status", () => {
|
||||
expect(getStatusText("error")).toBe("Error");
|
||||
});
|
||||
|
||||
it("returns 'Disconnected' for disconnected status", () => {
|
||||
expect(getStatusText("disconnected")).toBe("Disconnected");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { characterState, characterInfo } from "./character";
|
||||
@@ -26,7 +25,17 @@ describe("characterState store", () => {
|
||||
});
|
||||
|
||||
it("can set any valid state", () => {
|
||||
const states = ["idle", "thinking", "typing", "coding", "searching", "mcp", "permission", "success", "error"] as const;
|
||||
const states = [
|
||||
"idle",
|
||||
"thinking",
|
||||
"typing",
|
||||
"coding",
|
||||
"searching",
|
||||
"mcp",
|
||||
"permission",
|
||||
"success",
|
||||
"error",
|
||||
] as const;
|
||||
for (const state of states) {
|
||||
characterState.setState(state);
|
||||
expect(get(characterState)).toBe(state);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
formatCost,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/init-declarations -- Variables reassigned in beforeEach */
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { searchState, isSearchActive, searchQuery } from "./search";
|
||||
|
||||
Reference in New Issue
Block a user