mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-09 09:02:12 +00:00

* fetch all custom properties * fix database and schema filter aggregations * add playwright tests for custom props * fix tests * fix tests * wrap in try catch
513 lines
11 KiB
TypeScript
513 lines
11 KiB
TypeScript
/*
|
|
* Copyright 2024 Collate.
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
import { expect, Locator, Page } from '@playwright/test';
|
|
import { clickOutside } from './common';
|
|
import { getEncodedFqn } from './entity';
|
|
|
|
type EntityFields = {
|
|
id: string;
|
|
name: string;
|
|
localSearch: boolean;
|
|
skipConditions?: string[];
|
|
};
|
|
|
|
export const FIELDS: EntityFields[] = [
|
|
{
|
|
id: 'Owners',
|
|
name: 'owners.displayName.keyword',
|
|
localSearch: false,
|
|
},
|
|
{
|
|
id: 'Tags',
|
|
name: 'tags.tagFQN',
|
|
localSearch: false,
|
|
},
|
|
{
|
|
id: 'Tier',
|
|
name: 'tier.tagFQN',
|
|
localSearch: true,
|
|
},
|
|
{
|
|
id: 'Service',
|
|
name: 'service.displayName.keyword',
|
|
localSearch: false,
|
|
},
|
|
{
|
|
id: 'Database',
|
|
name: 'database.displayName.keyword',
|
|
localSearch: false,
|
|
},
|
|
{
|
|
id: 'Database Schema',
|
|
name: 'databaseSchema.displayName.keyword',
|
|
localSearch: false,
|
|
},
|
|
{
|
|
id: 'Column',
|
|
name: 'columns.name.keyword',
|
|
localSearch: false,
|
|
},
|
|
{
|
|
id: 'Display Name',
|
|
name: 'displayName.keyword',
|
|
localSearch: false,
|
|
skipConditions: ['isNull', 'isNotNull'], // Null and isNotNull conditions are not present for display name
|
|
},
|
|
];
|
|
|
|
export const OPERATOR = {
|
|
AND: {
|
|
name: 'AND',
|
|
index: 1,
|
|
},
|
|
OR: {
|
|
name: 'OR',
|
|
index: 2,
|
|
},
|
|
};
|
|
|
|
export const CONDITIONS_MUST = {
|
|
equalTo: {
|
|
name: '==',
|
|
filter: 'must',
|
|
},
|
|
contains: {
|
|
name: 'Contains',
|
|
filter: 'must',
|
|
},
|
|
anyIn: {
|
|
name: 'Any in',
|
|
filter: 'must',
|
|
},
|
|
};
|
|
|
|
export const CONDITIONS_MUST_NOT = {
|
|
notEqualTo: {
|
|
name: '!=',
|
|
filter: 'must_not',
|
|
},
|
|
notIn: {
|
|
name: 'Not in',
|
|
filter: 'must_not',
|
|
},
|
|
notContains: {
|
|
name: 'Not contains',
|
|
filter: 'must_not',
|
|
},
|
|
};
|
|
|
|
export const NULL_CONDITIONS = {
|
|
isNull: {
|
|
name: 'Is null',
|
|
filter: 'empty',
|
|
},
|
|
isNotNull: {
|
|
name: 'Is not null',
|
|
filter: 'empty',
|
|
},
|
|
};
|
|
|
|
export const showAdvancedSearchDialog = async (page: Page) => {
|
|
await page.getByTestId('advance-search-button').click();
|
|
|
|
await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
|
|
};
|
|
|
|
export const selectOption = async (
|
|
page: Page,
|
|
dropdownLocator: Locator,
|
|
optionTitle: string
|
|
) => {
|
|
await dropdownLocator.click();
|
|
await page.waitForSelector(`.ant-select-dropdown:visible`, {
|
|
state: 'visible',
|
|
});
|
|
await page.click(`.ant-select-dropdown:visible [title="${optionTitle}"]`);
|
|
};
|
|
|
|
export const fillRule = async (
|
|
page: Page,
|
|
{
|
|
condition,
|
|
field,
|
|
searchCriteria,
|
|
index,
|
|
}: {
|
|
condition: string;
|
|
field: EntityFields;
|
|
searchCriteria?: string;
|
|
index: number;
|
|
}
|
|
) => {
|
|
const ruleLocator = page.locator('.rule').nth(index - 1);
|
|
|
|
// Perform click on rule field
|
|
await selectOption(
|
|
page,
|
|
ruleLocator.locator('.rule--field .ant-select'),
|
|
field.id
|
|
);
|
|
|
|
// Perform click on operator
|
|
await selectOption(
|
|
page,
|
|
ruleLocator.locator('.rule--operator .ant-select'),
|
|
condition
|
|
);
|
|
|
|
if (searchCriteria) {
|
|
const inputElement = ruleLocator.locator(
|
|
'.rule--widget--TEXT input[type="text"]'
|
|
);
|
|
const searchData = field.localSearch
|
|
? searchCriteria
|
|
: searchCriteria.toLowerCase();
|
|
|
|
if (await inputElement.isVisible()) {
|
|
await inputElement.fill(searchData);
|
|
} else {
|
|
const dropdownInput = ruleLocator.locator(
|
|
'.widget--widget > .ant-select > .ant-select-selector input'
|
|
);
|
|
let aggregateRes;
|
|
|
|
if (!field.localSearch) {
|
|
aggregateRes = page.waitForResponse('/api/v1/search/aggregate?*');
|
|
}
|
|
|
|
await dropdownInput.click();
|
|
await dropdownInput.fill(searchData);
|
|
|
|
if (aggregateRes) {
|
|
await aggregateRes;
|
|
}
|
|
|
|
await page
|
|
.locator(`.ant-select-dropdown:visible [title="${searchData}"]`)
|
|
.click();
|
|
}
|
|
|
|
await clickOutside(page);
|
|
}
|
|
};
|
|
|
|
export const checkMustPaths = async (
|
|
page: Page,
|
|
{
|
|
condition,
|
|
field,
|
|
searchCriteria,
|
|
index,
|
|
}: {
|
|
condition: string;
|
|
field: EntityFields;
|
|
searchCriteria: string;
|
|
index: number;
|
|
}
|
|
) => {
|
|
const searchData = field.localSearch
|
|
? searchCriteria
|
|
: searchCriteria.toLowerCase();
|
|
|
|
await fillRule(page, {
|
|
condition,
|
|
field,
|
|
searchCriteria,
|
|
index,
|
|
});
|
|
|
|
const searchRes = page.waitForResponse(
|
|
'/api/v1/search/query?*index=dataAsset&from=0&size=10*'
|
|
);
|
|
await page.getByTestId('apply-btn').click();
|
|
|
|
const res = await searchRes;
|
|
|
|
expect(res.request().url()).toContain(getEncodedFqn(searchData, true));
|
|
|
|
const json = await res.json();
|
|
|
|
expect(JSON.stringify(json.hits.hits)).toContain(searchCriteria);
|
|
|
|
await expect(
|
|
page.getByTestId('advance-search-filter-container')
|
|
).toContainText(searchData);
|
|
};
|
|
|
|
export const checkMustNotPaths = async (
|
|
page: Page,
|
|
{
|
|
condition,
|
|
field,
|
|
searchCriteria,
|
|
index,
|
|
}: {
|
|
condition: string;
|
|
field: EntityFields;
|
|
searchCriteria: string;
|
|
index: number;
|
|
}
|
|
) => {
|
|
const searchData = field.localSearch
|
|
? searchCriteria
|
|
: searchCriteria.toLowerCase();
|
|
|
|
await fillRule(page, {
|
|
condition,
|
|
field,
|
|
searchCriteria,
|
|
index,
|
|
});
|
|
|
|
const searchRes = page.waitForResponse(
|
|
'/api/v1/search/query?*index=dataAsset&from=0&size=10*'
|
|
);
|
|
await page.getByTestId('apply-btn').click();
|
|
const res = await searchRes;
|
|
|
|
expect(res.request().url()).toContain(getEncodedFqn(searchData, true));
|
|
|
|
if (!['columns.name.keyword'].includes(field.name)) {
|
|
const json = await res.json();
|
|
|
|
expect(JSON.stringify(json.hits.hits)).not.toContain(searchCriteria);
|
|
}
|
|
|
|
await expect(
|
|
page.getByTestId('advance-search-filter-container')
|
|
).toContainText(searchData);
|
|
};
|
|
|
|
export const checkNullPaths = async (
|
|
page: Page,
|
|
{
|
|
condition,
|
|
field,
|
|
searchCriteria,
|
|
index,
|
|
}: {
|
|
condition: string;
|
|
field: EntityFields;
|
|
searchCriteria?: string;
|
|
index: number;
|
|
}
|
|
) => {
|
|
await fillRule(page, {
|
|
condition,
|
|
field,
|
|
searchCriteria,
|
|
index,
|
|
});
|
|
|
|
const searchRes = page.waitForResponse(
|
|
'/api/v1/search/query?*index=dataAsset&from=0&size=10*'
|
|
);
|
|
await page.getByTestId('apply-btn').click();
|
|
const res = await searchRes;
|
|
const urlParams = new URLSearchParams(res.request().url());
|
|
const queryFilter = JSON.parse(urlParams.get('query_filter') ?? '');
|
|
|
|
const resultQuery =
|
|
condition === 'Is null'
|
|
? {
|
|
query: {
|
|
bool: {
|
|
must: [
|
|
{
|
|
bool: {
|
|
must: [
|
|
{
|
|
bool: {
|
|
must_not: {
|
|
exists: { field: field.name },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
: {
|
|
query: {
|
|
bool: {
|
|
must: [
|
|
{
|
|
bool: {
|
|
must: [{ exists: { field: field.name } }],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(JSON.stringify(queryFilter)).toContain(JSON.stringify(resultQuery));
|
|
};
|
|
|
|
export const verifyAllConditions = async (
|
|
page: Page,
|
|
field: EntityFields,
|
|
searchCriteria: string
|
|
) => {
|
|
// Check for Must conditions
|
|
for (const condition of Object.values(CONDITIONS_MUST)) {
|
|
await showAdvancedSearchDialog(page);
|
|
await checkMustPaths(page, {
|
|
condition: condition.name,
|
|
field,
|
|
searchCriteria: searchCriteria,
|
|
index: 1,
|
|
});
|
|
await page.getByTestId('clear-filters').click();
|
|
}
|
|
|
|
// Check for Must Not conditions
|
|
for (const condition of Object.values(CONDITIONS_MUST_NOT)) {
|
|
await showAdvancedSearchDialog(page);
|
|
await checkMustNotPaths(page, {
|
|
condition: condition.name,
|
|
field,
|
|
searchCriteria: searchCriteria,
|
|
index: 1,
|
|
});
|
|
await page.getByTestId('clear-filters').click();
|
|
}
|
|
|
|
// Don't run null path if it's present in skipConditions
|
|
if (
|
|
!field.skipConditions?.includes('isNull') ||
|
|
!field.skipConditions?.includes('isNotNull')
|
|
) {
|
|
// Check for Null and Not Null conditions
|
|
for (const condition of Object.values(NULL_CONDITIONS)) {
|
|
await showAdvancedSearchDialog(page);
|
|
await checkNullPaths(page, {
|
|
condition: condition.name,
|
|
field,
|
|
searchCriteria: undefined,
|
|
index: 1,
|
|
});
|
|
await page.getByTestId('clear-filters').click();
|
|
}
|
|
}
|
|
};
|
|
|
|
export const checkAddRuleOrGroupWithOperator = async (
|
|
page: Page,
|
|
{
|
|
field,
|
|
operator,
|
|
condition1,
|
|
condition2,
|
|
searchCriteria1,
|
|
searchCriteria2,
|
|
}: {
|
|
field: EntityFields;
|
|
operator: string;
|
|
condition1: string;
|
|
condition2: string;
|
|
searchCriteria1: string;
|
|
searchCriteria2: string;
|
|
},
|
|
isGroupTest = false
|
|
) => {
|
|
await showAdvancedSearchDialog(page);
|
|
await fillRule(page, {
|
|
condition: condition1,
|
|
field,
|
|
searchCriteria: searchCriteria1,
|
|
index: 1,
|
|
});
|
|
|
|
if (!isGroupTest) {
|
|
await page.getByTestId('advanced-search-add-rule').nth(1).click();
|
|
} else {
|
|
await page.getByTestId('advanced-search-add-group').first().click();
|
|
}
|
|
|
|
await fillRule(page, {
|
|
condition: condition2,
|
|
field,
|
|
searchCriteria: searchCriteria2,
|
|
index: 2,
|
|
});
|
|
|
|
if (operator === 'OR') {
|
|
await page
|
|
.getByTestId('advanced-search-modal')
|
|
.getByRole('button', { name: 'Or' })
|
|
.click();
|
|
}
|
|
|
|
const searchRes = page.waitForResponse(
|
|
'/api/v1/search/query?*index=dataAsset&from=0&size=10*'
|
|
);
|
|
await page.getByTestId('apply-btn').click();
|
|
|
|
// Since the OR operator with must not conditions will result in huge API response
|
|
// with huge data, checking the required criteria might not be present on first page
|
|
// Hence, checking the criteria only for AND operator
|
|
if (field.id !== 'Column' && operator === 'AND') {
|
|
const res = await searchRes;
|
|
const json = await res.json();
|
|
|
|
expect(JSON.stringify(json)).toContain(searchCriteria1);
|
|
expect(JSON.stringify(json)).not.toContain(searchCriteria2);
|
|
}
|
|
};
|
|
|
|
export const runRuleGroupTests = async (
|
|
page: Page,
|
|
field: EntityFields,
|
|
operator: string,
|
|
isGroupTest: boolean,
|
|
searchCriteria: Record<string, string[]>
|
|
) => {
|
|
const searchCriteria1 = searchCriteria[field.name][0];
|
|
const searchCriteria2 = searchCriteria[field.name][1];
|
|
|
|
const testCases = [
|
|
{
|
|
condition1: CONDITIONS_MUST.equalTo.name,
|
|
condition2: CONDITIONS_MUST_NOT.notEqualTo.name,
|
|
},
|
|
{
|
|
condition1: CONDITIONS_MUST.contains.name,
|
|
condition2: CONDITIONS_MUST_NOT.notContains.name,
|
|
},
|
|
{
|
|
condition1: CONDITIONS_MUST.anyIn.name,
|
|
condition2: CONDITIONS_MUST_NOT.notIn.name,
|
|
},
|
|
];
|
|
|
|
for (const { condition1, condition2 } of testCases) {
|
|
await checkAddRuleOrGroupWithOperator(
|
|
page,
|
|
{
|
|
field,
|
|
operator,
|
|
condition1,
|
|
condition2,
|
|
searchCriteria1,
|
|
searchCriteria2,
|
|
},
|
|
isGroupTest
|
|
);
|
|
await page.getByTestId('clear-filters').click();
|
|
}
|
|
};
|