style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -13,12 +13,12 @@
|
||||
* path are both invoked without error (the Temporal hydration contracts from hydrateEvents.test.ts).
|
||||
*/
|
||||
|
||||
import 'temporal-polyfill/global'
|
||||
import React from 'react'
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import 'temporal-polyfill/global';
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
|
||||
// window.matchMedia is polyfilled in src/test-setup.ts (loaded via vitest.config setupFiles)
|
||||
|
||||
@@ -34,10 +34,10 @@ vi.mock('@schedule-x/react', () => ({
|
||||
ScheduleXCalendar: ({ calendarApp }: { calendarApp: unknown }) => (
|
||||
<div data-testid="schedule-x-calendar" data-has-app={calendarApp != null ? 'true' : 'false'} />
|
||||
),
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock @schedule-x/events-service to capture .set() calls
|
||||
const mockEventsServiceSet = vi.fn()
|
||||
const mockEventsServiceSet = vi.fn();
|
||||
vi.mock('@schedule-x/events-service', () => ({
|
||||
createEventsServicePlugin: vi.fn(() => ({
|
||||
name: 'events-service',
|
||||
@@ -48,35 +48,35 @@ vi.mock('@schedule-x/events-service', () => ({
|
||||
remove: vi.fn(),
|
||||
update: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock @schedule-x/event-modal
|
||||
vi.mock('@schedule-x/event-modal', () => ({
|
||||
createEventModalPlugin: vi.fn(() => ({
|
||||
name: 'event-modal',
|
||||
})),
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock the API client
|
||||
vi.mock('../api/client.js', () => ({
|
||||
fetchMe: vi.fn(),
|
||||
fetchEvents: vi.fn(),
|
||||
}))
|
||||
}));
|
||||
|
||||
// ── Spy on hydrateEvents (must be after vi.mock but before imports use it) ──
|
||||
vi.mock('../lib/hydrateEvents.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../lib/hydrateEvents.js')>()
|
||||
const actual = await importOriginal<typeof import('../lib/hydrateEvents.js')>();
|
||||
return {
|
||||
...actual,
|
||||
hydrateEvents: vi.fn(actual.hydrateEvents), // spy wrapping real implementation
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// ── Imports (after mocks are in place) ──────────────────────────────────────
|
||||
|
||||
import { fetchMe, fetchEvents } from '../api/client.js'
|
||||
import { hydrateEvents } from '../lib/hydrateEvents.js'
|
||||
import { CalendarShell } from './CalendarShell.js'
|
||||
import { fetchMe, fetchEvents } from '../api/client.js';
|
||||
import { hydrateEvents } from '../lib/hydrateEvents.js';
|
||||
import { CalendarShell } from './CalendarShell.js';
|
||||
|
||||
// ── Fixtures ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -94,7 +94,7 @@ const TIMED_OCCURRENCE = {
|
||||
allDay: false,
|
||||
location: null,
|
||||
description: null,
|
||||
}
|
||||
};
|
||||
|
||||
const ALLDAY_OCCURRENCE = {
|
||||
id: 'allday-uid::2026-06-20',
|
||||
@@ -110,7 +110,7 @@ const ALLDAY_OCCURRENCE = {
|
||||
allDay: true,
|
||||
location: null,
|
||||
description: null,
|
||||
}
|
||||
};
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -123,110 +123,110 @@ function makeQueryClient() {
|
||||
staleTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function renderWithClient(ui: React.ReactElement) {
|
||||
const client = makeQueryClient()
|
||||
const client = makeQueryClient();
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/calendar']}>
|
||||
<QueryClientProvider client={client}>{ui}</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('CalendarShell — CAL-03 render smoke', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.clearAllMocks();
|
||||
// Reset the one-shot redirect guard between tests
|
||||
sessionStorage.clear()
|
||||
sessionStorage.clear();
|
||||
|
||||
// Default mock responses
|
||||
;(fetchMe as Mock).mockResolvedValue({
|
||||
(fetchMe as Mock).mockResolvedValue({
|
||||
user: { id: 1, displayName: 'Lucas', color: '#4A90D9' },
|
||||
})
|
||||
;(fetchEvents as Mock).mockResolvedValue({
|
||||
});
|
||||
(fetchEvents as Mock).mockResolvedValue({
|
||||
occurrences: [TIMED_OCCURRENCE, ALLDAY_OCCURRENCE],
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('renders without throwing with all four views configured (CAL-03 smoke)', () => {
|
||||
// Validates @schedule-x/react@4.1.0 ↔ @schedule-x/calendar@4.6.0 import compatibility (Pitfall 6)
|
||||
expect(() => renderWithClient(<CalendarShell />)).not.toThrow()
|
||||
})
|
||||
expect(() => renderWithClient(<CalendarShell />)).not.toThrow();
|
||||
});
|
||||
|
||||
it('mounts the ScheduleXCalendar with a non-null calendarApp after data loads', async () => {
|
||||
renderWithClient(<CalendarShell />)
|
||||
renderWithClient(<CalendarShell />);
|
||||
// CalendarShell shows SkeletonCalendar during loading; wait for data to resolve
|
||||
const calEl = await screen.findByTestId('schedule-x-calendar')
|
||||
expect(calEl).toBeDefined()
|
||||
expect(calEl.getAttribute('data-has-app')).toBe('true')
|
||||
})
|
||||
const calEl = await screen.findByTestId('schedule-x-calendar');
|
||||
expect(calEl).toBeDefined();
|
||||
expect(calEl.getAttribute('data-has-app')).toBe('true');
|
||||
});
|
||||
|
||||
it('calls hydrateEvents with both timed and all-day occurrences and passes them to eventsService', async () => {
|
||||
renderWithClient(<CalendarShell />)
|
||||
renderWithClient(<CalendarShell />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(hydrateEvents as Mock).toHaveBeenCalled()
|
||||
})
|
||||
expect(hydrateEvents as Mock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const callArgs = (hydrateEvents as Mock).mock.calls[0][0] as typeof TIMED_OCCURRENCE[]
|
||||
const ids = callArgs.map((o) => o.id)
|
||||
expect(ids).toContain(TIMED_OCCURRENCE.id)
|
||||
expect(ids).toContain(ALLDAY_OCCURRENCE.id)
|
||||
const callArgs = (hydrateEvents as Mock).mock.calls[0][0] as (typeof TIMED_OCCURRENCE)[];
|
||||
const ids = callArgs.map((o) => o.id);
|
||||
expect(ids).toContain(TIMED_OCCURRENCE.id);
|
||||
expect(ids).toContain(ALLDAY_OCCURRENCE.id);
|
||||
|
||||
// Verify eventsService.set() was called with the hydrated events
|
||||
await waitFor(() => {
|
||||
expect(mockEventsServiceSet).toHaveBeenCalled()
|
||||
})
|
||||
const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>
|
||||
expect(sxEvents.length).toBe(2)
|
||||
})
|
||||
expect(mockEventsServiceSet).toHaveBeenCalled();
|
||||
});
|
||||
const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>;
|
||||
expect(sxEvents.length).toBe(2);
|
||||
});
|
||||
|
||||
it('converts all-day occurrence to Temporal.PlainDate (Pitfall 4 — no ISO-string rejection)', async () => {
|
||||
renderWithClient(<CalendarShell />)
|
||||
renderWithClient(<CalendarShell />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockEventsServiceSet).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockEventsServiceSet).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>
|
||||
const allDayEvt = sxEvents.find((e) => e.id === ALLDAY_OCCURRENCE.id)
|
||||
expect(allDayEvt).toBeDefined()
|
||||
const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>;
|
||||
const allDayEvt = sxEvents.find((e) => e.id === ALLDAY_OCCURRENCE.id);
|
||||
expect(allDayEvt).toBeDefined();
|
||||
// Must be Temporal.PlainDate, NOT ZonedDateTime — guards all-day date shift (Pitfall 2/4)
|
||||
expect(allDayEvt!.start).toBeInstanceOf(Temporal.PlainDate)
|
||||
expect(allDayEvt!.end).toBeInstanceOf(Temporal.PlainDate)
|
||||
})
|
||||
expect(allDayEvt!.start).toBeInstanceOf(Temporal.PlainDate);
|
||||
expect(allDayEvt!.end).toBeInstanceOf(Temporal.PlainDate);
|
||||
});
|
||||
|
||||
it('converts timed occurrence to Temporal.ZonedDateTime (Pitfall 4 — no ISO-string rejection)', async () => {
|
||||
renderWithClient(<CalendarShell />)
|
||||
renderWithClient(<CalendarShell />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockEventsServiceSet).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockEventsServiceSet).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>
|
||||
const timedEvt = sxEvents.find((e) => e.id === TIMED_OCCURRENCE.id)
|
||||
expect(timedEvt).toBeDefined()
|
||||
expect(timedEvt!.start).toBeInstanceOf(Temporal.ZonedDateTime)
|
||||
expect(timedEvt!.end).toBeInstanceOf(Temporal.ZonedDateTime)
|
||||
})
|
||||
const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>;
|
||||
const timedEvt = sxEvents.find((e) => e.id === TIMED_OCCURRENCE.id);
|
||||
expect(timedEvt).toBeDefined();
|
||||
expect(timedEvt!.start).toBeInstanceOf(Temporal.ZonedDateTime);
|
||||
expect(timedEvt!.end).toBeInstanceOf(Temporal.ZonedDateTime);
|
||||
});
|
||||
|
||||
it('shows sign-in required when /api/me returns an error', () => {
|
||||
;(fetchMe as Mock).mockRejectedValue(new Error('401'))
|
||||
renderWithClient(<CalendarShell />)
|
||||
(fetchMe as Mock).mockRejectedValue(new Error('401'));
|
||||
renderWithClient(<CalendarShell />);
|
||||
// Error renders asynchronously after query settles
|
||||
})
|
||||
});
|
||||
|
||||
it('renders dead-end AuthSplash when redirect guard is already exhausted (D-11)', async () => {
|
||||
// Simulate the one-shot guard having already fired (prior redirect attempt)
|
||||
sessionStorage.setItem('familysync.loginRedirectAttempted', '1')
|
||||
;(fetchMe as Mock).mockRejectedValue(new Error('401'))
|
||||
renderWithClient(<CalendarShell />)
|
||||
sessionStorage.setItem('familysync.loginRedirectAttempted', '1');
|
||||
(fetchMe as Mock).mockRejectedValue(new Error('401'));
|
||||
renderWithClient(<CalendarShell />);
|
||||
// After the auth error and the guard is exhausted, dead-end copy must be visible
|
||||
const tapToRetry = await screen.findByText(/Tap here to try again/i)
|
||||
expect(tapToRetry).toBeDefined()
|
||||
})
|
||||
})
|
||||
const tapToRetry = await screen.findByText(/Tap here to try again/i);
|
||||
expect(tapToRetry).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user