Skip to content

Commit 70ae341

Browse files
committed
fix(settings): surfaces sync failure notification, tests disabled state
1 parent c2268a4 commit 70ae341

2 files changed

Lines changed: 32 additions & 28 deletions

File tree

src/app/components/settings/SettingsPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useNavigate } from "@solidjs/router";
33
import { config, updateConfig } from "../../stores/config";
44
import { clearAuth } from "../../stores/auth";
55
import { clearCache } from "../../stores/cache";
6+
import { pushNotification } from "../../lib/errors";
67
import { buildOrgAccessUrl } from "../../lib/oauth";
78
import { isSafeGitHubUrl, openGitHubUrl } from "../../lib/url";
89
import { fetchOrgs } from "../../services/api";
@@ -221,7 +222,7 @@ export default function SettingsPage() {
221222
console.info(`[settings] merged ${newOrgs.length} new org(s)`);
222223
}
223224
} catch {
224-
// Non-fatal — user can manually manage orgs
225+
pushNotification("org-sync", "Failed to sync organizations — try again or manage manually", "warning");
225226
} finally {
226227
setMerging(false);
227228
}

tests/components/settings/SettingsPage.test.tsx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ vi.mock("../../../src/app/lib/url", () => ({
4646
openGitHubUrl: vi.fn(),
4747
}));
4848

49+
vi.mock("../../../src/app/lib/errors", () => ({
50+
pushNotification: vi.fn(),
51+
}));
52+
4953
// ── Imports after mocks ───────────────────────────────────────────────────────
5054

5155
import { render } from "@solidjs/testing-library";
@@ -530,53 +534,59 @@ describe("SettingsPage — Theme application", () => {
530534
});
531535

532536
describe("SettingsPage — Grant more orgs button", () => {
537+
beforeEach(() => {
538+
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
539+
});
540+
afterEach(() => {
541+
vi.unstubAllEnvs();
542+
});
543+
533544
it("renders 'Grant more orgs' button in Organizations & Repositories section", () => {
534545
renderSettings();
535546
screen.getByRole("button", { name: "Grant more orgs" });
536547
});
537548

538549
it("clicking 'Grant more orgs' opens GitHub app settings via openGitHubUrl", async () => {
539550
const user = userEvent.setup();
540-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
541551
renderSettings();
542552
const btn = screen.getByRole("button", { name: "Grant more orgs" });
543553
await user.click(btn);
544554
expect(urlModule.openGitHubUrl).toHaveBeenCalledWith(buildOrgAccessUrl());
545-
vi.unstubAllEnvs();
546555
});
547556

548557
it("clicking 'Grant more orgs' registers a focus listener for auto-merge", async () => {
549558
const user = userEvent.setup();
550-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
551559
const addSpy = vi.spyOn(window, "addEventListener");
552560
renderSettings();
553561
const btn = screen.getByRole("button", { name: "Grant more orgs" });
554562
await user.click(btn);
555563
expect(addSpy).toHaveBeenCalledWith("focus", expect.any(Function));
556-
vi.unstubAllEnvs();
557564
});
558565

559-
it("shows 'Syncing...' on button while merging and reverts after", async () => {
566+
it("shows disabled 'Syncing...' button during merge, reverts after", async () => {
560567
const user = userEvent.setup();
561-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
562568
updateConfig({ selectedOrgs: [] });
563-
vi.mocked(apiModule.fetchOrgs).mockResolvedValue([]);
569+
let resolveFetch!: (v: never[]) => void;
570+
vi.mocked(apiModule.fetchOrgs).mockReturnValue(
571+
new Promise((r) => { resolveFetch = r as (v: never[]) => void; })
572+
);
564573
renderSettings();
565574
const btn = screen.getByRole("button", { name: "Grant more orgs" });
566575
await user.click(btn);
567576
window.dispatchEvent(new Event("focus"));
568577
await waitFor(() => {
569-
expect(screen.getByRole("button", { name: "Syncing..." })).toBeDefined();
578+
const syncBtn = screen.getByRole("button", { name: "Syncing..." });
579+
expect(syncBtn.hasAttribute("disabled")).toBe(true);
570580
});
581+
resolveFetch([]);
571582
await waitFor(() => {
572-
expect(screen.getByRole("button", { name: "Grant more orgs" })).toBeDefined();
583+
const restored = screen.getByRole("button", { name: "Grant more orgs" });
584+
expect(restored.hasAttribute("disabled")).toBe(false);
573585
});
574-
vi.unstubAllEnvs();
575586
});
576587

577588
it("auto-merges new orgs when window regains focus after granting", async () => {
578589
const user = userEvent.setup();
579-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
580590
updateConfig({ selectedOrgs: ["existing-org"] });
581591
vi.mocked(apiModule.fetchOrgs).mockResolvedValue([
582592
{ login: "existing-org", avatarUrl: "", type: "org" },
@@ -585,7 +595,6 @@ describe("SettingsPage — Grant more orgs button", () => {
585595
renderSettings();
586596
const btn = screen.getByRole("button", { name: "Grant more orgs" });
587597
await user.click(btn);
588-
// Simulate user returning from GitHub settings tab
589598
window.dispatchEvent(new Event("focus"));
590599
await waitFor(() => {
591600
expect(apiModule.fetchOrgs).toHaveBeenCalled();
@@ -594,12 +603,11 @@ describe("SettingsPage — Grant more orgs button", () => {
594603
expect(config.selectedOrgs).toContain("new-org");
595604
expect(config.selectedOrgs).toContain("existing-org");
596605
});
597-
vi.unstubAllEnvs();
598606
});
599607

600-
it("silently handles fetchOrgs rejection on focus without breaking", async () => {
608+
it("pushes warning notification on fetchOrgs failure", async () => {
609+
const { pushNotification } = await import("../../../src/app/lib/errors");
601610
const user = userEvent.setup();
602-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
603611
updateConfig({ selectedOrgs: ["existing-org"] });
604612
vi.mocked(apiModule.fetchOrgs).mockRejectedValue(new Error("Network error"));
605613
renderSettings();
@@ -609,55 +617,52 @@ describe("SettingsPage — Grant more orgs button", () => {
609617
await waitFor(() => {
610618
expect(apiModule.fetchOrgs).toHaveBeenCalled();
611619
});
620+
await waitFor(() => {
621+
expect(pushNotification).toHaveBeenCalledWith(
622+
"org-sync",
623+
expect.stringContaining("Failed to sync"),
624+
"warning",
625+
);
626+
});
612627
expect(config.selectedOrgs).toEqual(["existing-org"]);
613-
vi.unstubAllEnvs();
614628
});
615629

616630
it("skips merge on focus when getClient returns null", async () => {
617631
const user = userEvent.setup();
618-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
619632
const github = await import("../../../src/app/services/github");
620633
vi.mocked(github.getClient).mockReturnValueOnce(null);
621634
renderSettings();
622635
const btn = screen.getByRole("button", { name: "Grant more orgs" });
623636
await user.click(btn);
624637
window.dispatchEvent(new Event("focus"));
625-
// Give mergeNewOrgs time to bail out
626638
await new Promise((r) => setTimeout(r, 50));
627639
expect(apiModule.fetchOrgs).not.toHaveBeenCalled();
628-
vi.unstubAllEnvs();
629640
});
630641

631642
it("rapid double-click deduplicates focus listeners", async () => {
632643
const user = userEvent.setup();
633-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
634644
const removeSpy = vi.spyOn(window, "removeEventListener");
635645
renderSettings();
636646
const btn = screen.getByRole("button", { name: "Grant more orgs" });
637647
await user.click(btn);
638648
await user.click(btn);
639-
// Second click removes the first listener before adding a new one
640649
const focusRemoves = removeSpy.mock.calls.filter(([evt]) => evt === "focus");
641650
expect(focusRemoves.length).toBeGreaterThanOrEqual(1);
642-
vi.unstubAllEnvs();
643651
});
644652

645653
it("cleans up pending focus listener on component unmount", async () => {
646654
const user = userEvent.setup();
647-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
648655
const removeSpy = vi.spyOn(window, "removeEventListener");
649656
const { unmount } = renderSettings();
650657
const btn = screen.getByRole("button", { name: "Grant more orgs" });
651658
await user.click(btn);
652659
unmount();
653660
const focusRemoves = removeSpy.mock.calls.filter(([evt]) => evt === "focus");
654661
expect(focusRemoves.length).toBeGreaterThanOrEqual(1);
655-
vi.unstubAllEnvs();
656662
});
657663

658664
it("focus listener self-removes — second focus does not re-trigger merge", async () => {
659665
const user = userEvent.setup();
660-
vi.stubEnv("VITE_GITHUB_CLIENT_ID", "test-client-id");
661666
updateConfig({ selectedOrgs: [] });
662667
vi.mocked(apiModule.fetchOrgs).mockResolvedValue([]);
663668
renderSettings();
@@ -667,10 +672,8 @@ describe("SettingsPage — Grant more orgs button", () => {
667672
await waitFor(() => {
668673
expect(apiModule.fetchOrgs).toHaveBeenCalledTimes(1);
669674
});
670-
// Second focus should NOT trigger another merge
671675
window.dispatchEvent(new Event("focus"));
672676
await new Promise((r) => setTimeout(r, 50));
673677
expect(apiModule.fetchOrgs).toHaveBeenCalledTimes(1);
674-
vi.unstubAllEnvs();
675678
});
676679
});

0 commit comments

Comments
 (0)