Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d6b214a
refactor(auth): update Google connection UI and logic
tyler-dane Mar 30, 2026
f6fda1c
refactor(auth): update Google connection UI and logic
tyler-dane Mar 30, 2026
2239567
refactor(sync): enhance StatusDotPopover for improved user feedback
tyler-dane Mar 30, 2026
25af54d
feat(auth): implement anonymous calendar change sign-up prompt
tyler-dane Mar 30, 2026
a64f5d1
fix(auth): update tooltip message for Google Calendar repair status
tyler-dane Mar 30, 2026
777fcf1
feat(sync): add sync dot pulse animation and update component logic
tyler-dane Mar 30, 2026
ec6b8c7
refactor(auth): remove AccountIcon component and related tests
tyler-dane Mar 30, 2026
8543790
refactor(calendar): simplify Header component structure and remove un…
tyler-dane Mar 30, 2026
7faffe4
refactor(calendar): streamline Header component by removing styled co…
tyler-dane Mar 30, 2026
26074cc
refactor(calendar): replace styled components with utility classes in…
tyler-dane Mar 30, 2026
fbf8ec5
refactor(sync): separate StatusDotPopover into its own component
tyler-dane Mar 30, 2026
1522685
refactor(calendar): remove Info icon component and update SubCalendar…
tyler-dane Mar 30, 2026
b27452e
refactor(auth): update Google connection status handling and introduc…
tyler-dane Mar 30, 2026
7ad7dfc
refactor(auth): remove icon references from Google connection handling
tyler-dane Mar 30, 2026
bb0a9d8
refactor(auth): enhance UserProvider and HeaderInfoIcon components fo…
tyler-dane Mar 30, 2026
010d32b
refactor(auth): streamline UserProvider and introduce useLoadProfile …
tyler-dane Mar 30, 2026
69c6048
feat(auth): implement Google authentication flow and enhance session …
tyler-dane Mar 30, 2026
41319c0
feat(tests): enhance e2e tests for Google Calendar connection status
tyler-dane Mar 30, 2026
e051434
feat(sync): enhance Google Calendar import handling and introduce aut…
tyler-dane Mar 31, 2026
bc7ef25
feat(sync): enhance Google Calendar import handling with new operatio…
tyler-dane Mar 31, 2026
dc6e14e
feat(sync): enhance Google Calendar metadata handling and user feedback
tyler-dane Mar 31, 2026
33914f6
feat(sync): improve Google Calendar sync handling and UI feedback
tyler-dane Mar 31, 2026
ced3808
fix(sync): correct promise handling in sync service tests
tyler-dane Mar 31, 2026
f54fd90
refactor(auth): reorganize Google authentication utilities and state …
tyler-dane Mar 31, 2026
2d26952
feat(auth): implement session management and user profile loading
tyler-dane Mar 31, 2026
b43cfbe
fix(auth): update Google OAuth error utility imports and add tests
tyler-dane Mar 31, 2026
69b72fe
feat(auth): add Posthog utility function and integrate into CompassPr…
tyler-dane Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 33 additions & 69 deletions e2e/oauth/sidebar-connection-status.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import { expect, test } from "@playwright/test";
import { type Page, expect, test } from "@playwright/test";
import {
type GoogleConnectionState,
SIDEBAR_STATUS_LABELS,
expectGoogleConnectionStateInStore,
markUserAsAuthenticated,
prepareOAuthTestPage,
setGoogleConnectionState,
waitForAppReady,
} from "../utils/oauth-test-utils";

/**
* E2E tests for sidebar Google Calendar connection status.
* E2E tests for Google Calendar connection state (Redux + header status when visible).
*
* These tests verify that the sidebar status container correctly reflects the 5 connection
* states from the server (GoogleConnectionState), plus the client-only "checking"
* state that appears while metadata is loading.
*
* Connection states and their status messages:
* - NOT_CONNECTED: "Google Calendar not connected. Click to connect."
* - RECONNECT_REQUIRED: "Google Calendar needs reconnecting. Click to reconnect."
* - IMPORTING: "Google Calendar is syncing in the background."
* - HEALTHY: "Google Calendar connected."
* - ATTENTION: "Google Calendar needs repair. Click to repair."
* - "checking" (client-only): "Checking Google Calendar status…"
*
* The status is rendered in SidebarIconRow with role="status" and managed by useConnectGoogle hook.
* HeaderInfoIcon only renders role="status" for warning/error states (reconnect required,
* needs repair). Other states are reflected in Redux only; command palette still exposes
* connect/repair actions.
*
* NOTE: These tests are skipped on mobile because the MobileGate component
* blocks the entire app on mobile viewports.
Expand All @@ -35,10 +26,8 @@ test.describe("Sidebar Connection Status", () => {
// Run tests serially to avoid state interference
test.describe.configure({ mode: "serial" });

// Helper to get the sidebar status container
// Filter: has aria-label (excludes DndLiveRegion), no aria-busy (excludes overlay)
const getSidebarStatus = (page: import("@playwright/test").Page) =>
page.locator('#sidebar [role="status"][aria-label]:not([aria-busy])');
const getHeaderGoogleStatus = (page: Page) =>
page.locator("#cal").getByRole("status", { name: /Google Calendar/i });

test.beforeEach(async ({ page }) => {
await prepareOAuthTestPage(page);
Expand All @@ -50,48 +39,33 @@ test.describe("Sidebar Connection Status", () => {
await setGoogleConnectionState(page, "NOT_CONNECTED");
});

test("shows NOT_CONNECTED status", async ({ page }) => {
// State already set in beforeEach - just verify it

const status = getSidebarStatus(page);
await expect(status).toHaveAttribute(
"aria-label",
SIDEBAR_STATUS_LABELS.notConnected,
);
test("stores NOT_CONNECTED in Redux (header icon hidden for muted state)", async ({
page,
}) => {
await expectGoogleConnectionStateInStore(page, "NOT_CONNECTED");
});

test("shows checking status when metadata is loading", async ({ page }) => {
// The "checking" state requires:
// 1. userMetadataStatus === "loading"
// 2. hasUserEverAuthenticated() returns true (localStorage flag)
//
// Set the localStorage flag first, then force loading state.
test("checking path: authenticated user with metadata loading", async ({
page,
}) => {
await markUserAsAuthenticated(page);

// Force the loading state by dispatching both clear and setLoading
await page.evaluate(() => {
const store = window.__COMPASS_E2E_STORE__;
if (!store) return;
// Clear any existing metadata
store.dispatch({ type: "userMetadata/clear" });
// Set to loading state
store.dispatch({ type: "userMetadata/setLoading" });
});

// Wait for state to be in loading
await page.waitForFunction(
() =>
window.__COMPASS_E2E_STORE__?.getState()?.userMetadata?.status ===
"loading",
{ timeout: 5000 },
);

// Wait for status container to show "checking" state via aria-label
const status = getSidebarStatus(page);
await expect(status).toHaveAttribute(
"aria-label",
/Checking Google Calendar/i,
);
// "Checking" does not render HeaderInfoIcon (muted / no warning-error icon).
await expect(getHeaderGoogleStatus(page)).toHaveCount(0);
});

test("shows IMPORTING status", async ({ page }) => {
Expand All @@ -118,11 +92,6 @@ test.describe("Sidebar Connection Status - State Transitions", () => {
// Run tests serially to avoid state interference
test.describe.configure({ mode: "serial" });

// Helper to get the sidebar status container
// Filter: has aria-label (excludes DndLiveRegion), no aria-busy (excludes overlay)
const getSidebarStatus = (page: import("@playwright/test").Page) =>
page.locator('#sidebar [role="status"][aria-label]:not([aria-busy])');

test.beforeEach(async ({ page }) => {
await prepareOAuthTestPage(page);
await page.goto("/week");
Expand All @@ -133,23 +102,20 @@ test.describe("Sidebar Connection Status - State Transitions", () => {
await setGoogleConnectionState(page, "NOT_CONNECTED");
});

test("transitions from NOT_CONNECTED to HEALTHY correctly", async ({
test("transitions from RECONNECT_REQUIRED to ATTENTION correctly", async ({
page,
}) => {
const status = getSidebarStatus(page);

// State already set to NOT_CONNECTED in beforeEach - just verify it
await expect(status).toHaveAttribute(
"aria-label",
SIDEBAR_STATUS_LABELS.notConnected,
);
await setGoogleConnectionState(page, "RECONNECT_REQUIRED");
await expect(
page.getByRole("status", {
name: SIDEBAR_STATUS_LABELS.reconnectRequired,
}),
).toBeVisible();

// Transition to HEALTHY (simulating successful OAuth flow)
await setGoogleConnectionState(page, "HEALTHY");
await expect(status).toHaveAttribute(
"aria-label",
SIDEBAR_STATUS_LABELS.connected,
);
await setGoogleConnectionState(page, "ATTENTION");
await expect(
page.getByRole("status", { name: SIDEBAR_STATUS_LABELS.needsRepair }),
).toBeVisible();
});

test("cycles through connection states without visual glitches", async ({
Expand All @@ -165,14 +131,12 @@ test.describe("Sidebar Connection Status - State Transitions", () => {

for (const state of states) {
await setGoogleConnectionState(page, state);
// setGoogleConnectionState now waits for aria-label, no timeout needed
}

// Should end on RECONNECT_REQUIRED
const status = getSidebarStatus(page);
await expect(status).toHaveAttribute(
"aria-label",
SIDEBAR_STATUS_LABELS.reconnectRequired,
);
await expect(
page.getByRole("status", {
name: SIDEBAR_STATUS_LABELS.reconnectRequired,
}),
).toBeVisible();
});
});
51 changes: 43 additions & 8 deletions e2e/utils/oauth-test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Page, expect } from "@playwright/test";
import { type Page, expect } from "@playwright/test";
import "./compass-window";

/**
Expand Down Expand Up @@ -200,13 +200,20 @@ const CONNECTION_STATE_TO_LABEL: Record<GoogleConnectionState, RegExp> = {
ATTENTION: /needs repair/i,
};

const SIDEBAR_STATUS_SELECTOR =
"#sidebar [role='status'][aria-label]:not([aria-busy])";
/**
* HeaderInfoIcon only renders role="status" for warning/error connection states
* (see HeaderInfoIcon.tsx). Muted / checking / importing-without-background-import
* do not expose this region.
*/
const GOOGLE_HEADER_STATUS_VISIBLE_STATES: GoogleConnectionState[] = [
"RECONNECT_REQUIRED",
"ATTENTION",
];

/**
* Set the Google connection state via Redux userMetadata slice.
* Dispatches to the store and waits for the sidebar aria-label to update.
* waitForAppReady in beforeEach guarantees the store is ready before this runs.
* Dispatches to the store, waits for Redux to match, then asserts the header
* status region when the UI shows it (reconnect required / needs repair).
*/
export const setGoogleConnectionState = async (
page: Page,
Expand All @@ -221,9 +228,37 @@ export const setGoogleConnectionState = async (
});
}, state);

await expect(page.locator(SIDEBAR_STATUS_SELECTOR)).toHaveAttribute(
"aria-label",
CONNECTION_STATE_TO_LABEL[state],
await page.waitForFunction(
(expected) => {
const cs =
window.__COMPASS_E2E_STORE__?.getState()?.userMetadata?.current?.google
?.connectionState;
return cs === expected;
},
state,
{ timeout: 5000 },
);

if (GOOGLE_HEADER_STATUS_VISIBLE_STATES.includes(state)) {
await expect(
page.getByRole("status", { name: CONNECTION_STATE_TO_LABEL[state] }),
).toBeVisible();
}
};

export const expectGoogleConnectionStateInStore = async (
page: Page,
state: GoogleConnectionState,
) => {
await page.waitForFunction(
(expected) => {
const cs =
window.__COMPASS_E2E_STORE__?.getState()?.userMetadata?.current?.google
?.connectionState;
return cs === expected;
},
state,
{ timeout: 5000 },
);
};

Expand Down
9 changes: 9 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ export default [
"jest/unbound-method": "error",
},
},
{
files: ["e2e/**/*.ts"],
rules: {
// Playwright e2e is not React Testing Library; page.getByRole is correct.
"testing-library/prefer-screen-queries": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
},
},
// Warn on console.log in packages/web to avoid leaking secure info
{
files: ["packages/web/**/*.{ts,tsx}"],
Expand Down
12 changes: 7 additions & 5 deletions packages/backend/src/sync/controllers/sync.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,22 @@ describe("SyncController", () => {
const baseDriver = new BaseDriver();
const syncDriver = new SyncControllerDriver(baseDriver);
const importTimeoutMs = 7_000;
type ImportOperation = "INCREMENTAL" | "REPAIR";

interface ImportSummary {
operation: "REPAIR";
operation: ImportOperation;
status: "COMPLETED";
eventsCount: number;
calendarsCount: number;
}

function parseImportResult(
result: ImportGCalEndPayload | undefined,
operation: ImportOperation = "INCREMENTAL",
): ImportSummary {
expect(result).toEqual(
expect.objectContaining({
operation: "REPAIR",
operation,
status: "COMPLETED",
eventsCount: expect.any(Number) as number,
calendarsCount: expect.any(Number) as number,
Expand Down Expand Up @@ -607,7 +609,7 @@ describe("SyncController", () => {
const result = (await importEndPromise) as ImportGCalEndPayload;
stream.close();

const parsed = parseImportResult(result);
const parsed = parseImportResult(result, "REPAIR");

expect(parsed).toHaveProperty("eventsCount");
expect(parsed).toHaveProperty("calendarsCount");
Expand Down Expand Up @@ -643,7 +645,7 @@ describe("SyncController", () => {
stream.close();

expect(failReason).toEqual({
operation: "REPAIR",
operation: "INCREMENTAL",
status: "IGNORED",
message: `User ${userId} gcal import is in progress or completed, ignoring this request`,
});
Expand Down Expand Up @@ -683,7 +685,7 @@ describe("SyncController", () => {
stream.close();

expect(failReason).toEqual({
operation: "REPAIR",
operation: "INCREMENTAL",
status: "IGNORED",
message: `User ${userId} gcal import is in progress or completed, ignoring this request`,
});
Expand Down
Loading
Loading