test(app-frontend): configure vitest and implement state and component test suite

This commit is contained in:
Stevan Freeborn
2026-07-23 12:09:08 -05:00
parent d0d74e2722
commit 15fee58ae2
9 changed files with 744 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
import { render, screen, act } from "@testing-library/react";
import { expect, test, vi, beforeEach } from "vitest";
import App from "./App";
import { api } from "./api";
vi.mock("./api", () => {
return {
api: {
getInitialState: vi.fn(),
showSettingsWindow: vi.fn(),
updateConfig: vi.fn(),
getHotkeyStatus: vi.fn(),
},
};
});
beforeEach(() => {
vi.clearAllMocks();
});
test("mounts App and fetches initial state", async () => {
const mockInitialPayload = {
config: {
strip_utms: true,
fix_quotes: true,
fix_newlines: true,
play_sound: true,
autostart: false,
hotkey: "Alt+Shift+C",
custom_params: [],
},
hotkey_conflict: false,
hwid: "device-123",
is_licensed: true,
};
vi.mocked(api.getInitialState).mockResolvedValue(mockInitialPayload);
vi.mocked(api.showSettingsWindow).mockResolvedValue(undefined);
await act(async () => {
render(<App />);
});
expect(api.getInitialState).toHaveBeenCalledTimes(1);
expect(api.showSettingsWindow).toHaveBeenCalledTimes(1);
expect(screen.getByText("Alt")).toBeInTheDocument();
expect(screen.getByText("Shift")).toBeInTheDocument();
expect(screen.getAllByText("C").length).toBe(2); // One in logo, one in keycap
expect(screen.getByText("Play sound on clean")).toBeInTheDocument();
});
+61
View File
@@ -0,0 +1,61 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { GeneralTab } from "./GeneralTab";
import { CleanConfig } from "../types";
const mockConfig: CleanConfig = {
strip_utms: true,
fix_quotes: true,
fix_newlines: true,
play_sound: true,
autostart: false,
hotkey: "Alt+Shift+C",
custom_params: [],
};
test("renders hotkey display and toggles", () => {
const onToggleRecording = vi.fn();
const onUpdateConfig = vi.fn();
render(
<GeneralTab
config={mockConfig}
isConflict={false}
isRecording={false}
onToggleRecording={onToggleRecording}
onUpdateConfig={onUpdateConfig}
/>,
);
expect(screen.getByText("Alt")).toBeInTheDocument();
expect(screen.getByText("Shift")).toBeInTheDocument();
expect(screen.getByText("C")).toBeInTheDocument();
const hotkeyZone = screen.getByTestId("hotkey-display");
expect(hotkeyZone).toBeInTheDocument();
fireEvent.click(hotkeyZone);
expect(onToggleRecording).toHaveBeenCalledTimes(1);
const soundCheckbox = screen.getAllByRole("checkbox")[0];
fireEvent.click(soundCheckbox);
expect(onUpdateConfig).toHaveBeenCalledWith({ play_sound: false });
});
test("shows conflict text when isConflict is true", () => {
render(
<GeneralTab
config={mockConfig}
isConflict={true}
isRecording={false}
onToggleRecording={() => {}}
onUpdateConfig={() => {}}
/>,
);
expect(screen.getByText(/conflict/i)).toBeInTheDocument();
});
+159
View File
@@ -0,0 +1,159 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { LicenseTab } from "./LicenseTab";
test("renders licensed status when licensed", () => {
render(
<LicenseTab
isLicensed={true}
hwid="hw-123"
keyInput=""
isActivating={false}
error=""
onKeyInputChange={() => {}}
onActivate={() => {}}
onOfflineActivate={() => {}}
/>,
);
expect(screen.getByText("Active")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /activate/i }),
).not.toBeInTheDocument();
expect(screen.getByText("hw-123")).toBeInTheDocument();
});
test("renders activation form when unlicensed", () => {
const onKeyInputChange = vi.fn();
const onActivate = vi.fn();
render(
<LicenseTab
isLicensed={false}
hwid="hw-123"
keyInput="my-key"
isActivating={false}
error="Invalid key"
onKeyInputChange={onKeyInputChange}
onActivate={onActivate}
onOfflineActivate={() => {}}
/>,
);
expect(screen.getByText("Unregistered")).toBeInTheDocument();
const input = screen.getByPlaceholderText(/enter license key.../i);
expect(input).toHaveValue("my-key");
fireEvent.change(input, { target: { value: "new-key" } });
expect(onKeyInputChange).toHaveBeenCalledWith("new-key");
expect(screen.getByText("Invalid key")).toBeInTheDocument();
const button = screen.getByRole("button", { name: "Activate" });
fireEvent.click(button);
expect(onActivate).toHaveBeenCalledTimes(1);
});
test("renders spinner and disables input when activating", () => {
render(
<LicenseTab
isLicensed={false}
hwid="hw-123"
keyInput="my-key"
isActivating={true}
error=""
onKeyInputChange={() => {}}
onActivate={() => {}}
onOfflineActivate={() => {}}
/>,
);
expect(screen.getByPlaceholderText(/enter license key.../i)).toBeDisabled();
expect(screen.getByRole("button", { name: /activating/i })).toBeDisabled();
expect(screen.getByText("Activating...")).toBeInTheDocument();
});
test("switches to offline mode when toggle clicked", () => {
render(
<LicenseTab
isLicensed={false}
hwid="hw-123"
keyInput=""
isActivating={false}
error=""
onKeyInputChange={() => {}}
onActivate={() => {}}
onOfflineActivate={() => {}}
/>,
);
const toggle = screen.getByText("Have an offline token?");
fireEvent.click(toggle);
expect(
screen.getByPlaceholderText(/paste offline token/i),
).toBeInTheDocument();
expect(
screen.queryByPlaceholderText(/enter license key.../i),
).not.toBeInTheDocument();
});
test("calls onOfflineActivate with token when offline mode submitted", () => {
const onOfflineActivate = vi.fn();
render(
<LicenseTab
isLicensed={false}
hwid="hw-123"
keyInput=""
isActivating={false}
error=""
onKeyInputChange={() => {}}
onActivate={() => {}}
onOfflineActivate={onOfflineActivate}
/>,
);
fireEvent.click(screen.getByText("Have an offline token?"));
const input = screen.getByPlaceholderText(/paste offline token/i);
fireEvent.change(input, { target: { value: "test.payload.signature" } });
fireEvent.click(screen.getByRole("button", { name: "Verify Token" }));
expect(onOfflineActivate).toHaveBeenCalledWith("test.payload.signature");
});
test("switches back to online mode from offline mode", () => {
render(
<LicenseTab
isLicensed={false}
hwid="hw-123"
keyInput=""
isActivating={false}
error=""
onKeyInputChange={() => {}}
onActivate={() => {}}
onOfflineActivate={() => {}}
/>,
);
fireEvent.click(screen.getByText("Have an offline token?"));
expect(
screen.getByPlaceholderText(/paste offline token/i),
).toBeInTheDocument();
fireEvent.click(screen.getByText("Activate with license key instead"));
expect(
screen.getByPlaceholderText(/enter license key.../i),
).toBeInTheDocument();
expect(
screen.queryByPlaceholderText(/paste offline token/i),
).not.toBeInTheDocument();
});
+137
View File
@@ -0,0 +1,137 @@
import { render, screen, fireEvent, act } from "@testing-library/react";
import { expect, test, vi, afterEach } from "vitest";
import { RulesTab } from "./RulesTab";
import { CleanConfig } from "../types";
const mockConfig: CleanConfig = {
strip_utms: true,
fix_quotes: false,
fix_newlines: true,
play_sound: true,
autostart: false,
hotkey: "Alt+Shift+C",
custom_params: [],
};
afterEach(() => {
vi.useRealTimers();
});
test("renders rule options and calls toggles", () => {
const onUpdateConfig = vi.fn();
render(<RulesTab config={mockConfig} onUpdateConfig={onUpdateConfig} />);
expect(screen.getByText("Strip tracking parameters")).toBeInTheDocument();
expect(screen.getByText("Normalize typography")).toBeInTheDocument();
expect(screen.getByText("Clean trailing newlines")).toBeInTheDocument();
const checkboxes = screen.getAllByRole("checkbox");
expect(checkboxes[0]).toBeChecked();
expect(checkboxes[1]).not.toBeChecked();
expect(checkboxes[2]).toBeChecked();
fireEvent.click(checkboxes[1]);
expect(onUpdateConfig).toHaveBeenCalledWith({ fix_quotes: true });
});
test("custom params input is visible when strip_utms is enabled", () => {
const onUpdateConfig = vi.fn();
render(<RulesTab config={mockConfig} onUpdateConfig={onUpdateConfig} />);
expect(
screen.getByPlaceholderText("ref, tracking_id, campaign"),
).toBeInTheDocument();
});
test("custom params input is hidden when strip_utms is disabled", () => {
const onUpdateConfig = vi.fn();
const configDisabled = { ...mockConfig, strip_utms: false };
render(<RulesTab config={configDisabled} onUpdateConfig={onUpdateConfig} />);
expect(
screen.queryByPlaceholderText("ref, tracking_id, campaign"),
).not.toBeInTheDocument();
});
test("typing shows debouncing indicator", () => {
vi.useFakeTimers();
const onUpdateConfig = vi.fn();
render(<RulesTab config={mockConfig} onUpdateConfig={onUpdateConfig} />);
const input = screen.getByPlaceholderText("ref, tracking_id, campaign");
fireEvent.change(input, { target: { value: "ref" } });
expect(screen.getByText("...")).toBeInTheDocument();
});
test("debouncing indicator disappears after debounce completes", async () => {
vi.useFakeTimers();
const onUpdateConfig = vi.fn();
render(<RulesTab config={mockConfig} onUpdateConfig={onUpdateConfig} />);
const input = screen.getByPlaceholderText("ref, tracking_id, campaign");
fireEvent.change(input, { target: { value: "ref" } });
expect(screen.getByText("...")).toBeInTheDocument();
await act(async () => {
vi.advanceTimersByTime(500);
});
expect(screen.queryByText("...")).not.toBeInTheDocument();
expect(screen.getByText("\u2713")).toBeInTheDocument();
});
test("saved indicator fades away after 1.5 seconds", async () => {
vi.useFakeTimers();
const onUpdateConfig = vi.fn();
render(<RulesTab config={mockConfig} onUpdateConfig={onUpdateConfig} />);
const input = screen.getByPlaceholderText("ref, tracking_id, campaign");
fireEvent.change(input, { target: { value: "ref" } });
await act(async () => {
vi.advanceTimersByTime(500);
});
expect(screen.getByText("\u2713")).toBeInTheDocument();
await act(async () => {
vi.advanceTimersByTime(1500);
});
expect(screen.queryByText("\u2713")).not.toBeInTheDocument();
});
test("rapid keystrokes only trigger one config update", async () => {
vi.useFakeTimers();
const onUpdateConfig = vi.fn();
render(<RulesTab config={mockConfig} onUpdateConfig={onUpdateConfig} />);
const input = screen.getByPlaceholderText("ref, tracking_id, campaign");
fireEvent.change(input, { target: { value: "r" } });
fireEvent.change(input, { target: { value: "re" } });
fireEvent.change(input, { target: { value: "ref" } });
await act(async () => {
vi.advanceTimersByTime(500);
});
expect(onUpdateConfig).toHaveBeenCalledTimes(1);
expect(onUpdateConfig).toHaveBeenCalledWith({
custom_params: ["ref"],
});
});
+66
View File
@@ -0,0 +1,66 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { TabHeaders } from "./TabHeaders";
test("renders tab headers and handles switching", () => {
const onTabChange = vi.fn();
render(
<TabHeaders
activeTab="general"
isLicensed={true}
onTabChange={onTabChange}
/>,
);
expect(screen.getByText("General")).toBeInTheDocument();
expect(screen.getByText("Rules")).toBeInTheDocument();
expect(screen.getByText("License")).toBeInTheDocument();
fireEvent.click(screen.getByText("Rules"));
expect(onTabChange).toHaveBeenCalledWith("rules");
});
test("shows warning badge on License tab when unlicensed", () => {
render(
<TabHeaders
activeTab="license"
isLicensed={false}
onTabChange={() => {}}
/>,
);
expect(screen.getByText("License ⚠️")).toBeInTheDocument();
});
test("General and Rules tabs are disabled when unlicensed", () => {
render(
<TabHeaders
activeTab="license"
isLicensed={false}
onTabChange={() => {}}
/>,
);
expect(screen.getByRole("tab", { name: "General" })).toBeDisabled();
expect(screen.getByRole("tab", { name: "Rules" })).toBeDisabled();
expect(screen.getByRole("tab", { name: /License/i })).not.toBeDisabled();
});
test("clicking disabled tabs when unlicensed does not fire onTabChange", () => {
const onTabChange = vi.fn();
render(
<TabHeaders
activeTab="license"
isLicensed={false}
onTabChange={onTabChange}
/>,
);
fireEvent.click(screen.getByRole("tab", { name: "General" }));
fireEvent.click(screen.getByRole("tab", { name: "Rules" }));
expect(onTabChange).not.toHaveBeenCalled();
});
+31
View File
@@ -0,0 +1,31 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { ToggleSwitch } from "./ToggleSwitch";
test("renders unchecked", () => {
render(<ToggleSwitch checked={false} onChange={() => {}} />);
expect(screen.getByRole("checkbox")).not.toBeChecked();
});
test("renders checked", () => {
render(<ToggleSwitch checked={true} onChange={() => {}} />);
expect(screen.getByRole("checkbox")).toBeChecked();
});
test("calls onChange with true when clicked unchecked", () => {
const onChange = vi.fn();
render(<ToggleSwitch checked={false} onChange={onChange} />);
fireEvent.click(screen.getByRole("checkbox"));
expect(onChange).toHaveBeenCalledWith(true);
});
test("calls onChange with false when clicked checked", () => {
const onChange = vi.fn();
render(<ToggleSwitch checked={true} onChange={onChange} />);
fireEvent.click(screen.getByRole("checkbox"));
expect(onChange).toHaveBeenCalledWith(false);
});
+207
View File
@@ -0,0 +1,207 @@
import { expect, test } from "vitest";
import { appReducer, initialAppState } from "./reducer";
import { CleanConfig } from "./types";
const mockConfig: CleanConfig = {
strip_utms: false,
fix_quotes: false,
fix_newlines: false,
play_sound: false,
autostart: true,
hotkey: "Ctrl+Shift+D",
custom_params: [],
};
test("INITIALIZE action when licensed", () => {
const result = appReducer(initialAppState, {
type: "INITIALIZE",
payload: {
config: mockConfig,
hotkeyConflict: true,
hwid: "device-123",
isLicensed: true,
},
});
expect(result.isLicensed).toBe(true);
expect(result.hwid).toBe("device-123");
expect(result.config.hotkey).toBe("Ctrl+Shift+D");
expect(result.tabState.tab).toBe("general");
});
test("INITIALIZE action when unlicensed", () => {
const result = appReducer(initialAppState, {
type: "INITIALIZE",
payload: {
config: mockConfig,
hotkeyConflict: false,
hwid: "device-123",
isLicensed: false,
},
});
expect(result.isLicensed).toBe(false);
expect(result.tabState.tab).toBe("license");
if (result.tabState.tab === "license") {
expect(result.tabState.keyInput).toBe("");
}
});
test("SET_TAB transition", () => {
const licensedState = appReducer(initialAppState, {
type: "INITIALIZE",
payload: {
config: mockConfig,
hotkeyConflict: false,
hwid: "",
isLicensed: true,
},
});
const state = appReducer(licensedState, {
type: "SET_TAB",
payload: "rules",
});
expect(state.tabState.tab).toBe("rules");
});
test("SET_CONFIG partial update", () => {
const state = appReducer(initialAppState, {
type: "SET_CONFIG",
payload: { play_sound: false },
});
expect(state.config.play_sound).toBe(false);
});
test("SET_RECORDING on general tab", () => {
let state = appReducer(initialAppState, {
type: "INITIALIZE",
payload: {
config: mockConfig,
hotkeyConflict: false,
hwid: "",
isLicensed: true,
},
});
state = appReducer(state, {
type: "SET_TAB",
payload: "general",
});
state = appReducer(state, {
type: "SET_RECORDING",
payload: true,
});
expect(state.tabState.tab).toBe("general");
if (state.tabState.tab === "general") {
expect(state.tabState.isRecording).toBe(true);
}
});
test("ACTIVATION lifecycle", () => {
let state = appReducer(initialAppState, {
type: "SET_TAB",
payload: "license",
});
state = appReducer(state, {
type: "SET_LICENSE_KEY_INPUT",
payload: "test-key",
});
if (state.tabState.tab === "license") {
expect(state.tabState.keyInput).toBe("test-key");
}
state = appReducer(state, { type: "START_ACTIVATION" });
if (state.tabState.tab === "license") {
expect(state.tabState.isActivating).toBe(true);
}
state = appReducer(state, {
type: "ACTIVATION_FAILURE",
payload: "Invalid Key",
});
if (state.tabState.tab === "license") {
expect(state.tabState.isActivating).toBe(false);
expect(state.tabState.error).toBe("Invalid Key");
}
state = appReducer(state, { type: "ACTIVATION_SUCCESS" });
expect(state.isLicensed).toBe(true);
expect(state.tabState.tab).toBe("license");
if (state.tabState.tab === "license") {
expect(state.tabState.keyInput).toBe("");
expect(state.tabState.isActivating).toBe(false);
expect(state.tabState.error).toBe("");
}
});
test("SET_TAB to general is blocked when unlicensed", () => {
const unlicensedState = appReducer(initialAppState, {
type: "INITIALIZE",
payload: {
config: mockConfig,
hotkeyConflict: false,
hwid: "",
isLicensed: false,
},
});
expect(unlicensedState.tabState.tab).toBe("license");
const after = appReducer(unlicensedState, {
type: "SET_TAB",
payload: "general",
});
expect(after.tabState.tab).toBe("license");
});
test("SET_TAB to rules is blocked when unlicensed", () => {
const unlicensedState = appReducer(initialAppState, {
type: "INITIALIZE",
payload: {
config: mockConfig,
hotkeyConflict: false,
hwid: "",
isLicensed: false,
},
});
const after = appReducer(unlicensedState, {
type: "SET_TAB",
payload: "rules",
});
expect(after.tabState.tab).toBe("license");
});
test("updating config does not reset active tab", () => {
let state = appReducer(initialAppState, {
type: "INITIALIZE",
payload: {
config: mockConfig,
hotkeyConflict: false,
hwid: "",
isLicensed: true,
},
});
state = appReducer(state, { type: "SET_TAB", payload: "rules" });
expect(state.tabState.tab).toBe("rules");
state = appReducer(state, { type: "SET_HOTKEY_CONFLICT", payload: false });
expect(state.tabState.tab).toBe("rules");
});
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
+31
View File
@@ -0,0 +1,31 @@
/// <reference types="vitest" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
export default defineConfig(async () => ({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: "./src/test/setup.ts",
},
clearScreen: false,
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
ignored: ["**/src-tauri/**"],
},
},
}));