mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
b194e8317d
Add genuine offline read/write capability for trips: - Dexie IndexedDB schema (trips, places, packing, todo, budget, reservations, files, mutationQueue, syncMeta, blobCache) - Repo layer for all domains: offline reads from Dexie, writes optimistically to Dexie and enqueue mutations for later replay - Mutation queue with UUID idempotency keys (X-Idempotency-Key), FIFO flush, temp-ID reconciliation on 2xx, fail-and-continue on 4xx - Trip sync manager: caches all trips with end_date >= today or null, auto-evicts 7d after end_date, fetches bundle endpoint in one request - Map tile prefetcher: bbox from place coords, zooms 10-16, 50MB cap, warms SW cache via fetch - Sync triggers: network online → flush + syncAll; WS reconnect → flush only (rate-limiter safe); visibilitychange/30s → flush only - WS remoteEventHandler writes through to Dexie on every event - Server idempotency middleware + idempotency_keys table (migration 100, 24h TTL nightly cleanup) - GET /api/trips/:id/bundle endpoint for efficient single-request sync - OfflineBanner component: amber (offline) / blue (syncing) / hidden - OfflineTab in Settings: cached trip list, re-sync and clear actions - usePendingMutations hook for per-item pending indicators Closes #505 #541
146 lines
5.8 KiB
TypeScript
146 lines
5.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { render, screen, fireEvent } from '../../../tests/helpers/render'
|
|
import { resetAllStores, seedStore } from '../../../tests/helpers/store'
|
|
import { buildUser } from '../../../tests/helpers/factories'
|
|
import { useAuthStore } from '../../store/authStore'
|
|
|
|
vi.mock('./CollabChat', () => ({ default: () => <div data-testid="collab-chat">Chat</div> }))
|
|
vi.mock('./CollabNotes', () => ({ default: () => <div data-testid="collab-notes">Notes</div> }))
|
|
vi.mock('./CollabPolls', () => ({ default: () => <div data-testid="collab-polls">Polls</div> }))
|
|
vi.mock('./WhatsNextWidget', () => ({ default: () => <div data-testid="whats-next">WhatsNext</div> }))
|
|
vi.mock('../../api/websocket', () => ({
|
|
connect: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
getSocketId: vi.fn(() => null),
|
|
setRefetchCallback: vi.fn(),
|
|
setPreReconnectHook: vi.fn(),
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
}))
|
|
|
|
import CollabPanel from './CollabPanel'
|
|
|
|
let originalInnerWidth: number
|
|
|
|
function setViewport(width: number) {
|
|
Object.defineProperty(window, 'innerWidth', { value: width, writable: true, configurable: true })
|
|
window.dispatchEvent(new Event('resize'))
|
|
}
|
|
|
|
describe('CollabPanel', () => {
|
|
beforeEach(() => {
|
|
originalInnerWidth = window.innerWidth
|
|
resetAllStores()
|
|
seedStore(useAuthStore, { user: buildUser() })
|
|
})
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, writable: true, configurable: true })
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-001
|
|
it('desktop layout renders all four panels', () => {
|
|
setViewport(1280)
|
|
render(<CollabPanel tripId={1} />)
|
|
expect(screen.getByTestId('collab-chat')).toBeInTheDocument()
|
|
expect(screen.getByTestId('collab-notes')).toBeInTheDocument()
|
|
expect(screen.getByTestId('collab-polls')).toBeInTheDocument()
|
|
expect(screen.getByTestId('whats-next')).toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-002
|
|
it('mobile layout renders tab bar, not all panels at once', () => {
|
|
setViewport(375)
|
|
render(<CollabPanel tripId={1} />)
|
|
// Tab buttons exist
|
|
expect(screen.getByRole('button', { name: /chat/i })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /notes/i })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /polls/i })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /what.?s next/i })).toBeInTheDocument()
|
|
// Only chat visible by default
|
|
expect(screen.getByTestId('collab-chat')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('collab-notes')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('collab-polls')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('whats-next')).not.toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-003
|
|
it('mobile: clicking Notes tab switches to CollabNotes', () => {
|
|
setViewport(375)
|
|
render(<CollabPanel tripId={1} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /notes/i }))
|
|
expect(screen.getByTestId('collab-notes')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('collab-chat')).not.toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-004
|
|
it('mobile: clicking Polls tab switches to CollabPolls', () => {
|
|
setViewport(375)
|
|
render(<CollabPanel tripId={1} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /polls/i }))
|
|
expect(screen.getByTestId('collab-polls')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('collab-chat')).not.toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-005
|
|
it('mobile: clicking What\'s Next tab shows WhatsNextWidget', () => {
|
|
setViewport(375)
|
|
render(<CollabPanel tripId={1} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /what.?s next/i }))
|
|
expect(screen.getByTestId('whats-next')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('collab-chat')).not.toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-006
|
|
it('mobile: active tab button has accent background style', () => {
|
|
setViewport(375)
|
|
render(<CollabPanel tripId={1} />)
|
|
const chatButton = screen.getByRole('button', { name: /chat/i })
|
|
expect(chatButton.style.background).toBe('var(--accent)')
|
|
const notesButton = screen.getByRole('button', { name: /notes/i })
|
|
expect(notesButton.style.background).toBe('transparent')
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-007
|
|
it('mobile: default active tab is Chat', () => {
|
|
setViewport(375)
|
|
render(<CollabPanel tripId={1} />)
|
|
expect(screen.getByTestId('collab-chat')).toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-008
|
|
it('tripMembers prop is forwarded to WhatsNextWidget', () => {
|
|
setViewport(1280)
|
|
render(<CollabPanel tripId={1} tripMembers={[{ id: 5, username: 'alice', avatar_url: null }]} />)
|
|
expect(screen.getByTestId('whats-next')).toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-009
|
|
it('tripId prop is forwarded to child components', () => {
|
|
setViewport(1280)
|
|
render(<CollabPanel tripId={1} />)
|
|
// All children render without errors, confirming props were forwarded
|
|
expect(screen.getByTestId('collab-chat')).toBeInTheDocument()
|
|
expect(screen.getByTestId('collab-notes')).toBeInTheDocument()
|
|
expect(screen.getByTestId('collab-polls')).toBeInTheDocument()
|
|
})
|
|
|
|
// FE-COMP-COLLABPANEL-010
|
|
it('resize from desktop to mobile hides side-by-side layout', () => {
|
|
setViewport(1280)
|
|
const { rerender } = render(<CollabPanel tripId={1} />)
|
|
// All four panels visible on desktop
|
|
expect(screen.getByTestId('collab-chat')).toBeInTheDocument()
|
|
expect(screen.getByTestId('collab-notes')).toBeInTheDocument()
|
|
|
|
// Switch to mobile
|
|
setViewport(375)
|
|
rerender(<CollabPanel tripId={1} />)
|
|
|
|
// Tab bar appears, only chat visible
|
|
expect(screen.getByRole('button', { name: /chat/i })).toBeInTheDocument()
|
|
expect(screen.getByTestId('collab-chat')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('collab-notes')).not.toBeInTheDocument()
|
|
})
|
|
})
|