From a1ee108ef84d39040ac8793d81baf346345ce9c6 Mon Sep 17 00:00:00 2001 From: Deuex Solutions Date: Thu, 20 Aug 2026 10:36:14 +0530 Subject: [PATCH] fix(ui): replace native title tooltip with styled Tooltip in Incident Manager and DataQualityTab tables Fixes #31726 - Wrap truncated table-name/test-case-name links in Tooltip/TooltipTrigger instead of relying on the native title attribute, fixing missing keyboard-focus and touch support (WCAG 1.4.13) - Keep Link as the real navigational element, wrapped in react-aria's Focusable to correctly bridge the tooltip's hover/focus context onto it. An earlier version of this fix used TooltipTrigger as the trigger itself (via onPress + navigate), but TooltipTrigger renders a ), - TooltipTrigger: ({ children }: React.PropsWithChildren) => <>{children}, Typography: ({ children, className, @@ -265,6 +284,14 @@ jest.mock('@openmetadata/ui-core-components', () => { }; }); +jest.mock('react-aria-components', () => ({ + ...jest.requireActual('react-aria-components'), + // Focusable is a transparent ref/context-wiring wrapper in real usage; for + // DOM-structure assertions in tests it's equivalent to rendering its child + // directly. + Focusable: ({ children }: React.PropsWithChildren) => <>{children}, +})); + jest.mock('../../../../rest/testAPI', () => ({ removeTestCaseFromTestSuite: jest.fn().mockResolvedValue({}), })); @@ -541,6 +568,45 @@ describe('DataQualityTab test', () => { expect(deleteButton).toBeInTheDocument(); }); + it('Should show a styled Tooltip with the full entity name for the Name cell, not a native title attribute', async () => { + const firstRowData = MOCK_TEST_CASE[0]; + await act(async () => { + render(); + }); + + const nameCellWrapper = await screen.findByTestId(firstRowData.name); + const trigger = within(nameCellWrapper).getByText( + getEntityName(firstRowData) + ); + + // The trigger is a real Link (wrapped in Focusable, not TooltipTrigger), + // so it keeps native link semantics, while still not relying on a + // native title attribute for the full name. + expect(trigger).not.toHaveAttribute('title'); + + const tooltip = within(nameCellWrapper).getByTestId('tooltip'); + + expect(tooltip).toHaveAttribute('title', getEntityName(firstRowData)); + }); + + it('Should link the Name cell trigger to the test case detail page', async () => { + const firstRowData = MOCK_TEST_CASE[0]; + await act(async () => { + render(); + }); + + const nameLinkCall = (Link as unknown as jest.Mock).mock.calls.find( + ([props]) => + props.to?.pathname === + observabilityRouterClassBase.getTestCaseDetailPagePath( + firstRowData.fullyQualifiedName ?? '' + ) + ); + + expect(nameLinkCall).toBeDefined(); + expect(nameLinkCall?.[0].state).toEqual({ breadcrumbData: undefined }); + }); + it('Should keep action dropdowns aligned when dimensions are present', async () => { const dimensionalTestCase: TestCase = { ...MOCK_TEST_CASE[0], diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx index f27d1ba47329..322ab9c5d66f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx @@ -28,6 +28,7 @@ import classNames from 'classnames'; import { isUndefined, sortBy, toLower } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; import type { Selection, SortDescriptor } from 'react-aria-components'; +import { Focusable } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; import { Link, useNavigate } from 'react-router-dom'; import { ReactComponent as DimensionIcon } from '../../../../assets/svg/data-observability/dimension.svg'; @@ -651,18 +652,21 @@ const DataQualityTab: React.FC = ({ data-testid={record.name} onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}> - - {getEntityName(record)} - + + + + {getEntityName(record)} + + + {showTableColumn && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.test.tsx index 177c3991a224..329ff8c13ee4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.test.tsx @@ -14,6 +14,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { Link } from 'react-router-dom'; import { TestCaseResolutionStatus } from '../../generated/tests/testCaseResolutionStatus'; +import { getNameFromFQN } from '../../utils/FqnUtils'; import { NextPreviousProps } from '../common/NextPrevious/NextPrevious.interface'; import { TestCasePermission } from '../Database/Profiler/ProfilerDashboard/profilerDashboard.interface'; import IncidentManagerTable, { @@ -88,9 +89,22 @@ jest.mock('@openmetadata/ui-core-components', () => { .fn() .mockImplementation(() =>
), Table: TableMock, + Tooltip: jest.fn().mockImplementation(({ children, title }) => ( +
+ {children} +
+ )), }; }); +jest.mock('react-aria-components', () => ({ + ...jest.requireActual('react-aria-components'), + // Focusable is a transparent ref/context-wiring wrapper in real usage; for + // DOM-structure assertions in tests it's equivalent to rendering its child + // directly. + Focusable: ({ children }: React.PropsWithChildren) => <>{children}, +})); + jest.mock('../common/NextPrevious/NextPrevious', () => { return jest .fn() @@ -292,12 +306,47 @@ describe('IncidentManagerTable', () => { expect(nameLinkCall?.[0].state).toEqual({ breadcrumbData }); }); - it('should truncate the table link and expose the full name via title', () => { + it('should wrap the truncated table link in a Tooltip showing the table FQN', () => { renderTable(); + const tooltip = screen.getAllByTestId('tooltip')[0]; const tableLink = screen.getAllByTestId('table-link')[0]; - expect(tableLink).toHaveAttribute('title', 'NameFromFQN'); + // The trigger is a real anchor (wrapped in Focusable, not TooltipTrigger), + // so it keeps native link semantics - Ctrl/Cmd+click, middle-click, + // right-click "copy link address", and screen readers reading it as a + // link - while still not relying on a native title attribute. + expect(tableLink.tagName).toBe('A'); + expect(tableLink).not.toHaveAttribute('title'); expect(tableLink).toHaveClass('tw:truncate'); + + // Tooltip shows the table's own FQN (Service.Database.Schema.Table, as + // returned by getPartialNameFromTableFQN), not the test case's FQN - + // the test case reference's fullyQualifiedName has the test case name + // appended and must not leak into the tooltip. + expect(tooltip).toHaveAttribute('title', 'PartialName'); + }); + + it('should fall back to fullyQualifiedName when getNameFromFQN returns an empty string', () => { + (getNameFromFQN as jest.Mock).mockReturnValueOnce(''); + + renderTable(); + + const tableLink = screen.getAllByTestId('table-link')[0]; + + // Should not render blank - falls back to ref.fullyQualifiedName via `||` + expect(tableLink).toHaveTextContent( + mockRecords[0].testCaseReference?.fullyQualifiedName ?? '' + ); + }); + + it('should link the table trigger to the table profiler page', () => { + renderTable(); + + const tableLinkCall = (Link as unknown as jest.Mock).mock.calls.find( + ([props]) => props['data-testid'] === 'table-link' + ); + + expect(tableLinkCall?.[0].to).toEqual('entity-details-path'); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.tsx index 087c0d90b22f..82e33f32f195 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.tsx @@ -15,9 +15,11 @@ import { EmptyPlaceholder, Skeleton, Table, + Tooltip, } from '@openmetadata/ui-core-components'; import { ShieldTick } from '@untitledui/icons'; import { useMemo } from 'react'; +import { Focusable } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { EntityTabs, EntityType, FqnPart } from '../../enums/entity.enum'; @@ -180,19 +182,22 @@ const IncidentManagerTable = ({ {isIncidentPage && ( - e.stopPropagation()}> - {getNameFromFQN(tableFqn) ?? ref?.fullyQualifiedName} - + + + e.stopPropagation()}> + {getNameFromFQN(tableFqn) || ref?.fullyQualifiedName} + + + )}