Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ describe("formatLastSyncedLabel", () => {

it("formats recent relative ages", () => {
expect(formatLastSyncedLabel("2026-07-24T11:59:30.000Z", nowMs)).toBe(
"Last synced just now",
"Updated just now",
);
expect(formatLastSyncedLabel("2026-07-24T11:59:00.000Z", nowMs)).toBe(
"Last synced 1 minute ago",
"Updated 1 minute ago",
);
expect(formatLastSyncedLabel("2026-07-24T11:45:00.000Z", nowMs)).toBe(
"Last synced 15 minutes ago",
"Updated 15 minutes ago",
);
expect(formatLastSyncedLabel("2026-07-24T10:00:00.000Z", nowMs)).toBe(
"Last synced 2 hours ago",
"Updated 2 hours ago",
);
expect(formatLastSyncedLabel("2026-07-22T12:00:00.000Z", nowMs)).toBe(
"Last synced 2 days ago",
"Updated 2 days ago",
);
});
});
Expand All @@ -41,25 +41,22 @@ describe("getGoogleSyncStatus", () => {
it("returns healthy copy for connected Google", () => {
expect(getGoogleSyncStatus("HEALTHY")).toEqual({
variant: "healthy",
text: "Calendar up-to-date",
text: "Calendar connected",
});
});

it("returns checking copy while metadata loads", () => {
expect(getGoogleSyncStatus("checking")).toEqual({
variant: "syncing",
text: "Checking calendar status…",
});
it("hides transient metadata loading without a connection summary", () => {
expect(getGoogleSyncStatus("checking")).toBeNull();
});

it("returns syncing copy while importing", () => {
it("uses setup copy while importing", () => {
expect(getGoogleSyncStatus("IMPORTING")).toEqual({
variant: "syncing",
text: "Syncing your calendar…",
text: "Adding your calendar…",
});
});

it("shows checking progress over a cached healthy connection", () => {
it("keeps a cached healthy connection calm while metadata loads", () => {
expect(
getGoogleSyncStatus("checking", {
id: "c1",
Expand All @@ -70,12 +67,12 @@ describe("getGoogleSyncStatus", () => {
accountEmail: "a@example.com",
}),
).toEqual({
variant: "syncing",
text: "Checking calendar status…",
variant: "healthy",
text: "Calendar connected",
});
});

it("shows import progress over a cached healthy connection", () => {
it("keeps a cached healthy connection calm during a routine refresh", () => {
expect(
getGoogleSyncStatus("IMPORTING", {
id: "c1",
Expand All @@ -86,24 +83,25 @@ describe("getGoogleSyncStatus", () => {
accountEmail: "a@example.com",
}),
).toEqual({
variant: "syncing",
text: "Syncing your calendar…",
variant: "healthy",
text: "Calendar connected",
});
});

it("returns warning copy for ATTENTION, without using the word 'repair'", () => {
const status = getGoogleSyncStatus("ATTENTION");

expect(status?.variant).toBe("warning");
expect(status?.text.toLowerCase()).not.toContain("repair");
expect(status?.text.toLowerCase()).toContain("refresh");
expect(status?.text).toBe(
"Calendar updates are taking longer than usual. We'll keep trying.",
);
});

it("returns error copy for RECONNECT_REQUIRED", () => {
expect(getGoogleSyncStatus("RECONNECT_REQUIRED")?.variant).toBe("error");
});

it("uses the same syncing copy for every in-progress Sync state", () => {
it("shows setup copy for a connection that has never been healthy", () => {
expect(
getGoogleSyncStatus("IMPORTING", {
id: "c1",
Expand All @@ -115,7 +113,7 @@ describe("getGoogleSyncStatus", () => {
}),
).toEqual({
variant: "syncing",
text: "Syncing your calendar…",
text: "Adding your calendar…",
});
});

Expand All @@ -131,7 +129,7 @@ describe("getGoogleSyncStatus", () => {
}),
).toEqual({
variant: "warning",
text: "Calendar sync is delayed — try Refresh",
text: "Calendar updates are taking longer than usual. We'll keep trying.",
});
});

Expand All @@ -147,7 +145,7 @@ describe("getGoogleSyncStatus", () => {
}),
).toEqual({
variant: "healthy",
text: "Calendar up-to-date",
text: "Calendar connected",
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import {

const CONNECT_ICON: CommandActionIcon = CloudArrowUpIcon;
const REFRESH_ICON: CommandActionIcon = ArrowsClockwiseIcon;
const CONNECTED_STATUS: SyncStatus = {
variant: "healthy",
text: "Calendar connected",
};
const DELAYED_STATUS: SyncStatus = {
variant: "warning",
text: "Calendar updates are taking longer than usual. We'll keep trying.",
};

/** Short relative label for Sync connection `lastSyncedAt` (ISO). */
export const formatLastSyncedLabel = (
Expand All @@ -26,31 +34,31 @@ export const formatLastSyncedLabel = (

const deltaSec = Math.max(0, Math.floor((nowMs - syncedMs) / 1000));
if (deltaSec < 60) {
return "Last synced just now";
return "Updated just now";
}

const deltaMin = Math.floor(deltaSec / 60);
if (deltaMin < 60) {
return deltaMin === 1
? "Last synced 1 minute ago"
: `Last synced ${deltaMin} minutes ago`;
? "Updated 1 minute ago"
: `Updated ${deltaMin} minutes ago`;
}

const deltaHr = Math.floor(deltaMin / 60);
if (deltaHr < 24) {
return deltaHr === 1
? "Last synced 1 hour ago"
: `Last synced ${deltaHr} hours ago`;
? "Updated 1 hour ago"
: `Updated ${deltaHr} hours ago`;
}

const deltaDay = Math.floor(deltaHr / 24);
if (deltaDay < 7) {
return deltaDay === 1
? "Last synced 1 day ago"
: `Last synced ${deltaDay} days ago`;
? "Updated 1 day ago"
: `Updated ${deltaDay} days ago`;
}

return `Last synced ${new Date(syncedMs).toLocaleDateString()}`;
return `Updated ${new Date(syncedMs).toLocaleDateString()}`;
};

export type GoogleConnectionHandlers = {
Expand Down Expand Up @@ -102,29 +110,21 @@ export const getGoogleSyncStatus = (
state: GoogleUiState,
connection?: GoogleSyncConnectionSummary | null,
): SyncStatus => {
// Local transient states are newer than a cached server summary, which can
// still report healthy while a fresh check or import is underway.
if (state === "checking") {
return { variant: "syncing", text: "Checking calendar status…" };
}

if (state === "IMPORTING") {
return { variant: "syncing", text: "Syncing your calendar…" };
}

// A connection summary describes durable provider work. Local metadata
// loading and a routine incremental pull must not replace a calm, usable
// calendar with transient "checking" or "syncing" copy.
if (connection) {
switch (connection.state) {
case "healthy":
return { variant: "healthy", text: "Calendar up-to-date" };
return CONNECTED_STATUS;
case "connecting":
case "importing":
case "catchingUp":
return { variant: "syncing", text: "Syncing your calendar…" };
return connection.lastHealthyAt
? CONNECTED_STATUS
: { variant: "syncing", text: "Adding your calendar…" };
case "delayed":
return {
variant: "warning",
text: "Calendar sync is delayed — try Refresh",
};
return DELAYED_STATUS;
case "actionRequired":
case "disconnected":
// Product enum already distinguishes reconnect vs soft attention.
Expand All @@ -133,13 +133,14 @@ export const getGoogleSyncStatus = (
}

switch (state) {
case "checking":
return null;
case "IMPORTING":
return { variant: "syncing", text: "Adding your calendar…" };
case "HEALTHY":
return { variant: "healthy", text: "Calendar up-to-date" };
return CONNECTED_STATUS;
case "ATTENTION":
return {
variant: "warning",
text: "Calendar needs a refresh",
};
return DELAYED_STATUS;
case "RECONNECT_REQUIRED":
return { variant: "error", text: "Calendar needs reconnecting" };
case "NOT_CONNECTED":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ describe("CalendarListHeader", () => {
expect(email.tagName).toBe("SPAN");
expect(email).toHaveClass("text-text");
expect(email).not.toHaveClass("c-sync-text-wave");
expect(screen.getByRole("status")).toHaveTextContent("Calendar up-to-date");
expect(screen.queryByText(/Last synced/)).toBeNull();
expect(screen.getByRole("status")).toHaveTextContent("Calendar connected");
expect(screen.queryByText(/Updated/)).toBeNull();

await user.hover(email);
expect(screen.queryByRole("tooltip")).toBeNull();
Expand All @@ -220,26 +220,25 @@ describe("CalendarListHeader", () => {

renderHeader();

expect(screen.getByRole("status")).toHaveTextContent("Calendar up-to-date");
expect(screen.getByText("Last synced just now")).toBeInTheDocument();
expect(screen.getByRole("status")).toHaveTextContent("Calendar connected");
expect(screen.getByText("Updated just now")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /Google Calendar/ }),
).toBeNull();
});

it.each([
["IMPORTING", "Syncing your calendar…"],
["checking", "Checking calendar status…"],
] as const)("shows the visible %s status for %s", async (state, status) => {
it("shows setup status only while the first calendar import is in progress", async () => {
const user = userEvent.setup();
mockEmail = "ahab@pequod.com";
mockGoogleState = state;
mockGoogleState = "IMPORTING";

renderHeader();

const email = screen.getByText("ahab@pequod.com");
expect(email).toHaveClass("c-sync-text-wave");
expect(screen.getByRole("status")).toHaveTextContent(status);
expect(screen.getByRole("status")).toHaveTextContent(
"Adding your calendar…",
);

await user.hover(email);
expect(screen.queryByRole("tooltip")).toBeNull();
Expand All @@ -257,7 +256,7 @@ describe("CalendarListHeader", () => {
expect(email).toHaveClass("text-text");
expect(email).not.toHaveClass("text-warning");
expect(screen.getByRole("status")).toHaveTextContent(
"Calendar needs a refresh",
"Calendar updates are taking longer than usual. We'll keep trying.",
);
expect(screen.getByRole("status")).toHaveClass("text-warning");

Expand Down Expand Up @@ -340,7 +339,7 @@ describe("CalendarListHeader", () => {
name: "Refreshing…",
});
expect(screen.getByRole("status")).toHaveTextContent(
"Requesting a calendar refresh…",
"Calendar updates are taking longer than usual. We'll keep trying.",
);
expect(refreshButton).toBeDisabled();
expect(refreshButton).toHaveAttribute("aria-busy", "true");
Expand Down Expand Up @@ -379,7 +378,7 @@ describe("CalendarListHeader", () => {
);
});

it("uses the same syncing copy for an in-progress reconciliation", () => {
it("keeps an established calendar calm during reconciliation", () => {
mockEmail = "ahab@pequod.com";
mockGoogleState = "HEALTHY";
userMetadataActions.set({
Expand All @@ -398,10 +397,8 @@ describe("CalendarListHeader", () => {

renderHeader();

expect(screen.getByRole("status")).toHaveTextContent(
"Syncing your calendar…",
);
expect(screen.queryByText("Catching up your calendar…")).toBeNull();
expect(screen.queryByText(/Last synced/)).toBeNull();
expect(screen.getByRole("status")).toHaveTextContent("Calendar connected");
expect(screen.queryByText("Adding your calendar…")).toBeNull();
expect(screen.getByText("Updated just now")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,11 @@ const getSidebarSyncStatus = ({
googleStatus,
hasPendingEventMutations,
isConnecting,
isRefreshing,
state,
}: {
googleStatus: SyncStatus;
hasPendingEventMutations: boolean;
isConnecting: boolean;
isRefreshing: boolean;
state: GoogleUiState;
}): SyncStatus => {
if (isConnecting) {
Expand All @@ -71,10 +69,6 @@ const getSidebarSyncStatus = ({
};
}

if (isRefreshing) {
return { variant: "syncing", text: "Requesting a calendar refresh…" };
}

if (googleStatus && googleStatus.variant !== "healthy") {
return googleStatus;
}
Expand Down Expand Up @@ -154,7 +148,6 @@ const AuthenticatedAccountHeader: FC<{ email: string }> = ({ email }) => {
googleStatus: getGoogleSyncStatus(state, syncConnection),
hasPendingEventMutations,
isConnecting,
isRefreshing,
state,
});
const isSyncing =
Expand Down
15 changes: 15 additions & 0 deletions packages/web/src/sse/hooks/useSyncFocusRefresh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ describe("useSyncFocusRefresh", () => {
expect(refresh).toHaveBeenCalledTimes(1);
});

it("does not retrigger after the refresh changes connection state", () => {
const refresh = mock();
let state: UseConnectGoogleResult["state"] = "HEALTHY";
const hook = renderHook(() =>
useSyncFocusRefresh(() => fakeConnectGoogle({ refresh, state })),
);

state = "IMPORTING";
hook.rerender();
state = "HEALTHY";
hook.rerender();

expect(refresh).toHaveBeenCalledTimes(1);
});

it("refreshes again when the tab regains focus after being hidden long enough", () => {
setSystemTime(new Date("2026-02-05T00:00:00.000Z"));
const refresh = mock();
Expand Down
Loading