feat: Added documentation panel for test case form (#23085)

* feat: add TestCaseForm documentation and enhance TestCaseForm component layout

* Refactor TestCaseForm and related components for improved structure and styling

- Updated IDs in TestCaseForm for consistency
- Enhanced styling in TestCaseFormV1 for better layout
- Added onClick handler to SelectionCardGroup for improved interactivity
- Improved ServiceDocPanel to handle markdown loading state

* Enhance TestCaseForm and related components with detailed test definitions and improved styling

* Enhance ServiceDocPanel with EntitySummaryPanel integration and add styling for embedded documentation

* Refactor TestCaseForm and EditTestCaseModalV1 components for improved field handling and integrate ServiceDocPanel for enhanced documentation display

* Add display name section to TestCaseForm and enhance EditTestCaseModalV1 styling

* Enhance EditTestCaseModalV1 and TestCaseFormV1 tests with ServiceDocPanel integration and improved field focus handling

* Enhance ServiceDocPanel tests with improved rendering, markdown fetching, and active field highlighting

* Refactor test case selectors in DataQuality and TestCases specs for improved stability and visibility checks

* fixed failing test

* Fix selectors in DataQualityAndProfiler tests for consistency

* fixed test case
This commit is contained in:
Shailesh Parmar 2025-09-04 14:02:54 +05:30 committed by GitHub
parent ebc67a9f97
commit 7961a583f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2202 additions and 640 deletions

View File

@ -19,17 +19,23 @@ import { test } from '../../fixtures/pages';
test.describe('Add TestCase New Flow', () => {
// Helper function to select table
const selectTable = async (page: Page, table: TableClass) => {
await page.click('#testCaseFormV1_selectedTable');
await page.click('[id="root\\/table"]');
const tableResponse = page.waitForResponse(
'/api/v1/search/query?*index=table_search_index*'
);
await page.fill('#testCaseFormV1_selectedTable', table.entity.name);
await page.fill('[id="root\\/table"]', table.entity.name);
await tableResponse;
await page
.locator(
`.ant-select-dropdown [title="${table.entityResponseData.fullyQualifiedName}"]`
)
.click();
await page.waitForSelector(`[data-id="selected-entity"]`, {
state: 'visible',
});
await expect(page.locator('[data-id="selected-entity"]')).toBeVisible();
};
// Helper function to create test case
@ -47,11 +53,26 @@ test.describe('Add TestCase New Flow', () => {
paramsValue,
expectSchedulerCard = true,
} = data;
await page.getByTestId('test-case-name').click();
await page.waitForSelector(`[data-id="name"]`, { state: 'visible' });
await expect(page.locator('[data-id="name"]')).toBeVisible();
await page.getByTestId('test-case-name').fill(`${testTypeId}_test_case`);
await page.click('#testCaseFormV1_testTypeId');
await page.fill('#testCaseFormV1_testTypeId', testType);
await page.click('[id="root\\/testType"]');
await page.waitForSelector(`[data-id="testType"]`, { state: 'visible' });
await expect(page.locator('[data-id="testType"]')).toBeVisible();
await page.fill('[id="root\\/testType"]', testType);
await page.getByTestId(testTypeId).click();
await page.waitForSelector(`[data-id="${testTypeId}"]`, {
state: 'visible',
});
await expect(page.locator(`[data-id="${testTypeId}"]`)).toBeVisible();
if (paramsValue) {
await page.fill('#testCaseFormV1_params_value', paramsValue);
}
@ -104,10 +125,14 @@ test.describe('Add TestCase New Flow', () => {
// Helper function to open test case form
const openTestCaseForm = async (page: Page) => {
const testCaseDoc = page.waitForResponse(
'/locales/en-US/OpenMetadata/TestCaseForm.md'
);
await page.getByTestId('add-test-case-btn').click();
await page.waitForSelector('[data-testid="test-case-form-v1"]', {
state: 'visible',
});
await testCaseDoc;
await page.waitForLoadState('networkidle');
};
@ -192,7 +217,7 @@ test.describe('Add TestCase New Flow', () => {
.click();
await selectTable(page, table);
await page.click('#testCaseFormV1_selectedColumn');
await page.click('[id="root\\/column"]');
// appearing dropdown takes bit time and its not based on API call so adding manual wait to prevent flakiness.
await page.waitForTimeout(2000);
await page.waitForSelector(

View File

@ -266,8 +266,8 @@ test('Permissions', async ({ userPage, adminPage }) => {
);
await userPage.getByTestId(`edit-${testCaseName}`).click();
await testDefinitionResponse;
await userPage.locator('#tableTestForm_displayName').clear();
await userPage.fill('#tableTestForm_displayName', 'Update_display_name');
await userPage.locator('[id="root\\/displayName"]').clear();
await userPage.fill('[id="root\\/displayName"]', 'Update_display_name');
const saveTestResponse = userPage.waitForResponse(
'/api/v1/dataQuality/testCases/*'
);

View File

@ -44,7 +44,7 @@ test(
await page.getByTestId('test-case-name').clear();
await page.getByTestId('test-case-name').fill(testCaseName);
await page.getByTestId('test-type').locator('div').click();
await page.getByText('Table Column Count To Equal').click();
await page.getByTestId('tableColumnCountToEqual').click();
await page.getByPlaceholder('Enter a Count').fill('13');
const createTestCaseResponse = page.waitForResponse(
(response) =>

View File

@ -331,7 +331,7 @@ test.describe('Data Contracts', () => {
NEW_TABLE_TEST_CASE.name
);
await page.locator('#testCaseFormV1_testTypeId').click();
await page.locator('[id="root\\/testType"]').click();
const dropdown = page.locator('.rc-virtual-list-holder-inner');

View File

@ -135,7 +135,7 @@ test('Table test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
await test.step('Create', async () => {
await page.fill('[data-testid="test-case-name"]', NEW_TABLE_TEST_CASE.name);
await page.click('#testCaseFormV1_testTypeId');
await page.click('[id="root\\/testType"]');
await page.waitForSelector(`text=${NEW_TABLE_TEST_CASE.label}`);
await page.click(`text=${NEW_TABLE_TEST_CASE.label}`);
await page.fill(
@ -285,7 +285,7 @@ test('Column test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
const testDefinitionResponse = page.waitForResponse(
'/api/v1/dataQuality/testDefinitions?limit=*&entityType=COLUMN&testPlatform=OpenMetadata&supportedDataType=VARCHAR'
);
await page.click('#testCaseFormV1_selectedColumn');
await page.click('[id="root\\/column"]');
await page.click(`[title="${NEW_COLUMN_TEST_CASE.column}"]`);
await testDefinitionResponse;
@ -293,7 +293,7 @@ test('Column test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
'[data-testid="test-case-name"]',
NEW_COLUMN_TEST_CASE.name
);
await page.click('#testCaseFormV1_testTypeId');
await page.click('[id="root\\/testType"]');
await page.click(`[data-testid="${NEW_COLUMN_TEST_CASE.type}"]`);
await page.fill(
'#testCaseFormV1_params_minLength',
@ -544,22 +544,22 @@ test(
await test.step('Validate patch request for edit test case', async () => {
await page.fill(
'#tableTestForm_displayName',
'[id="root\\/displayName"]',
'Table test case display name'
);
await expect(page.locator('#tableTestForm_table')).toHaveValue(
await expect(page.locator('[id="root\\/selected-entity"]')).toHaveValue(
table2.entityResponseData?.['name']
);
await expect(page.locator('#tableTestForm_column')).toHaveValue(
await expect(page.locator('[id="root\\/column"]')).toHaveValue(
table2.entity?.columns[3].name
);
await expect(page.locator('#tableTestForm_name')).toHaveValue(
await expect(page.locator('[id="root\\/name"]')).toHaveValue(
testCaseName
);
await expect(page.locator('#tableTestForm_testDefinition')).toHaveValue(
'Column Values To Be In Set'
);
await expect(
page.locator('[id="root\\/columnValuesToBeInSet"]')
).toHaveValue('Column Values To Be In Set');
// Edit test case display name
const updateTestCaseResponse = page.waitForResponse(
@ -665,12 +665,12 @@ test(
page.getByTestId('edit-test-case-drawer-title')
).toBeVisible();
await expect(page.locator('#tableTestForm_displayName')).toHaveValue(
await expect(page.locator('[id="root\\/displayName"]')).toHaveValue(
'Table test case display name'
);
await page.locator('#tableTestForm_displayName').clear();
await page.fill('#tableTestForm_displayName', 'Updated display name');
await page.locator('[id="root\\/displayName"]').clear();
await page.fill('[id="root\\/displayName"]', 'Updated display name');
await page.getByTestId('update-btn').click();
await toastNotification(page, 'Test case updated successfully.');

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, test } from '@playwright/test';
import { expect, Response, test } from '@playwright/test';
import { TableClass } from '../../support/entity/TableClass';
import {
descriptionBox,
@ -52,16 +52,36 @@ test('Table difference test case', async ({ page }) => {
try {
await test.step('Create', async () => {
await page.getByTestId('profiler-add-table-test-btn').click();
const testCaseDoc = page.waitForResponse(
'/locales/en-US/OpenMetadata/TestCaseForm.md'
);
await page.getByTestId('test-case').click();
await testCaseDoc;
await page.getByTestId('test-case-name').click();
await page.waitForSelector(`[data-id="name"]`, { state: 'visible' });
await expect(page.locator('[data-id="name"]')).toBeVisible();
await page.getByTestId('test-case-name').fill(testCase.name);
await page.getByTestId('test-type').click();
await page.fill('#testCaseFormV1_testTypeId', testCase.type);
await page.click('[id="root\\/testType"]');
await page.waitForSelector(`[data-id="testType"]`, { state: 'visible' });
await expect(page.locator('[data-id="testType"]')).toBeVisible();
await page.fill('[id="root\\/testType"]', testCase.type);
const tableListSearchResponse = page.waitForResponse(
`/api/v1/search/query?q=*index=table_search_index*`
);
await page.getByTestId('tableDiff').click();
await tableListSearchResponse;
await page.click('#testCaseFormV1_params_table2');
await page.waitForSelector(`[data-id="tableDiff"]`, {
state: 'visible',
});
await expect(page.locator('[data-id="tableDiff"]')).toBeVisible();
const tableSearchResponse = page.waitForResponse(
`/api/v1/search/query?q=*${testCase.table2}*index=table_search_index*`
);
@ -106,11 +126,14 @@ test('Table difference test case', async ({ page }) => {
await page.fill('#testCaseFormV1_params_where', 'test');
const createTestCaseResponse = page.waitForResponse(
`/api/v1/dataQuality/testCases`
(response: Response) =>
response.url().includes('/api/v1/dataQuality/testCases') &&
response.request().method() === 'POST'
);
await page.getByTestId('create-btn').click();
await createTestCaseResponse;
await toastNotification(page, 'Test case created successfully.');
const response = await createTestCaseResponse;
expect(response.status()).toBe(201);
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?*fields=*'
@ -129,7 +152,11 @@ test('Table difference test case', async ({ page }) => {
page.getByTestId(testCase.name).getByRole('link')
).toBeVisible();
const testCaseDoc = page.waitForResponse(
'/locales/en-US/OpenMetadata/TestCaseForm.md'
);
await page.getByTestId(`edit-${testCase.name}`).click();
await testCaseDoc;
await expect(page.getByTestId('edit-test-case-drawer-title')).toHaveText(
`Edit ${testCase.name}`
@ -140,6 +167,12 @@ test('Table difference test case', async ({ page }) => {
.filter({ hasText: 'Key Columns' })
.getByRole('button')
.click();
await page.waitForSelector(`[data-id="tableDiff"]`, {
state: 'visible',
});
await expect(page.locator('[data-id="tableDiff"]')).toBeVisible();
await page.fill(
'#tableTestForm_params_keyColumns_1_value',
table1.entity?.columns[3].name
@ -203,11 +236,33 @@ test('Custom SQL Query', async ({ page }) => {
try {
await test.step('Create', async () => {
await page.getByTestId('profiler-add-table-test-btn').click();
const testCaseDoc = page.waitForResponse(
'/locales/en-US/OpenMetadata/TestCaseForm.md'
);
await page.getByTestId('test-case').click();
await testCaseDoc;
await page.getByTestId('test-case-name').click();
await page.waitForSelector(`[data-id="name"]`, { state: 'visible' });
await expect(page.locator('[data-id="name"]')).toBeVisible();
await page.getByTestId('test-case-name').fill(testCase.name);
await page.getByTestId('test-type').click();
await page.fill('#testCaseFormV1_testTypeId', testCase.type);
await page.click('[id="root\\/testType"]');
await page.waitForSelector(`[data-id="testType"]`, { state: 'visible' });
await expect(page.locator('[data-id="testType"]')).toBeVisible();
await page.fill('[id="root\\/testType"]', testCase.type);
await page.getByTestId('tableCustomSQLQuery').click();
await page.waitForSelector(`[data-id="tableCustomSQLQuery"]`, {
state: 'visible',
});
await expect(
page.locator('[data-id="tableCustomSQLQuery"]')
).toBeVisible();
await page.click('#testCaseFormV1_params_strategy');
await page.locator('.CodeMirror-scroll').click();
await page
@ -218,11 +273,16 @@ test('Custom SQL Query', async ({ page }) => {
await page.getByTitle('ROWS').click();
await page.fill('#testCaseFormV1_params_threshold', '23');
const createTestCaseResponse = page.waitForResponse(
`/api/v1/dataQuality/testCases`
(response: Response) =>
response.url().includes('/api/v1/dataQuality/testCases') &&
response.request().method() === 'POST'
);
await page.getByTestId('create-btn').click();
await createTestCaseResponse;
await toastNotification(page, 'Test case created successfully.');
const response = await createTestCaseResponse;
expect(response.status()).toBe(201);
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?*fields=*'
@ -241,28 +301,40 @@ test('Custom SQL Query', async ({ page }) => {
page.getByTestId(testCase.name).getByRole('link')
).toBeVisible();
const testCaseDoc = page.waitForResponse(
'/locales/en-US/OpenMetadata/TestCaseForm.md'
);
await page.getByTestId(`edit-${testCase.name}`).click();
await testCaseDoc;
await expect(page.getByTestId('edit-test-case-drawer-title')).toHaveText(
`Edit ${testCase.name}`
);
await expect(page.locator('#tableTestForm_name')).toHaveValue(
await expect(page.locator('[id="root\\/name"]')).toHaveValue(
testCase.name
);
await expect(page.getByTestId('code-mirror-container')).toContainText(
testCase.sqlQuery
);
await page.locator('#tableTestForm_displayName').clear();
await page.fill('#tableTestForm_displayName', testCase.displayName);
await page.locator('[id="root\\/displayName"]').clear();
await page.fill('[id="root\\/displayName"]', testCase.displayName);
await page.locator('.CodeMirror-scroll').click();
await page
.getByTestId('code-mirror-container')
.getByRole('textbox')
.fill(' update');
await page.getByText('ROWS').click();
await page.getByTestId('edit-test-form').getByText('ROWS').click();
await page.getByTitle('COUNT').click();
await page.waitForSelector(`[data-id="tableCustomSQLQuery"]`, {
state: 'visible',
});
await expect(
page.locator('[data-id="tableCustomSQLQuery"]')
).toBeVisible();
await page.getByPlaceholder('Enter a Threshold').clear();
await page.getByPlaceholder('Enter a Threshold').fill('244');
await page.getByTestId('update-btn').click();
@ -297,37 +369,69 @@ test('Column Values To Be Not Null', async ({ page }) => {
await visitDataQualityTab(page, table);
await page.click('[data-testid="profiler-add-table-test-btn"]');
const testCaseDoc = page.waitForResponse(
'/locales/en-US/OpenMetadata/TestCaseForm.md'
);
await page.click('[data-testid="column"]');
await testCaseDoc;
try {
await test.step('Create', async () => {
const testDefinitionResponse = page.waitForResponse(
'/api/v1/dataQuality/testDefinitions?limit=*&entityType=COLUMN&testPlatform=OpenMetadata&supportedDataType=NUMERIC'
);
await page.click('#testCaseFormV1_selectedColumn');
await page.click('[id="root\\/column"]');
await page.waitForSelector(`[data-id="column"]`, { state: 'visible' });
await expect(page.locator('[data-id="column"]')).toBeVisible();
await page.click(
`[title="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.column}"]`
);
await testDefinitionResponse;
await page.getByTestId('test-case-name').click();
await page.waitForSelector(`[data-id="name"]`, { state: 'visible' });
await expect(page.locator('[data-id="name"]')).toBeVisible();
await page.fill(
'[data-testid="test-case-name"]',
NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name
);
await page.fill(
'#testCaseFormV1_testTypeId',
'[id="root\\/testType"]',
NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.type
);
await page.click(
`[data-testid="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.type}"]`
);
await page.waitForSelector(
`[data-id="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.type}"]`,
{
state: 'visible',
}
);
await expect(
page.locator(`[data-id="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.type}"]`)
).toBeVisible();
await page
.locator(descriptionBox)
.fill(NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.description);
await page.click('[data-testid="create-btn"]');
await toastNotification(page, 'Test case created successfully.');
const createTestCaseResponse = page.waitForResponse(
(response: Response) =>
response.url().includes('/api/v1/dataQuality/testCases') &&
response.request().method() === 'POST'
);
await page.click('[data-testid="create-btn"]');
const response = await createTestCaseResponse;
expect(response.status()).toBe(201);
await expect(
page.locator(
`[data-testid="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}"]`
@ -343,13 +447,13 @@ test('Column Values To Be Not Null', async ({ page }) => {
await expect(page.getByTestId('edit-test-case-drawer-title')).toHaveText(
`Edit ${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}`
);
await expect(page.locator('#tableTestForm_name')).toHaveValue(
await expect(page.locator('[id="root\\/name"]')).toHaveValue(
NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name
);
await page.locator('#tableTestForm_displayName').clear();
await page.locator('[id="root\\/displayName"]').clear();
await page.fill(
'#tableTestForm_displayName',
'[id="root\\/displayName"]',
NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.displayName
);
await page.getByText('New table test case for').first().click();

View File

@ -0,0 +1,845 @@
# Data Quality
OpenMetadata supports comprehensive data quality testing to help ensure your data meets defined standards and expectations.
$$section
### Test Level $(id="testLevel")
Select the level at which you want to apply the test case. Choose between:
- **Table Level**: Tests that validate entire table properties such as row count, freshness, or custom SQL queries
- **Column Level**: Tests that validate specific column properties such as values, uniqueness, or data types
The test level determines which test types will be available for selection.
$$
$$section
### Table $(id="table")
Select the table on which you want to create the test case. You can search for tables by name or browse through the available options. The selected table will be the target for your data quality tests.
For column-level tests, you'll need to select a specific column after choosing the table.
$$
$$section
### Column $(id="column")
When creating column-level tests, select the specific column you want to test. The available columns are based on the selected table's schema. Column tests allow you to validate data at a granular level, checking for nulls, unique values, data formats, and more.
$$
$$section
### Test Type $(id="testType")
Choose the type of test to apply based on your data quality requirements. Available test types depend on whether you selected table-level or column-level testing. Each test type has specific parameters and validation rules designed to check different aspects of data quality.
Common test types include:
- Value validation tests
- Uniqueness checks
- Null checks
- Pattern matching
- Range validations
- Custom SQL queries
$$
$$section
### Name $(id="name")
Provide a unique name for your test case. The name should be descriptive and follow these guidelines:
- Must start with a letter
- Can contain letters, numbers, and underscores
- Cannot contain spaces or special characters
- Maximum length of 256 characters
- Must be unique within the test suite
If left empty, a name will be automatically generated based on the test type and parameters.
$$
$$section
### Display Name $(id="displayName")
Provide a user-friendly display name for your test case that will be shown in the UI. Unlike the technical name, the display name:
- Can contain spaces and special characters
- Should be descriptive and meaningful to business users
- Can include punctuation and formatting for readability
- Maximum length of 256 characters
- Does not need to be unique (though recommended for clarity)
The display name helps team members quickly understand the purpose and scope of the test case in dashboards, reports, and notifications. If left empty, the technical name will be used as the display name.
$$
$$section
### Description $(id="description")
Add a detailed description of what this test case validates and why it's important. The description helps team members understand:
- The purpose of the test
- Expected outcomes
- Business context or requirements
- Any special considerations or dependencies
Use markdown formatting to structure your description with headings, lists, and emphasis as needed.
$$
$$section
### Tags $(id="tags")
Add tags to categorize and organize your test cases. Tags help with:
- Grouping related tests together
- Filtering and searching for specific test types
- Creating test execution workflows
- Documenting test categories (e.g., "critical", "regression", "performance")
You can select from existing tags or create new ones. Multiple tags can be added to a single test case.
$$
$$section
### Glossary Terms $(id="glossaryTerms")
Associate glossary terms with your test case to provide business context and standardized definitions. Glossary terms:
- Link technical tests to business concepts
- Provide consistent terminology across teams
- Help with documentation and knowledge sharing
- Enable better understanding of test purposes
Select from your organization's glossary to ensure alignment with established business terminology.
$$
$$section
### Create Pipeline $(id="createPipeline")
Enable automated execution of your test case by creating a pipeline. When enabled:
- Tests run automatically based on the configured schedule
- Results are tracked over time for trend analysis
- Alerts can be configured for test failures
- Multiple test cases can be grouped for batch execution
Configure the schedule using cron expressions or select from predefined intervals (hourly, daily, weekly).
$$
$$section
### Select All Test Cases $(id="selectAllTestCases")
When creating a pipeline, you can choose to include all existing test cases for the selected table. This option:
- Automatically includes all current test cases in the pipeline
- Includes any future test cases added to the table
- Simplifies pipeline management for comprehensive testing
- Ensures complete coverage without manual selection
If disabled, you can manually select specific test cases to include in the pipeline.
$$
$$section
### Test Cases $(id="testCases")
When not selecting all test cases, manually choose which specific test cases to include in the pipeline. This allows you to:
- Create focused test suites for specific scenarios
- Group related tests for targeted validation
- Control execution scope and performance
- Manage different testing strategies (smoke tests, regression tests, etc.)
Select multiple test cases from the available list for the chosen table.
$$
$$section
### Schedule Interval $(id="cron")
Define when and how often the test pipeline should run. You can:
- Use predefined intervals (Hourly, Daily, Weekly, Monthly)
- Create custom schedules using cron expressions
- Set specific times for test execution
- Configure timezone settings
The schedule determines the frequency of data quality checks and helps maintain continuous monitoring of your data assets.
$$
# Test Definitions Reference
This section provides detailed documentation for all available test definitions, their parameters, and use cases.
## Data Quality Dimensions
Tests are categorized into seven data quality dimensions:
- **Completeness**: Tests for missing or null values
- **Accuracy**: Tests for correctness and precision of data values
- **Consistency**: Tests for uniformity and coherence across data
- **Validity**: Tests for conformity to defined formats and patterns
- **Uniqueness**: Tests for duplicate detection and uniqueness constraints
- **Integrity**: Tests for referential integrity and structural consistency
- **SQL**: Custom SQL-based validation tests
## Test Platforms
All test definitions support the following platforms:
- **OpenMetadata** (native platform)
- **Great Expectations**
- **DBT**
- **Deequ**
- **Soda**
- **Other** (custom implementations)
## Column-Level Test Definitions
### Statistical Tests
$$section
#### Column Value Mean To Be Between $(id="columnValueMeanToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that the mean (average) value in a numeric column falls within a specified range.
**Parameters**:
- **Min** (INT) - The minimum acceptable average value for this column. The test will fail if the calculated mean is below this number
- **Max** (INT) - The maximum acceptable average value for this column. The test will fail if the calculated mean is above this number
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT, ARRAY, SET
**Use Cases**:
- Validating average transaction amounts
- Checking mean sensor readings
- Monitoring average response times
- Quality control for measurement data
**Dynamic Assertion**: Supported
$$
$$section
#### Column Value Max To Be Between $(id="columnValueMaxToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that the maximum value in a numeric column falls within a specified range.
**Parameters**:
- **Min** (INT) - The lowest acceptable value for the column's maximum. The test will fail if the highest value in the column is below this threshold
- **Max** (INT) - The highest acceptable value for the column's maximum. The test will fail if the highest value in the column is above this threshold
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT
**Use Cases**:
- Validating price ranges
- Checking temperature limits
- Monitoring system resource usage peaks
- Data boundary validation
**Dynamic Assertion**: Supported
$$
$$section
#### Column Value Min To Be Between $(id="columnValueMinToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that the minimum value in a numeric column falls within a specified range.
**Parameters**:
- **Min** (INT) - The lowest acceptable value for the column's minimum. The test will fail if the smallest value in the column is below this threshold
- **Max** (INT) - The highest acceptable value for the column's minimum. The test will fail if the smallest value in the column is above this threshold
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT
**Use Cases**:
- Ensuring no negative values where inappropriate
- Validating minimum thresholds
- Data quality checks for rates and percentages
- System performance monitoring
**Dynamic Assertion**: Supported
$$
$$section
#### Column Value Median To Be Between $(id="columnValueMedianToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that the median value in a numeric column falls within a specified range.
**Parameters**:
- **Min** (INT) - The minimum acceptable median value for this column. The test will fail if the calculated median is below this number
- **Max** (INT) - The maximum acceptable median value for this column. The test will fail if the calculated median is above this number
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT
**Use Cases**:
- Statistical validation of distributions
- Outlier detection support
- Financial data validation
- Performance metrics validation
**Dynamic Assertion**: Supported
$$
$$section
#### Column Value Standard Deviation To Be Between $(id="columnValueStdDevToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that the standard deviation of values in a numeric column falls within a specified range.
**Parameters**:
- **Min** (INT) - The minimum acceptable standard deviation for this column. Lower values indicate less data variation
- **Max** (INT) - The maximum acceptable standard deviation for this column. Higher values indicate more data variation
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT
**Use Cases**:
- Data consistency validation
- Volatility checks in financial data
- Quality control in manufacturing
- Detecting data anomalies
**Dynamic Assertion**: Supported
$$
$$section
#### Column Values Sum To Be Between $(id="columnValuesSumToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that the sum of all values in a numeric column falls within a specified range.
**Parameters**:
- **Min** (INT) - The minimum acceptable total when all column values are added together
- **Max** (INT) - The maximum acceptable total when all column values are added together
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT
**Use Cases**:
- Financial reconciliation checks
- Inventory validation
- Budget verification
- Resource allocation validation
**Dynamic Assertion**: Supported
$$
### Data Quality Tests
$$section
#### Column Values To Be Not Null $(id="columnValuesToBeNotNull")
**Dimension**: Completeness
**Description**: Tests that values in a column are not null. Empty strings don't count as null - values must be explicitly null.
**Parameters**: None
**Supported Data Types**: All data types supported
**Use Cases**:
- Mandatory field validation
- Data completeness checks
- Required information verification
- Data ingestion quality control
**Row-Level Support**: Yes - shows which specific rows contain null values
$$
$$section
#### Column Values To Be Unique $(id="columnValuesToBeUnique")
**Dimension**: Uniqueness
**Description**: Tests that all values in a column are unique (no duplicates).
**Parameters**: None
**Supported Data Types**: All data types supported
**Use Cases**:
- Primary key validation
- Unique identifier checks
- Email address uniqueness
- Account number validation
**Row-Level Support**: Yes - identifies duplicate values and their locations
$$
$$section
#### Column Values To Be Between $(id="columnValuesToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that all individual values in a numeric column fall within a specified range.
**Parameters**:
- **Min** (INT) - The smallest acceptable value for any individual entry in this column. Leave empty if you only want to set an upper limit
- **Max** (INT) - The largest acceptable value for any individual entry in this column. Leave empty if you only want to set a lower limit
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT, TIMESTAMP, TIMESTAMPZ, DATETIME, DATE
**Use Cases**:
- Age validation (0-120)
- Percentage validation (0-100)
- Rating scale validation (1-5)
- Temperature range checks
**Row-Level Support**: Yes - shows values outside the acceptable range
$$
$$section
#### Column Value Lengths To Be Between $(id="columnValueLengthsToBeBetween")
**Dimension**: Accuracy
**Description**: Tests that the length of string/text values in a column falls within a specified range.
**Parameters**:
- **Min** (INT) - The shortest acceptable length for text values in this column (number of characters). Leave empty if you only want to set a maximum length
- **Max** (INT) - The longest acceptable length for text values in this column (number of characters). Leave empty if you only want to set a minimum length
**Supported Data Types**: BYTES, STRING, MEDIUMTEXT, TEXT, CHAR, VARCHAR, ARRAY
**Use Cases**:
- Name field validation
- Description length checks
- Input validation
- Data format compliance
**Row-Level Support**: Yes - identifies strings with invalid lengths
$$
$$section
#### Column Values Missing Count $(id="columnValuesMissingCount")
**Dimension**: Completeness
**Description**: Tests that the exact number of missing/null values in a column equals a specific expected count.
**Parameters**:
- **Missing Count** (INT, Required) - The exact number of missing/null values you expect to find in this column
- **Missing Value to Match** (STRING, Optional) - Additional text values to treat as missing beyond null and empty values (e.g., 'N/A', 'NULL', 'Unknown')
**Supported Data Types**: All data types supported
**Use Cases**:
- Expected null value validation
- Data completeness monitoring
- Partial data load verification
- Survey response validation
**Row-Level Support**: Yes
$$
### Pattern & Set Tests
$$section
#### Column Values To Match Regex $(id="columnValuesToMatchRegex")
**Dimension**: Validity
**Description**: Tests that values in a column match a specified regular expression pattern. For databases without regex support (MSSQL, AzureSQL), uses LIKE operator.
**Parameters**:
- **RegEx Pattern** (STRING, Required) - The regular expression pattern that all values in this column must match. For example, use `^[A-Z]{2}\\d{4}$` to match patterns like 'AB1234'
**Supported Data Types**: BYTES, STRING, MEDIUMTEXT, TEXT, CHAR, VARCHAR
**Use Cases**:
- Email format validation (`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
- Phone number formats (`^\+?1?-?\.?\s?\(?(\d{3})\)?[\s\.-]?(\d{3})[\s\.-]?(\d{4})$`)
- Product code validation (`^[A-Z]{2}\d{4}$`)
- Custom format compliance
**Row-Level Support**: Yes - shows values that don't match the pattern
$$
$$section
#### Column Values To Not Match Regex $(id="columnValuesToNotMatchRegex")
**Dimension**: Validity
**Description**: Tests that values in a column do NOT match a specified regular expression pattern (inverse of regex match).
**Parameters**:
- **RegEx Pattern** (STRING, Required) - The regular expression pattern that values in this column must NOT match. Any value matching this pattern will cause the test to fail
**Supported Data Types**: BYTES, STRING, MEDIUMTEXT, TEXT, CHAR, VARCHAR
**Use Cases**:
- Profanity detection
- Sensitive data identification
- Invalid character detection
- Data cleansing validation
**Row-Level Support**: Yes - shows values that incorrectly match the forbidden pattern
$$
$$section
#### Column Values To Be In Set $(id="columnValuesToBeInSet")
**Dimension**: Validity
**Description**: Tests that all values in a column are members of a specified set of allowed values.
**Parameters**:
- **Allowed Values** (ARRAY, Required) - List of acceptable values for this column. Any value not in this list will cause the test to fail
- **Match enum** (BOOLEAN, Optional) - When enabled, validates each value independently against the allowed set
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT, BYTES, STRING, MEDIUMTEXT, TEXT, CHAR, VARCHAR, BOOLEAN
**Use Cases**:
- Status field validation (`['active', 'inactive', 'pending']`)
- Category validation (`['bronze', 'silver', 'gold', 'platinum']`)
- Country code validation
- Priority level validation
**Row-Level Support**: Yes - identifies values not in the allowed set
$$
$$section
#### Column Values To Be Not In Set $(id="columnValuesToBeNotInSet")
**Dimension**: Validity
**Description**: Tests that no values in a column are members of a specified set of forbidden values.
**Parameters**:
- **Forbidden Values** (ARRAY, Required) - List of values that must not appear in this column. Finding any of these values will cause the test to fail
**Supported Data Types**: NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT, BYTES, STRING, MEDIUMTEXT, TEXT, CHAR, VARCHAR, BOOLEAN
**Use Cases**:
- Restricted value detection
- Data quality validation
- Blacklist enforcement
- Content moderation
**Row-Level Support**: Yes - shows prohibited values found in the column
$$
### Location Tests
$$section
#### Column Value To Be At Expected Location $(id="columnValueToBeAtExpectedLocation")
**Dimension**: Accuracy
**Description**: Tests that lat/long values in a column are at the specified location within a given radius.
**Parameters**:
- **Location Reference Type** (ARRAY, Required) - How to identify the expected location: choose `CITY` or `POSTAL_CODE`
- **Longitude Column Name (X)** (STRING, Required) - Name of the column containing longitude coordinates in your table
- **Latitude Column Name (Y)** (STRING, Required) - Name of the column containing latitude coordinates in your table
- **Radius (in meters) from the expected location** (FLOAT, Required) - How far in meters the actual coordinates can be from the expected location before the test fails
**Supported Data Types**: BYTES, STRING, MEDIUMTEXT, TEXT, CHAR, VARCHAR, NUMBER, INT, FLOAT, DOUBLE, DECIMAL, TINYINT, SMALLINT, BIGINT, BYTEINT
**Use Cases**:
- GPS coordinate validation
- Address verification
- Geographic data quality
- Location-based service validation
**Row-Level Support**: No
$$
## Table-Level Test Definitions
### Row Count Tests
$$section
#### Table Row Count To Be Between $(id="tableRowCountToBeBetween")
**Dimension**: Integrity
**Description**: Tests that the total number of rows in a table falls within a specified range.
**Parameters**:
- **Min** (INT) - The minimum number of rows this table should contain. Leave empty if you only want to set a maximum
- **Max** (INT) - The maximum number of rows this table should contain. Leave empty if you only want to set a minimum
**Use Cases**:
- Data completeness validation
- ETL process verification
- Table size monitoring
- Data volume quality checks
**Dynamic Assertion**: Supported
**Validation Rules**: minValue must be ≤ maxValue
$$
$$section
#### Table Row Count To Equal $(id="tableRowCountToEqual")
**Dimension**: Integrity
**Description**: Tests that the total number of rows in a table exactly equals a specified value.
**Parameters**:
- **Count** (INT, Required) - The exact number of rows this table must contain
**Use Cases**:
- Reference table validation
- Complete data migration verification
- Fixed dataset validation
- Lookup table integrity
**Dynamic Assertion**: Supported
$$
$$section
#### Table Row Inserted Count To Be Between $(id="tableRowInsertedCountToBeBetween")
**Dimension**: Integrity
**Description**: Tests that the number of newly inserted rows (within a specified time period) falls within a range.
**Parameters**:
- **Min Row Count** (INT, Optional) - Minimum number of new rows expected in the time period
- **Max Row Count** (INT, Optional) - Maximum number of new rows expected in the time period
- **Column Name** (STRING, Required) - Name of the timestamp/date column used to identify new records
- **Range Type** (STRING, Required) - Time unit for measuring new records: 'HOUR', 'DAY', 'MONTH', or 'YEAR'
- **Interval** (INT, Required) - Number of time units to look back. For example, Interval=1 and Range Type=DAY means 'check rows added in the last 1 day'
**Use Cases**:
- ETL monitoring
- Data ingestion rate validation
- Business activity monitoring
- Growth trend validation
**Dynamic Assertion**: Supported
$$
### Schema Tests
$$section
#### Table Column Count To Be Between $(id="tableColumnCountToBeBetween")
**Dimension**: Consistency
**Description**: Tests that the number of columns in a table falls within a specified range.
**Parameters**:
- **Min** (INT) - The minimum number of columns this table should have. Leave empty if you only want to set a maximum
- **Max** (INT) - The maximum number of columns this table should have. Leave empty if you only want to set a minimum
**Use Cases**:
- Schema evolution monitoring
- Table structure validation
- Data model compliance
- Migration verification
**Dynamic Assertion**: Supported
$$
$$section
#### Table Column Count To Equal $(id="tableColumnCountToEqual")
**Dimension**: Consistency
**Description**: Tests that the number of columns in a table exactly equals a specified value.
**Parameters**:
- **Count** (INT, Required) - The exact number of columns this table must have
**Use Cases**:
- Strict schema validation
- Template compliance checking
- Data contract enforcement
- Schema regression testing
**Dynamic Assertion**: Supported
$$
$$section
#### Table Column Name To Exist $(id="tableColumnNameToExist")
**Dimension**: Integrity
**Description**: Tests that a table contains a column with a specific name.
**Parameters**:
- **Column Name** (STRING, Required) - The name of the column that must exist in this table
**Use Cases**:
- Required field validation
- Schema migration verification
- API contract validation
- Data model compliance
**Dynamic Assertion**: Not supported
$$
$$section
#### Table Column To Match Set $(id="tableColumnToMatchSet")
**Dimension**: Integrity
**Description**: Tests that the table column names match a set of values. Unordered by default.
**Parameters**:
- **Column Names** (STRING, Required) - List of expected column names separated by commas (e.g., 'id,name,email,created_date')
- **Ordered** (BOOLEAN, Optional) - When enabled, the columns must appear in the exact order specified. When disabled, columns can be in any order
**Use Cases**:
- Complete schema validation
- Data warehouse table verification
- Schema compliance testing
- Table standardization checks
**Dynamic Assertion**: Not supported
$$
### Custom & Advanced Tests
$$section
#### Custom SQL Query $(id="tableCustomSQLQuery")
**Dimension**: SQL
**Description**: Tests using custom SQL queries that should return 0 rows or COUNT(*) == 0 for the test to pass.
**Parameters**:
- **SQL Expression** (STRING, Required) - Your custom SQL query to run against the table. Write it to return rows that represent problems (test passes when query returns 0 rows)
- **Strategy** (ARRAY, Optional) - How to evaluate results: `ROWS` (count returned rows) or `COUNT` (use COUNT() in your query)
- **Operator** (STRING, Optional) - How to compare the result: `==` (equals), `>` (greater than), `>=` (greater or equal), `<` (less than), `<=` (less or equal), `!=` (not equal)
- **Threshold** (NUMBER, Optional) - The number to compare against (default is 0, meaning 'no problems found')
- **Partition Expression** (STRING, Optional) - SQL expression to group results for row-level analysis
**Strategy Options**:
- `ROWS`: Execute query and check if it returns rows
- `COUNT`: Execute query with COUNT() and compare to threshold
**Use Cases**:
- Complex business rule validation
- Cross-table referential integrity
- Custom data quality checks
- Domain-specific validations
**Examples**:
```sql
-- Check for orphaned records
SELECT * FROM orders WHERE customer_id NOT IN (SELECT id FROM customers)
-- Validate business rules
SELECT * FROM products WHERE price <= 0 OR price > 10000
-- Time-based validations
SELECT * FROM events WHERE created_date > CURRENT_DATE
```
**Row-Level Support**: Yes (with partition expression)
$$
$$section
#### Table Diff $(id="tableDiff")
**Dimension**: Consistency
**Description**: Compares two tables and identifies differences in structure, content, or both.
**Parameters**:
- **Table 2** (STRING, Required) - Full name of the second table to compare against (e.g., 'database.schema.table_name')
- **Key Columns** (ARRAY, Optional) - Columns that uniquely identify each row for comparison. If not specified, the system will use primary key or unique columns
- **Threshold** (NUMBER, Optional) - Maximum number of different rows allowed before the test fails (default is 0 for exact match)
- **Use Columns** (ARRAY, Optional) - Specific columns to compare. If not specified, all columns except key columns will be compared
- **SQL Where Clause** (STRING, Optional) - Condition to filter which rows to include in the comparison (e.g., 'status = "active"')
- **Case sensitive columns** (BOOLEAN, Optional) - When enabled, column name comparison is case-sensitive (e.g., 'Name' ≠ 'name')
**Key Features**:
- Automatically resolves key columns from primary key or unique constraints if not specified
- Supports filtering with SQL WHERE clauses
- Case-sensitive column comparison option
- Configurable difference threshold
**Use Cases**:
- Data migration validation
- Environment synchronization
- Backup verification
- Data replication monitoring
**Dynamic Assertion**: Supported
$$
## Collate-Specific Extensions
### Data Freshness
$$section
#### Table Data To Be Fresh $(id="tableDataToBeFresh")
**Dimension**: Accuracy
**Description**: **Collate-specific test** that validates if the data in a table has been updated within an allowed time frame.
**Parameters**:
- **Column** (STRING, Required) - Name of the timestamp column that tracks when data was last updated (e.g., 'updated_at', 'created_date')
- **Time Since Update** (INT, Required) - Maximum allowed time since the last update before considering data stale
- **Time Unit** (ARRAY, Required) - Unit of time to measure staleness: SECONDS, MINUTES, HOURS, DAYS, WEEKS, MONTHS, or YEARS
**Time Units**: SECONDS, MINUTES, HOURS, DAYS, WEEKS, MONTHS, YEARS
**Use Cases**:
- Real-time data freshness monitoring
- ETL pipeline validation
- Data staleness detection
- SLA compliance monitoring
**Examples**:
- Ensure customer data is updated within 24 hours
- Validate sensor data is fresh within 5 minutes
- Check daily reports are generated within 2 hours
**Dynamic Assertion**: Not supported
**Provider**: System (Collate)
$$
## Advanced Features
### Row-Level Pass/Failed Support
Many test definitions support row-level pass/failed analysis, which provides:
- **Detailed Failure Information**: See exactly which rows failed the test
- **Sample Data**: View sample failing records for debugging
- **Failure Patterns**: Identify common failure scenarios
- **Data Quality Scoring**: Calculate pass/fail ratios at the row level
Tests with row-level support are marked with "**Row-Level Support**: Yes" in their documentation.
### Dynamic Assertions
Tests marked as supporting dynamic assertions can:
- **Auto-adjust thresholds** based on historical data patterns
- **Learn from data trends** to set appropriate bounds
- **Reduce false positives** by adapting to normal data variations
- **Provide intelligent recommendations** for test parameters
### Validation Rules
Some test parameters include validation rules to ensure:
- **Parameter Consistency**: minValue ≤ maxValue relationships
- **Logical Constraints**: Prevent impossible or contradictory configurations
- **Data Type Compatibility**: Ensure parameters match expected data types
- **Business Logic**: Enforce domain-specific validation rules
## Best Practices
### Test Selection Guidelines
1. **Start with Basic Tests**: Begin with null checks, uniqueness, and range validations
2. **Layer Complexity**: Add statistical and pattern-based tests as needed
3. **Custom SQL for Complex Rules**: Use custom SQL tests for business-specific validations
4. **Monitor Performance**: Balance test coverage with execution time
### Parameter Configuration
1. **Use Realistic Ranges**: Base min/max values on actual data analysis
2. **Consider Data Growth**: Account for natural data volume increases
3. **Regular Review**: Update test parameters as business rules evolve
4. **Document Business Context**: Clearly explain why specific thresholds were chosen
### Monitoring Strategy
1. **Critical vs. Warning**: Classify tests by business impact
2. **Trend Analysis**: Monitor test results over time for patterns
3. **Alert Fatigue**: Avoid over-alerting with too many sensitive tests
4. **Root Cause Analysis**: Use row-level details for faster problem resolution

View File

@ -10,14 +10,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { forwardRef } from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { act, forwardRef } from 'react';
import {
LabelType,
State,
@ -63,6 +57,17 @@ jest.mock('../../../../pages/TasksPage/shared/TagSuggestion', () =>
))
);
// Mock ServiceDocPanel component
jest.mock('../../../common/ServiceDocPanel/ServiceDocPanel', () =>
jest
.fn()
.mockImplementation(({ activeField }) => (
<div data-testid="service-doc-panel">
ServiceDocPanel Component - Active Field: {activeField}
</div>
))
);
// Mock AlertBar component
jest.mock('../../../AlertBar/AlertBar', () => ({
__esModule: true,
@ -142,35 +147,24 @@ describe('EditTestCaseModalV1 Component', () => {
});
});
it('should render form with Card structure', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
expect(await screen.findByTestId('edit-test-form')).toBeInTheDocument();
// Check for Card containers
expect(document.querySelector('.form-card-section')).toBeInTheDocument();
});
it('should render all form fields correctly', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
act(() => {
render(<EditTestCaseModalV1 {...mockProps} />);
});
// Wait for form to load
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
// Check table and column fields
expect(await screen.findByLabelText('label.table')).toBeInTheDocument();
expect(await screen.findByLabelText('label.column')).toBeInTheDocument();
// Check table and column fields using text content
expect(screen.getByText('label.table')).toBeInTheDocument();
expect(screen.getByText('label.column')).toBeInTheDocument();
// Check test case details
expect(await screen.findByLabelText('label.name')).toBeInTheDocument();
expect(
await screen.findByLabelText('label.display-name')
).toBeInTheDocument();
expect(
await screen.findByLabelText('label.test-entity')
).toBeInTheDocument();
expect(screen.getByText('label.name')).toBeInTheDocument();
expect(screen.getByText('label.display-name')).toBeInTheDocument();
expect(screen.getByText('label.test-entity')).toBeInTheDocument();
// Check parameter form
expect(
@ -179,48 +173,50 @@ describe('EditTestCaseModalV1 Component', () => {
});
it('should have disabled fields for non-editable elements', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
act(() => {
render(<EditTestCaseModalV1 {...mockProps} />);
});
await waitFor(() => {
expect(screen.getByLabelText('label.table')).toBeDisabled();
expect(screen.getByLabelText('label.column')).toBeDisabled();
expect(screen.getByLabelText('label.name')).toBeDisabled();
expect(screen.getByLabelText('label.test-entity')).toBeDisabled();
const disabledInputs = document.querySelectorAll('input[disabled]');
expect(disabledInputs.length).toBeGreaterThanOrEqual(3); // table, column, name, test-entity fields
});
});
it('should have editable display name field', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
act(() => {
render(<EditTestCaseModalV1 {...mockProps} />);
});
const displayNameField = await screen.findByLabelText('label.display-name');
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
const displayNameField = document.querySelector(
'input[id="root/displayName"]'
);
expect(displayNameField).toBeInTheDocument();
expect(displayNameField).not.toBeDisabled();
expect(displayNameField).toHaveValue(MOCK_TEST_CASE[0].displayName);
});
it('should populate fields with test case data', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
act(() => {
render(<EditTestCaseModalV1 {...mockProps} />);
});
await waitFor(() => {
expect(screen.getByLabelText('label.table')).toHaveValue('dim_address');
expect(screen.getByLabelText('label.column')).toHaveValue('last_name');
expect(screen.getByLabelText('label.name')).toHaveValue(
'column_values_to_match_regex'
);
expect(screen.getByLabelText('label.test-entity')).toHaveValue(
'Column Values To Match Regex Pattern'
const tableField = document.querySelector(
'input[id="root/selected-entity"]'
);
const nameField = document.querySelector('input[id="root/name"]');
expect(tableField).toHaveValue('dim_address');
expect(nameField).toHaveValue('column_values_to_match_regex');
});
});
it('should render action buttons in drawer footer', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
expect(await screen.findByText('label.cancel')).toBeInTheDocument();
expect(await screen.findByText('label.update')).toBeInTheDocument();
});
it('should call onCancel when cancel button is clicked', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
@ -265,7 +261,12 @@ describe('EditTestCaseModalV1 Component', () => {
render(<EditTestCaseModalV1 {...mockProps} testCase={columnTestCase} />);
// Should render column field for column test
expect(await screen.findByLabelText('label.column')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('label.column')).toBeInTheDocument();
expect(
document.querySelector('input[id="root/column"]')
).toBeInTheDocument();
});
});
it('should handle table test case correctly', async () => {
@ -278,17 +279,22 @@ describe('EditTestCaseModalV1 Component', () => {
await waitFor(() => {
// Should not render column field for table test
expect(screen.queryByLabelText('label.column')).not.toBeInTheDocument();
expect(screen.queryByText('label.column')).not.toBeInTheDocument();
expect(
document.querySelector('input[id="root/column"]')
).not.toBeInTheDocument();
});
});
it('should render tags and glossary terms fields', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
expect(
await screen.findByTestId('glossary-terms-selector')
).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
expect(screen.getByTestId('tags-selector')).toBeInTheDocument();
expect(screen.getByTestId('glossary-terms-selector')).toBeInTheDocument();
// Verify TagSuggestion components are rendered
const tagComponents = screen.getAllByText('TagSuggestion Component');
@ -319,10 +325,12 @@ describe('EditTestCaseModalV1 Component', () => {
<EditTestCaseModalV1 {...mockProps} testCase={mockTestCaseWithTags} />
);
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
expect(
await screen.findByTestId('glossary-terms-selector')
).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
expect(screen.getByTestId('tags-selector')).toBeInTheDocument();
expect(screen.getByTestId('glossary-terms-selector')).toBeInTheDocument();
});
it('should filter out tier tags correctly', async () => {
@ -348,11 +356,13 @@ describe('EditTestCaseModalV1 Component', () => {
<EditTestCaseModalV1 {...mockProps} testCase={mockTestCaseWithTierTag} />
);
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
// Should still render tag fields
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
expect(
await screen.findByTestId('glossary-terms-selector')
).toBeInTheDocument();
expect(screen.getByTestId('tags-selector')).toBeInTheDocument();
expect(screen.getByTestId('glossary-terms-selector')).toBeInTheDocument();
});
it('should handle showOnlyParameter mode correctly', async () => {
@ -433,93 +443,250 @@ describe('EditTestCaseModalV1 Component', () => {
expect(cards.length).toBeGreaterThan(0);
// Verify basic form fields are present
expect(screen.getByLabelText('label.table')).toBeInTheDocument();
expect(screen.getByLabelText('label.name')).toBeInTheDocument();
// Verify basic form fields are present using text content
expect(screen.getByText('label.table')).toBeInTheDocument();
expect(screen.getByText('label.name')).toBeInTheDocument();
});
it('should close drawer when onClose is called', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
// =============================================
// NEW FEATURE TESTS
// =============================================
// Find and click the close button (if visible) or trigger onClose
const drawer = document.querySelector('.ant-drawer');
expect(drawer).toBeInTheDocument();
// Simulate drawer close
await act(async () => {
// This would typically be triggered by the drawer's close mechanism
mockProps.onCancel();
});
expect(mockProps.onCancel).toHaveBeenCalled();
});
it('should show loading state correctly', async () => {
// Mock a delayed response to test loading state
(getTestDefinitionById as jest.Mock).mockImplementation(
() =>
new Promise((resolve) =>
setTimeout(
() => resolve(MOCK_TEST_DEFINITION_COLUMN_VALUES_TO_MATCH_REGEX),
100
)
)
);
render(<EditTestCaseModalV1 {...mockProps} />);
// Should show loader initially
expect(screen.getByTestId('loader')).toBeInTheDocument();
// Wait for loading to complete
await waitFor(
() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
},
{ timeout: 5000 }
);
});
it('should handle test case without tags gracefully', async () => {
const mockTestCaseWithoutTags = {
...MOCK_TEST_CASE[0],
tags: undefined,
};
render(
<EditTestCaseModalV1 {...mockProps} testCase={mockTestCaseWithoutTags} />
);
// Should still render tag fields even when no tags exist
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
expect(
await screen.findByTestId('glossary-terms-selector')
).toBeInTheDocument();
});
it('should apply correct CSS classes', async () => {
it('should render ServiceDocPanel with correct props', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
// Check for test-case-form-v1 and drawer-mode classes
expect(document.querySelector('.test-case-form-v1')).toBeInTheDocument();
expect(document.querySelector('.drawer-mode')).toBeInTheDocument();
expect(screen.getByTestId('service-doc-panel')).toBeInTheDocument();
expect(
screen.getByText(/ServiceDocPanel Component - Active Field:/)
).toBeInTheDocument();
});
});
it('should handle drawerProps correctly', async () => {
const customDrawerProps = {
size: 'default' as const,
placement: 'left' as const,
};
it('should update activeField when field receives focus', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
render(
<EditTestCaseModalV1 {...mockProps} drawerProps={customDrawerProps} />
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
const displayNameField = document.querySelector(
'input[id="root/displayName"]'
);
const drawer = document.querySelector('.ant-drawer');
expect(displayNameField).toBeInTheDocument();
expect(drawer).toBeInTheDocument();
expect(displayNameField).toBeTruthy();
if (displayNameField) {
fireEvent.focus(displayNameField);
}
// ActiveField should be updated in ServiceDocPanel
await waitFor(() => {
expect(
screen.getByText(/Active Field: root\/displayName/)
).toBeInTheDocument();
});
});
it('should handle field focus for valid root patterns only', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
const tableField = document.querySelector(
'input[id="root/selected-entity"]'
);
expect(tableField).toBeInTheDocument();
expect(tableField).toBeTruthy();
if (tableField) {
fireEvent.focus(tableField);
}
// ActiveField should be updated for root/selected-entity pattern
await waitFor(() => {
expect(
screen.getByText(/Active Field: root\/selected-entity/)
).toBeInTheDocument();
});
});
it('should render drawer with dual-pane layout', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
expect(
document.querySelector('.drawer-content-wrapper')
).toBeInTheDocument();
expect(
document.querySelector('.drawer-form-content')
).toBeInTheDocument();
expect(document.querySelector('.drawer-doc-panel')).toBeInTheDocument();
});
});
it('should allow editing display name field', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
const displayNameField = document.querySelector(
'input[id="root/displayName"]'
);
expect(displayNameField).toBeInTheDocument();
expect(displayNameField).not.toBeDisabled();
expect(displayNameField).toBeTruthy();
if (displayNameField) {
fireEvent.change(displayNameField, {
target: { value: 'New Display Name' },
});
}
expect(displayNameField).toHaveValue('New Display Name');
});
it('should submit form with updated display name', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
const displayNameField = document.querySelector(
'input[id="root/displayName"]'
);
expect(displayNameField).toBeInTheDocument();
expect(displayNameField).toBeTruthy();
if (displayNameField) {
fireEvent.change(displayNameField, {
target: { value: 'Updated Display Name' },
});
}
const updateBtn = await screen.findByText('label.update');
fireEvent.click(updateBtn);
await waitFor(() => {
expect(mockProps.onUpdate).toHaveBeenCalled();
});
});
it('should handle parameter form click to update activeField', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
const parameterForm = screen.getByText('ParameterForm.component');
fireEvent.click(parameterForm);
// Should update activeField for parameter definition
await waitFor(() => {
expect(
screen.getByText(/Active Field: root\/columnValuesToMatchRegex/)
).toBeInTheDocument();
});
});
it('should preserve display name when showOnlyParameter is true', async () => {
render(<EditTestCaseModalV1 {...mockProps} showOnlyParameter />);
await waitFor(() => {
expect(screen.getByText('ParameterForm.component')).toBeInTheDocument();
});
const updateBtn = screen.getByText('label.update');
fireEvent.click(updateBtn);
// Since showOnlyParameter mode preserves the original displayName from testCase
await waitFor(() => {
expect(mockProps.onUpdate).toHaveBeenCalledWith(
expect.objectContaining({
displayName: MOCK_TEST_CASE[0].displayName,
})
);
});
});
it('should render form cards in correct structure', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
// Wait for loading to complete and form to render
await waitFor(
() => {
expect(screen.queryByTestId('loader')).not.toBeInTheDocument();
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
},
{ timeout: 10000 }
);
const formCards = document.querySelectorAll('.form-card-section');
expect(formCards).toHaveLength(3); // Table/Column card, Test config card, Test details card
});
it('should handle drawer footer actions correctly', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
expect(
document.querySelector('.drawer-footer-actions')
).toBeInTheDocument();
});
const cancelBtn = await screen.findByTestId('cancel-btn');
const updateBtn = await screen.findByTestId('update-btn');
expect(cancelBtn).toBeInTheDocument();
expect(updateBtn).toBeInTheDocument();
});
it('should handle focus events that do not match root pattern', async () => {
render(<EditTestCaseModalV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('edit-test-form')).toBeInTheDocument();
});
// Verify ServiceDocPanel shows no active field initially
expect(screen.getByTestId('service-doc-panel')).toBeInTheDocument();
expect(
screen.getByText('ServiceDocPanel Component - Active Field:')
).toBeInTheDocument();
// Create an element with non-root pattern id and focus it
const testElement = document.createElement('input');
testElement.id = 'invalid-pattern';
document.body.appendChild(testElement);
fireEvent.focus(testElement);
// ActiveField should remain empty for invalid patterns
await waitFor(() => {
expect(
screen.getByText('ServiceDocPanel Component - Active Field:')
).toBeInTheDocument();
});
// Cleanup
document.body.removeChild(testElement);
});
});

View File

@ -15,12 +15,21 @@ import { Button, Card, Drawer, Form, FormProps, Input, Space } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isArray, isEmpty, isEqual, pick } from 'lodash';
import { FC, useEffect, useMemo, useState } from 'react';
import {
FC,
FocusEvent,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as CloseIcon } from '../../../../assets/svg/close.svg';
import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants';
import { TABLE_DIFF } from '../../../../constants/TestSuite.constant';
import { EntityType } from '../../../../enums/entity.enum';
import { TEST_CASE_FORM } from '../../../../constants/service-guide.constant';
import { OPEN_METADATA } from '../../../../constants/Services.constant';
import { EntityType, TabSpecificField } from '../../../../enums/entity.enum';
import { ServiceCategory } from '../../../../enums/service.enum';
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
import { Table } from '../../../../generated/entity/data/table';
import {
@ -55,6 +64,7 @@ import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
import AlertBar from '../../../AlertBar/AlertBar';
import { EntityAttachmentProvider } from '../../../common/EntityDescription/EntityAttachmentProvider/EntityAttachmentProvider';
import Loader from '../../../common/Loader/Loader';
import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel';
import { EditTestCaseModalProps } from './EditTestCaseModal.interface';
import ParameterForm from './ParameterForm';
@ -79,8 +89,9 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
useState<TestDefinition>();
const [isLoading, setIsLoading] = useState(true);
const [isLoadingOnSave, setIsLoadingOnSave] = useState(false);
const [table, setTable] = useState<Table>();
const [table, setTable] = useState<Table & { entityType: EntityType }>();
const [errorMessage, setErrorMessage] = useState<string>('');
const [activeField, setActiveField] = useState<string>('');
// =============================================
// COMPUTED VALUES
@ -117,13 +128,34 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
};
}, [testCase?.tags]);
const handleActiveField = useCallback(
(id: string) => {
// Only update if id matches pattern root/{any string}
if (/^root\/.+/.test(id)) {
setActiveField((pre) => {
if (pre !== id) {
return id;
}
return pre;
});
}
},
[setActiveField]
);
const paramsField = useMemo(() => {
if (selectedDefinition?.parameterDefinition) {
return <ParameterForm definition={selectedDefinition} table={table} />;
return (
<div
onClick={() => handleActiveField(`root/${selectedDefinition.name}`)}>
<ParameterForm definition={selectedDefinition} table={table} />
</div>
);
}
return <></>;
}, [selectedDefinition, table]);
}, [selectedDefinition, table, handleActiveField]);
// =============================================
// FORM FIELDS
@ -138,8 +170,9 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
required: false,
props: {
'data-testid': 'compute-passed-failed-row-count',
id: 'root/computePassedFailedRowCount',
},
id: 'root/computePassedFailedRowCount',
id: 'computePassedFailedRowCount',
formItemLayout: FormItemLayout.HORIZONTAL,
newLook: true,
},
@ -167,12 +200,13 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
name: 'tags',
required: false,
label: t('label.tag-plural'),
id: 'root/tags',
id: 'tags',
type: FieldTypes.TAG_SUGGESTION,
props: {
selectProps: {
'data-testid': 'tags-selector',
getPopupContainer,
id: 'root/tags',
},
newLook: true,
initialValue: tags,
@ -182,12 +216,13 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
name: 'glossaryTerms',
required: false,
label: t('label.glossary-term-plural'),
id: 'root/glossaryTerms',
id: 'glossaryTerms',
type: FieldTypes.TAG_SUGGESTION,
props: {
selectProps: {
'data-testid': 'glossary-terms-selector',
getPopupContainer,
id: 'root/glossaryTerms',
},
newLook: true,
initialValue: glossaryTerms,
@ -207,6 +242,16 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
// =============================================
// HANDLERS
// =============================================
const handleFieldFocus = useCallback(
(event: FocusEvent<HTMLFormElement>) => {
if (event.target.id) {
handleActiveField(event.target.id);
}
},
[handleActiveField]
);
const handleFormSubmit: FormProps['onFinish'] = async (value) => {
setErrorMessage('');
const updatedTestCase = {
@ -295,9 +340,15 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
}
try {
const response = await getTableDetailsByFQN(tableFqn, {
fields: 'columns',
fields: [
TabSpecificField.TAGS,
TabSpecificField.OWNERS,
TabSpecificField.DOMAINS,
TabSpecificField.TESTSUITE,
TabSpecificField.COLUMNS,
],
});
setTable(response);
setTable({ ...response, entityType: EntityType.TABLE });
} catch (error) {
// Handle error silently
}
@ -311,9 +362,7 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
testCase.testDefinition.id || ''
);
if (testCase.testDefinition?.fullyQualifiedName === TABLE_DIFF) {
await fetchTableDetails(tableFqn);
}
await fetchTableDetails(tableFqn);
const formValue = pick(testCase, [
'name',
@ -346,14 +395,6 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
useEffect(() => {
if (testCase && open) {
fetchTestDefinitionById();
const isContainsColumnName = testCase.parameterValues?.find(
(value) => value.name === 'columnName' || value.name === 'column'
);
if (isContainsColumnName) {
fetchTableDetails(tableFqn);
}
}
}, [testCase, open]);
@ -417,20 +458,56 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
block: 'center',
scrollMode: 'if-needed',
}}
onFinish={handleFormSubmit}>
onFinish={handleFormSubmit}
onFocus={handleFieldFocus}>
{!showOnlyParameter && (
<Card className="form-card-section">
<Form.Item required label={t('label.table')} name="table">
<Input disabled />
<Input disabled id="root/selected-entity" />
</Form.Item>
{isColumn && (
<Form.Item required label={t('label.column')} name="column">
<Input disabled />
<Input disabled id="root/column" />
</Form.Item>
)}
</Card>
)}
<Card className="form-card-section">
<Form.Item
required
label={t('label.test-entity', {
entity: t('label.type'),
})}
name="testDefinition">
<Input
disabled
id={`root/${selectedDefinition?.name}`}
placeholder={t('message.enter-test-case-name')}
/>
</Form.Item>
{generateFormFields(
testCaseClassBase.createFormAdditionalFields(
selectedDefinition?.supportsDynamicAssertion ?? false
)
)}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => {
return !isEqual(
prevValues['useDynamicAssertion'],
currentValues['useDynamicAssertion']
);
}}>
{({ getFieldValue }) =>
getFieldValue('useDynamicAssertion') ? null : paramsField
}
</Form.Item>
{isComputeRowCountFieldVisible &&
generateFormFields(computeRowCountField)}
</Card>
{!showOnlyParameter && (
<Card className="form-card-section">
<Form.Item
@ -445,79 +522,21 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
]}>
<Input
disabled
id="root/name"
placeholder={t('message.enter-test-case-name')}
/>
</Form.Item>
<Form.Item label={t('label.display-name')} name="displayName">
<Input placeholder={t('message.enter-test-case-name')} />
</Form.Item>
{generateFormFields(formFields)}
</Card>
)}
{!showOnlyParameter && (
<Card className="form-card-section">
<Form.Item
required
label={t('label.test-entity', {
entity: t('label.type'),
})}
name="testDefinition">
<Input
disabled
id="root/displayName"
placeholder={t('message.enter-test-case-name')}
/>
</Form.Item>
{generateFormFields(
testCaseClassBase.createFormAdditionalFields(
selectedDefinition?.supportsDynamicAssertion ?? false
)
)}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => {
return !isEqual(
prevValues['useDynamicAssertion'],
currentValues['useDynamicAssertion']
);
}}>
{({ getFieldValue }) =>
getFieldValue('useDynamicAssertion') ? null : paramsField
}
</Form.Item>
{isComputeRowCountFieldVisible &&
generateFormFields(computeRowCountField)}
{generateFormFields(formFields)}
</Card>
)}
{/* Show params and dynamic assertion fields outside cards when showOnlyParameter is true */}
{showOnlyParameter && (
<>
{generateFormFields(
testCaseClassBase.createFormAdditionalFields(
selectedDefinition?.supportsDynamicAssertion ?? false
)
)}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => {
return !isEqual(
prevValues['useDynamicAssertion'],
currentValues['useDynamicAssertion']
);
}}>
{({ getFieldValue }) =>
getFieldValue('useDynamicAssertion') ? null : paramsField
}
</Form.Item>
</>
)}
</Form>
</div>
)}
@ -531,13 +550,12 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
return (
<Drawer
destroyOnClose
className="custom-drawer-style"
className="custom-drawer-style test-case-form-drawer"
closable={false}
footer={drawerFooter}
maskClosable={false}
open={open}
placement="right"
size="large"
title={
<label data-testid="edit-test-case-drawer-title">
{t('label.edit-entity', {
@ -545,6 +563,7 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
})}
</label>
}
width="75%"
{...drawerProps}
extra={
<Button
@ -558,7 +577,17 @@ const EditTestCaseModalV1: FC<EditTestCaseModalProps> = ({
form.resetFields();
onCancel();
}}>
<div className="drawer-form-content">{formContent}</div>
<div className="drawer-content-wrapper">
<div className="drawer-form-content">{formContent}</div>
<div className="drawer-doc-panel service-doc-panel markdown-parser">
<ServiceDocPanel
activeField={activeField}
selectedEntity={table}
serviceName={TEST_CASE_FORM}
serviceType={OPEN_METADATA as ServiceCategory}
/>
</div>
</div>
</Drawer>
);
};

View File

@ -184,15 +184,46 @@
}
.test-case-form-alert {
min-height: 75px;
min-height: 90px;
}
}
.test-case-form-drawer {
.ant-drawer-body {
padding: 0 @padding-lg;
.test-case-form-v1,
.edit-test-case-form-v1 {
padding-top: @padding-lg;
padding-bottom: @padding-lg;
}
}
}
// Drawer specific styles
.drawer-form-content {
.test-case-form-v1,
.edit-test-case-form-v1 {
padding-bottom: 0;
.drawer-content-wrapper {
display: grid;
grid-template-columns: 55% 45%;
gap: 24px;
height: calc(100vh - 120px); // Account for drawer header and footer
.drawer-form-content {
overflow-y: auto;
padding-right: 8px;
height: 100%;
.test-case-form-v1,
.edit-test-case-form-v1 {
padding-bottom: 0;
}
}
.drawer-doc-panel {
overflow-y: auto;
border: 1px solid @grey-15;
border-radius: @border-rad-sm;
margin: @margin-lg @margin-lg @margin-lg 0;
padding: 0 @padding-mlg;
}
}

View File

@ -21,10 +21,7 @@ import { forwardRef } from 'react';
import { MOCK_TABLE } from '../../../../mocks/TableData.mock';
import { MOCK_TEST_CASE } from '../../../../mocks/TestSuite.mock';
import { getIngestionPipelines } from '../../../../rest/ingestionPipelineAPI';
import {
createTestCase,
getListTestDefinitions,
} from '../../../../rest/testAPI';
import { getListTestDefinitions } from '../../../../rest/testAPI';
import TestCaseFormV1 from './TestCaseFormV1';
import { TestCaseFormV1Props } from './TestCaseFormV1.interface';
@ -163,7 +160,6 @@ jest.mock('crypto-random-string-with-promisify-polyfill', () =>
jest.mock('../../../../rest/testAPI', () => ({
getListTestDefinitions: jest.fn().mockResolvedValue(mockTestDefinitions),
getListTestCase: jest.fn().mockResolvedValue({ data: [] }),
createTestCase: jest.fn().mockResolvedValue(MOCK_TEST_CASE[0]),
getTestCaseByFqn: jest.fn().mockResolvedValue(MOCK_TEST_CASE[0]),
TestCaseType: {
all: 'all',
@ -205,6 +201,17 @@ jest.mock('../../../../pages/TasksPage/shared/TagSuggestion', () =>
))
);
// Mock ServiceDocPanel component
jest.mock('../../../common/ServiceDocPanel/ServiceDocPanel', () =>
jest
.fn()
.mockImplementation(({ activeField }) => (
<div data-testid="service-doc-panel">
ServiceDocPanel Component - Active Field: {activeField}
</div>
))
);
jest.mock('../../../common/AsyncSelect/AsyncSelect', () => ({
AsyncSelect: jest
.fn()
@ -311,34 +318,11 @@ describe('TestCaseFormV1 Component', () => {
});
describe('Component Rendering', () => {
it('should render form in drawer mode', async () => {
const drawerProps = {
title: 'Create Test Case',
open: true,
};
render(<TestCaseFormV1 {...mockProps} drawerProps={drawerProps} />);
it('should render form in drawer mode with all essential elements', async () => {
render(<TestCaseFormV1 {...mockProps} />);
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
expect(
document.querySelector('.custom-drawer-style')
).toBeInTheDocument();
expect(document.querySelector('.drawer-mode')).toBeInTheDocument();
});
it('should render all form sections with Cards', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(
document.querySelector('.form-card-section')
).toBeInTheDocument();
});
});
it('should render action buttons', async () => {
render(<TestCaseFormV1 {...mockProps} />);
expect(await screen.findByTestId('cancel-btn')).toBeInTheDocument();
expect(await screen.findByTestId('create-btn')).toBeInTheDocument();
});
@ -387,22 +371,18 @@ describe('TestCaseFormV1 Component', () => {
});
// Should show column selection dropdown
await waitFor(() => {
expect(
document.querySelector('#testCaseFormV1_selectedColumn')
).toBeInTheDocument(); // Column select should appear
});
await waitFor(
() => {
// Column selection should appear after switching to column level and selecting table
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
},
{ timeout: 5000 }
);
});
});
describe('Table Selection', () => {
it('should render table selection field', async () => {
render(<TestCaseFormV1 {...mockProps} />);
expect(await screen.findByTestId('async-select')).toBeInTheDocument();
});
it('should handle table selection', async () => {
it('should handle table selection and table prop integration', async () => {
render(<TestCaseFormV1 {...mockProps} />);
const tableSelect = await screen.findByTestId('async-select');
@ -417,29 +397,10 @@ describe('TestCaseFormV1 Component', () => {
'sample_data.ecommerce_db.shopify.users_table'
);
});
it('should use provided table prop when available', async () => {
render(<TestCaseFormV1 {...mockProps} table={MOCK_TABLE} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
// When table is provided, table selection may still be available for changing
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
});
describe('Test Type Selection', () => {
it('should render test type selection field', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(document.querySelector('.ant-select')).toBeInTheDocument();
});
});
it('should load test definitions for table level', async () => {
it('should load test definitions and handle test type selection', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
@ -449,94 +410,23 @@ describe('TestCaseFormV1 Component', () => {
testPlatform: 'OpenMetadata',
supportedDataType: undefined,
});
});
});
it('should show test type options when dropdown is opened', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(document.querySelector('.ant-select')).toBeInTheDocument();
});
// Test type options are loaded via mocked API
expect(getListTestDefinitions as jest.Mock).toHaveBeenCalled();
});
it('should handle test type selection', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(document.querySelector('.ant-select')).toBeInTheDocument();
});
// Test type selection is handled by the component
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
});
describe('Test Details Fields', () => {
it('should render test name field with auto-generation', async () => {
it('should render all essential form fields', async () => {
render(<TestCaseFormV1 {...mockProps} />);
expect(await screen.findByTestId('test-case-name')).toBeInTheDocument();
});
it('should render description field', async () => {
render(<TestCaseFormV1 {...mockProps} />);
expect(
await screen.findByText('RichTextEditor.component')
).toBeInTheDocument();
});
it('should render tags and glossary terms fields', async () => {
render(<TestCaseFormV1 {...mockProps} />);
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
expect(
await screen.findByTestId('glossary-terms-selector')
).toBeInTheDocument();
const tagComponents = screen.getAllByText('TagSuggestion Component');
expect(tagComponents).toHaveLength(2);
});
it('should show compute row count field when test supports it', async () => {
render(<TestCaseFormV1 {...mockProps} />);
// Component renders with compute row count field available
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
// Compute row count field may not be visible until test type supports it
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
});
describe('Parameter Form', () => {
it('should render parameter form when test type is selected', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
// Parameter form will be available when test type is selected
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
it('should not show parameter form when dynamic assertion is enabled', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
// Parameter form visibility is controlled by dynamic assertion
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
});
@ -576,36 +466,6 @@ describe('TestCaseFormV1 Component', () => {
);
});
it('should render pipeline name field in scheduler', async () => {
render(<TestCaseFormV1 {...mockProps} />);
// Select table to enable scheduler
const tableSelect = await screen.findByTestId('async-select');
await act(async () => {
fireEvent.change(tableSelect, {
target: { value: 'sample_data.ecommerce_db.shopify.users_table' },
});
});
expect(await screen.findByTestId('pipeline-name')).toBeInTheDocument();
});
it('should render schedule interval field', async () => {
render(<TestCaseFormV1 {...mockProps} />);
// Select table to enable scheduler
const tableSelect = await screen.findByTestId('async-select');
await act(async () => {
fireEvent.change(tableSelect, {
target: { value: 'sample_data.ecommerce_db.shopify.users_table' },
});
});
expect(
await screen.findByTestId('schedule-interval')
).toBeInTheDocument();
});
it('should render debug log and raise on error switches', async () => {
// Ensure getIngestionPipelines returns 0 pipelines for canCreatePipeline to be true
@ -650,69 +510,23 @@ describe('TestCaseFormV1 Component', () => {
});
describe('Form Interactions', () => {
it('should call onCancel when cancel button is clicked', async () => {
it('should handle cancel and submit actions', async () => {
render(<TestCaseFormV1 {...mockProps} />);
const cancelBtn = await screen.findByTestId('cancel-btn');
const createBtn = await screen.findByTestId('create-btn');
await act(async () => {
fireEvent.click(cancelBtn);
});
expect(mockProps.onCancel).toHaveBeenCalled();
});
it('should disable create button when form is not valid', async () => {
render(<TestCaseFormV1 {...mockProps} />);
const createBtn = await screen.findByTestId('create-btn');
// Create button is present
expect(createBtn).toBeInTheDocument();
});
it('should enable create button when required fields are filled', async () => {
render(<TestCaseFormV1 {...mockProps} />);
const createBtn = await screen.findByTestId('create-btn');
// Create button is present and functional
expect(createBtn).toBeInTheDocument();
});
it('should submit form with correct data', async () => {
render(<TestCaseFormV1 {...mockProps} />);
const createBtn = await screen.findByTestId('create-btn');
await act(async () => {
fireEvent.click(createBtn);
});
// Form submission is handled
expect(createBtn).toBeInTheDocument();
});
});
describe('Table Prop Handling', () => {
it('should pre-select table when provided via props', async () => {
const tableWithFQN = {
...MOCK_TABLE,
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_address',
};
render(<TestCaseFormV1 {...mockProps} table={tableWithFQN} />);
await waitFor(() => {
const tableSelect = screen.getByTestId('async-select');
expect(tableSelect).toHaveValue(
'sample_data.ecommerce_db.shopify.dim_address'
);
});
});
it('should disable table selection when table is provided', async () => {
it('should handle table prop correctly', async () => {
render(<TestCaseFormV1 {...mockProps} table={MOCK_TABLE} />);
const tableSelect = await screen.findByTestId('async-select');
@ -721,90 +535,6 @@ describe('TestCaseFormV1 Component', () => {
});
});
describe('Error Handling', () => {
it('should show loading state when loading prop is true', async () => {
render(<TestCaseFormV1 {...mockProps} loading />);
const createBtn = screen.getByTestId('create-btn');
expect(createBtn).toBeInTheDocument();
});
it('should handle API errors gracefully', async () => {
(createTestCase as jest.Mock).mockRejectedValueOnce(
new Error('API Error')
);
render(<TestCaseFormV1 {...mockProps} />);
const createBtn = await screen.findByTestId('create-btn');
await act(async () => {
fireEvent.click(createBtn);
});
// Should handle error without crashing
expect(createBtn).toBeInTheDocument();
});
});
describe('Drawer Specific', () => {
it('should not show fixed action buttons', async () => {
render(<TestCaseFormV1 {...mockProps} />);
// Action buttons should be in drawer footer, not fixed at bottom
expect(
document.querySelector('.test-case-form-actions')
).not.toBeInTheDocument();
});
it('should render custom drawer title when provided', async () => {
const drawerProps = {
title: 'Custom Test Case Title',
open: true,
};
render(<TestCaseFormV1 {...mockProps} drawerProps={drawerProps} />);
expect(screen.getByText('Custom Test Case Title')).toBeInTheDocument();
});
it('should call onCancel when drawer is closed', async () => {
const drawerProps = {
open: true,
onClose: mockProps.onCancel,
};
render(<TestCaseFormV1 {...mockProps} drawerProps={drawerProps} />);
// Simulate drawer close
await act(async () => {
drawerProps.onClose?.();
});
expect(mockProps.onCancel).toHaveBeenCalled();
});
});
describe('CSS Classes and Styling', () => {
it('should apply correct CSS classes in drawer mode', async () => {
render(<TestCaseFormV1 {...mockProps} className="custom-class" />);
const formContainer = document.querySelector('.test-case-form-v1');
expect(formContainer).toHaveClass('test-case-form-v1');
expect(formContainer).toHaveClass('drawer-mode');
expect(formContainer).toHaveClass('custom-class');
});
it('should render drawer successfully', async () => {
const drawerProps = { open: true };
render(<TestCaseFormV1 {...mockProps} drawerProps={drawerProps} />);
expect(document.body).toBeInTheDocument();
});
});
describe('Custom Query Functionality', () => {
it('should render custom query button for table level tests', async () => {
render(<TestCaseFormV1 {...mockProps} />);
@ -982,4 +712,231 @@ describe('TestCaseFormV1 Component', () => {
});
});
});
// =============================================
// NEW FEATURE TESTS
// =============================================
describe('ServiceDocPanel Integration', () => {
it('should render ServiceDocPanel with correct props', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('service-doc-panel')).toBeInTheDocument();
expect(
screen.getByText(/ServiceDocPanel Component - Active Field:/)
).toBeInTheDocument();
});
});
it('should render dual-pane drawer layout', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(
document.querySelector('.drawer-content-wrapper')
).toBeInTheDocument();
expect(
document.querySelector('.drawer-form-content')
).toBeInTheDocument();
expect(document.querySelector('.drawer-doc-panel')).toBeInTheDocument();
});
});
it('should update activeField when field receives focus', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
const testNameField = document.querySelector(
'input[data-testid="test-case-name"]'
);
expect(testNameField).toBeInTheDocument();
expect(testNameField).toBeTruthy();
if (testNameField) {
fireEvent.focus(testNameField);
}
await waitFor(() => {
expect(
screen.getByText(/Active Field: root\/name/)
).toBeInTheDocument();
});
});
});
describe('Enhanced Field Focus Handling', () => {
it('should handle focus events and activeField updates', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
const form = screen.getByTestId('test-case-form-v1');
// Create a mock focus event with root pattern
const mockEvent = {
target: { id: 'root/testLevel' },
};
fireEvent.focus(form, mockEvent);
await waitFor(() => {
expect(
screen.getByText(/Active Field: root\/testLevel/)
).toBeInTheDocument();
});
});
it('should handle scheduler card click for activeField', async () => {
// Mock getIngestionPipelines to return 0 pipelines
(getIngestionPipelines as jest.Mock).mockResolvedValue({
paging: { total: 0 },
});
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
// Select table to enable scheduler
const tableSelect = screen.getByTestId('async-select');
fireEvent.change(tableSelect, {
target: { value: 'sample_data.ecommerce_db.shopify.users_table' },
});
// Wait for scheduler card to appear
await waitFor(
() => {
const schedulerCard = document.querySelector(
'[data-testid="scheduler-card"]'
);
expect(schedulerCard).toBeInTheDocument();
if (schedulerCard) {
fireEvent.click(schedulerCard);
}
},
{ timeout: 5000 }
);
// ActiveField should be updated for scheduler
await waitFor(() => {
expect(
screen.getByText(/Active Field: root\/cron/)
).toBeInTheDocument();
});
});
});
describe('Display Name Field Enhancement', () => {
it('should set display name equal to test name in form submission', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
const testNameField = document.querySelector(
'input[data-testid="test-case-name"]'
);
expect(testNameField).toBeTruthy();
if (testNameField) {
fireEvent.change(testNameField, {
target: { value: 'test_with_display_name' },
});
}
// Form submission would use this name as both name and displayName
expect(testNameField).toHaveValue('test_with_display_name');
});
it('should handle display name in createTestCaseObj function', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
// Select required fields for form submission
const tableSelect = screen.getByTestId('async-select');
fireEvent.change(tableSelect, {
target: { value: 'sample_data.ecommerce_db.shopify.users_table' },
});
const testNameField = document.querySelector(
'input[data-testid="test-case-name"]'
);
if (testNameField) {
fireEvent.change(testNameField, {
target: { value: 'test_case_with_display' },
});
}
// The component internally sets displayName = name in createTestCaseObj
expect(testNameField).toHaveValue('test_case_with_display');
});
});
describe('Enhanced Table and Column Integration', () => {
it('should handle table selection with focus field updates', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
const tableSelect = screen.getByTestId('async-select');
// Focus and selection should work together
fireEvent.focus(tableSelect);
fireEvent.change(tableSelect, {
target: { value: 'sample_data.ecommerce_db.shopify.users_table' },
});
// Verify that the table selection worked
await waitFor(() => {
expect(tableSelect).toHaveValue(
'sample_data.ecommerce_db.shopify.users_table'
);
});
});
it('should handle column selection field focus when in column mode', async () => {
render(<TestCaseFormV1 {...mockProps} />);
await waitFor(() => {
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
});
// Switch to column level
const columnButton = screen.getByTestId('test-level-column');
fireEvent.click(columnButton);
// Select table first
const tableSelect = screen.getByTestId('async-select');
fireEvent.change(tableSelect, {
target: { value: 'sample_data.ecommerce_db.shopify.users_table' },
});
// Wait for column selection to appear and verify it's functional
await waitFor(
() => {
// Verify the form is still functional in column mode
expect(screen.getByTestId('test-case-form-v1')).toBeInTheDocument();
},
{ timeout: 5000 }
);
});
});
});

View File

@ -32,7 +32,14 @@ import { AxiosError } from 'axios';
import classNames from 'classnames';
import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill';
import { isEmpty, isEqual, isString, snakeCase } from 'lodash';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import {
FC,
FocusEvent,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as CloseIcon } from '../../../../assets/svg/close.svg';
import { ReactComponent as ColumnIcon } from '../../../../assets/svg/ic-column.svg';
@ -44,11 +51,14 @@ import {
} from '../../../../constants/constants';
import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants';
import { DEFAULT_SCHEDULE_CRON_DAILY } from '../../../../constants/Schedular.constants';
import { TEST_CASE_FORM } from '../../../../constants/service-guide.constant';
import { OPEN_METADATA } from '../../../../constants/Services.constant';
import { useAirflowStatus } from '../../../../context/AirflowStatusProvider/AirflowStatusProvider';
import { useLimitStore } from '../../../../context/LimitsProvider/useLimitsStore';
import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../../context/PermissionProvider/PermissionProvider.interface';
import { SearchIndex } from '../../../../enums/search.enum';
import { ServiceCategory } from '../../../../enums/service.enum';
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
import {
CreateIngestionPipeline,
@ -68,7 +78,6 @@ import {
FieldTypes,
FormItemLayout,
} from '../../../../interface/FormUtils.interface';
import { TableSearchSource } from '../../../../interface/search.interface';
import testCaseClassBase from '../../../../pages/IncidentManager/IncidentManagerDetailPage/TestCaseClassBase';
import {
addIngestionPipeline,
@ -102,6 +111,7 @@ import AlertBar from '../../../AlertBar/AlertBar';
import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect';
import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup';
import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface';
import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel';
import ScheduleIntervalV1 from '../../../Settings/Services/AddIngestion/Steps/ScheduleIntervalV1';
import { AddTestCaseList } from '../../AddTestCaseList/AddTestCaseList.component';
import { TestCaseFormType } from '../AddDataQualityTest.interface';
@ -114,14 +124,6 @@ import {
} from './TestCaseFormV1.interface';
import './TestCaseFormV1.less';
const TABLE_SEARCH_FIELDS: (keyof TableSearchSource)[] = [
'name',
'fullyQualifiedName',
'displayName',
'columns',
'testSuite',
];
// =============================================
// MAIN COMPONENT
// =============================================
@ -199,6 +201,8 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
useState<boolean>(false);
const [isCustomQuery, setIsCustomQuery] = useState<boolean>(false);
const [activeField, setActiveField] = useState<string>('');
// =============================================
// HOOKS - Form Watches
// =============================================
@ -275,6 +279,22 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
[selectedTestDefinition]
);
const handleActiveField = useCallback(
(id: string) => {
// Only update if id matches pattern root/{any string}
if (/^root\/.+/.test(id)) {
setActiveField((pre) => {
if (pre !== id) {
return id;
}
return pre;
});
}
},
[setActiveField]
);
// Parameter form rendering
const generateParamsField = useMemo(() => {
if (!selectedTestDefinition?.parameterDefinition) {
@ -282,12 +302,19 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
}
return (
<ParameterForm
definition={selectedTestDefinition}
table={selectedTableData}
/>
<div
onClick={() =>
handleActiveField(
selectedTestType ? `root/${selectedTestType}` : 'root/testType'
)
}>
<ParameterForm
definition={selectedTestDefinition}
table={selectedTableData}
/>
</div>
);
}, [selectedTestDefinition, selectedTableData]);
}, [selectedTestDefinition, selectedTableData, selectedTestType]);
// Dynamic test name generation
const generateDynamicTestName = useCallback(() => {
@ -339,7 +366,7 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
name: 'testName',
required: false,
label: t('label.name'),
id: 'root/testName',
id: 'root/name',
type: FieldTypes.TEXT,
placeholder: t('message.enter-test-case-name'),
rules: [
@ -389,13 +416,14 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
name: 'tags',
required: false,
label: t('label.tag-plural'),
id: 'root/tags',
id: 'tags',
type: FieldTypes.TAG_SUGGESTION,
props: {
selectProps: {
'data-testid': 'tags-selector',
getPopupContainer,
maxTagCount: 8,
id: 'root/tags',
},
newLook: true,
},
@ -404,13 +432,14 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
name: 'glossaryTerms',
required: false,
label: t('label.glossary-term-plural'),
id: 'root/glossaryTerms',
id: 'glossaryTerms',
type: FieldTypes.TAG_SUGGESTION,
props: {
selectProps: {
'data-testid': 'glossary-terms-selector',
getPopupContainer,
maxTagCount: 8,
id: 'root/glossaryTerms',
},
open: false,
hasNoActionButtons: true,
@ -504,6 +533,7 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
(definition) => definition.fullyQualifiedName === value
);
setSelectedTestDefinition(testDefinition);
setActiveField(() => `root/${value}`);
},
[testDefinitions]
);
@ -555,7 +585,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
pageNumber: page,
pageSize: PAGE_SIZE_MEDIUM,
searchIndex: SearchIndex.TABLE,
includeFields: TABLE_SEARCH_FIELDS,
fetchSource: true,
trackTotalHits: true,
});
@ -574,6 +603,9 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
return {
label: hit._source.fullyQualifiedName,
value: hit._source.fullyQualifiedName,
onclick: () => {
handleActiveField('root/selected-entity');
},
data: hit._source,
};
});
@ -832,6 +864,15 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
[createTestCaseObj, testSuite, selectedTable, table, onFormSubmit, onCancel]
);
const handleFieldFocus = useCallback(
(event: FocusEvent<HTMLFormElement>) => {
if (event.target.id) {
handleActiveField(event.target.id);
}
},
[handleActiveField]
);
// =============================================
// EFFECT HOOKS
// =============================================
@ -993,6 +1034,7 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
scrollMode: 'if-needed',
}}
onFinish={handleSubmit}
onFocus={handleFieldFocus}
onValuesChange={handleValuesChange}>
<Card className="form-card-section" data-testid="select-table-card">
<Form.Item
@ -1006,7 +1048,10 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
}),
},
]}>
<SelectionCardGroup options={TEST_LEVEL_OPTIONS} />
<SelectionCardGroup
options={TEST_LEVEL_OPTIONS}
onClick={() => handleActiveField('root/testLevel')}
/>
</Form.Item>
<Form.Item
label={t('label.select-entity', {
@ -1038,10 +1083,14 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
api={fetchTables}
disabled={Boolean(table)}
getPopupContainer={getPopupContainer}
id={selectedTable ? `root/selected-entity` : 'root/table'}
notFoundContent={undefined}
placeholder={t('label.select-entity', {
entity: t('label.table'),
})}
onChange={(value) =>
handleActiveField(value ? `root/selected-entity` : 'root/table')
}
/>
</Form.Item>
@ -1064,6 +1113,7 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
showSearch
filterOption={filterSelectOptions}
getPopupContainer={getPopupContainer}
id="root/column"
loading={!selectedTableData}
options={columnOptions}
placeholder={t('label.select-entity', {
@ -1139,6 +1189,11 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
data-testid="test-type"
filterOption={filterSelectOptions}
getPopupContainer={getPopupContainer}
id={
selectedTestType
? `root/${selectedTestType}`
: 'root/testType'
}
options={testTypeOptions}
placeholder={t('label.select-test-type')}
popupClassName="no-wrap-option"
@ -1194,7 +1249,11 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
</Col>
<Col span={24}>
<Card className="form-card-section" data-testid="scheduler-card">
<Card
className="form-card-section"
data-testid="scheduler-card"
id="root/cron"
onClick={() => handleActiveField('root/cron')}>
<div className="card-title-container">
<Typography.Paragraph className="card-title-text">
{t('label.create-entity', {
@ -1310,15 +1369,15 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
return (
<Drawer
destroyOnClose
className="custom-drawer-style"
className="custom-drawer-style test-case-form-drawer"
closable={false}
footer={drawerFooter}
maskClosable={false}
placement="right"
size="large"
title={t('label.add-entity', {
entity: t('label.test-case'),
})}
width="75%"
{...drawerProps}
extra={
<Button
@ -1329,7 +1388,17 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
/>
}
onClose={onCancel}>
<div className="drawer-form-content">{formContent}</div>
<div className="drawer-content-wrapper">
<div className="drawer-form-content">{formContent}</div>
<div className="drawer-doc-panel service-doc-panel markdown-parser">
<ServiceDocPanel
activeField={activeField}
selectedEntity={selectedTableData}
serviceName={TEST_CASE_FORM}
serviceType={OPEN_METADATA as ServiceCategory}
/>
</div>
</div>
</Drawer>
);
};

View File

@ -36,8 +36,8 @@ import React from 'react';
import ReactDOMServer from 'react-dom/server';
import CopyIcon from '../../../../assets/svg/icon-copy.svg';
import {
markdownTextAndIdRegex,
MARKDOWN_MATCH_ID,
markdownTextAndIdRegex,
} from '../../../../constants/regex.constants';
import { MarkdownToHTMLConverter } from '../../../../utils/FeedUtils';
import i18n from '../../../../utils/i18next/LocalUtil';

View File

@ -60,10 +60,9 @@
}
section[data-highlighted='true'] {
background: @primary-1;
color: @primary-color;
background: @primary-50;
transition: ease-in-out;
border-left: 3px solid @primary-color;
border-left: 3px solid @primary-6;
padding-bottom: 12px !important;
margin-top: 12px;

View File

@ -23,6 +23,7 @@ export interface SelectionCardGroupProps {
options: SelectionOption[];
value?: string;
onChange?: (value: string) => void;
onClick?: () => void;
className?: string;
disabled?: boolean;
}

View File

@ -58,11 +58,13 @@ const SelectionCardGroup: FC<SelectionCardGroupProps> = ({
value,
onChange,
className,
onClick,
disabled = false,
}: SelectionCardGroupProps) => {
const handleOptionSelect = (selectedValue: string) => {
if (!disabled) {
onChange?.(selectedValue);
onClick?.();
}
};

View File

@ -10,8 +10,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import ServiceRequirements from './ServiceDocPanel';
import { render, screen, waitFor } from '@testing-library/react';
import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { fetchMarkdownFile } from '../../../rest/miscAPI';
import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils';
import ServiceDocPanel from './ServiceDocPanel';
jest.mock('../Loader/Loader', () =>
jest.fn().mockReturnValue(<div data-testid="loader">Loader</div>)
@ -25,34 +28,316 @@ jest.mock('../RichTextEditor/RichTextEditorPreviewer', () =>
))
);
jest.mock('../../../rest/miscAPI', () => ({
fetchMarkdownFile: jest
jest.mock('../../Explore/EntitySummaryPanel/EntitySummaryPanel.component', () =>
jest
.fn()
.mockImplementation(() => Promise.resolve('markdown text')),
.mockReturnValue(
<div data-testid="entity-summary-panel">Entity Summary</div>
)
);
jest.mock('../../../rest/miscAPI', () => ({
fetchMarkdownFile: jest.fn(),
}));
const mockOnBack = jest.fn();
const mockOnNext = jest.fn();
jest.mock('../../../utils/ServiceUtils', () => ({
getActiveFieldNameForAppDocs: jest.fn(),
}));
const mockProps = {
serviceName: 'Test Service',
serviceType: 'Test Type',
onBack: mockOnBack,
onNext: mockOnNext,
jest.mock('react-i18next', () => ({
useTranslation: () => ({
i18n: {
language: 'en-US',
},
}),
}));
const mockFetchMarkdownFile = fetchMarkdownFile as jest.MockedFunction<
typeof fetchMarkdownFile
>;
const mockGetActiveFieldNameForAppDocs =
getActiveFieldNameForAppDocs as jest.MockedFunction<
typeof getActiveFieldNameForAppDocs
>;
const mockScrollIntoView = jest.fn();
const mockQuerySelector = jest.fn();
const mockQuerySelectorAll = jest.fn();
const mockSetAttribute = jest.fn();
const mockRemoveAttribute = jest.fn();
Object.defineProperty(window, 'requestAnimationFrame', {
writable: true,
value: jest.fn((callback: FrameRequestCallback) => callback(0)),
});
Object.defineProperty(document, 'querySelector', {
writable: true,
value: mockQuerySelector,
});
Object.defineProperty(document, 'querySelectorAll', {
writable: true,
value: mockQuerySelectorAll,
});
const createMockElement = (
setAttribute = mockSetAttribute,
removeAttribute = mockRemoveAttribute
) => ({
scrollIntoView: mockScrollIntoView,
setAttribute,
removeAttribute,
});
const defaultProps = {
serviceName: 'mysql',
serviceType: 'DatabaseService',
};
describe('ServiceRequirements Component', () => {
it('Should render the requirements and action buttons', async () => {
await act(async () => {
render(<ServiceRequirements {...mockProps} />);
const mockSelectedEntity = {
id: 'entity-1',
name: 'test-entity',
displayName: 'Test Entity',
serviceType: 'DatabaseService',
};
describe('ServiceDocPanel Component', () => {
beforeEach(() => {
jest.clearAllMocks();
mockFetchMarkdownFile.mockResolvedValue('markdown content');
mockQuerySelectorAll.mockReturnValue([]);
mockQuerySelector.mockReturnValue(null);
mockGetActiveFieldNameForAppDocs.mockReturnValue(undefined);
});
describe('Core Functionality', () => {
it('should render component and fetch markdown content', async () => {
render(<ServiceDocPanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId('service-requirements')).toBeInTheDocument();
expect(screen.getByTestId('requirement-text')).toBeInTheDocument();
expect(mockFetchMarkdownFile).toHaveBeenCalledWith(
'en-US/DatabaseService/mysql.md'
);
});
});
expect(screen.getByTestId('service-requirements')).toBeInTheDocument();
it('should show loader during fetch and hide when complete', async () => {
const pendingPromise = Promise.resolve('markdown content');
mockFetchMarkdownFile.mockReturnValue(pendingPromise);
const requirementTextElement = screen.getByTestId('requirement-text');
render(<ServiceDocPanel {...defaultProps} />);
expect(requirementTextElement).toBeInTheDocument();
expect(screen.getByTestId('loader')).toBeInTheDocument();
expect(requirementTextElement).toHaveTextContent('markdown text');
await waitFor(() => {
expect(screen.queryByTestId('loader')).not.toBeInTheDocument();
});
});
it('should handle Api service type conversion', async () => {
render(<ServiceDocPanel serviceName="rest-api" serviceType="Api" />);
await waitFor(() => {
expect(mockFetchMarkdownFile).toHaveBeenCalledWith(
'en-US/ApiEntity/rest-api.md'
);
});
});
it('should fetch workflow documentation when isWorkflow is true', async () => {
render(
<ServiceDocPanel
{...defaultProps}
isWorkflow
workflowType={PipelineType.Metadata}
/>
);
await waitFor(() => {
expect(mockFetchMarkdownFile).toHaveBeenCalledWith(
'en-US/DatabaseService/workflows/metadata.md'
);
});
});
});
describe('Error Handling', () => {
it('should handle fetch failures gracefully', async () => {
mockFetchMarkdownFile.mockRejectedValue(new Error('Network error'));
render(<ServiceDocPanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId('requirement-text')).toHaveTextContent('');
});
});
});
describe('Field Highlighting', () => {
beforeEach(() => {
const mockElement = createMockElement();
mockQuerySelector.mockReturnValue(mockElement);
mockQuerySelectorAll.mockReturnValue([createMockElement()]);
});
it('should highlight and scroll to active field', async () => {
render(
<ServiceDocPanel {...defaultProps} activeField="root/database/name" />
);
await waitFor(() => {
expect(mockQuerySelector).toHaveBeenCalledWith('[data-id="name"]');
expect(mockScrollIntoView).toHaveBeenCalledWith({
block: 'center',
behavior: 'smooth',
inline: 'center',
});
expect(mockSetAttribute).toHaveBeenCalledWith(
'data-highlighted',
'true'
);
});
});
it('should handle Applications service type with custom field processing', async () => {
mockGetActiveFieldNameForAppDocs.mockReturnValue('config.database');
render(
<ServiceDocPanel
activeField="root/config/database"
serviceName="app-service"
serviceType="Applications"
/>
);
await waitFor(() => {
expect(mockGetActiveFieldNameForAppDocs).toHaveBeenCalledWith(
'root/config/database'
);
expect(mockQuerySelector).toHaveBeenCalledWith(
'[data-id="config.database"]'
);
});
});
it('should clean up previous highlights before highlighting new element', async () => {
const previousElement = createMockElement();
const currentElement = createMockElement();
mockQuerySelectorAll.mockReturnValue([previousElement]);
mockQuerySelector.mockReturnValue(currentElement);
render(
<ServiceDocPanel {...defaultProps} activeField="root/database/host" />
);
await waitFor(() => {
expect(mockQuerySelectorAll).toHaveBeenCalledWith(
'[data-highlighted="true"]'
);
expect(previousElement.removeAttribute).toHaveBeenCalledWith(
'data-highlighted'
);
expect(currentElement.setAttribute).toHaveBeenCalledWith(
'data-highlighted',
'true'
);
});
});
it('should handle field names with special patterns', async () => {
render(
<ServiceDocPanel
{...defaultProps}
activeField="root/database/items/0"
/>
);
await waitFor(() => {
expect(mockQuerySelector).toHaveBeenCalledWith('[data-id="database"]');
});
});
});
describe('Entity Integration', () => {
it('should render EntitySummaryPanel when selectedEntity is provided', async () => {
render(
<ServiceDocPanel
{...defaultProps}
selectedEntity={mockSelectedEntity}
/>
);
await waitFor(() => {
expect(screen.getByTestId('entity-summary-panel')).toBeInTheDocument();
});
});
it('should handle complete integration with entity and highlighting', async () => {
const mockElement = createMockElement();
mockQuerySelector.mockReturnValue(mockElement);
mockGetActiveFieldNameForAppDocs.mockReturnValue('application.config');
render(
<ServiceDocPanel
activeField="root/application/config"
selectedEntity={mockSelectedEntity}
serviceName="custom-app"
serviceType="Applications"
/>
);
await waitFor(() => {
expect(screen.getByTestId('entity-summary-panel')).toBeInTheDocument();
expect(screen.getByTestId('requirement-text')).toBeInTheDocument();
expect(mockGetActiveFieldNameForAppDocs).toHaveBeenCalledWith(
'root/application/config'
);
expect(mockQuerySelector).toHaveBeenCalledWith(
'[data-id="application.config"]'
);
});
});
});
describe('Props Updates', () => {
it('should refetch content when serviceName changes', async () => {
const { rerender } = render(<ServiceDocPanel {...defaultProps} />);
await waitFor(() => {
expect(mockFetchMarkdownFile).toHaveBeenCalledTimes(1);
});
rerender(<ServiceDocPanel {...defaultProps} serviceName="postgres" />);
await waitFor(() => {
expect(mockFetchMarkdownFile).toHaveBeenCalledTimes(2);
expect(mockFetchMarkdownFile).toHaveBeenLastCalledWith(
'en-US/DatabaseService/postgres.md'
);
});
});
it('should update highlighting when activeField changes', async () => {
const mockElement = createMockElement();
mockQuerySelector.mockReturnValue(mockElement);
const { rerender } = render(
<ServiceDocPanel {...defaultProps} activeField="root/field1" />
);
await waitFor(() => {
expect(mockQuerySelector).toHaveBeenCalledWith('[data-id="field1"]');
});
rerender(<ServiceDocPanel {...defaultProps} activeField="root/field2" />);
await waitFor(() => {
expect(mockQuerySelector).toHaveBeenCalledWith('[data-id="field2"]');
});
});
});
});

View File

@ -11,8 +11,8 @@
* limitations under the License.
*/
import { Col, Row } from 'antd';
import { first, last } from 'lodash';
import { FC, useCallback, useEffect, useState } from 'react';
import { first, last, noop } from 'lodash';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ENDS_WITH_NUMBER_REGEX,
@ -22,16 +22,18 @@ import { PipelineType } from '../../../generated/entity/services/ingestionPipeli
import { fetchMarkdownFile } from '../../../rest/miscAPI';
import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface';
import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils';
import EntitySummaryPanel from '../../Explore/EntitySummaryPanel/EntitySummaryPanel.component';
import { SearchedDataProps } from '../../SearchedData/SearchedData.interface';
import Loader from '../Loader/Loader';
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
import './service-doc-panel.less';
interface ServiceDocPanelProp {
serviceName: string;
serviceType: string;
activeField?: string;
isWorkflow?: boolean;
workflowType?: PipelineType;
selectedEntity?: SearchedDataProps['data'][number]['_source'];
}
const ServiceDocPanel: FC<ServiceDocPanelProp> = ({
@ -40,12 +42,14 @@ const ServiceDocPanel: FC<ServiceDocPanelProp> = ({
activeField,
isWorkflow,
workflowType,
selectedEntity,
}) => {
const { i18n } = useTranslation();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [markdownContent, setMarkdownContent] = useState<string>('');
const [isMarkdownReady, setIsMarkdownReady] = useState<boolean>(false);
const getActiveFieldName = useCallback(
(activeFieldValue?: ServiceDocPanelProp['activeField']) => {
@ -116,6 +120,7 @@ const ServiceDocPanel: FC<ServiceDocPanelProp> = ({
setMarkdownContent('');
} finally {
setIsLoading(false);
setIsMarkdownReady(true);
}
};
@ -124,22 +129,60 @@ const ServiceDocPanel: FC<ServiceDocPanelProp> = ({
}, [serviceName, serviceType]);
useEffect(() => {
if (!isMarkdownReady) {
return;
}
const fieldName =
serviceType === 'Applications'
? getActiveFieldNameForAppDocs(activeField)
: getActiveFieldName(activeField);
if (fieldName) {
const element = document.querySelector(`[data-id="${fieldName}"]`);
if (element) {
element.scrollIntoView({
block: 'center',
behavior: 'smooth',
inline: 'center',
// Use requestAnimationFrame to ensure DOM is ready
requestAnimationFrame(() => {
// Remove all previous highlights first
const previousHighlighted = document.querySelectorAll(
'[data-highlighted="true"]'
);
previousHighlighted.forEach((el) => {
el.removeAttribute('data-highlighted');
});
element.setAttribute('data-highlighted', 'true');
}
const element = document.querySelector(`[data-id="${fieldName}"]`);
if (element) {
element.scrollIntoView({
block: fieldName === 'selected-entity' ? 'start' : 'center',
behavior: 'smooth',
inline: 'center',
});
element.setAttribute('data-highlighted', 'true');
}
});
}
}, [activeField, getActiveFieldName, serviceType]);
}, [activeField, serviceType, isMarkdownReady]);
const docsPanel = useMemo(() => {
return (
<>
<div className="entity-summary-in-docs" data-id="selected-entity">
{selectedEntity && (
<EntitySummaryPanel
entityDetails={{
details: selectedEntity,
}}
handleClosePanel={noop}
/>
)}
</div>
<RichTextEditorPreviewer
enableSeeMoreVariant={false}
markdown={markdownContent}
/>
</>
);
}, [markdownContent, serviceName, selectedEntity]);
if (isLoading) {
return <Loader />;
@ -147,12 +190,7 @@ const ServiceDocPanel: FC<ServiceDocPanelProp> = ({
return (
<Row data-testid="service-requirements">
<Col span={24}>
<RichTextEditorPreviewer
enableSeeMoreVariant={false}
markdown={markdownContent}
/>
</Col>
<Col span={24}>{docsPanel}</Col>
</Row>
);
};

View File

@ -24,4 +24,13 @@
padding: 4px 24px !important;
}
}
// Styling for EntitySummaryPanel when embedded in documentation
.entity-summary-in-docs {
margin: @margin-md -@margin-md;
.ant-card-bordered {
border: none;
}
}
}

View File

@ -153,3 +153,4 @@ export const OPENMETADATA_URL_CONFIG_SERVICE_CATEGORY =
'OpenMetadataUrlConfiguration';
export const CUSTOM_PROPERTY_CATEGORY = 'CustomProperty';
export const OPEN_METADATA = 'OpenMetadata';
export const TEST_CASE_FORM = 'TestCaseForm';