Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2249,6 +2249,88 @@
});
}
});

test('Contract created from the form defaults to Draft', async ({ page }) => {
const { apiContext } = await getApiContext(page);
const table = new TableClass();
await table.create(apiContext);

try {
await redirectToHomePage(page);
await table.visitEntityPage(page);
await navigateToContractTab(page);
await clickAddContractButton(page);

await expect(page.getByTestId('contract-entity-status')).toContainText(
'Draft'
);

await page.getByTestId('contract-name').fill('draft_default_contract');

const createResponse = page.waitForResponse(
(response) =>
response.url().endsWith('/api/v1/dataContracts') &&
response.request().method() === 'POST'
);
await page.getByTestId('save-contract-btn').click();
const created = await createResponse;

expect(created.ok()).toBe(true);
expect((await created.json()).entityStatus).toBe('Draft');

const statusCard = page.getByTestId('contract-status-card');
const statusBadge = statusCard.getByText('Draft', { exact: true });

await expect(statusBadge).toBeVisible();
// A Draft must not be painted with the green success palette.
await expect(statusBadge).toHaveClass(/utility-warning/);
await expect(statusBadge).not.toHaveClass(/utility-success/);
} finally {
await table.delete(apiContext);
}
});

test('Contract is created with the status picked in the form', async ({
page,
}) => {
const { apiContext } = await getApiContext(page);
const table = new TableClass();
await table.create(apiContext);

try {
await redirectToHomePage(page);
await table.visitEntityPage(page);
await navigateToContractTab(page);
await clickAddContractButton(page);

await page.getByTestId('contract-name').fill('approved_choice_contract');

await selectOption(
page,
page.getByTestId('contract-entity-status'),
'Approved'
);

const createResponse = page.waitForResponse(
(response) =>
response.url().endsWith('/api/v1/dataContracts') &&
response.request().method() === 'POST'
);
await page.getByTestId('save-contract-btn').click();
const created = await createResponse;

expect(created.ok()).toBe(true);
expect((await created.json()).entityStatus).toBe('Approved');

const statusCard = page.getByTestId('contract-status-card');
const statusBadge = statusCard.getByText('Approved', { exact: true });

await expect(statusBadge).toBeVisible();
await expect(statusBadge).toHaveClass(/utility-success/);
} finally {
await table.delete(apiContext);
}
});
});

entitiesWithDataContracts.forEach((EntityClass) => {
Expand All @@ -2258,7 +2340,7 @@

const testPersona = base.extend<{ page: Page }>({
page: async ({ browser }, use) => {
const adminPage = await browser.newPage();

Check warning on line 2343 in openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts

View workflow job for this annotation

GitHub Actions / checkstyle

Prefer the `page` fixture (test.use({ storageState })) over browser.newPage() + manual login for single-user admin tests. For multi-user tests that need a second non-admin page, this warning is expected — no action needed
await adminUser.login(adminPage);
await use(adminPage);
await adminPage.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ jest.mock('../ContractDetailFormTab/ContractDetailFormTab', () => ({
<button onClick={() => onChange({ name: 'Test Contract Change' })}>
Change
</button>
<button
data-testid="entity-status-change-btn"
onClick={() => onChange({ entityStatus: 'In Review' })}>
Change Status
</button>
<button onClick={onNext}>Next</button>
</div>
)),
Expand Down Expand Up @@ -442,7 +447,7 @@ describe('AddDataContract', () => {
type: EntityType.TABLE,
},
semantics: undefined, // validSemantics - undefined when no semantics provided
entityStatus: EntityStatus.Approved,
entityStatus: EntityStatus.Draft,
})
);
expect(showSuccessToast).toHaveBeenCalledWith(
Expand Down Expand Up @@ -474,7 +479,7 @@ describe('AddDataContract', () => {
type: EntityType.TABLE,
},
semantics: undefined, // validSemantics - undefined when no semantics provided
entityStatus: EntityStatus.Approved,
entityStatus: EntityStatus.Draft,
})
);
expect(showSuccessToast).toHaveBeenCalledWith(
Expand All @@ -483,6 +488,43 @@ describe('AddDataContract', () => {
expect(mockOnSave).toHaveBeenCalled();
});

it('should send the status picked in the form instead of forcing Approved', async () => {
render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />);

await act(async () => {
fireEvent.click(screen.getByText('Change'));
});

await act(async () => {
fireEvent.click(screen.getByTestId('entity-status-change-btn'));
});

await act(async () => {
fireEvent.click(screen.getByTestId('save-contract-btn'));
});

expect((createContract as jest.Mock).mock.calls[0][0]).toEqual(
expect.objectContaining({ entityStatus: EntityStatus.InReview })
);
});

it('should default a newly created contract to Draft, never Approved', async () => {
render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />);

await act(async () => {
fireEvent.click(screen.getByText('Change'));
});

await act(async () => {
fireEvent.click(screen.getByTestId('save-contract-btn'));
});

const payload = (createContract as jest.Mock).mock.calls[0][0];

expect(payload.entityStatus).toBe(EntityStatus.Draft);
expect(payload.entityStatus).not.toBe(EntityStatus.Approved);
});

it('should call updateContract for existing contract with JSON patch', async () => {
render(
<AddDataContract
Expand Down Expand Up @@ -514,6 +556,59 @@ describe('AddDataContract', () => {
expect(mockOnSave).toHaveBeenCalled();
});

it('should not patch entityStatus when editing without touching the status', async () => {
render(
<AddDataContract
contract={mockContract}
onCancel={mockOnCancel}
onSave={mockOnSave}
/>
);

// Change an unrelated field; mockContract is already Approved, so a
// spurious /entityStatus op would silently rewrite an existing status.
await act(async () => {
fireEvent.click(screen.getByText('Change'));
});

await act(async () => {
fireEvent.click(screen.getByTestId('save-contract-btn'));
});

expect((updateContract as jest.Mock).mock.calls[0][1]).toEqual(
expect.not.arrayContaining([
expect.objectContaining({ path: '/entityStatus' }),
])
);
});

it('should patch entityStatus when the author changes the status', async () => {
render(
<AddDataContract
contract={mockContract}
onCancel={mockOnCancel}
onSave={mockOnSave}
/>
);

await act(async () => {
fireEvent.click(screen.getByTestId('entity-status-change-btn'));
});

await act(async () => {
fireEvent.click(screen.getByTestId('save-contract-btn'));
});

expect((updateContract as jest.Mock).mock.calls[0][1]).toEqual(
expect.arrayContaining([
expect.objectContaining({
path: '/entityStatus',
value: EntityStatus.InReview,
}),
])
);
});

it('should handle save errors gracefully', async () => {
const mockError = new Error('Save failed');
(createContract as jest.Mock).mockRejectedValue(mockError);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
import { ReactComponent as SLAIcon } from '../../../assets/svg/timeout.svg';
import {
DataContractMode,
DEFAULT_DATA_CONTRACT_STATUS,
EDataContractTab,
} from '../../../constants/DataContract.constants';
import { CSMode } from '../../../enums/codemirror.enum';
import { EntityType } from '../../../enums/entity.enum';
import {
DataContract,
EntityStatus,
TermsOfUse,
} from '../../../generated/entity/data/dataContract';
import { Table } from '../../../generated/entity/data/table';
Expand Down Expand Up @@ -87,7 +87,7 @@
// Inherited fields should not be shown in the edit form
// IMPORTANT: We must completely REMOVE inherited fields from the object (not set to undefined)
// so that fast-json-patch generates /add operations instead of /replace when adding new values
const filteredContract = useMemo(() => {

Check warning on line 90 in openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

{"message":"Function has a complexity of 13 which is greater than 10 authorized.","cost":3,"secondaryLocations":[{"line":90,"column":38,"endLine":90,"endColumn":40,"message":"+1"},{"line":91,"column":4,"endLine":91,"endColumn":6,"message":"+1"},{"line":118,"column":4,"endLine":118,"endColumn":6,"message":"+1"},{"line":121,"column":11,"endLine":121,"endColumn":13,"message":"+1"},{"line":122,"column":46,"endLine":122,"endColumn":48,"message":"+1"},{"line":127,"column":11,"endLine":127,"endColumn":13,"message":"+1"},{"line":154,"column":4,"endLine":154,"endColumn":6,"message":"+1"},{"line":154,"column":26,"endLine":154,"endColumn":28,"message":"+1"},{"line":159,"column":4,"endLine":159,"endColumn":6,"message":"+1"},{"line":164,"column":4,"endLine":164,"endColumn":6,"message":"+1"},{"line":164,"column":29,"endLine":164,"endColumn":31,"message":"+1"},{"line":169,"column":4,"endLine":169,"endColumn":6,"message":"+1"},{"line":169,"column":24,"endLine":169,"endColumn":26,"message":"+1"}]}
if (!contract) {
return undefined;
}
Expand Down Expand Up @@ -264,7 +264,7 @@
semantics: validSemantics,
security: validSecurity,
termsOfUse: termsOfUseContent,
entityStatus: EntityStatus.Approved,
entityStatus: formValues.entityStatus ?? DEFAULT_DATA_CONTRACT_STATUS,
});
}

Expand All @@ -275,7 +275,7 @@
} finally {
setIsSubmitting(false);
}
}, [

Check warning on line 278 in openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

React Hook useCallback has missing dependencies: 'onSave' and 't'. Either include them or remove the dependency array. If 'onSave' changes too often, find the parent component that defines it and wrap that definition in useCallback
contract,
filteredContract,
formValues,
Expand Down Expand Up @@ -490,7 +490,7 @@
];

return tabs.filter((tab) => entityContractTabs.includes(Number(tab.key)));
}, [

Check warning on line 493 in openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array
entityContractTabs,
filteredContract,
onFormChange,
Expand Down Expand Up @@ -533,7 +533,7 @@
</div>
</div>
);
}, [

Check warning on line 536 in openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array
mode,
isSubmitting,
isSaveDisabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.
*/

/*
* ContractDetailFormTab.test.tsx stubs `generateFormFields`, so it can only
* inspect the field descriptors. These cases render the real Ant Design control
* to assert what the author actually sees and can pick.
*/
import '@testing-library/jest-dom';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { DATA_CONTRACT_AUTHORING_STATUS_OPTIONS } from '../../../constants/DataContract.constants';
import { EntityStatus } from '../../../generated/entity/data/dataContract';
import { ContractDetailFormTab } from './ContractDetailFormTab';

jest.mock('../../../hooks/useEntityRules', () => ({
useEntityRules: jest.fn().mockImplementation(() => ({
entityRules: {
canAddMultipleUserOwners: true,
canAddMultipleTeamOwner: true,
},
})),
}));

const commonProps = {
buttonProps: { isNextVisible: true },
onChange: jest.fn(),
onNext: jest.fn(),
};

const getStatusSelect = () => screen.getByTestId('contract-entity-status');

const openStatusDropdown = async () => {
await act(async () => {
fireEvent.mouseDown(
getStatusSelect().querySelector('.ant-select-selector') as Element
);
});
};

describe('ContractDetailFormTab entity status control', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should show Draft as the pre-selected status when creating a contract', () => {
render(<ContractDetailFormTab {...commonProps} />);

expect(getStatusSelect()).toHaveTextContent('label.draft');
expect(getStatusSelect()).not.toHaveTextContent('label.approved');
});

it('should show the contract status when editing an existing contract', () => {
render(
<ContractDetailFormTab
initialValues={{ entityStatus: EntityStatus.Approved }}
{...commonProps}
/>
);

expect(getStatusSelect()).toHaveTextContent('label.approved');
});

it('should offer only Draft, In Review and Approved as authoring statuses', async () => {
render(<ContractDetailFormTab {...commonProps} />);

await openStatusDropdown();

const options = document.querySelectorAll('.ant-select-item-option');

// Compare against the authoring list itself rather than literal i18n keys,
// so renaming a key cannot fail this for a non-behavioural reason. Which
// statuses that list may contain is asserted in ContractDetailFormTab.test.tsx.
expect(Array.from(options).map((option) => option.textContent)).toEqual(
DATA_CONTRACT_AUTHORING_STATUS_OPTIONS.map(({ labelKey }) => labelKey)
);
});

it('should report the picked status to the parent form', async () => {
render(<ContractDetailFormTab {...commonProps} />);

await openStatusDropdown();

await act(async () => {
fireEvent.click(screen.getByText('label.in-review'));
});

expect(commonProps.onChange).toHaveBeenCalledWith(
{ entityStatus: EntityStatus.InReview },
expect.anything()
);
});
});
Loading
Loading