306 lines
9.6 KiB
TypeScript
306 lines
9.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||
import { initializeLocale, setLocale } from '@/i18n'
|
||
import {
|
||
activeChips,
|
||
bodyDeltaLabel,
|
||
changeValue,
|
||
clearChip,
|
||
defaultFilters,
|
||
exportQuery,
|
||
fieldLabel,
|
||
initials,
|
||
listQuery,
|
||
summaryQuery,
|
||
targetLink,
|
||
type ActivityFilters,
|
||
} from '../activity'
|
||
import { dayLabel, formatDateRange, groupByDay, whenLabel } from '../activityTime'
|
||
import { barHeights, stripCards } from '../activitySummary'
|
||
import { summaryDefault, summaryNone, summaryZero } from '@/views/__tests__/activityFixtures'
|
||
|
||
beforeEach(() => {
|
||
localStorage.clear()
|
||
initializeLocale()
|
||
})
|
||
|
||
afterEach(() => {
|
||
localStorage.clear()
|
||
initializeLocale()
|
||
})
|
||
|
||
function filters(overrides: Partial<ActivityFilters> = {}): ActivityFilters {
|
||
return { ...defaultFilters(), ...overrides }
|
||
}
|
||
|
||
function entries(query: string) {
|
||
return Object.fromEntries(new URLSearchParams(query))
|
||
}
|
||
|
||
describe('Queries', () => {
|
||
it('always sends range, page and size and nothing else by default', () => {
|
||
expect(entries(listQuery(filters(), 1, 25))).toEqual({ range: '7d', page: '1', size: '25' })
|
||
})
|
||
|
||
it('adds q, actor, category, outcome and security only when set', () => {
|
||
const query = listQuery(
|
||
filters({
|
||
q: 'spring',
|
||
actor: 'user:3',
|
||
category: 'auth',
|
||
outcome: 'failed',
|
||
security: true,
|
||
}),
|
||
2,
|
||
50,
|
||
)
|
||
|
||
expect(entries(query)).toEqual({
|
||
q: 'spring',
|
||
actor: 'user:3',
|
||
category: 'auth',
|
||
outcome: 'failed',
|
||
security: 'true',
|
||
range: '7d',
|
||
page: '2',
|
||
size: '50',
|
||
})
|
||
})
|
||
|
||
it('sends from and to only with the custom range', () => {
|
||
expect(
|
||
entries(
|
||
listQuery(filters({ range: '30d', from: '2026-09-01', to: '2026-09-30' }), 1, 25),
|
||
),
|
||
).toEqual({ range: '30d', page: '1', size: '25' })
|
||
expect(
|
||
entries(
|
||
listQuery(
|
||
filters({ range: 'custom', from: '2026-09-01', to: '2026-09-30' }),
|
||
1,
|
||
25,
|
||
),
|
||
),
|
||
).toEqual({ range: 'custom', from: '2026-09-01', to: '2026-09-30', page: '1', size: '25' })
|
||
})
|
||
|
||
it('never sends an actor for a regular user', () => {
|
||
expect(entries(listQuery(filters({ actor: 'system' }), 1, 25, false)).actor).toBeUndefined()
|
||
})
|
||
|
||
it('keeps the summary to the date range and the export to the list filters', () => {
|
||
const all = filters({
|
||
q: 'x',
|
||
category: 'media',
|
||
range: 'custom',
|
||
from: '2026-09-01',
|
||
to: '2026-09-30',
|
||
})
|
||
|
||
expect(entries(summaryQuery(all))).toEqual({
|
||
range: 'custom',
|
||
from: '2026-09-01',
|
||
to: '2026-09-30',
|
||
})
|
||
expect(entries(exportQuery(all))).toEqual({
|
||
q: 'x',
|
||
category: 'media',
|
||
range: 'custom',
|
||
from: '2026-09-01',
|
||
to: '2026-09-30',
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('Chips', () => {
|
||
it('lists the active filters in order and never the default range', () => {
|
||
expect(activeChips(filters())).toEqual([])
|
||
expect(
|
||
activeChips(
|
||
filters({
|
||
q: 'spring',
|
||
actor: 'user:3',
|
||
actorLabel: 'Marta Alves',
|
||
category: 'auth',
|
||
outcome: 'failed',
|
||
security: true,
|
||
range: '30d',
|
||
}),
|
||
).map((chip) => chip.label),
|
||
).toEqual([
|
||
'Search: spring',
|
||
'Person: Marta Alves',
|
||
'Category: Authentication',
|
||
'Outcome: Failed',
|
||
'Type: Security events',
|
||
'Dates: 30 days',
|
||
])
|
||
})
|
||
|
||
it('words custom ranges', () => {
|
||
const range = (from: string, to: string) =>
|
||
activeChips(filters({ range: 'custom', from, to }))[0]!.label
|
||
|
||
expect(range('2026-09-01', '2026-09-30')).toBe('Dates: 1 – 30 Sep 2026')
|
||
expect(range('2026-09-28', '2026-10-03')).toBe('Dates: 28 Sep – 3 Oct 2026')
|
||
expect(range('2025-12-28', '2026-01-03')).toBe('Dates: 28 Dec 2025 – 3 Jan 2026')
|
||
expect(range('2026-09-05', '2026-09-05')).toBe('Dates: 5 Sep 2026')
|
||
})
|
||
|
||
it('words custom ranges in French', () => {
|
||
setLocale('fr')
|
||
|
||
expect(formatDateRange('2026-10-01', '2026-10-30')).toBe('1 – 30 oct. 2026')
|
||
})
|
||
|
||
it('resets one filter at a time', () => {
|
||
const all = filters({ q: 'a', actor: 'system', actorLabel: 'System', range: '30d' })
|
||
|
||
expect(clearChip(all, 'q').q).toBe('')
|
||
expect(clearChip(all, 'actor')).toMatchObject({ actor: '', actorLabel: '' })
|
||
expect(clearChip(all, 'range').range).toBe('7d')
|
||
})
|
||
})
|
||
|
||
describe('Times', () => {
|
||
const now = new Date(2026, 9, 6, 15, 0, 0)
|
||
|
||
it('labels days as Today, Yesterday or the date', () => {
|
||
expect(dayLabel(new Date(2026, 9, 6, 9, 0).toISOString(), now)).toBe('Today')
|
||
expect(dayLabel(new Date(2026, 9, 5, 23, 59).toISOString(), now)).toBe('Yesterday')
|
||
expect(dayLabel(new Date(2026, 9, 3, 14, 20).toISOString(), now)).toBe('3 Oct 2026')
|
||
setLocale('fr')
|
||
expect(dayLabel(new Date(2026, 9, 6, 9, 0).toISOString(), now)).toBe('Aujourd’hui')
|
||
expect(dayLabel(new Date(2026, 9, 3, 14, 20).toISOString(), now)).toBe('3 oct. 2026')
|
||
})
|
||
|
||
it('reads relative within today and as a clock time before', () => {
|
||
expect(whenLabel(new Date(2026, 9, 6, 14, 58).toISOString(), now)).toBe('2 min ago')
|
||
expect(whenLabel(new Date(2026, 9, 6, 14, 31).toISOString(), now)).toBe('29 min ago')
|
||
expect(whenLabel(new Date(2026, 9, 6, 14, 0).toISOString(), now)).toBe('1 h ago')
|
||
expect(whenLabel(new Date(2026, 9, 5, 18, 20).toISOString(), now)).toBe('18:20')
|
||
expect(whenLabel(new Date(2026, 9, 6, 14, 59, 50).toISOString(), now)).toBe('1 min ago')
|
||
})
|
||
|
||
it('groups consecutive items of the same local day in API order', () => {
|
||
const groups = groupByDay(
|
||
[
|
||
{ created_at: new Date(2026, 9, 6, 12).toISOString(), id: 1 },
|
||
{ created_at: new Date(2026, 9, 6, 9).toISOString(), id: 2 },
|
||
{ created_at: new Date(2026, 9, 5, 20).toISOString(), id: 3 },
|
||
],
|
||
now,
|
||
)
|
||
|
||
expect(groups.map((group) => [group.label, group.items.map((entry) => entry.id)])).toEqual([
|
||
['Today', [1, 2]],
|
||
['Yesterday', [3]],
|
||
])
|
||
})
|
||
})
|
||
|
||
describe('Labels', () => {
|
||
it('takes the first letters of the first two words', () => {
|
||
expect(initials('Marta Alves')).toBe('MA')
|
||
expect(initials('inès duarte jr')).toBe('ID')
|
||
expect(initials('Madonna')).toBe('M')
|
||
})
|
||
|
||
it('links each target type, and nothing for a deleted or unknown one', () => {
|
||
const link = (type: string, exists = true) =>
|
||
targetLink({ type, id: '8', name: 'x', exists })
|
||
|
||
expect(link('article')).toBe('/article/8/edit')
|
||
expect(link('image')).toBe('/media')
|
||
expect(link('user')).toBe('/users')
|
||
expect(link('invite')).toBe('/users')
|
||
expect(link('api_key')).toBe('/integration/api-keys')
|
||
expect(link('release')).toBeNull()
|
||
expect(link('article', false)).toBeNull()
|
||
expect(targetLink(null)).toBeNull()
|
||
})
|
||
|
||
it('labels fields and values, keeping unknown codes', () => {
|
||
expect(fieldLabel('title')).toBe('Title')
|
||
expect(fieldLabel('online')).toBe('Publish state')
|
||
expect(fieldLabel('mystery')).toBe('mystery')
|
||
expect(changeValue('online', 'false')).toBe('Draft')
|
||
expect(changeValue('online', 'true')).toBe('Published')
|
||
expect(changeValue('title', 'true')).toBe('true')
|
||
setLocale('fr')
|
||
expect(fieldLabel('filename')).toBe('Nom du fichier')
|
||
expect(changeValue('online', 'true')).toBe('Publié')
|
||
})
|
||
|
||
it('words body deltas with sign and singular', () => {
|
||
expect(bodyDeltaLabel(312)).toBe('Body edited (+312 characters)')
|
||
expect(bodyDeltaLabel(-48)).toBe('Body edited (-48 characters)')
|
||
expect(bodyDeltaLabel(1)).toBe('Body edited (+1 character)')
|
||
})
|
||
})
|
||
|
||
describe('Summary strip', () => {
|
||
it('derives the four cards from the summary', () => {
|
||
const cards = stripCards(summaryDefault, '7d')
|
||
|
||
expect(cards.map((card) => [card.label, card.value, card.subline])).toEqual([
|
||
['Total actions', '1,284', '12% vs previous 7 days'],
|
||
['Active people', '9', '7 people, 2 API keys'],
|
||
['Failed actions', '6', '0.5% of all actions'],
|
||
['Security events', '11', '11 flagged'],
|
||
])
|
||
expect(cards.map((card) => card.tone)).toEqual(['neutral', 'neutral', 'error', 'warning'])
|
||
expect(cards[0]!.icon).toBe('i-lucide-trending-up')
|
||
})
|
||
|
||
it('words the comparison for each range', () => {
|
||
const subline = (range: 'today' | '7d' | '30d' | 'custom') =>
|
||
stripCards(summaryDefault, range)[0]!.subline
|
||
|
||
expect(subline('today')).toBe('12% vs previous day')
|
||
expect(subline('30d')).toBe('12% vs previous 30 days')
|
||
expect(subline('custom')).toBe('12% vs previous period')
|
||
})
|
||
|
||
it('goes calm on zero and reads No data yet on an empty site', () => {
|
||
const quiet = stripCards(summaryZero, '7d')
|
||
expect(quiet[0]!.icon).toBe('i-lucide-trending-down')
|
||
expect(quiet.slice(2).map((card) => [card.tone, card.subline, card.icon])).toEqual([
|
||
['calm', 'Nothing failed', 'i-lucide-circle-check'],
|
||
['calm', 'Nothing to flag', 'i-lucide-circle-check'],
|
||
])
|
||
|
||
const none = stripCards(summaryNone, '7d')
|
||
expect(none.map((card) => card.subline)).toEqual([
|
||
'No data yet',
|
||
'No data yet',
|
||
'Nothing failed',
|
||
'Nothing to flag',
|
||
])
|
||
})
|
||
|
||
it('words singulars, the French forms and the flag count', () => {
|
||
const single = stripCards(
|
||
{ ...summaryDefault, active_people: { users: 1, api_keys: 1 }, security: 1 },
|
||
'7d',
|
||
)
|
||
expect(single[1]!.subline).toBe('1 person, 1 API key')
|
||
expect(single[3]!.subline).toBe('1 flagged')
|
||
|
||
setLocale('fr')
|
||
expect(stripCards(summaryDefault, '7d')[3]!.subline).toBe('11 signalés')
|
||
expect(stripCards({ ...summaryDefault, security: 1 }, '7d')[3]!.subline).toBe('1 signalé')
|
||
expect(
|
||
stripCards({ ...summaryDefault, total: 40, previous_total: 0 }, '7d')[0]!.subline,
|
||
).toBe('Aucune donnée antérieure')
|
||
expect(
|
||
stripCards({ ...summaryDefault, total: 40, previous_total: 40 }, '7d')[0]!.subline,
|
||
).toBe('Aucun changement')
|
||
})
|
||
|
||
it('sizes bars from the peak with a 2 px minimum', () => {
|
||
expect(barHeights([4, 6, 5, 8, 7, 9, 6], 20)).toEqual([9, 13, 11, 18, 16, 20, 13])
|
||
expect(barHeights([0, 0, 0, 0, 0, 0, 0], 20)).toEqual([2, 2, 2, 2, 2, 2, 2])
|
||
expect(barHeights([0, 1, 0, 0, 0, 0, 100], 16)).toEqual([2, 2, 2, 2, 2, 2, 16])
|
||
})
|
||
})
|