Files
hikari-desktop/src/lib/stores/quickActions.test.ts
T
naomi b3d79a82ef
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled
Security Scan and Upload / Security & DefectDojo Upload (push) Has been cancelled
feat: add tests and assert coverage (#71)
### Explanation

_No response_

### Issue

_No response_

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Co-authored-by: Hikari <hikari@nhcarrigan.com>
Reviewed-on: #71
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
2026-01-26 00:26:03 -08:00

352 lines
11 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { invoke } from "@tauri-apps/api/core";
import { setMockInvokeResult } from "../../../vitest.setup";
import { quickActionsStore, type QuickAction } from "./quickActions";
describe("QuickAction interface", () => {
it("defines all required fields", () => {
const action: QuickAction = {
id: "action-123",
name: "Run Tests",
prompt: "Please run the tests",
icon: "play",
is_default: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
expect(action.id).toBe("action-123");
expect(action.name).toBe("Run Tests");
expect(action.prompt).toBe("Please run the tests");
expect(action.icon).toBe("play");
expect(action.is_default).toBe(false);
expect(action.created_at).toBe("2024-01-01T00:00:00Z");
expect(action.updated_at).toBe("2024-01-01T00:00:00Z");
});
it("supports default actions", () => {
const defaultAction: QuickAction = {
id: "default-review-pr",
name: "Review PR",
prompt: "Please review this pull request",
icon: "git-pull-request",
is_default: true,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
expect(defaultAction.is_default).toBe(true);
expect(defaultAction.id).toContain("default-");
});
});
describe("quickActionsStore", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("store structure", () => {
it("has all expected methods", () => {
expect(typeof quickActionsStore.loadQuickActions).toBe("function");
expect(typeof quickActionsStore.saveQuickAction).toBe("function");
expect(typeof quickActionsStore.createQuickAction).toBe("function");
expect(typeof quickActionsStore.updateQuickAction).toBe("function");
expect(typeof quickActionsStore.deleteQuickAction).toBe("function");
expect(typeof quickActionsStore.resetDefaults).toBe("function");
});
it("has subscribable stores", () => {
expect(typeof quickActionsStore.actions.subscribe).toBe("function");
expect(typeof quickActionsStore.isLoading.subscribe).toBe("function");
});
});
describe("loadQuickActions", () => {
it("loads quick actions from backend", async () => {
const mockActions: QuickAction[] = [
{
id: "action-1",
name: "Action 1",
prompt: "prompt 1",
icon: "star",
is_default: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
];
setMockInvokeResult("list_quick_actions", mockActions);
await quickActionsStore.loadQuickActions();
expect(invoke).toHaveBeenCalledWith("list_quick_actions");
});
it("handles load errors gracefully", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("list_quick_actions", new Error("Failed to load"));
await quickActionsStore.loadQuickActions();
expect(consoleSpy).toHaveBeenCalledWith("Failed to load quick actions:", expect.any(Error));
consoleSpy.mockRestore();
});
});
describe("saveQuickAction", () => {
it("saves action and reloads list", async () => {
const action: QuickAction = {
id: "action-123",
name: "Test",
prompt: "test prompt",
icon: "star",
is_default: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
setMockInvokeResult("save_quick_action", undefined);
setMockInvokeResult("list_quick_actions", [action]);
const result = await quickActionsStore.saveQuickAction(action);
expect(result).toBe(true);
expect(invoke).toHaveBeenCalledWith("save_quick_action", { action });
});
it("handles save errors gracefully", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("save_quick_action", new Error("Failed to save"));
const action: QuickAction = {
id: "action-123",
name: "Test",
prompt: "test prompt",
icon: "star",
is_default: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
const result = await quickActionsStore.saveQuickAction(action);
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("Failed to save quick action:", expect.any(Error));
consoleSpy.mockRestore();
});
});
describe("createQuickAction", () => {
it("creates new action with generated ID and timestamps", async () => {
setMockInvokeResult("save_quick_action", undefined);
setMockInvokeResult("list_quick_actions", []);
const result = await quickActionsStore.createQuickAction("My Action", "Do something", "star");
expect(result).toBe(true);
expect(invoke).toHaveBeenCalledWith(
"save_quick_action",
expect.objectContaining({
action: expect.objectContaining({
name: "My Action",
prompt: "Do something",
icon: "star",
is_default: false,
}),
})
);
});
});
describe("updateQuickAction", () => {
it("updates existing action preserving created_at", async () => {
const existingAction: QuickAction = {
id: "action-123",
name: "Old Name",
prompt: "old prompt",
icon: "old-icon",
is_default: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
setMockInvokeResult("list_quick_actions", [existingAction]);
setMockInvokeResult("save_quick_action", undefined);
const result = await quickActionsStore.updateQuickAction(
"action-123",
"New Name",
"new prompt",
"new-icon"
);
expect(result).toBe(true);
expect(invoke).toHaveBeenCalledWith(
"save_quick_action",
expect.objectContaining({
action: expect.objectContaining({
id: "action-123",
name: "New Name",
prompt: "new prompt",
icon: "new-icon",
created_at: "2024-01-01T00:00:00Z",
}),
})
);
});
it("returns false for non-existent action", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("list_quick_actions", []);
const result = await quickActionsStore.updateQuickAction(
"non-existent",
"Name",
"prompt",
"icon"
);
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("Quick action not found for update");
consoleSpy.mockRestore();
});
});
describe("deleteQuickAction", () => {
it("deletes action by ID", async () => {
setMockInvokeResult("delete_quick_action", undefined);
setMockInvokeResult("list_quick_actions", []);
const result = await quickActionsStore.deleteQuickAction("action-123");
expect(result).toBe(true);
expect(invoke).toHaveBeenCalledWith("delete_quick_action", { actionId: "action-123" });
});
it("handles delete errors gracefully", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("delete_quick_action", new Error("Cannot delete default action"));
const result = await quickActionsStore.deleteQuickAction("default-1");
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("Failed to delete quick action:", expect.any(Error));
consoleSpy.mockRestore();
});
});
describe("resetDefaults", () => {
it("resets default actions", async () => {
setMockInvokeResult("reset_default_quick_actions", undefined);
setMockInvokeResult("list_quick_actions", []);
const result = await quickActionsStore.resetDefaults();
expect(result).toBe(true);
expect(invoke).toHaveBeenCalledWith("reset_default_quick_actions");
});
it("handles reset errors gracefully", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("reset_default_quick_actions", new Error("Reset failed"));
const result = await quickActionsStore.resetDefaults();
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith(
"Failed to reset default quick actions:",
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
});
describe("quick action ID generation", () => {
it("generates unique custom action IDs", () => {
const generateId = () => `custom-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
const id1 = generateId();
const id2 = generateId();
expect(id1).toMatch(/^custom-\d+-[a-z0-9]+$/);
expect(id2).toMatch(/^custom-\d+-[a-z0-9]+$/);
expect(id1).not.toBe(id2);
});
});
describe("action validation", () => {
it("requires non-empty name", () => {
const action = { name: "" };
const isValid = action.name.trim().length > 0;
expect(isValid).toBe(false);
});
it("requires non-empty prompt", () => {
const action = { prompt: " " };
const isValid = action.prompt.trim().length > 0;
expect(isValid).toBe(false);
});
it("requires non-empty icon", () => {
const action = { icon: "star" };
const isValid = action.icon.trim().length > 0;
expect(isValid).toBe(true);
});
});
describe("action icons", () => {
it("supports various icon names", () => {
const validIcons = [
"git-pull-request",
"play",
"file-text",
"alert-circle",
"check-square",
"refresh-cw",
"star",
"code",
"terminal",
];
validIcons.forEach((icon) => {
expect(typeof icon).toBe("string");
expect(icon.length).toBeGreaterThan(0);
});
});
});
describe("action sorting", () => {
it("sorts default actions before custom actions", () => {
const actions = [
{ id: "custom-1", name: "Custom", is_default: false },
{ id: "default-1", name: "Default", is_default: true },
];
const sorted = [...actions].sort((a, b) => {
const defaultCmp = (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0);
if (defaultCmp !== 0) return defaultCmp;
return a.name.localeCompare(b.name);
});
expect(sorted[0].is_default).toBe(true);
expect(sorted[1].is_default).toBe(false);
});
it("sorts alphabetically within same default status", () => {
const actions = [
{ id: "default-2", name: "Zebra", is_default: true },
{ id: "default-1", name: "Apple", is_default: true },
];
const sorted = [...actions].sort((a, b) => {
const defaultCmp = (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0);
if (defaultCmp !== 0) return defaultCmp;
return a.name.localeCompare(b.name);
});
expect(sorted[0].name).toBe("Apple");
expect(sorted[1].name).toBe("Zebra");
});
});