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() return { ...actual, useApi: vi.fn() } }) vi.mock('@nuxt/ui/composables', async (importOriginal) => { const actual = await importOriginal() 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') }) })