diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..cf781e2
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,63 @@
+name: CI
+
+on:
+ push:
+ branches: [ main, claude/** ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ name: Test and Build
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [18.x, 20.x]
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'yarn'
+
+ - name: Install dependencies
+ run: make install
+
+ - name: Run tests with coverage
+ run: make test-coverage
+
+ - name: Build
+ run: make build
+
+ - name: Upload coverage to Codecov
+ if: matrix.node-version == '20.x'
+ uses: codecov/codecov-action@v4
+ with:
+ fail_ci_if_error: false
+ files: ./coverage/lcov.info
+ continue-on-error: true
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.x
+ cache: 'yarn'
+
+ - name: Install dependencies
+ run: make install
+
+ - name: Check formatting with Prettier
+ run: make lint
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..264d266
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,51 @@
+.PHONY: help install test test-coverage build lint lint-fix clean ci start
+
+# Default target
+help:
+ @echo "Available targets:"
+ @echo " make install - Install dependencies"
+ @echo " make test - Run tests"
+ @echo " make test-coverage - Run tests with coverage"
+ @echo " make build - Build production bundle"
+ @echo " make lint - Check code formatting"
+ @echo " make lint-fix - Fix code formatting"
+ @echo " make clean - Clean build artifacts"
+ @echo " make ci - Run all CI checks (test + build + lint)"
+ @echo " make start - Start development server"
+
+# Install dependencies
+install:
+ yarn install --frozen-lockfile
+
+# Run tests without coverage
+test:
+ yarn test --watchAll=false
+
+# Run tests with coverage
+test-coverage:
+ yarn test --watchAll=false --coverage
+
+# Build production bundle
+build:
+ yarn build
+
+# Check code formatting with Prettier
+lint:
+ yarn prettier --check "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"
+
+# Fix code formatting with Prettier
+lint-fix:
+ yarn prettier --write "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"
+
+# Clean build artifacts
+clean:
+ rm -rf build
+ rm -rf coverage
+ rm -rf node_modules
+
+# Run all CI checks
+ci: test-coverage build lint
+
+# Start development server
+start:
+ yarn start
diff --git a/src/App.test.tsx b/src/App.test.tsx
index d76787e..6b503eb 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,9 +1,132 @@
import React from "react";
-import { render, screen } from "@testing-library/react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import App from "./App";
-test("renders learn react link", () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
+describe("App", () => {
+ test("renders MathJojo heading", () => {
+ render();
+ const heading = screen.getByText(/MathJojo/i);
+ expect(heading).toBeInTheDocument();
+ });
+
+ test("renders with default LaTeX value", () => {
+ render();
+ const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
+ expect(textarea.value).toBe(
+ "\\zeta(s) = \\sum_{n=1}^\\infty \\frac{1}{n^s}"
+ );
+ });
+
+ test("updates textarea value on user input", () => {
+ render();
+ const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
+
+ fireEvent.change(textarea, { target: { value: "x^2 + y^2 = z^2" } });
+
+ expect(textarea.value).toBe("x^2 + y^2 = z^2");
+ });
+
+ test("shows cheatsheet toggle", () => {
+ render();
+ const toggleLink = screen.getByText(/Hide Cheatsheet/i);
+ expect(toggleLink).toBeInTheDocument();
+ });
+
+ test("shows settings toggle", () => {
+ render();
+ const toggleLink = screen.getByText(/Show Settings/i);
+ expect(toggleLink).toBeInTheDocument();
+ });
+
+ test("cheatsheet can be toggled", () => {
+ render();
+
+ // Initially shows "Hide Cheatsheet"
+ let toggleLink = screen.getByText(/Hide Cheatsheet/i);
+ expect(toggleLink).toBeInTheDocument();
+
+ // Click to hide
+ fireEvent.click(toggleLink);
+
+ // Now shows "Show Cheatsheet"
+ toggleLink = screen.getByText(/Show Cheatsheet/i);
+ expect(toggleLink).toBeInTheDocument();
+
+ // Click to show again
+ fireEvent.click(toggleLink);
+
+ // Back to "Hide Cheatsheet"
+ toggleLink = screen.getByText(/Hide Cheatsheet/i);
+ expect(toggleLink).toBeInTheDocument();
+ });
+
+ test("settings can be toggled", () => {
+ render();
+
+ // Initially shows "Show Settings"
+ let toggleLink = screen.getByText(/Show Settings/i);
+ expect(toggleLink).toBeInTheDocument();
+
+ // Click to show
+ fireEvent.click(toggleLink);
+
+ // Now shows "Hide Settings"
+ toggleLink = screen.getByText(/Hide Settings/i);
+ expect(toggleLink).toBeInTheDocument();
+ });
+});
+
+describe("URL parameter handling", () => {
+ const originalLocation = window.location;
+
+ beforeEach(() => {
+ // Mock window.location
+ delete (window as any).location;
+ window.location = { ...originalLocation, search: "" } as any;
+ });
+
+ afterEach(() => {
+ window.location = originalLocation;
+ });
+
+ test("loads value from URL parameter", () => {
+ // Set up URL with compressed value
+ window.location.search = "?v=NobAgB";
+
+ render();
+ const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
+
+ // The compressed value should be decompressed
+ expect(textarea.value).toBeTruthy();
+ });
+
+ test("handles empty compressed value", () => {
+ window.location.search = "?v=Q";
+
+ render();
+ const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
+
+ // Should render empty value when v=Q
+ expect(textarea.value).toBe("");
+ });
+
+ test("loads displayMode from URL parameter", () => {
+ window.location.search = "?displayMode=0";
+
+ render();
+
+ // Open settings to check display mode
+ const toggleLink = screen.getByText(/Show Settings/i);
+ fireEvent.click(toggleLink);
+
+ // The "No" option for display mode should be bold (active)
+ const displayModeOptions = screen.getAllByText(/^(Yes|No)$/);
+ const noOption = displayModeOptions.find(
+ (el) =>
+ el.textContent === "No" &&
+ el.getAttribute("href") === "#disable-displaymode"
+ );
+
+ expect(noOption).toHaveStyle({ fontWeight: "bold" });
+ });
});
diff --git a/src/cheatsheet.test.tsx b/src/cheatsheet.test.tsx
new file mode 100644
index 0000000..3f453be
--- /dev/null
+++ b/src/cheatsheet.test.tsx
@@ -0,0 +1,182 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import CheatSheet from "./cheatsheet";
+
+// Mock the QuickInsert component
+jest.mock("./quickinsert", () => {
+ return function QuickInsert(props: any) {
+ return (
+ {
+ e.preventDefault();
+ props.onClick();
+ }}
+ data-testid={`quick-insert-${props.source}`}
+ >
+ {props.source}
+
+ );
+ };
+});
+
+describe("CheatSheet", () => {
+ const defaultProps = {
+ displayCheatSheet: false,
+ insertSource: jest.fn(),
+ toggleCheatSheet: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("when cheatsheet is hidden", () => {
+ test("shows 'Show Cheatsheet' link", () => {
+ render();
+ const link = screen.getByText(/Show Cheatsheet/i);
+ expect(link).toBeInTheDocument();
+ });
+
+ test("calls toggleCheatSheet when clicked", () => {
+ render();
+ const link = screen.getByText(/Show Cheatsheet/i);
+
+ fireEvent.click(link);
+
+ expect(defaultProps.toggleCheatSheet).toHaveBeenCalledTimes(1);
+ });
+
+ test("does not show greek letters", () => {
+ render();
+ const greekLettersText = screen.queryByText(/Greek Letters/i);
+ expect(greekLettersText).not.toBeInTheDocument();
+ });
+ });
+
+ describe("when cheatsheet is shown", () => {
+ const visibleProps = { ...defaultProps, displayCheatSheet: true };
+
+ test("shows 'Hide Cheatsheet' link", () => {
+ render();
+ const link = screen.getByText(/Hide Cheatsheet/i);
+ expect(link).toBeInTheDocument();
+ });
+
+ test("calls toggleCheatSheet when Hide Cheatsheet is clicked", () => {
+ render();
+ const link = screen.getByText(/Hide Cheatsheet/i);
+
+ fireEvent.click(link);
+
+ expect(defaultProps.toggleCheatSheet).toHaveBeenCalledTimes(1);
+ });
+
+ test("shows Greek Letters section", () => {
+ render();
+ const greekLettersText = screen.getByText(/Greek Letters/i);
+ expect(greekLettersText).toBeInTheDocument();
+ });
+
+ test("shows Symbols section", () => {
+ render();
+ const symbolsText = screen.getByText(/Symbols/i);
+ expect(symbolsText).toBeInTheDocument();
+ });
+
+ test("shows Accents section", () => {
+ render();
+ const accentsText = screen.getByText(/Accents/i);
+ expect(accentsText).toBeInTheDocument();
+ });
+
+ test("shows Layout / Common section", () => {
+ render();
+ const layoutText = screen.getByText(/Layout \/ Common/i);
+ expect(layoutText).toBeInTheDocument();
+ });
+
+ test("renders greek letter quick inserts", () => {
+ render();
+
+ const alphaInsert = screen.getByTestId("quick-insert-\\alpha");
+ expect(alphaInsert).toBeInTheDocument();
+
+ const betaInsert = screen.getByTestId("quick-insert-\\beta");
+ expect(betaInsert).toBeInTheDocument();
+ });
+
+ test("calls insertSource when a greek letter is clicked", () => {
+ render();
+
+ const alphaInsert = screen.getByTestId("quick-insert-\\alpha");
+ fireEvent.click(alphaInsert);
+
+ expect(defaultProps.insertSource).toHaveBeenCalledWith("\\alpha");
+ });
+
+ test("renders symbol quick inserts", () => {
+ render();
+
+ const infinityInsert = screen.getByTestId("quick-insert-\\infty");
+ expect(infinityInsert).toBeInTheDocument();
+ });
+
+ test("calls insertSource when a symbol is clicked", () => {
+ render();
+
+ const infinityInsert = screen.getByTestId("quick-insert-\\infty");
+ fireEvent.click(infinityInsert);
+
+ expect(defaultProps.insertSource).toHaveBeenCalledWith("\\infty");
+ });
+
+ test("renders accent quick inserts", () => {
+ render();
+
+ const hatInsert = screen.getByTestId("quick-insert-\\hat{x}");
+ expect(hatInsert).toBeInTheDocument();
+ });
+
+ test("calls insertSource when an accent is clicked", () => {
+ render();
+
+ const hatInsert = screen.getByTestId("quick-insert-\\hat{x}");
+ fireEvent.click(hatInsert);
+
+ expect(defaultProps.insertSource).toHaveBeenCalledWith("\\hat{x}");
+ });
+
+ test("renders layout/common quick inserts", () => {
+ render();
+
+ const fracInsert = screen.getByTestId("quick-insert-\\frac{a}{b}");
+ expect(fracInsert).toBeInTheDocument();
+ });
+
+ test("calls insertSource when a layout item is clicked", () => {
+ render();
+
+ const fracInsert = screen.getByTestId("quick-insert-\\frac{a}{b}");
+ fireEvent.click(fracInsert);
+
+ expect(defaultProps.insertSource).toHaveBeenCalledWith("\\frac{a}{b}");
+ });
+
+ test("renders uppercase greek letters", () => {
+ render();
+
+ const gammaInsert = screen.getByTestId("quick-insert-\\Gamma");
+ expect(gammaInsert).toBeInTheDocument();
+ });
+
+ test("calls insertSource when an uppercase greek letter is clicked", () => {
+ render();
+
+ const gammaInsert = screen.getByTestId("quick-insert-\\Gamma");
+ fireEvent.click(gammaInsert);
+
+ expect(defaultProps.insertSource).toHaveBeenCalledWith("\\Gamma");
+ });
+ });
+});
diff --git a/src/katex.test.tsx b/src/katex.test.tsx
new file mode 100644
index 0000000..326212a
--- /dev/null
+++ b/src/katex.test.tsx
@@ -0,0 +1,117 @@
+import React from "react";
+import { render } from "@testing-library/react";
+import Katex from "./katex";
+import katex from "katex";
+
+// Mock katex
+jest.mock("katex", () => ({
+ render: jest.fn(),
+}));
+
+describe("Katex", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("renders span element", () => {
+ const { container } = render();
+ const span = container.querySelector("span");
+ expect(span).toBeInTheDocument();
+ });
+
+ test("calls katex.render on mount", () => {
+ render();
+
+ expect(katex.render).toHaveBeenCalledTimes(1);
+ expect(katex.render).toHaveBeenCalledWith(
+ "x^2 + y^2 = z^2",
+ expect.any(HTMLElement),
+ expect.objectContaining({
+ throwOnError: false,
+ })
+ );
+ });
+
+ test("calls katex.render when source changes", () => {
+ const { rerender } = render();
+
+ expect(katex.render).toHaveBeenCalledTimes(1);
+
+ rerender();
+
+ expect(katex.render).toHaveBeenCalledTimes(2);
+ expect(katex.render).toHaveBeenLastCalledWith(
+ "y^2",
+ expect.any(HTMLElement),
+ expect.objectContaining({
+ throwOnError: false,
+ })
+ );
+ });
+
+ test("does not re-render when source is unchanged", () => {
+ const { rerender } = render();
+
+ expect(katex.render).toHaveBeenCalledTimes(1);
+
+ rerender();
+
+ expect(katex.render).toHaveBeenCalledTimes(1);
+ });
+
+ test("passes katexOptions to katex.render", () => {
+ const source = "\\frac{a}{b}";
+ render(
+
+ );
+
+ expect(katex.render).toHaveBeenCalledWith(
+ source,
+ expect.any(HTMLElement),
+ expect.objectContaining({
+ throwOnError: false,
+ displayMode: true,
+ fleqn: true,
+ })
+ );
+ });
+
+ test("re-renders when katexOptions change", () => {
+ const { rerender } = render(
+
+ );
+
+ expect(katex.render).toHaveBeenCalledTimes(1);
+
+ rerender();
+
+ expect(katex.render).toHaveBeenCalledTimes(2);
+ });
+
+ test("calls beforeRender callback if provided", () => {
+ const beforeRender = jest.fn();
+
+ render();
+
+ expect(beforeRender).toHaveBeenCalledTimes(1);
+ });
+
+ test("calls beforeRender before katex.render", () => {
+ const callOrder: string[] = [];
+
+ const beforeRender = jest.fn(() => {
+ callOrder.push("beforeRender");
+ });
+
+ (katex.render as jest.Mock).mockImplementation(() => {
+ callOrder.push("katex.render");
+ });
+
+ render();
+
+ expect(callOrder).toEqual(["beforeRender", "katex.render"]);
+ });
+});
diff --git a/src/quickinsert.test.tsx b/src/quickinsert.test.tsx
new file mode 100644
index 0000000..fdeb1b5
--- /dev/null
+++ b/src/quickinsert.test.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import QuickInsert from "./quickinsert";
+
+// Mock the Katex component
+jest.mock("./katex", () => {
+ return function Katex(props: any) {
+ return {props.source};
+ };
+});
+
+describe("QuickInsert", () => {
+ const mockOnClick = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("renders a link", () => {
+ render();
+
+ const link = screen.getByRole("link");
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute("href", "#insert");
+ });
+
+ test("renders Katex component with source", () => {
+ render();
+
+ const katex = screen.getByTestId("katex-mock");
+ expect(katex).toBeInTheDocument();
+ expect(katex).toHaveTextContent("\\alpha");
+ });
+
+ test("has title attribute with source", () => {
+ const source = "\\beta";
+ render();
+
+ const link = screen.getByRole("link");
+ expect(link).toHaveAttribute("title", source);
+ });
+
+ test("calls onClick when clicked", () => {
+ render();
+
+ const link = screen.getByRole("link");
+ fireEvent.click(link);
+
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ test("prevents default on click", () => {
+ render();
+
+ const link = screen.getByRole("link");
+ const event = new MouseEvent("click", { bubbles: true, cancelable: true });
+
+ Object.defineProperty(event, "preventDefault", {
+ value: jest.fn(),
+ writable: true,
+ });
+
+ link.dispatchEvent(event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+
+ test("works with complex LaTeX source", () => {
+ const complexSource = "\\frac{a}{b}";
+ render();
+
+ const link = screen.getByRole("link");
+ expect(link).toHaveAttribute("title", complexSource);
+
+ const katex = screen.getByTestId("katex-mock");
+ expect(katex).toHaveTextContent(complexSource);
+ });
+
+ test("can be clicked multiple times", () => {
+ render();
+
+ const link = screen.getByRole("link");
+
+ fireEvent.click(link);
+ fireEvent.click(link);
+ fireEvent.click(link);
+
+ expect(mockOnClick).toHaveBeenCalledTimes(3);
+ });
+});
diff --git a/src/settings.test.tsx b/src/settings.test.tsx
new file mode 100644
index 0000000..2147d06
--- /dev/null
+++ b/src/settings.test.tsx
@@ -0,0 +1,194 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import Settings from "./settings";
+
+describe("Settings", () => {
+ const defaultProps = {
+ displaySettings: false,
+ toggleSettings: jest.fn(),
+ displayMode: true,
+ setDisplayMode: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ localStorage.clear();
+ document.body.className = "";
+ });
+
+ describe("when settings are hidden", () => {
+ test("shows 'Show Settings' link", () => {
+ render();
+ const link = screen.getByText(/Show Settings/i);
+ expect(link).toBeInTheDocument();
+ });
+
+ test("calls toggleSettings when clicked", () => {
+ render();
+ const link = screen.getByText(/Show Settings/i);
+
+ fireEvent.click(link);
+
+ expect(defaultProps.toggleSettings).toHaveBeenCalledTimes(1);
+ });
+
+ test("does not show settings options", () => {
+ render();
+ const displayModeText = screen.queryByText(/Display mode:/i);
+ expect(displayModeText).not.toBeInTheDocument();
+ });
+ });
+
+ describe("when settings are shown", () => {
+ const visibleProps = { ...defaultProps, displaySettings: true };
+
+ test("shows 'Hide Settings' link", () => {
+ render();
+ const link = screen.getByText(/Hide Settings/i);
+ expect(link).toBeInTheDocument();
+ });
+
+ test("calls toggleSettings when Hide Settings is clicked", () => {
+ render();
+ const link = screen.getByText(/Hide Settings/i);
+
+ fireEvent.click(link);
+
+ expect(defaultProps.toggleSettings).toHaveBeenCalledTimes(1);
+ });
+
+ test("shows display mode options", () => {
+ render();
+ const displayModeText = screen.getByText(/Display mode:/i);
+ expect(displayModeText).toBeInTheDocument();
+ });
+
+ test("shows dark mode options", () => {
+ render();
+ const darkModeText = screen.getByText(/Dark mode:/i);
+ expect(darkModeText).toBeInTheDocument();
+ });
+
+ test("shows GitHub link", () => {
+ render();
+ const githubLink = screen.getByText(/GitHub/i);
+ expect(githubLink).toBeInTheDocument();
+ expect(githubLink).toHaveAttribute(
+ "href",
+ "https://github.com/dashed/mathjojo"
+ );
+ });
+
+ test("shows Supported TeX functions link", () => {
+ render();
+ const texLink = screen.getByText(/Supported TeX functions/i);
+ expect(texLink).toBeInTheDocument();
+ expect(texLink).toHaveAttribute(
+ "href",
+ "https://katex.org/docs/supported.html"
+ );
+ });
+ });
+
+ describe("display mode", () => {
+ const visibleProps = { ...defaultProps, displaySettings: true };
+
+ test("highlights 'Yes' when displayMode is true", () => {
+ render();
+
+ // Find the Yes link for display mode (not dark mode)
+ const displayModeYes = Array.from(
+ screen.getAllByRole("link", { name: /Yes/i })
+ ).find((el) => el.getAttribute("href") === "#enable-displaymode");
+
+ expect(displayModeYes).toHaveStyle({ fontWeight: "bold" });
+ });
+
+ test("highlights 'No' when displayMode is false", () => {
+ render();
+
+ const displayModeNo = Array.from(
+ screen.getAllByRole("link", { name: /No/i })
+ ).find((el) => el.getAttribute("href") === "#disable-displaymode");
+
+ expect(displayModeNo).toHaveStyle({ fontWeight: "bold" });
+ });
+
+ test("calls setDisplayMode(true) when Yes is clicked", () => {
+ render();
+
+ const displayModeYes = Array.from(
+ screen.getAllByRole("link", { name: /Yes/i })
+ ).find((el) => el.getAttribute("href") === "#enable-displaymode");
+
+ fireEvent.click(displayModeYes!);
+
+ expect(defaultProps.setDisplayMode).toHaveBeenCalledWith(true);
+ });
+
+ test("calls setDisplayMode(false) when No is clicked", () => {
+ render();
+
+ const displayModeNo = Array.from(
+ screen.getAllByRole("link", { name: /No/i })
+ ).find((el) => el.getAttribute("href") === "#disable-displaymode");
+
+ fireEvent.click(displayModeNo!);
+
+ expect(defaultProps.setDisplayMode).toHaveBeenCalledWith(false);
+ });
+ });
+
+ describe("dark mode", () => {
+ const visibleProps = { ...defaultProps, displaySettings: true };
+
+ test("initializes dark mode from localStorage", () => {
+ localStorage.setItem("useDarkMode", "1");
+
+ render();
+
+ expect(document.body.classList.contains("latex-dark")).toBe(true);
+ });
+
+ test("adds latex-dark class when dark mode is enabled", () => {
+ render();
+
+ const darkModeYes = Array.from(
+ screen.getAllByRole("link", { name: /Yes/i })
+ ).find((el) => el.getAttribute("href") === "#enable-darkmode");
+
+ fireEvent.click(darkModeYes!);
+
+ expect(document.body.classList.contains("latex-dark")).toBe(true);
+ expect(localStorage.getItem("useDarkMode")).toBe("1");
+ });
+
+ test("removes latex-dark class when dark mode is disabled", () => {
+ document.body.classList.add("latex-dark");
+ localStorage.setItem("useDarkMode", "1");
+
+ render();
+
+ const darkModeNo = Array.from(
+ screen.getAllByRole("link", { name: /No/i })
+ ).find((el) => el.getAttribute("href") === "#disable-darkmode");
+
+ fireEvent.click(darkModeNo!);
+
+ expect(document.body.classList.contains("latex-dark")).toBe(false);
+ expect(localStorage.getItem("useDarkMode")).toBe("0");
+ });
+
+ test("persists dark mode preference to localStorage", () => {
+ render();
+
+ const darkModeYes = Array.from(
+ screen.getAllByRole("link", { name: /Yes/i })
+ ).find((el) => el.getAttribute("href") === "#enable-darkmode");
+
+ fireEvent.click(darkModeYes!);
+
+ expect(localStorage.getItem("useDarkMode")).toBe("1");
+ });
+ });
+});