thats-me/frontend/test/vitest/components-timeline-nav.spec.js
Kevin Adametz 250f8390cf Timeline: Sprungleiste über dem Monat/Jahr-Pill
Ein Tipp auf das Pill öffnet eine Leiste mit allen Jahren, die Ereignisse
enthalten, samt Dichte (Punkte bis drei, sonst Zahl); ab zwölf Jahren
kommt eine Jahrzehnte-Zeile zum Scrubben dazu. Hat das gewählte Jahr
Ereignisse in mehreren Monaten, erscheint eine Monatszeile, in der nur
belegte Monate aktiv sind. Ein Jahr springt sofort, ein Monat zentriert
dessen Ereignisse; Tippen außerhalb oder Escape schließt.

Ersetzt das q-menu im Pill, das durch doppeltes Umschalten nie sichtbar
wurde. Die Timeline liefert dafür einen Jahr/Monat-Index und kann auf
einen Monat zentrieren.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 09:31:35 +00:00

150 lines
5.9 KiB
JavaScript
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 { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import TimelineNav from 'components/TimelineNav.vue'
function yearEntry(year, months) {
return {
year,
count: months.reduce((sum, [, count]) => sum + count, 0),
months: months.map(([month, count]) => ({ month, count }))
}
}
const YEARS = [
yearEntry(2025, [[3, 1]]),
yearEntry(2026, [[0, 2], [5, 1], [11, 4]]),
yearEntry(2027, [[7, 1]])
]
function mountNav(props = {}) {
return mount(TimelineNav, {
props: {
year: 2026,
month: 'Januar',
monthIndex: 0,
yearIndex: YEARS,
canPrev: true,
canNext: true,
...props
}
})
}
describe('TimelineNav — the position pill', () => {
it('shows month and year of the centred event', () => {
const wrapper = mountNav()
expect(wrapper.find('.timeline-nav__month').text()).toBe('Januar')
expect(wrapper.find('.timeline-nav__year').text()).toBe('2026')
})
it('emits prev and next from the arrows', async () => {
const wrapper = mountNav()
const arrows = wrapper.findAll('.timeline-nav__arrow')
await arrows[0].trigger('click')
await arrows[1].trigger('click')
expect(wrapper.emitted('prev')).toHaveLength(1)
expect(wrapper.emitted('next')).toHaveLength(1)
})
it('disables the arrows at either end of the data', () => {
const wrapper = mountNav({ canPrev: false, canNext: false })
const arrows = wrapper.findAll('.timeline-nav__arrow')
expect(arrows[0].attributes('disabled')).toBeDefined()
expect(arrows[1].attributes('disabled')).toBeDefined()
})
it('shows a dash and no month while there is no event', () => {
const wrapper = mountNav({ year: null, month: '', monthIndex: null, yearIndex: [], canPrev: false, canNext: false })
expect(wrapper.find('.timeline-nav__year').text()).toBe('–')
expect(wrapper.find('.timeline-nav__month').exists()).toBe(false)
expect(wrapper.find('.timeline-nav__label').attributes('disabled')).toBeDefined()
})
})
describe('TimelineNav — the quick-jump strip', () => {
it('opens on tap with every year, its density and the months of the year in view', async () => {
const wrapper = mountNav()
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(false)
await wrapper.find('.timeline-nav__label').trigger('click')
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(true)
const chips = wrapper.findAll('.timeline-nav__chip')
expect(chips.map(chip => chip.find('.timeline-nav__chip-year').text())).toEqual(['2025', '2026', '2027'])
// Up to three events are dots, more is a number.
expect(chips[0].findAll('.timeline-nav__chip-dot')).toHaveLength(1)
expect(chips[1].find('.timeline-nav__chip-count').text()).toBe('7')
expect(chips[1].classes()).toContain('timeline-nav__chip--current')
// 2026 spreads over three months, so the month row is there right away.
const cells = wrapper.findAll('.timeline-nav__month-cell')
expect(cells).toHaveLength(12)
expect(cells.filter(cell => cell.attributes('disabled') === undefined).map(cell => cell.text()))
.toEqual(['Jan', 'Jun', 'Dez'])
expect(cells[0].classes()).toContain('timeline-nav__month-cell--current')
// Three years: no decade row.
expect(wrapper.find('.timeline-nav__decades').exists()).toBe(false)
})
it('jumps to a single-month year and closes', async () => {
const wrapper = mountNav()
await wrapper.find('.timeline-nav__label').trigger('click')
await wrapper.findAll('.timeline-nav__chip')[2].trigger('click')
expect(wrapper.emitted('jump')).toEqual([[2027]])
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(false)
})
it('stays open for a year with several months and jumps to the chosen month', async () => {
const wrapper = mountNav({ year: 2025, month: 'April', monthIndex: 3 })
await wrapper.find('.timeline-nav__label').trigger('click')
// The year in view has one month only: no month row yet.
expect(wrapper.find('.timeline-nav__months').exists()).toBe(false)
await wrapper.findAll('.timeline-nav__chip')[1].trigger('click')
expect(wrapper.emitted('jump')).toEqual([[2026]])
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(true)
expect(wrapper.findAll('.timeline-nav__chip')[1].classes()).toContain('timeline-nav__chip--expanded')
const cells = wrapper.findAll('.timeline-nav__month-cell')
expect(cells[3].attributes('disabled')).toBeDefined()
await cells[11].trigger('click')
expect(wrapper.emitted('jump-month')).toEqual([[{ year: 2026, month: 11 }]])
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(false)
})
it('adds a decade row once there are many years', async () => {
const years = Array.from({ length: 30 }, (_, i) => yearEntry(1990 + i, [[i % 12, 1]]))
const wrapper = mountNav({ year: 2005, month: 'Mai', monthIndex: 4, yearIndex: years })
await wrapper.find('.timeline-nav__label').trigger('click')
const decades = wrapper.findAll('.timeline-nav__decade')
expect(decades.map(decade => decade.text())).toEqual(['1990er', '2000er', '2010er'])
expect(decades[1].classes()).toContain('timeline-nav__decade--current')
})
it('closes on Escape and when tapping elsewhere', async () => {
const wrapper = mountNav({ attachTo: document.body })
await wrapper.find('.timeline-nav__label').trigger('click')
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(true)
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
await wrapper.vm.$nextTick()
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(false)
await wrapper.find('.timeline-nav__label').trigger('click')
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(true)
document.body.dispatchEvent(new Event('pointerdown', { bubbles: true }))
await wrapper.vm.$nextTick()
expect(wrapper.find('.timeline-nav__panel').exists()).toBe(false)
wrapper.unmount()
})
})