325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
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')
|
||
})
|
||
})
|