Skip to content

Commit 66690ee

Browse files
romanlutzCopilot
andauthored
TEST: fix flaky CreateTargetDialog accessibility test (#2335)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 48ec2fc7-3fd7-4188-a3ff-df3cc2a64790
1 parent ff1fa6b commit 66690ee

1 file changed

Lines changed: 29 additions & 3 deletions

File tree

frontend/src/components/Config/CreateTargetDialog.test.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({
8888
// Fluent's Dropdown renders its listbox in a portal guarded by a focus
8989
// modalizer. Under jsdom the popover only toggles via a direct click event,
9090
// and the `aria-hidden` the modalizer puts on the dialog while the listbox is
91-
// open is never restored once it closes, which would hide the rest of the form
92-
// from role-based queries. Both behaviors are jsdom artifacts — real pointer
93-
// and keyboard interaction is covered by e2e/config.spec.ts.
91+
// open is not reliably restored once it closes, which would hide the rest of
92+
// the form from role-based queries. Both behaviors are jsdom artifacts — real
93+
// pointer and keyboard interaction is covered by e2e/config.spec.ts.
9494
async function openTargetTypePicker(): Promise<HTMLElement> {
9595
const picker = screen.getByRole("combobox", { name: /target type/i });
9696
await waitFor(() => {
@@ -101,9 +101,13 @@ async function openTargetTypePicker(): Promise<HTMLElement> {
101101
return picker;
102102
}
103103

104+
// Drops the leftover `aria-hidden` from the dialog and everything above it.
105+
// It is a no-op while a listbox is open, so the modalizer keeps its real
106+
// behavior and only the missing restore is compensated for.
104107
function restoreDialogAccessibility(): void {
105108
const dialog = document.querySelector('[role="dialog"]');
106109
if (!dialog) return;
110+
if (document.querySelector('[role="listbox"]')) return;
107111
const hiddenAncestors = document.querySelectorAll('[aria-hidden="true"]');
108112
for (const element of Array.from(hiddenAncestors)) {
109113
if (element === dialog || element.contains(dialog)) {
@@ -112,6 +116,21 @@ function restoreDialogAccessibility(): void {
112116
}
113117
}
114118

119+
// The modalizer can re-apply `aria-hidden` well after the listbox closed —
120+
// on an unrelated focus change, for instance — so a one-shot cleanup leaves
121+
// every later `*ByRole` query racing against it. Re-run the cleanup whenever
122+
// the attribute reappears instead.
123+
function watchDialogAccessibility(): MutationObserver {
124+
const observer = new MutationObserver(restoreDialogAccessibility);
125+
observer.observe(document.documentElement, {
126+
subtree: true,
127+
childList: true,
128+
attributes: true,
129+
attributeFilter: ["aria-hidden"],
130+
});
131+
return observer;
132+
}
133+
115134
async function selectTargetType(value: string): Promise<void> {
116135
await openTargetTypePicker();
117136
fireEvent.click(
@@ -198,7 +217,10 @@ describe("CreateTargetDialog", () => {
198217
onCreated: jest.fn(),
199218
};
200219

220+
let dialogAccessibilityObserver: MutationObserver;
221+
201222
beforeEach(() => {
223+
dialogAccessibilityObserver = watchDialogAccessibility();
202224
jest.clearAllMocks();
203225
mockedTargetsApi.listTargetCatalog.mockResolvedValue(TARGET_CATALOG);
204226
mockedTargetsApi.listTargets.mockResolvedValue({
@@ -207,6 +229,10 @@ describe("CreateTargetDialog", () => {
207229
} as unknown as Awaited<ReturnType<typeof mockedTargetsApi.listTargets>>);
208230
});
209231

232+
afterEach(() => {
233+
dialogAccessibilityObserver.disconnect();
234+
});
235+
210236
it("should render dialog when open", () => {
211237
render(
212238
<TestWrapper>

0 commit comments

Comments
 (0)