Skip to content

Commit b7e35e2

Browse files
committed
Fix controlled input transforms
1 parent fa9925b commit b7e35e2

6 files changed

Lines changed: 429 additions & 29 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ jobs:
3737
- name: Build Storybook
3838
run: yarn build-storybook
3939

40+
- name: Install Playwright browsers
41+
run: npx playwright install --with-deps chromium
42+
43+
- name: Run Storybook tests
44+
run: yarn workspace @lambdacurry/medusa-forms-docs test
45+
4046
- name: Lint and format check
4147
run: yarn format-and-lint
4248

@@ -47,4 +53,3 @@ jobs:
4753
name: test-failure
4854
path: apps/docs/storybook-static
4955
retention-days: 2
50-

apps/docs/src/medusa-forms/ControlledCurrencyInput.stories.tsx

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { zodResolver } from '@hookform/resolvers/zod';
22
import { ControlledCurrencyInput } from '@lambdacurry/medusa-forms/controlled/ControlledCurrencyInput';
33
import type { Meta, StoryObj } from '@storybook/react-vite';
4+
import { expect, userEvent, waitFor, within } from '@storybook/test';
5+
import { useEffect } from 'react';
46
import { FormProvider, useForm } from 'react-hook-form';
57
import { z } from 'zod';
68

@@ -21,7 +23,7 @@ export default meta;
2123
type Story = StoryObj<typeof meta>;
2224

2325
interface CurrencyFormData {
24-
price: string;
26+
price: string | number;
2527
}
2628

2729
// Base wrapper component for stories
@@ -61,6 +63,84 @@ const CurrencyInputWithHookForm = ({
6163
);
6264
};
6365

66+
const CurrencyInputWithStringState = () => {
67+
const form = useForm<CurrencyFormData>({
68+
defaultValues: { price: '' },
69+
});
70+
const price = form.watch('price');
71+
72+
return (
73+
<FormProvider {...form}>
74+
<div className="w-[400px] space-y-4">
75+
<ControlledCurrencyInput<CurrencyFormData> name="price" label="String price" symbol="$" code="usd" />
76+
<pre className="rounded bg-gray-100 p-2 text-xs">
77+
{JSON.stringify({ value: price, type: typeof price }, null, 2)}
78+
</pre>
79+
</div>
80+
</FormProvider>
81+
);
82+
};
83+
84+
const CurrencyInputWithRequiredError = () => {
85+
const form = useForm<CurrencyFormData>({
86+
defaultValues: { price: '' },
87+
mode: 'onChange',
88+
});
89+
90+
useEffect(() => {
91+
form.trigger('price').then(() => undefined);
92+
}, [form]);
93+
94+
return (
95+
<FormProvider {...form}>
96+
<div className="w-[400px]">
97+
<ControlledCurrencyInput
98+
name="price"
99+
label="Required price"
100+
symbol="$"
101+
code="usd"
102+
rules={{ required: 'Price is required' }}
103+
/>
104+
</div>
105+
</FormProvider>
106+
);
107+
};
108+
109+
const CurrencyInputWithValueAsNumber = () => {
110+
const form = useForm<CurrencyFormData>({
111+
defaultValues: { price: '' },
112+
});
113+
const price = form.watch('price');
114+
115+
return (
116+
<FormProvider {...form}>
117+
<div className="w-[400px] space-y-4">
118+
<ControlledCurrencyInput<CurrencyFormData>
119+
name="price"
120+
label="Numeric price"
121+
symbol="$"
122+
code="usd"
123+
rules={{ valueAsNumber: true }}
124+
/>
125+
<pre className="rounded bg-gray-100 p-2 text-xs">
126+
{JSON.stringify({ value: price, type: typeof price }, null, 2)}
127+
</pre>
128+
</div>
129+
</FormProvider>
130+
);
131+
};
132+
133+
const getStateOutput = (canvas: ReturnType<typeof within>) => canvas.getByText((content) => content.includes('"type"'));
134+
const getInputByName = (canvasElement: HTMLElement, name: string) => {
135+
const input = canvasElement.querySelector<HTMLInputElement>(`input[name="${name}"]`);
136+
137+
if (!input) {
138+
throw new Error(`Input with name "${name}" was not found.`);
139+
}
140+
141+
return input;
142+
};
143+
64144
// 1. Different Currency Symbols
65145
export const USDCurrency: Story = {
66146
args: {
@@ -194,6 +274,74 @@ export const RequiredFieldValidation: Story = {
194274
),
195275
};
196276

277+
export const RequiredRuleError: Story = {
278+
args: {
279+
name: 'price',
280+
symbol: '$',
281+
code: 'usd',
282+
},
283+
render: () => <CurrencyInputWithRequiredError />,
284+
play: async ({ canvasElement }) => {
285+
const canvas = within(canvasElement);
286+
287+
await waitFor(() => {
288+
expect(canvas.getByText('Price is required')).toBeInTheDocument();
289+
});
290+
},
291+
};
292+
293+
export const DefaultStringValue: Story = {
294+
args: {
295+
name: 'price',
296+
symbol: '$',
297+
code: 'usd',
298+
},
299+
render: () => <CurrencyInputWithStringState />,
300+
play: async ({ canvasElement }) => {
301+
const canvas = within(canvasElement);
302+
const input = getInputByName(canvasElement, 'price');
303+
const state = getStateOutput(canvas);
304+
305+
await userEvent.type(input, '1234');
306+
307+
await waitFor(() => {
308+
expect(input.value).toContain('1,234');
309+
expect(state).toHaveTextContent('"value": "1234"');
310+
expect(state).toHaveTextContent('"type": "string"');
311+
});
312+
},
313+
};
314+
315+
export const ValueAsNumber: Story = {
316+
args: {
317+
name: 'price',
318+
symbol: '$',
319+
code: 'usd',
320+
},
321+
render: () => <CurrencyInputWithValueAsNumber />,
322+
play: async ({ canvasElement }) => {
323+
const canvas = within(canvasElement);
324+
const input = getInputByName(canvasElement, 'price');
325+
const state = getStateOutput(canvas);
326+
327+
await userEvent.type(input, '1234');
328+
329+
await waitFor(() => {
330+
expect(input.value).toContain('1,234');
331+
expect(state).toHaveTextContent('"value": 1234');
332+
expect(state).toHaveTextContent('"type": "number"');
333+
});
334+
335+
await userEvent.clear(input);
336+
337+
await waitFor(() => {
338+
expect(input.value).toBe('');
339+
expect(state).toHaveTextContent('"value": null');
340+
expect(state).toHaveTextContent('"type": "number"');
341+
});
342+
},
343+
};
344+
197345
const customValidationSchema = z.object({
198346
price: z.string().refine((val) => {
199347
const num = Number.parseFloat(val);

0 commit comments

Comments
 (0)