Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions electron/src/__tests__/vaultSwitch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
jest.mock("../logger", () => ({
logMessage: jest.fn()
}));

jest.mock("../vaults", () => ({
setActiveVaultId: jest.fn()
}));

jest.mock("../server", () => ({
initializeBackendServer: jest.fn().mockResolvedValue(undefined),
stopServer: jest.fn().mockResolvedValue(undefined)
}));

jest.mock("../shortcuts", () => ({
setupWorkflowShortcuts: jest.fn().mockResolvedValue(undefined)
}));

jest.mock("../window", () => ({
reloadMainWindow: jest.fn()
}));

import { applyVaultSwitch } from "../vaultSwitch";
import { setActiveVaultId } from "../vaults";
import { initializeBackendServer, stopServer } from "../server";
import { setupWorkflowShortcuts } from "../shortcuts";
import { reloadMainWindow } from "../window";
import { logMessage } from "../logger";

describe("applyVaultSwitch", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it("persists the vault id", async () => {
const promise = applyVaultSwitch("vault-42");
await jest.advanceTimersByTimeAsync(300);
await promise;

expect(setActiveVaultId).toHaveBeenCalledWith("vault-42");
});

it("logs the switch", async () => {
const promise = applyVaultSwitch("vault-42");
await jest.advanceTimersByTimeAsync(300);
await promise;

expect(logMessage).toHaveBeenCalledWith(
expect.stringContaining("vault-42")
);
});

it("stops the server", async () => {
const promise = applyVaultSwitch("vault-42");
await jest.advanceTimersByTimeAsync(300);
await promise;

expect(stopServer).toHaveBeenCalled();
});

it("does not restart the backend before the delay elapses", async () => {
const promise = applyVaultSwitch("vault-42");

expect(initializeBackendServer).not.toHaveBeenCalled();

await jest.advanceTimersByTimeAsync(300);
await promise;

expect(initializeBackendServer).toHaveBeenCalled();
});

it("re-registers workflow shortcuts", async () => {
const promise = applyVaultSwitch("vault-42");
await jest.advanceTimersByTimeAsync(300);
await promise;

expect(setupWorkflowShortcuts).toHaveBeenCalled();
});

it("reloads the main window", async () => {
const promise = applyVaultSwitch("vault-42");
await jest.advanceTimersByTimeAsync(300);
await promise;

expect(reloadMainWindow).toHaveBeenCalled();
});

it("calls all steps in the correct order", async () => {
const order: string[] = [];
(setActiveVaultId as jest.Mock).mockImplementation(() => order.push("setVault"));
(stopServer as jest.Mock).mockImplementation(async () => order.push("stop"));
(initializeBackendServer as jest.Mock).mockImplementation(async () => order.push("start"));
(setupWorkflowShortcuts as jest.Mock).mockImplementation(async () => order.push("shortcuts"));
(reloadMainWindow as jest.Mock).mockImplementation(() => order.push("reload"));

const promise = applyVaultSwitch("v1");
await jest.advanceTimersByTimeAsync(300);
await promise;

expect(order).toEqual(["setVault", "stop", "start", "shortcuts", "reload"]);
});
});
94 changes: 94 additions & 0 deletions web/src/components/tutorials/__tests__/tutorialsData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { TUTORIALS, getTutorial } from "../tutorialsData";
import type { Tutorial } from "../tutorialsData";

describe("tutorialsData", () => {
describe("TUTORIALS", () => {
it("is a non-empty array", () => {
expect(TUTORIALS.length).toBeGreaterThan(0);
});

it("every entry has all required fields", () => {
const requiredKeys: (keyof Tutorial)[] = [
"id",
"title",
"tagline",
"description",
"level",
"durationLabel",
"video",
"poster",
"accent",
"learn"
];
for (const tutorial of TUTORIALS) {
for (const key of requiredKeys) {
expect(tutorial).toHaveProperty(key);
}
}
});

it("every id is unique", () => {
const ids = TUTORIALS.map((t) => t.id);
expect(new Set(ids).size).toBe(ids.length);
});

it("every learn array is non-empty", () => {
for (const tutorial of TUTORIALS) {
expect(tutorial.learn.length).toBeGreaterThan(0);
}
});

it("every video path starts with /tutorials/", () => {
for (const tutorial of TUTORIALS) {
expect(tutorial.video).toMatch(/^\/tutorials\//);
}
});

it("every poster path starts with /tutorials/", () => {
for (const tutorial of TUTORIALS) {
expect(tutorial.poster).toMatch(/^\/tutorials\//);
}
});

it("every accent is a valid hex color", () => {
for (const tutorial of TUTORIALS) {
expect(tutorial.accent).toMatch(/^#[0-9a-fA-F]{6}$/);
}
});

it("every durationLabel matches M:SS format", () => {
for (const tutorial of TUTORIALS) {
expect(tutorial.durationLabel).toMatch(/^\d+:\d{2}$/);
}
});
});

describe("getTutorial", () => {
it("returns the matching tutorial by id", () => {
const first = TUTORIALS[0];
const result = getTutorial(first.id);
expect(result).toBe(first);
});

it("returns the last tutorial when it exists", () => {
const last = TUTORIALS[TUTORIALS.length - 1];
const result = getTutorial(last.id);
expect(result).toBe(last);
});

it("falls back to the first tutorial for an unknown id", () => {
const result = getTutorial("nonexistent-tutorial-id");
expect(result).toBe(TUTORIALS[0]);
});

it("falls back to the first tutorial when id is null", () => {
const result = getTutorial(null);
expect(result).toBe(TUTORIALS[0]);
});

it("falls back to the first tutorial when id is undefined", () => {
const result = getTutorial(undefined);
expect(result).toBe(TUTORIALS[0]);
});
});
});
191 changes: 191 additions & 0 deletions web/src/components/ui_primitives/__tests__/tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {
FONT_WEIGHT,
FONT_SIZE_SANS,
FONT_SIZE_MONO,
TYPOGRAPHY,
MOTION,
reducedMotion,
Z_INDEX,
BORDER_RADIUS
} from "../tokens";

describe("design token constants", () => {
describe("FONT_WEIGHT", () => {
it("defines the three sanctioned weights", () => {
expect(FONT_WEIGHT.normal).toBe(400);
expect(FONT_WEIGHT.medium).toBe(500);
expect(FONT_WEIGHT.semibold).toBe(600);
});

it("contains exactly three entries", () => {
expect(Object.keys(FONT_WEIGHT)).toHaveLength(3);
});
});

describe("FONT_SIZE_SANS", () => {
it("maps each role to a CSS variable", () => {
expect(FONT_SIZE_SANS.title).toBe("var(--fontSizeBig)");
expect(FONT_SIZE_SANS.body).toBe("var(--fontSizeNormal)");
expect(FONT_SIZE_SANS.label).toBe("var(--fontSizeSmall)");
expect(FONT_SIZE_SANS.caption).toBe("var(--fontSizeSmaller)");
});

it("contains exactly four sizes", () => {
expect(Object.keys(FONT_SIZE_SANS)).toHaveLength(4);
});
});

describe("FONT_SIZE_MONO", () => {
it("maps each role to a CSS variable", () => {
expect(FONT_SIZE_MONO.code).toBe("var(--fontSizeSmall)");
expect(FONT_SIZE_MONO.strong).toBe("var(--fontSizeSmall)");
expect(FONT_SIZE_MONO.label).toBe("var(--fontSizeSmall)");
expect(FONT_SIZE_MONO.caption).toBe("var(--fontSizeSmaller)");
});

it("contains exactly four sizes", () => {
expect(Object.keys(FONT_SIZE_MONO)).toHaveLength(4);
});
});

describe("TYPOGRAPHY", () => {
it("has sans and mono families", () => {
expect(TYPOGRAPHY).toHaveProperty("sans");
expect(TYPOGRAPHY).toHaveProperty("mono");
});

it("sans has exactly four roles", () => {
expect(Object.keys(TYPOGRAPHY.sans)).toEqual([
"title",
"body",
"label",
"caption"
]);
});

it("mono has exactly four roles", () => {
expect(Object.keys(TYPOGRAPHY.mono)).toEqual([
"code",
"strong",
"label",
"caption"
]);
});

it("every style has fontSize, fontWeight, fontFamily, lineHeight", () => {
const required = ["fontSize", "fontWeight", "fontFamily", "lineHeight"];
for (const family of Object.values(TYPOGRAPHY)) {
for (const style of Object.values(family)) {
for (const key of required) {
expect(style).toHaveProperty(key);
}
}
}
});

it("sans uses fontFamily1, mono uses fontFamily2", () => {
for (const style of Object.values(TYPOGRAPHY.sans)) {
expect(style.fontFamily).toBe("var(--fontFamily1)");
}
for (const style of Object.values(TYPOGRAPHY.mono)) {
expect(style.fontFamily).toBe("var(--fontFamily2)");
}
});

it("all weights are from the sanctioned set", () => {
const allowed = new Set(Object.values(FONT_WEIGHT));
for (const family of Object.values(TYPOGRAPHY)) {
for (const style of Object.values(family)) {
expect(allowed).toContain(style.fontWeight);
}
}
});
});

describe("MOTION", () => {
it("defines fast/normal/slow durations", () => {
expect(MOTION.fast).toBe("120ms ease");
expect(MOTION.normal).toBe("200ms ease");
expect(MOTION.slow).toBe("350ms ease");
});

it("defines property shorthands", () => {
expect(MOTION.border).toContain("border-color");
expect(MOTION.opacity).toContain("opacity");
expect(MOTION.transform).toContain("transform");
expect(MOTION.shadow).toContain("box-shadow");
expect(MOTION.background).toContain("background-color");
expect(MOTION.all).toContain("all");
});

it("defines keyframe loop tiers", () => {
expect(MOTION.spin).toBe("1s linear");
expect(MOTION.pulse).toBe("2s ease-in-out");
});

it("defines none for reduced motion overrides", () => {
expect(MOTION.none).toBe("none");
});
});

describe("reducedMotion", () => {
it("wraps overrides in a prefers-reduced-motion media query", () => {
const result = reducedMotion({ transition: "none" });
expect(result).toEqual({
"@media (prefers-reduced-motion: reduce)": { transition: "none" }
});
});

it("passes through multiple overrides", () => {
const overrides = { animation: "none", opacity: 0.6 };
const result = reducedMotion(overrides);
expect(
result["@media (prefers-reduced-motion: reduce)"]
).toEqual(overrides);
});

it("handles empty overrides", () => {
const result = reducedMotion({});
expect(result).toEqual({
"@media (prefers-reduced-motion: reduce)": {}
});
});
});

describe("Z_INDEX", () => {
it("defines the stacking scale", () => {
expect(Z_INDEX.base).toBe(0);
expect(Z_INDEX.raised).toBe(1);
expect(Z_INDEX.dropdown).toBe(10);
expect(Z_INDEX.sticky).toBe(20);
expect(Z_INDEX.overlay).toBe(100);
expect(Z_INDEX.modal).toBe(200);
expect(Z_INDEX.tooltip).toBe(300);
expect(Z_INDEX.toast).toBe(400);
});

it("values are strictly ascending", () => {
const values = Object.values(Z_INDEX);
for (let i = 1; i < values.length; i++) {
expect(values[i]).toBeGreaterThan(values[i - 1]);
}
});
});

describe("BORDER_RADIUS", () => {
it("maps each size to a CSS variable", () => {
expect(BORDER_RADIUS.xs).toBe("var(--rounded-xs)");
expect(BORDER_RADIUS.sm).toBe("var(--rounded-sm)");
expect(BORDER_RADIUS.md).toBe("var(--rounded-md)");
expect(BORDER_RADIUS.lg).toBe("var(--rounded-lg)");
expect(BORDER_RADIUS.xl).toBe("var(--rounded-xl)");
expect(BORDER_RADIUS.xxl).toBe("var(--rounded-xxl)");
expect(BORDER_RADIUS.circle).toBe("var(--rounded-circle)");
expect(BORDER_RADIUS.pill).toBe("var(--rounded-pill)");
});

it("contains exactly eight entries", () => {
expect(Object.keys(BORDER_RADIUS)).toHaveLength(8);
});
});
});
Loading