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
63 changes: 63 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
133 changes: 128 additions & 5 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
describe("App", () => {
test("renders MathJojo heading", () => {
render(<App />);
const heading = screen.getByText(/MathJojo/i);
expect(heading).toBeInTheDocument();
});

test("renders with default LaTeX value", () => {
render(<App />);
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(<App />);
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(<App />);
const toggleLink = screen.getByText(/Hide Cheatsheet/i);
expect(toggleLink).toBeInTheDocument();
});

test("shows settings toggle", () => {
render(<App />);
const toggleLink = screen.getByText(/Show Settings/i);
expect(toggleLink).toBeInTheDocument();
});

test("cheatsheet can be toggled", () => {
render(<App />);

// 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(<App />);

// 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(<App />);
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(<App />);
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(<App />);

// 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" });
});
});
Loading