From 5663d57d5f675ed5e099bbc3eba41fc3c43c357d Mon Sep 17 00:00:00 2001 From: Tigran Babloyan Date: Fri, 31 Jul 2026 15:43:44 +0400 Subject: [PATCH] fix(test): match active tab panel by ARIA role, not AntD class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit antd 6.5 bumps @rc-component/tabs 1.9 -> 1.11, which renames the Tabs content DOM: the active pane goes from `ant-tabs-tabpane-active` to `ant-tabs-content-active` (and the holder/list from `-content-holder`/ `-content` to `-body-holder`/`-body`). 13 test call sites hard-coded the old class to scope assertions to the visible pane, breaking 3 frontend unit tests and 6 e2e specs on #662. Scope by `role="tabpanel"` instead — both rc-tabs versions emit it, with inactive panes aria-hidden, so Playwright's and RTL's role queries resolve to the visible pane on either version. Adds an `activeTabPanel(scope)` e2e helper so the next rename is a one-line change. Verified: full frontend suite (1303 tests) plus the 6 affected e2e specs (20 tests) green on both antd 6.4.3 and 6.5.2. --- e2e/helpers/ui.ts | 13 +++++++++++++ e2e/tests/access-requests-connector.spec.ts | 6 +++--- e2e/tests/admin-oauth2-config.spec.ts | 15 ++++++++------- e2e/tests/api-connector-masking.spec.ts | 3 ++- e2e/tests/api-connector-variables.spec.ts | 3 ++- e2e/tests/api-governance.spec.ts | 3 ++- e2e/tests/request-groups.spec.ts | 3 ++- .../apigov/ApiConnectorSettingsPage.test.tsx | 4 +++- 8 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 e2e/helpers/ui.ts diff --git a/e2e/helpers/ui.ts b/e2e/helpers/ui.ts new file mode 100644 index 00000000..33fbbc49 --- /dev/null +++ b/e2e/helpers/ui.ts @@ -0,0 +1,13 @@ +import type { Locator, Page } from '@playwright/test'; + +/** + * The active AntD tab panel within `scope`. + * + * AntD keeps inactive panes mounted (`aria-hidden` + `display: none`), so a bare page-level query + * can match a hidden duplicate. Matching by ARIA role rather than AntD's internal class survives + * rc-tabs DOM renames (`ant-tabs-tabpane-active` → `ant-tabs-content-active` in antd 6.5); + * Playwright's role engine excludes aria-hidden elements, so this resolves to the visible pane only. + */ +export function activeTabPanel(scope: Page | Locator): Locator { + return scope.getByRole('tabpanel'); +} diff --git a/e2e/tests/access-requests-connector.spec.ts b/e2e/tests/access-requests-connector.spec.ts index cb917393..83957df3 100644 --- a/e2e/tests/access-requests-connector.spec.ts +++ b/e2e/tests/access-requests-connector.spec.ts @@ -16,6 +16,7 @@ import { uploadApiSchemaViaApi, type CreatedApiConnector, } from '../helpers/apiConnectors'; +import { activeTabPanel } from '../helpers/ui'; // AF-567 — "Request access" for API connectors. End-to-end: an analyst requests // time-boxed access to an API connector (scoped to one operation) → an admin @@ -120,8 +121,7 @@ test.describe.serial('access requests for API connectors (AF-567)', () => { // 3. The grant materialised as a time-boxed row on the connector's permissions tab. await page.goto(`/api-connectors/${connector!.id}/settings`); await page.getByRole('tab', { name: 'Permissions' }).click(); - const permissionRow = page - .locator('.ant-tabs-tabpane-active') + const permissionRow = activeTabPanel(page) .getByRole('row') .filter({ hasText: requesterEmail }); await expect(permissionRow).toBeVisible({ timeout: 15_000 }); @@ -157,7 +157,7 @@ test.describe.serial('access requests for API connectors (AF-567)', () => { await page.goto(`/api-connectors/${connector!.id}/settings`); await page.getByRole('tab', { name: 'Permissions' }).click(); await expect( - page.locator('.ant-tabs-tabpane-active').getByRole('row').filter({ hasText: requesterEmail }), + activeTabPanel(page).getByRole('row').filter({ hasText: requesterEmail }), ).toHaveCount(0, { timeout: 15_000 }); }); }); diff --git a/e2e/tests/admin-oauth2-config.spec.ts b/e2e/tests/admin-oauth2-config.spec.ts index c8e81a8a..2772b21d 100644 --- a/e2e/tests/admin-oauth2-config.spec.ts +++ b/e2e/tests/admin-oauth2-config.spec.ts @@ -6,6 +6,7 @@ import { type Page, } from '@playwright/test'; import { loginViaApi } from '../helpers/datasources'; +import { activeTabPanel } from '../helpers/ui'; const ADMIN_EMAIL = 'e2e@accessflow.test'; const ADMIN_PASSWORD = 'E2ePassword!123'; @@ -132,7 +133,7 @@ test.describe.serial('/admin/oauth2 — config CRUD (no provider roundtrip)', () // Google tab is the initial active tab — the Active switch should reflect // the unseeded "disabled" default, and both credentials fields should be // empty. - const activeTab = page.locator('.ant-tabs-tabpane-active'); + const activeTab = activeTabPanel(page); await expect(activeTab.getByRole('switch')).not.toBeChecked(); await expect(activeTab.getByLabel('Client ID')).toHaveValue(''); await expect(activeTab.getByLabel('Client secret')).toHaveValue(''); @@ -149,7 +150,7 @@ test.describe.serial('/admin/oauth2 — config CRUD (no provider roundtrip)', () await page.goto('/admin/oauth2'); await waitForOAuth2ConfigLoaded(page); - const activeTab = page.locator('.ant-tabs-tabpane-active'); + const activeTab = activeTabPanel(page); await activeTab.getByLabel('Client ID').fill(GOOGLE_CLIENT_ID); await activeTab.getByLabel('Client secret').fill(GOOGLE_CLIENT_SECRET); await activeTab.getByRole('switch').click(); @@ -191,7 +192,7 @@ test.describe.serial('/admin/oauth2 — config CRUD (no provider roundtrip)', () await page.goto('/admin/oauth2'); await waitForOAuth2ConfigLoaded(page); - const activeTab = page.locator('.ant-tabs-tabpane-active'); + const activeTab = activeTabPanel(page); await expect(activeTab.getByLabel('Client ID')).toHaveValue(GOOGLE_CLIENT_ID); await expect(activeTab.getByLabel('Client secret')).toHaveValue('********'); await expect(activeTab.getByRole('switch')).toBeChecked(); @@ -236,7 +237,7 @@ test.describe.serial('/admin/oauth2 — config CRUD (no provider roundtrip)', () await page.goto('/admin/oauth2'); await waitForOAuth2ConfigLoaded(page); - const activeTab = page.locator('.ant-tabs-tabpane-active'); + const activeTab = activeTabPanel(page); await activeTab.getByRole('button', { name: 'Copy redirect URI' }).click(); await expect( @@ -257,7 +258,7 @@ test.describe.serial('/admin/oauth2 — config CRUD (no provider roundtrip)', () await page.goto('/admin/oauth2'); await waitForOAuth2ConfigLoaded(page); - const activeTab = page.locator('.ant-tabs-tabpane-active'); + const activeTab = activeTabPanel(page); // The previous tests left active=true. await expect(activeTab.getByRole('switch')).toBeChecked(); await activeTab.getByRole('switch').click(); @@ -300,7 +301,7 @@ test.describe.serial('/admin/oauth2 — config CRUD (no provider roundtrip)', () await page.getByRole('tab', { name: 'GitHub Enterprise' }).click(); - const activeTab = page.locator('.ant-tabs-tabpane-active'); + const activeTab = activeTabPanel(page); await expect(activeTab.getByLabel('Server base URL')).toBeVisible(); await activeTab.getByLabel('Client ID').fill('ghe-id'); await activeTab.getByLabel('Client secret').fill('ghe-secret'); @@ -356,7 +357,7 @@ test.describe.serial('/admin/oauth2 — config CRUD (no provider roundtrip)', () await page.goto('/admin/oauth2'); await waitForOAuth2ConfigLoaded(page); - const activeTab = page.locator('.ant-tabs-tabpane-active'); + const activeTab = activeTabPanel(page); await expect(activeTab.getByLabel('Client ID')).toHaveValue(''); await expect(activeTab.getByLabel('Client secret')).toHaveValue(''); diff --git a/e2e/tests/api-connector-masking.spec.ts b/e2e/tests/api-connector-masking.spec.ts index 2228645c..9c8394af 100644 --- a/e2e/tests/api-connector-masking.spec.ts +++ b/e2e/tests/api-connector-masking.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Page } from '@playwright/test'; +import { activeTabPanel } from '../helpers/ui'; // AF-518: API connector masking & classification — an admin creates an API connector, configures a // response-masking policy on the Masking tab, and a data-classification tag on the Classification @@ -42,7 +43,7 @@ test('admin configures connector masking policy and classification tag', async ( // Ant Design keeps prior tab panels mounted (display:none), and tagging a field auto-derives a // masking policy that repeats the field text — so a bare getByText('user.ssn') would match hidden // occurrences in inactive panels. Scope every content assertion to the active (visible) tab panel. - const activePanel = page.locator('.ant-tabs-tabpane-active'); + const activePanel = activeTabPanel(page); // Masking tab → add a JSON-path masking policy. await page.getByRole('tab', { name: 'Masking' }).click(); diff --git a/e2e/tests/api-connector-variables.spec.ts b/e2e/tests/api-connector-variables.spec.ts index 0db76fd3..191a0f30 100644 --- a/e2e/tests/api-connector-variables.spec.ts +++ b/e2e/tests/api-connector-variables.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Page } from '@playwright/test'; +import { activeTabPanel } from '../helpers/ui'; // AF-613: dynamic variables for API connectors — an admin creates a connector, declares an // HMAC signing variable and an overridable nonce on the Variables tab, reorders them, and deletes @@ -41,7 +42,7 @@ test('admin configures connector dynamic variables', async ({ page }) => { // Ant Design keeps inactive tab panels mounted (display:none), so scope content assertions to the // active panel or a bare getByText can match a hidden duplicate. - const activePanel = page.locator('.ant-tabs-tabpane-active'); + const activePanel = activeTabPanel(page); await page.getByRole('tab', { name: 'Variables' }).click(); await expect(activePanel.getByText(/No variables yet/i)).toBeVisible(); diff --git a/e2e/tests/api-governance.spec.ts b/e2e/tests/api-governance.spec.ts index 16a4e8af..fcbac9e6 100644 --- a/e2e/tests/api-governance.spec.ts +++ b/e2e/tests/api-governance.spec.ts @@ -8,6 +8,7 @@ import { waitForInviteToken, } from '../helpers/datasources'; import { createApiConnectorViaApi } from '../helpers/apiConnectors'; +import { activeTabPanel } from '../helpers/ui'; // AF-500: API Access Governance — admin creates an API connector via the UI, uploads an OpenAPI // schema, and sees the parsed operation catalog. Seeded admin comes from the bootstrap module. @@ -127,7 +128,7 @@ test('admin imports a Postman collection as a connector schema (#612)', async ({ await page.getByRole('tab', { name: 'Schema' }).click(); // Pick the Postman schema type; the caveat banner must appear before upload. - const panel = page.locator('.ant-tabs-tabpane-active'); + const panel = activeTabPanel(page); await panel.getByRole('combobox', { name: 'Schema type' }).click(); await page.getByTitle('Postman Collection').click(); await expect(page.getByText(/inferred from the saved example bodies/)).toBeVisible(); diff --git a/e2e/tests/request-groups.spec.ts b/e2e/tests/request-groups.spec.ts index 58c583dc..f849c7e6 100644 --- a/e2e/tests/request-groups.spec.ts +++ b/e2e/tests/request-groups.spec.ts @@ -19,6 +19,7 @@ import { deleteApiConnectorViaApi, type CreatedApiConnector, } from '../helpers/apiConnectors'; +import { activeTabPanel } from '../helpers/ui'; // AF-501: Request chaining & grouping. An analyst bundles two ordered database-query steps into one // request group, submits it as a single element, an independent reviewer approves the bundle, and it @@ -307,7 +308,7 @@ test.describe('request groups (AF-501)', () => { await drawer.getByLabel('Path').fill('/api/v1/echo'); // AntD keeps inactive tab panes mounted (hidden), so scope every interaction to the active pane. - const activePane = drawer.locator('.ant-tabs-tabpane-active'); + const activePane = activeTabPanel(drawer); await activePane.getByRole('button', { name: 'Add' }).click(); await activePane.getByLabel('Key').fill('dryRun'); await activePane.getByLabel('Value').fill('true'); diff --git a/frontend/src/pages/apigov/ApiConnectorSettingsPage.test.tsx b/frontend/src/pages/apigov/ApiConnectorSettingsPage.test.tsx index aaec9641..7b2950cc 100644 --- a/frontend/src/pages/apigov/ApiConnectorSettingsPage.test.tsx +++ b/frontend/src/pages/apigov/ApiConnectorSettingsPage.test.tsx @@ -338,7 +338,9 @@ describe('ApiConnectorSettingsPage — Postman collection import (AF-612)', () = async function openSchemaTabWithType(label: string) { render(wrap()); fireEvent.click(await screen.findByRole('tab', { name: 'Schema' })); - const panel = document.querySelector('.ant-tabs-tabpane-active') as HTMLElement; + // AntD keeps inactive panes mounted (aria-hidden), so scope to the active one. Matching by + // role rather than AntD's internal class survives rc-tabs DOM renames. + const panel = await screen.findByRole('tabpanel'); fireEvent.mouseDown(within(panel).getByRole('combobox', { name: 'Schema type' })); fireEvent.click(await screen.findByTitle(label)); return panel;