Skip to content

Commit 926fad4

Browse files
committed
Merge branch 'codegen-bot/persist-todos-localstorage-STA-2-1754688724' of github.com:lambda-curry/react-router-starter into codegen/sta-7-major-add-runtime-validation-to-storage-loader
2 parents e67690e + 073512b commit 926fad4

File tree

7 files changed

+448
-118
lines changed

7 files changed

+448
-118
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import { describe, it, expect, vi, beforeEach } from 'vitest';
3+
import { AddTodo } from '../add-todo';
4+
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
5+
import type { ReactElement, ReactNode, ChangeEvent, FormEvent } from 'react';
6+
7+
// Create a stateful mock for the input field
8+
let testInputValue = '';
9+
10+
// Mock lucide-react icons
11+
vi.mock('lucide-react', () => ({
12+
Plus: () => null
13+
}));
14+
15+
// Mock the @lambdacurry/forms components
16+
interface TextFieldProps {
17+
name: string;
18+
placeholder: string;
19+
className: string;
20+
}
21+
22+
vi.mock('@lambdacurry/forms', () => ({
23+
TextField: ({ name, placeholder, className }: TextFieldProps) => (
24+
<input
25+
name={name}
26+
placeholder={placeholder}
27+
className={className}
28+
type="text"
29+
value={testInputValue}
30+
onChange={e => {
31+
testInputValue = e.target.value;
32+
}}
33+
/>
34+
),
35+
FormError: () => null
36+
}));
37+
38+
interface ButtonProps {
39+
children: ReactNode;
40+
onClick: () => void;
41+
type: 'button' | 'submit' | 'reset';
42+
}
43+
44+
vi.mock('@lambdacurry/forms/ui', () => ({
45+
Button: ({ children, onClick, type }: ButtonProps) => (
46+
<button type={type} onClick={onClick}>
47+
{children}
48+
</button>
49+
)
50+
}));
51+
52+
// Mock the remix-hook-form module
53+
interface RemixFormConfig {
54+
submitHandlers?: {
55+
onValid: (data: { text: string }) => void;
56+
};
57+
[key: string]: unknown;
58+
}
59+
60+
vi.mock('remix-hook-form', () => ({
61+
RemixFormProvider: ({ children }: { children: ReactNode }) => children,
62+
useRemixForm: (config: RemixFormConfig) => {
63+
return {
64+
...config,
65+
getValues: (_name: string) => testInputValue,
66+
reset: vi.fn(() => {
67+
testInputValue = '';
68+
// Force re-render by dispatching a custom event
69+
const inputs = document.querySelectorAll('input[name="text"]');
70+
inputs.forEach(input => {
71+
(input as HTMLInputElement).value = '';
72+
});
73+
}),
74+
setValue: vi.fn((_name: string, value: string) => {
75+
testInputValue = value;
76+
}),
77+
register: vi.fn((name: string) => ({
78+
name,
79+
onChange: (e: ChangeEvent<HTMLInputElement>) => {
80+
testInputValue = e.target.value;
81+
},
82+
value: testInputValue
83+
})),
84+
handleSubmit: vi.fn((onValid: (data: { text: string }) => void) => (e: FormEvent) => {
85+
e.preventDefault();
86+
if (testInputValue?.trim()) {
87+
onValid({ text: testInputValue.trim() });
88+
}
89+
}),
90+
formState: { errors: {} },
91+
watch: vi.fn((_name: string) => testInputValue)
92+
};
93+
}
94+
}));
95+
96+
function renderWithRouter(ui: ReactElement) {
97+
const router = createMemoryRouter([{ path: '/', element: ui }], { initialEntries: ['/'] });
98+
return render(<RouterProvider router={router} />);
99+
}
100+
101+
// hoist regex literals to top-level to satisfy biome's useTopLevelRegex
102+
const ADD_REGEX = /add/i;
103+
104+
describe('AddTodo', () => {
105+
beforeEach(() => {
106+
// Reset the test state before each test
107+
testInputValue = '';
108+
});
109+
110+
it('renders input and button', () => {
111+
const mockOnAdd = vi.fn();
112+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
113+
114+
expect(screen.getByPlaceholderText('Add a new todo...')).toBeInTheDocument();
115+
expect(screen.getByRole('button', { name: ADD_REGEX })).toBeInTheDocument();
116+
});
117+
118+
it('calls onAdd when form is submitted with text', () => {
119+
const mockOnAdd = vi.fn();
120+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
121+
122+
const input = screen.getByPlaceholderText('Add a new todo...');
123+
const button = screen.getByRole('button', { name: ADD_REGEX });
124+
125+
fireEvent.change(input, { target: { value: 'New todo' } });
126+
fireEvent.click(button);
127+
128+
expect(mockOnAdd).toHaveBeenCalledWith('New todo');
129+
});
130+
131+
it('clears input after adding todo', () => {
132+
const mockOnAdd = vi.fn();
133+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
134+
135+
const input = screen.getByPlaceholderText('Add a new todo...') as HTMLInputElement;
136+
const button = screen.getByRole('button', { name: ADD_REGEX });
137+
138+
fireEvent.change(input, { target: { value: 'New todo' } });
139+
fireEvent.click(button);
140+
141+
expect(input.value).toBe('');
142+
});
143+
144+
it('does not call onAdd with empty text', () => {
145+
const mockOnAdd = vi.fn();
146+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
147+
148+
const button = screen.getByRole('button', { name: ADD_REGEX });
149+
fireEvent.click(button);
150+
151+
expect(mockOnAdd).not.toHaveBeenCalled();
152+
});
153+
154+
it('trims whitespace from input', () => {
155+
const mockOnAdd = vi.fn();
156+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
157+
158+
const input = screen.getByPlaceholderText('Add a new todo...');
159+
const button = screen.getByRole('button', { name: ADD_REGEX });
160+
161+
fireEvent.change(input, { target: { value: ' New todo ' } });
162+
fireEvent.click(button);
163+
164+
expect(mockOnAdd).toHaveBeenCalledWith('New todo');
165+
});
166+
});

0 commit comments

Comments
 (0)