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
@@ -6,8 +6,8 @@ import router from '@/router'
|
||||
import App from '@/App.vue'
|
||||
import { useSessionStore, type Session } from '@/stores/session'
|
||||
import { ApiError, NetworkError, authRedirectPending, useApi } from '@/composables/useApi'
|
||||
import { DESKTOP_WIDTH, setViewportWidth } from '@/components/__tests__/viewport'
|
||||
import { accessFor } from '@/test/access'
|
||||
import { DESKTOP_WIDTH, MOBILE_WIDTH, setViewportWidth } from '@/components/__tests__/viewport'
|
||||
import { accessFor, customAccess } from '@/test/access'
|
||||
|
||||
vi.mock('@/composables/useApi', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/composables/useApi')>()
|
||||
@@ -85,13 +85,20 @@ interface MountOptions {
|
||||
getMe?: () => Promise<Session>
|
||||
handlers?: Record<string, Handler>
|
||||
path?: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
let mountedWrapper: VueWrapper | null = null
|
||||
|
||||
async function mountApp(options: MountOptions = {}): Promise<Mounted> {
|
||||
const { session = MARTA, status = 'ready', handlers = {}, path = '/profile' } = options
|
||||
setViewportWidth(DESKTOP_WIDTH)
|
||||
const {
|
||||
session = MARTA,
|
||||
status = 'ready',
|
||||
handlers = {},
|
||||
path = '/profile',
|
||||
width = DESKTOP_WIDTH,
|
||||
} = options
|
||||
setViewportWidth(width)
|
||||
setActivePinia(createPinia())
|
||||
const store = useSessionStore()
|
||||
store.session = session ? { ...session } : null
|
||||
@@ -304,18 +311,24 @@ describe('Default: the usual first visit with no display name', () => {
|
||||
links.forEach((link) => expect(link.getAttribute('aria-current')).toBeNull())
|
||||
})
|
||||
|
||||
it('places the four cards in order Display name, Account, Author profile, Password', async () => {
|
||||
it('places the cards in order Display name, Account, Your role, Author profile, Password', async () => {
|
||||
await mountApp()
|
||||
|
||||
const titles = Array.from(document.body.querySelectorAll('section h2')).map((el) =>
|
||||
el.textContent?.trim(),
|
||||
)
|
||||
expect(titles).toEqual(['Display name', 'Account', 'Author profile', 'Password'])
|
||||
expect(titles).toEqual([
|
||||
'Display name',
|
||||
'Account',
|
||||
'Your role',
|
||||
'Author profile',
|
||||
'Password',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading: opened by URL before /auth/me has returned', () => {
|
||||
it('shows the header and four skeleton cards without AppBoot, then the Default state', async () => {
|
||||
it('shows the header and five skeleton cards without AppBoot, then the Default state', async () => {
|
||||
const pending = deferred<Session>()
|
||||
await mountApp({
|
||||
session: null,
|
||||
@@ -328,7 +341,8 @@ describe('Loading: opened by URL before /auth/me has returned', () => {
|
||||
)
|
||||
expect(document.body.textContent).toContain('Your account on this RenewCMS workspace.')
|
||||
const busy = document.body.querySelector('[aria-busy="true"]')!
|
||||
expect(busy.querySelectorAll('[data-testid="profile-skeleton-card"]')).toHaveLength(4)
|
||||
expect(busy.querySelectorAll('[data-testid="profile-skeleton-card"]')).toHaveLength(5)
|
||||
expect(document.body.textContent).not.toContain('Your role')
|
||||
expect(document.body.querySelector('[aria-label="Loading"]')).toBeNull()
|
||||
expect(displayNameField()).toBeNull()
|
||||
|
||||
@@ -905,3 +919,105 @@ describe('Reset: failed leaves the button pressable', () => {
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const MARTA_ASSIGNER = { id: 3, name: 'Marta Alves' }
|
||||
const KEYS_SENTENCE = 'Your API keys follow this role. If your role changes, they change too.'
|
||||
|
||||
function contributor(assigner?: { id: number; name: string } | null): Session {
|
||||
const access = customAccess(
|
||||
'content.read=all, content.create, content.update=own',
|
||||
'Contributor',
|
||||
)
|
||||
return {
|
||||
...MARTA,
|
||||
access: assigner === undefined ? access : { ...access, roleAssignedBy: assigner },
|
||||
}
|
||||
}
|
||||
|
||||
function pageText() {
|
||||
return document.body.textContent ?? ''
|
||||
}
|
||||
|
||||
function roleRequests(request: ReturnType<typeof vi.fn>) {
|
||||
return request.mock.calls.filter(([path]) => String(path).startsWith('/v1/role'))
|
||||
}
|
||||
|
||||
describe('Profile: your role', () => {
|
||||
it('shows the role card with the badge, the assigner and the keys sentence', async () => {
|
||||
await mountApp({ session: contributor(MARTA_ASSIGNER) })
|
||||
|
||||
expect(pageText()).toContain('Your role')
|
||||
expect(pageText()).toContain('Contributor')
|
||||
expect(pageText()).toContain('Assigned by Marta Alves')
|
||||
expect(pageText()).toContain(KEYS_SENTENCE)
|
||||
expect(buttonByText(/^See what I can do$/)).toBeDefined()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['null', null],
|
||||
['an absent key', undefined],
|
||||
])('renders no Assigned by text for %s', async (_label, assigner) => {
|
||||
await mountApp({ session: contributor(assigner) })
|
||||
|
||||
expect(pageText()).toContain('Your role')
|
||||
expect(pageText()).not.toContain('Assigned by')
|
||||
})
|
||||
|
||||
it('places the card after the Account card', async () => {
|
||||
await mountApp({ session: contributor(MARTA_ASSIGNER) })
|
||||
|
||||
const titles = [...document.body.querySelectorAll('h2')].map((h2) => h2.textContent?.trim())
|
||||
expect(titles.indexOf('Your role')).toBe(titles.indexOf('Account') + 1)
|
||||
})
|
||||
|
||||
it('follows a refreshed session for the assigner and the role', async () => {
|
||||
await mountApp({ session: contributor(MARTA_ASSIGNER) })
|
||||
|
||||
useSessionStore().session = {
|
||||
...contributor({ id: 2, name: 'Jonas Weber' }),
|
||||
}
|
||||
await flushPromises()
|
||||
expect(pageText()).toContain('Assigned by Jonas Weber')
|
||||
expect(pageText()).not.toContain('Assigned by Marta Alves')
|
||||
|
||||
const writer = customAccess('content.read=all', 'Writer')
|
||||
useSessionStore().session = { ...MARTA, access: writer }
|
||||
await flushPromises()
|
||||
expect(pageText()).toContain('Writer')
|
||||
expect(pageText()).not.toContain('Assigned by')
|
||||
})
|
||||
|
||||
it('opens the summary dialog from the session grants without reading roles, and Close returns focus', async () => {
|
||||
const { request } = await mountApp({ session: contributor(MARTA_ASSIGNER) })
|
||||
const opener = buttonByText(/^See what I can do$/)!
|
||||
opener.focus()
|
||||
opener.click()
|
||||
await flushPromises()
|
||||
|
||||
const dialog = document.body.querySelector('[role="dialog"]')!
|
||||
expect(dialog.textContent).toContain('What you can do')
|
||||
expect(dialog.textContent).toContain('A summary of your role, Contributor. Read-only.')
|
||||
expect(dialog.textContent).not.toContain('Updates as you edit.')
|
||||
expect(dialog.textContent).toContain('Content')
|
||||
expect(roleRequests(request)).toHaveLength(0)
|
||||
|
||||
const close = [...dialog.querySelectorAll('button')].find(
|
||||
(button) => button.textContent?.trim() === 'Close',
|
||||
)!
|
||||
close.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(document.body.querySelector('[role="dialog"]')).toBeNull()
|
||||
expect(document.activeElement).toBe(opener)
|
||||
})
|
||||
|
||||
it('opens the summary as a bottom sheet on mobile', async () => {
|
||||
await mountApp({ session: contributor(MARTA_ASSIGNER), width: MOBILE_WIDTH })
|
||||
buttonByText(/^See what I can do$/)!.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(document.body.querySelector('[role="dialog"]')?.textContent).toContain(
|
||||
'What you can do',
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user