diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/ContractSemantics.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/ContractSemantics.component.tsx index de9770d692d..62e298d7fde 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/ContractSemantics.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/ContractSemantics.component.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ import Icon from '@ant-design/icons'; -import { Col, Row, Typography } from 'antd'; +import { Typography } from 'antd'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { ReactComponent as FailIcon } from '../../../assets/svg/ic-fail.svg'; @@ -22,6 +22,7 @@ import { DataContractResult } from '../../../generated/entity/datacontract/dataC import { getContractStatusType } from '../../../utils/DataContract/DataContractUtils'; import RichTextEditorPreviewerNew from '../../common/RichTextEditor/RichTextEditorPreviewNew'; import StatusBadgeV2 from '../../common/StatusBadge/StatusBadgeV2.component'; +import './contract-semantics.less'; const ContractSemantics: React.FC<{ semantics: SemanticsRule[]; @@ -47,48 +48,44 @@ const ContractSemantics: React.FC<{ }; return ( - - -
- {semantics.map((item) => ( -
- -
- - {item.name} - - - - -
-
- ))} -
- - - {contractStatus && ( -
- {`${t('label.entity-status', { - entity: t('label.schema'), - })} :`} - +
+ {semantics.map((item) => ( +
+ +
+ + {item.name} + + + + +
- )} - - + ))} +
+ {contractStatus && ( +
+ {`${t('label.entity-status', { + entity: t('label.semantic-plural'), + })} :`} + +
+ )} +
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/ContractSemantics.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/ContractSemantics.test.tsx new file mode 100644 index 00000000000..c4160f74cda --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/ContractSemantics.test.tsx @@ -0,0 +1,335 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { SemanticsRule } from '../../../generated/entity/data/dataContract'; +import { + ContractExecutionStatus, + DataContractResult, +} from '../../../generated/entity/datacontract/dataContractResult'; +import { getContractStatusType } from '../../../utils/DataContract/DataContractUtils'; +import ContractSemantics from './ContractSemantics.component'; + +jest.mock('../../common/RichTextEditor/RichTextEditorPreviewNew', () => { + return function MockRichTextEditorPreviewerNew({ + markdown, + }: { + markdown: string; + }) { + return
{markdown}
; + }; +}); + +jest.mock('../../common/StatusBadge/StatusBadgeV2.component', () => { + return function MockStatusBadgeV2({ + label, + dataTestId, + }: { + label: string; + dataTestId: string; + }) { + return
{label}
; + }; +}); + +jest.mock('../../../utils/DataContract/DataContractUtils', () => ({ + getContractStatusType: jest.fn((status: string) => status), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: Record) => { + const translations: Record = { + 'label.entity-status': `${options?.entity} Status`, + 'label.semantic-plural': 'Semantic', + }; + + return translations[key] || key; + }, + }), +})); + +const mockSemantics: SemanticsRule[] = [ + { + name: 'Rule 1', + description: 'First semantic rule description', + rule: '{"and":[{"==":[{"var":"name"},"test"]}]}', + enabled: true, + }, + { + name: 'Rule 2', + description: 'Second semantic rule description', + rule: '{"and":[{"==":[{"var":"age"},"25"]}]}', + enabled: true, + }, +]; + +const mockLatestContractResults: DataContractResult = { + timestamp: Date.now(), + dataContractFQN: 'contract', + contractExecutionStatus: ContractExecutionStatus.Success, + semanticsValidation: { + passed: 1, + failed: 1, + total: 2, + failedRules: [ + { + ruleName: 'Rule 2', + reason: 'Second semantic rule description', + }, + ], + }, +} as DataContractResult; + +describe('ContractSemantics', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Basic Rendering', () => { + it('should render without crashing', () => { + render(); + + expect(screen.getByText('Rule 1')).toBeInTheDocument(); + expect(screen.getByText('Rule 2')).toBeInTheDocument(); + }); + + it('should render all semantic rules', () => { + render(); + + expect( + screen.getByText('First semantic rule description') + ).toBeInTheDocument(); + expect( + screen.getByText('Second semantic rule description') + ).toBeInTheDocument(); + }); + + it('should render with empty semantics array', () => { + const { container } = render(); + + expect(container.querySelector('.rule-item')).not.toBeInTheDocument(); + }); + }); + + describe('Icon Rendering Based on Execution Results', () => { + it('should render default icons when no latest results are provided', () => { + const { container } = render( + + ); + + const icons = container.querySelectorAll('.rule-icon-default'); + + expect(icons).toHaveLength(2); + }); + + it('should render success icon for passed rules', () => { + render( + + ); + + const icons = document.querySelectorAll('.rule-icon'); + + expect(icons).toHaveLength(2); + }); + + it('should render fail icon for failed rules', () => { + render( + + ); + + const icons = document.querySelectorAll('.rule-icon'); + + expect(icons).toHaveLength(2); + }); + }); + + describe('Contract Status Display', () => { + it('should display contract status when provided', () => { + render( + + ); + + expect(screen.getByText('Semantic Status :')).toBeInTheDocument(); + expect( + screen.getByTestId('contract-status-card-item-semantics-status') + ).toBeInTheDocument(); + expect(screen.getByText('Passed')).toBeInTheDocument(); + }); + + it('should not display contract status when not provided', () => { + render(); + + expect(screen.queryByText('Semantic Status :')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('contract-status-card-item-semantics-status') + ).not.toBeInTheDocument(); + }); + + it('should display different status values correctly', () => { + const { rerender } = render( + + ); + + expect(screen.getByText('Failed')).toBeInTheDocument(); + + rerender( + + ); + + expect(screen.getByText('Success')).toBeInTheDocument(); + }); + }); + + describe('Rich Text Preview Integration', () => { + it('should render RichTextEditorPreviewerNew for each rule description', () => { + render(); + + const richTextPreviews = screen.getAllByTestId('rich-text-preview'); + + expect(richTextPreviews).toHaveLength(2); + expect(richTextPreviews[0]).toHaveTextContent( + 'First semantic rule description' + ); + expect(richTextPreviews[1]).toHaveTextContent( + 'Second semantic rule description' + ); + }); + }); + + describe('Layout and Structure', () => { + it('should render with proper row and column structure', () => { + const { container } = render( + + ); + + expect( + container.querySelector('.contract-semantic-component-container') + ).toBeInTheDocument(); + expect( + container.querySelector('.rule-item-container') + ).toBeInTheDocument(); + }); + + it('should render multiple rule items', () => { + const { container } = render( + + ); + + const ruleItems = container.querySelectorAll('.rule-item'); + + expect(ruleItems).toHaveLength(2); + }); + }); + + describe('Edge Cases', () => { + it('should handle semantics with missing descriptions', () => { + const semanticsWithoutDescription: SemanticsRule[] = [ + { + name: 'Rule Without Description', + description: '', + rule: '{"and":[{"==":[{"var":"name"},"test"]}]}', + enabled: true, + }, + ]; + + render(); + + expect(screen.getByText('Rule Without Description')).toBeInTheDocument(); + }); + + it('should handle latestContractResults without semanticsValidation', () => { + const resultsWithoutSemantics = { + ...mockLatestContractResults, + semanticsValidation: undefined, + }; + + render( + + ); + + expect(screen.getByText('Rule 1')).toBeInTheDocument(); + expect(screen.getByText('Rule 2')).toBeInTheDocument(); + }); + + it('should handle latestContractResults without failedRules', () => { + const resultsWithoutFailedRules: DataContractResult = { + ...mockLatestContractResults, + semanticsValidation: { + passed: 2, + failed: 0, + total: 2, + failedRules: [], + }, + }; + + render( + + ); + + expect(screen.getByText('Rule 1')).toBeInTheDocument(); + expect(screen.getByText('Rule 2')).toBeInTheDocument(); + }); + + it('should handle single semantic rule', () => { + const singleSemantic: SemanticsRule[] = [ + { + name: 'Single Rule', + description: 'Single rule description', + rule: '{"and":[{"==":[{"var":"name"},"test"]}]}', + enabled: true, + }, + ]; + + render(); + + expect(screen.getByText('Single Rule')).toBeInTheDocument(); + expect(screen.getByText('Single rule description')).toBeInTheDocument(); + }); + }); + + describe('Integration with getContractStatusType', () => { + it('should call getContractStatusType with correct status', () => { + render( + + ); + + expect(getContractStatusType).toHaveBeenCalledWith('Passed'); + }); + }); + + describe('Accessibility', () => { + it('should have proper semantic HTML structure', () => { + const { container } = render( + + ); + + expect(container.querySelector('.rule-name')).toBeInTheDocument(); + expect(container.querySelector('.rule-description')).toBeInTheDocument(); + expect( + container.querySelector('.contract-status-container') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/contract-semantics.less b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/contract-semantics.less new file mode 100644 index 00000000000..019f45bbdff --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemantics/contract-semantics.less @@ -0,0 +1,22 @@ +/* + * 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. + */ +.contract-semantic-component-container { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 120px; + + .contract-status-container { + flex: none; + } +}