mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Make the setup flow pass its own journeys and axe sweep
Run against the standalone build, the four journeys and the axe sweep surfaced five things the unit tier could not see. The completion route derives only once every question is settled, and Q6 stays pending for somebody with no unit-bearing area — the confirm screen now passes the questions the flow never showed before it completes. The dashboard seed matched a JSON null where the unset column is a SQL NULL, so it never landed; the retired step route carried the same filter. The auth shell's onboarding branch matched only the front door, so every question screen rendered inside the full app chrome with the sidebar. Inline links relied on colour alone, the step list sat outside any landmark, and the baseline form brought a second h1 onto the confirm screen. The two sweeps run serially, since each drives one account through the API. The integration file gains the seed case that would have caught the null filter.
This commit is contained in:
1 parent
e98457151d
commit
d91a8dfebe
11 files changed
+137
-49
No files matched your search
@@ -130,7 +130,11 @@ export function AuthShell({
|
||||
inSharedRecord &&
|
||||
!isDestinationInSharedRecord(pathname, sections) &&
|
||||
!isRecordSettingsPath;
|
||||
const isOnboardingPage = pathname === "/onboarding";
|
||||
// The whole setup flow, not only its front door: every screen lives under
|
||||
// `/onboarding/<step>` (v1.39 C2), and an exact match here put the
|
||||
// questions inside the full app chrome, sidebar and all.
|
||||
const isOnboardingPage =
|
||||
pathname === "/onboarding" || pathname.startsWith("/onboarding/");
|
||||
const showUnlockNotifier = isAuthenticated && !isPublicPage && !!user?.id;
|
||||
|
||||
// v1.9.0 — the document-level scrollbar-gutter (globals.css) is reserved
|
||||
|
||||
@@ -202,16 +202,12 @@ export function BaselineForm({
|
||||
|
||||
return (
|
||||
<section aria-labelledby="onboarding-baseline-title" className="space-y-6">
|
||||
<header className="space-y-2">
|
||||
{/* Onboarding hero H1: intentionally semibold, not the app-wide bold PageHeader H1 (UI-STANDARDS §5 hero exception). Do not sweep to font-bold. */}
|
||||
<h1
|
||||
id="onboarding-baseline-title"
|
||||
tabIndex={-1}
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
>
|
||||
<header className="space-y-1">
|
||||
{/* A section of the confirm screen, which owns the page's h1. */}
|
||||
<h2 id="onboarding-baseline-title" className="text-base font-semibold">
|
||||
{t("onboarding.baseline.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-base leading-relaxed">
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("onboarding.baseline.body")}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -20,11 +20,15 @@ import { localeLabels } from "@/lib/i18n/config";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { MODULE_REGISTRY } from "@/lib/modules/registry";
|
||||
import { confirmedModules } from "@/lib/onboarding/confirm-summary";
|
||||
import type { OnboardingStateDto } from "@/lib/onboarding/needs";
|
||||
import {
|
||||
ONBOARDING_SKIPPABLE_STEP_IDS,
|
||||
type OnboardingStateDto,
|
||||
} from "@/lib/onboarding/needs";
|
||||
import {
|
||||
firstResultApplies,
|
||||
nextScreen,
|
||||
previousScreen,
|
||||
questionScreens,
|
||||
} from "@/lib/onboarding/wizard-steps";
|
||||
|
||||
/**
|
||||
@@ -71,6 +75,19 @@ export function ConfirmScreen({ state }: { state: OnboardingStateDto }) {
|
||||
if (finishing) return;
|
||||
setFinishing(true);
|
||||
try {
|
||||
// A question this flow never showed — Q6 for somebody with no
|
||||
// unit-bearing area — is passed on the ledger before the completion,
|
||||
// because the route derives only once EVERY question is answered or
|
||||
// passed, and a step nobody was asked cannot be answered.
|
||||
const shown = new Set<string>(questionScreens(state));
|
||||
for (const id of ONBOARDING_SKIPPABLE_STEP_IDS) {
|
||||
if (id === "first-result") continue;
|
||||
const pending =
|
||||
state.steps.find((step) => step.id === id)?.status === "pending";
|
||||
if (!shown.has(id) && pending) {
|
||||
await answer.mutateAsync({ step: id, status: "skipped" });
|
||||
}
|
||||
}
|
||||
const { onboarding } = await complete.mutateAsync();
|
||||
let written = onboarding ?? state;
|
||||
if (!firstResultApplies(written)) {
|
||||
@@ -122,7 +139,7 @@ export function ConfirmScreen({ state }: { state: OnboardingStateDto }) {
|
||||
{t("onboarding.flow.confirm.everythingElse")}{" "}
|
||||
<Link
|
||||
href="/settings/modules"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
className="text-primary underline underline-offset-4"
|
||||
>
|
||||
{t("onboarding.flow.confirm.modulesLink")}
|
||||
</Link>
|
||||
@@ -162,7 +179,7 @@ export function ConfirmScreen({ state }: { state: OnboardingStateDto }) {
|
||||
<p className="text-sm">
|
||||
<Link
|
||||
href="/settings/account"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
className="text-primary underline underline-offset-4"
|
||||
>
|
||||
{t("onboarding.flow.confirm.changeInSettings")}
|
||||
</Link>
|
||||
|
||||
@@ -241,7 +241,7 @@ export function DoneScreen({ state }: { state: OnboardingStateDto }) {
|
||||
|
||||
<Link
|
||||
href="/settings/ai"
|
||||
className="text-primary text-sm font-medium underline-offset-4 hover:underline"
|
||||
className="text-primary text-sm font-medium underline underline-offset-4"
|
||||
>
|
||||
{t("onboarding.ai.setupCta")}
|
||||
</Link>
|
||||
@@ -323,14 +323,14 @@ export function DoneScreen({ state }: { state: OnboardingStateDto }) {
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5">
|
||||
<Link
|
||||
href="/settings/export"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline underline-offset-4"
|
||||
>
|
||||
<FileUp className="size-3.5" />
|
||||
{t("onboarding.done.importCta")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings/modules"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline underline-offset-4"
|
||||
>
|
||||
<Settings2 className="size-3.5" />
|
||||
{t("onboarding.done.modulesCta")}
|
||||
|
||||
@@ -80,29 +80,34 @@ export async function OnboardingShell({
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ol
|
||||
aria-label={t("onboarding.shell.stepsLabel")}
|
||||
className="mb-8 flex items-center gap-2"
|
||||
data-slot="onboarding-step-list"
|
||||
>
|
||||
{steps.map((id, index) => {
|
||||
const reached = currentIndex >= index;
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
aria-current={id === screen ? "step" : undefined}
|
||||
data-reached={reached}
|
||||
className={cn(
|
||||
"h-1.5 flex-1 rounded-full transition-colors",
|
||||
reached ? "bg-primary" : "bg-muted",
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{t(`onboarding.steps.${id}`)}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
{/* A landmark of its own: every piece of page content sits inside one
|
||||
(axe `region`), and the step list is the flow's navigation. */}
|
||||
<nav aria-label={t("onboarding.shell.stepsLabel")} className="mb-8">
|
||||
<ol
|
||||
className="flex items-center gap-2"
|
||||
data-slot="onboarding-step-list"
|
||||
>
|
||||
{steps.map((id, index) => {
|
||||
const reached = currentIndex >= index;
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
aria-current={id === screen ? "step" : undefined}
|
||||
data-reached={reached}
|
||||
className={cn(
|
||||
"h-1.5 flex-1 rounded-full transition-colors",
|
||||
reached ? "bg-primary" : "bg-muted",
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{t(`onboarding.steps.${id}`)}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{/* The page's one `<main>`: the auth shell's onboarding branch renders
|
||||
no landmark of its own around this frame. */}
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -159,7 +159,7 @@ export function WelcomeScreen({ variant }: { variant: "fresh" | "again" }) {
|
||||
href="/privacy#medical-boundary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
className="text-primary underline underline-offset-4"
|
||||
>
|
||||
{t("onboarding.disclaimer.learnMore")}
|
||||
</Link>
|
||||
|
||||
Reference in new issue
Block a user