From 03e953158ab18a5d661786f2685f3e8e4ab223c1 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 11 Jun 2026 20:23:38 -0400 Subject: [PATCH] =?UTF-8?q?fix(13-02):=20eliminate=20all=20ESLint=20violat?= =?UTF-8?q?ions=20=E2=80=94=20pnpm=20lint=20exits=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - eslint.config.js: disable React Compiler rules (v7 flat.recommended enables them; codebase does not use the Compiler); add e2e/ to disableTypeChecked block; promote exhaustive-deps to error - API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler, expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument disables with justifying comments inside try blocks - API routes/sse.ts: fix no-misused-promises on async writeSSE callback with void+IIFE+catch pattern - API routes/lists.ts: let → const for updateValues - API tests: remove unused imports (beforeEach, eq, vi); rename unused vars with _ prefix; remove unused lastActiveId assignment - PWA components: void navigate() and void queryClient.invalidateQueries() on all fire-and-forget call sites; fix CalendarShell explicit-type-casts; Couldn't → HTML entity - PWA test files: as unknown as Response for partial mock objects; string | null type annotation on mockLastSyncedUid; remove async from test callbacks without await; act(() => {}) not await act(async () => {}) for sync ops - sw.ts: restructure Notification.data?.url access as let+if so disable comments land on the exact violation lines; void self.skipWaiting() --- apps/api/src/broker/expand.ts | 4 +- apps/api/src/broker/outboxWorker.ts | 32 ++++----- apps/api/src/broker/poller.ts | 2 +- apps/api/src/broker/reminderScheduler.ts | 2 +- apps/api/src/broker/spike.ts | 4 +- apps/api/src/broker/sync.ts | 5 +- apps/api/src/broker/vevent.ts | 2 + apps/api/src/routes/lists.ts | 2 +- apps/api/src/routes/sse.ts | 18 +++-- apps/api/tests/auth/devBypass.test.ts | 2 +- apps/api/tests/broker/poller.test.ts | 6 +- apps/api/tests/routes/events.test.ts | 2 +- apps/api/tests/routes/lists.test.ts | 7 +- apps/pwa/src/api/client.test.ts | 67 ++++++++++--------- apps/pwa/src/components/AppNav.test.tsx | 2 +- apps/pwa/src/components/CalendarShell.tsx | 10 +-- apps/pwa/src/components/CreateListSheet.tsx | 8 +-- .../DeleteConfirmationDialog.test.tsx | 2 +- apps/pwa/src/components/EventForm.test.tsx | 12 ++-- apps/pwa/src/components/EventForm.tsx | 2 +- .../pwa/src/components/InstallPrompt.test.tsx | 4 +- .../src/components/InstructionSheet.test.tsx | 2 +- apps/pwa/src/components/ListCard.tsx | 2 +- .../src/components/SyncStateToast.test.tsx | 2 +- apps/pwa/src/hooks/useListSSE.test.ts | 2 +- apps/pwa/src/hooks/useListSSE.ts | 7 +- apps/pwa/src/routes/ListDetail.test.tsx | 20 +++--- apps/pwa/src/routes/ListDetail.tsx | 14 ++-- apps/pwa/src/routes/ListsIndex.tsx | 7 +- apps/pwa/src/sw.ts | 19 ++++-- eslint.config.js | 27 ++++++++ 31 files changed, 176 insertions(+), 121 deletions(-) diff --git a/apps/api/src/broker/expand.ts b/apps/api/src/broker/expand.ts index 63888b2..e4dbd0b 100644 --- a/apps/api/src/broker/expand.ts +++ b/apps/api/src/broker/expand.ts @@ -180,11 +180,13 @@ export function expandOccurrences( // --- 1. Parse VCALENDAR — return [] on malformed input (matches sync.ts pattern) --- let parsed: ReturnType try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- ical.js parse() returns 'any'; result is only passed to ICAL.Component which accepts it parsed = ICAL.parse(rawVevent) } catch { return [] } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- ical.js parse() output is 'any'; ICAL.Component is the correct consumer of this value const comp = new ICAL.Component(parsed) // --- 2. Register VTIMEZONE components BEFORE constructing RecurExpansion (MANDATORY) --- @@ -230,7 +232,7 @@ export function expandOccurrences( // Use ICAL.Event.endDate which derives end from DTEND, or DTSTART+DURATION, or sensible default. // Do NOT use getFirstPropertyValue('dtend') directly — events with only DURATION set return null, // producing zero-duration occurrences (BUG 1). - let occEnd: ICAL.Time = (event.endDate ?? dtstart) as ICAL.Time + let occEnd: ICAL.Time = event.endDate ?? dtstart // Positive-duration guard: ensure timed events have non-zero height in Schedule-X. if (!allDay && occEnd.compare(dtstart) <= 0) { diff --git a/apps/api/src/broker/outboxWorker.ts b/apps/api/src/broker/outboxWorker.ts index 129e607..1ad40e9 100644 --- a/apps/api/src/broker/outboxWorker.ts +++ b/apps/api/src/broker/outboxWorker.ts @@ -372,7 +372,7 @@ async function dispatchRow(row: OutboxRow): Promise { rruleFromPayload, fields.recurrenceUntil, fields.recurrenceCount, - fields.allDay as boolean, + fields.allDay, ) : undefined } else if (preservedRrule) { @@ -383,7 +383,7 @@ async function dispatchRow(row: OutboxRow): Promise { strippedPreset, fields.recurrenceUntil, fields.recurrenceCount, - fields.allDay as boolean, + fields.allDay, ) } else { finalRruleString = preservedRrule @@ -394,12 +394,12 @@ async function dispatchRow(row: OutboxRow): Promise { const { icsString } = buildVeventString({ uid: row.uid, - summary: fields.title as string, - allDay: fields.allDay as boolean, - dtstart: fields.allDay ? (fields.start as string) : new Date(fields.start as string), - dtend: fields.allDay ? (fields.end as string) : new Date(fields.end as string), - location: fields.location as string | undefined, - description: fields.description as string | undefined, + summary: fields.title, + allDay: fields.allDay, + dtstart: fields.allDay ? fields.start : new Date(fields.start), + dtend: fields.allDay ? fields.end : new Date(fields.end), + location: fields.location, + description: fields.description, rruleString: finalRruleString, }) @@ -463,7 +463,7 @@ async function dispatchRow(row: OutboxRow): Promise { rruleFromPayload, fields.recurrenceUntil, fields.recurrenceCount, - fields.allDay as boolean, + fields.allDay, ) : undefined } else if (preservedRrule) { @@ -474,7 +474,7 @@ async function dispatchRow(row: OutboxRow): Promise { strippedPreset, fields.recurrenceUntil, fields.recurrenceCount, - fields.allDay as boolean, + fields.allDay, ) } else { finalRruleString = preservedRrule @@ -485,12 +485,12 @@ async function dispatchRow(row: OutboxRow): Promise { const { icsString } = buildVeventString({ uid: row.uid, - summary: fields.title as string, - allDay: fields.allDay as boolean, - dtstart: fields.allDay ? (fields.start as string) : new Date(fields.start as string), - dtend: fields.allDay ? (fields.end as string) : new Date(fields.end as string), - location: fields.location as string | undefined, - description: fields.description as string | undefined, + summary: fields.title, + allDay: fields.allDay, + dtstart: fields.allDay ? fields.start : new Date(fields.start), + dtend: fields.allDay ? fields.end : new Date(fields.end), + location: fields.location, + description: fields.description, rruleString: finalRruleString, }) // Build a minimal DAVCalendar for the write wrapper (only url is needed) diff --git a/apps/api/src/broker/poller.ts b/apps/api/src/broker/poller.ts index 64bc6c2..1919772 100644 --- a/apps/api/src/broker/poller.ts +++ b/apps/api/src/broker/poller.ts @@ -56,7 +56,7 @@ export async function runPoll(): Promise { // ctag/syncToken: defensive null handling (Pitfall #6) const knownCtag = stored?.ctag ?? null - const currentCtag = (davCal.ctag ?? davCal.syncToken ?? null) as string | null + const currentCtag = davCal.ctag ?? davCal.syncToken ?? null // Skip if ctag is present on both sides and unchanged if (currentCtag !== null && currentCtag === knownCtag) { diff --git a/apps/api/src/broker/reminderScheduler.ts b/apps/api/src/broker/reminderScheduler.ts index 91ab3ea..2da6f88 100644 --- a/apps/api/src/broker/reminderScheduler.ts +++ b/apps/api/src/broker/reminderScheduler.ts @@ -129,7 +129,7 @@ export async function runReminderCheck(now = new Date()): Promise { if (row.subId != null) { byUid.get(row.uid)!.subs.push({ id: row.subId, - userId: row.subUserId!, + userId: row.subUserId, endpoint: row.subEndpoint, p256dh: row.subP256dh, auth: row.subAuth, diff --git a/apps/api/src/broker/spike.ts b/apps/api/src/broker/spike.ts index ca75abe..004715f 100644 --- a/apps/api/src/broker/spike.ts +++ b/apps/api/src/broker/spike.ts @@ -50,7 +50,9 @@ async function main() { for (const cal of calendars) { console.log('---') console.log(` url: ${cal.url}`) - console.log(` displayName: ${cal.displayName ?? '(none)'}`) + // displayName may be a string or a Record (language-tagged value) per CalDAV spec + const displayName = typeof cal.displayName === 'string' ? cal.displayName : JSON.stringify(cal.displayName ?? '(none)') + console.log(` displayName: ${displayName}`) // ctag/syncToken: Fastmail may return either field (Pitfall #6) console.log(` ctag: ${(cal as { ctag?: string }).ctag ?? '(not returned)'}`) console.log(` syncToken: ${(cal as { syncToken?: string }).syncToken ?? '(not returned)'}`) diff --git a/apps/api/src/broker/sync.ts b/apps/api/src/broker/sync.ts index a9ed7f1..4386fef 100644 --- a/apps/api/src/broker/sync.ts +++ b/apps/api/src/broker/sync.ts @@ -86,12 +86,14 @@ export async function syncCalendar( let parsed: ReturnType try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- ical.js parse() returns 'any'; result is only passed to ICAL.Component which accepts it parsed = ICAL.parse(obj.data as string) } catch { // Malformed VCALENDAR — skip but do not crash the sync continue } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- ical.js parse() output is 'any'; ICAL.Component is the correct consumer of this value const comp = new ICAL.Component(parsed) const vevent = comp.getFirstSubcomponent('vevent') if (!vevent) continue @@ -198,13 +200,14 @@ export async function syncCalendar( if ((oldRow.allDay ? true : false) !== allDay) changedFields.push('allDay') // Compare title (SUMMARY) - const oldTitle = (oldRow.title ?? null) as string | null + const oldTitle = oldRow.title ?? null if (oldTitle !== titleValue) changedFields.push('title') // Compare location — extract from old rawVevent for comparison let oldLocation: string | null = null if (oldRow.rawVevent) { try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- ical.js parse() returns 'any'; ICAL.Component is the correct consumer of this value const oldComp = new ICAL.Component(ICAL.parse(oldRow.rawVevent)) const oldVevent = oldComp.getFirstSubcomponent('vevent') if (oldVevent) { diff --git a/apps/api/src/broker/vevent.ts b/apps/api/src/broker/vevent.ts index 0cec54d..71d84ce 100644 --- a/apps/api/src/broker/vevent.ts +++ b/apps/api/src/broker/vevent.ts @@ -63,10 +63,12 @@ export const RRULE_PRESETS: Record = { export function extractRruleString(rawVevent: string): string | undefined { let parsed: ReturnType try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- ical.js parse() returns 'any'; result is only passed to ICAL.Component which accepts it parsed = ICAL.parse(rawVevent) } catch { return undefined } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- ical.js parse() output is 'any'; ICAL.Component is the correct consumer of this value const comp = new ICAL.Component(parsed) const vevent = comp.getFirstSubcomponent('vevent') if (!vevent) return undefined diff --git a/apps/api/src/routes/lists.ts b/apps/api/src/routes/lists.ts index 94d07f0..5325cba 100644 --- a/apps/api/src/routes/lists.ts +++ b/apps/api/src/routes/lists.ts @@ -601,7 +601,7 @@ listItemsRouter.patch('/:itemId', zValidator('json', patchItemSchema), async (c) } // Build the update payload — single-field write with updatedAt=NOW() - let updateValues: { + const updateValues: { checked?: boolean text?: string rank?: string diff --git a/apps/api/src/routes/sse.ts b/apps/api/src/routes/sse.ts index 3095b78..61b812b 100644 --- a/apps/api/src/routes/sse.ts +++ b/apps/api/src/routes/sse.ts @@ -93,12 +93,18 @@ sseRouter.get('/lists', async (c) => { // Subscribe to each accessible list's channel (D-04 — scoped, not global) for (const listId of accessibleListIds) { - const unsub = subscribeListEvents(listId, async (event) => { - if (stream.aborted) return - await stream.writeSSE({ - data: JSON.stringify(event), - event: event.type, - id: `${listId}-${Date.now()}`, + const unsub = subscribeListEvents(listId, (event) => { + // The handler signature is void-returning; wrap the async write in void+catch. + // writeSSE errors are non-fatal — the SSE loop detects stream.aborted and cleans up. + void (async () => { + if (stream.aborted) return + await stream.writeSSE({ + data: JSON.stringify(event), + event: event.type, + id: `${listId}-${Date.now()}`, + }) + })().catch((err: unknown) => { + console.error('[sse] writeSSE error:', err) }) }) unsubscribers.push(unsub) diff --git a/apps/api/tests/auth/devBypass.test.ts b/apps/api/tests/auth/devBypass.test.ts index 0853032..1efe1ab 100644 --- a/apps/api/tests/auth/devBypass.test.ts +++ b/apps/api/tests/auth/devBypass.test.ts @@ -7,7 +7,7 @@ * 3. NODE_ENV!='production' + DEV_AUTH_BYPASS='true' → DEV_USER injected into context */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, afterEach } from 'vitest' import { Hono } from 'hono' // We import after env manipulation since devAuthBypass() reads env vars at call time. diff --git a/apps/api/tests/broker/poller.test.ts b/apps/api/tests/broker/poller.test.ts index 7a7a7f8..c559571 100644 --- a/apps/api/tests/broker/poller.test.ts +++ b/apps/api/tests/broker/poller.test.ts @@ -32,7 +32,7 @@ const mockCredentialsSelectResult: Array<{ // Each call to db.select() needs to return different chains // We use a call counter to decide which data to return -let selectCallCount = 0 +let _callCount = 0 const mockSelectLimit = vi.fn() const mockSelectWhere = vi.fn().mockReturnValue({ limit: mockSelectLimit }) @@ -44,7 +44,7 @@ mockSelectFrom.mockImplementation(() => ({ where: mockSelectWhere, // Support both: direct await (no where) and .where().limit() then: (resolve: (v: typeof mockCredentialsSelectResult) => void) => { - selectCallCount++ + _callCount++ resolve(mockCredentialsSelectResult) return Promise.resolve(mockCredentialsSelectResult) }, @@ -71,7 +71,7 @@ vi.mock('../../src/broker/client.js', () => ({ describe('broker poller — runPoll', () => { beforeEach(() => { vi.clearAllMocks() - selectCallCount = 0 + _callCount = 0 // Reset implementations mockSyncCalendar.mockResolvedValue(undefined) diff --git a/apps/api/tests/routes/events.test.ts b/apps/api/tests/routes/events.test.ts index 6099fa4..766ae9c 100644 --- a/apps/api/tests/routes/events.test.ts +++ b/apps/api/tests/routes/events.test.ts @@ -518,7 +518,7 @@ describe('GET /api/events/sync-status', () => { const mockOrderBy = vi.fn().mockImplementation(() => Promise.resolve(mockDbRows)) const mockLimit = vi.fn().mockReturnValue(mockOrderBy) const mockSimpleWhere = vi.fn().mockReturnValue({ limit: mockLimit }) - const mockOrderByDirect = vi.fn().mockImplementation(() => Promise.resolve(mockDbRows)) + const _mockOrderByDirect = vi.fn().mockImplementation(() => Promise.resolve(mockDbRows)) // Some implementations use .where().orderBy() or just .where() mockSimpleWhere.mockReturnValue({ limit: mockLimit, orderBy: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue(mockDbRows) }) }) mockFromFn.mockReturnValue({ where: mockSimpleWhere }) diff --git a/apps/api/tests/routes/lists.test.ts b/apps/api/tests/routes/lists.test.ts index 86657cc..e16b832 100644 --- a/apps/api/tests/routes/lists.test.ts +++ b/apps/api/tests/routes/lists.test.ts @@ -510,7 +510,6 @@ async function seedItem( rank: string, checked = false, ): Promise { - const { eq } = await import('drizzle-orm') const [result] = await db.insert(listItems).values({ listId, text, rank, checked }).$returningId() return result.id } @@ -646,7 +645,7 @@ describe('PATCH /api/list-items/:id — per-field LWW (D-08)', () => { const listId = await seedList(ownerId, 'Uncheck Rank', false) // Two active items await seedItem(listId, 'alpha', 'a0') - const lastActiveId = await seedItem(listId, 'beta', 'a1') + await seedItem(listId, 'beta', 'a1') // One checked item (to uncheck) const checkedItemId = await seedItem(listId, 'checked-one', 'Zz', true) @@ -1055,8 +1054,8 @@ describe('PATCH /api/list-items/:id { position } — reorder ordering (LIST-03, const ownerId = await seedUser('reorder-lww') currentDevUserId = ownerId const listId = await seedList(ownerId, 'Reorder LWW', false) - const id1 = await seedItem(listId, 'alpha', 'a0') - const id2 = await seedItem(listId, 'beta', 'a1') + const _id1 = await seedItem(listId, 'alpha', 'a0') + const _id2 = await seedItem(listId, 'beta', 'a1') const id3 = await seedItem(listId, 'gamma', 'a2') const app = await getApp() diff --git a/apps/pwa/src/api/client.test.ts b/apps/pwa/src/api/client.test.ts index 02a1937..c122b48 100644 --- a/apps/pwa/src/api/client.test.ts +++ b/apps/pwa/src/api/client.test.ts @@ -175,8 +175,8 @@ describe('createEvent', () => { const mockFetch = vi.mocked(fetch) mockFetch.mockResolvedValueOnce({ ok: true, - json: async () => ({ uid: 'test-uid-123' }), - } as Response) + json: () => ({ uid: 'test-uid-123' }), + } as unknown as Response) const { createEvent } = await import('./client.js') const payload = { @@ -193,6 +193,7 @@ describe('createEvent', () => { expect.objectContaining({ method: 'POST', credentials: 'include', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expect.objectContaining returns 'any' (Vitest asymmetric matcher); safe in assertion context headers: expect.objectContaining({ 'Content-Type': 'application/json' }), }), ) @@ -201,8 +202,8 @@ describe('createEvent', () => { it('returns { uid } from 202 response', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({ uid: 'returned-uid-456' }), - } as Response) + json: () => ({ uid: 'returned-uid-456' }), + } as unknown as Response) const { createEvent } = await import('./client.js') const result = await createEvent({ @@ -220,8 +221,8 @@ describe('createEvent', () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: false, status: 400, - json: async () => ({ error: 'Bad Request' }), - } as Response) + json: () => ({ error: 'Bad Request' }), + } as unknown as Response) const { createEvent } = await import('./client.js') await expect( @@ -250,8 +251,8 @@ describe('updateEvent', () => { const mockFetch = vi.mocked(fetch) mockFetch.mockResolvedValueOnce({ ok: true, - json: async () => ({ uid: 'edit-uid-789' }), - } as Response) + json: () => ({ uid: 'edit-uid-789' }), + } as unknown as Response) const { updateEvent } = await import('./client.js') await updateEvent('edit-uid-789', { @@ -274,8 +275,8 @@ describe('updateEvent', () => { it('returns { uid } on success', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({ uid: 'patched-uid' }), - } as Response) + json: () => ({ uid: 'patched-uid' }), + } as unknown as Response) const { updateEvent } = await import('./client.js') const result = await updateEvent('some-uid', { @@ -303,12 +304,12 @@ describe('fetchWritableCalendars', () => { it('GETs /api/events/writable-calendars with credentials:include', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({ + json: () => ({ calendars: [ { url: 'https://caldav.fastmail.com/cal1', displayName: 'My Calendar', color: '#4A90D9', isShared: false }, ], }), - } as Response) + } as unknown as Response) const { fetchWritableCalendars } = await import('./client.js') await fetchWritableCalendars() @@ -326,8 +327,8 @@ describe('fetchWritableCalendars', () => { ] vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({ calendars: mockCalendars }), - } as Response) + json: () => ({ calendars: mockCalendars }), + } as unknown as Response) const { fetchWritableCalendars } = await import('./client.js') const result = await fetchWritableCalendars() @@ -340,7 +341,7 @@ describe('fetchWritableCalendars', () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: false, status: 401, - } as Response) + } as unknown as Response) const { fetchWritableCalendars } = await import('./client.js') await expect(fetchWritableCalendars()).rejects.toThrow() @@ -361,8 +362,8 @@ describe('deleteEvent', () => { const mockFetch = vi.mocked(fetch) mockFetch.mockResolvedValueOnce({ ok: true, - json: async () => ({}), - } as Response) + json: () => ({}), + } as unknown as Response) const { deleteEvent } = await import('./client.js') await deleteEvent('uid-to-delete') @@ -379,8 +380,8 @@ describe('deleteEvent', () => { it('resolves void on success (204/202)', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({}), - } as Response) + json: () => ({}), + } as unknown as Response) const { deleteEvent } = await import('./client.js') const result = await deleteEvent('uid-abc') @@ -391,7 +392,7 @@ describe('deleteEvent', () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: false, status: 404, - } as Response) + } as unknown as Response) const { deleteEvent } = await import('./client.js') await expect(deleteEvent('missing-uid')).rejects.toThrow() @@ -411,8 +412,8 @@ describe('fetchSyncStatus', () => { it('GETs /api/events/sync-status?uid= with credentials:include', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({ uid: 'my-uid', status: 'pending' }), - } as Response) + json: () => ({ uid: 'my-uid', status: 'pending' }), + } as unknown as Response) const { fetchSyncStatus } = await import('./client.js') await fetchSyncStatus('my-uid') @@ -426,8 +427,8 @@ describe('fetchSyncStatus', () => { it('returns SyncStatus object with uid and status', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({ uid: 'test-uid', status: 'done' }), - } as Response) + json: () => ({ uid: 'test-uid', status: 'done' }), + } as unknown as Response) const { fetchSyncStatus } = await import('./client.js') const result = await fetchSyncStatus('test-uid') @@ -437,8 +438,8 @@ describe('fetchSyncStatus', () => { it('returns SyncStatus with error field for failed status', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, - json: async () => ({ uid: 'fail-uid', status: 'failed', error: '412 Conflict' }), - } as Response) + json: () => ({ uid: 'fail-uid', status: 'failed', error: '412 Conflict' }), + } as unknown as Response) const { fetchSyncStatus } = await import('./client.js') const result = await fetchSyncStatus('fail-uid') @@ -449,7 +450,7 @@ describe('fetchSyncStatus', () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: false, status: 500, - } as Response) + } as unknown as Response) const { fetchSyncStatus } = await import('./client.js') await expect(fetchSyncStatus('any-uid')).rejects.toThrow() @@ -471,8 +472,8 @@ describe('fetchMe', () => { ok: true, type: 'basic', status: 200, - json: async () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }), - } as Response) + json: () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }), + } as unknown as Response) const { fetchMe } = await import('./client.js') await fetchMe() @@ -488,8 +489,8 @@ describe('fetchMe', () => { ok: true, type: 'basic', status: 200, - json: async () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }), - } as Response) + json: () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }), + } as unknown as Response) const { fetchMe } = await import('./client.js') const result = await fetchMe() @@ -503,7 +504,7 @@ describe('fetchMe', () => { ok: false, type: 'opaqueredirect', status: 0, - json: async () => { + json: () => { throw new Error('body not accessible on opaqueredirect') }, } as unknown as Response) @@ -517,7 +518,7 @@ describe('fetchMe', () => { ok: false, type: 'basic', status: 401, - } as Response) + } as unknown as Response) const { fetchMe } = await import('./client.js') await expect(fetchMe()).rejects.toThrow(/authentication required/i) diff --git a/apps/pwa/src/components/AppNav.test.tsx b/apps/pwa/src/components/AppNav.test.tsx index 6c053f5..ce8d200 100644 --- a/apps/pwa/src/components/AppNav.test.tsx +++ b/apps/pwa/src/components/AppNav.test.tsx @@ -12,7 +12,7 @@ import React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // ── Module mocks ──────────────────────────────────────────────────────────── diff --git a/apps/pwa/src/components/CalendarShell.tsx b/apps/pwa/src/components/CalendarShell.tsx index e6500fb..69af1ec 100644 --- a/apps/pwa/src/components/CalendarShell.tsx +++ b/apps/pwa/src/components/CalendarShell.tsx @@ -156,7 +156,7 @@ export function CalendarShell() { // IANATimezone (the config's timezone type) is declared but not exported by @schedule-x/calendar, // so derive it from useCalendarApp's config parameter rather than importing it. type SxTimeZone = NonNullable[0]['timezone']> - const displayTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone as SxTimeZone + const displayTimeZone: SxTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone // useCalendarApp — config is stable; plugins passed as second argument const calendar = useCalendarApp( @@ -169,7 +169,7 @@ export function CalendarShell() { ], defaultView, timezone: displayTimeZone, - firstDayOfWeek: SX_FIRST_DAY_OF_WEEK as 7, + firstDayOfWeek: SX_FIRST_DAY_OF_WEEK, calendars: calendarsConfig, callbacks: { onRangeUpdate(range) { @@ -197,7 +197,7 @@ export function CalendarShell() { useEffect(() => { if (!eventsQuery.data) return const sxEvents = hydrateEvents(eventsQuery.data.occurrences) - eventsService.set(sxEvents as Parameters[0]) + eventsService.set(sxEvents) }, [eventsQuery.data, eventsService]) // Auth redirect — one-shot full-page nav to /api/login when /api/me fails. @@ -351,7 +351,7 @@ export function CalendarShell() { color: 'var(--color-text-primary)', }} > - Couldn't load events + Couldn't load events