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
@@ -1,10 +1,11 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import uiPlugin from '@nuxt/ui/vue-plugin'
|
||||
import router from '@/router'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import DashboardShell from '../DashboardShell.vue'
|
||||
import { DESKTOP_WIDTH, MOBILE_WIDTH, setViewportWidth } from './viewport'
|
||||
|
||||
vi.mock('@/composables/useApi', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/composables/useApi')>()
|
||||
@@ -14,10 +15,27 @@ vi.mock('@/composables/useApi', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
async function mountShell(path: string, username = 'marta') {
|
||||
interface ShellOptions {
|
||||
username?: string
|
||||
displayName?: string
|
||||
email?: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
let mountedShell: VueWrapper | null = null
|
||||
|
||||
async function mountShell(path: string, options: ShellOptions = {}) {
|
||||
const {
|
||||
username = 'marta',
|
||||
displayName = '',
|
||||
email = 'marta@renew.example',
|
||||
width = DESKTOP_WIDTH,
|
||||
} = options
|
||||
localStorage.clear()
|
||||
setViewportWidth(width)
|
||||
setActivePinia(createPinia())
|
||||
vi.mocked(useApi).mockReturnValue({
|
||||
getMe: vi.fn().mockResolvedValue({ verified: true, username }),
|
||||
getMe: vi.fn().mockResolvedValue({ verified: true, username, email, displayName }),
|
||||
request: vi.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
await router.push(path)
|
||||
@@ -27,11 +45,14 @@ async function mountShell(path: string, username = 'marta') {
|
||||
plugins: [router, uiPlugin],
|
||||
},
|
||||
})
|
||||
mountedShell = wrapper
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
mountedShell?.unmount()
|
||||
mountedShell = null
|
||||
await router.push('/login')
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
@@ -166,3 +187,112 @@ describe('Collapsed sidebar hides the wordmark and username text but keeps an ac
|
||||
expect(footer.querySelector('[data-slot="fallback"]')?.textContent).toBe('MA')
|
||||
})
|
||||
})
|
||||
|
||||
describe('The account chip shows the display name and no item is current on /profile', () => {
|
||||
it('shows the display name and its initials in the sidebar chip', async () => {
|
||||
await mountShell('/home', { displayName: 'Marta Alves' })
|
||||
|
||||
const footer = document.body.querySelector('[data-slot="footer"]') as HTMLElement
|
||||
expect(footer.textContent).toContain('Marta Alves')
|
||||
expect(footer.querySelector('[data-slot="fallback"]')?.textContent).toBe('MA')
|
||||
})
|
||||
|
||||
it('keeps the collapsed accessible name on the display name', async () => {
|
||||
await mountShell('/home', { displayName: 'Marta Alves' })
|
||||
|
||||
const collapseButton = document.body.querySelector(
|
||||
'[data-testid="sidebar-collapse"]',
|
||||
) as HTMLButtonElement
|
||||
collapseButton.click()
|
||||
await flushPromises()
|
||||
|
||||
const trigger = document.body.querySelector('[data-slot="footer"] button[aria-haspopup]')
|
||||
expect(trigger?.getAttribute('aria-label')).toBe('Marta Alves — account menu')
|
||||
})
|
||||
|
||||
it('marks none of the four navigation items current on /profile', async () => {
|
||||
const wrapper = await mountShell('/profile')
|
||||
|
||||
const links = wrapper.findAll('nav[aria-label="Main"] a')
|
||||
expect(links).toHaveLength(4)
|
||||
links.forEach((link) => expect(link.attributes('aria-current')).toBeUndefined())
|
||||
})
|
||||
})
|
||||
|
||||
describe('Account menu: opened from the drawer on mobile', () => {
|
||||
it('closes the drawer and opens a bottom sheet with a grab handle, the header and 48 px items', async () => {
|
||||
await mountShell('/home', { width: MOBILE_WIDTH })
|
||||
|
||||
const toggle = document.body.querySelector('[data-slot="toggle"]') as HTMLButtonElement
|
||||
toggle.click()
|
||||
await flushPromises()
|
||||
|
||||
const accountRow = document.body.querySelector(
|
||||
'[role="dialog"] button[aria-haspopup]',
|
||||
) as HTMLButtonElement
|
||||
expect(accountRow).not.toBeNull()
|
||||
accountRow.click()
|
||||
await flushPromises()
|
||||
|
||||
const sheets = Array.from(document.body.querySelectorAll('[role="dialog"]'))
|
||||
expect(sheets).toHaveLength(1)
|
||||
const menu = document.body.querySelector('[role="menu"]') as HTMLElement
|
||||
expect(
|
||||
menu.querySelector('[aria-hidden="true"].w-9.h-1, [aria-hidden="true"].h-1'),
|
||||
).not.toBeNull()
|
||||
expect(menu.querySelector('[data-slot="fallback"]')?.textContent).toBe('MA')
|
||||
expect(menu.textContent).toContain('marta@renew.example')
|
||||
const labels = Array.from(
|
||||
menu.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"]'),
|
||||
).map((item) => item.textContent?.trim())
|
||||
expect(labels).toEqual(['Profile', 'System', 'Light', 'Dark', 'Log out'])
|
||||
menu.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"]').forEach((item) =>
|
||||
expect(item.className).toContain('h-12'),
|
||||
)
|
||||
})
|
||||
|
||||
it('navigates to /profile and closes the sheet from Profile', async () => {
|
||||
await mountShell('/home', { width: MOBILE_WIDTH })
|
||||
;(document.body.querySelector('[data-slot="toggle"]') as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
;(
|
||||
document.body.querySelector(
|
||||
'[role="dialog"] button[aria-haspopup]',
|
||||
) as HTMLButtonElement
|
||||
).click()
|
||||
await flushPromises()
|
||||
|
||||
const profile = Array.from(
|
||||
document.body.querySelectorAll<HTMLElement>('[role="menu"] [role="menuitem"]'),
|
||||
).find((item) => item.textContent?.trim() === 'Profile')!
|
||||
profile.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/profile')
|
||||
expect(document.body.querySelector('[role="menu"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('logs out from the sheet', async () => {
|
||||
await mountShell('/home', { width: MOBILE_WIDTH })
|
||||
;(document.body.querySelector('[data-slot="toggle"]') as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
;(
|
||||
document.body.querySelector(
|
||||
'[role="dialog"] button[aria-haspopup]',
|
||||
) as HTMLButtonElement
|
||||
).click()
|
||||
await flushPromises()
|
||||
|
||||
const logOut = Array.from(
|
||||
document.body.querySelectorAll<HTMLElement>('[role="menu"] [role="menuitem"]'),
|
||||
).find((item) => item.textContent?.trim() === 'Log out')!
|
||||
logOut.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(vi.mocked(useApi)().request).toHaveBeenCalledWith(
|
||||
'/v1/auth/logout',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user