Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand All @@ -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: () => <Link to="/routines/new">Back to editor</Link>,
});
const historyRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/history',
component: () => <h1>History</h1>,
});
const testRouter = createRouter({
history: createMemoryHistory({ initialEntries: ['/routines/new'] }),
routeTree: rootRoute.addChildren([
routineCreationRoute.addChildren([editorRoute, exercisePickerRoute]),
editorRoute,
exercisePickerRoute,
historyRoute,
]),
});

Expand All @@ -120,7 +158,7 @@ function renderEditor() {
return testRouter;
}

function TestRoutineCreationLayout() {
function TestApplicationLayout() {
return (
<RoutineDraftFormProvider>
<Outlet />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import { RoutineScheduleField } from './routine-schedule-field';

export function RoutineEditor() {
const navigate = useNavigate();
const { reset } = useFormContext<RoutineDraftFormValues>();

const addExercise = async () => {
await navigate({ to: '/routines/new/exercises' });
};

return (
<section className="mx-auto w-full max-w-5xl">
<EditorHeader />
<EditorHeader onClear={() => reset()} />

<form className="grid gap-6 lg:grid-cols-2">
<div className="space-y-6">
Expand All @@ -35,7 +36,7 @@ export function RoutineEditor() {
);
}

function EditorHeader() {
function EditorHeader({ onClear }: { onClear: () => void }) {
return (
<header className="mb-7 flex items-center gap-3 border-b pb-5">
<Button aria-label="Back to routines" asChild size="icon" variant="ghost">
Expand All @@ -45,8 +46,8 @@ function EditorHeader() {
</Button>
<PageTitle>Create Routine</PageTitle>
<div className="ml-auto flex gap-2">
<Button className="hidden sm:inline-flex" disabled variant="outline">
Preview
<Button onClick={onClear} type="button" variant="ghost">
Clear
</Button>
<Button disabled>Save</Button>
</div>
Expand Down
10 changes: 2 additions & 8 deletions apps/web/src/routes/_app._routine-create/route.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<RoutineDraftFormProvider>
<Outlet />
</RoutineDraftFormProvider>
);
return <Outlet />;
}
17 changes: 10 additions & 7 deletions apps/web/src/routes/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,13 +29,15 @@ function ProtectedApplicationLayout() {
<ApplicationAccessBoundary>
{(identity) => (
<ApplicationIdentityContext.Provider value={identity}>
<ApplicationShell
identity={identity}
onExitGuest={identity.kind === 'guest' ? exitGuest : undefined}
onLogout={identity.kind === 'user' ? logoutUser : undefined}
>
<Outlet />
</ApplicationShell>
<RoutineDraftFormProvider>
<ApplicationShell
identity={identity}
onExitGuest={identity.kind === 'guest' ? exitGuest : undefined}
onLogout={identity.kind === 'user' ? logoutUser : undefined}
>
<Outlet />
</ApplicationShell>
</RoutineDraftFormProvider>
</ApplicationIdentityContext.Provider>
)}
</ApplicationAccessBoundary>
Expand Down