diff --git a/frontend/src/components/Home/Home.test.tsx b/frontend/src/components/Home/Home.test.tsx index 4f55cce73f..9938bf9c7e 100644 --- a/frontend/src/components/Home/Home.test.tsx +++ b/frontend/src/components/Home/Home.test.tsx @@ -3,7 +3,7 @@ * Licensed under the MIT license. */ -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { FluentProvider, webLightTheme } from "@fluentui/react-components"; import Home from "./Home"; @@ -142,6 +142,96 @@ describe("Home", () => { expect(screen.getByTestId("home-operation-op_beta")).toHaveTextContent(/1 attack/); }); + it("updates last-activity for newer attacks and formats hour/day/older timestamps", async () => { + const now = Date.now(); + const HOUR = 60 * 60 * 1000; + const DAY = 24 * HOUR; + mockListAttacks.mockResolvedValue({ + items: [ + // Oldest first, so a later (newer) attack in the same operation updates + // the group's last-activity — exercising the "newer than current" branch. + makeAttack({ + attack_result_id: "ar-old", + labels: { operator: "alice", operation: "op_time" }, + last_message_preview: "older than a week", + updated_at: new Date(now - 10 * DAY).toISOString(), + }), + makeAttack({ + attack_result_id: "ar-hours", + labels: { operator: "alice", operation: "op_time" }, + last_message_preview: "a few hours ago", + updated_at: new Date(now - 3 * HOUR).toISOString(), + }), + makeAttack({ + attack_result_id: "ar-days", + labels: { operator: "alice", operation: "op_time" }, + last_message_preview: "a few days ago", + updated_at: new Date(now - 3 * DAY).toISOString(), + }), + ], + pagination: { has_more: false, next_cursor: null }, + }); + + render(); + + const card = await screen.findByTestId("home-operation-op_time"); + // All three attacks render, exercising the hour/day/older formatting paths. + expect(within(card).getByText("a few hours ago")).toBeInTheDocument(); + expect(within(card).getByText("a few days ago")).toBeInTheDocument(); + expect(within(card).getByText("older than a week")).toBeInTheDocument(); + // The newer (3h-ago) attack wins as the group's most recent activity. + expect(within(card).getAllByText(/3h ago/).length).toBeGreaterThan(0); + expect(within(card).getAllByText(/3d ago/).length).toBeGreaterThan(0); + }); + + it("caps visible attacks and falls back for missing preview, outcome, and timestamp", async () => { + const now = Date.now(); + mockListAttacks.mockResolvedValue({ + items: [ + makeAttack({ + attack_result_id: "f1", + labels: { operator: "alice", operation: "op_full" }, + outcome: "success", + last_message_preview: "first preview", + updated_at: new Date(now - 60_000).toISOString(), + }), + makeAttack({ + attack_result_id: "f2", + labels: { operator: "alice", operation: "op_full" }, + outcome: null, // unknown outcome -> default icon via the ?? 'undetermined' branch + last_message_preview: null, // missing preview -> falls back to attack_type + updated_at: new Date(now - 120_000).toISOString(), + }), + makeAttack({ + attack_result_id: "f3", + labels: { operator: "alice", operation: "op_full" }, + // Outcome not present in the icon map -> exercises the icon fallback branch. + outcome: "mystery" as unknown as AttackSummary["outcome"], + last_message_preview: "third preview", + updated_at: "not-a-date", // invalid -> empty relative time (NaN guard) + }), + makeAttack({ + attack_result_id: "f4", + labels: { operator: "alice", operation: "op_full" }, + last_message_preview: "fourth preview", + updated_at: new Date(now - 240_000).toISOString(), + }), + ], + pagination: { has_more: false, next_cursor: null }, + }); + + render(); + + const card = await screen.findByTestId("home-operation-op_full"); + // Only the first three attacks render; the fourth is summarized as overflow. + expect(within(card).getByText("first preview")).toBeInTheDocument(); + expect(within(card).getByText("third preview")).toBeInTheDocument(); + expect(within(card).queryByText("fourth preview")).not.toBeInTheDocument(); + expect(within(card).getByText(/\+1 more in history/)).toBeInTheDocument(); + // f2 has no preview, so its row shows the attack type instead. + expect(within(card).getByText("TestAttack")).toBeInTheDocument(); + }); + it("groups attacks with no operation label under '(no operation)'", async () => { mockListAttacks.mockResolvedValue({ items: [