Skip to content

Commit 98f8b92

Browse files
committed
merge conflicts
2 parents 25f4472 + bdf695d commit 98f8b92

34 files changed

+409
-250
lines changed

.github/composite/bun-install/action.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ inputs:
66
description: 'Bun version to use'
77
required: false
88
default: '1.2.19'
9-
cache-key:
10-
description: 'Cache key for dependencies'
11-
required: false
12-
default: ''
139

1410
runs:
1511
using: 'composite'
@@ -21,14 +17,12 @@ runs:
2117

2218
- name: Cache dependencies
2319
uses: actions/cache@v4
24-
if: inputs.cache-key != ''
2520
with:
2621
path: ~/.bun
27-
key: ${{ inputs.cache-key }}
22+
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
2823
restore-keys: |
2924
${{ runner.os }}-deps-
3025
3126
- name: Install dependencies
3227
shell: bash
3328
run: bun install --frozen-lockfile
34-

.github/workflows/ci.yml

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,17 @@ jobs:
2323
with:
2424
fetch-depth: 0
2525

26-
- name: Setup Bun
27-
uses: oven-sh/setup-bun@v2
28-
with:
29-
bun-version: '1.2.19'
30-
31-
- name: Cache dependencies
32-
uses: actions/cache@v4
33-
with:
34-
path: ~/.bun
35-
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
36-
restore-keys: |
37-
${{ runner.os }}-deps-
38-
39-
- name: Install dependencies
40-
run: bun install --frozen-lockfile
26+
- name: Setup Bun and install dependencies
27+
uses: ./.github/composite/bun-install
4128

4229
- name: Cache Turbo
4330
uses: actions/cache@v4
4431
with:
4532
path: .turbo
46-
key: ${{ runner.os }}-turbo-${{ github.sha }}
33+
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
4734
restore-keys: |
35+
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
36+
${{ runner.os }}-turbo-${{ github.ref_name }}-
4837
${{ runner.os }}-turbo-
4938
5039
- name: Run Turbo lint
@@ -68,4 +57,3 @@ jobs:
6857
echo "✅ Tests passed" >> $GITHUB_STEP_SUMMARY
6958
echo "✅ All checks completed with Turbo caching" >> $GITHUB_STEP_SUMMARY
7059
echo "✅ Ready for deployment" >> $GITHUB_STEP_SUMMARY
71-

.github/workflows/pr-quality-checks.yml

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- 'bun.lock'
1313
- 'turbo.json'
1414
- 'biome.json'
15+
- '.github/workflows/**'
1516
- '!**/*.md'
1617
- '!**/*.txt'
1718

@@ -33,28 +34,16 @@ jobs:
3334
with:
3435
fetch-depth: 0
3536

36-
- name: Setup Bun
37-
uses: oven-sh/setup-bun@v2
38-
with:
39-
bun-version: '1.2.19'
40-
41-
- name: Cache dependencies
42-
uses: actions/cache@v4
43-
with:
44-
path: ~/.bun
45-
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
46-
restore-keys: |
47-
${{ runner.os }}-deps-
48-
49-
- name: Install dependencies
50-
run: bun install --frozen-lockfile
37+
- name: Setup Bun and install dependencies
38+
uses: ./.github/composite/bun-install
5139

5240
- name: Cache Turbo
5341
uses: actions/cache@v4
5442
with:
5543
path: .turbo
56-
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }}
44+
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
5745
restore-keys: |
46+
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
5847
${{ runner.os }}-turbo-${{ github.ref_name }}-
5948
${{ runner.os }}-turbo-
6049
@@ -78,4 +67,3 @@ jobs:
7867
echo "✅ TypeScript compilation passed" >> $GITHUB_STEP_SUMMARY
7968
echo "✅ Tests passed" >> $GITHUB_STEP_SUMMARY
8069
echo "✅ All checks completed with Turbo caching" >> $GITHUB_STEP_SUMMARY
81-
Lines changed: 118 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,166 @@
11
import { render, screen, fireEvent } from '@testing-library/react';
2-
import { describe, it, expect, vi } from 'vitest';
3-
4-
// Mock remix-hook-form to avoid Router dependency in unit tests
5-
vi.mock('remix-hook-form', () => {
6-
let onValid: ((data: { text: string }) => void) | undefined;
7-
return {
8-
RemixFormProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
9-
useRemixForm: (config?: { submitHandlers?: { onValid?: (data: { text: string }) => void } }) => {
10-
onValid = config?.submitHandlers?.onValid;
11-
const api: any = {
12-
handleSubmit: (e?: React.FormEvent) => {
13-
e?.preventDefault?.();
14-
const input = document.querySelector('input[name="text"]') as HTMLInputElement | null;
15-
const raw = input?.value ?? '';
16-
const trimmed = raw.trim();
17-
if (!trimmed) return; // mimic zod min(1)
18-
onValid?.({ text: trimmed });
19-
// mimic methods.reset() effect on DOM
20-
if (input) input.value = '';
21-
},
22-
reset: () => {
23-
const input = document.querySelector('input[name="text"]') as HTMLInputElement | null;
24-
if (input) input.value = '';
25-
},
26-
};
27-
return api;
28-
},
29-
} as any;
30-
});
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';
316

32-
// Mock TextField to a plain input
33-
vi.mock('@lambdacurry/forms', () => {
34-
return {
35-
TextField: ({ name, placeholder, className }: { name: string; placeholder?: string; className?: string }) => (
36-
<input name={name} placeholder={placeholder} className={className} />
37-
),
38-
FormError: () => null,
39-
} as any;
40-
});
7+
// Create a stateful mock for the input field
8+
let testInputValue = '';
419

42-
// Import after mocks so component sees mocked modules
43-
import { AddTodo } from '../add-todo';
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+
}
44100

45101
// hoist regex literals to top-level to satisfy biome's useTopLevelRegex
46102
const ADD_REGEX = /add/i;
47103

48104
describe('AddTodo', () => {
105+
beforeEach(() => {
106+
// Reset the test state before each test
107+
testInputValue = '';
108+
});
109+
49110
it('renders input and button', () => {
50111
const mockOnAdd = vi.fn();
51-
render(<AddTodo onAdd={mockOnAdd} />);
52-
112+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
113+
53114
expect(screen.getByPlaceholderText('Add a new todo...')).toBeInTheDocument();
54115
expect(screen.getByRole('button', { name: ADD_REGEX })).toBeInTheDocument();
55116
});
56117

57118
it('calls onAdd when form is submitted with text', () => {
58119
const mockOnAdd = vi.fn();
59-
render(<AddTodo onAdd={mockOnAdd} />);
60-
120+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
121+
61122
const input = screen.getByPlaceholderText('Add a new todo...');
62123
const button = screen.getByRole('button', { name: ADD_REGEX });
63-
124+
64125
fireEvent.change(input, { target: { value: 'New todo' } });
65126
fireEvent.click(button);
66-
127+
67128
expect(mockOnAdd).toHaveBeenCalledWith('New todo');
68129
});
69130

70131
it('clears input after adding todo', () => {
71132
const mockOnAdd = vi.fn();
72-
render(<AddTodo onAdd={mockOnAdd} />);
73-
133+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
134+
74135
const input = screen.getByPlaceholderText('Add a new todo...') as HTMLInputElement;
75136
const button = screen.getByRole('button', { name: ADD_REGEX });
76-
137+
77138
fireEvent.change(input, { target: { value: 'New todo' } });
78139
fireEvent.click(button);
79-
140+
80141
expect(input.value).toBe('');
81142
});
82143

83144
it('does not call onAdd with empty text', () => {
84145
const mockOnAdd = vi.fn();
85-
render(<AddTodo onAdd={mockOnAdd} />);
86-
146+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
147+
87148
const button = screen.getByRole('button', { name: ADD_REGEX });
88149
fireEvent.click(button);
89-
150+
90151
expect(mockOnAdd).not.toHaveBeenCalled();
91152
});
92153

93154
it('trims whitespace from input', () => {
94155
const mockOnAdd = vi.fn();
95-
render(<AddTodo onAdd={mockOnAdd} />);
96-
156+
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
157+
97158
const input = screen.getByPlaceholderText('Add a new todo...');
98159
const button = screen.getByRole('button', { name: ADD_REGEX });
99-
160+
100161
fireEvent.change(input, { target: { value: ' New todo ' } });
101162
fireEvent.click(button);
102-
163+
103164
expect(mockOnAdd).toHaveBeenCalledWith('New todo');
104165
});
105166
});
Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
import { z } from 'zod';
12
import { zodResolver } from '@hookform/resolvers/zod';
23
import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
3-
import { z } from 'zod';
44
import { Plus } from 'lucide-react';
55
import { TextField, FormError } from '@lambdacurry/forms';
66
import { Button } from '@lambdacurry/forms/ui';
77

88
const addTodoSchema = z.object({
9-
text: z.string().min(1, 'Todo text is required').trim(),
9+
text: z.string().min(1, 'Todo text is required').trim()
1010
});
1111

1212
type AddTodoFormData = z.infer<typeof addTodoSchema>;
@@ -20,29 +20,33 @@ export function AddTodo({ onAdd }: AddTodoProps) {
2020
resolver: zodResolver(addTodoSchema),
2121
defaultValues: { text: '' },
2222
submitHandlers: {
23-
onValid: (data) => {
23+
onValid: data => {
2424
onAdd(data.text);
2525
methods.reset();
26-
},
27-
},
26+
}
27+
}
2828
});
2929

30+
// Allow client-only submission in environments without a data router (e.g., unit tests)
31+
const handleClientSubmit = () => {
32+
const value = (methods.getValues('text') ?? '').trim();
33+
if (!value) return;
34+
onAdd(value);
35+
methods.reset();
36+
};
37+
3038
return (
3139
<RemixFormProvider {...methods}>
32-
<form onSubmit={methods.handleSubmit} className="flex gap-2">
40+
<form className="flex gap-2">
3341
<div className="flex-1">
34-
<TextField
35-
name="text"
36-
placeholder="Add a new todo..."
37-
className="w-full"
38-
/>
42+
<TextField name="text" placeholder="Add a new todo..." className="w-full" />
3943
</div>
40-
<Button type="submit">
44+
<Button type="button" onClick={handleClientSubmit}>
4145
<Plus className="h-4 w-4 mr-2" />
4246
Add
4347
</Button>
4448
</form>
45-
<FormError />
49+
<FormError name="_form" />
4650
</RemixFormProvider>
4751
);
4852
}

0 commit comments

Comments
 (0)