feat: SPA Roles screens
This commit is contained in:
1 parent
907949aa12
commit
3db490a615
47 files changed
+6947
-9
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { flushPromises, type VueWrapper } from '@vue/test-utils'
|
||||
import PermissionGroup from '../PermissionGroup.vue'
|
||||
import { radiosOf, renderInApp } from './roleKit'
|
||||
import { effectiveScope } from '@/lib/permissions'
|
||||
import { grantsFrom } from '@/test/access'
|
||||
import type { PermissionGroupItem } from '@/lib/roles'
|
||||
|
||||
let wrapper: VueWrapper | null = null
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
wrapper = null
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
const ACTIVITY: PermissionGroupItem = {
|
||||
key: 'activity',
|
||||
items: [
|
||||
{ key: 'activity.view', scope_kind: 'scoped', owner_only: false, per_type: false },
|
||||
{ key: 'activity.export', scope_kind: 'toggle', owner_only: false, per_type: false },
|
||||
],
|
||||
}
|
||||
|
||||
const SYSTEM: PermissionGroupItem = {
|
||||
key: 'system',
|
||||
items: [
|
||||
{ key: 'system.apply_updates', scope_kind: 'toggle', owner_only: true, per_type: false },
|
||||
{ key: 'system.edit_retention', scope_kind: 'toggle', owner_only: true, per_type: false },
|
||||
],
|
||||
}
|
||||
|
||||
const CALLER = (spec: string) => {
|
||||
const grants = grantsFrom(spec)
|
||||
return (key: string, options?: { typeKey?: string }) => effectiveScope(grants, key, options)
|
||||
}
|
||||
|
||||
function mountGroup(group: PermissionGroupItem, props: Record<string, unknown> = {}) {
|
||||
const changes: { key: string; scope: string }[] = []
|
||||
wrapper = renderInApp(PermissionGroup, {
|
||||
group,
|
||||
grants: { 'activity.view': 'all', 'activity.export': 'all' },
|
||||
initial: { 'activity.view': 'all', 'activity.export': 'all' },
|
||||
callerScope: CALLER('activity.view, activity.export, system.apply_updates'),
|
||||
open: true,
|
||||
onChange: (change: { key: string; scope: string }) => changes.push(change),
|
||||
...props,
|
||||
})
|
||||
return { changes }
|
||||
}
|
||||
|
||||
describe('PermissionGroup', () => {
|
||||
it('reads "x of y enabled" from the grants', () => {
|
||||
mountGroup(ACTIVITY)
|
||||
|
||||
expect(wrapper!.text()).toContain('2 of 2 enabled')
|
||||
})
|
||||
|
||||
it('counts only the enabled items', () => {
|
||||
mountGroup(ACTIVITY, { grants: { 'activity.view': 'own' } })
|
||||
|
||||
expect(wrapper!.text()).toContain('1 of 2 enabled')
|
||||
})
|
||||
|
||||
it('shows the human names and descriptions of the design', () => {
|
||||
mountGroup(ACTIVITY)
|
||||
|
||||
const text = wrapper!.text()
|
||||
expect(text).toContain('See activity')
|
||||
expect(text).toContain('Read the log of who did what.')
|
||||
expect(text).toContain('Export activity')
|
||||
expect(text).toContain('Download the log as a CSV file.')
|
||||
})
|
||||
|
||||
it('renders scoped rows as None, Own, All and toggle rows as a switch', async () => {
|
||||
const { changes } = mountGroup(ACTIVITY)
|
||||
|
||||
expect(radiosOf(wrapper!, 'See activity')).toHaveLength(3)
|
||||
expect(wrapper!.find('[role="switch"][aria-label="Export activity"]').exists()).toBe(true)
|
||||
|
||||
radiosOf(wrapper!, 'See activity')[1]!.click()
|
||||
await wrapper!.find('[role="switch"][aria-label="Export activity"]').trigger('click')
|
||||
|
||||
expect(changes).toEqual([
|
||||
{ key: 'activity.view', scope: 'own' },
|
||||
{ key: 'activity.export', scope: 'none' },
|
||||
])
|
||||
})
|
||||
|
||||
it('announces a change in a polite live region', async () => {
|
||||
mountGroup(ACTIVITY)
|
||||
|
||||
radiosOf(wrapper!, 'See activity')[1]!.click()
|
||||
await flushPromises()
|
||||
|
||||
const live = wrapper!.find('[role="status"][aria-live="polite"]')
|
||||
expect(live.text()).toBe('See activity, set to Own.')
|
||||
})
|
||||
|
||||
it('hides the rows while collapsed', () => {
|
||||
mountGroup(ACTIVITY, { open: false })
|
||||
|
||||
expect(wrapper!.text()).not.toContain('Read the log of who did what.')
|
||||
expect(wrapper!.text()).toContain('2 of 2 enabled')
|
||||
})
|
||||
|
||||
it('disables owner-only items, keeps them off and tags them Owner only', async () => {
|
||||
const { changes } = mountGroup(SYSTEM, { grants: {}, initial: {} })
|
||||
|
||||
expect(wrapper!.findAll('[data-testid="owner-only-tag"]')).toHaveLength(2)
|
||||
const toggle = wrapper!.find('[role="switch"][aria-label="Apply software updates"]')
|
||||
expect(toggle.attributes('aria-checked')).toBe('false')
|
||||
expect(toggle.attributes('disabled')).toBeDefined()
|
||||
await toggle.trigger('click')
|
||||
|
||||
expect(changes).toEqual([])
|
||||
expect(wrapper!.text()).toContain('0 of 2 enabled')
|
||||
})
|
||||
|
||||
it('locks All on a scoped row the caller holds only as Own', () => {
|
||||
mountGroup(ACTIVITY, { callerScope: CALLER('activity.view=own, activity.export') })
|
||||
|
||||
const all = radiosOf(wrapper!, 'See activity')[2]!
|
||||
expect(all.getAttribute('aria-disabled')).toBe('true')
|
||||
})
|
||||
|
||||
it('disables everything when read-only', () => {
|
||||
mountGroup(ACTIVITY, { readOnly: true })
|
||||
|
||||
for (const radio of radiosOf(wrapper!, 'See activity')) {
|
||||
expect(radio.getAttribute('aria-disabled')).toBe('true')
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user