generated from nhcarrigan/template
test: fix clipboard coverage (real imports) and add cd/new success paths
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { claudeStore } from "$lib/stores/claude";
|
||||
@@ -714,5 +714,153 @@ describe("slashCommands", () => {
|
||||
expect(characterState.setTemporaryState).toHaveBeenCalledWith("error", 3000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/cd success path", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("changes directory and shows success message", async () => {
|
||||
getMock
|
||||
.mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId)
|
||||
.mockReturnValueOnce("/current") // get(claudeStore.currentWorkingDirectory)
|
||||
.mockReturnValueOnce(null); // get(conversationsStore.activeConversation)
|
||||
vi.mocked(claudeStore.getConversationHistory).mockReturnValue("");
|
||||
invokeMock
|
||||
.mockResolvedValueOnce("/new/path") // validate_directory
|
||||
.mockResolvedValueOnce(undefined) // stop_claude
|
||||
.mockResolvedValueOnce(undefined); // start_claude
|
||||
|
||||
const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!;
|
||||
const promise = cdCmd.execute("/new/path");
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith(
|
||||
"system",
|
||||
"Changed directory to: /new/path"
|
||||
);
|
||||
expect(characterState.setState).toHaveBeenCalledWith("idle");
|
||||
});
|
||||
|
||||
it("sends context restoration message when conversation history exists", async () => {
|
||||
getMock
|
||||
.mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId)
|
||||
.mockReturnValueOnce("/current") // get(claudeStore.currentWorkingDirectory)
|
||||
.mockReturnValueOnce(null); // get(conversationsStore.activeConversation)
|
||||
vi.mocked(claudeStore.getConversationHistory).mockReturnValue(
|
||||
"previous conversation history"
|
||||
);
|
||||
invokeMock
|
||||
.mockResolvedValueOnce("/new/path") // validate_directory
|
||||
.mockResolvedValueOnce(undefined) // stop_claude
|
||||
.mockResolvedValueOnce(undefined) // start_claude
|
||||
.mockResolvedValueOnce(undefined); // send_prompt
|
||||
|
||||
const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!;
|
||||
const promise = cdCmd.execute("/new/path");
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
expect(invokeMock).toHaveBeenCalledWith(
|
||||
"send_prompt",
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("previous conversation history"),
|
||||
})
|
||||
);
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith(
|
||||
"system",
|
||||
"Changed directory to: /new/path"
|
||||
);
|
||||
});
|
||||
|
||||
it("calls updateDiscordRpc when activeConversation is available", async () => {
|
||||
const activeConv = {
|
||||
name: "Test Conversation",
|
||||
model: "claude-sonnet",
|
||||
startedAt: new Date("2026-03-03T12:00:00Z"),
|
||||
grantedTools: new Set<string>(),
|
||||
};
|
||||
getMock
|
||||
.mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId)
|
||||
.mockReturnValueOnce("/current") // get(claudeStore.currentWorkingDirectory)
|
||||
.mockReturnValueOnce(activeConv); // get(conversationsStore.activeConversation)
|
||||
vi.mocked(claudeStore.getConversationHistory).mockReturnValue("");
|
||||
invokeMock
|
||||
.mockResolvedValueOnce("/new/path")
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockResolvedValueOnce(undefined);
|
||||
const { updateDiscordRpc } = await import("$lib/tauri");
|
||||
|
||||
const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!;
|
||||
const promise = cdCmd.execute("/new/path");
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
expect(updateDiscordRpc).toHaveBeenCalledWith(
|
||||
"Test Conversation",
|
||||
expect.any(String),
|
||||
expect.any(Date)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/new success path", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("starts a new conversation and shows success message", async () => {
|
||||
getMock
|
||||
.mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId)
|
||||
.mockReturnValueOnce(null); // get(conversationsStore.activeConversation)
|
||||
invokeMock
|
||||
.mockResolvedValueOnce("/working/dir") // get_working_directory
|
||||
.mockResolvedValueOnce(undefined) // interrupt_claude
|
||||
.mockResolvedValueOnce(undefined); // start_claude
|
||||
|
||||
const newCmd = slashCommands.find((cmd) => cmd.name === "new")!;
|
||||
const promise = newCmd.execute("");
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("system", "New conversation started!");
|
||||
expect(characterState.setState).toHaveBeenCalledWith("idle");
|
||||
});
|
||||
|
||||
it("calls updateDiscordRpc when activeConversation is available", async () => {
|
||||
const activeConv = {
|
||||
name: "My Conv",
|
||||
model: "claude-sonnet",
|
||||
startedAt: new Date("2026-03-03T12:00:00Z"),
|
||||
grantedTools: new Set<string>(["tool1"]),
|
||||
};
|
||||
getMock.mockReturnValueOnce("conv-123").mockReturnValueOnce(activeConv);
|
||||
invokeMock
|
||||
.mockResolvedValueOnce("/working/dir")
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockResolvedValueOnce(undefined);
|
||||
const { updateDiscordRpc } = await import("$lib/tauri");
|
||||
|
||||
const newCmd = slashCommands.find((cmd) => cmd.name === "new")!;
|
||||
const promise = newCmd.execute("");
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
expect(updateDiscordRpc).toHaveBeenCalledWith(
|
||||
"My Conv",
|
||||
expect.any(String),
|
||||
expect.any(Date)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+379
-114
@@ -1,259 +1,212 @@
|
||||
/**
|
||||
* Clipboard Store Tests
|
||||
*
|
||||
* Tests the pure helper functions from the clipboard store:
|
||||
* Tests the pure helper functions and store actions from the clipboard store:
|
||||
* - detectLanguage: identifies programming language from code content
|
||||
* - formatTimestamp: converts an ISO timestamp to a relative time string
|
||||
*
|
||||
* What this store does:
|
||||
* - Maintains a history of clipboard entries (code snippets)
|
||||
* - Auto-detects the language of captured content
|
||||
* - Supports filtering by language and search query
|
||||
* - Pinned entries persist across clear operations
|
||||
*
|
||||
* Manual testing checklist:
|
||||
* - [ ] Clipboard entries appear when code is copied during a session
|
||||
* - [ ] Language is detected and labelled correctly
|
||||
* - [ ] Pinned entries survive "Clear history"
|
||||
* - [ ] Language filter dropdown shows only languages present in history
|
||||
* - [ ] Search query filters by content, language, and source
|
||||
* - Store actions: loadEntries, captureClipboard, deleteEntry, togglePin, etc.
|
||||
* - Derived stores: filteredEntries (language + search filtering), languages
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// Mirror: detectLanguage from clipboard.ts
|
||||
function detectLanguage(content: string): string | null {
|
||||
const patterns: [RegExp, string][] = [
|
||||
[/^(import|export|const|let|var|function|class|interface|type)\s/m, "typescript"],
|
||||
[/^(def|class|import|from|if __name__|async def)\s/m, "python"],
|
||||
[/^(fn|let|mut|impl|struct|enum|use|mod|pub)\s/m, "rust"],
|
||||
[/^(package|import|func|type|var|const)\s/m, "go"],
|
||||
[/<\?php/m, "php"],
|
||||
[/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\s/im, "sql"],
|
||||
[/^<!DOCTYPE|^<html|^<div|^<span/im, "html"],
|
||||
[/^\s*\{[\s\S]*"[\w-]+":/m, "json"],
|
||||
[/^---\s*\n/m, "yaml"],
|
||||
[/^\s*#\s*(include|define|ifdef)/m, "c"],
|
||||
[/^(public|private|protected)\s+(class|interface|static)/m, "java"],
|
||||
[/^\$[\w_]+\s*=/m, "bash"],
|
||||
];
|
||||
|
||||
for (const [pattern, lang] of patterns) {
|
||||
if (pattern.test(content)) {
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mirror: formatTimestamp from clipboard.ts
|
||||
function formatTimestamp(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
// ---
|
||||
import { get } from "svelte/store";
|
||||
import { clipboardStore } from "$lib/stores/clipboard";
|
||||
import { setMockInvokeResult } from "../../../vitest.setup";
|
||||
|
||||
describe("detectLanguage", () => {
|
||||
describe("TypeScript detection", () => {
|
||||
it("detects import statements", () => {
|
||||
expect(detectLanguage("import React from 'react';")).toBe("typescript");
|
||||
expect(clipboardStore.detectLanguage("import React from 'react';")).toBe("typescript");
|
||||
});
|
||||
|
||||
it("detects export statements", () => {
|
||||
expect(detectLanguage("export function foo() {}")).toBe("typescript");
|
||||
expect(clipboardStore.detectLanguage("export function foo() {}")).toBe("typescript");
|
||||
});
|
||||
|
||||
it("detects const declarations", () => {
|
||||
expect(detectLanguage("const x = 1;")).toBe("typescript");
|
||||
expect(clipboardStore.detectLanguage("const x = 1;")).toBe("typescript");
|
||||
});
|
||||
|
||||
it("detects interface declarations", () => {
|
||||
expect(detectLanguage("interface Foo {\n bar: string;\n}")).toBe("typescript");
|
||||
expect(clipboardStore.detectLanguage("interface Foo {\n bar: string;\n}")).toBe(
|
||||
"typescript"
|
||||
);
|
||||
});
|
||||
|
||||
it("detects type aliases", () => {
|
||||
expect(detectLanguage("type MyType = string | number;")).toBe("typescript");
|
||||
expect(clipboardStore.detectLanguage("type MyType = string | number;")).toBe("typescript");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Python detection", () => {
|
||||
it("detects def statements", () => {
|
||||
expect(detectLanguage("def foo():\n pass")).toBe("python");
|
||||
expect(clipboardStore.detectLanguage("def foo():\n pass")).toBe("python");
|
||||
});
|
||||
|
||||
it("detects async def statements", () => {
|
||||
expect(detectLanguage("async def bar():\n pass")).toBe("python");
|
||||
expect(clipboardStore.detectLanguage("async def bar():\n pass")).toBe("python");
|
||||
});
|
||||
|
||||
it("detects from imports", () => {
|
||||
expect(detectLanguage("from os import path")).toBe("python");
|
||||
expect(clipboardStore.detectLanguage("from os import path")).toBe("python");
|
||||
});
|
||||
|
||||
it("detects the __name__ guard", () => {
|
||||
expect(detectLanguage("if __name__ == '__main__':\n main()")).toBe("python");
|
||||
expect(clipboardStore.detectLanguage("if __name__ == '__main__':\n main()")).toBe("python");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rust detection", () => {
|
||||
it("detects fn declarations", () => {
|
||||
expect(detectLanguage('fn main() {\n println!("hello");\n}')).toBe("rust");
|
||||
expect(clipboardStore.detectLanguage('fn main() {\n println!("hello");\n}')).toBe("rust");
|
||||
});
|
||||
|
||||
it("detects impl blocks", () => {
|
||||
expect(detectLanguage("impl Foo {\n pub fn new() -> Self {}\n}")).toBe("rust");
|
||||
expect(clipboardStore.detectLanguage("impl Foo {\n pub fn new() -> Self {}\n}")).toBe(
|
||||
"rust"
|
||||
);
|
||||
});
|
||||
|
||||
it("detects struct declarations", () => {
|
||||
expect(detectLanguage("struct Point {\n x: f64,\n y: f64,\n}")).toBe("rust");
|
||||
expect(clipboardStore.detectLanguage("struct Point {\n x: f64,\n y: f64,\n}")).toBe("rust");
|
||||
});
|
||||
|
||||
it("detects enum declarations", () => {
|
||||
expect(detectLanguage("enum Direction {\n North,\n South,\n}")).toBe("rust");
|
||||
expect(clipboardStore.detectLanguage("enum Direction {\n North,\n South,\n}")).toBe("rust");
|
||||
});
|
||||
|
||||
it("detects mod declarations", () => {
|
||||
expect(detectLanguage("mod utils;")).toBe("rust");
|
||||
expect(clipboardStore.detectLanguage("mod utils;")).toBe("rust");
|
||||
});
|
||||
|
||||
it("detects pub visibility", () => {
|
||||
expect(detectLanguage("pub fn exported() {}")).toBe("rust");
|
||||
expect(clipboardStore.detectLanguage("pub fn exported() {}")).toBe("rust");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Go detection", () => {
|
||||
it("detects package declarations", () => {
|
||||
expect(detectLanguage("package main")).toBe("go");
|
||||
expect(clipboardStore.detectLanguage("package main")).toBe("go");
|
||||
});
|
||||
|
||||
it("detects func declarations", () => {
|
||||
expect(detectLanguage("func main() {}")).toBe("go");
|
||||
expect(clipboardStore.detectLanguage("func main() {}")).toBe("go");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PHP detection", () => {
|
||||
it("detects the PHP open tag", () => {
|
||||
expect(detectLanguage("<?php\necho 'hello';")).toBe("php");
|
||||
expect(clipboardStore.detectLanguage("<?php\necho 'hello';")).toBe("php");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SQL detection", () => {
|
||||
it("detects SELECT statements", () => {
|
||||
expect(detectLanguage("SELECT * FROM users WHERE id = 1")).toBe("sql");
|
||||
expect(clipboardStore.detectLanguage("SELECT * FROM users WHERE id = 1")).toBe("sql");
|
||||
});
|
||||
|
||||
it("detects INSERT statements", () => {
|
||||
expect(detectLanguage("INSERT INTO users (name) VALUES ('Alice')")).toBe("sql");
|
||||
expect(clipboardStore.detectLanguage("INSERT INTO users (name) VALUES ('Alice')")).toBe(
|
||||
"sql"
|
||||
);
|
||||
});
|
||||
|
||||
it("detects CREATE statements", () => {
|
||||
expect(detectLanguage("CREATE TABLE users (id INT PRIMARY KEY)")).toBe("sql");
|
||||
expect(clipboardStore.detectLanguage("CREATE TABLE users (id INT PRIMARY KEY)")).toBe("sql");
|
||||
});
|
||||
|
||||
it("detects SQL case-insensitively", () => {
|
||||
expect(detectLanguage("select * from users")).toBe("sql");
|
||||
expect(clipboardStore.detectLanguage("select * from users")).toBe("sql");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML detection", () => {
|
||||
it("detects DOCTYPE declarations", () => {
|
||||
expect(detectLanguage("<!DOCTYPE html>")).toBe("html");
|
||||
expect(clipboardStore.detectLanguage("<!DOCTYPE html>")).toBe("html");
|
||||
});
|
||||
|
||||
it("detects html tags", () => {
|
||||
expect(detectLanguage("<html><body></body></html>")).toBe("html");
|
||||
expect(clipboardStore.detectLanguage("<html><body></body></html>")).toBe("html");
|
||||
});
|
||||
|
||||
it("detects div tags", () => {
|
||||
expect(detectLanguage("<div class='foo'>bar</div>")).toBe("html");
|
||||
expect(clipboardStore.detectLanguage("<div class='foo'>bar</div>")).toBe("html");
|
||||
});
|
||||
|
||||
it("detects span tags", () => {
|
||||
expect(detectLanguage("<span>text</span>")).toBe("html");
|
||||
expect(clipboardStore.detectLanguage("<span>text</span>")).toBe("html");
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON detection", () => {
|
||||
it("detects JSON object syntax", () => {
|
||||
expect(detectLanguage('{"name": "test", "value": 42}')).toBe("json");
|
||||
expect(clipboardStore.detectLanguage('{"name": "test", "value": 42}')).toBe("json");
|
||||
});
|
||||
|
||||
it("detects JSON with hyphenated keys", () => {
|
||||
expect(detectLanguage('{"my-key": "value"}')).toBe("json");
|
||||
expect(clipboardStore.detectLanguage('{"my-key": "value"}')).toBe("json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("YAML detection", () => {
|
||||
it("detects YAML document separator", () => {
|
||||
expect(detectLanguage("---\nkey: value\nother: 123")).toBe("yaml");
|
||||
expect(clipboardStore.detectLanguage("---\nkey: value\nother: 123")).toBe("yaml");
|
||||
});
|
||||
});
|
||||
|
||||
describe("C detection", () => {
|
||||
it("detects #include directives", () => {
|
||||
expect(detectLanguage("#include <stdio.h>\nint main() {}")).toBe("c");
|
||||
expect(clipboardStore.detectLanguage("#include <stdio.h>\nint main() {}")).toBe("c");
|
||||
});
|
||||
|
||||
it("detects #define directives", () => {
|
||||
expect(detectLanguage("#define MAX 100")).toBe("c");
|
||||
expect(clipboardStore.detectLanguage("#define MAX 100")).toBe("c");
|
||||
});
|
||||
|
||||
it("detects #ifdef directives", () => {
|
||||
expect(detectLanguage("#ifdef DEBUG\n// debug code\n#endif")).toBe("c");
|
||||
expect(clipboardStore.detectLanguage("#ifdef DEBUG\n// debug code\n#endif")).toBe("c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Java detection", () => {
|
||||
it("detects public class declarations", () => {
|
||||
expect(detectLanguage("public class Foo {\n // ...\n}")).toBe("java");
|
||||
expect(clipboardStore.detectLanguage("public class Foo {\n // ...\n}")).toBe("java");
|
||||
});
|
||||
|
||||
it("detects private static methods", () => {
|
||||
expect(detectLanguage("private static void helper() {}")).toBe("java");
|
||||
expect(clipboardStore.detectLanguage("private static void helper() {}")).toBe("java");
|
||||
});
|
||||
|
||||
it("detects protected interface declarations", () => {
|
||||
expect(detectLanguage("protected interface Bar {}")).toBe("java");
|
||||
expect(clipboardStore.detectLanguage("protected interface Bar {}")).toBe("java");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bash detection", () => {
|
||||
it("detects shell variable assignments", () => {
|
||||
expect(detectLanguage("$HOME=/usr/local")).toBe("bash");
|
||||
expect(clipboardStore.detectLanguage("$HOME=/usr/local")).toBe("bash");
|
||||
});
|
||||
|
||||
it("detects variable assignments with underscores", () => {
|
||||
expect(detectLanguage("$MY_VAR=some_value")).toBe("bash");
|
||||
expect(clipboardStore.detectLanguage("$MY_VAR=some_value")).toBe("bash");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unknown content", () => {
|
||||
it("returns null for plain text", () => {
|
||||
expect(detectLanguage("Hello, world!")).toBeNull();
|
||||
expect(clipboardStore.detectLanguage("Hello, world!")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for empty string", () => {
|
||||
expect(detectLanguage("")).toBeNull();
|
||||
expect(clipboardStore.detectLanguage("")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for mathematical expressions", () => {
|
||||
expect(detectLanguage("1 + 1 = 2")).toBeNull();
|
||||
expect(clipboardStore.detectLanguage("1 + 1 = 2")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a markdown heading", () => {
|
||||
expect(detectLanguage("# My Heading\nSome text")).toBeNull();
|
||||
expect(clipboardStore.detectLanguage("# My Heading\nSome text")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -273,75 +226,75 @@ describe("formatTimestamp", () => {
|
||||
describe("'Just now' threshold (< 1 minute)", () => {
|
||||
it("returns 'Just now' for a timestamp 30 seconds ago", () => {
|
||||
const ts = new Date("2026-03-03T11:59:30.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("Just now");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("Just now");
|
||||
});
|
||||
|
||||
it("returns 'Just now' for the current moment", () => {
|
||||
const ts = NOW.toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("Just now");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("Just now");
|
||||
});
|
||||
|
||||
it("returns 'Just now' for a timestamp 59 seconds ago", () => {
|
||||
const ts = new Date("2026-03-03T11:59:01.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("Just now");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("Just now");
|
||||
});
|
||||
});
|
||||
|
||||
describe("'Xm ago' threshold (1–59 minutes)", () => {
|
||||
it("returns '1m ago' at exactly 1 minute", () => {
|
||||
const ts = new Date("2026-03-03T11:59:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("1m ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("1m ago");
|
||||
});
|
||||
|
||||
it("returns '5m ago' for a timestamp 5 minutes ago", () => {
|
||||
const ts = new Date("2026-03-03T11:55:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("5m ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("5m ago");
|
||||
});
|
||||
|
||||
it("returns '59m ago' just before the 1-hour threshold", () => {
|
||||
const ts = new Date("2026-03-03T11:01:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("59m ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("59m ago");
|
||||
});
|
||||
});
|
||||
|
||||
describe("'Xh ago' threshold (1–23 hours)", () => {
|
||||
it("returns '1h ago' at exactly 1 hour", () => {
|
||||
const ts = new Date("2026-03-03T11:00:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("1h ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("1h ago");
|
||||
});
|
||||
|
||||
it("returns '2h ago' for a timestamp 2 hours ago", () => {
|
||||
const ts = new Date("2026-03-03T10:00:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("2h ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("2h ago");
|
||||
});
|
||||
|
||||
it("returns '23h ago' just before the 1-day threshold", () => {
|
||||
const ts = new Date("2026-03-02T13:00:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("23h ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("23h ago");
|
||||
});
|
||||
});
|
||||
|
||||
describe("'Xd ago' threshold (1–6 days)", () => {
|
||||
it("returns '1d ago' at exactly 1 day", () => {
|
||||
const ts = new Date("2026-03-02T12:00:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("1d ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("1d ago");
|
||||
});
|
||||
|
||||
it("returns '3d ago' for a timestamp 3 days ago", () => {
|
||||
const ts = new Date("2026-02-28T12:00:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("3d ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("3d ago");
|
||||
});
|
||||
|
||||
it("returns '6d ago' just before the 7-day threshold", () => {
|
||||
const ts = new Date("2026-02-25T12:00:00.000Z").toISOString();
|
||||
expect(formatTimestamp(ts)).toBe("6d ago");
|
||||
expect(clipboardStore.formatTimestamp(ts)).toBe("6d ago");
|
||||
});
|
||||
});
|
||||
|
||||
describe("locale date string (7+ days ago)", () => {
|
||||
it("returns a locale date string for a 2-week-old timestamp", () => {
|
||||
const ts = new Date("2026-02-17T12:00:00.000Z").toISOString();
|
||||
const result = formatTimestamp(ts);
|
||||
const result = clipboardStore.formatTimestamp(ts);
|
||||
// Should not be a relative time string
|
||||
expect(result).not.toContain("m ago");
|
||||
expect(result).not.toContain("h ago");
|
||||
@@ -351,8 +304,320 @@ describe("formatTimestamp", () => {
|
||||
|
||||
it("returns a locale date string for a 1-month-old timestamp", () => {
|
||||
const ts = new Date("2026-02-03T12:00:00.000Z").toISOString();
|
||||
const result = formatTimestamp(ts);
|
||||
const result = clipboardStore.formatTimestamp(ts);
|
||||
expect(result).not.toContain("ago");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("clipboardStore - derived stores", () => {
|
||||
const makeEntry = (overrides: Record<string, unknown> = {}) => ({
|
||||
id: "entry-1",
|
||||
content: "const x = 1;",
|
||||
language: "typescript",
|
||||
source: "test.ts",
|
||||
timestamp: "2026-03-03T12:00:00.000Z",
|
||||
is_pinned: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clipboardStore.entries.set([]);
|
||||
clipboardStore.searchQuery.set("");
|
||||
clipboardStore.languageFilter.set(null);
|
||||
});
|
||||
|
||||
describe("filteredEntries - language filter", () => {
|
||||
it("returns all entries when no language filter is set", () => {
|
||||
clipboardStore.entries.set([
|
||||
makeEntry({ id: "1", language: "typescript" }),
|
||||
makeEntry({ id: "2", language: "python" }),
|
||||
]);
|
||||
const filtered = get(clipboardStore.filteredEntries);
|
||||
expect(filtered).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("filters entries by language", () => {
|
||||
clipboardStore.entries.set([
|
||||
makeEntry({ id: "1", language: "typescript" }),
|
||||
makeEntry({ id: "2", language: "python" }),
|
||||
makeEntry({ id: "3", language: "typescript" }),
|
||||
]);
|
||||
clipboardStore.languageFilter.set("typescript");
|
||||
const filtered = get(clipboardStore.filteredEntries);
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.every((e) => e.language === "typescript")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns empty array when filter matches nothing", () => {
|
||||
clipboardStore.entries.set([makeEntry({ language: "typescript" })]);
|
||||
clipboardStore.languageFilter.set("rust");
|
||||
const filtered = get(clipboardStore.filteredEntries);
|
||||
expect(filtered).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filteredEntries - search query", () => {
|
||||
it("filters by content (case-insensitive)", () => {
|
||||
clipboardStore.entries.set([
|
||||
makeEntry({ id: "1", content: "const HELLO = 1;" }),
|
||||
makeEntry({ id: "2", content: "let world = 2;" }),
|
||||
]);
|
||||
clipboardStore.searchQuery.set("hello");
|
||||
const filtered = get(clipboardStore.filteredEntries);
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].id).toBe("1");
|
||||
});
|
||||
|
||||
it("filters by language field", () => {
|
||||
clipboardStore.entries.set([
|
||||
makeEntry({ id: "1", language: "typescript", content: "unrelated" }),
|
||||
makeEntry({ id: "2", language: "python", content: "also unrelated" }),
|
||||
]);
|
||||
clipboardStore.searchQuery.set("python");
|
||||
const filtered = get(clipboardStore.filteredEntries);
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].id).toBe("2");
|
||||
});
|
||||
|
||||
it("filters by source field", () => {
|
||||
clipboardStore.entries.set([
|
||||
makeEntry({ id: "1", source: "main.rs", content: "fn main() {}" }),
|
||||
makeEntry({ id: "2", source: "app.ts", content: "const x = 1;" }),
|
||||
]);
|
||||
clipboardStore.searchQuery.set("main.rs");
|
||||
const filtered = get(clipboardStore.filteredEntries);
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].id).toBe("1");
|
||||
});
|
||||
|
||||
it("returns all entries when search query is empty", () => {
|
||||
clipboardStore.entries.set([makeEntry({ id: "1" }), makeEntry({ id: "2" })]);
|
||||
clipboardStore.searchQuery.set("");
|
||||
const filtered = get(clipboardStore.filteredEntries);
|
||||
expect(filtered).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("languages derived store", () => {
|
||||
it("returns empty array when no entries", () => {
|
||||
clipboardStore.entries.set([]);
|
||||
expect(get(clipboardStore.languages)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns unique sorted languages", () => {
|
||||
clipboardStore.entries.set([
|
||||
makeEntry({ language: "typescript" }),
|
||||
makeEntry({ language: "python" }),
|
||||
makeEntry({ language: "typescript" }),
|
||||
makeEntry({ language: "rust" }),
|
||||
]);
|
||||
const langs = get(clipboardStore.languages);
|
||||
expect(langs).toEqual(["python", "rust", "typescript"]);
|
||||
});
|
||||
|
||||
it("excludes entries with null language", () => {
|
||||
clipboardStore.entries.set([
|
||||
makeEntry({ language: "typescript" }),
|
||||
makeEntry({ language: null }),
|
||||
]);
|
||||
const langs = get(clipboardStore.languages);
|
||||
expect(langs).toEqual(["typescript"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("clipboardStore - setSearchQuery and setLanguageFilter", () => {
|
||||
it("setSearchQuery updates the searchQuery store", () => {
|
||||
clipboardStore.setSearchQuery("hello world");
|
||||
expect(get(clipboardStore.searchQuery)).toBe("hello world");
|
||||
clipboardStore.setSearchQuery("");
|
||||
});
|
||||
|
||||
it("setLanguageFilter updates the languageFilter store", () => {
|
||||
clipboardStore.setLanguageFilter("python");
|
||||
expect(get(clipboardStore.languageFilter)).toBe("python");
|
||||
clipboardStore.setLanguageFilter(null);
|
||||
});
|
||||
|
||||
it("setLanguageFilter can be reset to null", () => {
|
||||
clipboardStore.setLanguageFilter("rust");
|
||||
clipboardStore.setLanguageFilter(null);
|
||||
expect(get(clipboardStore.languageFilter)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clipboardStore - store actions", () => {
|
||||
const mockEntry = {
|
||||
id: "entry-1",
|
||||
content: "const x = 1;",
|
||||
language: "typescript",
|
||||
source: "test.ts",
|
||||
timestamp: "2026-03-03T12:00:00.000Z",
|
||||
is_pinned: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clipboardStore.entries.set([]);
|
||||
});
|
||||
|
||||
describe("loadEntries", () => {
|
||||
it("loads entries from backend and updates the store", async () => {
|
||||
setMockInvokeResult("list_clipboard_entries", [mockEntry]);
|
||||
await clipboardStore.loadEntries();
|
||||
expect(get(clipboardStore.entries)).toEqual([mockEntry]);
|
||||
});
|
||||
|
||||
it("handles errors gracefully without crashing", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("list_clipboard_entries", new Error("Backend unavailable"));
|
||||
await clipboardStore.loadEntries();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to load clipboard entries:",
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("sets isLoading to false after completion", async () => {
|
||||
setMockInvokeResult("list_clipboard_entries", []);
|
||||
await clipboardStore.loadEntries();
|
||||
expect(get(clipboardStore.isLoading)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("captureClipboard", () => {
|
||||
it("captures clipboard entry with provided language", async () => {
|
||||
setMockInvokeResult("capture_clipboard", mockEntry);
|
||||
setMockInvokeResult("list_clipboard_entries", [mockEntry]);
|
||||
const result = await clipboardStore.captureClipboard("const x = 1;", "typescript", "test.ts");
|
||||
expect(result).toEqual(mockEntry);
|
||||
});
|
||||
|
||||
it("auto-detects language when none provided", async () => {
|
||||
setMockInvokeResult("capture_clipboard", mockEntry);
|
||||
setMockInvokeResult("list_clipboard_entries", [mockEntry]);
|
||||
const result = await clipboardStore.captureClipboard("const x = 1;");
|
||||
expect(result).toEqual(mockEntry);
|
||||
});
|
||||
|
||||
it("returns null on error", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("capture_clipboard", new Error("Failed"));
|
||||
const result = await clipboardStore.captureClipboard("const x = 1;");
|
||||
expect(result).toBeNull();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteEntry", () => {
|
||||
it("removes entry from store on success", async () => {
|
||||
clipboardStore.entries.set([mockEntry, { ...mockEntry, id: "entry-2" }]);
|
||||
setMockInvokeResult("delete_clipboard_entry", undefined);
|
||||
await clipboardStore.deleteEntry("entry-1");
|
||||
expect(get(clipboardStore.entries)).toHaveLength(1);
|
||||
expect(get(clipboardStore.entries)[0].id).toBe("entry-2");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("delete_clipboard_entry", new Error("Failed"));
|
||||
await clipboardStore.deleteEntry("entry-1");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to delete clipboard entry:",
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("togglePin", () => {
|
||||
it("updates entry pin status and sorts pinned first", async () => {
|
||||
const unpinned1 = { ...mockEntry, id: "entry-1", is_pinned: false };
|
||||
const unpinned2 = { ...mockEntry, id: "entry-2", is_pinned: false };
|
||||
clipboardStore.entries.set([unpinned1, unpinned2]);
|
||||
const pinned = { ...unpinned2, is_pinned: true };
|
||||
setMockInvokeResult("toggle_pin_clipboard_entry", pinned);
|
||||
await clipboardStore.togglePin("entry-2");
|
||||
const entries = get(clipboardStore.entries);
|
||||
expect(entries[0].id).toBe("entry-2");
|
||||
expect(entries[0].is_pinned).toBe(true);
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("toggle_pin_clipboard_entry", new Error("Failed"));
|
||||
await clipboardStore.togglePin("entry-1");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to toggle pin:", expect.any(Error));
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearHistory", () => {
|
||||
it("removes unpinned entries and keeps pinned ones", async () => {
|
||||
const pinned = { ...mockEntry, id: "pinned", is_pinned: true };
|
||||
const unpinned = { ...mockEntry, id: "unpinned", is_pinned: false };
|
||||
clipboardStore.entries.set([pinned, unpinned]);
|
||||
setMockInvokeResult("clear_clipboard_history", undefined);
|
||||
await clipboardStore.clearHistory();
|
||||
const entries = get(clipboardStore.entries);
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].id).toBe("pinned");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("clear_clipboard_history", new Error("Failed"));
|
||||
await clipboardStore.clearHistory();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to clear clipboard history:",
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateLanguage", () => {
|
||||
it("updates entry language in the store", async () => {
|
||||
clipboardStore.entries.set([mockEntry]);
|
||||
const updated = { ...mockEntry, language: "javascript" };
|
||||
setMockInvokeResult("update_clipboard_language", updated);
|
||||
await clipboardStore.updateLanguage("entry-1", "javascript");
|
||||
expect(get(clipboardStore.entries)[0].language).toBe("javascript");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("update_clipboard_language", new Error("Failed"));
|
||||
await clipboardStore.updateLanguage("entry-1", "javascript");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to update language:", expect.any(Error));
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("copyToClipboard", () => {
|
||||
it("returns true and copies text on success", async () => {
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
|
||||
});
|
||||
const result = await clipboardStore.copyToClipboard("hello world");
|
||||
expect(result).toBe(true);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("hello world");
|
||||
});
|
||||
|
||||
it("returns false and logs error on failure", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: vi.fn().mockRejectedValue(new Error("Clipboard denied")) },
|
||||
});
|
||||
const result = await clipboardStore.copyToClipboard("hello world");
|
||||
expect(result).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to copy to clipboard:",
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user