Skip to content

Commit 9dd671b

Browse files
committed
test(dashboard): adds expandDefault and flash-peek coverage
Adds tests for the per-tab expandDefault custom-tab cleanup (removeCustomTabState, resetViewState, and DashboardPage mount/delete pruning including the expandDefault-only footprint path), the jiraAssigned expanded-by-default behavior via isRepoExpanded, later-surfacing-repo default inheritance in the PullRequests and Actions tabs, and the flashDetection peek suppression and aggregation branches.
1 parent 1efd320 commit 9dd671b

5 files changed

Lines changed: 195 additions & 7 deletions

File tree

‎tests/components/ActionsTab.test.tsx‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,28 @@ describe("ActionsTab", () => {
394394
expect(screen.getAllByText("CI").length).toBeGreaterThanOrEqual(1);
395395
});
396396

397+
it("expand all keeps a repo that appears in a later data update expanded", async () => {
398+
const user = userEvent.setup();
399+
const [runs, setRuns] = createSignal<WorkflowRun[]>([
400+
makeWorkflowRun({ repoFullName: "owner/repo-a", workflowId: 1, name: "CI-A" }),
401+
]);
402+
render(() => <ActionsTab workflowRuns={runs()} />);
403+
404+
// Expand all — sets the tab default to expanded
405+
await user.click(screen.getByRole("button", { name: /Expand all/i }));
406+
expect(screen.getAllByText("CI-A").length).toBeGreaterThanOrEqual(1);
407+
408+
// A brand-new repo arrives later (never present when Expand all was clicked)
409+
setRuns([
410+
makeWorkflowRun({ repoFullName: "owner/repo-a", workflowId: 1, name: "CI-A" }),
411+
makeWorkflowRun({ repoFullName: "owner/repo-b", workflowId: 2, name: "CI-B" }),
412+
]);
413+
414+
// It inherits the expanded default with no further interaction
415+
screen.getByText("owner/repo-b");
416+
expect(screen.getAllByText("CI-B").length).toBeGreaterThanOrEqual(1);
417+
});
418+
397419
it("expanded repo state persists in viewState", async () => {
398420
const user = userEvent.setup();
399421
const runs = [

‎tests/components/DashboardPage.test.tsx‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,7 +1484,7 @@ describe("DashboardPage — runtime redirect when active custom tab is deleted",
14841484
// ── Orphaned view state cleanup ──────────────────────────────────────────────
14851485

14861486
describe("DashboardPage — orphaned view state cleanup", () => {
1487-
it("removes customTabFilters and expandedRepos keys when a custom tab is deleted", async () => {
1487+
it("removes customTabFilters, expandedRepos, and expandDefault keys when a custom tab is deleted", async () => {
14881488
configStore.addCustomTab({
14891489
id: "orphan01",
14901490
name: "Orphan Tab",
@@ -1495,29 +1495,49 @@ describe("DashboardPage — orphaned view state cleanup", () => {
14951495
exclusive: false,
14961496
});
14971497
viewStore.setCustomTabFilter("orphan01", "role", "author");
1498+
viewStore.setAllExpanded("orphan01", true);
14981499
viewStore.toggleExpandedRepo("orphan01", "myorg/repo");
14991500

15001501
render(() => <DashboardPage />);
15011502

15021503
await waitFor(() => {
15031504
expect(viewStore.viewState.customTabFilters["orphan01"]).toBeDefined();
1505+
expect(viewStore.viewState.expandDefault["orphan01"]).toBe(true);
15041506
});
15051507

15061508
configStore.removeCustomTab("orphan01");
15071509

15081510
await waitFor(() => {
15091511
expect(viewStore.viewState.customTabFilters["orphan01"]).toBeUndefined();
15101512
expect(viewStore.viewState.expandedRepos["orphan01"]).toBeUndefined();
1513+
expect(viewStore.viewState.expandDefault["orphan01"]).toBeUndefined();
15111514
});
15121515
});
15131516

1514-
it("prunes stale customTabFilters entries at mount time for unknown tab IDs", async () => {
1517+
it("prunes stale customTabFilters and expandDefault entries at mount time for unknown tab IDs", async () => {
15151518
viewStore.setCustomTabFilter("ghost-tab", "role", "assignee");
1519+
viewStore.setAllExpanded("ghost-tab", true);
1520+
expect(viewStore.viewState.expandDefault["ghost-tab"]).toBe(true);
15161521

15171522
render(() => <DashboardPage />);
15181523

15191524
await waitFor(() => {
15201525
expect(viewStore.viewState.customTabFilters["ghost-tab"]).toBeUndefined();
1526+
expect(viewStore.viewState.expandDefault["ghost-tab"]).toBeUndefined();
1527+
});
1528+
});
1529+
1530+
it("prunes a stale tab whose only footprint is an expandDefault entry at mount time", async () => {
1531+
// A divergent persisted blob can carry an expandDefault entry for a tab with no
1532+
// matching expandedRepos/customTabFilters key — the cleanup must still discover it
1533+
// via the expandDefault key set alone.
1534+
viewStore.updateViewState({ expandDefault: { jiraAssigned: true, "ghost-default": true } });
1535+
expect(viewStore.viewState.expandDefault["ghost-default"]).toBe(true);
1536+
1537+
render(() => <DashboardPage />);
1538+
1539+
await waitFor(() => {
1540+
expect(viewStore.viewState.expandDefault["ghost-default"]).toBeUndefined();
15211541
});
15221542
});
15231543
});

‎tests/components/PullRequestsTab.test.tsx‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,28 @@ describe("PullRequestsTab", () => {
613613
screen.getByText("Repo B PR 0");
614614
});
615615

616+
it("expand all keeps a repo that appears in a later data update expanded", async () => {
617+
const user = userEvent.setup();
618+
const [prs, setPrs] = createSignal<PullRequest[]>([
619+
makePullRequest({ id: 1, title: "Repo A PR", repoFullName: "org/repo-a" }),
620+
]);
621+
render(() => <PullRequestsTab pullRequests={prs()} userLogin="" />);
622+
623+
// Expand all — sets the tab default to expanded
624+
await user.click(screen.getByLabelText("Expand all repos"));
625+
screen.getByText("Repo A PR");
626+
627+
// A brand-new repo arrives later (never present when Expand all was clicked)
628+
setPrs([
629+
makePullRequest({ id: 1, title: "Repo A PR", repoFullName: "org/repo-a" }),
630+
makePullRequest({ id: 2, title: "Repo B PR", repoFullName: "org/repo-b" }),
631+
]);
632+
633+
// It inherits the expanded default with no further interaction
634+
screen.getByText("org/repo-b");
635+
screen.getByText("Repo B PR");
636+
});
637+
616638
it("applies shimmer class to rows whose IDs are in hotPollingPRIds", () => {
617639
const prs = [
618640
makePullRequest({ id: 42, number: 42, title: "Hot PR", repoFullName: "org/repo" }),

‎tests/lib/flashDetection.test.ts‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,102 @@ describe("createFlashDetection", () => {
9999
dispose();
100100
});
101101
});
102+
103+
it("builds a peek update when a hot-polled item changes status and its repo is collapsed", async () => {
104+
await createRoot(async (dispose) => {
105+
const [items, setItems] = createSignal<MockItem[]>([
106+
{ id: 1, repoFullName: "org/repo", status: "pending" },
107+
]);
108+
const { flashingIds, peekUpdates } = createFlashDetection({
109+
getItems: items,
110+
getHotIds: () => new Set([1]),
111+
isRepoExpanded: () => false,
112+
trackKey: (item) => item.status,
113+
itemLabel: (item) => `Item ${item.id}`,
114+
itemStatus: (item) => item.status,
115+
});
116+
117+
// Let the initial effect seed prevValues (Solid schedules effects as microtasks)
118+
await Promise.resolve();
119+
120+
// Status change on a hot-polled item whose repo is collapsed — peek expected
121+
setItems([{ id: 1, repoFullName: "org/repo", status: "success" }]);
122+
await Promise.resolve();
123+
124+
expect(flashingIds().has(1)).toBe(true);
125+
expect(peekUpdates().size).toBe(1);
126+
expect(peekUpdates().get("org/repo")).toEqual({
127+
itemLabel: "Item 1",
128+
newStatus: "success",
129+
});
130+
131+
dispose();
132+
});
133+
});
134+
135+
it("suppresses the peek update when the item's repo is expanded", async () => {
136+
await createRoot(async (dispose) => {
137+
const [items, setItems] = createSignal<MockItem[]>([
138+
{ id: 1, repoFullName: "org/repo", status: "pending" },
139+
]);
140+
const { flashingIds, peekUpdates } = createFlashDetection({
141+
getItems: items,
142+
getHotIds: () => new Set([1]),
143+
isRepoExpanded: () => true,
144+
trackKey: (item) => item.status,
145+
itemLabel: (item) => `Item ${item.id}`,
146+
itemStatus: (item) => item.status,
147+
});
148+
149+
// Let the initial effect seed prevValues (Solid schedules effects as microtasks)
150+
await Promise.resolve();
151+
152+
// Status change on a hot-polled item whose repo is expanded — flash still
153+
// fires, but the peek preview is suppressed for the expanded repo
154+
setItems([{ id: 1, repoFullName: "org/repo", status: "success" }]);
155+
await Promise.resolve();
156+
157+
expect(flashingIds().has(1)).toBe(true);
158+
expect(peekUpdates().size).toBe(0);
159+
160+
dispose();
161+
});
162+
});
163+
164+
it("aggregates the peek label when multiple items in a collapsed repo change", async () => {
165+
await createRoot(async (dispose) => {
166+
const [items, setItems] = createSignal<MockItem[]>([
167+
{ id: 1, repoFullName: "org/repo", status: "pending" },
168+
{ id: 2, repoFullName: "org/repo", status: "pending" },
169+
]);
170+
const { peekUpdates } = createFlashDetection({
171+
getItems: items,
172+
getHotIds: () => new Set([1, 2]),
173+
isRepoExpanded: () => false,
174+
trackKey: (item) => item.status,
175+
itemLabel: (item) => `Item ${item.id}`,
176+
itemStatus: (item) => item.status,
177+
});
178+
179+
// Let the initial effect seed prevValues (Solid schedules effects as microtasks)
180+
await Promise.resolve();
181+
182+
// Two hot-polled items in the same collapsed repo change — the peek label
183+
// aggregates into a "first + N more" summary. The differing statuses confirm
184+
// the aggregated newStatus reflects the first changed item, not the last.
185+
setItems([
186+
{ id: 1, repoFullName: "org/repo", status: "success" },
187+
{ id: 2, repoFullName: "org/repo", status: "failure" },
188+
]);
189+
await Promise.resolve();
190+
191+
expect(peekUpdates().size).toBe(1);
192+
expect(peekUpdates().get("org/repo")).toEqual({
193+
itemLabel: "Item 1 + 1 more",
194+
newStatus: "success",
195+
});
196+
197+
dispose();
198+
});
199+
});
102200
});

‎tests/stores/view.test.ts‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -717,20 +717,20 @@ describe("ViewStateSchema", () => {
717717
});
718718

719719
it("migrates pre-expandDefault data: Jira defaults to expanded, GitHub tabs collapsed, existing entries preserved", () => {
720-
// Pre-refactor blob: no expandDefault key; expandedRepos has explicit manual-expand
721-
// entries. Projects the user previously collapsed are simply absent from the map.
720+
// Legacy blob: no expandDefault key; expandedRepos holds explicit per-repo entries.
721+
// A Jira project with no entry in the map has no recorded expand/collapse state.
722722
const oldData = {
723723
lastActiveTab: "issues",
724724
expandedRepos: { issues: { "org/repo": true }, jiraAssigned: { "PROJ": true } },
725725
};
726726
const result = ViewStateSchema.parse(oldData);
727-
// Backfilled default: Jira expanded (preserves prior auto-expand), GitHub tabs collapsed.
727+
// Backfilled default: jiraAssigned defaults to expanded; GitHub tabs default to collapsed.
728728
expect(result.expandDefault).toEqual({ jiraAssigned: true });
729729
// Explicit entries are preserved as exceptions.
730730
expect(result.expandedRepos.issues["org/repo"]).toBe(true);
731731
expect(result.expandedRepos.jiraAssigned["PROJ"]).toBe(true);
732-
// A Jira project absent from the map (default true, no exception) now reads expanded —
733-
// the intended one-time reset for projects collapsed under the old delete-on-collapse model.
732+
// A Jira project absent from the map has no exception recorded, so it reads
733+
// expanded via the jiraAssigned default.
734734
expect(result.expandedRepos.jiraAssigned["ABSENT-PROJ"]).toBeUndefined();
735735
expect(result.expandDefault.jiraAssigned).toBe(true);
736736
});
@@ -795,6 +795,15 @@ describe("expandedRepos helpers", () => {
795795
expect(isRepoExpanded("issues", "owner/anything")).toBe(false);
796796
});
797797

798+
it("jiraAssigned defaults to expanded on a fresh store", () => {
799+
expect(isRepoExpanded("jiraAssigned", "SOMEPROJ")).toBe(true);
800+
});
801+
802+
it("setAllExpanded('jiraAssigned', false) persists for a later-surfacing project group", () => {
803+
setAllExpanded("jiraAssigned", false);
804+
expect(isRepoExpanded("jiraAssigned", "LATER-PROJ")).toBe(false);
805+
});
806+
798807
it("pruneExpandedRepos removes stale exception keys and keeps active ones", () => {
799808
toggleExpandedRepo("actions", "owner/active");
800809
toggleExpandedRepo("actions", "owner/stale");
@@ -1257,6 +1266,13 @@ describe("removeCustomTabState", () => {
12571266
expect("tab-abc" in viewState.expandedRepos).toBe(false);
12581267
});
12591268

1269+
it("cleans expandDefault for the given tab ID", () => {
1270+
setAllExpanded("tab-abc", true);
1271+
expect(viewState.expandDefault["tab-abc"]).toBe(true);
1272+
removeCustomTabState("tab-abc");
1273+
expect("tab-abc" in viewState.expandDefault).toBe(false);
1274+
});
1275+
12601276
it("removes both customTabFilters and expandedRepos in a single call", () => {
12611277
setCustomTabFilter("tab-abc", "scope", "all");
12621278
toggleExpandedRepo("tab-abc", "owner/repo");
@@ -1309,6 +1325,16 @@ describe("resetViewState — custom tab fields", () => {
13091325
expect("tab-custom" in viewState.expandedRepos).toBe(false);
13101326
});
13111327

1328+
it("clears custom tab keys from expandDefault while preserving the jiraAssigned default", () => {
1329+
setAllExpanded("tab-custom", true);
1330+
expect(viewState.expandDefault["tab-custom"]).toBe(true);
1331+
resetViewState();
1332+
// Builtin jiraAssigned default is preserved (not deleted)
1333+
expect(viewState.expandDefault["jiraAssigned"]).toBe(true);
1334+
// Custom key is fully deleted
1335+
expect("tab-custom" in viewState.expandDefault).toBe(false);
1336+
});
1337+
13121338
it("clears custom tab keys from lockedRepos and resets built-in keys to []", () => {
13131339
lockRepo("issues", "owner/repo");
13141340
lockRepo("tab-custom", "owner/repo");

0 commit comments

Comments
 (0)