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:
@@ -11,43 +11,39 @@
|
||||
* - renders nothing when lastSyncedUid is null
|
||||
*/
|
||||
|
||||
import 'temporal-polyfill/global'
|
||||
import React from 'react'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { SyncStateToast } from './SyncStateToast.js'
|
||||
import 'temporal-polyfill/global';
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { SyncStateToast } from './SyncStateToast.js';
|
||||
|
||||
// ── Module mocks ──────────────────────────────────────────────────────────────
|
||||
// vi.hoisted() required for variables referenced inside vi.mock() factories (D-03-04-hoisting)
|
||||
|
||||
const {
|
||||
mockLastSyncedUid,
|
||||
mockSetLastSyncedUid,
|
||||
mockFetchSyncStatus,
|
||||
} = vi.hoisted(() => {
|
||||
const { mockLastSyncedUid, mockSetLastSyncedUid, mockFetchSyncStatus } = vi.hoisted(() => {
|
||||
// Declare with explicit type so later null assignments (line 82+) stay type-safe
|
||||
const mockLastSyncedUid: { value: string | null } = { value: 'test-uid-123' }
|
||||
const mockLastSyncedUid: { value: string | null } = { value: 'test-uid-123' };
|
||||
return {
|
||||
mockLastSyncedUid,
|
||||
mockSetLastSyncedUid: vi.fn(),
|
||||
mockFetchSyncStatus: vi.fn(),
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../store/calendarStore.js', () => ({
|
||||
useCalendarStore: (selector: (s: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
lastSyncedUid: mockLastSyncedUid.value,
|
||||
setLastSyncedUid: mockSetLastSyncedUid,
|
||||
}
|
||||
return selector(state)
|
||||
};
|
||||
return selector(state);
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('../api/client.js', () => ({
|
||||
fetchSyncStatus: mockFetchSyncStatus,
|
||||
}))
|
||||
}));
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -56,7 +52,7 @@ function makeQueryClient() {
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function renderToast(queryClient: QueryClient) {
|
||||
@@ -64,174 +60,194 @@ function renderToast(queryClient: QueryClient) {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SyncStateToast />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('SyncStateToast', () => {
|
||||
let queryClient: QueryClient
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = makeQueryClient()
|
||||
mockLastSyncedUid.value = 'test-uid-123'
|
||||
})
|
||||
vi.clearAllMocks();
|
||||
queryClient = makeQueryClient();
|
||||
mockLastSyncedUid.value = 'test-uid-123';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders nothing when lastSyncedUid is null', () => {
|
||||
mockLastSyncedUid.value = null
|
||||
const { container } = renderToast(queryClient)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
mockLastSyncedUid.value = null;
|
||||
const { container } = renderToast(queryClient);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('pending: renders "Syncing…" with role="status"', async () => {
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'pending' })
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'pending' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.getByText('Syncing…')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
expect(screen.getByText('Syncing…')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('done: renders "Saved"', async () => {
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' })
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Saved')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText('Saved')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('done: calls queryClient.invalidateQueries for ["events"]', async () => {
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' })
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Saved')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText('Saved')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryKey: ['events'] }),
|
||||
)
|
||||
})
|
||||
expect(invalidateSpy).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['events'] }));
|
||||
});
|
||||
|
||||
it('done: auto-dismisses after 2s via setLastSyncedUid(null)', async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true })
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' })
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
// Wait for the "Saved" state (real timer resolution happens via shouldAdvanceTime)
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync()
|
||||
})
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(mockSetLastSyncedUid).toHaveBeenCalledWith(null)
|
||||
})
|
||||
expect(mockSetLastSyncedUid).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('failed (generic): renders "Didn\'t save. Try again." with role="alert"', async () => {
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' })
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||
expect(screen.getByText("Didn't save. Try again.")).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByText("Didn't save. Try again.")).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('failed (generic): persists and has a dismiss button', async () => {
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' })
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
// Should have a dismiss button
|
||||
const dismissBtn = screen.getByRole('button', { name: /dismiss/i })
|
||||
expect(dismissBtn).toBeInTheDocument()
|
||||
})
|
||||
const dismissBtn = screen.getByRole('button', { name: /dismiss/i });
|
||||
expect(dismissBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('failed (conflict/412): renders conflict copy and invalidates ["events"]', async () => {
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
mockFetchSyncStatus.mockResolvedValue({
|
||||
uid: 'test-uid-123',
|
||||
status: 'failed',
|
||||
error: '412 Conflict',
|
||||
})
|
||||
});
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('This event changed elsewhere — review the latest version'),
|
||||
).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
screen.getByText('This event changed elsewhere — review the latest version'),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryKey: ['events'] }),
|
||||
)
|
||||
})
|
||||
expect(invalidateSpy).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['events'] }));
|
||||
});
|
||||
|
||||
it('dead: renders "Not saved. Check your connection." with role="alert"', async () => {
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'dead' })
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'dead' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||
expect(screen.getByText('Not saved. Check your connection.')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('Not saved. Check your connection.')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('refetchInterval is active (3000ms) while pending', async () => {
|
||||
let callCount = 0
|
||||
let callCount = 0;
|
||||
mockFetchSyncStatus.mockImplementation(() => {
|
||||
callCount++
|
||||
return Promise.resolve({ uid: 'test-uid-123', status: 'pending' })
|
||||
})
|
||||
callCount++;
|
||||
return Promise.resolve({ uid: 'test-uid-123', status: 'pending' });
|
||||
});
|
||||
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true })
|
||||
renderToast(queryClient)
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
renderToast(queryClient);
|
||||
|
||||
// Wait for first fetch
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
})
|
||||
const countAfterFirst = callCount
|
||||
expect(countAfterFirst).toBeGreaterThanOrEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
});
|
||||
const countAfterFirst = callCount;
|
||||
expect(countAfterFirst).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Advance 3s to trigger refetch
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3100)
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(3100);
|
||||
});
|
||||
|
||||
expect(callCount).toBeGreaterThan(countAfterFirst)
|
||||
})
|
||||
expect(callCount).toBeGreaterThan(countAfterFirst);
|
||||
});
|
||||
|
||||
it('dismiss button on failed toast calls setLastSyncedUid(null)', async () => {
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' })
|
||||
mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' });
|
||||
|
||||
renderToast(queryClient)
|
||||
renderToast(queryClient);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
const dismissBtn = screen.getByRole('button', { name: /dismiss/i })
|
||||
fireEvent.click(dismissBtn)
|
||||
const dismissBtn = screen.getByRole('button', { name: /dismiss/i });
|
||||
fireEvent.click(dismissBtn);
|
||||
|
||||
expect(mockSetLastSyncedUid).toHaveBeenCalledWith(null)
|
||||
})
|
||||
})
|
||||
expect(mockSetLastSyncedUid).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user