**Related issue:** Resolves #44325 ## Summary In GitOps mode, disabled settings fields show a "Manage in YAML (GitOps mode)" tooltip via `GitOpsModeTooltipWrapper`. The wrapper passed the entire field (label + input + help text) to the shared `TooltipWrapper`, so react-tooltip anchored to the whole field's bounding box. For `position="left"`/`"right"` fields this centered the tooltip vertically across the field, landing the arrow between the input and the help text instead of at the label. This change anchors the tooltip to the field's label/control row (via react-tooltip-5 `anchorSelect` scoped to a unique wrapper id), so the arrow points at the label regardless of input, help-text, or tooltip-content length. The change is contained to `GitOpsModeTooltipWrapper`, leaving `TooltipWrapper` untouched. Non-field usages (buttons, icon rows) fall back to the previous whole-element anchoring. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually Verified manually with Playwright against a live instance in GitOps mode: the tooltip arrow points at the checkbox/control row (incl. fields with help text) and at the label for `isInputField` inputs; button usages are unchanged. <img width="1726" height="1041" alt="Screenshot 2026-06-18 at 15 31 08" src="https://github.com/user-attachments/assets/58229743-58d4-4b26-b69d-c78ca2e6b3b1" /> <img width="1721" height="1040" alt="Screenshot 2026-06-18 at 15 30 58" src="https://github.com/user-attachments/assets/253db973-2a57-4f62-9d6f-2d18f0c2a7ca" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Corrected GitOps mode tooltip positioning for disabled settings fields. Tooltips now properly target field labels instead of the combined label/input/help element area, improving visual alignment and clarity. * **Tests** * Enhanced test coverage for GitOps mode tooltip anchoring with tests for single form fields and grouped field controls to ensure correct positioning behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
304 lines
8.3 KiB
TypeScript
304 lines
8.3 KiB
TypeScript
import React from "react";
|
|
|
|
import { screen, waitFor } from "@testing-library/react";
|
|
import { createCustomRenderer } from "test/test-utils";
|
|
|
|
import Button from "components/buttons/Button";
|
|
import Checkbox from "components/forms/fields/Checkbox";
|
|
|
|
import GitOpsModeTooltipWrapper from "./GitOpsModeTooltipWrapper";
|
|
|
|
describe("GitOpsModeTooltipWrapper", () => {
|
|
it("renders clickable children without a tooltip when GOM is not enabled", async () => {
|
|
const render = createCustomRenderer({
|
|
context: {
|
|
app: {
|
|
isGlobalAdmin: true,
|
|
isTeamAdmin: false,
|
|
// thanks, DeepPartial!
|
|
config: {
|
|
gitops: {
|
|
gitops_mode_enabled: false,
|
|
repository_url: "",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const onSave = jest.fn();
|
|
|
|
const { user } = render(
|
|
<GitOpsModeTooltipWrapper
|
|
renderChildren={(disableChildren) => (
|
|
<Button disabled={disableChildren} onClick={onSave}>
|
|
Save
|
|
</Button>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
const btn = screen.getByText("Save");
|
|
expect(btn).toBeInTheDocument();
|
|
|
|
await user.hover(btn);
|
|
expect(screen.queryByRole("tooltip")).toBeNull();
|
|
|
|
await user.click(btn);
|
|
expect(onSave).toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders non-clickable children with the tooltip when GOM is enabled", async () => {
|
|
const render = createCustomRenderer({
|
|
context: {
|
|
app: {
|
|
isGlobalAdmin: true,
|
|
isTeamAdmin: false,
|
|
// thanks, DeepPartial!
|
|
config: {
|
|
gitops: {
|
|
gitops_mode_enabled: true,
|
|
repository_url: "a.b.cc",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const onSave = jest.fn();
|
|
|
|
const { user } = render(
|
|
<GitOpsModeTooltipWrapper
|
|
renderChildren={(disableChildren) => (
|
|
<Button disabled={disableChildren} onClick={onSave}>
|
|
Save
|
|
</Button>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
const btn = screen.getByText("Save");
|
|
expect(btn).toBeInTheDocument();
|
|
|
|
await user.hover(btn);
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
});
|
|
|
|
await user.click(btn);
|
|
expect(onSave).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders clickable children when GOM is enabled but entity type is excepted", async () => {
|
|
const render = createCustomRenderer({
|
|
context: {
|
|
app: {
|
|
isGlobalAdmin: true,
|
|
isTeamAdmin: false,
|
|
config: {
|
|
gitops: {
|
|
gitops_mode_enabled: true,
|
|
repository_url: "a.b.cc",
|
|
exceptions: { labels: true, software: false, secrets: false },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const onSave = jest.fn();
|
|
|
|
const { user } = render(
|
|
<GitOpsModeTooltipWrapper
|
|
entityType="labels"
|
|
renderChildren={(disableChildren) => (
|
|
<Button disabled={disableChildren} onClick={onSave}>
|
|
Save
|
|
</Button>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
const btn = screen.getByText("Save");
|
|
expect(btn).toBeInTheDocument();
|
|
|
|
await user.hover(btn);
|
|
expect(screen.queryByRole("tooltip")).toBeNull();
|
|
|
|
await user.click(btn);
|
|
expect(onSave).toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders non-clickable children when GOM is enabled and entity type is not excepted", async () => {
|
|
const render = createCustomRenderer({
|
|
context: {
|
|
app: {
|
|
isGlobalAdmin: true,
|
|
isTeamAdmin: false,
|
|
config: {
|
|
gitops: {
|
|
gitops_mode_enabled: true,
|
|
repository_url: "a.b.cc",
|
|
exceptions: { labels: false, software: true, secrets: false },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const onSave = jest.fn();
|
|
|
|
const { user } = render(
|
|
<GitOpsModeTooltipWrapper
|
|
entityType="labels"
|
|
renderChildren={(disableChildren) => (
|
|
<Button disabled={disableChildren} onClick={onSave}>
|
|
Save
|
|
</Button>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
const btn = screen.getByText("Save");
|
|
expect(btn).toBeInTheDocument();
|
|
|
|
await user.hover(btn);
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
});
|
|
|
|
await user.click(btn);
|
|
expect(onSave).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("disables children when GOM is enabled and no entityType is specified", async () => {
|
|
const render = createCustomRenderer({
|
|
context: {
|
|
app: {
|
|
isGlobalAdmin: true,
|
|
isTeamAdmin: false,
|
|
config: {
|
|
gitops: {
|
|
gitops_mode_enabled: true,
|
|
repository_url: "a.b.cc",
|
|
exceptions: { labels: true, software: true, secrets: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const onSave = jest.fn();
|
|
|
|
const { user } = render(
|
|
<GitOpsModeTooltipWrapper
|
|
renderChildren={(disableChildren) => (
|
|
<Button disabled={disableChildren} onClick={onSave}>
|
|
Save
|
|
</Button>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
const btn = screen.getByText("Save");
|
|
await user.click(btn);
|
|
expect(onSave).not.toHaveBeenCalled();
|
|
});
|
|
|
|
// For a wrapped form field, the tooltip anchors to the label/control row rather than the
|
|
// whole field, so the arrow points at the label instead of the center of
|
|
// label + input + help text (#44325). jsdom has no layout, so the geometric arrow position
|
|
// is verified manually; here we assert the anchor binding and trigger behavior.
|
|
it("anchors the tooltip to the field's control row, not the help text, when GOM is enabled", async () => {
|
|
const render = createCustomRenderer({
|
|
context: {
|
|
app: {
|
|
isGlobalAdmin: true,
|
|
isTeamAdmin: false,
|
|
config: {
|
|
gitops: {
|
|
gitops_mode_enabled: true,
|
|
repository_url: "a.b.cc",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const { user, container } = render(
|
|
<GitOpsModeTooltipWrapper
|
|
position="left"
|
|
renderChildren={(disableChildren) => (
|
|
<Checkbox
|
|
disabled={disableChildren}
|
|
name="deleteActivities"
|
|
value={false}
|
|
helpText="This setting will delete existing results."
|
|
>
|
|
Delete activities
|
|
</Checkbox>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
// The anchor target (the checkbox's control row) and the help text are distinct
|
|
// elements; the help text must never be the anchor.
|
|
const controlRow = container.querySelector(".form-field > label");
|
|
expect(controlRow).not.toBeNull();
|
|
expect(
|
|
container.querySelector(".form-field__help-text")
|
|
).toBeInTheDocument();
|
|
|
|
// Hovering the help text (not an anchor) does not open the tooltip...
|
|
await user.hover(screen.getByText(/will delete existing results/i));
|
|
expect(screen.queryByRole("tooltip")).toBeNull();
|
|
|
|
// ...but hovering the control row does.
|
|
await user.hover(screen.getByText("Delete activities"));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// When the wrapped content is a group (multiple fields/controls) rather than a single
|
|
// field, keep whole-wrapper anchoring so hovering any control still shows the tooltip.
|
|
it("keeps whole-wrapper anchoring for grouped content with multiple controls", async () => {
|
|
const render = createCustomRenderer({
|
|
context: {
|
|
app: {
|
|
isGlobalAdmin: true,
|
|
isTeamAdmin: false,
|
|
config: {
|
|
gitops: {
|
|
gitops_mode_enabled: true,
|
|
repository_url: "a.b.cc",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const { user } = render(
|
|
<GitOpsModeTooltipWrapper
|
|
position="left"
|
|
renderChildren={(disableChildren) => (
|
|
<div>
|
|
<Checkbox disabled={disableChildren} name="first" value={false}>
|
|
First option
|
|
</Checkbox>
|
|
<Checkbox disabled={disableChildren} name="second" value={false}>
|
|
Second option
|
|
</Checkbox>
|
|
</div>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
// Hovering the second control (not the first) still shows the tooltip.
|
|
await user.hover(screen.getByText("Second option"));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|