Files
PaperClipAI/ui/storybook/fixtures/onboardingDraft.test.ts
TonioandClaude Fable 5 141c5b1340 feat(onboarding): Figma pass over the tenant arc (#12726)
Takes the four tenant onboarding steps to the design, and makes the model
choice explicit.

**Nothing is preselected on the connect step.** It arrived with a source
already chosen, which made the row read as a confirmation rather than a
question and let a customer pass the step having touched none of it. The
step now opens unanswered and cannot advance until a source is selected in
the visible row.

Two defects fell out of that, both found by Greptile in review:

- A saved draft can name an adapter the row does not show — one the registry
  dropped, or one in the advanced list. The gate asked `sourcePicked`, which
  only means "a draft named something", so Next stayed live on a step that
  had visibly asked nothing and would hire against the hidden name. It now
  asks `sourceSelected`, read off the visible row, and the snap that replaces
  an unofferable adapter clears the pick rather than presenting its
  replacement as chosen.
- Cmd+Enter bypassed that gate entirely, because the condition was written
  out twice and drifted. Both paths now ask one predicate.

Also: selection is a fill rather than a border; the input canvas opens only
for a chosen source; the close control is gone from every step (nothing
downstream of the connect step works until a model is connected); the arc
draws to the design's 424px column with a filled 44px name field; the CTA
reads "Next" through the arc and "Get started" at the end; the login spinner
reads "Preparing..."; the OAuth panels number their fields and show the URL
above the code.

Storybook: the arc stories waited for a button named "Connect" and had been
silently rendering the wrong step since the CTA was renamed. They now wait on
the destination heading. Three e2e specs had the same fault and now select a
source before advancing.

One test was removed rather than replaced — `hydrates again when the same
company comes back through onboarding` drove the close control, and three
substitutes each passed against a wizard with the behaviour deleted. The
reason is recorded where it stood.

Known and not fixed here: SidebarCompanyMenu opens the wizard at step 1 for
"create a new organization", and with no exit an existing user who changes
their mind is trapped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 12:19:15 -07:00

73 lines
3.1 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import { ONBOARDING_STORAGE_KEY } from "@/components/OnboardingWizard";
import {
ONBOARDING_ARC_ENTRY_STEP,
STORYBOOK_COMPANY_ID,
clearOnboardingDraft,
readOnboardingDraft,
seedOnboardingDraft,
} from "./onboardingDraft";
afterEach(() => {
window.localStorage.clear();
});
describe("storybook onboarding draft", () => {
// The leak this exists to stop: `localStorage` is per-origin and shared by
// every story in a session, so a seeded draft left behind makes the *next*
// story restore a saved step instead of the one it asked for. The reviewer
// then sees a screen they did not click on, which reads as a wizard bug.
it("leaves nothing behind once cleared", () => {
seedOnboardingDraft();
expect(readOnboardingDraft()).not.toBeNull();
clearOnboardingDraft();
expect(readOnboardingDraft()).toBeNull();
expect(window.localStorage.getItem(ONBOARDING_STORAGE_KEY)).toBeNull();
});
// The wizard captures `entryStep` from this draft once, at mount, and offers
// Back only while `currentStep > entryStep`. Seeding a later step is therefore
// not a shortcut to it — it is a step that can never show its Back button.
it("enters the arc at its first step, so later steps can be walked into", () => {
seedOnboardingDraft();
expect(readOnboardingDraft()?.step).toBe(ONBOARDING_ARC_ENTRY_STEP);
});
// `createdAgentId` is what `launchStateIncomplete` checks. Filling it in
// before the hire would paint over the guard step 5 is supposed to show when
// it is reached without an agent.
it("does not claim an agent exists before the hire", () => {
seedOnboardingDraft();
expect(readOnboardingDraft()?.createdAgentId).toBe("");
});
// A run standing on step 3 has not reached the connect step, so it cannot
// have chosen a source there. Seeding one is not a harmless head start: the
// step reads a saved `adapterType` as "already picked", and every arc story
// opened with Claude Code selected and its sign-in panel already showing —
// the preselection the step was changed to stop doing, restored by the
// fixture. Stories that need a source click one, the way a customer does.
it("does not claim a model source was chosen before the connect step", () => {
seedOnboardingDraft();
expect(readOnboardingDraft()).not.toHaveProperty("adapterType");
});
// `restoreOnboardingState` treats restoring as an authorization decision and
// throws the whole blob away when the saved company is not one the account
// owns. Seeding a company the fixtures do not report would silently restore
// nothing, and every story would quietly fall back to its `initialStep`.
it("names the company the fixtures report as owned", () => {
seedOnboardingDraft();
expect(readOnboardingDraft()?.createdCompanyId).toBe(STORYBOOK_COMPANY_ID);
});
it("survives a malformed value without throwing", () => {
window.localStorage.setItem(ONBOARDING_STORAGE_KEY, "{not json");
expect(readOnboardingDraft()).toBeNull();
});
});