Scope settings loading tests to their section and document onboarding paths
The /settings/notifications and /settings/ai loading tests passed on any skeleton, including the webhook and Recipe search cards' own. They now wait for those sections to settle and check the one skeleton left sits before them, so removing the held section's skeleton fails the test. docs/STATES.md listed /onboarding as the Regulars step. It now lists the Welcome (/onboarding), Regulars (/onboarding/regulars) and Notifications (/onboarding/notifications) steps, each tested at its own path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -27,7 +27,9 @@ screen (a Quest log count, a calendar list, a webhook card) use the same
|
||||
| Route | Screen | Empty | Loading | Error |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `/` | Home | `EmptyState` "No quest planned tonight" with **Plan this week**; rows that do not apply are hidden. | Skeletons for the date, Tonight card and status rows. | "Couldn't load tonight's quest. …" + Retry. |
|
||||
| `/onboarding` | Onboarding — Regulars step | "Your regulars · 0" with the Common picks to add; **Next: notifications** stays disabled until enough Regulars are saved. | Skeleton under "Your regulars". | "Your Regulars could not load: …" + Retry. |
|
||||
| `/onboarding` | Onboarding — Welcome step | None: the step reads nothing; it shows the welcome and the Regulars, Side quests and Quest log terms with **Let's start**. | None: nothing to read. | None: nothing to read. |
|
||||
| `/onboarding/regulars` | Onboarding — Regulars step | "Your regulars · 0" with the Common picks to add; **Next: notifications** stays disabled until enough Regulars are saved. | Skeleton under "Your regulars". | "Your Regulars could not load: …" + Retry. |
|
||||
| `/onboarding/notifications` | Onboarding — Notifications step | None: the step reads nothing. | While finishing, **Turn on notifications** and **Maybe later** are disabled. | "Could not finish setting up: …"; the buttons come back to try again, with **Add Regulars** when fewer than three Regulars are saved. |
|
||||
| `/plan` | Weekly plan (Draft board) | `EmptyState` "Nothing planned yet" with **Draft this week**. | Seven skeleton tiles ("Plan loading"; "Drafting this week" while the AI drafts). | "Couldn't load this week's plan. …" + Retry. |
|
||||
| `/plan/shopping` | Supply run | `EmptyState` "No shopping list yet" with **Make the shopping list**; an archived week with none shows "No shopping list". A list with no ingredients shows "Nothing to buy". | Skeleton title and dinner cards. | "Couldn't load the shopping list. …" + Retry. |
|
||||
| `/plan/shopping/archive` | Supply archive | `EmptyState` "Nothing archived yet" — "Each week's Supply run becomes a post here once it's made." | Skeleton posts. | "Couldn't load the Supply archive. …" + Retry. |
|
||||
|
||||
+59
-5
@@ -113,6 +113,18 @@ async function expectLoading() {
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
}
|
||||
|
||||
// expectOnlyLoadingBefore checks, once a page's independent sections have
|
||||
// settled, that the one skeleton left is the held read's: it sits before
|
||||
// next, the first settled section after it, and nothing failed.
|
||||
function expectOnlyLoadingBefore(next: Element) {
|
||||
expect(skeletons()).toHaveLength(1)
|
||||
const left = skeletons().item(0)
|
||||
if (left === null) throw new Error('no skeleton left')
|
||||
expect(next.contains(left)).toBe(false)
|
||||
expect(left.compareDocumentPosition(next) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
}
|
||||
|
||||
// expectRetry finds the error banner saying said, presses its Retry and
|
||||
// waits for recovered.
|
||||
async function expectRetry(said: string, recovered: () => Promise<unknown>) {
|
||||
@@ -160,7 +172,18 @@ describe('/ Home', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('/onboarding Regulars step', () => {
|
||||
describe('/onboarding Welcome step', () => {
|
||||
it('reads nothing, so it has no empty, loading or error state', async () => {
|
||||
const fetchMock = serve({})
|
||||
await renderRoute('/onboarding', '/onboarding/*', <OnboardingPage />)
|
||||
expect(screen.getByRole('heading', { name: 'Welcome to MealQuest' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: "Let's start" })).toBeEnabled()
|
||||
expect(skeletons()).toHaveLength(0)
|
||||
expect(fetchMock.mock.calls.map(([input]) => String(input))).toEqual(['/api/v1/me'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('/onboarding/regulars Regulars step', () => {
|
||||
const page = async (regulars: Responder) => {
|
||||
serve({ 'GET /api/v1/dinners?status=regular': regulars })
|
||||
await renderRoute('/onboarding/regulars', '/onboarding/*', <OnboardingPage />)
|
||||
@@ -186,6 +209,27 @@ describe('/onboarding Regulars step', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('/onboarding/notifications Notifications step', () => {
|
||||
const page = async (finish: Responder) => {
|
||||
serve({ 'PATCH /api/v1/settings/household': finish })
|
||||
await renderRoute('/onboarding/notifications', '/onboarding/*', <OnboardingPage />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Maybe later' }))
|
||||
}
|
||||
|
||||
it("loading: the step's buttons wait while it finishes", async () => {
|
||||
await page(pending)
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Maybe later' })).toBeDisabled())
|
||||
expect(screen.getByRole('button', { name: 'Turn on notifications' })).toBeDisabled()
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('error: says finishing failed and lets it be tried again', async () => {
|
||||
await page(failThen(pending))
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('Could not finish setting up: internal message')
|
||||
expect(screen.getByRole('button', { name: 'Maybe later' })).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('/plan Draft board', () => {
|
||||
const page = async (plan: Responder) => {
|
||||
serve({ [`GET /api/v1/plans/${WEEK()}`]: plan, [`GET /api/v1/shopping/${WEEK()}`]: () => errorResponse(404, 'not_found') })
|
||||
@@ -427,9 +471,15 @@ describe('/settings/notifications Notifications', () => {
|
||||
await renderRoute('/settings/notifications', '/settings/notifications', <NotificationsPage />)
|
||||
}
|
||||
|
||||
it('loading: skeleton', async () => {
|
||||
it('loading: skeleton for the channel matrix', async () => {
|
||||
await page(pending)
|
||||
await expectLoading()
|
||||
// The webhook card and this device's push status read on their own;
|
||||
// once they settle, the one skeleton left is the channel matrix's.
|
||||
await screen.findByRole('button', { name: 'Save webhook' })
|
||||
const device = screen.getByRole('group', { name: 'Notifications on this device' })
|
||||
await waitFor(() => expect(device.querySelectorAll(`.${skeletonStyles.skeleton}`)).toHaveLength(0))
|
||||
expectOnlyLoadingBefore(device)
|
||||
expect(screen.queryByRole('group', { name: 'Notification channels' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('empty: no Google Chat webhook stored', async () => {
|
||||
@@ -456,9 +506,13 @@ describe('/settings/ai AI assistant', () => {
|
||||
await renderRoute('/settings/ai', '/settings/ai', <AISettingsPage />)
|
||||
}
|
||||
|
||||
it('loading: skeleton', async () => {
|
||||
it('loading: skeleton for the AI assistant form', async () => {
|
||||
await page(pending)
|
||||
await expectLoading()
|
||||
// The Recipe search card reads on its own; once it settles, the one
|
||||
// skeleton left is the AI assistant form's.
|
||||
await screen.findByRole('button', { name: 'Save recipe search' })
|
||||
expectOnlyLoadingBefore(screen.getByRole('region', { name: 'Recipe search' }))
|
||||
expect(screen.queryByText('No key stored')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('empty: no key stored, with Add key', async () => {
|
||||
|
||||
Reference in New Issue
Block a user