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
45 changes: 45 additions & 0 deletions packages/plugin-form/src/WizardForm.regression.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
* 2. the create-mode seeding effect reset `formData` to `{}` on re-render.
* The buttons now submit the inner form natively (`type="submit"` + `form={id}`)
* and the create seed is idempotent.
*
* What each case here actually pins down — the first two overlap less than they
* look, so don't read either as covering the whole merge:
* - "submits the merged payload" is an END-TO-END outcome check. Two redundant
* mechanisms carry values across steps (react-hook-form keeps unmounted
* fields, since `shouldUnregister` is false; and the wizard merges
* `{...formData, ...stepData}`), and breaking EITHER ONE ALONE still passes.
* It catches a total failure to accumulate, not a broken merge.
* - "carries earlier steps through a field-less review step" is what pins the
* merge itself: a step with no inputs has no react-hook-form instance to
* retain anything, so `formData` is the only carrier left.
* - "wires the footer Next/Create buttons" pins the native-submit wiring.
*/
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
Expand Down Expand Up @@ -90,6 +102,39 @@ describe('WizardForm — multi-step create collects every step', () => {
expect(payload).toEqual(expect.objectContaining({ name: 'Alice', note: 'hello' }));
});

it('carries earlier steps through a field-less review step (and can finish it)', async () => {
const ds = makeDS();
// A final review/confirm step with no inputs — the shape this component's
// own usage example ends on. It renders no react-hook-form instance, so
// it is the one path where the wizard's own `formData` is the ONLY thing
// carrying the earlier steps.
const reviewSchema = {
...schema,
sections: [{ label: 'Step 1', fields: ['name'] }, { label: 'Review', fields: [] }],
};
const { container } = render(<WizardForm schema={reviewSchema as any} dataSource={ds as any} />);

fireEvent.change(await settledStepInput(container, 'name'), { target: { value: 'Alice' } });
fireEvent.submit(container.querySelector('form') as HTMLFormElement); // → review step

await waitFor(() => {
if (!screen.queryByText(/No fields configured/i)) throw new Error('not on the review step');
});
await act(async () => {});

// The review step must still expose a form for the footer button to submit;
// without one the button targets nothing and the wizard can never finish.
const createBtn = screen.getByRole('button', { name: /create/i });
const reviewForm = container.querySelector('form');
expect(reviewForm).not.toBeNull();
expect(createBtn.getAttribute('form')).toBe(reviewForm!.getAttribute('id'));

await act(async () => { fireEvent.click(createBtn); });
await waitFor(() => expect(ds.create).toHaveBeenCalledTimes(1));
// Step 1's value survives a step that contributed nothing of its own.
expect(ds.create.mock.calls[0][1]).toEqual(expect.objectContaining({ name: 'Alice' }));
});

it('wires the footer Next/Create buttons to the step form (cannot bypass it)', async () => {
const ds = makeDS();
const { container } = render(<WizardForm schema={schema as any} dataSource={ds as any} />);
Expand Down
23 changes: 20 additions & 3 deletions packages/plugin-form/src/WizardForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,26 @@ export const WizardForm: React.FC<WizardFormProps> = ({
}}
/>
) : (
<div className="text-center py-8 text-muted-foreground">
No fields configured for this step
</div>
// A step can legitimately have NO inputs — a final "Review"/confirm
// step is the canonical case (this component's own usage example
// ends on `{ label: 'Step 3: Review', fields: [] }`). The footer
// Next/Create buttons submit the step form *natively* by id, so
// that form has to exist even when there is nothing to fill in:
// without it the buttons point at a missing target, the click does
// nothing at all, and a wizard that ends on a review step can never
// be completed. Submitting contributes no new values — the record
// is whatever the earlier steps accumulated in `formData`.
<form
id={stepFormId}
onSubmit={(e) => {
e.preventDefault();
void handleStepSubmit({});
}}
>
<div className="text-center py-8 text-muted-foreground">
No fields configured for this step
</div>
</form>
)}
</FormSection>
)}
Expand Down
Loading