Skip to content

Commit e259a97

Browse files
committed
feat(tests): add end-to-end tests for sidebar Google Calendar connection status
- Introduced a new test suite for verifying the sidebar connection status icons based on various Google connection states. - Implemented tests for all connection states: NOT_CONNECTED, RECONNECT_REQUIRED, IMPORTING, HEALTHY, ATTENTION, and the client-only "checking" state. - Added state transition tests to ensure correct visual representation and behavior when cycling through connection states. - Updated utility functions to support the new testing scenarios, enhancing overall test coverage and reliability.
1 parent 290242f commit e259a97

2 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { expect, test } from "@playwright/test";
2+
import {
3+
type GoogleConnectionState,
4+
SIDEBAR_ICON_LABELS,
5+
markUserAsAuthenticated,
6+
prepareOAuthTestPage,
7+
setGoogleConnectionState,
8+
waitForAppReady,
9+
} from "../utils/oauth-test-utils";
10+
11+
/**
12+
* E2E tests for sidebar Google Calendar connection status icon.
13+
*
14+
* These tests verify that the sidebar icon correctly reflects the 5 connection
15+
* states from the server (GoogleConnectionState), plus the client-only "checking"
16+
* state that appears while metadata is loading.
17+
*
18+
* Connection states and their icons:
19+
* - NOT_CONNECTED: CloudArrowUpIcon (cloud with arrow)
20+
* - RECONNECT_REQUIRED: LinkBreakIcon (broken link warning)
21+
* - IMPORTING: SpinnerIcon (loading spinner)
22+
* - HEALTHY: LinkIcon (connected link)
23+
* - ATTENTION: CloudWarningIcon (cloud with warning)
24+
* - "checking" (client-only): SpinnerIcon (loading spinner)
25+
*
26+
* The icon is rendered in SidebarIconRow and state is managed by useConnectGoogle hook.
27+
*
28+
* NOTE: These tests are skipped on mobile because the MobileGate component
29+
* blocks the entire app on mobile viewports.
30+
*/
31+
test.describe("Sidebar Connection Status", () => {
32+
// Skip on mobile - MobileGate blocks the app
33+
test.skip(({ isMobile }) => isMobile, "Sidebar not available on mobile");
34+
35+
// Run tests serially to avoid state interference
36+
test.describe.configure({ mode: "serial" });
37+
38+
test.beforeEach(async ({ page }) => {
39+
await prepareOAuthTestPage(page);
40+
await page.goto("/week");
41+
await waitForAppReady(page);
42+
// Extra wait to ensure app is fully stable
43+
await page.waitForTimeout(200);
44+
});
45+
46+
test("shows cloud upload icon when NOT_CONNECTED", async ({ page }) => {
47+
await setGoogleConnectionState(page, "NOT_CONNECTED");
48+
49+
const icon = page.getByLabel(SIDEBAR_ICON_LABELS.notConnected);
50+
await expect(icon).toBeVisible();
51+
});
52+
53+
test("shows spinner icon when metadata is loading (checking state)", async ({
54+
page,
55+
}) => {
56+
// The "checking" state requires:
57+
// 1. userMetadataStatus === "loading"
58+
// 2. hasUserEverAuthenticated() returns true (localStorage flag)
59+
//
60+
// Set the localStorage flag first, then force loading state.
61+
await markUserAsAuthenticated(page);
62+
63+
// Force the loading state by dispatching both clear and setLoading
64+
await page.evaluate(() => {
65+
const store = (window as any).__COMPASS_STORE__;
66+
if (!store) return;
67+
// Clear any existing metadata
68+
store.dispatch({ type: "userMetadata/clear" });
69+
// Set to loading state
70+
store.dispatch({ type: "userMetadata/setLoading" });
71+
});
72+
73+
// Wait for state to be in loading
74+
await page.waitForFunction(
75+
() => {
76+
const store = (window as any).__COMPASS_STORE__;
77+
return store?.getState()?.userMetadata?.status === "loading";
78+
},
79+
{ timeout: 5000 },
80+
);
81+
82+
// Small delay for React to re-render
83+
await page.waitForTimeout(100);
84+
85+
const icon = page.getByLabel(SIDEBAR_ICON_LABELS.syncing);
86+
await expect(icon).toBeVisible({ timeout: 5000 });
87+
});
88+
89+
test("shows spinner icon during IMPORTING state", async ({ page }) => {
90+
await setGoogleConnectionState(page, "IMPORTING");
91+
92+
const icon = page.getByLabel(SIDEBAR_ICON_LABELS.syncing);
93+
await expect(icon).toBeVisible();
94+
});
95+
96+
test("shows link icon when HEALTHY", async ({ page }) => {
97+
await setGoogleConnectionState(page, "HEALTHY");
98+
99+
const icon = page.getByLabel(SIDEBAR_ICON_LABELS.connected);
100+
await expect(icon).toBeVisible();
101+
});
102+
103+
test("shows warning icon when ATTENTION", async ({ page }) => {
104+
await setGoogleConnectionState(page, "ATTENTION");
105+
106+
const icon = page.getByLabel(SIDEBAR_ICON_LABELS.needsRepair);
107+
await expect(icon).toBeVisible();
108+
});
109+
110+
test("shows broken link icon when RECONNECT_REQUIRED", async ({ page }) => {
111+
await setGoogleConnectionState(page, "RECONNECT_REQUIRED");
112+
113+
const icon = page.getByLabel(SIDEBAR_ICON_LABELS.reconnectRequired);
114+
await expect(icon).toBeVisible();
115+
});
116+
});
117+
118+
test.describe("Sidebar Connection Status - State Transitions", () => {
119+
// Skip on mobile - MobileGate blocks the app
120+
test.skip(({ isMobile }) => isMobile, "Sidebar not available on mobile");
121+
122+
// Run tests serially to avoid state interference
123+
test.describe.configure({ mode: "serial" });
124+
125+
test.beforeEach(async ({ page }) => {
126+
await prepareOAuthTestPage(page);
127+
await page.goto("/week");
128+
await waitForAppReady(page);
129+
// Extra wait to ensure app is fully stable
130+
await page.waitForTimeout(200);
131+
});
132+
133+
test("transitions from NOT_CONNECTED to HEALTHY correctly", async ({
134+
page,
135+
}) => {
136+
// Start with NOT_CONNECTED
137+
await setGoogleConnectionState(page, "NOT_CONNECTED");
138+
await expect(
139+
page.getByLabel(SIDEBAR_ICON_LABELS.notConnected),
140+
).toBeVisible();
141+
142+
// Transition to HEALTHY (simulating successful OAuth flow)
143+
await setGoogleConnectionState(page, "HEALTHY");
144+
await expect(page.getByLabel(SIDEBAR_ICON_LABELS.connected)).toBeVisible();
145+
146+
// Previous icon should no longer be visible
147+
await expect(
148+
page.getByLabel(SIDEBAR_ICON_LABELS.notConnected),
149+
).not.toBeVisible();
150+
});
151+
152+
test("cycles through connection states without visual glitches", async ({
153+
page,
154+
}) => {
155+
const states: GoogleConnectionState[] = [
156+
"NOT_CONNECTED",
157+
"IMPORTING",
158+
"HEALTHY",
159+
"ATTENTION",
160+
"RECONNECT_REQUIRED",
161+
];
162+
163+
for (const state of states) {
164+
await setGoogleConnectionState(page, state);
165+
// Brief pause to allow state to settle
166+
await page.waitForTimeout(50);
167+
}
168+
169+
// Should end on RECONNECT_REQUIRED
170+
await expect(
171+
page.getByLabel(SIDEBAR_ICON_LABELS.reconnectRequired),
172+
).toBeVisible();
173+
});
174+
});

e2e/utils/oauth-test-utils.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,80 @@ export const expectNoOverlay = async (page: Page) => {
140140
await expect(page.getByText(OVERLAY_TEXT.oauthTitle)).not.toBeVisible();
141141
await expectBodyLocked(page, false);
142142
};
143+
144+
/**
145+
* Google connection states that can be set via Redux.
146+
*/
147+
export type GoogleConnectionState =
148+
| "NOT_CONNECTED"
149+
| "RECONNECT_REQUIRED"
150+
| "IMPORTING"
151+
| "HEALTHY"
152+
| "ATTENTION";
153+
154+
/**
155+
* Set the Google connection state via Redux userMetadata slice.
156+
* This updates the sidebar icon to reflect the given connection state.
157+
*/
158+
export const setGoogleConnectionState = async (
159+
page: Page,
160+
state: GoogleConnectionState,
161+
) => {
162+
// Wait for store to be fully available
163+
await page.waitForFunction(
164+
() => typeof (window as any).__COMPASS_STORE__?.dispatch === "function",
165+
{ timeout: 5000 },
166+
);
167+
168+
await page.evaluate((connectionState) => {
169+
const store = (window as any).__COMPASS_STORE__;
170+
if (!store) return;
171+
store.dispatch({
172+
type: "userMetadata/set",
173+
payload: { google: { connectionState } },
174+
});
175+
}, state);
176+
177+
// Wait for the icon to reflect the new state by checking the store
178+
await page.waitForFunction(
179+
(expectedState) => {
180+
const store = (window as any).__COMPASS_STORE__;
181+
return (
182+
store?.getState()?.userMetadata?.current?.google?.connectionState ===
183+
expectedState
184+
);
185+
},
186+
state,
187+
{ timeout: 5000 },
188+
);
189+
190+
// Small additional delay for React to re-render
191+
await page.waitForTimeout(50);
192+
};
193+
194+
/**
195+
* Mark user as having previously authenticated (sets localStorage flag).
196+
* This is required for the "checking" state to appear when metadata is loading.
197+
*/
198+
export const markUserAsAuthenticated = async (page: Page) => {
199+
await page.evaluate(() => {
200+
// Must match STORAGE_KEYS.AUTH from storage.constants.ts
201+
const AUTH_STATE_KEY = "compass.auth";
202+
const existing = localStorage.getItem(AUTH_STATE_KEY);
203+
const state = existing ? JSON.parse(existing) : {};
204+
state.hasAuthenticated = true;
205+
localStorage.setItem(AUTH_STATE_KEY, JSON.stringify(state));
206+
});
207+
};
208+
209+
/**
210+
* Icon aria-labels for each Google connection state in the sidebar.
211+
* These match the aria-labels defined in SidebarIconRow.tsx's getGoogleStatusIcon.
212+
*/
213+
export const SIDEBAR_ICON_LABELS = {
214+
notConnected: "Google Calendar not connected",
215+
reconnectRequired: "Google Calendar needs reconnecting",
216+
syncing: "Google Calendar syncing", // Used for both "checking" and "IMPORTING" states
217+
connected: "Google Calendar connected",
218+
needsRepair: "Google Calendar needs repair",
219+
};

0 commit comments

Comments
 (0)