Skip to content
Merged
2 changes: 0 additions & 2 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
"ignore": [
"src/api/**",
"src/components/ui/**",
"src/config/announcements.ts",
"src/config/notices.ts",
"src/config/preSubmitHooks.ts",
"src/components/shared/BetaFeatureWrapper/BetaFeatureWrapper.tsx"
],
Expand Down
3 changes: 2 additions & 1 deletion react-compiler.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const REACT_COMPILER_ENABLED_DIRS = [
"src/hooks/useExecutionArtifacts.ts",
"src/hooks/useContainerLog.ts",
"src/hooks/usePipelineRunList.ts",
"src/hooks/useNotices.ts",
"src/components/shared/FavoriteToggle.tsx",
"src/components/shared/FloatingSelectionBar.tsx",
"src/components/shared/ComponentLifecycleBadges.tsx",
Expand Down Expand Up @@ -78,7 +79,7 @@ export const REACT_COMPILER_ENABLED_DIRS = [
"src/components/shared/Dialogs/PipelineNameDialog.tsx",
"src/components/shared/SecretsManagement/components/SecretsBackendUnavailable.tsx",
"src/components/shared/HighlightText.tsx",
"src/components/shared/AnnouncementBanners.tsx",
"src/components/shared/Notices",
"src/components/shared/Markdown",
"src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection",
"src/components/ui/typography.tsx",
Expand Down
64 changes: 0 additions & 64 deletions src/components/shared/AnnouncementBanners.tsx

This file was deleted.

263 changes: 263 additions & 0 deletions src/components/shared/Notices/NoticeBanners.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { installRawSource, installSource } from "@/config/noticeTestSource";
import { resetNoticeStateForTests } from "@/hooks/useNotices";

import { NoticeBanners } from "./NoticeBanners";

describe("<NoticeBanners />", () => {
beforeEach(() => {
localStorage.clear();
});

afterEach(() => {
cleanup();
resetNoticeStateForTests();
delete window.__TANGLE_NOTICE_SOURCE__;
});

it("renders no DOM node when no source is installed", () => {
const { container } = render(<NoticeBanners />);

expect(container).toBeEmptyDOMElement();
expect(screen.queryByTestId("notice-banners")).not.toBeInTheDocument();
});

it("renders no DOM node when the source declares an unsupported version", () => {
installRawSource({
version: 2,
getSnapshot: () => [{ id: "a", title: "Later contract", body: "" }],
subscribe: () => () => {},
});

const { container } = render(<NoticeBanners />);

expect(container).toBeEmptyDOMElement();
});

it("renders a source that rebuilds its array on every read", () => {
const notices = [{ id: "a", title: "Scheduled maintenance", body: "" }];
installRawSource({
version: 1,
getSnapshot: () => notices.map((notice) => ({ ...notice })),
subscribe: () => () => {},
});

render(<NoticeBanners />);

expect(screen.getByText("Scheduled maintenance")).toBeInTheDocument();
});

it("shows every notice as a banner rather than capping the list", () => {
installSource(
["a", "b", "c", "d", "e", "f"].map((id) => ({
id,
title: `Notice ${id}`,
body: "",
variant: "error" as const,
})),
);

render(<NoticeBanners />);

expect(screen.getAllByTestId("info-box-title")).toHaveLength(6);
expect(screen.getByText("Notice f")).toBeInTheDocument();
});

it("promotes the cards in severity order", () => {
installSource([
{ id: "a", title: "Nice to know", body: "", variant: "info" },
{ id: "b", title: "Worked", body: "", variant: "success" },
{ id: "c", title: "Everything is broken", body: "", variant: "error" },
{ id: "d", title: "Heads up", body: "", variant: "warning" },
]);

render(<NoticeBanners />);

expect(
screen.getAllByTestId("info-box-title").map((el) => el.textContent),
).toEqual(["Everything is broken", "Heads up", "Worked", "Nice to know"]);
});

it("gives each notice a column of the page grid rather than the full width", () => {
installSource([
{ id: "a", title: "One", body: "", variant: "error" },
{ id: "b", title: "Two", body: "", variant: "warning" },
]);

const { container } = render(<NoticeBanners />);

const className = screen.getByTestId("notice-banners").className;
expect(className).toContain("grid-cols-1");
expect(className).toContain("md:grid-cols-2");
expect(className).toContain("lg:grid-cols-3");
expect(container.querySelector(".overflow-x-auto")).toBeNull();
expect(screen.getByTestId("info-box-error").className).toContain("w-full");
});

it("leaves opening the full list to the header", () => {
installSource([
{ id: "a", title: "Only notice", body: "", dismissible: true },
]);

render(<NoticeBanners />);

expect(screen.getAllByRole("button")).toHaveLength(1);
expect(screen.getByLabelText("Dismiss")).toBeInTheDocument();
});

it("renders a brief body inline as Markdown", () => {
installSource([
{
id: "a",
title: "Scheduled maintenance",
body: "Submissions paused **09:00-11:00 UTC**.",
},
]);

render(<NoticeBanners />);

expect(screen.getByText("09:00-11:00 UTC").tagName).toBe("STRONG");
});

it("renders a long body inline rather than truncating it", () => {
installSource([
{
id: "a",
title: "Release notes",
body: `Line one\n\n${"detail ".repeat(40)}`,
},
]);

render(<NoticeBanners />);

expect(screen.getByText("Line one")).toBeInTheDocument();
});

it("scrolls a long body within the card, leaving the action in place", () => {
installSource([
{
id: "a",
title: "Release notes",
body: `Line one\n\n${"detail ".repeat(40)}`,
action: { url: "https://example.com", text: "Read more" },
},
]);

render(<NoticeBanners />);

const body = screen.getByTestId("notice-body");
const action = screen.getByRole("link");

expect(body).toHaveClass("overflow-y-auto");
expect(body).not.toContainElement(action);
expect(screen.getByTestId("notice-banners")).not.toHaveClass(
"overflow-y-auto",
);
});

it("does not render raw HTML embedded in the body", () => {
installSource([
{
id: "a",
title: "Notice",
body: "<script>window.__noticeXss = true;</script><b>bold</b>",
},
]);

const { container } = render(<NoticeBanners />);

expect(container.querySelector("script")).toBeNull();
expect(document.querySelector("script")).toBeNull();
expect(screen.queryByText("bold")).not.toBeInTheDocument();
expect(
(window as unknown as Record<string, unknown>).__noticeXss,
).toBeUndefined();
});

it("renders an action link with an accessible name that includes the title", () => {
installSource([
{
id: "a",
title: "Scheduled maintenance",
body: "",
action: { url: "https://example.com/notes", text: "Read the notes" },
},
]);

render(<NoticeBanners />);

const link = screen.getByRole("link", {
name: "Read the notes: Scheduled maintenance",
});

expect(link).toHaveAttribute("href", "https://example.com/notes");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});

it("offers to dismiss only the notices the host allows", () => {
installSource([
{ id: "a", title: "Scheduled maintenance", body: "", dismissible: true },
{ id: "b", title: "Mandatory notice", body: "" },
]);

render(<NoticeBanners />);

expect(screen.getAllByLabelText("Dismiss")).toHaveLength(1);
});

it("takes one notice off the banners while leaving the rest in place", () => {
installSource([
{
id: "a",
title: "First",
body: "",
variant: "error",
dismissible: true,
},
{ id: "b", title: "Second", body: "", variant: "warning" },
]);

render(<NoticeBanners />);
fireEvent.click(screen.getByLabelText("Dismiss"));

expect(
screen.getAllByTestId("info-box-title").map((el) => el.textContent),
).toEqual(["Second"]);
});

it("keeps a dismissed notice off the banners across a remount", () => {
installSource([
{ id: "a", title: "Scheduled maintenance", body: "", dismissible: true },
]);

render(<NoticeBanners />);
fireEvent.click(screen.getByLabelText("Dismiss"));

cleanup();
const { container } = render(<NoticeBanners />);

expect(container).toBeEmptyDOMElement();
expect(localStorage.getItem("dismissed-notices") ?? "").toContain("a");
});

it("picks up a source installed after mount", () => {
render(<NoticeBanners />);

expect(screen.queryByTestId("notice-banners")).not.toBeInTheDocument();

act(() => {
installSource([{ id: "a", title: "Scheduled maintenance", body: "" }]);
});

expect(screen.getByText("Scheduled maintenance")).toBeInTheDocument();
});
});
24 changes: 24 additions & 0 deletions src/components/shared/Notices/NoticeBanners.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NoticeCard } from "@/components/shared/Notices/NoticeCard";
import { useNotices } from "@/hooks/useNotices";

export const NoticeBanners = () => {
const { notices, dismiss } = useNotices();

if (notices.length === 0) return null;

return (
<div
className="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-start"
data-testid="notice-banners"
>
{notices.map((notice) => (
<NoticeCard
key={notice.id}
notice={notice}
clampBody
onDismiss={notice.dismissible ? () => dismiss(notice) : undefined}
/>
))}
</div>
);
};
Loading
Loading