feat(SPA): profile page, display name and the new account menu
This commit is contained in:
1 parent
55525c06d6
commit
b0f7b7892b
29 files changed
+3166
-469
No files matched your search
+165
-1
@@ -152,7 +152,13 @@ const sixArticlesFixture = [
|
||||
|
||||
const verifiedSession: ApiMock = {
|
||||
status: 200,
|
||||
body: { id: 1, is_verified: true, username: 'marta' },
|
||||
body: {
|
||||
id: 1,
|
||||
is_verified: true,
|
||||
username: 'marta',
|
||||
email: 'marta@renew.example',
|
||||
display_name: '',
|
||||
},
|
||||
}
|
||||
|
||||
const editorNow = '2026-09-26T11:02:00Z'
|
||||
@@ -667,7 +673,165 @@ function submitAcceptInvite(username: string, password: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const profileDisplayName = 'Marta Alves'
|
||||
const profileFieldPlaceholder = 'e.g. Marta Alves'
|
||||
const profileTooLongName = 'Marta Alves, Head of Renovations, Brussels and Wallonia office'
|
||||
const profileSavedBody = {
|
||||
id: 1,
|
||||
username: 'marta',
|
||||
display_name: profileDisplayName,
|
||||
}
|
||||
|
||||
function profileSession(displayName = ''): ApiMock {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
id: 1,
|
||||
is_verified: true,
|
||||
username: 'marta',
|
||||
email: 'marta@renew.example',
|
||||
display_name: displayName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function profileApi(
|
||||
displayName = '',
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
): Record<string, ApiMock> {
|
||||
return {
|
||||
'GET /v1/auth/me': profileSession(displayName),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function profileState(
|
||||
displayName = '',
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
steps?: (page: Page) => Promise<void>,
|
||||
): VisualState {
|
||||
return { path: '/profile', api: profileApi(displayName, overrides), steps }
|
||||
}
|
||||
|
||||
async function typeDisplayName(page: Page, value: string) {
|
||||
await page.getByPlaceholder(profileFieldPlaceholder).fill(value)
|
||||
}
|
||||
|
||||
async function scrollToTop(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
window.scrollTo(0, 0)
|
||||
document.querySelectorAll<HTMLElement>('[data-slot="body"]').forEach((element) => {
|
||||
element.scrollTop = 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function pressSave(page: Page) {
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
||||
}
|
||||
|
||||
async function saveDisplayName(page: Page, value: string) {
|
||||
await typeDisplayName(page, value)
|
||||
await pressSave(page)
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
async function pressSendResetLink(page: Page) {
|
||||
await page.getByRole('button', { name: 'Send reset link', exact: true }).click()
|
||||
await page.mouse.move(0, 0)
|
||||
await scrollToTop(page)
|
||||
}
|
||||
|
||||
function resetState(response: ApiMock): VisualState {
|
||||
return profileState(
|
||||
profileDisplayName,
|
||||
{ 'POST /v1/auth/password-reset': response },
|
||||
pressSendResetLink,
|
||||
)
|
||||
}
|
||||
|
||||
async function openAccountMenu(page: Page) {
|
||||
const isMobile = (page.viewportSize()?.width ?? 0) < 1024
|
||||
if (isMobile) {
|
||||
await page.locator('[data-slot="toggle"]').click()
|
||||
await page.locator('[role="dialog"] button[aria-haspopup]').click()
|
||||
await page.locator('[role="menu"]').waitFor()
|
||||
} else {
|
||||
await page.locator('[data-slot="footer"] button[aria-haspopup]').click()
|
||||
await page.locator('[role="menu"]').waitFor()
|
||||
}
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
const usersWithDisplayNames = usersFixture.map((item) =>
|
||||
item.kind === 'user' && item.username === 'marta'
|
||||
? { ...item, display_name: profileDisplayName }
|
||||
: item,
|
||||
)
|
||||
|
||||
export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
profile: {
|
||||
'Account menu': {
|
||||
path: '/home',
|
||||
now: boardNow,
|
||||
api: {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
|
||||
},
|
||||
steps: openAccountMenu,
|
||||
exception: {
|
||||
maxPercent: 26,
|
||||
reason: 'Mobile sheet is ~153px taller than the board because the System/Light/Dark items are kept by user decision, and the board draws Profile highlighted on /home while the feature highlights it only on /profile',
|
||||
approvedBy: 'Florian Sylvain, 2026-09-29',
|
||||
},
|
||||
},
|
||||
Loading: {
|
||||
path: '/profile',
|
||||
api: { 'GET /v1/auth/me': { status: 200, hang: true } },
|
||||
},
|
||||
'Default, no display name': profileState(),
|
||||
Editing: profileState('', {}, async (page) => {
|
||||
await typeDisplayName(page, profileDisplayName)
|
||||
await page.mouse.move(0, 0)
|
||||
}),
|
||||
'Too long': profileState('', {}, async (page) => {
|
||||
await typeDisplayName(page, profileTooLongName)
|
||||
await page.mouse.move(0, 0)
|
||||
}),
|
||||
Saving: profileState(
|
||||
'',
|
||||
{ 'PATCH /v1/user/me': { status: 200, body: profileSavedBody, hang: true } },
|
||||
async (page) => {
|
||||
await saveDisplayName(page, profileDisplayName)
|
||||
},
|
||||
),
|
||||
Saved: profileState(
|
||||
'',
|
||||
{ 'PATCH /v1/user/me': { status: 200, body: profileSavedBody } },
|
||||
async (page) => {
|
||||
await saveDisplayName(page, profileDisplayName)
|
||||
await page.getByText('Display name saved').first().waitFor()
|
||||
},
|
||||
),
|
||||
'Save failed': profileState('', { 'PATCH /v1/user/me': { status: 500 } }, async (page) => {
|
||||
await saveDisplayName(page, profileDisplayName)
|
||||
await page.getByText("Couldn't save your display name").waitFor()
|
||||
}),
|
||||
'Reset: sending': resetState({ status: 200, hang: true }),
|
||||
'Reset: sent': resetState({ status: 200, body: { message: 'ok' } }),
|
||||
'Reset: too many requests (429)': resetState({
|
||||
status: 429,
|
||||
headers: { 'Retry-After': '300' },
|
||||
}),
|
||||
'Reset: failed': resetState({ status: 500 }),
|
||||
'Display names in the Users list': {
|
||||
path: '/users',
|
||||
api: {
|
||||
'GET /v1/auth/me': profileSession(profileDisplayName),
|
||||
'GET /v1/user/': { status: 200, body: usersWithDisplayNames },
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
Default: { path: '/users', api: usersApi() },
|
||||
Loading: {
|
||||
|
||||
Reference in new issue
Block a user