thats-me/frontend/test/vitest/components-timeline-nav.spec.js
Kevin Adametz 83c6c5813d Frontend: Offline-Warteschlange, PWA, Event-Panel und Markenauftritt
Offline
- Alle Schreibzugriffe von API-Nutzern laufen über eine Warteschlange in
  IndexedDB (services/syncQueue.js): FIFO, zusammengefasste Updates,
  Wiederholung bei Netz/Sichtbarkeit/Backoff, 4xx wird als Fehler gemeldet.
- Medien-Uploads liegen mit Bytes in der Queue, erscheinen sofort mit
  Marker und werden nach dem Upload durch das Server-Objekt ersetzt.
- PWA-Modus mit Service Worker für die App-Hülle; API nie gecacht.
  npm run build erzeugt dist/pwa.
- Speicherzustand im Panel: ausstehend / Fehler mit Wiederholen-Knopf,
  Verbindungsanzeige im Header.

Event
- Event entsteht erst mit dem ersten Eintrag; jede Löschung fragt nach.
- Key Image als 4:3-Rahmen, Galerie mit Video, mehrere Sprachnotizen mit
  Notiz, Live-Pegel beim Aufnehmen, WAV-Umwandlung für lokale Aufnahmen.
- Neue Felder: Zeitraum, Kategorie, Personen, Meilenstein; Stimmung als
  kompakte Zeile mit ausklappbarer Punktfarbe.

Oberfläche
- Untere Leiste: Darstellung und Zoom links, Jahres-Pille mittig, Plus rechts.
- Logos und Goldpalette, Login neu, zehn Wellen-Presets mit Vorschau,
  Hintergrundbilder als Kacheln, Kontomenü und Einstellungen bereinigt,
  Escape schließt Ebenen, größere Touch-Ziele.
- Alten Sync-Service und ungenutzte Legacy-Seiten entfernt.

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

77 lines
2.5 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'
// The menu is a portal; rendering it inline keeps the year list testable.
const inlineMenu = {
QMenu: {
props: ['modelValue'],
template: '<div v-if="modelValue" class="menu-stub"><slot /></div>'
}
}
function mountNav(props = {}) {
return mount(TimelineNav, {
props: {
year: 2026,
month: 'Januar',
years: [2025, 2026, 2027],
canPrev: true,
canNext: true,
...props
},
global: { stubs: inlineMenu }
})
}
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('opens the year list on tap and emits the chosen year', async () => {
const wrapper = mountNav()
expect(wrapper.find('.menu-stub').exists()).toBe(false)
await wrapper.find('.timeline-nav__label').trigger('click')
expect(wrapper.find('.menu-stub').exists()).toBe(true)
const items = wrapper.findAll('.timeline-nav__year-item')
expect(items.map(item => item.text())).toEqual(['2025', '2026', '2027'])
await items[2].trigger('click')
expect(wrapper.emitted('jump')).toEqual([[2027]])
// Choosing closes the list.
expect(wrapper.find('.menu-stub').exists()).toBe(false)
})
it('shows a dash and no month while there is no event', () => {
const wrapper = mountNav({ year: null, month: '', years: [], 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()
})
})