fix contract status button not visible in persona having contract tab (#23237)

* fix contract status button not visible in persona having contract tab

* supported playwright test for the same

* fix the unit test failing
This commit is contained in:
Ashish Gupta 2025-09-05 10:21:13 +05:30 committed by GitHub
parent 1f4186b661
commit 999af800b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 396 additions and 10 deletions

View File

@ -54,7 +54,6 @@ const test = base.extend<{ page: Page }>({
test.describe('Data Contracts', () => {
const table = new TableClass();
const table2 = new TableClass();
const testClassification = new ClassificationClass();
const testTag = new TagClass({
classification: testClassification.data.name,
@ -68,7 +67,6 @@ test.describe('Data Contracts', () => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await table.create(apiContext);
await table2.create(apiContext);
await testClassification.create(apiContext);
await testTag.create(apiContext);
await testGlossary.create(apiContext);
@ -111,7 +109,6 @@ test.describe('Data Contracts', () => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await table.delete(apiContext);
await table2.delete(apiContext);
await testClassification.delete(apiContext);
await testTag.delete(apiContext);
await testGlossary.delete(apiContext);
@ -541,7 +538,7 @@ test.describe('Data Contracts', () => {
});
});
test('Contract Status badge should not be visible if Contract Tab is hidden by Person', async ({
test('Contract Status badge should be visible on condition if Contract Tab is present/hidden by Persona', async ({
page,
}) => {
test.slow(true);
@ -549,7 +546,7 @@ test.describe('Data Contracts', () => {
await test.step(
'Create Data Contract in Table and validate it fails',
async () => {
await table2.visitEntityPage(page);
await table.visitEntityPage(page);
// Open contract section and start adding contract
await page.click('[data-testid="contract"]');
@ -679,6 +676,35 @@ test.describe('Data Contracts', () => {
await personaResponse;
});
await test.step(
'Verify Contract tab and status badge are visible if persona is set',
async () => {
await redirectToHomePage(page);
await table.visitEntityPage(page);
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
// Verify Contract tab is not visible (should be hidden by persona customization)
await expect(page.getByTestId('contract')).toBeVisible();
// Verify Contract status badge is not visible in header
await expect(
page.getByTestId('data-contract-latest-result-btn')
).toBeVisible();
// Additional verification: Check that other tabs are still visible
await expect(page.getByTestId('schema')).toBeVisible();
await expect(page.getByTestId('activity_feed')).toBeVisible();
await expect(page.getByTestId('sample_data')).toBeVisible();
await expect(page.getByTestId('table_queries')).toBeVisible();
await expect(page.getByTestId('profiler')).toBeVisible();
await expect(page.getByTestId('lineage')).toBeVisible();
await expect(page.getByTestId('custom_properties')).toBeVisible();
}
);
await test.step('Customize Table page to hide Contract tab', async () => {
await settingClick(page, GlobalSettingOptions.PERSONA);
await page.waitForLoadState('networkidle');

View File

@ -429,14 +429,14 @@ export const DataAssetsHeader = ({
]);
const dataContractLatestResultButton = useMemo(() => {
const entityContainContractTab =
isUndefined(customizedPage?.tabs) ??
const entityContainContractTabVisible =
isUndefined(customizedPage?.tabs) ||
Boolean(
customizedPage?.tabs?.find((item) => item.id === EntityTabs.CONTRACT)
);
if (
entityContainContractTab &&
entityContainContractTabVisible &&
dataContract?.latestResult?.status &&
[
ContractExecutionStatus.Aborted,

View File

@ -12,21 +12,26 @@
*/
import { act, fireEvent, render, screen } from '@testing-library/react';
import { AUTO_PILOT_APP_NAME } from '../../../constants/Applications.constant';
import { EntityType } from '../../../enums/entity.enum';
import { EntityTabs, EntityType } from '../../../enums/entity.enum';
import { ServiceCategory } from '../../../enums/service.enum';
import {
Container,
StorageServiceType,
} from '../../../generated/entity/data/container';
import { ContractExecutionStatus } from '../../../generated/entity/data/dataContract';
import { DatabaseServiceType } from '../../../generated/entity/services/databaseService';
import { LabelType, State, TagSource } from '../../../generated/tests/testCase';
import { AssetCertification } from '../../../generated/type/assetCertification';
import { useCustomPages } from '../../../hooks/useCustomPages';
import { MOCK_DATA_CONTRACT } from '../../../mocks/DataContract.mock';
import { MOCK_TIER_DATA } from '../../../mocks/TableData.mock';
import { triggerOnDemandApp } from '../../../rest/applicationAPI';
import { getDataQualityLineage } from '../../../rest/lineageAPI';
import { getContainerByName } from '../../../rest/storageAPI';
import { ExtraInfoLink } from '../../../utils/DataAssetsHeader.utils';
import { getDataContractStatusIcon } from '../../../utils/DataContract/DataContractUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
import { useRequiredParams } from '../../../utils/useRequiredParams';
import { DataAssetsHeader } from './DataAssetsHeader.component';
import { DataAssetsHeaderProps } from './DataAssetsHeader.interface';
@ -58,9 +63,11 @@ const mockProps: DataAssetsHeaderProps = {
onOwnerUpdate: jest.fn(),
};
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useNavigate: jest.fn().mockImplementation(() => mockNavigate),
}));
jest.mock('../../../utils/useRequiredParams', () => ({
@ -179,6 +186,18 @@ jest.mock('../../../rest/lineageAPI', () => ({
getDataQualityLineage: jest.fn(),
}));
jest.mock('../../../utils/DataContract/DataContractUtils', () => ({
getDataContractStatusIcon: jest.fn(),
}));
jest.mock('../../../hooks/useCustomPages', () => ({
useCustomPages: jest.fn().mockReturnValue({ customizedPage: null }),
}));
jest.mock('../../../utils/RouterUtils', () => ({
getEntityDetailsPath: jest.fn(),
}));
describe('ExtraInfoLink component', () => {
const mockProps = {
label: 'myLabel',
@ -475,4 +494,220 @@ describe('DataAssetsHeader component', () => {
expect(button).toBeEnabled();
});
describe('dataContractLatestResultButton', () => {
const mockGetDataContractStatusIcon =
getDataContractStatusIcon as jest.Mock;
const mockUseCustomPages = useCustomPages as jest.Mock;
const mockGetEntityDetailsPath = getEntityDetailsPath as jest.Mock;
it('should render data contract button when contract tab is visible and status is in allowed list', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: {
tabs: [{ id: EntityTabs.CONTRACT }],
},
});
render(
<DataAssetsHeader {...mockProps} dataContract={MOCK_DATA_CONTRACT} />
);
const button = screen.getByTestId('data-contract-latest-result-btn');
expect(button).toBeInTheDocument();
expect(button).toHaveClass('data-contract-latest-result-button');
expect(button).toHaveClass('failed');
});
it('should render data contract button when customizedPage tabs is undefined', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: {
tabs: undefined,
},
});
const mockDataContract = MOCK_DATA_CONTRACT;
render(
<DataAssetsHeader {...mockProps} dataContract={mockDataContract} />
);
expect(
screen.getByTestId('data-contract-latest-result-btn')
).toBeInTheDocument();
});
it('should navigate to contract tab when button is clicked', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: {
tabs: [{ id: EntityTabs.CONTRACT }],
},
});
const mockDataContract = {
...MOCK_DATA_CONTRACT,
latestResult: {
status: ContractExecutionStatus.Running,
},
};
render(
<DataAssetsHeader {...mockProps} dataContract={mockDataContract} />
);
const button = screen.getByTestId('data-contract-latest-result-btn');
fireEvent.click(button);
expect(mockGetEntityDetailsPath).toHaveBeenCalledWith(
EntityType.CONTAINER,
'fullyQualifiedName',
EntityTabs.CONTRACT
);
expect(mockNavigate).toHaveBeenCalled();
});
it('should not render data contract button when contract tab is not visible', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: {
tabs: [{ id: EntityTabs.ACTIVITY_FEED }],
},
});
render(
<DataAssetsHeader {...mockProps} dataContract={MOCK_DATA_CONTRACT} />
);
expect(
screen.queryByTestId('data-contract-latest-result-btn')
).not.toBeInTheDocument();
});
it('should not render data contract button when status is not in allowed list', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: {
tabs: [{ id: EntityTabs.CONTRACT }],
},
});
const mockDataContract = {
...MOCK_DATA_CONTRACT,
latestResult: {
status: ContractExecutionStatus.Success,
},
};
render(
<DataAssetsHeader {...mockProps} dataContract={mockDataContract} />
);
expect(
screen.queryByTestId('data-contract-latest-result-btn')
).not.toBeInTheDocument();
});
it('should not render data contract button when dataContract is undefined', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: {
tabs: [{ id: EntityTabs.CONTRACT }],
},
});
render(<DataAssetsHeader {...mockProps} dataContract={undefined} />);
expect(
screen.queryByTestId('data-contract-latest-result-btn')
).not.toBeInTheDocument();
});
it('should not render data contract button when latestResult is undefined', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: {
tabs: [{ id: EntityTabs.CONTRACT }],
},
});
const mockDataContract = {
...MOCK_DATA_CONTRACT,
latestResult: undefined,
};
render(
<DataAssetsHeader {...mockProps} dataContract={mockDataContract} />
);
expect(
screen.queryByTestId('data-contract-latest-result-btn')
).not.toBeInTheDocument();
});
it('should render button with correct class names for each status', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: { tabs: [{ id: EntityTabs.CONTRACT }] },
});
const statuses = [
ContractExecutionStatus.Failed,
ContractExecutionStatus.Aborted,
ContractExecutionStatus.Running,
];
statuses.forEach((status) => {
const { unmount } = render(
<DataAssetsHeader
{...mockProps}
dataContract={{ ...MOCK_DATA_CONTRACT, latestResult: { status } }}
/>
);
const button = screen.getByTestId('data-contract-latest-result-btn');
expect(button).toHaveClass(`data-contract-latest-result-button`);
expect(button).toHaveClass(status.toLowerCase());
unmount();
});
});
it('should render button with icon when getDataContractStatusIcon returns an icon', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: { tabs: [{ id: EntityTabs.CONTRACT }] },
});
mockGetDataContractStatusIcon.mockReturnValue('TestIcon');
render(
<DataAssetsHeader
{...mockProps}
dataContract={{
...MOCK_DATA_CONTRACT,
latestResult: { status: ContractExecutionStatus.Failed },
}}
/>
);
const button = screen.getByTestId('data-contract-latest-result-btn');
expect(button.querySelector('.anticon')).toBeInTheDocument();
});
it('should render button without icon when getDataContractStatusIcon returns null', () => {
mockUseCustomPages.mockReturnValue({
customizedPage: { tabs: [{ id: EntityTabs.CONTRACT }] },
});
mockGetDataContractStatusIcon.mockReturnValue(null);
render(
<DataAssetsHeader
{...mockProps}
dataContract={{
...MOCK_DATA_CONTRACT,
latestResult: { status: ContractExecutionStatus.Failed },
}}
/>
);
const button = screen.getByTestId('data-contract-latest-result-btn');
expect(button.querySelector('.anticon')).not.toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,125 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Constraint,
DataType,
LabelType,
State,
} from '../generated/entity/data/table';
import { ContractExecutionStatus } from '../generated/type/contractExecutionStatus';
import { TagSource } from '../generated/type/tagLabel';
export const MOCK_DATA_CONTRACT = {
id: 'bf7b3a0d-6a85-4dd6-95e3-243a1769d1a9',
name: 'Customer 360',
fullyQualifiedName:
'redshift prod.dev.dbt_jaffle.customers.dataContract_Customer 360',
description: '<strong>Customer 360 Data Contract</strong> ',
version: 0.2,
updatedAt: 1755860378527,
updatedBy: 'joseph',
status: 'Active',
entity: {
id: 'ee9d44a0-815d-4ac9-8422-4f9d02ddf04d',
type: 'table',
href: 'https://demo.getcollate.io/v1/tables/ee9d44a0-815d-4ac9-8422-4f9d02ddf04d',
},
testSuite: {
id: '24859b7c-a2ef-4e0e-b3b7-67a61ed14bc9',
type: 'testSuite',
fullyQualifiedName: 'bf7b3a0d-6a85-4dd6-95e3-243a1769d1a9',
},
schema: [
{
name: 'customer_id',
dataType: DataType.Array,
dataLength: 1,
dataTypeDisplay: 'integer',
description: 'Unique identifier for each customer',
fullyQualifiedName: 'redshift prod.dev.dbt_jaffle.customers.customer_id',
tags: [
{
tagFQN: 'PII.NonSensitive',
name: 'NonSensitive',
displayName: 'Non Sensitive ',
description:
'PII which is easily accessible from public sources and can include zip code, race, gender, and date of birth.',
style: {},
source: TagSource.Classification,
labelType: LabelType.Automated,
state: State.Suggested,
},
],
constraint: Constraint.Null,
children: [],
},
],
semantics: [
{
name: 'Tiering Elements',
description: '',
// eslint-disable-next-line max-len
rule: '{"and":[{"some":[{"var":"owners"},{"==":[{"var":"fullyQualifiedName"},"customer team"]}]},{"some":[{"var":"dataProduct"},{"==":[{"var":"fullyQualifiedName"},"C360"]}]},{"==":[{"var":"tier.tagFQN"},"Tier.Tier1"]}]}',
enabled: true,
ignoredEntities: [],
},
],
qualityExpectations: [
{
id: 'f496f0d9-58c3-4a0e-a836-2479b457c68e',
type: 'testCase',
name: 'CLV Must be Positive',
description:
'<p>The customer lifetime value must always be greater or equal to zero </p>',
},
{
id: 'e57ffd73-b8f1-4f5f-91b1-7ebe614dc26e',
type: 'testCase',
name: 'Customer ID To Be Unique',
},
{
id: '484e016e-ed87-4f6c-ae77-7d5b16f07ad0',
type: 'testCase',
name: 'Table Row Count To Equal',
},
],
reviewers: [],
changeDescription: {
fieldsAdded: [
{
name: 'latestResult',
newValue: {
status: ContractExecutionStatus.Failed,
resultId: '04551202-c2b9-4d7b-aecf-6c9c8fe22c1c',
timestamp: 1756944000095,
},
},
],
fieldsUpdated: [],
fieldsDeleted: [],
previousVersion: 0.1,
changeSummary: {},
},
incrementalChangeDescription: {
fieldsAdded: [],
fieldsUpdated: [],
fieldsDeleted: [],
previousVersion: 0.2,
},
deleted: false,
latestResult: {
timestamp: 1756944000095,
status: ContractExecutionStatus.Failed,
resultId: '04551202-c2b9-4d7b-aecf-6c9c8fe22c1c',
},
};