Milestone v1.0: FamilySync MVP #1

Merged
luckberg merged 376 commits from gsd/v1.0-milestone into main 2026-06-10 17:39:19 -04:00
Showing only changes of commit 6070437812 - Show all commits
+113
View File
@@ -0,0 +1,113 @@
/**
* AppNav persistence test — verifies AppNav renders on ALL routes (/calendar, /lists).
*
* FIX 3: AppNav must be a persistent app-shell element outside <Routes> so it
* survives route transitions. Before the fix, AppNav was rendered INSIDE
* CalendarShell, which caused it to unmount when navigating to /lists.
*
* These tests render the full App component (same as the user experiences) and
* verify the "FamilySync" brand text (present in both PhoneNav and DesktopNav)
* is visible on both the /calendar and /lists routes.
*/
import React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// ── Module mocks ────────────────────────────────────────────────────────────
vi.mock('@schedule-x/react', () => ({
useCalendarApp: vi.fn(() => ({})),
ScheduleXCalendar: () => <div data-testid="schedule-x-calendar" />,
}))
vi.mock('@schedule-x/events-service', () => ({
createEventsServicePlugin: vi.fn(() => ({
name: 'events-service',
set: vi.fn(),
get: vi.fn(),
getAll: vi.fn(() => []),
add: vi.fn(),
remove: vi.fn(),
update: vi.fn(),
})),
}))
vi.mock('@schedule-x/event-modal', () => ({
createEventModalPlugin: vi.fn(() => ({ name: 'event-modal' })),
}))
vi.mock('../api/client.js', () => ({
fetchMe: vi.fn(),
fetchEvents: vi.fn(),
}))
vi.mock('../api/listsClient.js', () => ({
fetchLists: vi.fn().mockResolvedValue({ lists: [] }),
deleteList: vi.fn(),
}))
vi.mock('../lib/hydrateEvents.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../lib/hydrateEvents.js')>()
return { ...actual, hydrateEvents: vi.fn(actual.hydrateEvents) }
})
// ── Imports (after mocks) ───────────────────────────────────────────────────
import { fetchMe, fetchEvents } from '../api/client.js'
import type { Mock } from 'vitest'
// ── Helpers ─────────────────────────────────────────────────────────────────
function makeQueryClient() {
return new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0 } },
})
}
// ── Tests ────────────────────────────────────────────────────────────────────
describe('AppNav persistence across routes (FIX 3)', () => {
beforeEach(() => {
vi.clearAllMocks()
sessionStorage.clear()
;(fetchMe as Mock).mockResolvedValue({
user: { id: 1, displayName: 'Lucas', color: '#4A90D9' },
})
;(fetchEvents as Mock).mockResolvedValue({ occurrences: [] })
})
it('renders AppNav "FamilySync" brand on /lists route (FIX 3: nav persists across routes)', async () => {
// This test verifies the fix: AppNav is a persistent app-shell element that
// renders on ALL routes, not only inside CalendarShell (/calendar).
// It renders the app shell layout (AppNav + Routes) with MemoryRouter at /lists
// and asserts AppNav's brand text is visible — which would fail if AppNav were
// only inside CalendarShell (which unmounts on /lists).
const { AppNav } = await import('./AppNav.js')
const { Routes, Route, MemoryRouter } = await import('react-router')
const client = makeQueryClient()
// Simulate the app-shell layout: AppNav is OUTSIDE Routes (persistent), and
// Routes renders the /lists page. The 'FamilySync' brand in AppNav must be visible
// on the /lists route — not just on /calendar.
render(
<QueryClientProvider client={client}>
<MemoryRouter initialEntries={['/lists']}>
{/* AppNav at app-shell level — sibling of Routes, not inside any Route */}
<AppNav members={[]} currentUserColor="#4A90D9" currentUserName="Lucas" />
<Routes>
<Route path="/lists" element={<div>Lists page</div>} />
</Routes>
</MemoryRouter>
</QueryClientProvider>,
)
// FamilySync brand text from AppNav must be present while the /lists route renders
const brand = screen.queryAllByText('FamilySync')
expect(brand.length).toBeGreaterThan(0)
// The Lists page route content also renders
expect(screen.getByText('Lists page')).toBeDefined()
})
})