Files
RenewCMS/web/src/views/__tests__/ActivityCards.spec.ts
T

325 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import router from '@/router'
import { useApi } from '@/composables/useApi'
import { initializeLocale, setLocale } from '@/i18n'
import ActivityCategorySelect from '@/components/ActivityCategorySelect.vue'
import {
LIST_PATH,
SUMMARY_PATH,
callsTo,
chipTexts,
click,
clickButton,
findButton,
harness,
lastListQuery,
mountActivity,
queryObject,
stripCards,
textOf,
unmountActivity,
} from './activityHarness'
import { makeItem, pageOf } from './activityFixtures'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return { ...actual, useApi: vi.fn() }
})
vi.mock('@nuxt/ui/composables', async (importOriginal) => {
const actual = await importOriginal<typeof import('@nuxt/ui/composables')>()
return { ...actual, useToast: () => ({ add: vi.fn() }) }
})
const DEFAULT_QUERY = { range: '7d', page: '1', size: '25' }
const FAILED = 2
const SECURITY = 3
const FAILED_CHIP = 'Outcome: Failed'
const SECURITY_CHIP = 'Type: Security events'
beforeEach(() => {
localStorage.clear()
initializeLocale()
})
afterEach(async () => {
unmountActivity()
vi.mocked(useApi).mockReset()
await router.push('/login')
})
function card(index: number) {
return stripCards()[index]!
}
function pressed(index: number) {
return card(index).getAttribute('aria-pressed')
}
function segmentSelection() {
const group = document.body.querySelector('[role="group"][aria-label="Outcome"]')!
return textOf(group.querySelector('[aria-pressed="true"]'))
}
async function clickSegment(text: string) {
const group = document.body.querySelector('[role="group"][aria-label="Outcome"]')!
await click(
Array.from(group.querySelectorAll('button')).find((button) => textOf(button) === text),
)
}
async function hover(element: Element) {
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }))
await flushPromises()
}
async function leave(element: Element) {
element.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }))
await flushPromises()
}
function hintOf(index: number) {
return textOf(card(index).querySelector('[data-testid="activity-strip-subline"]'))
}
describe('Cards: the toggle cycle keeps card, chip, segment and request in sync', () => {
it.each([
['Failed actions', FAILED, FAILED_CHIP, 'Failed', { outcome: 'failed' }],
['Security events', SECURITY, SECURITY_CHIP, 'All', { security: 'true' }],
])('%s', async (_, index, chip, segment, query) => {
await mountActivity()
const listCalls = callsTo(LIST_PATH).length
await click(card(index))
expect(pressed(index)).toBe('true')
expect(chipTexts()).toEqual([chip])
expect(findButton('Clear all')).toBeDefined()
expect(segmentSelection()).toBe(segment)
expect(callsTo(LIST_PATH)).toHaveLength(listCalls + 1)
expect(queryObject(lastListQuery())).toEqual({ ...DEFAULT_QUERY, ...query })
expect(callsTo(SUMMARY_PATH)).toHaveLength(1)
await click(card(index))
expect(pressed(index)).toBe('false')
expect(chipTexts()).toEqual([])
expect(findButton('Clear all')).toBeUndefined()
expect(segmentSelection()).toBe('All')
expect(queryObject(lastListQuery())).toEqual(DEFAULT_QUERY)
expect(callsTo(SUMMARY_PATH)).toHaveLength(1)
})
})
describe('Cards: every way of removing the filter returns all three controls to default', () => {
async function removeChip(label: string) {
await click(document.body.querySelector(`button[aria-label="Remove filter ${label}"]`))
}
it.each([
['Failed actions', FAILED, FAILED_CHIP, 'the x of the chip', () => removeChip(FAILED_CHIP)],
['Failed actions', FAILED, FAILED_CHIP, 'Clear all', () => clickButton('Clear all')],
['Failed actions', FAILED, FAILED_CHIP, 'All in the segment', () => clickSegment('All')],
[
'Failed actions',
FAILED,
FAILED_CHIP,
'Succeeded in the segment',
() => clickSegment('Succeeded'),
],
[
'Security events',
SECURITY,
SECURITY_CHIP,
'the x of the chip',
() => removeChip(SECURITY_CHIP),
],
['Security events', SECURITY, SECURITY_CHIP, 'Clear all', () => clickButton('Clear all')],
])('%s card removed with %s', async (_, index, chip, __, remove) => {
await mountActivity()
await click(card(index))
expect(chipTexts()).toEqual([chip])
await remove()
expect(pressed(index)).toBe('false')
expect(chipTexts().filter((text) => text === chip)).toEqual([])
if (chip === FAILED_CHIP && __ === 'Succeeded in the segment') {
expect(segmentSelection()).toBe('Succeeded')
expect(chipTexts()).toEqual(['Outcome: Succeeded'])
} else {
expect(segmentSelection()).toBe('All')
expect(findButton('Clear all')).toBeUndefined()
}
})
})
describe('Cards: shared state with the other controls', () => {
it('presses the Failed card when Failed is chosen in the segment', async () => {
await mountActivity()
await clickSegment('Failed')
expect(pressed(FAILED)).toBe('true')
expect(chipTexts()).toEqual([FAILED_CHIP])
})
it('presses both cards together', async () => {
await mountActivity()
await click(card(FAILED))
await click(card(SECURITY))
expect(pressed(FAILED)).toBe('true')
expect(pressed(SECURITY)).toBe('true')
expect(chipTexts()).toEqual([FAILED_CHIP, SECURITY_CHIP])
expect(queryObject(lastListQuery())).toEqual({
...DEFAULT_QUERY,
outcome: 'failed',
security: 'true',
})
})
it('removes only its own chip when the card is unpressed among other filters', async () => {
await mountActivity()
harness.wrapper!.findComponent(ActivityCategorySelect).vm.$emit('select', 'auth')
await flushPromises()
const group = document.body.querySelector('[role="group"][aria-label="Dates"]')!
await click(
Array.from(group.querySelectorAll('button')).find(
(button) => textOf(button) === '30 days',
),
)
await click(card(FAILED))
expect(chipTexts()).toEqual(['Category: Authentication', FAILED_CHIP, 'Dates: 30 days'])
expect(findButton('Clear all')).toBeDefined()
await click(card(FAILED))
expect(chipTexts()).toEqual(['Category: Authentication', 'Dates: 30 days'])
expect(queryObject(lastListQuery())).toEqual({
category: 'auth',
range: '30d',
page: '1',
size: '25',
})
})
it('unpresses the cards from Clear filters in the empty panel', async () => {
await mountActivity({
list: (query) =>
query.get('security') ? pageOf([], { total: 0 }) : pageOf([makeItem({ id: 1 })]),
})
await click(card(SECURITY))
expect(textOf(document.body)).toContain('No activity matches these filters')
await clickButton('Clear filters')
expect(pressed(SECURITY)).toBe('false')
expect(chipTexts()).toEqual([])
})
it('shares the state with the mobile Filters sheet', async () => {
await mountActivity({ desktop: false })
await click(card(FAILED))
expect(textOf(document.body.querySelector('[data-testid="activity-filter-count"]'))).toBe(
'1',
)
await click(
Array.from(document.body.querySelectorAll('button')).find((button) =>
textOf(button).startsWith('Filters'),
),
)
const group = document.body.querySelector('[role="group"][aria-label="Outcome"]')!
expect(textOf(group.querySelector('[aria-pressed="true"]'))).toBe('Failed')
await click(
Array.from(group.querySelectorAll('button')).find((button) => textOf(button) === 'All'),
)
expect(pressed(FAILED)).toBe('false')
})
})
describe('Cards: hint lines, hover and keyboard', () => {
it('words the hint for unpressed, hovered, pressed and pressed-hovered', async () => {
await mountActivity()
const failed = card(FAILED)
expect(hintOf(FAILED)).toBe('0.5% of all actions')
await hover(failed)
expect(hintOf(FAILED)).toBe('Click to filter the list')
expect(failed.className).toContain('hover:bg-elevated')
await leave(failed)
await click(failed)
expect(hintOf(FAILED)).toBe('Filtering. Click to clear')
await hover(failed)
expect(hintOf(FAILED)).toBe('Click to clear this filter')
expect(failed.className).toContain('bg-(--rc-sand)')
expect(failed.className).not.toContain('hover:bg-elevated')
expect(textOf(failed.querySelector('span:last-child'))).toContain(
'Click to clear this filter',
)
})
it('uses the sand tokens for a pressed card and the focus-visible outline', async () => {
await mountActivity()
await click(card(SECURITY))
expect(card(SECURITY).className).toContain('bg-(--rc-sand)')
expect(card(SECURITY).className).toContain('border-primary')
expect(card(SECURITY).querySelector('.text-\\(--rc-on-sand\\)')).not.toBeNull()
expect(card(SECURITY).className).toContain('focus-visible:outline-2')
expect(card(SECURITY).className).toContain('focus-visible:outline-offset-2')
expect(card(SECURITY).className).toContain('focus-visible:outline-primary')
})
it.each([['Enter'], [' ']])('toggles with the %j key on the focused card', async (key) => {
await mountActivity()
const failed = card(FAILED)
failed.focus()
expect(document.activeElement).toBe(failed)
failed.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
failed.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }))
await click(failed)
expect(pressed(FAILED)).toBe('true')
})
})
describe('Cards: Total actions and Active people are not toggles', () => {
it('clears everything from Total and sends nothing from Active people', async () => {
await mountActivity()
await click(card(FAILED))
await click(card(0))
expect(pressed(FAILED)).toBe('false')
expect(chipTexts()).toEqual([])
const before = harness.calls.length
await click(card(1))
expect(harness.calls).toHaveLength(before)
expect(card(0).hasAttribute('aria-pressed')).toBe(false)
expect(card(1).hasAttribute('aria-pressed')).toBe(false)
})
})
describe('Cards: French', () => {
it('words the chip and the hints in French', async () => {
setLocale('fr')
await mountActivity()
await click(card(SECURITY))
expect(chipTexts()).toEqual(['Type : Événements de sécurité'])
expect(hintOf(SECURITY)).toBe('Filtre actif. Cliquez pour l’effacer')
await hover(card(SECURITY))
expect(hintOf(SECURITY)).toBe('Cliquez pour effacer ce filtre')
})
})