Skip to content

Commit da5b08c

Browse files
committed
Added a storybook story to test the remix textarea component
1 parent 63a4eee commit da5b08c

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

.changeset/great-bears-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lambdacurry/forms-docs": minor
3+
---
4+
5+
Added the remix text area story
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { zodResolver } from '@hookform/resolvers/zod';
2+
import type { ActionFunctionArgs } from '@remix-run/node';
3+
import { useFetcher } from '@remix-run/react';
4+
import type { Meta, StoryContext, StoryObj } from '@storybook/react';
5+
import { expect, userEvent, within } from '@storybook/test';
6+
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
7+
import { z } from 'zod';
8+
import { withRemixStubDecorator } from '../lib/storybook/remix-stub';
9+
import { RemixTextarea } from '@lambdacurry/forms/remix/remix-textarea';
10+
import { Button } from '@lambdacurry/forms/ui/button';
11+
12+
const formSchema = z.object({
13+
comment: z.string().min(10, 'Comment must be at least 10 characters'),
14+
});
15+
16+
type FormData = z.infer<typeof formSchema>;
17+
18+
const INITIAL_COMMENT = 'Initial comment text';
19+
const BLOCKED_CONTENT = 'blocked_content';
20+
const BLOCKED_CONTENT_ERROR = 'This content is not allowed';
21+
22+
const ControlledTextareaExample = () => {
23+
const fetcher = useFetcher<{ message: string }>();
24+
const methods = useRemixForm<FormData>({
25+
resolver: zodResolver(formSchema),
26+
defaultValues: {
27+
comment: INITIAL_COMMENT,
28+
},
29+
fetcher,
30+
});
31+
32+
return (
33+
<RemixFormProvider {...methods}>
34+
<fetcher.Form onSubmit={methods.handleSubmit} method="post" action="/">
35+
<RemixTextarea name="comment" label="Comment" description="Enter your comment (minimum 10 characters)" />
36+
<Button type="submit" className="mt-4">
37+
Submit
38+
</Button>
39+
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
40+
{methods.formState.errors.comment && (
41+
<p className="mt-2 text-red-600">{methods.formState.errors.comment.message}</p>
42+
)}
43+
</fetcher.Form>
44+
</RemixFormProvider>
45+
);
46+
};
47+
48+
// Action function for form submission
49+
const handleFormSubmission = async (request: Request) => {
50+
const {
51+
errors,
52+
data,
53+
receivedValues: defaultValues,
54+
} = await getValidatedFormData<FormData>(request, zodResolver(formSchema));
55+
56+
if (errors) {
57+
return { errors, defaultValues };
58+
}
59+
60+
if (data.comment.includes(BLOCKED_CONTENT)) {
61+
return {
62+
errors: {
63+
comment: {
64+
type: 'manual',
65+
message: BLOCKED_CONTENT_ERROR,
66+
},
67+
},
68+
defaultValues,
69+
};
70+
}
71+
72+
return { message: 'Comment submitted successfully' };
73+
};
74+
75+
// Storybook configuration
76+
const meta: Meta<typeof RemixTextarea> = {
77+
title: 'Remix/RemixTextarea',
78+
component: RemixTextarea,
79+
parameters: { layout: 'centered' },
80+
tags: ['autodocs'],
81+
decorators: [
82+
withRemixStubDecorator([
83+
{
84+
path: '/',
85+
Component: ControlledTextareaExample,
86+
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
87+
},
88+
]),
89+
],
90+
} satisfies Meta<typeof RemixTextarea>;
91+
92+
export default meta;
93+
type Story = StoryObj<typeof meta>;
94+
95+
// Test scenarios
96+
const testDefaultValues = ({ canvas }: StoryContext) => {
97+
const textarea = canvas.getByRole('textbox');
98+
expect(textarea).toHaveValue(INITIAL_COMMENT);
99+
};
100+
101+
const testInvalidSubmission = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
102+
const canvas = within(canvasElement);
103+
const textarea = canvas.getByRole('textbox');
104+
const submitButton = canvas.getByRole('button', { name: 'Submit' });
105+
106+
await userEvent.click(textarea);
107+
await userEvent.clear(textarea);
108+
await userEvent.type(textarea, 'short');
109+
await userEvent.click(submitButton);
110+
111+
expect(canvas.getByText((content) => content.includes('Comment must be at least 10 characters'))).toBeInTheDocument();
112+
};
113+
114+
const testBlockedContent = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
115+
const canvas = within(canvasElement);
116+
const textarea = canvas.getByRole('textbox');
117+
const submitButton = canvas.getByRole('button', { name: 'Submit' });
118+
119+
// First verify we can interact with the form
120+
await userEvent.click(textarea);
121+
await userEvent.clear(textarea);
122+
const testText = `This is a ${BLOCKED_CONTENT} test`;
123+
await userEvent.type(textarea, testText);
124+
125+
// Verify the text was entered
126+
expect(textarea).toHaveValue(testText);
127+
128+
// Submit and wait for response
129+
await userEvent.click(submitButton);
130+
131+
// Debug the form submission
132+
console.log('Form submitted with blocked content');
133+
134+
// Wait for any state updates
135+
await new Promise((resolve) => setTimeout(resolve, 100));
136+
137+
console.log('After submission DOM:', canvasElement.innerHTML);
138+
139+
// Check if the error is in the DOM
140+
const errorElements = canvas.queryAllByText((content, element) => {
141+
console.log('Found element with content:', content);
142+
return content.includes(BLOCKED_CONTENT_ERROR);
143+
});
144+
145+
expect(errorElements.length).toBeGreaterThan(0);
146+
};
147+
148+
const testValidSubmission = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
149+
const canvas = within(canvasElement);
150+
const textarea = canvas.getByRole('textbox');
151+
const submitButton = canvas.getByRole('button', { name: 'Submit' });
152+
153+
await userEvent.click(textarea);
154+
await userEvent.clear(textarea);
155+
await userEvent.type(textarea, 'This is a valid comment that is long enough');
156+
await userEvent.click(submitButton);
157+
158+
// Check for success message
159+
await expect(canvas.getByText('Comment submitted successfully')).toBeInTheDocument();
160+
};
161+
162+
// Stories
163+
export const Tests: Story = {
164+
play: async (context) => {
165+
testDefaultValues(context);
166+
await testInvalidSubmission(context);
167+
await testBlockedContent(context);
168+
await testValidSubmission(context);
169+
},
170+
};

0 commit comments

Comments
 (0)