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
69 changes: 64 additions & 5 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
import { act, render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import { useGitHubData } from "./hooks/useGitHubData";
import { useDashboardStore } from "./stores/dashboard";
import type { Activity } from "./lib/types/github";
import type { DashboardData, DashboardStats } from "./lib/types/dashboard";

vi.mock("./lib/open", () => ({
openUrl: vi.fn(),
Expand All @@ -18,16 +21,55 @@ vi.mock("./lib/tauri", async (importOriginal) => {
});

vi.mock("./hooks/useGitHubData", () => ({
useGitHubData: vi.fn().mockReturnValue({
dashboard: { syncedAt: "2026-03-28T10:00:00Z", reviewRequests: [], myPullRequests: [], assignedIssues: [], recentActivity: [], workspaces: [] },
stats: { pendingReviews: 3, openPrs: 5, openIssues: 2, totalWorkspaces: 1, unreadActivity: 0 },
useGitHubData: vi.fn(),
}));

function makeActivity(id: string): Activity {
return {
id,
activityType: "comment_added",
actor: "alice",
repoId: "repo-1",
pullRequestId: "pr-1",
issueId: null,
message: "Some comment",
isRead: false,
createdAt: "2026-03-28T10:00:00Z",
};
}

function makeGitHubDataMock(overrides?: {
readonly dashboard?: Partial<DashboardData>;
readonly stats?: Partial<DashboardStats>;
}): ReturnType<typeof useGitHubData> {
return {
dashboard: {
syncedAt: "2026-03-28T10:00:00Z",
reviewRequests: [],
myPullRequests: [],
assignedIssues: [],
recentActivity: [],
workspaces: [],
...overrides?.dashboard,
},
stats: {
pendingReviews: 3,
openPrs: 5,
openIssues: 2,
totalWorkspaces: 1,
unreadActivity: 0,
...overrides?.stats,
},
isLoading: false,
error: null,
authExpired: false,
syncError: null,
forceSync: vi.fn(),
isSyncing: false,
}),
}));
};
}

const mockedUseGitHubData = vi.mocked(useGitHubData);

function renderApp() {
const queryClient = new QueryClient({
Expand All @@ -42,6 +84,7 @@ function renderApp() {

describe("App layout", () => {
beforeEach(() => {
mockedUseGitHubData.mockReturnValue(makeGitHubDataMock());
useDashboardStore.setState({
currentView: "overview",
activeFilters: {},
Expand Down Expand Up @@ -104,6 +147,21 @@ describe("App layout", () => {
expect(await screen.findByTestId("activity-feed")).toBeInTheDocument();
});

it("should use unread activity stats for the activity heading", async () => {
mockedUseGitHubData.mockReturnValue(
makeGitHubDataMock({
dashboard: { recentActivity: [makeActivity("act-1")] },
stats: { unreadActivity: 149 },
}),
);
useDashboardStore.setState({ currentView: "feed" });

renderApp();

expect(await screen.findByTestId("activity-feed")).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Activity 149" })).toBeInTheDocument();
});

it("should render settings view", async () => {
useDashboardStore.setState({ currentView: "settings" });
renderApp();
Expand Down Expand Up @@ -153,6 +211,7 @@ describe("App keyboard shortcuts", () => {
let mockedOpenUrl: ReturnType<typeof vi.fn>;

beforeEach(async () => {
mockedUseGitHubData.mockReturnValue(makeGitHubDataMock());
// Use "feed" view — it has no useRegisterNavigableItems hook,
// so pre-seeded navigableItems are preserved for keyboard tests.
useDashboardStore.setState({
Expand Down
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ interface MainContentProps {
}

function MainContent({ view, onBackToDashboard }: MainContentProps): ReactElement {
const { dashboard } = useGitHubData();
const { dashboard, stats } = useGitHubData();
const queryClient = useQueryClient();

const markAllRead = useMutation({
Expand Down Expand Up @@ -184,6 +184,7 @@ function MainContent({ view, onBackToDashboard }: MainContentProps): ReactElemen
return (
<ActivityFeed
activities={dashboard?.recentActivity ?? []}
headerCount={stats?.unreadActivity}
onMarkAllRead={() => markAllRead.mutate()}
/>
);
Expand Down
13 changes: 13 additions & 0 deletions src/components/ActivityFeed/ActivityFeed.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ describe("ActivityFeed", () => {
expect(screen.getByText("6")).toBeInTheDocument();
});

it("should prefer an explicit header count over the visible activity count", () => {
render(
<ActivityFeed
activities={[commentActivity]}
headerCount={149}
onMarkAllRead={onMarkAllRead}
/>,
);

expect(screen.getByRole("heading", { name: "Activity 149" })).toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Activity 1" })).not.toBeInTheDocument();
});

it("should hide section header when hideHeader is true", () => {
render(<ActivityFeed activities={allActivities} onMarkAllRead={onMarkAllRead} hideHeader />);

Expand Down
5 changes: 4 additions & 1 deletion src/components/ActivityFeed/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ActivityItem } from "./ActivityItem";

interface ActivityFeedProps {
readonly activities: readonly Activity[];
readonly headerCount?: number;
readonly isLoading?: boolean;
readonly onMarkAllRead: () => void;
readonly hideHeader?: boolean;
Expand Down Expand Up @@ -56,6 +57,7 @@ function matchesFilter(activity: Activity, filter: FilterType): boolean {

export function ActivityFeed({
activities,
headerCount,
isLoading = false,
onMarkAllRead,
hideHeader = false,
Expand All @@ -79,14 +81,15 @@ export function ActivityFeed({
value.toLowerCase().includes(normalizedQuery),
);
});
const sectionCount = isLoading ? undefined : headerCount ?? visible.length;

return (
<section
data-testid="activity-feed"
aria-busy={isLoading ? "true" : undefined}
className="flex flex-col gap-2"
>
{!hideHeader && <SectionHead title="Activity" count={isLoading ? undefined : visible.length} />}
{!hideHeader && <SectionHead title="Activity" count={sectionCount} />}

{isLoading ? (
<>
Expand Down
Loading