From 15fee58ae2e518e3eb3dc01c08dc40b7007f6203 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Thu, 23 Jul 2026 12:09:08 -0500
Subject: [PATCH] test(app-frontend): configure vitest and implement state and
component test suite
---
app/src/App.test.tsx | 51 ++++++
app/src/components/GeneralTab.test.tsx | 61 +++++++
app/src/components/LicenseTab.test.tsx | 159 +++++++++++++++++
app/src/components/RulesTab.test.tsx | 137 +++++++++++++++
app/src/components/TabHeaders.test.tsx | 66 ++++++++
app/src/components/ToggleSwitch.test.tsx | 31 ++++
app/src/reducer.test.ts | 207 +++++++++++++++++++++++
app/src/test/setup.ts | 1 +
app/vite.config.ts | 31 ++++
9 files changed, 744 insertions(+)
create mode 100644 app/src/App.test.tsx
create mode 100644 app/src/components/GeneralTab.test.tsx
create mode 100644 app/src/components/LicenseTab.test.tsx
create mode 100644 app/src/components/RulesTab.test.tsx
create mode 100644 app/src/components/TabHeaders.test.tsx
create mode 100644 app/src/components/ToggleSwitch.test.tsx
create mode 100644 app/src/reducer.test.ts
create mode 100644 app/src/test/setup.ts
create mode 100644 app/vite.config.ts
diff --git a/app/src/App.test.tsx b/app/src/App.test.tsx
new file mode 100644
index 0000000..0d13579
--- /dev/null
+++ b/app/src/App.test.tsx
@@ -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();
+ });
+
+ 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();
+});
diff --git a/app/src/components/GeneralTab.test.tsx b/app/src/components/GeneralTab.test.tsx
new file mode 100644
index 0000000..51e632c
--- /dev/null
+++ b/app/src/components/GeneralTab.test.tsx
@@ -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(
+ ,
+ );
+
+ 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(
+ {}}
+ onUpdateConfig={() => {}}
+ />,
+ );
+
+ expect(screen.getByText(/conflict/i)).toBeInTheDocument();
+});
diff --git a/app/src/components/LicenseTab.test.tsx b/app/src/components/LicenseTab.test.tsx
new file mode 100644
index 0000000..dbbad82
--- /dev/null
+++ b/app/src/components/LicenseTab.test.tsx
@@ -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(
+ {}}
+ 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(
+ {}}
+ />,
+ );
+
+ 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(
+ {}}
+ 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(
+ {}}
+ 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(
+ {}}
+ 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(
+ {}}
+ 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();
+});
diff --git a/app/src/components/RulesTab.test.tsx b/app/src/components/RulesTab.test.tsx
new file mode 100644
index 0000000..3f2c3ca
--- /dev/null
+++ b/app/src/components/RulesTab.test.tsx
@@ -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();
+
+ 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();
+
+ 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();
+
+ expect(
+ screen.queryByPlaceholderText("ref, tracking_id, campaign"),
+ ).not.toBeInTheDocument();
+});
+
+test("typing shows debouncing indicator", () => {
+ vi.useFakeTimers();
+ const onUpdateConfig = vi.fn();
+
+ render();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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"],
+ });
+});
diff --git a/app/src/components/TabHeaders.test.tsx b/app/src/components/TabHeaders.test.tsx
new file mode 100644
index 0000000..540a37e
--- /dev/null
+++ b/app/src/components/TabHeaders.test.tsx
@@ -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(
+ ,
+ );
+
+ 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(
+ {}}
+ />,
+ );
+
+ expect(screen.getByText("License ⚠️")).toBeInTheDocument();
+});
+
+test("General and Rules tabs are disabled when unlicensed", () => {
+ render(
+ {}}
+ />,
+ );
+
+ 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(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("tab", { name: "General" }));
+ fireEvent.click(screen.getByRole("tab", { name: "Rules" }));
+
+ expect(onTabChange).not.toHaveBeenCalled();
+});
diff --git a/app/src/components/ToggleSwitch.test.tsx b/app/src/components/ToggleSwitch.test.tsx
new file mode 100644
index 0000000..32d275c
--- /dev/null
+++ b/app/src/components/ToggleSwitch.test.tsx
@@ -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( {}} />);
+ expect(screen.getByRole("checkbox")).not.toBeChecked();
+});
+
+test("renders checked", () => {
+ render( {}} />);
+ expect(screen.getByRole("checkbox")).toBeChecked();
+});
+
+test("calls onChange with true when clicked unchecked", () => {
+ const onChange = vi.fn();
+
+ render();
+ fireEvent.click(screen.getByRole("checkbox"));
+
+ expect(onChange).toHaveBeenCalledWith(true);
+});
+
+test("calls onChange with false when clicked checked", () => {
+ const onChange = vi.fn();
+
+ render();
+ fireEvent.click(screen.getByRole("checkbox"));
+
+ expect(onChange).toHaveBeenCalledWith(false);
+});
diff --git a/app/src/reducer.test.ts b/app/src/reducer.test.ts
new file mode 100644
index 0000000..9da5aad
--- /dev/null
+++ b/app/src/reducer.test.ts
@@ -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");
+});
diff --git a/app/src/test/setup.ts b/app/src/test/setup.ts
new file mode 100644
index 0000000..d0de870
--- /dev/null
+++ b/app/src/test/setup.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom";
diff --git a/app/vite.config.ts b/app/vite.config.ts
new file mode 100644
index 0000000..0fa90ad
--- /dev/null
+++ b/app/vite.config.ts
@@ -0,0 +1,31 @@
+///
+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/**"],
+ },
+ },
+}));