diff --git a/web/frontend/app/features/auth/components/AuthSplitLayout.stories.tsx b/web/frontend/app/features/auth/components/AuthSplitLayout.stories.tsx new file mode 100644 index 00000000..8e54f8c2 --- /dev/null +++ b/web/frontend/app/features/auth/components/AuthSplitLayout.stories.tsx @@ -0,0 +1,151 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useForm } from 'react-hook-form'; +import { expect, fireEvent, userEvent, within } from 'storybook/test'; +import AuthCardHeading from './AuthCardHeading'; +import AuthErrorAlert from './AuthErrorAlert'; +import AuthSplitLayout from './AuthSplitLayout'; +import AuthSwitchPrompt from './AuthSwitchPrompt'; +import LoginForm from './LoginForm'; +import RegisterForm from './RegisterForm'; +import RegisterStepProgress from './RegisterStepProgress'; +import { useRegisterWizard } from '../hooks/useRegisterWizard'; +import { loginSchema, type LoginSchema } from '../schemas'; + +const noop = () => undefined; + +const authSplitLayoutDescription = `The shared split-screen shell behind /login and /register: a full-bleed bar photo carrying the hero headline, and a solid card holding the page's own heading, form, and switch prompt. The grid splits 6fr/5fr from 820px up and stacks hero over card below it. Both stories compose the real forms, so they show the production layout at any viewport.`; + +const LoginPanel = ({ error }: { error?: string }) => { + const form = useForm({ resolver: zodResolver(loginSchema) }); + + return ( + <> + + + {error ? : null} + + + + + + ); +}; + +const RegisterPanel = () => { + const wizard = useRegisterWizard(noop); + + return ( + <> + + + + + + + + + ); +}; + +const meta = { + title: 'Layout/AuthSplitLayout', + component: AuthSplitLayout, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: authSplitLayoutDescription, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const loginArgs = { + headline: 'Good to see you again.', + blurb: 'Sign in to pick up where you left off with our partner breweries.', +}; + +const registerArgs = { + headline: 'Pull up a chair.', + blurb: 'Create an account to explore partner breweries and their beers.', +}; + +export const Login: Story = { + args: { + ...loginArgs, + children: , + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole('heading', { name: 'Sign in' })).toBeInTheDocument(); + await expect(canvas.getByRole('button', { name: /show password/i })).toBeInTheDocument(); + }, +}; + +export const LoginWithServerError: Story = { + args: { + ...loginArgs, + children: , + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole('alert')).toHaveTextContent('Invalid username or password.'); + }, +}; + +export const Register: Story = { + args: { + ...registerArgs, + children: , + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByText(/step 1 of 3 ยท account/i)).toBeInTheDocument(); + await expect(canvas.getByRole('button', { name: /continue/i })).toBeInTheDocument(); + }, +}; + +export const RegisterFinalStep: Story = { + args: Register.args, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(canvas.getByLabelText(/^username$/i), 'jane_doe'); + await userEvent.type(canvas.getByLabelText(/^email$/i), 'jane@example.com'); + await userEvent.click(canvas.getByRole('button', { name: /continue/i })); + + await userEvent.type(await canvas.findByLabelText(/first name/i), 'Jane'); + await userEvent.type(canvas.getByLabelText(/last name/i), 'Doe'); + await fireEvent.change(canvas.getByLabelText(/date of birth/i), { + target: { value: '2000-01-15' }, + }); + await userEvent.click(canvas.getByRole('button', { name: /continue/i })); + + await expect( + await canvas.findByRole('heading', { name: 'Secure your account' }), + ).toBeInTheDocument(); + await expect(canvas.getByRole('button', { name: /create account/i })).toBeInTheDocument(); + }, +}; + +export const RegisterMobile: Story = { + args: Register.args, + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +};