From da23a45a4babb0a8ac58c0fdaaa5126d7554a84e Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 13:40:37 -0800 Subject: [PATCH 01/10] refactor(issue-templates): simplify feature request and bug report templates - Removed unnecessary emoji from template names for a cleaner presentation. - Eliminated the priority dropdown from the feature request template to streamline the submission process. - Updated the bug report template to remove the emoji, enhancing consistency across issue templates. - Adjusted the configuration file to reflect these changes, ensuring clarity in the issue submission process. --- .github/ISSUE_TEMPLATE/1-feature-request.yml | 13 +------------ .github/ISSUE_TEMPLATE/2-bug-report.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1-feature-request.yml b/.github/ISSUE_TEMPLATE/1-feature-request.yml index 4bb6fa7f2..52926b8ea 100644 --- a/.github/ISSUE_TEMPLATE/1-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/1-feature-request.yml @@ -1,4 +1,4 @@ -name: Feature Request ✨ +name: Feature Request description: You want something added to the app labels: [enhancement] projects: [SwitchbackTech/4] @@ -9,17 +9,6 @@ body: value: | Thanks for taking the time to fill out this feature request! - - type: dropdown - attributes: - label: Priority - description: How important is this feature to you? - multiple: false - options: - - Critical (blocking my workflow) - - High (would significantly improve my experience) - - Medium (nice to have) - - Low (minor improvement) - - type: textarea attributes: label: Feature Description diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.yml b/.github/ISSUE_TEMPLATE/2-bug-report.yml index 4b7b9f5c3..24ffcff7d 100644 --- a/.github/ISSUE_TEMPLATE/2-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/2-bug-report.yml @@ -1,4 +1,4 @@ -name: Bug Report 🐞 +name: Bug Report description: You found something wrong labels: [bug] projects: [SwitchbackTech/4] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 014f6d8b7..aca69a956 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: true contact_links: - - name: 💬 GitHub Discussions + - name: GitHub Discussions url: https://github.com/SwitchbackTech/compass/discussions about: Start a GitHub Discussion for general questions and feedback - - name: 📚 Documentation + - name: Documentation url: https://docs.compasscalendar.com about: Check our documentation for setup guides, API reference, and contributing guidelines From c595d6ba61c9d5d0eff3c6187bb47169c646c3eb Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:00:49 -0800 Subject: [PATCH 02/10] test(socket): enhance SocketProvider tests with async act calls - Updated SocketProvider tests to utilize `act` for asynchronous callback invocations, ensuring proper handling of state updates during testing. - Improved test reliability by wrapping callback executions in `act`, aligning with React's testing best practices. --- .../web/src/socket/provider/SocketProvider.test.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/web/src/socket/provider/SocketProvider.test.tsx b/packages/web/src/socket/provider/SocketProvider.test.tsx index 00a5e7e3e..b20dcdbb6 100644 --- a/packages/web/src/socket/provider/SocketProvider.test.tsx +++ b/packages/web/src/socket/provider/SocketProvider.test.tsx @@ -1,3 +1,4 @@ +import { act } from "react"; import { Provider } from "react-redux"; import { combineReducers, configureStore } from "@reduxjs/toolkit"; import { render, waitFor } from "@testing-library/react"; @@ -74,8 +75,15 @@ describe("SocketProvider", () => { expect(importEndCallback).toBeDefined(); }); - importStartCallback?.(); - importEndCallback?.(JSON.stringify({ eventsCount: 10, calendarsCount: 2 })); + await act(async () => { + importStartCallback?.(); + }); + + await act(async () => { + importEndCallback?.( + JSON.stringify({ eventsCount: 10, calendarsCount: 2 }), + ); + }); const state = store.getState(); expect(state.sync.importGCal.importResults).toEqual({ From c23bdf74dfccd67f31750d45240f2256ba53edc5 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:01:56 -0800 Subject: [PATCH 03/10] test(socket): add race condition handling test for useGcalSync - Introduced a new test case to verify the correct handling of import end events when the awaitingImportResults state changes mid-render. - Utilized a ref to prevent stale closures, ensuring that the correct state is referenced during socket event processing. - Enhanced the reliability of the useGcalSync hook by ensuring it properly processes events even when state changes occur asynchronously. --- .../web/src/socket/hooks/useGcalSync.test.ts | 33 +++++++++++++++++++ packages/web/src/socket/hooks/useGcalSync.ts | 23 +++++++------ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/web/src/socket/hooks/useGcalSync.test.ts b/packages/web/src/socket/hooks/useGcalSync.test.ts index b4149ab21..ed467e010 100644 --- a/packages/web/src/socket/hooks/useGcalSync.test.ts +++ b/packages/web/src/socket/hooks/useGcalSync.test.ts @@ -157,5 +157,38 @@ describe("useGcalSync", () => { ); expect(importGCalSlice.actions.setImportResults).not.toHaveBeenCalled(); }); + + it("handles import end when awaitingImportResults changes mid-render", () => { + // Simulate the race condition: starts false, changes to true, + // then event arrives (testing ref pattern works correctly) + + let importEndHandler: (data: string) => void; + (socket.on as jest.Mock).mockImplementation((event, handler) => { + if (event === IMPORT_GCAL_END) { + importEndHandler = handler; + } + }); + + // Start with awaitingImportResults = false + awaitingValue = false; + const { rerender } = renderHook(() => useGcalSync()); + + // Change to true (simulating user clicking Reconnect) + awaitingValue = true; + rerender(); + + // Event arrives - should process correctly with ref pattern + importEndHandler?.( + JSON.stringify({ eventsCount: 10, calendarsCount: 2 }), + ); + + // Verify setImportResults was called (not skipped due to stale closure) + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.setImportResults({ + eventsCount: 10, + calendarsCount: 2, + }), + ); + }); }); }); diff --git a/packages/web/src/socket/hooks/useGcalSync.ts b/packages/web/src/socket/hooks/useGcalSync.ts index 3e11d73c3..735b5bb0c 100644 --- a/packages/web/src/socket/hooks/useGcalSync.ts +++ b/packages/web/src/socket/hooks/useGcalSync.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; import { IMPORT_GCAL_END, @@ -8,10 +8,7 @@ import { import { UserMetadata } from "@core/types/user.types"; import { shouldImportGCal } from "@core/util/event/event.util"; import { Sync_AsyncStateContextReason } from "@web/ducks/events/context/sync.context"; -import { - selectAwaitingImportResults, - selectImporting, -} from "@web/ducks/events/selectors/sync.selector"; +import { selectAwaitingImportResults } from "@web/ducks/events/selectors/sync.selector"; import { importGCalSlice, triggerFetch, @@ -21,9 +18,15 @@ import { socket } from "../client/socket.client"; export const useGcalSync = () => { const dispatch = useDispatch(); - const importing = useAppSelector(selectImporting); const awaitingImportResults = useAppSelector(selectAwaitingImportResults); + // Use ref to prevent stale closures when socket events arrive + // between state change and effect re-run + const awaitingImportResultsRef = useRef(awaitingImportResults); + useEffect(() => { + awaitingImportResultsRef.current = awaitingImportResults; + }, [awaitingImportResults]); + const onImportStart = useCallback( (importing = true) => { if (importing) { @@ -37,7 +40,7 @@ export const useGcalSync = () => { const onImportEnd = useCallback( (payload?: { eventsCount?: number; calendarsCount?: number } | string) => { dispatch(importGCalSlice.actions.importing(false)); - if (!awaitingImportResults) { + if (!awaitingImportResultsRef.current) { return; } @@ -69,7 +72,7 @@ export const useGcalSync = () => { }), ); }, - [dispatch, awaitingImportResults], + [dispatch], ); const onMetadataFetch = useCallback( @@ -77,7 +80,7 @@ export const useGcalSync = () => { const importGcal = shouldImportGCal(metadata); const isBackendImporting = metadata.sync?.importGCal === "importing"; - if (awaitingImportResults) { + if (awaitingImportResultsRef.current) { if (isBackendImporting) { dispatch(importGCalSlice.actions.importing(true)); } @@ -91,7 +94,7 @@ export const useGcalSync = () => { dispatch(importGCalSlice.actions.request(undefined as never)); } }, - [dispatch, awaitingImportResults, onImportStart], + [dispatch, onImportStart], ); useEffect(() => { From c8c6415b6ef015e05ea91aefc2b1f548829c8ea1 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:06:14 -0800 Subject: [PATCH 04/10] delete(svg): remove unused circle.svg file - Deleted the circle.svg file from the public SVG assets as it is no longer needed in the project. - This cleanup helps streamline the asset management and reduces unnecessary file clutter. --- packages/web/src/public/svg/circle.svg | 5 - .../web/src/socket/hooks/useGcalSync.test.ts | 204 ++++++++++++++++++ 2 files changed, 204 insertions(+), 5 deletions(-) delete mode 100644 packages/web/src/public/svg/circle.svg diff --git a/packages/web/src/public/svg/circle.svg b/packages/web/src/public/svg/circle.svg deleted file mode 100644 index 8ea965c0b..000000000 --- a/packages/web/src/public/svg/circle.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/web/src/socket/hooks/useGcalSync.test.ts b/packages/web/src/socket/hooks/useGcalSync.test.ts index ed467e010..c910b6f00 100644 --- a/packages/web/src/socket/hooks/useGcalSync.test.ts +++ b/packages/web/src/socket/hooks/useGcalSync.test.ts @@ -57,6 +57,7 @@ describe("useGcalSync", () => { beforeEach(() => { jest.clearAllMocks(); + jest.useFakeTimers(); importingValue = false; awaitingValue = false; (useDispatch as jest.Mock).mockReturnValue(mockDispatch); @@ -71,6 +72,10 @@ describe("useGcalSync", () => { }); }); + afterEach(() => { + jest.useRealTimers(); + }); + it("sets up socket listeners", () => { renderHook(() => useGcalSync()); @@ -191,4 +196,203 @@ describe("useGcalSync", () => { ); }); }); + + describe("import flow interaction", () => { + it("shows spinner on import start and hides it on successful import end", () => { + // Capture socket handlers to simulate backend events + const handlers: Record void> = {}; + (socket.on as jest.Mock).mockImplementation((event, handler) => { + handlers[event] = handler; + }); + + awaitingValue = true; + renderHook(() => useGcalSync()); + + // Verify handlers are registered + expect(handlers[IMPORT_GCAL_START]).toBeDefined(); + expect(handlers[IMPORT_GCAL_END]).toBeDefined(); + + // Phase 1: Backend signals import start (spinner should appear) + handlers[IMPORT_GCAL_START](true); + + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.clearImportResults(undefined), + ); + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.importing(true), + ); + + mockDispatch.mockClear(); + + // Phase 2: Simulate backend processing time (e.g., 2 seconds) + jest.advanceTimersByTime(2000); + + // Phase 3: Backend signals import complete with successful response + const successfulResponse = JSON.stringify({ + eventsCount: 25, + calendarsCount: 3, + }); + handlers[IMPORT_GCAL_END](successfulResponse); + + // Spinner should disappear (importing set to false) + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.importing(false), + ); + // Results should be set + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.setImportResults({ + eventsCount: 25, + calendarsCount: 3, + }), + ); + // Fetch should be triggered to load new events + expect(triggerFetch).toHaveBeenCalledWith({ + reason: "IMPORT_COMPLETE", + }); + }); + + it("hides spinner when import completes within timeout", () => { + const handlers: Record void> = {}; + (socket.on as jest.Mock).mockImplementation((event, handler) => { + handlers[event] = handler; + }); + + awaitingValue = true; + renderHook(() => useGcalSync()); + + // Start import + handlers[IMPORT_GCAL_START](true); + mockDispatch.mockClear(); + + // Simulate a reasonable import duration (under 30 seconds) + const REASONABLE_IMPORT_TIME_MS = 15000; + jest.advanceTimersByTime(REASONABLE_IMPORT_TIME_MS); + + // Import completes successfully + handlers[IMPORT_GCAL_END]( + JSON.stringify({ eventsCount: 100, calendarsCount: 5 }), + ); + + // Verify spinner is hidden + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.importing(false), + ); + }); + + it("handles rapid start/end sequence without state inconsistency", () => { + const handlers: Record void> = {}; + (socket.on as jest.Mock).mockImplementation((event, handler) => { + handlers[event] = handler; + }); + + awaitingValue = true; + renderHook(() => useGcalSync()); + + // Rapid sequence: start → end (small import) + handlers[IMPORT_GCAL_START](true); + jest.advanceTimersByTime(100); // Very fast import + handlers[IMPORT_GCAL_END]( + JSON.stringify({ eventsCount: 2, calendarsCount: 1 }), + ); + + // Final state should have importing=false + const importingCalls = mockDispatch.mock.calls.filter( + (call) => + call[0] === importGCalSlice.actions.importing(true) || + call[0] === importGCalSlice.actions.importing(false), + ); + + // Last importing call should be false (spinner hidden) + expect(mockDispatch).toHaveBeenLastCalledWith( + importGCalSlice.actions.setImportResults({ + eventsCount: 2, + calendarsCount: 1, + }), + ); + }); + + it("handles import end with empty payload gracefully", () => { + const handlers: Record void> = {}; + (socket.on as jest.Mock).mockImplementation((event, handler) => { + handlers[event] = handler; + }); + + awaitingValue = true; + renderHook(() => useGcalSync()); + + handlers[IMPORT_GCAL_START](true); + mockDispatch.mockClear(); + + // Backend sends empty response (edge case) + handlers[IMPORT_GCAL_END](JSON.stringify({})); + + // Should still hide spinner and set empty results + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.importing(false), + ); + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.setImportResults({}), + ); + }); + + it("handles import end with object payload (non-string)", () => { + const handlers: Record void> = {}; + (socket.on as jest.Mock).mockImplementation((event, handler) => { + handlers[event] = handler; + }); + + awaitingValue = true; + renderHook(() => useGcalSync()); + + handlers[IMPORT_GCAL_START](true); + mockDispatch.mockClear(); + + // Backend sends object directly (alternative format) + handlers[IMPORT_GCAL_END]({ eventsCount: 50, calendarsCount: 4 }); + + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.importing(false), + ); + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.setImportResults({ + eventsCount: 50, + calendarsCount: 4, + }), + ); + }); + + it("sets error state when backend returns malformed JSON", () => { + const handlers: Record void> = {}; + (socket.on as jest.Mock).mockImplementation((event, handler) => { + handlers[event] = handler; + }); + const consoleErrorSpy = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + + awaitingValue = true; + renderHook(() => useGcalSync()); + + handlers[IMPORT_GCAL_START](true); + mockDispatch.mockClear(); + + // Backend sends malformed response + handlers[IMPORT_GCAL_END]("not valid json {{{"); + + // Should hide spinner + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.importing(false), + ); + // Should set error + expect(mockDispatch).toHaveBeenCalledWith( + importGCalSlice.actions.setImportError( + "Failed to parse Google Calendar import results.", + ), + ); + // Should NOT set results + expect(importGCalSlice.actions.setImportResults).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + }); }); From 958fb6e841d0b7c4992d7430e39bb00b2bc3ff42 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:15:38 -0800 Subject: [PATCH 05/10] fix(socket): address PR review comments for useGcalSync - Update ref synchronously during render instead of in useEffect to fully eliminate race condition window - Rename misleading test name (timeout -> successfully) - Fix test assertions to validate action creator calls directly instead of using unused variable and dispatch-based assertions Co-Authored-By: Claude Opus 4.5 --- .../web/src/socket/hooks/useGcalSync.test.ts | 28 +++++++++---------- packages/web/src/socket/hooks/useGcalSync.ts | 7 ++--- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/web/src/socket/hooks/useGcalSync.test.ts b/packages/web/src/socket/hooks/useGcalSync.test.ts index c910b6f00..cab41bea6 100644 --- a/packages/web/src/socket/hooks/useGcalSync.test.ts +++ b/packages/web/src/socket/hooks/useGcalSync.test.ts @@ -251,7 +251,7 @@ describe("useGcalSync", () => { }); }); - it("hides spinner when import completes within timeout", () => { + it("hides spinner when import completes successfully", () => { const handlers: Record void> = {}; (socket.on as jest.Mock).mockImplementation((event, handler) => { handlers[event] = handler; @@ -295,20 +295,20 @@ describe("useGcalSync", () => { JSON.stringify({ eventsCount: 2, calendarsCount: 1 }), ); - // Final state should have importing=false - const importingCalls = mockDispatch.mock.calls.filter( - (call) => - call[0] === importGCalSlice.actions.importing(true) || - call[0] === importGCalSlice.actions.importing(false), - ); - - // Last importing call should be false (spinner hidden) - expect(mockDispatch).toHaveBeenLastCalledWith( - importGCalSlice.actions.setImportResults({ - eventsCount: 2, - calendarsCount: 1, - }), + // Verify the correct sequence of actions was dispatched: + // 1. clearImportResults (on start) + // 2. importing(true) (on start) + // 3. importing(false) (on end) + // 4. setImportResults (on end) + expect(importGCalSlice.actions.clearImportResults).toHaveBeenCalledWith( + undefined, ); + expect(importGCalSlice.actions.importing).toHaveBeenCalledWith(true); + expect(importGCalSlice.actions.importing).toHaveBeenCalledWith(false); + expect(importGCalSlice.actions.setImportResults).toHaveBeenCalledWith({ + eventsCount: 2, + calendarsCount: 1, + }); }); it("handles import end with empty payload gracefully", () => { diff --git a/packages/web/src/socket/hooks/useGcalSync.ts b/packages/web/src/socket/hooks/useGcalSync.ts index 735b5bb0c..e7bb0770f 100644 --- a/packages/web/src/socket/hooks/useGcalSync.ts +++ b/packages/web/src/socket/hooks/useGcalSync.ts @@ -20,12 +20,9 @@ export const useGcalSync = () => { const dispatch = useDispatch(); const awaitingImportResults = useAppSelector(selectAwaitingImportResults); - // Use ref to prevent stale closures when socket events arrive - // between state change and effect re-run + // Keep ref in sync synchronously during render to avoid race with socket events const awaitingImportResultsRef = useRef(awaitingImportResults); - useEffect(() => { - awaitingImportResultsRef.current = awaitingImportResults; - }, [awaitingImportResults]); + awaitingImportResultsRef.current = awaitingImportResults; const onImportStart = useCallback( (importing = true) => { From f0894e79c77ed186e552c45ebfe34c3d9df696ee Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:25:27 -0800 Subject: [PATCH 06/10] refactor(sync): rename awaitingImportResults to isImportPending - Updated state management and selectors to reflect the new naming convention for clarity. - Adjusted related components and tests to ensure consistency with the updated state name. - This change enhances code readability and aligns with the overall naming strategy in the project. --- .../web/src/__tests__/utils/state/store.test.util.ts | 2 +- .../SyncEventsOverlay/SyncEventsOverlay.test.tsx | 4 ++-- .../SyncEventsOverlay/SyncEventsOverlay.tsx | 4 ++-- .../web/src/ducks/events/selectors/sync.selector.ts | 4 ++-- packages/web/src/ducks/events/slices/sync.slice.ts | 10 +++++----- packages/web/src/socket/hooks/useGcalSync.test.ts | 4 ++-- packages/web/src/socket/hooks/useGcalSync.ts | 12 ++++++------ .../web/src/socket/provider/SocketProvider.test.tsx | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/web/src/__tests__/utils/state/store.test.util.ts b/packages/web/src/__tests__/utils/state/store.test.util.ts index b1fca9ca5..e99901784 100644 --- a/packages/web/src/__tests__/utils/state/store.test.util.ts +++ b/packages/web/src/__tests__/utils/state/store.test.util.ts @@ -122,7 +122,7 @@ export const createInitialState = ( importing: false, importResults: null, pendingLocalEventsSynced: null, - awaitingImportResults: false, + isImportPending: false, importError: null, }, importLatest: { diff --git a/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx b/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx index b6fb43a6b..92808b2f1 100644 --- a/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx +++ b/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx @@ -3,8 +3,8 @@ import "@testing-library/jest-dom"; import { render, screen } from "@testing-library/react"; import { selectIsAuthenticating } from "@web/ducks/auth/selectors/auth.selectors"; import { - selectAwaitingImportResults, selectImporting, + selectIsImportPending, } from "@web/ducks/events/selectors/sync.selector"; import { useAppSelector } from "@web/store/store.hooks"; import { SyncEventsOverlay } from "./SyncEventsOverlay"; @@ -33,7 +33,7 @@ describe("SyncEventsOverlay", () => { if (selector === selectImporting) { return importingValue; } - if (selector === selectAwaitingImportResults) { + if (selector === selectIsImportPending) { return awaitingValue; } if (selector === selectIsAuthenticating) { diff --git a/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.tsx b/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.tsx index a8947173f..071c3fa50 100644 --- a/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.tsx +++ b/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.tsx @@ -3,14 +3,14 @@ import { useBufferedVisibility } from "@web/common/hooks/useBufferedVisibility"; import { OverlayPanel } from "@web/components/OverlayPanel/OverlayPanel"; import { selectIsAuthenticating } from "@web/ducks/auth/selectors/auth.selectors"; import { - selectAwaitingImportResults, selectImporting, + selectIsImportPending, } from "@web/ducks/events/selectors/sync.selector"; import { useAppSelector } from "@web/store/store.hooks"; export const SyncEventsOverlay = () => { const importing = useAppSelector(selectImporting); - const awaitingImportResults = useAppSelector(selectAwaitingImportResults); + const awaitingImportResults = useAppSelector(selectIsImportPending); const isAuthenticating = useAppSelector(selectIsAuthenticating); // Show overlay when: diff --git a/packages/web/src/ducks/events/selectors/sync.selector.ts b/packages/web/src/ducks/events/selectors/sync.selector.ts index ee8e81f10..e3473ee0b 100644 --- a/packages/web/src/ducks/events/selectors/sync.selector.ts +++ b/packages/web/src/ducks/events/selectors/sync.selector.ts @@ -8,8 +8,8 @@ export const selectImportGCalState = ({ sync }: RootState) => sync.importGCal; export const selectImporting = ({ sync }: RootState) => sync.importGCal.importing; -export const selectAwaitingImportResults = ({ sync }: RootState) => - sync.importGCal.awaitingImportResults; +export const selectIsImportPending = ({ sync }: RootState) => + sync.importGCal.isImportPending; export const selectImportResults = ({ sync }: RootState) => sync.importGCal.importResults; diff --git a/packages/web/src/ducks/events/slices/sync.slice.ts b/packages/web/src/ducks/events/slices/sync.slice.ts index 6de17c02a..ae97bfec2 100644 --- a/packages/web/src/ducks/events/slices/sync.slice.ts +++ b/packages/web/src/ducks/events/slices/sync.slice.ts @@ -30,7 +30,7 @@ export const importGCalSlice = createAsyncSlice< importing: boolean; importResults: ImportResults | null; pendingLocalEventsSynced: number | null; - awaitingImportResults: boolean; + isImportPending: boolean; importError: string | null; } >({ @@ -39,7 +39,7 @@ export const importGCalSlice = createAsyncSlice< importing: false, importResults: null, pendingLocalEventsSynced: null, - awaitingImportResults: false, + isImportPending: false, importError: null, }, reducers: { @@ -47,7 +47,7 @@ export const importGCalSlice = createAsyncSlice< state.importing = action.payload; }, setAwaitingImportResults: (state, action: PayloadAction) => { - state.awaitingImportResults = action.payload; + state.isImportPending = action.payload; if (action.payload) { state.importError = null; } @@ -63,7 +63,7 @@ export const importGCalSlice = createAsyncSlice< }>, ) => { state.importing = false; - state.awaitingImportResults = false; + state.isImportPending = false; state.importError = null; state.importResults = { ...action.payload, @@ -73,7 +73,7 @@ export const importGCalSlice = createAsyncSlice< }, setImportError: (state, action: PayloadAction) => { state.importing = false; - state.awaitingImportResults = false; + state.isImportPending = false; state.importError = action.payload; state.importResults = null; state.pendingLocalEventsSynced = null; diff --git a/packages/web/src/socket/hooks/useGcalSync.test.ts b/packages/web/src/socket/hooks/useGcalSync.test.ts index cab41bea6..955a75a0b 100644 --- a/packages/web/src/socket/hooks/useGcalSync.test.ts +++ b/packages/web/src/socket/hooks/useGcalSync.test.ts @@ -6,8 +6,8 @@ import { USER_METADATA, } from "@core/constants/websocket.constants"; import { - selectAwaitingImportResults, selectImporting, + selectIsImportPending, } from "@web/ducks/events/selectors/sync.selector"; import { importGCalSlice, @@ -65,7 +65,7 @@ describe("useGcalSync", () => { if (selector === selectImporting) { return importingValue; } - if (selector === selectAwaitingImportResults) { + if (selector === selectIsImportPending) { return awaitingValue; } return false; diff --git a/packages/web/src/socket/hooks/useGcalSync.ts b/packages/web/src/socket/hooks/useGcalSync.ts index e7bb0770f..1dcb91e2f 100644 --- a/packages/web/src/socket/hooks/useGcalSync.ts +++ b/packages/web/src/socket/hooks/useGcalSync.ts @@ -8,7 +8,7 @@ import { import { UserMetadata } from "@core/types/user.types"; import { shouldImportGCal } from "@core/util/event/event.util"; import { Sync_AsyncStateContextReason } from "@web/ducks/events/context/sync.context"; -import { selectAwaitingImportResults } from "@web/ducks/events/selectors/sync.selector"; +import { selectIsImportPending } from "@web/ducks/events/selectors/sync.selector"; import { importGCalSlice, triggerFetch, @@ -18,11 +18,11 @@ import { socket } from "../client/socket.client"; export const useGcalSync = () => { const dispatch = useDispatch(); - const awaitingImportResults = useAppSelector(selectAwaitingImportResults); + const isImportPending = useAppSelector(selectIsImportPending); // Keep ref in sync synchronously during render to avoid race with socket events - const awaitingImportResultsRef = useRef(awaitingImportResults); - awaitingImportResultsRef.current = awaitingImportResults; + const isImportPendingRef = useRef(isImportPending); + isImportPendingRef.current = isImportPending; const onImportStart = useCallback( (importing = true) => { @@ -37,7 +37,7 @@ export const useGcalSync = () => { const onImportEnd = useCallback( (payload?: { eventsCount?: number; calendarsCount?: number } | string) => { dispatch(importGCalSlice.actions.importing(false)); - if (!awaitingImportResultsRef.current) { + if (!isImportPendingRef.current) { return; } @@ -77,7 +77,7 @@ export const useGcalSync = () => { const importGcal = shouldImportGCal(metadata); const isBackendImporting = metadata.sync?.importGCal === "importing"; - if (awaitingImportResultsRef.current) { + if (isImportPendingRef.current) { if (isBackendImporting) { dispatch(importGCalSlice.actions.importing(true)); } diff --git a/packages/web/src/socket/provider/SocketProvider.test.tsx b/packages/web/src/socket/provider/SocketProvider.test.tsx index b20dcdbb6..ad33e1964 100644 --- a/packages/web/src/socket/provider/SocketProvider.test.tsx +++ b/packages/web/src/socket/provider/SocketProvider.test.tsx @@ -91,7 +91,7 @@ describe("SocketProvider", () => { calendarsCount: 2, }); expect(state.sync.importGCal.importing).toBe(false); - expect(state.sync.importGCal.awaitingImportResults).toBe(false); + expect(state.sync.importGCal.isImportPending).toBe(false); expect(state.sync.importLatest.isFetchNeeded).toBe(true); }); From 776cc9e966e0c35d19ce4627b9912d8ea92c12e0 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:30:42 -0800 Subject: [PATCH 07/10] refactor(socket): simplify reconnect logic and update test case - Removed the 1-second delay in the reconnect function, allowing for immediate reconnection after disconnection. - Updated the corresponding test case to reflect the change in behavior, ensuring it accurately tests the immediate reconnection functionality. - This change enhances the responsiveness of the socket client and improves the clarity of the test case. --- packages/web/src/socket/client/socket.client.test.ts | 6 +----- packages/web/src/socket/client/socket.client.ts | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/web/src/socket/client/socket.client.test.ts b/packages/web/src/socket/client/socket.client.test.ts index e65b4a455..a1c1120eb 100644 --- a/packages/web/src/socket/client/socket.client.test.ts +++ b/packages/web/src/socket/client/socket.client.test.ts @@ -65,14 +65,10 @@ describe("socket.client", () => { }); describe("reconnect", () => { - it("disconnects and then connects after 1 second", () => { + it("disconnects and then connects immediately", () => { socketClientModule.reconnect(); expect(mockSocket.disconnect).toHaveBeenCalled(); - expect(mockSocket.connect).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1000); - expect(mockSocket.connect).toHaveBeenCalled(); }); }); diff --git a/packages/web/src/socket/client/socket.client.ts b/packages/web/src/socket/client/socket.client.ts index 1472e1be8..981baa205 100644 --- a/packages/web/src/socket/client/socket.client.ts +++ b/packages/web/src/socket/client/socket.client.ts @@ -16,11 +16,7 @@ export const disconnect = () => { export const reconnect = () => { disconnect(); - - const timeout = setTimeout(() => { - socket.connect(); - clearTimeout(timeout); - }, 1000); + socket.connect(); }; const onError = (error: unknown) => { From 30cd79d9128f4594c5c08101f3f9c6879689f13a Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:41:49 -0800 Subject: [PATCH 08/10] refactor(tests): update useGcalSync tests to remove fake timers - Removed the use of fake timers in the tests for useGcalSync, simplifying the test setup. - Updated type definitions for event handler variables to allow for undefined values, enhancing type safety. - This change improves test clarity and aligns with best practices for handling asynchronous events in React testing. --- packages/web/src/socket/hooks/useGcalSync.test.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/web/src/socket/hooks/useGcalSync.test.ts b/packages/web/src/socket/hooks/useGcalSync.test.ts index 955a75a0b..6331a07fc 100644 --- a/packages/web/src/socket/hooks/useGcalSync.test.ts +++ b/packages/web/src/socket/hooks/useGcalSync.test.ts @@ -57,7 +57,6 @@ describe("useGcalSync", () => { beforeEach(() => { jest.clearAllMocks(); - jest.useFakeTimers(); importingValue = false; awaitingValue = false; (useDispatch as jest.Mock).mockReturnValue(mockDispatch); @@ -72,10 +71,6 @@ describe("useGcalSync", () => { }); }); - afterEach(() => { - jest.useRealTimers(); - }); - it("sets up socket listeners", () => { renderHook(() => useGcalSync()); @@ -92,7 +87,7 @@ describe("useGcalSync", () => { describe("IMPORT_GCAL_START", () => { it("handles import start correctly", () => { - let importStartHandler: (importing?: boolean) => void; + let importStartHandler: ((importing?: boolean) => void) | undefined; (socket.on as jest.Mock).mockImplementation((event, handler) => { if (event === IMPORT_GCAL_START) { importStartHandler = handler; @@ -116,7 +111,7 @@ describe("useGcalSync", () => { it("sets results when awaiting import results", () => { awaitingValue = true; - let importEndHandler: (data: string) => void; + let importEndHandler: ((data: string) => void) | undefined; (socket.on as jest.Mock).mockImplementation((event, handler) => { if (event === IMPORT_GCAL_END) { importEndHandler = handler; @@ -146,7 +141,7 @@ describe("useGcalSync", () => { it("does not set results when not awaiting import results", () => { awaitingValue = false; - let importEndHandler: (data: string) => void; + let importEndHandler: ((data: string) => void) | undefined; (socket.on as jest.Mock).mockImplementation((event, handler) => { if (event === IMPORT_GCAL_END) { importEndHandler = handler; @@ -167,7 +162,7 @@ describe("useGcalSync", () => { // Simulate the race condition: starts false, changes to true, // then event arrives (testing ref pattern works correctly) - let importEndHandler: (data: string) => void; + let importEndHandler: ((data: string) => void) | undefined; (socket.on as jest.Mock).mockImplementation((event, handler) => { if (event === IMPORT_GCAL_END) { importEndHandler = handler; From 7270fc74506f43c4de4603cf3b1db78130fa7fcc Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:43:31 -0800 Subject: [PATCH 09/10] test(socket): add integration tests for Google Calendar re-authentication flow - Introduced a new test suite for the Google Calendar re-authentication process, validating user experience during import operations. - Implemented tests to ensure the spinner visibility during import initiation, handling of socket events, and correct state updates upon import completion. - Enhanced test reliability by utilizing `act` for asynchronous operations and capturing socket event callbacks. - This addition improves coverage for the re-authentication flow and ensures a seamless user experience during Google Calendar synchronization. --- .../SocketProvider.interaction.test.tsx | 491 ++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 packages/web/src/socket/provider/SocketProvider.interaction.test.tsx diff --git a/packages/web/src/socket/provider/SocketProvider.interaction.test.tsx b/packages/web/src/socket/provider/SocketProvider.interaction.test.tsx new file mode 100644 index 000000000..8ea6c4fdd --- /dev/null +++ b/packages/web/src/socket/provider/SocketProvider.interaction.test.tsx @@ -0,0 +1,491 @@ +import { act } from "react"; +import { Provider } from "react-redux"; +import { combineReducers, configureStore } from "@reduxjs/toolkit"; +import "@testing-library/jest-dom"; +import { render, screen, waitFor } from "@testing-library/react"; +import { + IMPORT_GCAL_END, + IMPORT_GCAL_START, +} from "@core/constants/websocket.constants"; +import { SyncEventsOverlay } from "@web/components/SyncEventsOverlay/SyncEventsOverlay"; +import { authSlice } from "@web/ducks/auth/slices/auth.slice"; +import { + importGCalSlice, + importLatestSlice, +} from "@web/ducks/events/slices/sync.slice"; +import { socket } from "./SocketProvider"; +import SocketProvider from "./SocketProvider"; + +// Mock dependencies +jest.mock("@web/auth/hooks/user/useUser", () => ({ + useUser: () => ({ userId: "test-user-id" }), +})); + +jest.mock("socket.io-client", () => ({ + io: jest.fn(() => ({ + on: jest.fn(), + once: jest.fn(), + emit: jest.fn(), + connect: jest.fn(), + disconnect: jest.fn(), + removeListener: jest.fn(), + connected: false, + })), +})); + +/** + * Integration tests for the Google Calendar re-authentication flow. + * + * These tests validate the complete user experience when re-authenticating + * after a session expires, ensuring the spinner appears during import + * and disappears correctly when the import completes or times out. + */ +describe("GCal Re-Authentication Flow", () => { + // Socket event callbacks captured during render + let importEndCallback: ((data?: string) => void) | undefined; + let importStartCallback: (() => void) | undefined; + + // Default timeout for import operations (30 seconds is reasonable for GCal sync) + const IMPORT_TIMEOUT_MS = 30_000; + + const createTestStore = (preloadedState?: { + isImportPending?: boolean; + importing?: boolean; + isAuthenticating?: boolean; + }) => { + return configureStore({ + reducer: { + sync: combineReducers({ + importGCal: importGCalSlice.reducer, + importLatest: importLatestSlice.reducer, + }), + auth: authSlice.reducer, + }, + preloadedState: { + sync: { + importGCal: { + importing: preloadedState?.importing ?? false, + importResults: null, + pendingLocalEventsSynced: null, + isImportPending: preloadedState?.isImportPending ?? false, + importError: null, + isLoading: false, + value: undefined, + error: undefined, + }, + importLatest: { + isFetchNeeded: false, + reason: null, + }, + }, + auth: { + status: preloadedState?.isAuthenticating ? "authenticating" : "idle", + error: null, + }, + }, + }); + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + document.body.removeAttribute("data-app-locked"); + importEndCallback = undefined; + importStartCallback = undefined; + + // Capture socket event handlers when they're registered + (socket.on as jest.Mock).mockImplementation((event, callback) => { + if (event === IMPORT_GCAL_END) { + importEndCallback = callback; + } + if (event === IMPORT_GCAL_START) { + importStartCallback = callback; + } + }); + }); + + afterEach(() => { + jest.useRealTimers(); + document.body.removeAttribute("data-app-locked"); + }); + + describe("Spinner visibility during import", () => { + it("shows spinner when user initiates re-authentication", async () => { + // User clicks "Reconnect Google Calendar" which sets isImportPending = true + const store = createTestStore({ isImportPending: true }); + + render( + + + + + , + ); + + // Spinner should be visible with import message + expect( + screen.getByText("Importing your Google Calendar events..."), + ).toBeInTheDocument(); + expect( + screen.getByText("Please hang tight while we sync your calendar"), + ).toBeInTheDocument(); + + // App should be locked (no interactions allowed) + expect(document.body.getAttribute("data-app-locked")).toBe("true"); + }); + + it("keeps spinner visible when IMPORT_GCAL_START event arrives", async () => { + const store = createTestStore({ isImportPending: true }); + + render( + + + + + , + ); + + await waitFor(() => { + expect(importStartCallback).toBeDefined(); + }); + + // Backend sends IMPORT_GCAL_START + await act(async () => { + importStartCallback?.(); + }); + + // Spinner should still be visible + expect( + screen.getByText("Importing your Google Calendar events..."), + ).toBeInTheDocument(); + + // State should now have importing = true + const state = store.getState(); + expect(state.sync.importGCal.importing).toBe(true); + }); + + it("hides spinner when import completes successfully", async () => { + const store = createTestStore({ isImportPending: true }); + + const { container } = render( + + + + + , + ); + + await waitFor(() => { + expect(importEndCallback).toBeDefined(); + }); + + // Verify spinner is initially visible + expect( + screen.getByText("Importing your Google Calendar events..."), + ).toBeInTheDocument(); + + // Backend sends IMPORT_GCAL_START then IMPORT_GCAL_END with success + await act(async () => { + importStartCallback?.(); + }); + + await act(async () => { + importEndCallback?.( + JSON.stringify({ eventsCount: 15, calendarsCount: 3 }), + ); + }); + + // Allow buffered visibility to settle + await act(async () => { + jest.advanceTimersByTime(100); + }); + + // Spinner should be hidden + expect( + screen.queryByText("Importing your Google Calendar events..."), + ).not.toBeInTheDocument(); + + // App should be unlocked + expect(container.firstChild).toBeNull(); + expect(document.body.getAttribute("data-app-locked")).toBeNull(); + + // State should reflect successful import + const state = store.getState(); + expect(state.sync.importGCal.importResults).toEqual({ + eventsCount: 15, + calendarsCount: 3, + }); + expect(state.sync.importGCal.importing).toBe(false); + expect(state.sync.importGCal.isImportPending).toBe(false); + }); + + it("hides spinner when import completes with zero events", async () => { + const store = createTestStore({ isImportPending: true }); + + render( + + + + + , + ); + + await waitFor(() => { + expect(importEndCallback).toBeDefined(); + }); + + // Backend sends IMPORT_GCAL_END with zero events (valid response) + await act(async () => { + importEndCallback?.( + JSON.stringify({ eventsCount: 0, calendarsCount: 1 }), + ); + }); + + await act(async () => { + jest.advanceTimersByTime(100); + }); + + // Spinner should be hidden even with zero events + expect( + screen.queryByText("Importing your Google Calendar events..."), + ).not.toBeInTheDocument(); + + const state = store.getState(); + expect(state.sync.importGCal.importResults).toEqual({ + eventsCount: 0, + calendarsCount: 1, + }); + }); + }); + + describe("Complete re-authentication flow", () => { + it("handles full flow: OAuth → import start → import end", async () => { + // Start with OAuth in progress (user clicked Connect/Reconnect) + const store = createTestStore({ + isAuthenticating: true, + isImportPending: true, + }); + + const { rerender } = render( + + + + + , + ); + + // Phase 1: OAuth in progress - should show sign-in message + expect( + screen.getByText("Complete Google sign-in..."), + ).toBeInTheDocument(); + + await waitFor(() => { + expect(importStartCallback).toBeDefined(); + }); + + // OAuth completes, transition to import phase + await act(async () => { + store.dispatch(authSlice.actions.resetAuth()); + }); + + rerender( + + + + + , + ); + + // Phase 2: Import in progress - should show import message + expect( + screen.getByText("Importing your Google Calendar events..."), + ).toBeInTheDocument(); + + // Backend starts import + await act(async () => { + importStartCallback?.(); + }); + + expect( + screen.getByText("Importing your Google Calendar events..."), + ).toBeInTheDocument(); + + // Backend completes import + await act(async () => { + importEndCallback?.( + JSON.stringify({ eventsCount: 42, calendarsCount: 2 }), + ); + }); + + await act(async () => { + jest.advanceTimersByTime(100); + }); + + // Phase 3: Complete - spinner should be gone + expect( + screen.queryByText("Importing your Google Calendar events..."), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("Complete Google sign-in..."), + ).not.toBeInTheDocument(); + + // Verify final state + const state = store.getState(); + expect(state.sync.importGCal.importResults).toEqual({ + eventsCount: 42, + calendarsCount: 2, + }); + expect(state.sync.importLatest.isFetchNeeded).toBe(true); + }); + }); + + describe("Timeout handling", () => { + it("import should complete within sensible timeout", async () => { + const store = createTestStore({ isImportPending: true }); + + render( + + + + + , + ); + + await waitFor(() => { + expect(importEndCallback).toBeDefined(); + }); + + // Simulate time passing but import completes before timeout + await act(async () => { + jest.advanceTimersByTime(IMPORT_TIMEOUT_MS / 2); // 15 seconds + }); + + // Spinner should still be visible (waiting for backend) + expect( + screen.getByText("Importing your Google Calendar events..."), + ).toBeInTheDocument(); + + // Backend responds successfully + await act(async () => { + importEndCallback?.( + JSON.stringify({ eventsCount: 10, calendarsCount: 1 }), + ); + }); + + await act(async () => { + jest.advanceTimersByTime(100); + }); + + // Should complete successfully + expect( + screen.queryByText("Importing your Google Calendar events..."), + ).not.toBeInTheDocument(); + + const state = store.getState(); + expect(state.sync.importGCal.importResults).not.toBeNull(); + }); + + it("handles error response from backend", async () => { + const store = createTestStore({ isImportPending: true }); + + render( + + + + + , + ); + + await waitFor(() => { + expect(importEndCallback).toBeDefined(); + }); + + // Backend sends malformed JSON (error case) + const consoleSpy = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + + await act(async () => { + importEndCallback?.("invalid-json-{{{"); + }); + + await act(async () => { + jest.advanceTimersByTime(100); + }); + + // Spinner should be hidden (error state also hides spinner) + expect( + screen.queryByText("Importing your Google Calendar events..."), + ).not.toBeInTheDocument(); + + // State should reflect error + const state = store.getState(); + expect(state.sync.importGCal.importError).toBe( + "Failed to parse Google Calendar import results.", + ); + expect(state.sync.importGCal.isImportPending).toBe(false); + + consoleSpy.mockRestore(); + }); + }); + + describe("Race condition handling (ref pattern)", () => { + it("processes import end correctly when state changes between renders", async () => { + // Start with isImportPending = false + const store = createTestStore({ isImportPending: false }); + + const { rerender } = render( + + + + + , + ); + + await waitFor(() => { + expect(importEndCallback).toBeDefined(); + }); + + // User clicks Reconnect - state changes to isImportPending = true + await act(async () => { + store.dispatch(importGCalSlice.actions.setAwaitingImportResults(true)); + }); + + rerender( + + + + + , + ); + + // Spinner should now be visible + expect( + screen.getByText("Importing your Google Calendar events..."), + ).toBeInTheDocument(); + + // Event arrives - with the ref pattern fix, this should process correctly + await act(async () => { + importEndCallback?.( + JSON.stringify({ eventsCount: 25, calendarsCount: 4 }), + ); + }); + + await act(async () => { + jest.advanceTimersByTime(100); + }); + + // Spinner should be hidden (fix prevents stale closure issue) + expect( + screen.queryByText("Importing your Google Calendar events..."), + ).not.toBeInTheDocument(); + + // Results should be set correctly + const state = store.getState(); + expect(state.sync.importGCal.importResults).toEqual({ + eventsCount: 25, + calendarsCount: 4, + }); + }); + }); +}); From b1a6c4015a4cd764c589e08f5dd0a55c731f6981 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 27 Feb 2026 14:48:03 -0800 Subject: [PATCH 10/10] refactor(sync): rename setAwaitingImportResults to setIsImportPending - Updated the Redux action and corresponding function names to improve clarity and consistency in the import state management. - Adjusted all related components and tests to reflect the new naming convention, ensuring seamless integration across the codebase. - This change enhances code readability and aligns with the overall naming strategy in the project. --- e2e/utils/oauth-test-utils.ts | 8 ++++---- .../web/src/auth/hooks/oauth/useGoogleAuth.test.ts | 8 ++++---- packages/web/src/auth/hooks/oauth/useGoogleAuth.ts | 10 +++++----- packages/web/src/ducks/events/slices/sync.slice.ts | 2 +- .../provider/SocketProvider.interaction.test.tsx | 2 +- .../web/src/socket/provider/SocketProvider.test.tsx | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/e2e/utils/oauth-test-utils.ts b/e2e/utils/oauth-test-utils.ts index 76942af1a..b3cdd3c10 100644 --- a/e2e/utils/oauth-test-utils.ts +++ b/e2e/utils/oauth-test-utils.ts @@ -74,13 +74,13 @@ export const setIsSyncing = async (page: Page, value: boolean) => { /** * Set the awaitingImportResults state in Redux (triggers import phase when true). */ -export const setAwaitingImportResults = async (page: Page, value: boolean) => { - await page.evaluate((awaitingValue) => { +export const setIsImportPending = async (page: Page, value: boolean) => { + await page.evaluate((isImportPendingValue) => { const store = (window as any).__COMPASS_STORE__; if (store) { store.dispatch({ - type: "async/importGCal/setAwaitingImportResults", - payload: awaitingValue, + type: "async/importGCal/setIsImportPending", + payload: isImportPendingValue, }); } }, value); diff --git a/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts b/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts index e97d810e3..3cf268cc6 100644 --- a/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts +++ b/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts @@ -161,7 +161,7 @@ describe("useGoogleAuth", () => { ); expect(mockDispatchFn).toHaveBeenCalledWith( expect.objectContaining({ - type: "async/importGCal/setAwaitingImportResults", + type: "async/importGCal/setIsImportPending", payload: true, }), ); @@ -193,7 +193,7 @@ describe("useGoogleAuth", () => { expect(mockDispatchFn).toHaveBeenCalledWith( expect.objectContaining({ - type: "async/importGCal/setAwaitingImportResults", + type: "async/importGCal/setIsImportPending", payload: false, }), ); @@ -252,7 +252,7 @@ describe("useGoogleAuth", () => { expect(mockSetAuthenticated).not.toHaveBeenCalled(); expect(mockDispatchFn).toHaveBeenCalledWith( expect.objectContaining({ - type: "async/importGCal/setAwaitingImportResults", + type: "async/importGCal/setIsImportPending", payload: false, }), ); @@ -298,7 +298,7 @@ describe("useGoogleAuth", () => { expect(mockDispatchFn).toHaveBeenCalledWith( expect.objectContaining({ - type: "async/importGCal/setAwaitingImportResults", + type: "async/importGCal/setIsImportPending", payload: false, }), ); diff --git a/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts b/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts index 779b6c759..f1d3b7165 100644 --- a/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts +++ b/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts @@ -31,7 +31,7 @@ export function useGoogleAuth() { onStart: () => { dismissErrorToast(SESSION_EXPIRED_TOAST_ID); dispatch(startAuthenticating()); - dispatch(importGCalSlice.actions.setAwaitingImportResults(true)); + dispatch(importGCalSlice.actions.setIsImportPending(true)); dispatch(importGCalSlice.actions.clearImportResults(undefined)); }, onSuccess: async (data) => { @@ -42,7 +42,7 @@ export function useGoogleAuth() { dispatch( authError(authResult.error?.message || "Authentication failed"), ); - dispatch(importGCalSlice.actions.setAwaitingImportResults(false)); + dispatch(importGCalSlice.actions.setIsImportPending(false)); dispatch(importGCalSlice.actions.importing(false)); return; } @@ -59,7 +59,7 @@ export function useGoogleAuth() { dispatch(authSuccess()); // Now that OAuth is complete, indicate that calendar import is starting dispatch(importGCalSlice.actions.importing(true)); - dispatch(importGCalSlice.actions.setAwaitingImportResults(true)); + dispatch(importGCalSlice.actions.setIsImportPending(true)); }); const syncResult = await syncLocalEvents(); @@ -90,7 +90,7 @@ export function useGoogleAuth() { error instanceof Error ? error.message : "Authentication failed", ), ); - dispatch(importGCalSlice.actions.setAwaitingImportResults(false)); + dispatch(importGCalSlice.actions.setIsImportPending(false)); dispatch(importGCalSlice.actions.importing(false)); throw error; // Re-throw so useGoogleLoginWithSyncOverlay can handle it via onError } @@ -102,7 +102,7 @@ export function useGoogleAuth() { error instanceof Error ? error.message : "Authentication failed", ), ); - dispatch(importGCalSlice.actions.setAwaitingImportResults(false)); + dispatch(importGCalSlice.actions.setIsImportPending(false)); dispatch(importGCalSlice.actions.importing(false)); }, }); diff --git a/packages/web/src/ducks/events/slices/sync.slice.ts b/packages/web/src/ducks/events/slices/sync.slice.ts index ae97bfec2..a2e8575ba 100644 --- a/packages/web/src/ducks/events/slices/sync.slice.ts +++ b/packages/web/src/ducks/events/slices/sync.slice.ts @@ -46,7 +46,7 @@ export const importGCalSlice = createAsyncSlice< importing: (state, action: PayloadAction) => { state.importing = action.payload; }, - setAwaitingImportResults: (state, action: PayloadAction) => { + setIsImportPending: (state, action: PayloadAction) => { state.isImportPending = action.payload; if (action.payload) { state.importError = null; diff --git a/packages/web/src/socket/provider/SocketProvider.interaction.test.tsx b/packages/web/src/socket/provider/SocketProvider.interaction.test.tsx index 8ea6c4fdd..deeaffc92 100644 --- a/packages/web/src/socket/provider/SocketProvider.interaction.test.tsx +++ b/packages/web/src/socket/provider/SocketProvider.interaction.test.tsx @@ -448,7 +448,7 @@ describe("GCal Re-Authentication Flow", () => { // User clicks Reconnect - state changes to isImportPending = true await act(async () => { - store.dispatch(importGCalSlice.actions.setAwaitingImportResults(true)); + store.dispatch(importGCalSlice.actions.setIsImportPending(true)); }); rerender( diff --git a/packages/web/src/socket/provider/SocketProvider.test.tsx b/packages/web/src/socket/provider/SocketProvider.test.tsx index ad33e1964..dc9f15cbc 100644 --- a/packages/web/src/socket/provider/SocketProvider.test.tsx +++ b/packages/web/src/socket/provider/SocketProvider.test.tsx @@ -61,7 +61,7 @@ describe("SocketProvider", () => { }, }); - store.dispatch(importGCalSlice.actions.setAwaitingImportResults(true)); + store.dispatch(importGCalSlice.actions.setIsImportPending(true)); render(