Skip to content

Commit ab7dde3

Browse files
authored
Merge pull request #148 from lambda-curry/codegen/lc-324-researcher-test
2 parents 42f6659 + ac82256 commit ab7dde3

26 files changed

+841
-272
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: PR Quality Checks
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'apps/**'
7+
- 'packages/**'
8+
- '.github/workflows/**'
9+
- '*.json'
10+
- '*.js'
11+
- '*.ts'
12+
- '*.tsx'
13+
- 'yarn.lock'
14+
- 'turbo.json'
15+
- 'biome.json'
16+
- '!**/*.md'
17+
- '!**/*.txt'
18+
workflow_dispatch:
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
quality-checks:
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 15
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- name: Setup Node.js
35+
uses: actions/setup-node@v3
36+
with:
37+
node-version: '22.9.0'
38+
39+
- name: Setup Yarn Corepack
40+
run: corepack enable
41+
42+
- name: Install dependencies
43+
run: yarn install
44+
45+
- name: Cache Turbo
46+
uses: actions/cache@v4
47+
with:
48+
path: .turbo
49+
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/yarn.lock') }}-${{ github.sha }}
50+
restore-keys: |
51+
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/yarn.lock') }}-
52+
${{ runner.os }}-turbo-${{ github.ref_name }}-
53+
${{ runner.os }}-turbo-
54+
55+
- name: Run Turbo lint
56+
run: yarn turbo run lint
57+
58+
- name: Run Turbo typecheck
59+
run: yarn turbo run type-check --filter=@lambdacurry/forms
60+
61+
- name: Summary
62+
run: |
63+
echo "## PR Quality Checks Summary" >> $GITHUB_STEP_SUMMARY
64+
echo "✅ Linting passed (Biome)" >> $GITHUB_STEP_SUMMARY
65+
echo "✅ TypeScript compilation passed" >> $GITHUB_STEP_SUMMARY
66+
echo "✅ All checks completed with Turbo caching" >> $GITHUB_STEP_SUMMARY

.vscode/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"Bazza",
55
"biomejs",
66
"cleanbuild",
7+
"cmdk",
78
"Filenaming",
89
"hookform",
910
"isbot",
@@ -23,5 +24,8 @@
2324
"source.fixAll.biome": "explicit",
2425
"source.organizeImports.biome": "explicit"
2526
},
26-
"tailwindCSS.classAttributes": ["class", "className", "ngClass", "class:list", "wrapperClassName"]
27+
"tailwindCSS.classAttributes": ["class", "className", "ngClass", "class:list", "wrapperClassName"],
28+
"[jsonc]": {
29+
"editor.defaultFormatter": "biomejs.biome"
30+
}
2731
}

apps/docs/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"storybook": "storybook dev -p 6006",
99
"serve": "http-server ./storybook-static -p 6006 -s",
1010
"test": "start-server-and-test serve http://127.0.0.1:6006 'test-storybook --url http://127.0.0.1:6006'",
11-
"test:local": "test-storybook"
11+
"test:local": "test-storybook",
12+
"type-check": "tsc -p tsconfig.json --noEmit"
1213
},
1314
"dependencies": {
1415
"@hookform/error-message": "^2.0.0",
@@ -30,6 +31,9 @@
3031
"@storybook/testing-library": "^0.2.2",
3132
"@tailwindcss/postcss": "^4.1.8",
3233
"@tailwindcss/vite": "^4.0.0",
34+
"@testing-library/jest-dom": "^6.8.0",
35+
"@testing-library/react": "^16.3.0",
36+
"@types/jest": "^30.0.0",
3337
"@types/react": "^19.0.0",
3438
"@typescript-eslint/eslint-plugin": "^6.21.0",
3539
"@typescript-eslint/parser": "^6.21.0",

apps/docs/src/examples/middleware-example.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@ export const action = async ({ context }: ActionFunctionArgs) => {
3333

3434
// Component
3535
export default function MiddlewareExample() {
36-
const {
37-
handleSubmit,
38-
formState: { errors },
39-
register,
40-
} = useRemixForm<FormData>({
36+
const methods = useRemixForm<FormData>({
4137
mode: 'onSubmit',
4238
resolver,
4339
});
@@ -46,12 +42,12 @@ export default function MiddlewareExample() {
4642
<div className="p-4">
4743
<h1 className="text-2xl font-bold mb-4">Remix Hook Form v7 Middleware Example</h1>
4844

49-
<RemixFormProvider>
50-
<Form method="POST" onSubmit={handleSubmit}>
45+
<RemixFormProvider {...methods}>
46+
<Form method="POST" onSubmit={methods.handleSubmit}>
5147
<div className="space-y-4">
52-
<TextField label="Name" {...register('name')} error={errors.name?.message} />
48+
<TextField name="name" label="Name" />
5349

54-
<TextField label="Email" type="email" {...register('email')} error={errors.email?.message} />
50+
<TextField name="email" type="email" label="Email" />
5551

5652
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
5753
Submit

apps/docs/src/remix-hook-form/checkbox-list.stories.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Checkbox } from '@lambdacurry/forms/remix-hook-form/checkbox';
33
import { FormMessage } from '@lambdacurry/forms/remix-hook-form/form';
44
import { Button } from '@lambdacurry/forms/ui/button';
55
import type { Meta, StoryObj } from '@storybook/react-vite';
6-
import { expect, userEvent, type within } from '@storybook/test';
6+
import { expect, userEvent, within } from '@storybook/test';
77
import { type ActionFunctionArgs, Form, useFetcher } from 'react-router';
88
import { createFormData, getValidatedFormData, RemixFormProvider, useRemixForm } from 'remix-hook-form';
99
import { z } from 'zod';
@@ -137,18 +137,18 @@ const meta: Meta<typeof Checkbox> = {
137137
export default meta;
138138
type Story = StoryObj<typeof meta>;
139139

140-
interface StoryContext {
141-
canvas: ReturnType<typeof within>;
142-
}
140+
type StoryContext = { canvasElement: HTMLElement };
143141

144-
const testDefaultValues = ({ canvas }: StoryContext) => {
142+
const testDefaultValues = ({ canvasElement }: StoryContext) => {
143+
const canvas = within(canvasElement);
145144
AVAILABLE_COLORS.forEach(({ label }) => {
146145
const checkbox = canvas.getByLabelText(label);
147146
expect(checkbox).not.toBeChecked();
148147
});
149148
};
150149

151-
const testErrorState = async ({ canvas }: StoryContext) => {
150+
const testErrorState = async ({ canvasElement }: StoryContext) => {
151+
const canvas = within(canvasElement);
152152
// Submit form without selecting any colors
153153
const submitButton = canvas.getByRole('button', { name: 'Submit' });
154154
await userEvent.click(submitButton);
@@ -157,7 +157,8 @@ const testErrorState = async ({ canvas }: StoryContext) => {
157157
await expect(await canvas.findByText('Please select at least one color')).toBeInTheDocument();
158158
};
159159

160-
const testColorSelection = async ({ canvas }: StoryContext) => {
160+
const testColorSelection = async ({ canvasElement }: StoryContext) => {
161+
const canvas = within(canvasElement);
161162
// Select two colors
162163
const redCheckbox = canvas.getByLabelText('Red');
163164
const blueCheckbox = canvas.getByLabelText('Blue');

apps/docs/src/remix-hook-form/data-table/data-table-filter-accessibility-tests.stories.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useDataTableFilters } from '@lambdacurry/forms/ui/data-table-filter/hoo
44
import { useFilterSync } from '@lambdacurry/forms/ui/utils/use-filter-sync';
55
import { CheckCircledIcon, PersonIcon, StarIcon, TextIcon } from '@radix-ui/react-icons';
66
import type { Meta, StoryContext, StoryObj } from '@storybook/react';
7-
import { expect } from '@storybook/test';
7+
import { expect, within } from '@storybook/test';
88
import { withReactRouterStubDecorator } from '../../lib/storybook/react-router-stub';
99

1010
/**
@@ -228,15 +228,17 @@ type Story = StoryObj<typeof meta>;
228228
/**
229229
* Test functions for accessibility testing
230230
*/
231-
const testBasicRendering = ({ canvas }: StoryContext) => {
231+
const testBasicRendering = ({ canvasElement }: StoryContext) => {
232+
const canvas = within(canvasElement);
232233
const title = canvas.getByText('Data Table Filter Accessibility Test');
233234
expect(title).toBeInTheDocument();
234235

235236
const filterInterface = canvas.getByText('Filter Interface');
236237
expect(filterInterface).toBeInTheDocument();
237238
};
238239

239-
const testKeyboardNavigation = async ({ canvas }: StoryContext) => {
240+
const testKeyboardNavigation = async ({ canvasElement }: StoryContext) => {
241+
const canvas = within(canvasElement);
240242
// Look for filter-related buttons or elements
241243
const buttons = canvas.getAllByRole('button');
242244
await expect(buttons.length).toBeGreaterThan(0);
@@ -248,7 +250,8 @@ const testKeyboardNavigation = async ({ canvas }: StoryContext) => {
248250
}
249251
};
250252

251-
const testAriaAttributes = async ({ canvas }: StoryContext) => {
253+
const testAriaAttributes = async ({ canvasElement }: StoryContext) => {
254+
const canvas = within(canvasElement);
252255
// Test that interactive elements have proper roles
253256
const buttons = canvas.getAllByRole('button');
254257
expect(buttons.length).toBeGreaterThan(0);

apps/docs/src/remix-hook-form/data-table/data-table-router-form.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ export default meta;
302302
type Story = StoryObj<typeof meta>;
303303

304304
export const Default: Story = {
305-
args: {} satisfies Record<string, unknown>, // Args for DataTableRouterForm if needed, handled by Example component
305+
// biome-ignore lint/suspicious/noExplicitAny: any for flexibility
306+
args: {} as any,
306307
render: () => <DataTableRouterFormExample />,
307308
parameters: {
308309
docs: {

apps/docs/src/remix-hook-form/form-error.test.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { render, screen } from '@testing-library/react';
55
import { useFetcher } from 'react-router';
66
import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
77
import { z } from 'zod';
8-
import type { ElementType, PropsWithChildren } from 'react';
8+
import type { ElementType } from 'react';
9+
import type { FetcherWithComponents } from 'react-router';
10+
import type { FormMessageProps } from '@lambdacurry/forms/ui/form';
911

1012
// Mock useFetcher
1113
jest.mock('react-router', () => ({
@@ -31,15 +33,15 @@ const TestFormWithError = ({
3133
}: {
3234
initialErrors?: Record<string, { message: string }>;
3335
formErrorName?: string;
34-
customComponents?: { FormMessage?: React.ComponentType<PropsWithChildren<Record<string, unknown>>> };
36+
customComponents?: { FormMessage?: React.ComponentType<FormMessageProps> };
3537
className?: string;
3638
}) => {
3739
const mockFetcher = {
3840
data: { errors: initialErrors },
3941
state: 'idle' as const,
4042
submit: jest.fn(),
4143
Form: 'form' as ElementType,
42-
};
44+
} as unknown as FetcherWithComponents<unknown>;
4345

4446
mockUseFetcher.mockReturnValue(mockFetcher);
4547

@@ -143,9 +145,9 @@ describe('FormError Component', () => {
143145

144146
describe('Component Customization', () => {
145147
it('uses custom FormMessage component when provided', () => {
146-
const CustomFormMessage = ({ children, ...props }: PropsWithChildren<Record<string, unknown>>) => (
148+
const CustomFormMessage = (props: FormMessageProps) => (
147149
<div data-testid="custom-form-message" className="custom-message" {...props}>
148-
Custom: {children}
150+
Custom: {props.children}
149151
</div>
150152
);
151153

@@ -218,7 +220,7 @@ describe('FormError Component', () => {
218220
state: 'idle' as const,
219221
submit: jest.fn(),
220222
Form: 'form' as ElementType,
221-
};
223+
} as unknown as FetcherWithComponents<unknown>;
222224

223225
mockUseFetcher.mockReturnValue(mockFetcher);
224226

@@ -315,9 +317,9 @@ describe('FormError Component', () => {
315317
it('does not re-render unnecessarily when unrelated form state changes', () => {
316318
const renderSpy = jest.fn();
317319

318-
const CustomFormMessage = ({ children, ...props }: PropsWithChildren<Record<string, unknown>>) => {
320+
const CustomFormMessage = (props: FormMessageProps) => {
319321
renderSpy();
320-
return <div {...props}>{children}</div>;
322+
return <div {...props}>{props.children}</div>;
321323
};
322324

323325
const errors = {
@@ -346,7 +348,7 @@ describe('FormError Integration Tests', () => {
346348
state: 'idle' as const,
347349
submit: jest.fn(),
348350
Form: 'form' as ElementType,
349-
};
351+
} as unknown as FetcherWithComponents<unknown>;
350352

351353
mockUseFetcher.mockReturnValue(mockFetcher);
352354

apps/docs/src/remix-hook-form/password-field.stories.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
22
import { PasswordField } from '@lambdacurry/forms/remix-hook-form/password-field';
33
import { Button } from '@lambdacurry/forms/ui/button';
44
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
5-
import { expect, userEvent } from '@storybook/test';
5+
import { expect, userEvent, within } from '@storybook/test';
66
import { useRef } from 'react';
77
import { type ActionFunctionArgs, useFetcher } from 'react-router';
88
import { getValidatedFormData, RemixFormProvider, useRemixForm } from 'remix-hook-form';
@@ -114,14 +114,16 @@ export default meta;
114114
type Story = StoryObj<typeof meta>;
115115

116116
// Test scenarios
117-
const testDefaultValues = ({ canvas }: StoryContext) => {
117+
const testDefaultValues = ({ canvasElement }: StoryContext) => {
118+
const canvas = within(canvasElement);
118119
const passwordInput = canvas.getByLabelText('Password');
119120
const confirmInput = canvas.getByLabelText('Confirm Password');
120121
expect(passwordInput).toHaveValue(INITIAL_PASSWORD);
121122
expect(confirmInput).toHaveValue(INITIAL_PASSWORD);
122123
};
123124

124-
const testPasswordVisibilityToggle = async ({ canvas }: StoryContext) => {
125+
const testPasswordVisibilityToggle = async ({ canvasElement }: StoryContext) => {
126+
const canvas = within(canvasElement);
125127
const passwordInput = canvas.getByLabelText('Password');
126128

127129
// Find the toggle button within the same form item as the password input
@@ -150,7 +152,8 @@ const testPasswordVisibilityToggle = async ({ canvas }: StoryContext) => {
150152
expect(showButtonAgain).toBeInTheDocument();
151153
};
152154

153-
const testWeakPasswordValidation = async ({ canvas }: StoryContext) => {
155+
const testWeakPasswordValidation = async ({ canvasElement }: StoryContext) => {
156+
const canvas = within(canvasElement);
154157
const passwordInput = canvas.getByLabelText('Password');
155158
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
156159

@@ -162,7 +165,8 @@ const testWeakPasswordValidation = async ({ canvas }: StoryContext) => {
162165
await expect(await canvas.findByText(WEAK_PASSWORD_ERROR)).toBeInTheDocument();
163166
};
164167

165-
const testPasswordMismatchValidation = async ({ canvas }: StoryContext) => {
168+
const testPasswordMismatchValidation = async ({ canvasElement }: StoryContext) => {
169+
const canvas = within(canvasElement);
166170
const passwordInput = canvas.getByLabelText('Password');
167171
const confirmInput = canvas.getByLabelText('Confirm Password');
168172
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
@@ -180,7 +184,8 @@ const testPasswordMismatchValidation = async ({ canvas }: StoryContext) => {
180184
await expect(await canvas.findByText(MISMATCH_PASSWORD_ERROR)).toBeInTheDocument();
181185
};
182186

183-
const testValidSubmission = async ({ canvas }: StoryContext) => {
187+
const testValidSubmission = async ({ canvasElement }: StoryContext) => {
188+
const canvas = within(canvasElement);
184189
const passwordInput = canvas.getByLabelText('Password');
185190
const confirmInput = canvas.getByLabelText('Confirm Password');
186191
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
@@ -199,7 +204,8 @@ const testValidSubmission = async ({ canvas }: StoryContext) => {
199204
expect(successMessage).toBeInTheDocument();
200205
};
201206

202-
const testRefFunctionality = async ({ canvas }: StoryContext) => {
207+
const testRefFunctionality = async ({ canvasElement }: StoryContext) => {
208+
const canvas = within(canvasElement);
203209
const refInput = canvas.getByLabelText('Ref Example');
204210
const focusButton = canvas.getByRole('button', { name: 'Focus' });
205211

0 commit comments

Comments
 (0)