diff --git a/mcpjam-inspector/client/src/components/evaluate/__tests__/create-suite-page.test.tsx b/mcpjam-inspector/client/src/components/evaluate/__tests__/create-suite-page.test.tsx index 40c9efbf59..5c14768e1d 100644 --- a/mcpjam-inspector/client/src/components/evaluate/__tests__/create-suite-page.test.tsx +++ b/mcpjam-inspector/client/src/components/evaluate/__tests__/create-suite-page.test.tsx @@ -146,6 +146,9 @@ describe("CreateSuitePage", () => { expect(screen.getByTestId("create-suite-page")).toBeTruthy(); expect(screen.queryByRole("dialog")).toBeNull(); + expect(screen.getByTestId("required-legend")).toHaveTextContent( + "Required field", + ); expect( screen.getByRole("heading", { name: "Create a new eval suite" }), ).toBeTruthy(); diff --git a/mcpjam-inspector/client/src/components/evaluate/create-suite-page.tsx b/mcpjam-inspector/client/src/components/evaluate/create-suite-page.tsx index fee0a6fdd3..43d05820f0 100644 --- a/mcpjam-inspector/client/src/components/evaluate/create-suite-page.tsx +++ b/mcpjam-inspector/client/src/components/evaluate/create-suite-page.tsx @@ -40,7 +40,10 @@ import { useComposerResolver } from "@/components/environment-composer/use-compo import { MAX_SUITE_ENVIRONMENTS } from "@/components/project-environments/environment-picker"; import { useProjectEnvironmentsEnabled } from "@/hooks/useProjectEnvironmentsEnabled"; import { useProjectEnvironments } from "@/hooks/useProjectEnvironments"; -import { RequiredMark } from "@/components/shared/required-mark"; +import { + RequiredLegend, + RequiredMark, +} from "@/components/shared/required-mark"; import { toast } from "@/lib/toast"; import type { HostAttachmentDraft } from "../evals/client-attachments-editor"; import { @@ -371,6 +374,7 @@ export function CreateSuitePage({

Set up the environment you will be evaluating.

+
diff --git a/mcpjam-inspector/client/src/components/scenarios/UserTestingScenarioCreateFlow.tsx b/mcpjam-inspector/client/src/components/scenarios/UserTestingScenarioCreateFlow.tsx index 63a290fa93..d0f0699d5c 100644 --- a/mcpjam-inspector/client/src/components/scenarios/UserTestingScenarioCreateFlow.tsx +++ b/mcpjam-inspector/client/src/components/scenarios/UserTestingScenarioCreateFlow.tsx @@ -13,7 +13,10 @@ import { DropdownMenuTrigger, } from "@mcpjam/design-system/dropdown-menu"; import { EnvironmentComposer } from "@/components/environment-composer/environment-composer"; -import { RequiredMark } from "@/components/shared/required-mark"; +import { + RequiredLegend, + RequiredMark, +} from "@/components/shared/required-mark"; import { emptyComposerState, isComposeMode, @@ -345,6 +348,9 @@ export function UserTestingScenarioCreateFlow({ Publish one of your environments, hand them to users, then read what happened in their sessions.

+
+ +
diff --git a/mcpjam-inspector/client/src/components/scenarios/__tests__/UserTestingScenarioCreateFlow.test.tsx b/mcpjam-inspector/client/src/components/scenarios/__tests__/UserTestingScenarioCreateFlow.test.tsx index 6851eb9a61..aa31dcd70a 100644 --- a/mcpjam-inspector/client/src/components/scenarios/__tests__/UserTestingScenarioCreateFlow.test.tsx +++ b/mcpjam-inspector/client/src/components/scenarios/__tests__/UserTestingScenarioCreateFlow.test.tsx @@ -223,6 +223,9 @@ describe("UserTestingScenarioCreateFlow", () => { // Marked on the label too — an asterisk is what a scanning user reads as // "required" before they try Save. expect(screen.getAllByText("(required)").length).toBeGreaterThan(0); + expect(screen.getByTestId("required-legend")).toHaveTextContent( + "Required field", + ); fireEvent.change(screen.getByTestId("user-testing-create-environment"), { target: { value: "env-1" }, diff --git a/mcpjam-inspector/client/src/components/shared/__tests__/required-mark.test.tsx b/mcpjam-inspector/client/src/components/shared/__tests__/required-mark.test.tsx new file mode 100644 index 0000000000..3c853bc94a --- /dev/null +++ b/mcpjam-inspector/client/src/components/shared/__tests__/required-mark.test.tsx @@ -0,0 +1,56 @@ +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { RequiredLegend, RequiredMark } from "../required-mark"; + +describe("RequiredMark", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("hides the glyph from assistive tech and names the field required", () => { + render( + , + ); + + const glyph = screen.getByTestId("required-mark"); + expect(glyph).toHaveTextContent("*"); + expect(glyph).toHaveAttribute("aria-hidden", "true"); + expect(screen.getByText("(required)")).toHaveClass("sr-only"); + }); + + it("explains itself on hover", () => { + render( + , + ); + + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); + + const glyph = screen.getByTestId("required-mark"); + act(() => { + fireEvent.pointerMove(glyph, { pointerType: "mouse" }); + vi.runAllTimers(); + }); + + expect(screen.getByRole("tooltip")).toHaveTextContent("Required"); + }); +}); + +describe("RequiredLegend", () => { + it("spells out what the mark means", () => { + render(); + + expect(screen.getByTestId("required-legend")).toHaveTextContent( + "* Required field", + ); + }); +}); diff --git a/mcpjam-inspector/client/src/components/shared/required-mark.tsx b/mcpjam-inspector/client/src/components/shared/required-mark.tsx index 56be868c8f..efc56c4543 100644 --- a/mcpjam-inspector/client/src/components/shared/required-mark.tsx +++ b/mcpjam-inspector/client/src/components/shared/required-mark.tsx @@ -1,3 +1,9 @@ +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@mcpjam/design-system/tooltip"; + /** * The marker that makes a required field LOOK required, for forms whose Save is * already gated on the field being filled. A gate nobody can see reads as a @@ -6,7 +12,8 @@ * * The glyph is decorative: assistive tech gets the word, and the control itself * still has to carry `aria-required` — this marks the LABEL, it does not - * annotate the input. + * annotate the input. Hovering the glyph names it for everyone else — a bare + * asterisk with no footnote to point at reads as a typo. * * Primary, not destructive: every Production Redesign frame draws the marker in * the brand colour, and red on a field nobody has touched yet reads as an error @@ -15,10 +22,36 @@ export function RequiredMark() { return ( <> - + + + + + + Required + + (required) ); } + +/** + * The legend a form shows once, near the top, so the marks below it need no + * guessing. Only for forms that render at least one {@link RequiredMark}. + */ +export function RequiredLegend() { + return ( +

+ {" "} + Required field +

+ ); +} diff --git a/mcpjam-inspector/client/src/components/swarms/__tests__/SwarmsTab.createFlow.test.tsx b/mcpjam-inspector/client/src/components/swarms/__tests__/SwarmsTab.createFlow.test.tsx index 438caca489..1b053a4589 100644 --- a/mcpjam-inspector/client/src/components/swarms/__tests__/SwarmsTab.createFlow.test.tsx +++ b/mcpjam-inspector/client/src/components/swarms/__tests__/SwarmsTab.createFlow.test.tsx @@ -469,6 +469,9 @@ describe("SwarmsTab — New swarm create flow", () => { openDescribe(); expect(screen.getByTestId("new-swarm-create-flow")).toBeInTheDocument(); + expect(screen.getByTestId("required-legend")).toHaveTextContent( + "Required field", + ); expect( screen.getByRole("heading", { name: /create an agentic swarm/i, diff --git a/mcpjam-inspector/client/src/components/swarms/new-swarm-create-flow.tsx b/mcpjam-inspector/client/src/components/swarms/new-swarm-create-flow.tsx index ca89f42da5..7fe9da70d7 100644 --- a/mcpjam-inspector/client/src/components/swarms/new-swarm-create-flow.tsx +++ b/mcpjam-inspector/client/src/components/swarms/new-swarm-create-flow.tsx @@ -18,7 +18,10 @@ import { Textarea } from "@mcpjam/design-system/textarea"; import { ChevronLeft, Loader2, X } from "lucide-react"; import { PersonaPickerPopover } from "@/components/swarms/persona-picker-popover"; import { ProgressStepper } from "@/components/shared/progress-stepper"; -import { RequiredMark } from "@/components/shared/required-mark"; +import { + RequiredLegend, + RequiredMark, +} from "@/components/shared/required-mark"; import { ErrorBoundary } from "@/components/ui/error-boundary"; import { SwarmTargetComposer } from "@/components/swarms/swarm-target-composer"; import { @@ -1798,6 +1801,7 @@ export function NewSwarmCreateFlow({

Set up your environment and then describe your users.

+