diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts
index 711638e5798..01d78b4d763 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts
@@ -391,6 +391,10 @@ test.describe('Teams Page', () => {
.getByTestId('manage-button')
.click();
+ await expect(
+ page.getByTestId('manage-dropdown-list-container')
+ ).toBeVisible();
+
await expect(page.locator('button[role="switch"]')).toHaveAttribute(
'aria-checked',
'true'
@@ -398,6 +402,10 @@ test.describe('Teams Page', () => {
await clickOutside(page);
+ await expect(
+ page.getByTestId('manage-dropdown-list-container')
+ ).not.toBeVisible();
+
await hardDeleteTeam(page);
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate.test.tsx
new file mode 100644
index 00000000000..9fcc031815c
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate.test.tsx
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2022 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 {
+ ObjectFieldTemplatePropertyType,
+ ObjectFieldTemplateProps,
+ RJSFSchema,
+} from '@rjsf/utils';
+import { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import serviceUtilClassBase from '../../../../../utils/ServiceUtilClassBase';
+import { ObjectFieldTemplate } from './ObjectFieldTemplate';
+
+jest.mock('../../../../../utils/ServiceUtilClassBase', () => ({
+ __esModule: true,
+ default: {
+ getProperties: jest.fn(),
+ },
+}));
+
+jest.mock('../../../../../constants/Services.constant', () => ({
+ ADVANCED_PROPERTIES: ['connectionArguments', 'connectionOptions'],
+}));
+
+describe('ObjectFieldTemplate', () => {
+ const mockOnAddClick = jest.fn();
+ const mockHandleFocus = jest.fn();
+ const mockServiceUtil = serviceUtilClassBase as jest.Mocked<
+ typeof serviceUtilClassBase
+ >;
+
+ const createMockProperty = (
+ name: string,
+ content: React.ReactElement | null = null
+ ): ObjectFieldTemplatePropertyType => ({
+ name,
+ content: content ||
{name} content
,
+ disabled: false,
+ hidden: false,
+ readonly: false,
+ });
+
+ const defaultProps: ObjectFieldTemplateProps = {
+ title: 'Test Title',
+ description: 'Test Description',
+ properties: [createMockProperty('field1'), createMockProperty('field2')],
+ required: false,
+ disabled: false,
+ readonly: false,
+ idSchema: { $id: 'test-id' },
+ schema: {} as RJSFSchema,
+ uiSchema: {},
+ formData: {},
+ formContext: {},
+ registry: {} as ObjectFieldTemplateProps['registry'],
+ onAddClick: mockOnAddClick,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockServiceUtil.getProperties.mockReturnValue({
+ properties: defaultProps.properties,
+ additionalField: '',
+ additionalFieldContent: null,
+ });
+ });
+
+ describe('Basic Rendering', () => {
+ it('should render title correctly', () => {
+ render();
+
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
+ });
+
+ it('should render all normal properties', () => {
+ render();
+
+ expect(screen.getByText('field1 content')).toBeInTheDocument();
+ expect(screen.getByText('field2 content')).toBeInTheDocument();
+ });
+
+ it('should apply correct CSS classes to property wrappers', () => {
+ const { container } = render();
+
+ const propertyWrappers = container.querySelectorAll('.property-wrapper');
+
+ expect(propertyWrappers).toHaveLength(2);
+ });
+ });
+
+ describe('Advanced Properties', () => {
+ const propsWithAdvanced: ObjectFieldTemplateProps = {
+ ...defaultProps,
+ properties: [
+ createMockProperty('field1'),
+ createMockProperty('connectionArguments'),
+ createMockProperty('connectionOptions'),
+ ],
+ };
+
+ beforeEach(() => {
+ mockServiceUtil.getProperties.mockReturnValue({
+ properties: [createMockProperty('field1')],
+ additionalField: '',
+ additionalFieldContent: null,
+ });
+ });
+
+ it('should render advanced properties in a collapse panel', () => {
+ const { container } = render(
+
+ );
+
+ const collapse = container.querySelector('.ant-collapse');
+
+ expect(collapse).toBeInTheDocument();
+
+ const collapseHeader = screen.getByRole('button', {
+ name: /Test Title label.advanced-config/,
+ });
+
+ expect(collapseHeader).toBeInTheDocument();
+ });
+
+ it('should render advanced properties inside collapse', () => {
+ const { container } = render(
+
+ );
+
+ // The collapse should exist
+ const collapse = container.querySelector('.ant-collapse');
+
+ expect(collapse).toBeInTheDocument();
+
+ // Advanced properties are in the collapse but may not be visible until expanded
+ // Let's check that the collapse panel header exists
+ const collapseHeader = screen.getByRole('button', {
+ name: /Test Title label.advanced-config/,
+ });
+
+ expect(collapseHeader).toBeInTheDocument();
+ });
+
+ it('should not render collapse when no advanced properties', () => {
+ const { container } = render();
+
+ expect(container.querySelector('.ant-collapse')).not.toBeInTheDocument();
+ });
+
+ it('should separate normal and advanced properties correctly', () => {
+ const { container } = render(
+
+ );
+
+ // Check that collapse exists for advanced properties
+ const collapse = container.querySelector('.ant-collapse');
+
+ expect(collapse).toBeInTheDocument();
+
+ // Check that only normal properties are visible (advanced are in collapsed panel)
+ const visibleWrappers = container.querySelectorAll('.property-wrapper');
+
+ expect(visibleWrappers).toHaveLength(1); // Only normal properties visible
+
+ // Verify normal property content is present
+ expect(screen.getByText('field1 content')).toBeInTheDocument();
+
+ // Verify advanced config header is present (but content is collapsed)
+ const collapseHeader = screen.getByRole('button', {
+ name: /Test Title label.advanced-config/,
+ });
+
+ expect(collapseHeader).toBeInTheDocument();
+ });
+
+ it('should remove advanced property fields from DOM when collapse panel is closed', () => {
+ render();
+
+ // Initially, advanced properties should not be in DOM (collapsed by default)
+ expect(
+ screen.queryByText('connectionArguments content')
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('connectionOptions content')
+ ).not.toBeInTheDocument();
+
+ // Open the collapse panel
+ const collapseHeader = screen.getByRole('button', {
+ name: /Test Title label.advanced-config/,
+ });
+ fireEvent.click(collapseHeader);
+
+ // After opening, advanced properties should be in DOM
+ expect(
+ screen.getByText('connectionArguments content')
+ ).toBeInTheDocument();
+ expect(screen.getByText('connectionOptions content')).toBeInTheDocument();
+
+ // Close the collapse panel again
+ fireEvent.click(collapseHeader);
+
+ // After closing, advanced properties should be removed from DOM due to destroyInactivePanel
+ expect(
+ screen.queryByText('connectionArguments content')
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('connectionOptions content')
+ ).not.toBeInTheDocument();
+
+ // Normal properties should remain visible throughout
+ expect(screen.getByText('field1 content')).toBeInTheDocument();
+ });
+ });
+
+ describe('Additional Properties', () => {
+ const propsWithAdditional: ObjectFieldTemplateProps = {
+ ...defaultProps,
+ schema: {
+ additionalProperties: true,
+ } as RJSFSchema,
+ };
+
+ it('should render add button when additionalProperties is true', () => {
+ render();
+
+ const addButton = screen.getByTestId('add-item-Test Title');
+
+ expect(addButton).toBeInTheDocument();
+ });
+
+ it('should not render add button when additionalProperties is false', () => {
+ render();
+
+ expect(
+ screen.queryByTestId('add-item-Test Title')
+ ).not.toBeInTheDocument();
+ });
+
+ it('should call onAddClick when add button is clicked', () => {
+ const mockOnAddClickFn = jest.fn(() => jest.fn());
+ const props = {
+ ...propsWithAdditional,
+ onAddClick: mockOnAddClickFn,
+ };
+
+ render();
+
+ const addButton = screen.getByTestId('add-item-Test Title');
+ fireEvent.click(addButton);
+
+ expect(mockOnAddClickFn).toHaveBeenCalledWith(props.schema);
+ });
+
+ it('should apply additional-fields class when additionalProperties is true', () => {
+ const { container } = render(
+
+ );
+
+ const propertyWrappers = container.querySelectorAll(
+ '.property-wrapper.additional-fields'
+ );
+
+ expect(propertyWrappers.length).toBeGreaterThan(0);
+ });
+
+ it('should call handleFocus when add button is focused', () => {
+ const propsWithFocus = {
+ ...propsWithAdditional,
+ formContext: {
+ handleFocus: mockHandleFocus,
+ },
+ };
+
+ render();
+
+ const addButton = screen.getByTestId('add-item-Test Title');
+ fireEvent.focus(addButton);
+
+ expect(mockHandleFocus).toHaveBeenCalledWith('test-id');
+ });
+
+ it('should not call handleFocus when it is undefined', () => {
+ render();
+
+ const addButton = screen.getByTestId('add-item-Test Title');
+ fireEvent.focus(addButton);
+
+ expect(mockHandleFocus).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Additional Field Rendering', () => {
+ it('should not render AdditionalField when not provided (default behavior)', () => {
+ render();
+
+ // Since additionalField is empty string by default, no additional field should render
+ const { container } = render();
+ const additionalElements = container.querySelectorAll(
+ '[data-additional-field]'
+ );
+
+ expect(additionalElements).toHaveLength(0);
+ });
+
+ it('should render AdditionalField when provided as string', () => {
+ // Mock a component name that would resolve to an actual component
+ mockServiceUtil.getProperties.mockReturnValue({
+ properties: defaultProps.properties,
+ additionalField: 'div',
+ additionalFieldContent: null,
+ });
+
+ const { container } = render();
+ const divElements = container.querySelectorAll('div');
+
+ expect(divElements.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle empty properties array', () => {
+ const emptyProps = {
+ ...defaultProps,
+ properties: [],
+ };
+
+ mockServiceUtil.getProperties.mockReturnValue({
+ properties: [],
+ additionalField: '',
+ additionalFieldContent: null,
+ });
+
+ const { container } = render();
+
+ expect(container.querySelectorAll('.property-wrapper')).toHaveLength(0);
+ });
+
+ it('should handle properties with all advanced fields', () => {
+ const allAdvancedProps = {
+ ...defaultProps,
+ properties: [
+ createMockProperty('connectionArguments'),
+ createMockProperty('connectionOptions'),
+ ],
+ };
+
+ mockServiceUtil.getProperties.mockReturnValue({
+ properties: [],
+ additionalField: '',
+ additionalFieldContent: null,
+ });
+
+ const { container } = render(
+
+ );
+
+ expect(container.querySelector('.ant-collapse')).toBeInTheDocument();
+
+ // Advanced properties are in collapsed panel, so check for the header
+ const collapseHeader = screen.getByRole('button', {
+ name: /Test Title label.advanced-config/,
+ });
+
+ expect(collapseHeader).toBeInTheDocument();
+ });
+
+ it('should handle missing title', () => {
+ const noTitleProps = {
+ ...defaultProps,
+ title: '',
+ };
+
+ const { container } = render();
+
+ const label = container.querySelector('label');
+
+ expect(label).toBeInTheDocument();
+ expect(label?.textContent).toBe('');
+ });
+
+ it('should handle complex nested content in properties', () => {
+ const complexProps = {
+ ...defaultProps,
+ properties: [
+ {
+ ...createMockProperty('complex'),
+ content: (
+
+ ),
+ },
+ ],
+ };
+
+ mockServiceUtil.getProperties.mockReturnValue({
+ properties: complexProps.properties,
+ additionalField: '',
+ additionalFieldContent: null,
+ });
+
+ render();
+
+ expect(screen.getByText('Nested')).toBeInTheDocument();
+ expect(screen.getByText('Content')).toBeInTheDocument();
+ });
+ });
+
+ describe('Translation', () => {
+ it('should use translation for advanced config label', () => {
+ const propsWithAdvanced = {
+ ...defaultProps,
+ properties: [
+ createMockProperty('field1'),
+ createMockProperty('connectionArguments'),
+ ],
+ };
+
+ mockServiceUtil.getProperties.mockReturnValue({
+ properties: [createMockProperty('field1')],
+ additionalField: '',
+ additionalFieldContent: null,
+ });
+
+ render();
+
+ const collapseHeader = screen.getByRole('button', {
+ name: /label\.advanced-config/,
+ });
+
+ expect(collapseHeader).toBeInTheDocument();
+ });
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate.tsx
index 614aa13ba4c..3705582ccd2 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate.tsx
@@ -118,6 +118,7 @@ export const ObjectFieldTemplate: FunctionComponent =
))}
{!isEmpty(advancedProperties) && (