mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-17 03:38:18 +00:00
Feat: add support of adding tags in test case form (#22007)
* Feat: add support of adding tags in test case form * fix: don't override test case tags with entity tags * feat: enhance test case form to support glossary terms and improve tag handling * feat: implement tags and glossary terms functionality in test case forms and enhance related tests * fixed failing unit test * feat: add tier tag filtering and preservation in test case forms and related tests * feat: add tags to addTestCaseAction --------- Co-authored-by: Teddy Crepineau <teddy.crepineau@gmail.com>
This commit is contained in:
parent
64f09e8614
commit
62ab5689d2
@ -114,7 +114,6 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
|
||||
fields.contains(TEST_CASE_RESULT) ? getTestCaseResult(test) : test.getTestCaseResult());
|
||||
test.setIncidentId(
|
||||
fields.contains(INCIDENTS_FIELD) ? getIncidentId(test) : test.getIncidentId());
|
||||
test.setTags(fields.contains(FIELD_TAGS) ? getTestCaseTags(test) : test.getTags());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -129,15 +128,19 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
|
||||
private void inheritTags(TestCase testCase, Fields fields, Table table) {
|
||||
if (fields.contains(FIELD_TAGS)) {
|
||||
EntityLink entityLink = EntityLink.parse(testCase.getEntityLink());
|
||||
List<TagLabel> tags = new ArrayList<>(table.getTags());
|
||||
List<TagLabel> testCaseTags =
|
||||
testCase.getTags() != null ? new ArrayList<>(testCase.getTags()) : new ArrayList<>();
|
||||
List<TagLabel> tableTags =
|
||||
table.getTags() != null ? new ArrayList<>(table.getTags()) : new ArrayList<>();
|
||||
EntityUtil.mergeTags(testCaseTags, tableTags);
|
||||
if (entityLink.getFieldName() != null && entityLink.getFieldName().equals("columns")) {
|
||||
// if we have a column test case get the columns tags as well
|
||||
// if we have a column test case inherit the columns tags as well
|
||||
table.getColumns().stream()
|
||||
.filter(column -> column.getName().equals(entityLink.getArrayFieldName()))
|
||||
.findFirst()
|
||||
.ifPresent(column -> tags.addAll(column.getTags()));
|
||||
.ifPresent(column -> EntityUtil.mergeTags(testCaseTags, column.getTags()));
|
||||
}
|
||||
testCase.setTags(tags);
|
||||
testCase.setTags(testCaseTags);
|
||||
}
|
||||
}
|
||||
|
||||
@ -479,20 +482,6 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
|
||||
return ongoingIncident;
|
||||
}
|
||||
|
||||
private List<TagLabel> getTestCaseTags(TestCase test) {
|
||||
EntityLink entityLink = EntityLink.parse(test.getEntityLink());
|
||||
Table table = Entity.getEntity(entityLink, "tags,columns", ALL);
|
||||
List<TagLabel> tags = new ArrayList<>(table.getTags());
|
||||
if (entityLink.getFieldName() != null && entityLink.getFieldName().equals("columns")) {
|
||||
// if we have a column test case get the columns tags as well
|
||||
table.getColumns().stream()
|
||||
.filter(column -> column.getName().equals(entityLink.getArrayFieldName()))
|
||||
.findFirst()
|
||||
.ifPresent(column -> tags.addAll(column.getTags()));
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
public int getTestCaseCount(List<UUID> testCaseIds) {
|
||||
return daoCollection.testCaseDAO().countOfTestCases(testCaseIds);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOf;
|
||||
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||
import static org.openmetadata.schema.api.teams.CreateTeam.TeamType.GROUP;
|
||||
import static org.openmetadata.schema.type.ColumnDataType.BIGINT;
|
||||
import static org.openmetadata.schema.type.MetadataOperation.DELETE;
|
||||
@ -1192,6 +1193,84 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_getTestCaseWithTagsField(TestInfo testInfo) throws IOException {
|
||||
// Create a table with tags
|
||||
TableResourceTest tableResourceTest = new TableResourceTest();
|
||||
String columnName = "taggedColumn";
|
||||
CreateTable createTable = tableResourceTest.createRequest(testInfo);
|
||||
createTable.setDatabaseSchema(DATABASE_SCHEMA.getFullyQualifiedName());
|
||||
List<Column> columns = new ArrayList<>(createTable.getColumns());
|
||||
columns.addAll(
|
||||
List.of(
|
||||
new Column()
|
||||
.withName(columnName)
|
||||
.withDisplayName(columnName)
|
||||
.withDataType(ColumnDataType.VARCHAR)
|
||||
.withDataLength(10)
|
||||
.withTags(List.of(PII_SENSITIVE_TAG_LABEL)),
|
||||
new Column()
|
||||
.withName("normalColumn")
|
||||
.withDisplayName("normalColumn")
|
||||
.withDataType(ColumnDataType.BIGINT)));
|
||||
createTable.setColumns(columns);
|
||||
createTable.setTags(List.of(PERSONAL_DATA_TAG_LABEL));
|
||||
|
||||
Table table = tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Create test cases - one for table level, one for column level
|
||||
CreateTestCase tableTestCase =
|
||||
createRequest(testInfo)
|
||||
.withName("tableTestCaseWithTags")
|
||||
.withEntityLink(String.format("<#E::table::%s>", table.getFullyQualifiedName()))
|
||||
.withTags(List.of(TIER1_TAG_LABEL))
|
||||
.withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName());
|
||||
TestCase createdTableTestCase = createEntity(tableTestCase, ADMIN_AUTH_HEADERS);
|
||||
|
||||
CreateTestCase columnTestCase =
|
||||
createRequest(testInfo)
|
||||
.withName("columnTestCaseWithTags")
|
||||
.withEntityLink(
|
||||
String.format(
|
||||
"<#E::table::%s::columns::%s>", table.getFullyQualifiedName(), columnName))
|
||||
.withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName())
|
||||
.withTags(List.of(TIER1_TAG_LABEL))
|
||||
.withParameterValues(
|
||||
List.of(
|
||||
new TestCaseParameterValue().withValue("10").withName("missingCountValue")));
|
||||
TestCase createdColumnTestCase = createEntity(columnTestCase, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Test 1: Get table-level test case by ID with tags field
|
||||
TestCase fetchedTableTestCase =
|
||||
getEntity(createdTableTestCase.getId(), "tags", ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(fetchedTableTestCase.getTags());
|
||||
assertEquals(2, fetchedTableTestCase.getTags().size());
|
||||
Set<String> tableTestCaseTags =
|
||||
fetchedTableTestCase.getTags().stream().map(TagLabel::getName).collect(Collectors.toSet());
|
||||
assertTrue(tableTestCaseTags.contains(PERSONAL_DATA_TAG_LABEL.getName()));
|
||||
assertTrue(tableTestCaseTags.contains(TIER1_TAG_LABEL.getName()));
|
||||
|
||||
// Test 2: Get column-level test case by ID with tags field
|
||||
TestCase fetchedColumnTestCase =
|
||||
getEntity(createdColumnTestCase.getId(), "tags", ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(fetchedColumnTestCase.getTags());
|
||||
assertEquals(3, fetchedColumnTestCase.getTags().size());
|
||||
Set<String> columnTestCaseTags =
|
||||
fetchedColumnTestCase.getTags().stream().map(TagLabel::getName).collect(Collectors.toSet());
|
||||
assertTrue(columnTestCaseTags.contains(TIER1_TAG_LABEL.getName()));
|
||||
assertTrue(columnTestCaseTags.contains(PII_SENSITIVE_TAG_LABEL.getName()));
|
||||
|
||||
// Test 3: Get test case by name with tags field
|
||||
TestCase fetchedByName =
|
||||
getEntityByName(createdTableTestCase.getFullyQualifiedName(), "tags", ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(fetchedByName.getTags());
|
||||
assertEquals(2, fetchedByName.getTags().size());
|
||||
|
||||
// Test 4: Verify tags are not included when not requested
|
||||
TestCase withoutTags = getEntity(createdTableTestCase.getId(), null, ADMIN_AUTH_HEADERS);
|
||||
assertTrue(nullOrEmpty(withoutTags.getTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void post_entity_as_non_admin_401(TestInfo test) {
|
||||
|
@ -34,6 +34,14 @@
|
||||
"description": "If the test definition supports it, use dynamic assertion to evaluate the test case.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"tags": {
|
||||
"description": "Tags to apply",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../../../../../type/tagLabel.json"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config';
|
||||
import { SidebarItem } from '../../constant/sidebar';
|
||||
import { Domain } from '../../support/domain/Domain';
|
||||
import { TableClass } from '../../support/entity/TableClass';
|
||||
import { Glossary } from '../../support/glossary/Glossary';
|
||||
import { GlossaryTerm } from '../../support/glossary/GlossaryTerm';
|
||||
import { ClassificationClass } from '../../support/tag/ClassificationClass';
|
||||
import { TagClass } from '../../support/tag/TagClass';
|
||||
import {
|
||||
assignDomain,
|
||||
clickOutside,
|
||||
@ -36,13 +40,22 @@ test.use({ storageState: 'playwright/.auth/admin.json' });
|
||||
const table1 = new TableClass();
|
||||
const table2 = new TableClass();
|
||||
|
||||
// Test data for tags and glossary terms
|
||||
const testClassification = new ClassificationClass();
|
||||
const testTag1 = new TagClass({
|
||||
classification: testClassification.data.name,
|
||||
});
|
||||
const testTag2 = new TagClass({
|
||||
classification: testClassification.data.name,
|
||||
});
|
||||
const testGlossary = new Glossary();
|
||||
const testGlossaryTerm1 = new GlossaryTerm(testGlossary);
|
||||
const testGlossaryTerm2 = new GlossaryTerm(testGlossary);
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
await table1.create(apiContext);
|
||||
await table2.create(apiContext);
|
||||
const { testSuiteData } = await table2.createTestSuiteAndPipelines(
|
||||
apiContext
|
||||
);
|
||||
await table2.createTestCase(apiContext, {
|
||||
name: `email_column_values_to_be_in_set_${uuid()}`,
|
||||
entityLink: `<#E::table::${table2.entityResponseData?.['fullyQualifiedName']}::columns::${table2.entity?.columns[3].name}>`,
|
||||
@ -51,6 +64,15 @@ test.beforeAll(async ({ browser }) => {
|
||||
],
|
||||
testDefinition: 'columnValuesToBeInSet',
|
||||
});
|
||||
|
||||
// Create test tags and glossary terms
|
||||
await testClassification.create(apiContext);
|
||||
await testTag1.create(apiContext);
|
||||
await testTag2.create(apiContext);
|
||||
await testGlossary.create(apiContext);
|
||||
await testGlossaryTerm1.create(apiContext);
|
||||
await testGlossaryTerm2.create(apiContext);
|
||||
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
@ -58,6 +80,15 @@ test.afterAll(async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
await table1.delete(apiContext);
|
||||
await table2.delete(apiContext);
|
||||
|
||||
// Clean up test tags and glossary terms
|
||||
await testGlossaryTerm1.delete(apiContext);
|
||||
await testGlossaryTerm2.delete(apiContext);
|
||||
await testGlossary.delete(apiContext);
|
||||
await testTag1.delete(apiContext);
|
||||
await testTag2.delete(apiContext);
|
||||
await testClassification.delete(apiContext);
|
||||
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
@ -90,6 +121,35 @@ test('Table test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
|
||||
NEW_TABLE_TEST_CASE.field
|
||||
);
|
||||
await page.locator(descriptionBox).fill(NEW_TABLE_TEST_CASE.description);
|
||||
|
||||
// Add tags to test case
|
||||
await page.click('[data-testid="tags-selector"] input');
|
||||
const tagsSearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=tag_search_index*`
|
||||
);
|
||||
await page.fill('[data-testid="tags-selector"] input', testTag1.data.name);
|
||||
await tagsSearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testTag1.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
// Add glossary terms to test case
|
||||
await page.click('[data-testid="glossary-terms-selector"] input');
|
||||
const glossarySearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=glossary_term_search_index*`
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="glossary-terms-selector"] input',
|
||||
testGlossaryTerm1.data.name
|
||||
);
|
||||
await glossarySearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testGlossaryTerm1.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
|
||||
await page.click('[data-testid="submit-test"]');
|
||||
|
||||
await page.waitForSelector('[data-testid="success-line"]');
|
||||
@ -138,6 +198,43 @@ test('Table test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
|
||||
await page.waitForSelector('.ant-modal-title');
|
||||
await page.locator('#tableTestForm_params_columnName').clear();
|
||||
await page.fill('#tableTestForm_params_columnName', 'new_column_name');
|
||||
|
||||
// Remove existing tag and add new one
|
||||
await page.click(
|
||||
`[data-testid="selected-tag-${testTag1.responseData.fullyQualifiedName}"] svg`
|
||||
);
|
||||
|
||||
await page.click('[data-testid="tags-selector"] input');
|
||||
const newTagsSearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=tag_search_index*`
|
||||
);
|
||||
await page.fill('[data-testid="tags-selector"] input', testTag2.data.name);
|
||||
await newTagsSearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testTag2.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
|
||||
// Remove existing glossary term and add new one
|
||||
await page.click(
|
||||
`[data-testid="glossary-terms-selector"] [data-testid="remove-tags"]`
|
||||
);
|
||||
await page.click('[data-testid="glossary-terms-selector"] input');
|
||||
const newGlossarySearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=glossary_term_search_index*`
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="glossary-terms-selector"] input',
|
||||
testGlossaryTerm2.data.name
|
||||
);
|
||||
await newGlossarySearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testGlossaryTerm2.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
|
||||
const updateTestCaseResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/*'
|
||||
);
|
||||
@ -197,6 +294,35 @@ test('Column test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
|
||||
);
|
||||
await page.locator(descriptionBox).fill(NEW_COLUMN_TEST_CASE.description);
|
||||
|
||||
// Add tags to column test case
|
||||
await page.click('[data-testid="tags-selector"] input');
|
||||
const columnTagsSearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=tag_search_index*`
|
||||
);
|
||||
await page.fill('[data-testid="tags-selector"] input', testTag1.data.name);
|
||||
await columnTagsSearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testTag1.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
|
||||
// Add glossary terms to column test case
|
||||
await page.click('[data-testid="glossary-terms-selector"] input');
|
||||
const columnGlossarySearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=glossary_term_search_index*`
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="glossary-terms-selector"] input',
|
||||
testGlossaryTerm1.data.name
|
||||
);
|
||||
await columnGlossarySearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testGlossaryTerm1.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
|
||||
await page.click('[data-testid="submit-test"]');
|
||||
await page.waitForSelector('[data-testid="success-line"]');
|
||||
|
||||
@ -222,6 +348,42 @@ test('Column test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
|
||||
await page.waitForSelector('#tableTestForm_params_minLength');
|
||||
await page.locator('#tableTestForm_params_minLength').clear();
|
||||
await page.fill('#tableTestForm_params_minLength', '4');
|
||||
|
||||
// Remove existing tag and add new one for column test case
|
||||
await page.click(
|
||||
`[data-testid="selected-tag-${testTag1.responseData.fullyQualifiedName}"] svg`
|
||||
);
|
||||
await page.click('[data-testid="tags-selector"] input');
|
||||
const columnNewTagsSearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=tag_search_index*`
|
||||
);
|
||||
await page.fill('[data-testid="tags-selector"] input', testTag2.data.name);
|
||||
await columnNewTagsSearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testTag2.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
|
||||
// Remove existing glossary term and add new one for column test case
|
||||
await page.click(
|
||||
`[data-testid="glossary-terms-selector"] [data-testid="remove-tags"]`
|
||||
);
|
||||
await page.click('[data-testid="glossary-terms-selector"] input');
|
||||
const columnNewGlossarySearchResponse = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*index=glossary_term_search_index*`
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="glossary-terms-selector"] input',
|
||||
testGlossaryTerm2.data.name
|
||||
);
|
||||
await columnNewGlossarySearchResponse;
|
||||
await page
|
||||
.getByTestId(`tag-${testGlossaryTerm2.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
|
||||
await clickOutside(page);
|
||||
|
||||
const updateTestCaseResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/*'
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ import { ReactNode } from 'react';
|
||||
import { CreateTestCase } from '../../../generated/api/tests/createTestCase';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import { IngestionPipeline } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
|
||||
import { TestCase } from '../../../generated/tests/testCase';
|
||||
import { TagLabel, TestCase } from '../../../generated/tests/testCase';
|
||||
import { TestDefinition } from '../../../generated/tests/testDefinition';
|
||||
import { TestSuite } from '../../../generated/tests/testSuite';
|
||||
import { ListTestCaseParamsBySearch } from '../../../rest/testAPI';
|
||||
@ -91,4 +91,6 @@ export type TestCaseFormType = {
|
||||
testTypeId: string;
|
||||
computePassedFailedRowCount?: boolean;
|
||||
description?: string;
|
||||
tags?: TagLabel[];
|
||||
glossaryTerms?: TagLabel[];
|
||||
};
|
||||
|
@ -12,6 +12,7 @@
|
||||
*/
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { LabelType, State, TagSource } from '../../../generated/type/tagLabel';
|
||||
import {
|
||||
MOCK_TEST_CASE,
|
||||
MOCK_TEST_DEFINITION_COLUMN_VALUES_TO_MATCH_REGEX,
|
||||
@ -40,6 +41,14 @@ jest.mock('../../common/RichTextEditor/RichTextEditor', () => {
|
||||
jest.mock('./components/ParameterForm', () => {
|
||||
return jest.fn().mockImplementation(() => <div>ParameterForm.component</div>);
|
||||
});
|
||||
jest.mock('../../../pages/TasksPage/shared/TagSuggestion', () =>
|
||||
jest.fn().mockImplementation(({ children, ...props }) => (
|
||||
<div data-testid={props.selectProps?.['data-testid']}>
|
||||
TagSuggestion Component
|
||||
{children}
|
||||
</div>
|
||||
))
|
||||
);
|
||||
jest.mock('../../../rest/testAPI', () => {
|
||||
return {
|
||||
getTestCaseByFqn: jest
|
||||
@ -149,4 +158,292 @@ describe('EditTestCaseModal Component', () => {
|
||||
|
||||
expect(screen.queryByText('ParameterForm.component')).toBeNull();
|
||||
});
|
||||
|
||||
// Tags and Glossary Terms functionality tests
|
||||
it('should render tags and glossary terms fields', async () => {
|
||||
render(<EditTestCaseModal {...mockProps} />);
|
||||
|
||||
// Check if tags field is rendered
|
||||
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
|
||||
|
||||
// Check if glossary terms field is rendered
|
||||
expect(
|
||||
await screen.findByTestId('glossary-terms-selector')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Verify TagSuggestion components are rendered
|
||||
const tagComponents = screen.getAllByText('TagSuggestion Component');
|
||||
|
||||
expect(tagComponents).toHaveLength(2); // One for tags, one for glossary terms
|
||||
});
|
||||
|
||||
it('should separate tags and glossary terms correctly', async () => {
|
||||
const mockTestCaseWithTags = {
|
||||
...MOCK_TEST_CASE[0],
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PersonalData.Email',
|
||||
source: TagSource.Glossary,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const propsWithTags = {
|
||||
...mockProps,
|
||||
testCase: mockTestCaseWithTags,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...propsWithTags} />);
|
||||
|
||||
// Verify that both tag fields are rendered
|
||||
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('glossary-terms-selector')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle test case with no tags gracefully', async () => {
|
||||
const mockTestCaseWithoutTags = {
|
||||
...MOCK_TEST_CASE[0],
|
||||
tags: undefined,
|
||||
};
|
||||
|
||||
const propsWithoutTags = {
|
||||
...mockProps,
|
||||
testCase: mockTestCaseWithoutTags,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...propsWithoutTags} />);
|
||||
|
||||
// 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 handle test case with empty tags array', async () => {
|
||||
const mockTestCaseWithEmptyTags = {
|
||||
...MOCK_TEST_CASE[0],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const propsWithEmptyTags = {
|
||||
...mockProps,
|
||||
testCase: mockTestCaseWithEmptyTags,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...propsWithEmptyTags} />);
|
||||
|
||||
// Should render tag fields with empty arrays
|
||||
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('glossary-terms-selector')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render tags and glossary terms in parameter-only mode', async () => {
|
||||
const parameterOnlyProps = {
|
||||
...mockProps,
|
||||
showOnlyParameter: true,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...parameterOnlyProps} />);
|
||||
|
||||
// Should not render tag fields when showOnlyParameter is true
|
||||
expect(screen.queryByTestId('tags-selector')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('glossary-terms-selector')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should include tags and glossary terms in form submission', async () => {
|
||||
render(<EditTestCaseModal {...mockProps} />);
|
||||
|
||||
// Wait for form to load
|
||||
expect(await screen.findByTestId('edit-test-form')).toBeInTheDocument();
|
||||
|
||||
// Submit the form
|
||||
const submitBtn = await screen.findByText('label.submit');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(submitBtn);
|
||||
});
|
||||
|
||||
// Verify that onUpdate was called (indicating form submission)
|
||||
expect(mockProps.onUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Tier tag filtering tests
|
||||
it('should filter out tier tags from displayed tags', async () => {
|
||||
const mockTestCaseWithTierTag = {
|
||||
...MOCK_TEST_CASE[0],
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'Tier.Tier1',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PersonalData.Email',
|
||||
source: TagSource.Glossary,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const propsWithTierTag = {
|
||||
...mockProps,
|
||||
testCase: mockTestCaseWithTierTag,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...propsWithTierTag} />);
|
||||
|
||||
// Verify that tag fields are rendered
|
||||
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('glossary-terms-selector')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// The tier tag should be filtered out and not displayed in the form
|
||||
// But it should be preserved when updating
|
||||
});
|
||||
|
||||
it('should preserve tier tags when updating test case', async () => {
|
||||
const mockUpdateTestCaseById = jest.fn().mockResolvedValue({});
|
||||
jest.doMock('../../../rest/testAPI', () => ({
|
||||
...jest.requireActual('../../../rest/testAPI'),
|
||||
updateTestCaseById: mockUpdateTestCaseById,
|
||||
}));
|
||||
|
||||
const mockTestCaseWithTierTag = {
|
||||
...MOCK_TEST_CASE[0],
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'Tier.Tier2',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const propsWithTierTag = {
|
||||
...mockProps,
|
||||
testCase: mockTestCaseWithTierTag,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...propsWithTierTag} />);
|
||||
|
||||
// Wait for form to load
|
||||
expect(await screen.findByTestId('edit-test-form')).toBeInTheDocument();
|
||||
|
||||
// Submit the form
|
||||
const submitBtn = await screen.findByText('label.submit');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(submitBtn);
|
||||
});
|
||||
|
||||
// The tier tag should be preserved in the update
|
||||
expect(mockProps.onUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle multiple tier tags correctly', async () => {
|
||||
const mockTestCaseWithMultipleTierTags = {
|
||||
...MOCK_TEST_CASE[0],
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'Tier.Tier1',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'Tier.Tier2', // This should not happen in practice, but test it
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const propsWithMultipleTierTags = {
|
||||
...mockProps,
|
||||
testCase: mockTestCaseWithMultipleTierTags,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...propsWithMultipleTierTags} />);
|
||||
|
||||
// Should still render properly
|
||||
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should work correctly when no tier tags are present', async () => {
|
||||
const mockTestCaseWithoutTierTag = {
|
||||
...MOCK_TEST_CASE[0],
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PersonalData.Email',
|
||||
source: TagSource.Glossary,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const propsWithoutTierTag = {
|
||||
...mockProps,
|
||||
testCase: mockTestCaseWithoutTierTag,
|
||||
};
|
||||
|
||||
render(<EditTestCaseModal {...propsWithoutTierTag} />);
|
||||
|
||||
// Wait for form to load
|
||||
expect(await screen.findByTestId('edit-test-form')).toBeInTheDocument();
|
||||
|
||||
// Submit the form
|
||||
const submitBtn = await screen.findByText('label.submit');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(submitBtn);
|
||||
});
|
||||
|
||||
// Should work normally without tier tags
|
||||
expect(mockProps.onUpdate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { ENTITY_NAME_REGEX } from '../../../constants/regex.constants';
|
||||
import { TABLE_DIFF } from '../../../constants/TestSuite.constant';
|
||||
import { EntityType, TabSpecificField } from '../../../enums/entity.enum';
|
||||
import { TagSource } from '../../../generated/api/domains/createDataProduct';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import {
|
||||
TestDataType,
|
||||
@ -46,6 +47,8 @@ import {
|
||||
import { getEntityFQN } from '../../../utils/FeedUtils';
|
||||
import { generateFormFields } from '../../../utils/formUtils';
|
||||
import { isValidJSONString } from '../../../utils/StringsUtils';
|
||||
import { getFilterTags } from '../../../utils/TableTags/TableTags.utils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { EntityAttachmentProvider } from '../../common/EntityDescription/EntityAttachmentProvider/EntityAttachmentProvider';
|
||||
import Loader from '../../common/Loader/Loader';
|
||||
@ -103,6 +106,24 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
return <></>;
|
||||
}, [selectedDefinition, table]);
|
||||
|
||||
const { tags, glossaryTerms, tierTag } = useMemo(() => {
|
||||
if (!testCase?.tags) {
|
||||
return { tags: [], glossaryTerms: [], tierTag: null };
|
||||
}
|
||||
|
||||
// First extract tier tag
|
||||
const tierTag = getTierTags(testCase.tags);
|
||||
// Filter out tier tags before processing
|
||||
const tagsWithoutTier = getTagsWithoutTier(testCase.tags);
|
||||
const filteredTags = getFilterTags(tagsWithoutTier);
|
||||
|
||||
return {
|
||||
tags: filteredTags.Classification,
|
||||
glossaryTerms: filteredTags.Glossary,
|
||||
tierTag,
|
||||
};
|
||||
}, [testCase?.tags]);
|
||||
|
||||
const handleFormSubmit: FormProps['onFinish'] = async (value) => {
|
||||
const updatedTestCase = {
|
||||
...testCase,
|
||||
@ -118,6 +139,13 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
computePassedFailedRowCount: isComputeRowCountFieldVisible
|
||||
? value.computePassedFailedRowCount
|
||||
: testCase?.computePassedFailedRowCount,
|
||||
tags: showOnlyParameter
|
||||
? testCase.tags
|
||||
: [
|
||||
...(tierTag ? [tierTag] : []),
|
||||
...(value.tags ?? []),
|
||||
...(value.glossaryTerms ?? []),
|
||||
],
|
||||
};
|
||||
const jsonPatch = compare(testCase, updatedTestCase);
|
||||
|
||||
@ -206,6 +234,8 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
params: getParamsValue(definition),
|
||||
table: getNameFromFQN(tableFqn),
|
||||
column: getColumnNameFromEntityLink(testCase?.entityLink),
|
||||
tags: tags,
|
||||
glossaryTerms: glossaryTerms,
|
||||
...formValue,
|
||||
});
|
||||
setSelectedDefinition(definition);
|
||||
@ -216,22 +246,57 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const descriptionField: FieldProp = useMemo(
|
||||
() => ({
|
||||
name: 'description',
|
||||
required: false,
|
||||
label: t('label.description'),
|
||||
id: 'root/description',
|
||||
type: FieldTypes.DESCRIPTION,
|
||||
props: {
|
||||
'data-testid': 'description',
|
||||
initialValue: testCase?.description ?? '',
|
||||
style: {
|
||||
margin: 0,
|
||||
const formField: FieldProp[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'description',
|
||||
required: false,
|
||||
label: t('label.description'),
|
||||
id: 'root/description',
|
||||
type: FieldTypes.DESCRIPTION,
|
||||
props: {
|
||||
'data-testid': 'description',
|
||||
initialValue: testCase?.description ?? '',
|
||||
style: {
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[testCase?.description]
|
||||
{
|
||||
name: 'tags',
|
||||
required: false,
|
||||
label: t('label.tag-plural'),
|
||||
id: 'root/tags',
|
||||
type: FieldTypes.TAG_SUGGESTION,
|
||||
props: {
|
||||
selectProps: {
|
||||
'data-testid': 'tags-selector',
|
||||
},
|
||||
initialValue: tags,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'glossaryTerms',
|
||||
required: false,
|
||||
label: t('label.glossary-term-plural'),
|
||||
id: 'root/glossaryTerms',
|
||||
type: FieldTypes.TAG_SUGGESTION,
|
||||
props: {
|
||||
selectProps: {
|
||||
'data-testid': 'glossary-terms-selector',
|
||||
},
|
||||
initialValue: glossaryTerms,
|
||||
open: false,
|
||||
hasNoActionButtons: true,
|
||||
isTreeSelect: true,
|
||||
tagType: TagSource.Glossary,
|
||||
placeholder: t('label.select-field', {
|
||||
field: t('label.glossary-term-plural'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
[testCase?.description, tags, glossaryTerms]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -340,7 +405,7 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
|
||||
{!showOnlyParameter && (
|
||||
<>
|
||||
{generateFormFields([descriptionField])}
|
||||
{generateFormFields(formField)}
|
||||
{isComputeRowCountFieldVisible
|
||||
? generateFormFields(formFields)
|
||||
: null}
|
||||
|
@ -10,16 +10,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
findByRole,
|
||||
render,
|
||||
screen,
|
||||
waitForElement,
|
||||
} from '@testing-library/react';
|
||||
import { findByRole, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ProfilerDashboardType } from '../../../../enums/table.enum';
|
||||
import {
|
||||
LabelType,
|
||||
State,
|
||||
TagSource,
|
||||
} from '../../../../generated/type/tagLabel';
|
||||
import { MOCK_TABLE } from '../../../../mocks/TableData.mock';
|
||||
import { getListTestDefinitions } from '../../../../rest/testAPI';
|
||||
import TestCaseForm from './TestCaseForm';
|
||||
@ -105,6 +105,14 @@ jest.mock('./ParameterForm', () =>
|
||||
jest.mock('crypto-random-string-with-promisify-polyfill', () =>
|
||||
jest.fn().mockImplementation(() => '4B3B')
|
||||
);
|
||||
jest.mock('../../../../pages/TasksPage/shared/TagSuggestion', () =>
|
||||
jest.fn().mockImplementation(({ children, ...props }) => (
|
||||
<div data-testid={props.selectProps?.['data-testid']}>
|
||||
TagSuggestion Component
|
||||
{children}
|
||||
</div>
|
||||
))
|
||||
);
|
||||
|
||||
describe('TestCaseForm', () => {
|
||||
it('should render component', async () => {
|
||||
@ -152,17 +160,15 @@ describe('TestCaseForm', () => {
|
||||
'combobox'
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.click(typeSelector);
|
||||
await userEvent.click(typeSelector);
|
||||
});
|
||||
|
||||
expect(typeSelector).toBeInTheDocument();
|
||||
|
||||
await waitForElement(() =>
|
||||
screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
await waitFor(() => screen.getByText('Column Value Lengths To Be Between'));
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(
|
||||
await userEvent.click(
|
||||
await screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
});
|
||||
@ -183,6 +189,7 @@ describe('TestCaseForm', () => {
|
||||
entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>',
|
||||
name: 'dim_address_column_value_lengths_to_be_between_4B3B',
|
||||
parameterValues: [],
|
||||
tags: [],
|
||||
testDefinition: 'columnValueLengthsToBeBetween',
|
||||
});
|
||||
});
|
||||
@ -223,15 +230,15 @@ describe('TestCaseForm', () => {
|
||||
'combobox'
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.click(column);
|
||||
await userEvent.click(column);
|
||||
});
|
||||
|
||||
expect(column).toBeInTheDocument();
|
||||
|
||||
await waitForElement(() => screen.findByText('last_name'));
|
||||
await waitFor(() => screen.getByText('last_name'));
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(await screen.findByText('last_name'));
|
||||
await userEvent.click(await screen.findByText('last_name'));
|
||||
});
|
||||
|
||||
expect(mockUseHistory.push).toHaveBeenCalledWith({
|
||||
@ -258,17 +265,15 @@ describe('TestCaseForm', () => {
|
||||
'combobox'
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.click(typeSelector);
|
||||
await userEvent.click(typeSelector);
|
||||
});
|
||||
|
||||
expect(typeSelector).toBeInTheDocument();
|
||||
|
||||
await waitForElement(() =>
|
||||
screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
await waitFor(() => screen.getByText('Column Value Lengths To Be Between'));
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(
|
||||
await userEvent.click(
|
||||
await screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
});
|
||||
@ -277,4 +282,205 @@ describe('TestCaseForm', () => {
|
||||
await screen.findByTestId('compute-passed-failed-row-count')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Tags and Glossary Terms functionality tests
|
||||
it('should render tags and glossary terms fields', async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
// Check if tags field is rendered
|
||||
expect(await screen.findByTestId('tags-selector')).toBeInTheDocument();
|
||||
|
||||
// Check if glossary terms field is rendered
|
||||
expect(
|
||||
await screen.findByTestId('glossary-terms-selector')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Verify TagSuggestion components are rendered
|
||||
const tagComponents = screen.getAllByText('TagSuggestion Component');
|
||||
|
||||
expect(tagComponents).toHaveLength(2); // One for tags, one for glossary terms
|
||||
});
|
||||
|
||||
it('should include both tags and glossary terms in form submission', async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
// Select a test type first
|
||||
const typeSelector = await findByRole(
|
||||
await screen.findByTestId('test-type'),
|
||||
'combobox'
|
||||
);
|
||||
await act(async () => {
|
||||
await userEvent.click(typeSelector);
|
||||
});
|
||||
|
||||
await waitFor(() => screen.getByText('Column Value Lengths To Be Between'));
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(
|
||||
await screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
});
|
||||
|
||||
// Submit form
|
||||
const submitBtn = await screen.findByTestId('submit-test');
|
||||
await act(async () => {
|
||||
submitBtn.click();
|
||||
});
|
||||
|
||||
// Verify that onSubmit is called with tags array (empty in this case since no tags selected)
|
||||
expect(mockProps.onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tags: [], // Will be empty when no tags/glossary terms are selected
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine tags and glossary terms when creating test case object', async () => {
|
||||
const mockTags = [
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
];
|
||||
|
||||
const mockWithValues = {
|
||||
...mockProps,
|
||||
initialValue: {
|
||||
tags: mockTags,
|
||||
name: 'test-case',
|
||||
testDefinition: 'columnValueLengthsToBeBetween',
|
||||
entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>',
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockWithValues} />);
|
||||
});
|
||||
|
||||
// Submit without changing anything to test initial value handling
|
||||
const submitBtn = await screen.findByTestId('submit-test');
|
||||
await act(async () => {
|
||||
submitBtn.click();
|
||||
});
|
||||
|
||||
// Should include tags from initial values
|
||||
expect(mockProps.onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tags: expect.any(Array), // Should contain the combined tags
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize tags field with initial values when editing', async () => {
|
||||
const mockInitialTags = [
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PersonalData.Email',
|
||||
source: TagSource.Glossary,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
];
|
||||
|
||||
const mockWithTags = {
|
||||
...mockProps,
|
||||
initialValue: {
|
||||
tags: mockInitialTags,
|
||||
name: 'existing-test-case',
|
||||
testDefinition: 'columnValueLengthsToBeBetween',
|
||||
entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>',
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockWithTags} />);
|
||||
});
|
||||
|
||||
// Verify that the tags field components are rendered
|
||||
const tagsField = await screen.findByTestId('tags-selector');
|
||||
|
||||
expect(tagsField).toBeInTheDocument();
|
||||
|
||||
const glossaryTermsField = await screen.findByTestId(
|
||||
'glossary-terms-selector'
|
||||
);
|
||||
|
||||
expect(glossaryTermsField).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle empty tags and glossary terms gracefully', async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
// Select test type and submit form without any tags or glossary terms
|
||||
const typeSelector = await findByRole(
|
||||
await screen.findByTestId('test-type'),
|
||||
'combobox'
|
||||
);
|
||||
await act(async () => {
|
||||
await userEvent.click(typeSelector);
|
||||
});
|
||||
|
||||
await waitFor(() => screen.getByText('Column Value Lengths To Be Between'));
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(
|
||||
await screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
});
|
||||
|
||||
const submitBtn = await screen.findByTestId('submit-test');
|
||||
await act(async () => {
|
||||
submitBtn.click();
|
||||
});
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tags: [], // Should be empty array when no tags are selected
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should render glossary terms field with correct props', async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
const glossaryTermsField = await screen.findByTestId(
|
||||
'glossary-terms-selector'
|
||||
);
|
||||
|
||||
expect(glossaryTermsField).toBeInTheDocument();
|
||||
|
||||
// Verify that the glossary terms field has the expected data-testid
|
||||
expect(glossaryTermsField).toHaveAttribute(
|
||||
'data-testid',
|
||||
'glossary-terms-selector'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render tags field with correct props', async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
const tagsField = await screen.findByTestId('tags-selector');
|
||||
|
||||
expect(tagsField).toBeInTheDocument();
|
||||
|
||||
// Verify that the tags field has the expected data-testid
|
||||
expect(tagsField).toHaveAttribute('data-testid', 'tags-selector');
|
||||
});
|
||||
});
|
||||
|
@ -31,6 +31,7 @@ import { useHistory, useParams } from 'react-router-dom';
|
||||
import { PAGE_SIZE_LARGE } from '../../../../constants/constants';
|
||||
import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants';
|
||||
import { ProfilerDashboardType } from '../../../../enums/table.enum';
|
||||
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
|
||||
import { CreateTestCase } from '../../../../generated/api/tests/createTestCase';
|
||||
import { TestCase } from '../../../../generated/tests/testCase';
|
||||
import {
|
||||
@ -188,6 +189,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
),
|
||||
testDefinition: value.testTypeId,
|
||||
description: isEmpty(value.description) ? undefined : value.description,
|
||||
tags: [...(value.tags ?? []), ...(value.glossaryTerms ?? [])],
|
||||
...testCaseClassBase.getCreateTestCaseObject(value, selectedDefinition),
|
||||
};
|
||||
};
|
||||
@ -234,22 +236,55 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const descriptionField: FieldProp = useMemo(
|
||||
() => ({
|
||||
name: 'description',
|
||||
required: false,
|
||||
label: t('label.description'),
|
||||
id: 'root/description',
|
||||
type: FieldTypes.DESCRIPTION,
|
||||
props: {
|
||||
'data-testid': 'description',
|
||||
initialValue: initialValue?.description ?? '',
|
||||
style: {
|
||||
margin: 0,
|
||||
const formField: FieldProp[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'description',
|
||||
required: false,
|
||||
label: t('label.description'),
|
||||
id: 'root/description',
|
||||
type: FieldTypes.DESCRIPTION,
|
||||
props: {
|
||||
'data-testid': 'description',
|
||||
initialValue: initialValue?.description ?? '',
|
||||
style: {
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[initialValue?.description]
|
||||
{
|
||||
name: 'tags',
|
||||
required: false,
|
||||
label: t('label.tag-plural'),
|
||||
id: 'root/tags',
|
||||
type: FieldTypes.TAG_SUGGESTION,
|
||||
props: {
|
||||
selectProps: {
|
||||
'data-testid': 'tags-selector',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'glossaryTerms',
|
||||
required: false,
|
||||
label: t('label.glossary-term-plural'),
|
||||
id: 'root/glossaryTerms',
|
||||
type: FieldTypes.TAG_SUGGESTION,
|
||||
props: {
|
||||
selectProps: {
|
||||
'data-testid': 'glossary-terms-selector',
|
||||
},
|
||||
open: false,
|
||||
hasNoActionButtons: true,
|
||||
isTreeSelect: true,
|
||||
tagType: TagSource.Glossary,
|
||||
placeholder: t('label.select-field', {
|
||||
field: t('label.glossary-term-plural'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
[initialValue?.description, initialValue?.tags]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -284,6 +319,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
? getParamsValue()
|
||||
: undefined,
|
||||
columnName: activeColumnFqn ? getNameFromFQN(activeColumnFqn) : undefined,
|
||||
tags: initialValue?.tags || [],
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -422,7 +458,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
{generateFormFields([descriptionField])}
|
||||
{generateFormFields(formField)}
|
||||
|
||||
{isComputeRowCountFieldVisible ? generateFormFields(formFields) : null}
|
||||
|
||||
|
@ -21,24 +21,33 @@ import { useTranslation } from 'react-i18next';
|
||||
import { CSMode } from '../../../../enums/codemirror.enum';
|
||||
import { EntityType } from '../../../../enums/entity.enum';
|
||||
|
||||
import { EntityTags } from 'Models';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ReactComponent as StarIcon } from '../../../../assets/svg/ic-suggestions.svg';
|
||||
import { EntityField } from '../../../../constants/Feeds.constants';
|
||||
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
|
||||
import {
|
||||
ChangeDescription,
|
||||
TagLabel,
|
||||
TestCaseParameterValue,
|
||||
} from '../../../../generated/tests/testCase';
|
||||
import { useTestCaseStore } from '../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store';
|
||||
import { updateTestCaseById } from '../../../../rest/testAPI';
|
||||
import {
|
||||
getEntityVersionByField,
|
||||
getEntityVersionTags,
|
||||
getParameterValueDiffDisplay,
|
||||
} from '../../../../utils/EntityVersionUtils';
|
||||
import { VersionEntityTypes } from '../../../../utils/EntityVersionUtils.interface';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../../../utils/TableUtils';
|
||||
import { createTagObject } from '../../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
|
||||
import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1';
|
||||
import { EditIconButton } from '../../../common/IconButtons/EditIconButton';
|
||||
import TestSummary from '../../../Database/Profiler/TestSummary/TestSummary';
|
||||
import SchemaEditor from '../../../Database/SchemaEditor/SchemaEditor';
|
||||
import TagsContainerV2 from '../../../Tag/TagsContainerV2/TagsContainerV2';
|
||||
import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface';
|
||||
import EditTestCaseModal from '../../AddDataQualityTest/EditTestCaseModal';
|
||||
import '../incident-manager.style.less';
|
||||
import './test-case-result-tab.style.less';
|
||||
@ -51,6 +60,7 @@ const TestCaseResultTab = () => {
|
||||
setTestCase,
|
||||
showAILearningBanner,
|
||||
testCasePermission,
|
||||
isTabExpanded,
|
||||
} = useTestCaseStore();
|
||||
const { version } = useParams<{ version: string }>();
|
||||
const isVersionPage = !isUndefined(version);
|
||||
@ -58,16 +68,28 @@ const TestCaseResultTab = () => {
|
||||
testCaseResultTabClassBase.getAdditionalComponents();
|
||||
const [isParameterEdit, setIsParameterEdit] = useState<boolean>(false);
|
||||
|
||||
const { hasEditPermission, hasEditDescriptionPermission } = useMemo(() => {
|
||||
const {
|
||||
hasEditPermission,
|
||||
hasEditDescriptionPermission,
|
||||
hasEditTagsPermission,
|
||||
hasEditGlossaryTermsPermission,
|
||||
} = useMemo(() => {
|
||||
return isVersionPage
|
||||
? {
|
||||
hasEditPermission: false,
|
||||
hasEditDescriptionPermission: false,
|
||||
hasEditTagsPermission: false,
|
||||
hasEditGlossaryTermsPermission: false,
|
||||
}
|
||||
: {
|
||||
hasEditPermission: testCasePermission?.EditAll,
|
||||
hasEditDescriptionPermission:
|
||||
testCasePermission?.EditAll || testCasePermission?.EditDescription,
|
||||
hasEditTagsPermission:
|
||||
testCasePermission?.EditAll || testCasePermission?.EditTags,
|
||||
hasEditGlossaryTermsPermission:
|
||||
testCasePermission?.EditAll ||
|
||||
testCasePermission?.EditGlossaryTerms,
|
||||
};
|
||||
}, [testCasePermission, isVersionPage]);
|
||||
|
||||
@ -91,6 +113,29 @@ const TestCaseResultTab = () => {
|
||||
);
|
||||
}, [testCaseData?.parameterValues]);
|
||||
|
||||
const handleTagSelection = async (selectedTags: EntityTags[]) => {
|
||||
if (!testCaseData) {
|
||||
return;
|
||||
}
|
||||
// Preserve tier tags
|
||||
const tierTag = getTierTags(testCaseData.tags ?? []);
|
||||
const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags);
|
||||
|
||||
const updatedTestCase = {
|
||||
...testCaseData,
|
||||
tags: [...(tierTag ? [tierTag] : []), ...(updatedTags ?? [])],
|
||||
};
|
||||
const jsonPatch = compare(testCaseData, updatedTestCase);
|
||||
if (jsonPatch.length) {
|
||||
try {
|
||||
const res = await updateTestCaseById(testCaseData.id ?? '', jsonPatch);
|
||||
setTestCase(res);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
async (description: string) => {
|
||||
if (testCaseData) {
|
||||
@ -145,6 +190,13 @@ const TestCaseResultTab = () => {
|
||||
isVersionPage,
|
||||
]);
|
||||
|
||||
const updatedTags = isVersionPage
|
||||
? getEntityVersionTags(
|
||||
testCaseData as VersionEntityTypes,
|
||||
testCaseData?.changeDescription as ChangeDescription
|
||||
)
|
||||
: getTagsWithoutTier(testCaseData?.tags ?? []);
|
||||
|
||||
const testCaseParams = useMemo(() => {
|
||||
if (isVersionPage) {
|
||||
return getParameterValueDiffDisplay(
|
||||
@ -191,64 +243,36 @@ const TestCaseResultTab = () => {
|
||||
|
||||
return (
|
||||
<Row
|
||||
className="p-lg test-case-result-tab"
|
||||
className="p-md test-case-result-tab"
|
||||
data-testid="test-case-result-tab-container"
|
||||
gutter={[0, 20]}>
|
||||
<Col span={24}>
|
||||
<DescriptionV1
|
||||
wrapInCard
|
||||
description={description}
|
||||
entityType={EntityType.TEST_CASE}
|
||||
hasEditAccess={hasEditDescriptionPermission}
|
||||
showCommentsIcon={false}
|
||||
onDescriptionUpdate={handleDescriptionChange}
|
||||
/>
|
||||
</Col>
|
||||
gutter={[20, 20]}>
|
||||
<Col className="transition-all-200ms" span={isTabExpanded ? 18 : 24}>
|
||||
<Row gutter={[0, 20]}>
|
||||
<Col span={24}>
|
||||
<DescriptionV1
|
||||
wrapInCard
|
||||
description={description}
|
||||
entityType={EntityType.TEST_CASE}
|
||||
hasEditAccess={hasEditDescriptionPermission}
|
||||
showCommentsIcon={false}
|
||||
onDescriptionUpdate={handleDescriptionChange}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col data-testid="parameter-container" span={24}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Space align="center" size={8}>
|
||||
<Typography.Text className="right-panel-label">
|
||||
{t('label.parameter-plural')}
|
||||
</Typography.Text>
|
||||
{hasEditPermission &&
|
||||
Boolean(
|
||||
testCaseData?.parameterValues?.length ||
|
||||
testCaseData?.useDynamicAssertion
|
||||
) && (
|
||||
<EditIconButton
|
||||
newLook
|
||||
data-testid="edit-parameter-icon"
|
||||
size="small"
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.parameter'),
|
||||
})}
|
||||
onClick={() => setIsParameterEdit(true)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{testCaseParams}
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
{!isUndefined(withSqlParams) && !isVersionPage ? (
|
||||
<Col>
|
||||
{withSqlParams.map((param) => (
|
||||
<Row
|
||||
className="sql-expression-container"
|
||||
data-testid="sql-expression-container"
|
||||
gutter={[8, 8]}
|
||||
key={param.name}>
|
||||
<Col span={24}>
|
||||
<Space align="center" size={8}>
|
||||
<Typography.Text className="right-panel-label">
|
||||
{startCase(param.name)}
|
||||
</Typography.Text>
|
||||
{hasEditPermission && (
|
||||
<Col data-testid="parameter-container" span={24}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Space align="center" size={8}>
|
||||
<Typography.Text className="right-panel-label">
|
||||
{t('label.parameter-plural')}
|
||||
</Typography.Text>
|
||||
{hasEditPermission &&
|
||||
Boolean(
|
||||
testCaseData?.parameterValues?.length ||
|
||||
testCaseData?.useDynamicAssertion
|
||||
) && (
|
||||
<EditIconButton
|
||||
newLook
|
||||
data-testid="edit-sql-param-icon"
|
||||
data-testid="edit-parameter-icon"
|
||||
size="small"
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.parameter'),
|
||||
@ -256,52 +280,116 @@ const TestCaseResultTab = () => {
|
||||
onClick={() => setIsParameterEdit(true)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<SchemaEditor
|
||||
className="custom-code-mirror-theme query-editor-min-h-60"
|
||||
editorClass="table-query-editor"
|
||||
mode={{ name: CSMode.SQL }}
|
||||
options={{
|
||||
styleActiveLine: false,
|
||||
readOnly: true,
|
||||
}}
|
||||
value={param.value ?? ''}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
) : null}
|
||||
</Space>
|
||||
|
||||
{showAILearningBanner &&
|
||||
testCaseData?.useDynamicAssertion &&
|
||||
AlertComponent && (
|
||||
<Col span={24}>
|
||||
<AlertComponent />
|
||||
{testCaseParams}
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{testCaseData && (
|
||||
<Col className="test-case-result-tab-graph" span={24}>
|
||||
<TestSummary data={testCaseData} />
|
||||
|
||||
{!isUndefined(withSqlParams) && !isVersionPage ? (
|
||||
<Col>
|
||||
{withSqlParams.map((param) => (
|
||||
<Row
|
||||
className="sql-expression-container"
|
||||
data-testid="sql-expression-container"
|
||||
gutter={[8, 8]}
|
||||
key={param.name}>
|
||||
<Col span={24}>
|
||||
<Space align="center" size={8}>
|
||||
<Typography.Text className="right-panel-label">
|
||||
{startCase(param.name)}
|
||||
</Typography.Text>
|
||||
{hasEditPermission && (
|
||||
<EditIconButton
|
||||
newLook
|
||||
data-testid="edit-sql-param-icon"
|
||||
size="small"
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.parameter'),
|
||||
})}
|
||||
onClick={() => setIsParameterEdit(true)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<SchemaEditor
|
||||
className="custom-code-mirror-theme query-editor-min-h-60"
|
||||
editorClass="table-query-editor"
|
||||
mode={{ name: CSMode.SQL }}
|
||||
options={{
|
||||
styleActiveLine: false,
|
||||
readOnly: true,
|
||||
}}
|
||||
value={param.value ?? ''}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
) : null}
|
||||
|
||||
{showAILearningBanner &&
|
||||
testCaseData?.useDynamicAssertion &&
|
||||
AlertComponent && (
|
||||
<Col span={24}>
|
||||
<AlertComponent />
|
||||
</Col>
|
||||
)}
|
||||
{testCaseData && (
|
||||
<Col className="test-case-result-tab-graph" span={24}>
|
||||
<TestSummary data={testCaseData} />
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{!isEmpty(additionalComponent) &&
|
||||
additionalComponent.map(({ Component, id }) => (
|
||||
<Component key={id} testCaseData={testCaseData} />
|
||||
))}
|
||||
|
||||
{testCaseData && isParameterEdit && (
|
||||
<EditTestCaseModal
|
||||
showOnlyParameter
|
||||
testCase={testCaseData}
|
||||
visible={isParameterEdit}
|
||||
onCancel={handleCancelParameter}
|
||||
onUpdate={setTestCase}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
{isTabExpanded && (
|
||||
<Col className="transition-all-200ms" span={6}>
|
||||
<Row gutter={[20, 20]}>
|
||||
<Col span={24}>
|
||||
<TagsContainerV2
|
||||
newLook
|
||||
displayType={DisplayType.READ_MORE}
|
||||
entityFqn={testCaseData?.fullyQualifiedName}
|
||||
entityType={EntityType.TEST_CASE}
|
||||
permission={hasEditTagsPermission ?? false}
|
||||
selectedTags={updatedTags ?? []}
|
||||
showTaskHandler={false}
|
||||
tagType={TagSource.Classification}
|
||||
onSelectionChange={handleTagSelection}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<TagsContainerV2
|
||||
newLook
|
||||
displayType={DisplayType.READ_MORE}
|
||||
entityFqn={testCaseData?.fullyQualifiedName}
|
||||
entityType={EntityType.TEST_CASE}
|
||||
permission={hasEditGlossaryTermsPermission ?? false}
|
||||
selectedTags={updatedTags ?? []}
|
||||
showTaskHandler={false}
|
||||
tagType={TagSource.Glossary}
|
||||
onSelectionChange={handleTagSelection}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{!isEmpty(additionalComponent) &&
|
||||
additionalComponent.map(({ Component, id }) => (
|
||||
<Component key={id} testCaseData={testCaseData} />
|
||||
))}
|
||||
|
||||
{testCaseData && isParameterEdit && (
|
||||
<EditTestCaseModal
|
||||
showOnlyParameter
|
||||
testCase={testCaseData}
|
||||
visible={isParameterEdit}
|
||||
onCancel={handleCancelParameter}
|
||||
onUpdate={setTestCase}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -18,7 +18,12 @@ import {
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestCase } from '../../../../generated/tests/testCase';
|
||||
import { TagLabel, TestCase } from '../../../../generated/tests/testCase';
|
||||
import {
|
||||
LabelType,
|
||||
State,
|
||||
TagSource,
|
||||
} from '../../../../generated/type/tagLabel';
|
||||
import { MOCK_PERMISSIONS } from '../../../../mocks/Glossary.mock';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils';
|
||||
import TestCaseResultTab from './TestCaseResultTab.component';
|
||||
@ -74,6 +79,7 @@ const mockUseTestCaseStore = {
|
||||
testCasePermission: MOCK_PERMISSIONS,
|
||||
setTestCasePermission: jest.fn(),
|
||||
setIsPermissionLoading: jest.fn(),
|
||||
isTabExpanded: false,
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@ -117,6 +123,27 @@ jest.mock('../../AddDataQualityTest/EditTestCaseModal', () => {
|
||||
));
|
||||
});
|
||||
|
||||
const mockUpdateTestCaseById = jest.fn();
|
||||
jest.mock('../../../../rest/testAPI', () => ({
|
||||
updateTestCaseById: jest
|
||||
.fn()
|
||||
.mockImplementation(() => mockUpdateTestCaseById()),
|
||||
}));
|
||||
|
||||
// Mock TagsContainerV2 to capture props
|
||||
const mockTagsContainerV2 = jest.fn();
|
||||
jest.mock('../../../Tag/TagsContainerV2/TagsContainerV2', () => {
|
||||
return jest.fn().mockImplementation((props) => {
|
||||
mockTagsContainerV2(props);
|
||||
|
||||
return (
|
||||
<div data-testid={`tags-container-${props.tagType}`}>
|
||||
TagsContainerV2 - {props.tagType}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestCaseResultTab', () => {
|
||||
it('Should render component', async () => {
|
||||
render(<TestCaseResultTab />);
|
||||
@ -243,4 +270,220 @@ describe('TestCaseResultTab', () => {
|
||||
mockTestCaseData.useDynamicAssertion = false;
|
||||
mockUseTestCaseStore.showAILearningBanner = false;
|
||||
});
|
||||
|
||||
// Tier tag tests
|
||||
describe('Tier tag filtering', () => {
|
||||
beforeEach(() => {
|
||||
mockTagsContainerV2.mockClear();
|
||||
mockUpdateTestCaseById.mockClear();
|
||||
mockUpdateTestCaseById.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it('should filter out tier tags from displayed tags in TagsContainerV2', async () => {
|
||||
const testCaseWithTierTag = {
|
||||
...mockTestCaseData,
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'Tier.Tier1',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PersonalData.Email',
|
||||
source: TagSource.Glossary,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockUseTestCaseStore.testCase = testCaseWithTierTag;
|
||||
mockUseTestCaseStore.isTabExpanded = true;
|
||||
|
||||
render(<TestCaseResultTab />);
|
||||
|
||||
// Wait for tags containers to render
|
||||
expect(
|
||||
await screen.findByTestId('tags-container-Classification')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('tags-container-Glossary')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Check that TagsContainerV2 is called with filtered tags (without tier tags)
|
||||
const classificationCall = mockTagsContainerV2.mock.calls.find(
|
||||
(call) => call[0].tagType === TagSource.Classification
|
||||
);
|
||||
const glossaryCall = mockTagsContainerV2.mock.calls.find(
|
||||
(call) => call[0].tagType === TagSource.Glossary
|
||||
);
|
||||
|
||||
expect(classificationCall).toBeDefined();
|
||||
expect(glossaryCall).toBeDefined();
|
||||
|
||||
// The selectedTags prop should not contain tier tags
|
||||
const selectedTags = classificationCall[0].selectedTags;
|
||||
|
||||
expect(selectedTags).toBeDefined();
|
||||
expect(
|
||||
selectedTags.some((tag: TagLabel) => tag.tagFQN.startsWith('Tier.'))
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve tier tags when updating tags', async () => {
|
||||
const testCaseWithTierTag = {
|
||||
...mockTestCaseData,
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'Tier.Tier2',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockUseTestCaseStore.testCase = testCaseWithTierTag;
|
||||
mockUseTestCaseStore.isTabExpanded = true;
|
||||
|
||||
render(<TestCaseResultTab />);
|
||||
|
||||
// Wait for tags container to render
|
||||
expect(
|
||||
await screen.findByTestId('tags-container-Classification')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Get the onSelectionChange handler
|
||||
const classificationCall = mockTagsContainerV2.mock.calls.find(
|
||||
(call) => call[0].tagType === TagSource.Classification
|
||||
);
|
||||
const onSelectionChange = classificationCall[0].onSelectionChange;
|
||||
|
||||
// Simulate tag selection change with a new tag
|
||||
const newTags = [
|
||||
{
|
||||
tagFQN: 'PII.NonSensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
];
|
||||
|
||||
await onSelectionChange(newTags);
|
||||
|
||||
// Verify updateTestCaseById was called
|
||||
expect(mockUpdateTestCaseById).toHaveBeenCalled();
|
||||
|
||||
// The tier tag should be preserved in the update
|
||||
// Note: The actual preservation logic is in the component
|
||||
});
|
||||
|
||||
it('should work correctly when no tier tags are present', async () => {
|
||||
const testCaseWithoutTierTag = {
|
||||
...mockTestCaseData,
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PersonalData.Email',
|
||||
source: TagSource.Glossary,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockUseTestCaseStore.testCase = testCaseWithoutTierTag;
|
||||
mockUseTestCaseStore.isTabExpanded = true;
|
||||
|
||||
render(<TestCaseResultTab />);
|
||||
|
||||
// Wait for tags containers to render
|
||||
expect(
|
||||
await screen.findByTestId('tags-container-Classification')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('tags-container-Glossary')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Should work normally without tier tags
|
||||
const classificationCall = mockTagsContainerV2.mock.calls.find(
|
||||
(call) => call[0].tagType === TagSource.Classification
|
||||
);
|
||||
|
||||
expect(classificationCall).toBeDefined();
|
||||
|
||||
// Should have both tags but no tier tags
|
||||
const allTags = classificationCall[0].selectedTags;
|
||||
|
||||
expect(allTags).toHaveLength(2); // PII.Sensitive and PersonalData.Email
|
||||
expect(
|
||||
allTags.some((tag: TagLabel) => tag.tagFQN.startsWith('Tier.'))
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should display only non-tier tags when test case has multiple tier tags', async () => {
|
||||
const testCaseWithMultipleTierTags = {
|
||||
...mockTestCaseData,
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'Tier.Tier1',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'Tier.Tier2', // This shouldn't happen in practice
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'PII.Sensitive',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockUseTestCaseStore.testCase = testCaseWithMultipleTierTags;
|
||||
mockUseTestCaseStore.isTabExpanded = true;
|
||||
|
||||
render(<TestCaseResultTab />);
|
||||
|
||||
// Wait for tags container to render
|
||||
expect(
|
||||
await screen.findByTestId('tags-container-Classification')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Check that TagsContainerV2 is called with filtered tags
|
||||
const classificationCall = mockTagsContainerV2.mock.calls.find(
|
||||
(call) => call[0].tagType === TagSource.Classification
|
||||
);
|
||||
|
||||
const selectedTags = classificationCall[0].selectedTags;
|
||||
|
||||
// Should only have the non-tier tag
|
||||
expect(selectedTags).toHaveLength(1);
|
||||
expect(selectedTags[0].tagFQN).toBe('PII.Sensitive');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -42,6 +42,10 @@ export interface TestCaseDefinitions {
|
||||
*/
|
||||
computePassedFailedRowCount?: boolean;
|
||||
parameterValues?: TestCaseParameterValue[];
|
||||
/**
|
||||
* Tags to apply
|
||||
*/
|
||||
tags?: TagLabel[];
|
||||
/**
|
||||
* Fully qualified name of the test definition.
|
||||
*/
|
||||
@ -69,6 +73,94 @@ export interface TestCaseParameterValue {
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This schema defines the type for labeling an entity with a Tag.
|
||||
*/
|
||||
export interface TagLabel {
|
||||
/**
|
||||
* Description for the tag label.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Display Name that identifies this tag.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Link to the tag resource.
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* Label type describes how a tag label was applied. 'Manual' indicates the tag label was
|
||||
* applied by a person. 'Derived' indicates a tag label was derived using the associated tag
|
||||
* relationship (see Classification.json for more details). 'Propagated` indicates a tag
|
||||
* label was propagated from upstream based on lineage. 'Automated' is used when a tool was
|
||||
* used to determine the tag label.
|
||||
*/
|
||||
labelType: LabelType;
|
||||
/**
|
||||
* Name of the tag or glossary term.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Label is from Tags or Glossary.
|
||||
*/
|
||||
source: TagSource;
|
||||
/**
|
||||
* 'Suggested' state is used when a tag label is suggested by users or tools. Owner of the
|
||||
* entity must confirm the suggested labels before it is marked as 'Confirmed'.
|
||||
*/
|
||||
state: State;
|
||||
style?: Style;
|
||||
tagFQN: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Label type describes how a tag label was applied. 'Manual' indicates the tag label was
|
||||
* applied by a person. 'Derived' indicates a tag label was derived using the associated tag
|
||||
* relationship (see Classification.json for more details). 'Propagated` indicates a tag
|
||||
* label was propagated from upstream based on lineage. 'Automated' is used when a tool was
|
||||
* used to determine the tag label.
|
||||
*/
|
||||
export enum LabelType {
|
||||
Automated = "Automated",
|
||||
Derived = "Derived",
|
||||
Generated = "Generated",
|
||||
Manual = "Manual",
|
||||
Propagated = "Propagated",
|
||||
}
|
||||
|
||||
/**
|
||||
* Label is from Tags or Glossary.
|
||||
*/
|
||||
export enum TagSource {
|
||||
Classification = "Classification",
|
||||
Glossary = "Glossary",
|
||||
}
|
||||
|
||||
/**
|
||||
* 'Suggested' state is used when a tag label is suggested by users or tools. Owner of the
|
||||
* entity must confirm the suggested labels before it is marked as 'Confirmed'.
|
||||
*/
|
||||
export enum State {
|
||||
Confirmed = "Confirmed",
|
||||
Suggested = "Suggested",
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Style is used to associate a color code and/or icon to entity to customize the look of
|
||||
* that entity in UI.
|
||||
*/
|
||||
export interface Style {
|
||||
/**
|
||||
* Hex Color Code to mark an entity such as GlossaryTerm, Tag, Domain or Data Product.
|
||||
*/
|
||||
color?: string;
|
||||
/**
|
||||
* An icon to associate with GlossaryTerm, Tag, Domain or Data Product.
|
||||
*/
|
||||
iconURL?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application Type
|
||||
*
|
||||
|
@ -85,6 +85,8 @@ const mockUseTestCase: UseTestCaseStoreInterface = {
|
||||
testCasePermission: MOCK_PERMISSIONS,
|
||||
setTestCasePermission: jest.fn(),
|
||||
setIsPermissionLoading: jest.fn(),
|
||||
isTabExpanded: false,
|
||||
setIsTabExpanded: jest.fn(),
|
||||
};
|
||||
jest.mock('./useTestCase.store', () => ({
|
||||
useTestCaseStore: jest.fn().mockImplementation(() => mockUseTestCase),
|
||||
|
@ -25,6 +25,7 @@ import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.sv
|
||||
import { withActivityFeed } from '../../../components/AppRouter/withActivityFeed';
|
||||
import ManageButton from '../../../components/common/EntityPageInfos/ManageButton/ManageButton';
|
||||
import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import { AlignRightIconButton } from '../../../components/common/IconButtons/EditIconButton';
|
||||
import Loader from '../../../components/common/Loader/Loader';
|
||||
import TitleBreadcrumb from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
|
||||
import { TitleBreadcrumbProps } from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface';
|
||||
@ -91,6 +92,8 @@ const IncidentManagerDetailPage = ({
|
||||
testCasePermission,
|
||||
setTestCasePermission,
|
||||
setIsPermissionLoading,
|
||||
isTabExpanded,
|
||||
setIsTabExpanded,
|
||||
} = useTestCaseStore();
|
||||
const [feedCount, setFeedCount] = useState<FeedCounts>(
|
||||
FEED_COUNT_INITIAL_DATA
|
||||
@ -112,6 +115,15 @@ const IncidentManagerDetailPage = ({
|
||||
};
|
||||
}, [testCasePermission]);
|
||||
|
||||
const isExpandViewSupported = useMemo(
|
||||
() => activeTab === TestCasePageTabs.TEST_CASE_RESULTS,
|
||||
[activeTab]
|
||||
);
|
||||
|
||||
const toggleTabExpanded = useCallback(() => {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
}, [isTabExpanded, setIsTabExpanded]);
|
||||
|
||||
const tabDetails: TabsProps['items'] = useMemo(() => {
|
||||
const tabs = testCaseClassBase.getTab(
|
||||
feedCount.openTaskCount,
|
||||
@ -418,6 +430,17 @@ const IncidentManagerDetailPage = ({
|
||||
className="tabs-new"
|
||||
data-testid="tabs"
|
||||
items={tabDetails}
|
||||
tabBarExtraContent={
|
||||
isExpandViewSupported && (
|
||||
<AlignRightIconButton
|
||||
className={isTabExpanded ? 'rotate-180' : ''}
|
||||
title={
|
||||
isTabExpanded ? t('label.collapse') : t('label.expand')
|
||||
}
|
||||
onClick={toggleTabExpanded}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -74,6 +74,7 @@ describe('TestCaseClassBase', () => {
|
||||
'testDefinition',
|
||||
'owners',
|
||||
'incidentId',
|
||||
'tags',
|
||||
];
|
||||
|
||||
const result = testCaseClassBase.getFields();
|
||||
|
@ -80,6 +80,7 @@ class TestCaseClassBase {
|
||||
TabSpecificField.TEST_DEFINITION,
|
||||
TabSpecificField.OWNERS,
|
||||
TabSpecificField.INCIDENT_ID,
|
||||
TabSpecificField.TAGS,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,8 @@ export interface UseTestCaseStoreInterface {
|
||||
reset: () => void;
|
||||
dqLineageData: EntityLineageResponse | undefined;
|
||||
setDqLineageData: (data: EntityLineageResponse | undefined) => void;
|
||||
isTabExpanded: boolean;
|
||||
setIsTabExpanded: (isTabExpanded: boolean) => void;
|
||||
}
|
||||
export const useTestCaseStore = create<UseTestCaseStoreInterface>()((set) => ({
|
||||
testCase: undefined,
|
||||
@ -39,6 +41,7 @@ export const useTestCaseStore = create<UseTestCaseStoreInterface>()((set) => ({
|
||||
isPermissionLoading: true,
|
||||
showAILearningBanner: false,
|
||||
testCasePermission: undefined,
|
||||
isTabExpanded: true,
|
||||
setTestCase: (testCase: TestCase) => {
|
||||
set({ testCase });
|
||||
},
|
||||
@ -59,7 +62,15 @@ export const useTestCaseStore = create<UseTestCaseStoreInterface>()((set) => ({
|
||||
setDqLineageData: (data: EntityLineageResponse | undefined) => {
|
||||
set({ dqLineageData: data });
|
||||
},
|
||||
setIsTabExpanded: (isTabExpanded: boolean) => {
|
||||
set({ isTabExpanded });
|
||||
},
|
||||
reset: () => {
|
||||
set({ testCase: undefined, isLoading: true, showAILearningBanner: false });
|
||||
set({
|
||||
testCase: undefined,
|
||||
isLoading: true,
|
||||
showAILearningBanner: false,
|
||||
isTabExpanded: true,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
@ -970,3 +970,9 @@ a[href].link-text-grey,
|
||||
margin-top: -3px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
// transition
|
||||
|
||||
.transition-all-200ms {
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user