Skip to content

Commit 080ef5c

Browse files
committed
Add comprehensive tests and GitHub Actions CI
- Update App.test.tsx with relevant tests for MathJojo functionality - Add Katex.test.tsx to test LaTeX rendering component - Add Settings.test.tsx to test display mode and dark mode settings - Add CheatSheet.test.tsx to test cheat sheet toggle and quick inserts - Add QuickInsert.test.tsx to test quick insert component - Add GitHub Actions CI workflow for running tests and build verification - All 61 tests passing with ~81% code coverage
1 parent 3a1e1ce commit 080ef5c

File tree

6 files changed

+772
-5
lines changed

6 files changed

+772
-5
lines changed

.github/workflows/ci.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, claude/** ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
name: Test and Build
12+
runs-on: ubuntu-latest
13+
14+
strategy:
15+
matrix:
16+
node-version: [18.x, 20.x]
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Setup Node.js ${{ matrix.node-version }}
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: ${{ matrix.node-version }}
26+
cache: 'yarn'
27+
28+
- name: Install dependencies
29+
run: yarn install --frozen-lockfile
30+
31+
- name: Run tests
32+
run: yarn test --coverage --watchAll=false
33+
34+
- name: Build
35+
run: yarn build
36+
37+
- name: Upload coverage to Codecov
38+
if: matrix.node-version == '20.x'
39+
uses: codecov/codecov-action@v4
40+
with:
41+
fail_ci_if_error: false
42+
files: ./coverage/lcov.info
43+
continue-on-error: true
44+
45+
lint:
46+
name: Lint
47+
runs-on: ubuntu-latest
48+
49+
steps:
50+
- name: Checkout code
51+
uses: actions/checkout@v4
52+
53+
- name: Setup Node.js
54+
uses: actions/setup-node@v4
55+
with:
56+
node-version: 20.x
57+
cache: 'yarn'
58+
59+
- name: Install dependencies
60+
run: yarn install --frozen-lockfile
61+
62+
- name: Check formatting with Prettier
63+
run: yarn prettier --check "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"

src/App.test.tsx

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,130 @@
11
import React from "react";
2-
import { render, screen } from "@testing-library/react";
2+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
33
import App from "./App";
44

5-
test("renders learn react link", () => {
6-
render(<App />);
7-
const linkElement = screen.getByText(/learn react/i);
8-
expect(linkElement).toBeInTheDocument();
5+
describe("App", () => {
6+
test("renders MathJojo heading", () => {
7+
render(<App />);
8+
const heading = screen.getByText(/MathJojo/i);
9+
expect(heading).toBeInTheDocument();
10+
});
11+
12+
test("renders with default LaTeX value", () => {
13+
render(<App />);
14+
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
15+
expect(textarea.value).toBe(
16+
"\\zeta(s) = \\sum_{n=1}^\\infty \\frac{1}{n^s}"
17+
);
18+
});
19+
20+
test("updates textarea value on user input", () => {
21+
render(<App />);
22+
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
23+
24+
fireEvent.change(textarea, { target: { value: "x^2 + y^2 = z^2" } });
25+
26+
expect(textarea.value).toBe("x^2 + y^2 = z^2");
27+
});
28+
29+
test("shows cheatsheet toggle", () => {
30+
render(<App />);
31+
const toggleLink = screen.getByText(/Hide Cheatsheet/i);
32+
expect(toggleLink).toBeInTheDocument();
33+
});
34+
35+
test("shows settings toggle", () => {
36+
render(<App />);
37+
const toggleLink = screen.getByText(/Show Settings/i);
38+
expect(toggleLink).toBeInTheDocument();
39+
});
40+
41+
test("cheatsheet can be toggled", () => {
42+
render(<App />);
43+
44+
// Initially shows "Hide Cheatsheet"
45+
let toggleLink = screen.getByText(/Hide Cheatsheet/i);
46+
expect(toggleLink).toBeInTheDocument();
47+
48+
// Click to hide
49+
fireEvent.click(toggleLink);
50+
51+
// Now shows "Show Cheatsheet"
52+
toggleLink = screen.getByText(/Show Cheatsheet/i);
53+
expect(toggleLink).toBeInTheDocument();
54+
55+
// Click to show again
56+
fireEvent.click(toggleLink);
57+
58+
// Back to "Hide Cheatsheet"
59+
toggleLink = screen.getByText(/Hide Cheatsheet/i);
60+
expect(toggleLink).toBeInTheDocument();
61+
});
62+
63+
test("settings can be toggled", () => {
64+
render(<App />);
65+
66+
// Initially shows "Show Settings"
67+
let toggleLink = screen.getByText(/Show Settings/i);
68+
expect(toggleLink).toBeInTheDocument();
69+
70+
// Click to show
71+
fireEvent.click(toggleLink);
72+
73+
// Now shows "Hide Settings"
74+
toggleLink = screen.getByText(/Hide Settings/i);
75+
expect(toggleLink).toBeInTheDocument();
76+
});
77+
});
78+
79+
describe("URL parameter handling", () => {
80+
const originalLocation = window.location;
81+
82+
beforeEach(() => {
83+
// Mock window.location
84+
delete (window as any).location;
85+
window.location = { ...originalLocation, search: "" } as any;
86+
});
87+
88+
afterEach(() => {
89+
window.location = originalLocation;
90+
});
91+
92+
test("loads value from URL parameter", () => {
93+
// Set up URL with compressed value
94+
window.location.search = "?v=NobAgB";
95+
96+
render(<App />);
97+
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
98+
99+
// The compressed value should be decompressed
100+
expect(textarea.value).toBeTruthy();
101+
});
102+
103+
test("handles empty compressed value", () => {
104+
window.location.search = "?v=Q";
105+
106+
render(<App />);
107+
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
108+
109+
// Should render empty value when v=Q
110+
expect(textarea.value).toBe("");
111+
});
112+
113+
test("loads displayMode from URL parameter", () => {
114+
window.location.search = "?displayMode=0";
115+
116+
render(<App />);
117+
118+
// Open settings to check display mode
119+
const toggleLink = screen.getByText(/Show Settings/i);
120+
fireEvent.click(toggleLink);
121+
122+
// The "No" option for display mode should be bold (active)
123+
const displayModeOptions = screen.getAllByText(/^(Yes|No)$/);
124+
const noOption = displayModeOptions.find(
125+
(el) => el.textContent === "No" && el.getAttribute("href") === "#disable-displaymode"
126+
);
127+
128+
expect(noOption).toHaveStyle({ fontWeight: "bold" });
129+
});
9130
});

src/cheatsheet.test.tsx

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React from "react";
2+
import { render, screen, fireEvent } from "@testing-library/react";
3+
import CheatSheet from "./cheatsheet";
4+
5+
// Mock the QuickInsert component
6+
jest.mock("./quickinsert", () => {
7+
return function QuickInsert(props: any) {
8+
return (
9+
<a
10+
href="#insert"
11+
onClick={(e) => {
12+
e.preventDefault();
13+
props.onClick();
14+
}}
15+
data-testid={`quick-insert-${props.source}`}
16+
>
17+
{props.source}
18+
</a>
19+
);
20+
};
21+
});
22+
23+
describe("CheatSheet", () => {
24+
const defaultProps = {
25+
displayCheatSheet: false,
26+
insertSource: jest.fn(),
27+
toggleCheatSheet: jest.fn(),
28+
};
29+
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
});
33+
34+
describe("when cheatsheet is hidden", () => {
35+
test("shows 'Show Cheatsheet' link", () => {
36+
render(<CheatSheet {...defaultProps} />);
37+
const link = screen.getByText(/Show Cheatsheet/i);
38+
expect(link).toBeInTheDocument();
39+
});
40+
41+
test("calls toggleCheatSheet when clicked", () => {
42+
render(<CheatSheet {...defaultProps} />);
43+
const link = screen.getByText(/Show Cheatsheet/i);
44+
45+
fireEvent.click(link);
46+
47+
expect(defaultProps.toggleCheatSheet).toHaveBeenCalledTimes(1);
48+
});
49+
50+
test("does not show greek letters", () => {
51+
render(<CheatSheet {...defaultProps} />);
52+
const greekLettersText = screen.queryByText(/Greek Letters/i);
53+
expect(greekLettersText).not.toBeInTheDocument();
54+
});
55+
});
56+
57+
describe("when cheatsheet is shown", () => {
58+
const visibleProps = { ...defaultProps, displayCheatSheet: true };
59+
60+
test("shows 'Hide Cheatsheet' link", () => {
61+
render(<CheatSheet {...visibleProps} />);
62+
const link = screen.getByText(/Hide Cheatsheet/i);
63+
expect(link).toBeInTheDocument();
64+
});
65+
66+
test("calls toggleCheatSheet when Hide Cheatsheet is clicked", () => {
67+
render(<CheatSheet {...visibleProps} />);
68+
const link = screen.getByText(/Hide Cheatsheet/i);
69+
70+
fireEvent.click(link);
71+
72+
expect(defaultProps.toggleCheatSheet).toHaveBeenCalledTimes(1);
73+
});
74+
75+
test("shows Greek Letters section", () => {
76+
render(<CheatSheet {...visibleProps} />);
77+
const greekLettersText = screen.getByText(/Greek Letters/i);
78+
expect(greekLettersText).toBeInTheDocument();
79+
});
80+
81+
test("shows Symbols section", () => {
82+
render(<CheatSheet {...visibleProps} />);
83+
const symbolsText = screen.getByText(/Symbols/i);
84+
expect(symbolsText).toBeInTheDocument();
85+
});
86+
87+
test("shows Accents section", () => {
88+
render(<CheatSheet {...visibleProps} />);
89+
const accentsText = screen.getByText(/Accents/i);
90+
expect(accentsText).toBeInTheDocument();
91+
});
92+
93+
test("shows Layout / Common section", () => {
94+
render(<CheatSheet {...visibleProps} />);
95+
const layoutText = screen.getByText(/Layout \/ Common/i);
96+
expect(layoutText).toBeInTheDocument();
97+
});
98+
99+
test("renders greek letter quick inserts", () => {
100+
render(<CheatSheet {...visibleProps} />);
101+
102+
const alphaInsert = screen.getByTestId("quick-insert-\\alpha");
103+
expect(alphaInsert).toBeInTheDocument();
104+
105+
const betaInsert = screen.getByTestId("quick-insert-\\beta");
106+
expect(betaInsert).toBeInTheDocument();
107+
});
108+
109+
test("calls insertSource when a greek letter is clicked", () => {
110+
render(<CheatSheet {...visibleProps} />);
111+
112+
const alphaInsert = screen.getByTestId("quick-insert-\\alpha");
113+
fireEvent.click(alphaInsert);
114+
115+
expect(defaultProps.insertSource).toHaveBeenCalledWith("\\alpha");
116+
});
117+
118+
test("renders symbol quick inserts", () => {
119+
render(<CheatSheet {...visibleProps} />);
120+
121+
const infinityInsert = screen.getByTestId("quick-insert-\\infty");
122+
expect(infinityInsert).toBeInTheDocument();
123+
});
124+
125+
test("calls insertSource when a symbol is clicked", () => {
126+
render(<CheatSheet {...visibleProps} />);
127+
128+
const infinityInsert = screen.getByTestId("quick-insert-\\infty");
129+
fireEvent.click(infinityInsert);
130+
131+
expect(defaultProps.insertSource).toHaveBeenCalledWith("\\infty");
132+
});
133+
134+
test("renders accent quick inserts", () => {
135+
render(<CheatSheet {...visibleProps} />);
136+
137+
const hatInsert = screen.getByTestId("quick-insert-\\hat{x}");
138+
expect(hatInsert).toBeInTheDocument();
139+
});
140+
141+
test("calls insertSource when an accent is clicked", () => {
142+
render(<CheatSheet {...visibleProps} />);
143+
144+
const hatInsert = screen.getByTestId("quick-insert-\\hat{x}");
145+
fireEvent.click(hatInsert);
146+
147+
expect(defaultProps.insertSource).toHaveBeenCalledWith("\\hat{x}");
148+
});
149+
150+
test("renders layout/common quick inserts", () => {
151+
render(<CheatSheet {...visibleProps} />);
152+
153+
const fracInsert = screen.getByTestId("quick-insert-\\frac{a}{b}");
154+
expect(fracInsert).toBeInTheDocument();
155+
});
156+
157+
test("calls insertSource when a layout item is clicked", () => {
158+
render(<CheatSheet {...visibleProps} />);
159+
160+
const fracInsert = screen.getByTestId("quick-insert-\\frac{a}{b}");
161+
fireEvent.click(fracInsert);
162+
163+
expect(defaultProps.insertSource).toHaveBeenCalledWith("\\frac{a}{b}");
164+
});
165+
166+
test("renders uppercase greek letters", () => {
167+
render(<CheatSheet {...visibleProps} />);
168+
169+
const gammaInsert = screen.getByTestId("quick-insert-\\Gamma");
170+
expect(gammaInsert).toBeInTheDocument();
171+
});
172+
173+
test("calls insertSource when an uppercase greek letter is clicked", () => {
174+
render(<CheatSheet {...visibleProps} />);
175+
176+
const gammaInsert = screen.getByTestId("quick-insert-\\Gamma");
177+
fireEvent.click(gammaInsert);
178+
179+
expect(defaultProps.insertSource).toHaveBeenCalledWith("\\Gamma");
180+
});
181+
});
182+
});

0 commit comments

Comments
 (0)