refac: hardening pass with multiple overhauls

This commit is contained in:
fsylvain committed 2026-09-29 10:55:23 +02:00
1 parent b0f7b7892b
commit da36c78bc0
28 files changed
+1375 -39

No files matched your search

+27 -2
View File
@@ -9,7 +9,7 @@ import { ApiError, useApi } from '@/composables/useApi'
import { VERIFIED_REDIRECT_DELAY_MS, wait } from '@/lib/verifiedRedirect'
import { useSessionStore } from '@/stores/session'
type InviteState = 'checking' | 'form' | 'invalid' | 'expired' | 'success'
type InviteState = 'checking' | 'form' | 'invalid' | 'expired' | 'failed' | 'success'
const EMAIL_HAS_ACCOUNT_MESSAGE = 'This address already has an account.'
const USERNAME_TAKEN_MESSAGE = 'That username is taken. Try another.'
@@ -100,6 +100,7 @@ function applyDeadLinkStatus(error: unknown): boolean {
}
async function loadInvite() {
state.value = 'checking'
try {
const invite = await api.request<{ email: string }>(
`/v1/auth/invite?c=${encodeURIComponent(code)}`,
@@ -108,7 +109,9 @@ async function loadInvite() {
inviteEmail.value = invite.email
state.value = 'form'
} catch (error) {
applyDeadLinkStatus(error)
if (!applyDeadLinkStatus(error)) {
state.value = 'failed'
}
}
}
@@ -203,6 +206,28 @@ onMounted(() => {
<p class="text-base text-muted">This only takes a moment.</p>
</div>
<div v-else-if="state === 'failed'" class="flex flex-col gap-4.5">
<UAlert
color="error"
icon="i-lucide-circle-x"
title="Couldn't check your invite"
description="Something went wrong on the server. Try again in a moment."
variant="soft"
:close="false"
role="alert"
/>
<UButton
block
color="neutral"
variant="outline"
:size="controlSize"
:ui="{ base: controlClass }"
@click="loadInvite"
>
Try again
</UButton>
</div>
<ResultPanel
v-else-if="state === 'invalid'"
tone="error"
@@ -368,3 +368,95 @@ describe('Success', () => {
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/home'))
})
})
describe('Failed lookup', () => {
const lookupKey = 'GET /v1/auth/invite?c=abc123'
const alertTitle = "Couldn't check your invite"
const alertText = 'Something went wrong on the server. Try again in a moment.'
function tryAgainButton(wrapper: Wrapper) {
return wrapper.findAll('button').find((button) => button.text() === 'Try again')
}
function expectFailedLookup(wrapper: Wrapper) {
expect(wrapper.text()).toContain(alertTitle)
expect(wrapper.text()).toContain(alertText)
expect(tryAgainButton(wrapper)).toBeTruthy()
expect(wrapper.text()).not.toContain('Checking your invite…')
expect(wrapper.find('form').exists()).toBe(false)
expect(emailInput(wrapper).exists()).toBe(false)
expect(wrapper.text()).not.toContain("This invite link doesn't work")
}
it.each([
{ name: 'answers 500', handler: () => mockResponse(500) },
{ name: 'answers 502', handler: () => mockResponse(502) },
{
name: 'fails with a network error',
handler: () => Promise.reject(new TypeError('Failed to fetch')),
},
])('shows the alert and a Try again button when the lookup $name', async ({ handler }) => {
const { wrapper } = await mountAccept('/invite/accept?c=abc123', { [lookupKey]: handler })
expectFailedLookup(wrapper)
expect(wrapper.find('[role="alert"]').exists()).toBe(true)
})
it('looks the invite up again when Try again is activated', async () => {
let calls = 0
const { wrapper, fetchMock } = await mountAccept('/invite/accept?c=abc123', {
[lookupKey]: () => {
calls += 1
return calls === 1
? mockResponse(500)
: new Promise<Response>((resolve) =>
setTimeout(
() =>
resolve(
mockResponse(200, { email: 'lea.martin@renew.example' }),
),
0,
),
)
},
})
await tryAgainButton(wrapper)!.trigger('click')
expect(wrapper.text()).toContain('Checking your invite…')
expect(wrapper.text()).not.toContain(alertTitle)
expect(fetchMock).toHaveBeenCalledTimes(2)
await new Promise((resolve) => setTimeout(resolve, 5))
await flushPromises()
expect((emailInput(wrapper).element as HTMLInputElement).value).toBe(
'lea.martin@renew.example',
)
expect(wrapper.find('h1').text()).toBe('Accept your invite')
})
it('shows the alert again with an enabled button when the lookup fails again', async () => {
const { wrapper, fetchMock } = await mountAccept('/invite/accept?c=abc123', {
[lookupKey]: () => mockResponse(500),
})
await tryAgainButton(wrapper)!.trigger('click')
await flushPromises()
expect(fetchMock).toHaveBeenCalledTimes(2)
expectFailedLookup(wrapper)
expect(tryAgainButton(wrapper)!.attributes('disabled')).toBeUndefined()
})
it.each([
{ status: 404, text: "This invite link doesn't work" },
{ status: 410, text: 'This invite has expired' },
])('keeps the design state when the lookup answers $status', async ({ status, text }) => {
const { wrapper } = await mountAccept('/invite/accept?c=abc123', {
[lookupKey]: () => mockResponse(status),
})
expect(wrapper.text()).toContain(text)
expect(wrapper.text()).not.toContain(alertTitle)
expect(tryAgainButton(wrapper)).toBeUndefined()
})
})