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>
77 lines
2.5 KiB
JavaScript
77 lines
2.5 KiB
JavaScript
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()
|
||
})
|
||
})
|