feat: roles SPA Users, Profile, API keys, Activity
This commit is contained in:
1 parent
3db490a615
commit
34dffe222f
98 files changed
+4957
-434
No files matched your search
@@ -47,7 +47,6 @@ function defaultFixture(): UserListItem[] {
|
||||
kind: 'invite',
|
||||
id: 1,
|
||||
email: 'p.huber@gmail.com',
|
||||
display_name: 'Paul Huber',
|
||||
role: roleRefFor('admin'),
|
||||
status: 'expired',
|
||||
created_at: '2026-08-03T09:00:00Z',
|
||||
@@ -284,7 +283,7 @@ describe('Default — the list loads newest first, with display name, "you" tag
|
||||
|
||||
expect(rowTexts()).toEqual([
|
||||
'sam.oliveira@renew.example — admin Pending 27 Sep 2026 Cancel invite',
|
||||
'p.huber@gmail.com Paul Huber admin Expired 3 Aug 2026 Renew invite',
|
||||
'p.huber@gmail.com — admin Expired 3 Aug 2026 Renew invite',
|
||||
'ines.duarte@renew.example Inès Duarte admin Accepted 18 Jun 2026',
|
||||
'jonas@renew.example — admin Accepted 2 Apr 2026',
|
||||
'marta@renew.example you — owner Accepted 12 Mar 2026',
|
||||
@@ -306,7 +305,7 @@ describe('Default — the list loads newest first, with display name, "you" tag
|
||||
Array.from(row.querySelectorAll('span')).findLast(
|
||||
(el) => el.textContent?.trim() === role,
|
||||
)!
|
||||
const [sam, huber, , jonas, marta] = rows()
|
||||
const [sam, huber, ines, jonas, marta] = rows()
|
||||
|
||||
expect(badge(marta, 'owner').className).toContain('bg-primary')
|
||||
expect(badge(marta, 'owner').className).not.toContain('bg-transparent')
|
||||
@@ -315,10 +314,11 @@ describe('Default — the list loads newest first, with display name, "you" tag
|
||||
|
||||
expect(badge(jonas, '—').className).toContain('text-muted')
|
||||
expect(badge(sam, '—').className).toContain('text-muted')
|
||||
expect(badge(huber, 'Paul Huber').className).toContain('text-highlighted')
|
||||
expect(badge(huber, 'Paul Huber').className).not.toContain('text-muted')
|
||||
expect(badge(huber, '—').className).toContain('text-muted')
|
||||
expect(badge(ines, 'Inès Duarte').className).toContain('text-highlighted')
|
||||
expect(badge(ines, 'Inès Duarte').className).not.toContain('text-muted')
|
||||
|
||||
expect(badge(marta, 'you')).toBeTruthy()
|
||||
expect(badge(marta, 'you')).toBeInstanceOf(HTMLElement)
|
||||
expect(rows().filter((row) => badge(row, 'you'))).toEqual([marta])
|
||||
})
|
||||
|
||||
@@ -326,15 +326,16 @@ describe('Default — the list loads newest first, with display name, "you" tag
|
||||
await mountPage({}, false)
|
||||
|
||||
const spans = (row: Element) => Array.from(row.querySelectorAll('span'))
|
||||
const [, huber, , jonas, marta] = rows()
|
||||
const [, huber, ines, jonas, marta] = rows()
|
||||
const find = (row: Element, text: string) =>
|
||||
spans(row).findLast((el) => el.textContent?.trim() === text)
|
||||
|
||||
expect(find(marta, 'owner')!.className).toContain('bg-primary')
|
||||
expect(find(jonas, 'admin')!.className).toContain('bg-transparent')
|
||||
expect(find(jonas, '—')).toBeUndefined()
|
||||
expect(find(huber, 'Paul Huber')).toBeTruthy()
|
||||
expect(find(marta, 'you')).toBeTruthy()
|
||||
expect(find(ines, 'Inès Duarte')).toBeInstanceOf(HTMLElement)
|
||||
expect(find(huber, '—')).toBeUndefined()
|
||||
expect(find(marta, 'you')).toBeInstanceOf(HTMLElement)
|
||||
expect(find(jonas, 'you')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -343,7 +344,7 @@ describe('Default — the list loads newest first, with display name, "you" tag
|
||||
|
||||
expect(rowTexts()).toEqual([
|
||||
'sam.oliveira@renew.example admin Pending Created 27 Sep 2026 Cancel invite',
|
||||
'p.huber@gmail.com Paul Huber admin Expired Created 3 Aug 2026 Renew invite',
|
||||
'p.huber@gmail.com admin Expired Created 3 Aug 2026 Renew invite',
|
||||
'ines.duarte@renew.example Inès Duarte admin Accepted Created 18 Jun 2026',
|
||||
'jonas@renew.example admin Accepted Created 2 Apr 2026',
|
||||
'marta@renew.example you owner Accepted Created 12 Mar 2026',
|
||||
@@ -515,10 +516,8 @@ describe('Invite: default — opening the modal', () => {
|
||||
await mountPage()
|
||||
await openModal()
|
||||
|
||||
expect(dialog()?.textContent).toContain('Invite a user')
|
||||
expect(dialog()?.textContent).toContain(
|
||||
"We'll email them a link to choose a username and password. The link expires after 3 days.",
|
||||
)
|
||||
expect(dialog()?.textContent).toContain('Invite user')
|
||||
expect(dialog()?.textContent).toContain('They get an email with a link to set a password.')
|
||||
expect(emailInput().value).toBe('')
|
||||
expect(document.activeElement).toBe(emailInput())
|
||||
expect(findButton('Cancel')).toBeTruthy()
|
||||
@@ -616,7 +615,7 @@ describe('Invite sent — a 201 closes the modal, adds a Pending row on top and
|
||||
expect(texts[0]).toContain('lea.martin@renew.example — admin Pending Today')
|
||||
expect(texts.slice(1)).toEqual([
|
||||
'sam.oliveira@renew.example — admin Pending 27 Sep 2026 Cancel invite',
|
||||
'p.huber@gmail.com Paul Huber admin Expired 3 Aug 2026 Renew invite',
|
||||
'p.huber@gmail.com — admin Expired 3 Aug 2026 Renew invite',
|
||||
'ines.duarte@renew.example Inès Duarte admin Accepted 18 Jun 2026',
|
||||
'jonas@renew.example — admin Accepted 2 Apr 2026',
|
||||
'marta@renew.example you — owner Accepted 12 Mar 2026',
|
||||
@@ -632,6 +631,27 @@ describe('Invite sent — a 201 closes the modal, adds a Pending row on top and
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[1, 'has 1 day to accept.'],
|
||||
[7, 'has 7 days to accept.'],
|
||||
])('words a %s-day expiry from expires_at', async (days, wording) => {
|
||||
const created = new Date()
|
||||
await mountPage({
|
||||
invite: () =>
|
||||
Promise.resolve({
|
||||
...inviteFixture(),
|
||||
created_at: created.toISOString(),
|
||||
expires_at: new Date(
|
||||
created.getTime() + days * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
}),
|
||||
})
|
||||
await openModal()
|
||||
await submitWith(VALID_EMAIL)
|
||||
|
||||
expect(toastAdd.mock.calls[0]![0].description).toBe(`${VALID_EMAIL} ${wording}`)
|
||||
})
|
||||
|
||||
it('returns focus to Invite user', async () => {
|
||||
await mountPage()
|
||||
await openModal()
|
||||
@@ -670,36 +690,46 @@ describe('Invite sent into the Only-you state keeps a coherent list', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Invite: already a user (409) — the field error depends on the response text', () => {
|
||||
describe('Invite: 409 conflicts are told apart by the error code', () => {
|
||||
it.each([
|
||||
{
|
||||
email: 'jonas@renew.example',
|
||||
body: 'This address already has an account.',
|
||||
code: 'email_has_account' as const,
|
||||
error: 'This address already has an account.',
|
||||
},
|
||||
{
|
||||
email: 'sam.oliveira@renew.example',
|
||||
body: "There's already a pending invite for this address.",
|
||||
code: 'invite_pending' as const,
|
||||
error: "There's already a pending invite for this address.",
|
||||
},
|
||||
{
|
||||
email: 'jonas@renew.example',
|
||||
body: 'Something else',
|
||||
error: 'This address already has an account.',
|
||||
},
|
||||
])('shows "$error" for the body "$body"', async ({ email, body, error }) => {
|
||||
])('shows "$error" for the code $code', async ({ email, code, error }) => {
|
||||
await mountPage({
|
||||
invite: () => Promise.reject(new ApiError(409, undefined, body)),
|
||||
invite: () => Promise.reject(new ApiError(409, undefined, 'ignored body', { code })),
|
||||
})
|
||||
await openModal()
|
||||
await submitWith(email)
|
||||
|
||||
expect(dialog()).toBeTruthy()
|
||||
expect(dialog()).not.toBeNull()
|
||||
expect(emailInput().value).toBe(email)
|
||||
expect(emailInput().disabled).toBe(false)
|
||||
expect(dialog()?.textContent).toContain(error)
|
||||
expect(document.body.querySelector('[role="alert"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores the server wording and falls back to the generic alert on an unknown 409', async () => {
|
||||
await mountPage({
|
||||
invite: () =>
|
||||
Promise.reject(
|
||||
new ApiError(409, undefined, 'This address already has an account.'),
|
||||
),
|
||||
})
|
||||
await openModal()
|
||||
await submitWith('jonas@renew.example')
|
||||
|
||||
expect(alertText()).toContain(FAILED_ALERT)
|
||||
expect(dialog()?.textContent).not.toContain('This address already has an account.')
|
||||
expect(emailInput().disabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("A 400 that got past this form's own check", () => {
|
||||
|
||||
Reference in new issue
Block a user