135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
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')
|
|
}
|
|
})
|
|
})
|