diff --git a/apps/web/src/features/routines/create/editor/routine-editor.test.tsx b/apps/web/src/features/routines/create/editor/routine-editor.test.tsx index edb455b..bea9163 100644 --- a/apps/web/src/features/routines/create/editor/routine-editor.test.tsx +++ b/apps/web/src/features/routines/create/editor/routine-editor.test.tsx @@ -35,6 +35,9 @@ describe('routine editor', () => { screen.getByRole('heading', { name: 'Exercises (0)' }), ).toBeVisible(); expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); + expect( + screen.queryByRole('button', { name: 'Preview' }), + ).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Add Exercise' })).toBeVisible(); expect(screen.getByRole('button', { name: 'Add schedule' })).toBeDisabled(); expect( @@ -68,6 +71,21 @@ describe('routine editor', () => { expect(routineNameInput).not.toHaveAttribute('aria-invalid'); }); + it('clears the draft and its validation state when requested', async () => { + const user = userEvent.setup(); + renderEditor(); + const routineNameInput = await screen.findByLabelText('Routine Name'); + + await user.type(routineNameInput, 'x'.repeat(ROUTINE_NAME_MAX_LENGTH + 1)); + expect(await screen.findByRole('alert')).toBeVisible(); + + await user.click(screen.getByRole('button', { name: 'Clear' })); + + expect(routineNameInput).toHaveValue(''); + expect(routineNameInput).not.toHaveAttribute('aria-invalid'); + expect(screen.queryByRole('alert')).toBeNull(); + }); + it('keeps the current draft and opens exercises when the name is invalid', async () => { const user = userEvent.setup(); const router = renderEditor(); @@ -89,29 +107,49 @@ describe('routine editor', () => { expect(await screen.findByRole('alert')).toBeVisible(); expect(screen.getByLabelText('Routine Name')).toHaveValue(invalidName); }); + + it('keeps the draft while visiting a different application page', async () => { + const user = userEvent.setup(); + const router = renderEditor(); + const invalidName = 'x'.repeat(ROUTINE_NAME_MAX_LENGTH + 1); + + await user.type(await screen.findByLabelText('Routine Name'), invalidName); + expect(await screen.findByRole('alert')).toBeVisible(); + + await router.navigate({ to: '/history' }); + expect( + await screen.findByRole('heading', { name: 'History' }), + ).toBeVisible(); + + await router.navigate({ to: '/routines/new' }); + expect(await screen.findByRole('alert')).toBeVisible(); + expect(screen.getByLabelText('Routine Name')).toHaveValue(invalidName); + }); }); function renderEditor() { - const rootRoute = createRootRoute(); - const routineCreationRoute = createRoute({ - getParentRoute: () => rootRoute, - id: 'routine-creation', - component: TestRoutineCreationLayout, - }); + const rootRoute = createRootRoute({ component: TestApplicationLayout }); const editorRoute = createRoute({ - getParentRoute: () => routineCreationRoute, + getParentRoute: () => rootRoute, path: '/routines/new', component: RoutineEditor, }); const exercisePickerRoute = createRoute({ - getParentRoute: () => routineCreationRoute, + getParentRoute: () => rootRoute, path: '/routines/new/exercises', component: () => Back to editor, }); + const historyRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/history', + component: () =>

History

, + }); const testRouter = createRouter({ history: createMemoryHistory({ initialEntries: ['/routines/new'] }), routeTree: rootRoute.addChildren([ - routineCreationRoute.addChildren([editorRoute, exercisePickerRoute]), + editorRoute, + exercisePickerRoute, + historyRoute, ]), }); @@ -120,7 +158,7 @@ function renderEditor() { return testRouter; } -function TestRoutineCreationLayout() { +function TestApplicationLayout() { return ( diff --git a/apps/web/src/features/routines/create/editor/routine-editor.tsx b/apps/web/src/features/routines/create/editor/routine-editor.tsx index 7140059..7fe17ff 100644 --- a/apps/web/src/features/routines/create/editor/routine-editor.tsx +++ b/apps/web/src/features/routines/create/editor/routine-editor.tsx @@ -14,6 +14,7 @@ import { RoutineScheduleField } from './routine-schedule-field'; export function RoutineEditor() { const navigate = useNavigate(); + const { reset } = useFormContext(); const addExercise = async () => { await navigate({ to: '/routines/new/exercises' }); @@ -21,7 +22,7 @@ export function RoutineEditor() { return (
- + reset()} />
@@ -35,7 +36,7 @@ export function RoutineEditor() { ); } -function EditorHeader() { +function EditorHeader({ onClear }: { onClear: () => void }) { return (
Create Routine
-
diff --git a/apps/web/src/routes/_app._routine-create/route.tsx b/apps/web/src/routes/_app._routine-create/route.tsx index fb57d35..7bc496b 100644 --- a/apps/web/src/routes/_app._routine-create/route.tsx +++ b/apps/web/src/routes/_app._routine-create/route.tsx @@ -1,16 +1,10 @@ -/** Keeps routine draft form state mounted across the complete creation flow. */ +/** Groups the routine-creation pages without adding a public URL segment. */ import { createFileRoute, Outlet } from '@tanstack/react-router'; -import { RoutineDraftFormProvider } from '../../features/routines/create/routine-draft-form'; - export const Route = createFileRoute('/_app/_routine-create')({ component: RoutineCreationLayout, }); function RoutineCreationLayout() { - return ( - - - - ); + return ; } diff --git a/apps/web/src/routes/_app.tsx b/apps/web/src/routes/_app.tsx index 036050d..e040987 100644 --- a/apps/web/src/routes/_app.tsx +++ b/apps/web/src/routes/_app.tsx @@ -6,6 +6,7 @@ import { ApplicationIdentityContext } from '../app/application-identity'; import { ApplicationShell } from '../app/application-shell'; import { clearGuestSession } from '../features/auth/guest-session'; import { useLogoutMutation } from '../features/auth/logout.mutation'; +import { RoutineDraftFormProvider } from '../features/routines/create/routine-draft-form'; export const Route = createFileRoute('/_app')({ component: ProtectedApplicationLayout, @@ -28,13 +29,15 @@ function ProtectedApplicationLayout() { {(identity) => ( - - - + + + + + )}