diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts deleted file mode 100644 index b6521e87ce06..000000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2023 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 { - searchAndClickOnOption, - selectNullOption, -} from '../../common/advancedSearchQuickFilters'; -import { interceptURL, verifyResponseStatusCode } from '../../common/common'; -import { goToAdvanceSearch } from '../../common/Utils/AdvancedSearch'; -import { addDomainToEntity } from '../../common/Utils/Domain'; -import { - createEntityViaREST, - deleteEntityViaREST, - visitEntityDetailsPage, -} from '../../common/Utils/Entity'; -import { getToken } from '../../common/Utils/LocalStorage'; -import { addOwner, removeOwner } from '../../common/Utils/Owner'; -import { assignTags, removeTags } from '../../common/Utils/Tags'; -import { addTier, removeTier } from '../../common/Utils/Tier'; -import { - FilterItem, - QUICK_FILTERS_BY_ASSETS, - SUPPORTED_EMPTY_FILTER_FIELDS, -} from '../../constants/advancedSearchQuickFilters.constants'; -import { SEARCH_ENTITY_TABLE } from '../../constants/constants'; -import { EntityType, SidebarItem } from '../../constants/Entity.interface'; -import { DOMAIN_QUICK_FILTERS_DETAILS } from '../../constants/EntityConstant'; -const ownerName = 'Aaron Johnson'; - -const preRequisitesForTests = () => { - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - createEntityViaREST({ - body: DOMAIN_QUICK_FILTERS_DETAILS, - endPoint: EntityType.Domain, - token, - }); - - visitEntityDetailsPage({ - term: SEARCH_ENTITY_TABLE.table_1.term, - entity: SEARCH_ENTITY_TABLE.table_1.entity, - serviceName: SEARCH_ENTITY_TABLE.table_1.serviceName, - }); - addDomainToEntity(DOMAIN_QUICK_FILTERS_DETAILS.displayName); - addOwner(ownerName); - assignTags('PersonalData.Personal', EntityType.Table); - addTier('Tier1'); - }); -}; - -const postRequisitesForTests = () => { - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - // Domain 1 to test - deleteEntityViaREST({ - entityName: DOMAIN_QUICK_FILTERS_DETAILS.name, - endPoint: EntityType.Domain, - token, - }); - visitEntityDetailsPage({ - term: SEARCH_ENTITY_TABLE.table_1.term, - entity: SEARCH_ENTITY_TABLE.table_1.entity, - serviceName: SEARCH_ENTITY_TABLE.table_1.serviceName, - }); - removeOwner(ownerName); - removeTags('PersonalData.Personal', EntityType.Table); - removeTier(); - }); -}; - -// migrated to playwright -describe.skip( - `Advanced search quick filters should work properly for assets`, - { tags: 'DataAssets' }, - () => { - before(() => { - cy.login(); - preRequisitesForTests(); - }); - - after(() => { - cy.login(); - postRequisitesForTests(); - }); - - beforeEach(() => { - cy.login(); - }); - - it(`should show the quick filters for respective assets`, () => { - // Navigate to explore page - cy.sidebarClick(SidebarItem.EXPLORE); - QUICK_FILTERS_BY_ASSETS.map((asset) => { - cy.get(`[data-testid="${asset.tab}"]`).scrollIntoView().click(); - - asset.filters.map((filter) => { - cy.get(`[data-testid="search-dropdown-${filter.label}"]`) - .scrollIntoView() - .should('be.visible'); - }); - }); - }); - - it('search dropdown should work properly for tables', () => { - // Table - const asset = QUICK_FILTERS_BY_ASSETS[0]; - - // Navigate to explore page - cy.sidebarClick(SidebarItem.EXPLORE); - cy.get(`[data-testid="${asset.tab}"]`).scrollIntoView().click(); - - asset.filters - .filter((item: FilterItem) => item.select) - .map((filter: FilterItem) => { - cy.get(`[data-testid="search-dropdown-${filter.label}"]`).click(); - searchAndClickOnOption(asset, filter, true); - - const querySearchURL = `/api/v1/search/query?*index=${ - asset.searchIndex - }*query_filter=*should*${filter.key}*${encodeURI( - Cypress._.toLower(filter.selectOption1).replace(' ', '+') - )}*`; - - interceptURL('GET', querySearchURL, 'querySearchAPI'); - - cy.get('[data-testid="update-btn"]').click(); - - verifyResponseStatusCode('@querySearchAPI', 200); - }); - }); - - it('should search for empty or null filters', () => { - const initialQuery = encodeURI(JSON.stringify({ query: { bool: {} } })); - // Table - interceptURL( - 'GET', - `/api/v1/search/query?*index=table_search_index&*query_filter=${initialQuery}&*`, - 'initialQueryAPI' - ); - - const asset = QUICK_FILTERS_BY_ASSETS[0]; - cy.sidebarClick(SidebarItem.EXPLORE); - verifyResponseStatusCode('@initialQueryAPI', 200); - cy.get(`[data-testid="${asset.tab}"]`).scrollIntoView().click(); - asset.filters - .filter((item) => SUPPORTED_EMPTY_FILTER_FIELDS.includes(item.key)) - .map((filter) => { - selectNullOption(asset, filter); - }); - }); - - it('should search for multiple values alongwith null filters', () => { - const initialQuery = encodeURI(JSON.stringify({ query: { bool: {} } })); - // Table - interceptURL( - 'GET', - `/api/v1/search/query?*index=table_search_index&*query_filter=${initialQuery}&*`, - 'initialQueryAPI' - ); - - const asset = QUICK_FILTERS_BY_ASSETS[0]; - cy.sidebarClick(SidebarItem.EXPLORE); - verifyResponseStatusCode('@initialQueryAPI', 200); - cy.get(`[data-testid="${asset.tab}"]`).scrollIntoView().click(); - // Checking Owner with multiple values - asset.filters - .filter((item) => SUPPORTED_EMPTY_FILTER_FIELDS.includes(item.key)) - .map((filter: FilterItem) => { - selectNullOption(asset, filter, filter?.selectOptionTestId1); - }); - }); - } -); - -const testIsNullAndIsNotNullFilters = (operatorTitle, queryFilter, alias) => { - goToAdvanceSearch(); - - // Check Is Null or Is Not Null - cy.get('.rule--operator > .ant-select > .ant-select-selector').eq(0).click(); - cy.get(`[title="${operatorTitle}"]`).click(); - - cy.intercept('GET', '/api/v1/search/query?*', (req) => { - req.alias = alias; - }).as(alias); - - cy.get('[data-testid="apply-btn"]').click(); - - cy.wait(`@${alias}`).then((xhr) => { - const actualQueryFilter = JSON.parse(xhr.request.query['query_filter']); - - expect(actualQueryFilter).to.deep.equal(queryFilter); - }); -}; - -describe(`Advanced Search Modal`, () => { - beforeEach(() => { - cy.login(); - }); - - it('should check isNull and isNotNull filters', () => { - // Check Is Null - const isNullQuery = { - query: { - bool: { - must: [ - { - bool: { - must: [ - { - bool: { - must_not: { - exists: { field: 'owners.displayName.keyword' }, - }, - }, - }, - ], - }, - }, - ], - }, - }, - }; - testIsNullAndIsNotNullFilters('Is null', isNullQuery, 'searchAPI'); - - // Check Is Not Null - const isNotNullQuery = { - query: { - bool: { - must: [ - { - bool: { - must: [{ exists: { field: 'owners.displayName.keyword' } }], - }, - }, - ], - }, - }, - }; - testIsNullAndIsNotNullFilters( - 'Is not null', - isNotNullQuery, - 'newSearchAPI' - ); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/AdditionalRuleSearch.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/AdditionalRuleSearch.spec.ts deleted file mode 100644 index 7c940d557e89..000000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/AdditionalRuleSearch.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2023 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. - */ - -// The spec is related to advance search feature - -import { - advancedSearchFlowCleanup, - advanceSearchPreRequests, - checkAddRuleWithOperator, - CONDITIONS_MUST, - CONDITIONS_MUST_NOT, - FIELDS, - OPERATOR, -} from '../../../common/Utils/AdvancedSearch'; -import { getToken } from '../../../common/Utils/LocalStorage'; - -describe('Search with additional rule', () => { - const testData = { - user_1: { - id: '', - }, - user_2: { - id: '', - }, - }; - - before(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - advanceSearchPreRequests(testData, token); - }); - }); - - after(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - advancedSearchFlowCleanup(token); - }); - Cypress.session.clearAllSavedSessions(); - }); - - beforeEach(() => { - cy.login(); - }); - - Object.values(OPERATOR).forEach((operator) => { - it(`Verify Add Rule functionality for All with ${operator.name} operator & condition ${CONDITIONS_MUST.equalTo.name} and ${CONDITIONS_MUST_NOT.notEqualTo.name} `, () => { - Object.values(FIELDS).forEach((field) => { - let val = field.searchCriteriaSecondGroup; - if (field.owner) { - val = field.responseValueSecondGroup; - } - checkAddRuleWithOperator({ - condition_1: CONDITIONS_MUST.equalTo.name, - condition_2: CONDITIONS_MUST_NOT.notEqualTo.name, - fieldId: field.testId, - searchCriteria_1: field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - searchCriteria_2: field.isLocalSearch - ? field.searchCriteriaSecondGroup - : Cypress._.toLower(field.searchCriteriaSecondGroup), - index_1: 0, - index_2: 1, - operatorIndex: operator.index, - filter_1: CONDITIONS_MUST.equalTo.filter, - filter_2: CONDITIONS_MUST_NOT.notEqualTo.filter, - response: field.isLocalSearch ? val : Cypress._.toLower(val), - }); - }); - }); - - it(`Verify Add Rule functionality for All with ${operator.name} operator & condition ${CONDITIONS_MUST.anyIn.name} and ${CONDITIONS_MUST_NOT.notIn.name} `, () => { - Object.values(FIELDS).forEach((field) => { - let val = field.searchCriteriaSecondGroup; - if (field.owner) { - val = field.responseValueSecondGroup; - } - checkAddRuleWithOperator({ - condition_1: CONDITIONS_MUST.anyIn.name, - condition_2: CONDITIONS_MUST_NOT.notIn.name, - fieldId: field.testId, - searchCriteria_1: field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - searchCriteria_2: field.isLocalSearch - ? field.searchCriteriaSecondGroup - : Cypress._.toLower(field.searchCriteriaSecondGroup), - index_1: 0, - index_2: 1, - operatorIndex: operator.index, - filter_1: CONDITIONS_MUST.anyIn.filter, - filter_2: CONDITIONS_MUST_NOT.notIn.filter, - response: field.isLocalSearch ? val : Cypress._.toLower(val), - }); - }); - }); - - it(`Verify Add Rule functionality for All with ${operator.name} operator & condition ${CONDITIONS_MUST.contains.name} and ${CONDITIONS_MUST_NOT.notContains.name} `, () => { - Object.values(FIELDS).forEach((field) => { - const val = field.searchCriteriaSecondGroup; - checkAddRuleWithOperator({ - condition_1: CONDITIONS_MUST.contains.name, - condition_2: CONDITIONS_MUST_NOT.notContains.name, - fieldId: field.testId, - searchCriteria_1: field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - searchCriteria_2: field.isLocalSearch - ? field.searchCriteriaSecondGroup - : Cypress._.toLower(field.searchCriteriaSecondGroup), - index_1: 0, - index_2: 1, - operatorIndex: operator.index, - filter_1: CONDITIONS_MUST.contains.filter, - filter_2: CONDITIONS_MUST_NOT.notContains.filter, - response: field.isLocalSearch ? val : Cypress._.toLower(val), - }); - }); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/GroupSearch.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/GroupSearch.spec.ts deleted file mode 100644 index f7a0b805f3fe..000000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/GroupSearch.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2023 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. - */ - -// The spec is related to advance search feature - -import { - advancedSearchFlowCleanup, - advanceSearchPreRequests, - checkAddGroupWithOperator, - CONDITIONS_MUST, - CONDITIONS_MUST_NOT, - FIELDS, - OPERATOR, -} from '../../../common/Utils/AdvancedSearch'; -import { getToken } from '../../../common/Utils/LocalStorage'; - -describe('Group search', () => { - const testData = { - user_1: { - id: '', - }, - user_2: { - id: '', - }, - }; - - before(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - advanceSearchPreRequests(testData, token); - }); - }); - - after(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - advancedSearchFlowCleanup(token); - }); - Cypress.session.clearAllSavedSessions(); - }); - - beforeEach(() => { - cy.login(); - }); - - Object.values(OPERATOR).forEach((operator) => { - it(`Verify Add group functionality for All with ${operator.name} operator & condition ${CONDITIONS_MUST.equalTo.name} and ${CONDITIONS_MUST_NOT.notEqualTo.name} `, () => { - Object.values(FIELDS).forEach((field) => { - checkAddGroupWithOperator({ - condition_1: CONDITIONS_MUST.equalTo.name, - condition_2: CONDITIONS_MUST_NOT.notEqualTo.name, - fieldId: field.testId, - searchCriteria_1: field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - searchCriteria_2: field.isLocalSearch - ? field.searchCriteriaSecondGroup - : Cypress._.toLower(field.searchCriteriaSecondGroup), - index_1: 0, - index_2: 1, - operatorIndex: operator.index, - isLocalSearch: field.isLocalSearch, - }); - }); - }); - - it(`Verify Add group functionality for All with ${operator.name} operator & condition ${CONDITIONS_MUST.anyIn.name} and ${CONDITIONS_MUST_NOT.notIn.name} `, () => { - Object.values(FIELDS).forEach((field) => { - checkAddGroupWithOperator({ - condition_1: CONDITIONS_MUST.anyIn.name, - condition_2: CONDITIONS_MUST_NOT.notIn.name, - fieldId: field.testId, - searchCriteria_1: field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - searchCriteria_2: field.isLocalSearch - ? field.searchCriteriaSecondGroup - : Cypress._.toLower(field.searchCriteriaSecondGroup), - index_1: 0, - index_2: 1, - operatorIndex: operator.index, - isLocalSearch: field.isLocalSearch, - }); - }); - }); - - it(`Verify Add group functionality for All with ${operator.name} operator & condition ${CONDITIONS_MUST.contains.name} and ${CONDITIONS_MUST_NOT.notContains.name} `, () => { - Object.values(FIELDS).forEach((field) => { - checkAddGroupWithOperator({ - condition_1: CONDITIONS_MUST.contains.name, - condition_2: CONDITIONS_MUST_NOT.notContains.name, - fieldId: field.testId, - searchCriteria_1: field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - searchCriteria_2: field.isLocalSearch - ? field.searchCriteriaSecondGroup - : Cypress._.toLower(field.searchCriteriaSecondGroup), - index_1: 0, - index_2: 1, - operatorIndex: operator.index, - isLocalSearch: field.isLocalSearch, - }); - }); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/SingleFiledSearch.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/SingleFiledSearch.spec.ts deleted file mode 100644 index d241e1eb0952..000000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/SearchFlow/SingleFiledSearch.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 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. - */ - -// The spec is related to advance search feature - -import { - advancedSearchFlowCleanup, - advanceSearchPreRequests, - checkMustPaths, - checkMust_notPaths, - CONDITIONS_MUST, - CONDITIONS_MUST_NOT, - FIELDS, -} from '../../../common/Utils/AdvancedSearch'; -import { getToken } from '../../../common/Utils/LocalStorage'; - -describe('Single filed search', () => { - const testData = { - user_1: { - id: '', - }, - user_2: { - id: '', - }, - }; - - before(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - advanceSearchPreRequests(testData, token); - }); - }); - - after(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - advancedSearchFlowCleanup(token); - }); - Cypress.session.clearAllSavedSessions(); - }); - - beforeEach(() => { - cy.login(); - }); - - Object.values(FIELDS).forEach((field) => { - it(`Verify advance search results for ${field.name} field and all conditions`, () => { - Object.values(CONDITIONS_MUST).forEach((condition) => { - checkMustPaths( - condition.name, - field.testId, - field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - 0, - field.responseValueFirstGroup, - field.isLocalSearch - ); - }); - - Object.values(CONDITIONS_MUST_NOT).forEach((condition) => { - checkMust_notPaths( - condition.name, - field.testId, - field.isLocalSearch - ? field.searchCriteriaFirstGroup - : Cypress._.toLower(field.searchCriteriaFirstGroup), - 0, - field.responseValueFirstGroup, - field.isLocalSearch - ); - }); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearch.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearch.spec.ts new file mode 100644 index 000000000000..5d6841933a44 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearch.spec.ts @@ -0,0 +1,152 @@ +/* + * 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 test from '@playwright/test'; +import { SidebarItem } from '../../constant/sidebar'; +import { TableClass } from '../../support/entity/TableClass'; +import { TopicClass } from '../../support/entity/TopicClass'; +import { UserClass } from '../../support/user/UserClass'; +import { + FIELDS, + OPERATOR, + runRuleGroupTests, + verifyAllConditions, +} from '../../utils/advancedSearch'; +import { createNewPage, redirectToHomePage } from '../../utils/common'; +import { addMultiOwner, assignTag, assignTier } from '../../utils/entity'; +import { sidebarClick } from '../../utils/sidebar'; + +test.describe('Advanced Search', { tag: '@advanced-search' }, () => { + // use the admin user to login + test.use({ storageState: 'playwright/.auth/admin.json' }); + + const user1 = new UserClass(); + const user2 = new UserClass(); + const table1 = new TableClass(); + const table2 = new TableClass(); + const topic1 = new TopicClass(); + const topic2 = new TopicClass(); + + let searchCriteria = {}; + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + test.setTimeout(150000); + + const { page, apiContext, afterAction } = await createNewPage(browser); + await Promise.all([ + user1.create(apiContext), + user2.create(apiContext), + table1.create(apiContext), + table2.create(apiContext), + topic1.create(apiContext), + topic2.create(apiContext), + ]); + + // Add Owner & Tag to the table + await table1.visitEntityPage(page); + await addMultiOwner({ + page, + ownerNames: [user1.getUserName()], + activatorBtnDataTestId: 'edit-owner', + resultTestId: 'data-assets-header', + endpoint: table1.endpoint, + type: 'Users', + }); + await assignTag(page, 'PersonalData.Personal'); + + await table2.visitEntityPage(page); + await addMultiOwner({ + page, + ownerNames: [user2.getUserName()], + activatorBtnDataTestId: 'edit-owner', + resultTestId: 'data-assets-header', + endpoint: table1.endpoint, + type: 'Users', + }); + await assignTag(page, 'PII.None'); + + // Add Tier To the topic 1 + await topic1.visitEntityPage(page); + await assignTier(page, 'Tier1', topic1.endpoint); + + // Add Tier To the topic 2 + await topic2.visitEntityPage(page); + await assignTier(page, 'Tier2', topic2.endpoint); + + // Update Search Criteria here + searchCriteria = { + 'owners.displayName.keyword': [user1.getUserName(), user2.getUserName()], + 'tags.tagFQN': ['PersonalData.Personal', 'PII.None'], + 'tier.tagFQN': ['Tier.Tier1', 'Tier.Tier2'], + 'service.displayName.keyword': [table1.service.name, table2.service.name], + 'database.displayName.keyword': [ + table1.database.name, + table2.database.name, + ], + 'databaseSchema.displayName.keyword': [ + table1.schema.name, + table2.schema.name, + ], + 'columns.name.keyword': ['email', 'shop_id'], + }; + + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await Promise.all([ + user1.delete(apiContext), + user2.delete(apiContext), + table1.delete(apiContext), + table2.delete(apiContext), + topic1.delete(apiContext), + topic2.delete(apiContext), + ]); + await afterAction(); + }); + + test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + FIELDS.forEach((field) => { + test(`Verify All conditions for ${field.id} field`, async ({ page }) => { + test.slow(true); + + await verifyAllConditions(page, field, searchCriteria[field.name][0]); + }); + }); + + Object.values(OPERATOR).forEach(({ name: operator }) => { + FIELDS.forEach((field) => { + // Rule based search + test(`Verify Rule functionality for field ${field.id} with ${operator} operator`, async ({ + page, + }) => { + test.slow(true); + + await runRuleGroupTests(page, field, operator, false, searchCriteria); + }); + + // Group based search + test(`Verify Group functionality for field ${field.id} with ${operator} operator`, async ({ + page, + }) => { + test.slow(true); + + await runRuleGroupTests(page, field, operator, true, searchCriteria); + }); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts new file mode 100644 index 000000000000..bbcca1b90046 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts @@ -0,0 +1,471 @@ +/* + * 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'; + +type EntityFields = { + id: string; + name: string; + localSearch: boolean; +}; + +export const FIELDS: EntityFields[] = [ + { + id: 'Owner', + 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, + }, +]; + +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(); +}; + +const selectOption = async ( + page: Page, + dropdownLocator: Locator, + optionTitle: string +) => { + await dropdownLocator.click(); + 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 [title="${searchData}"]`) + .click(); + } + + await clickOutside(page); + } +}; + +export const checkMustPaths = async ( + page: Page, + { condition, field, searchCriteria, index } +) => { + 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(); + await searchRes.then(async (res) => { + await expect(res.request().url()).toContain(encodeURI(searchData)); + + await res.json().then(async (json) => { + await 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 } +) => { + 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(); + await searchRes.then(async (res) => { + await expect(res.request().url()).toContain(encodeURI(searchData)); + + if (!['columns.name.keyword'].includes(field.name)) { + await res.json().then(async (json) => { + await 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 } +) => { + 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(); + await searchRes.then(async (res) => { + 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 } }], + }, + }, + ], + }, + }, + }; + + await 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(); + } + + // 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, + { + 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' }); + } + + const searchRes = page.waitForResponse( + '/api/v1/search/query?*index=dataAsset&from=0&size=10*' + ); + await page.getByTestId('apply-btn').click(); + await searchRes; + await searchRes.then(async (res) => { + await res.json().then(async (json) => { + if (field.id !== 'Column') { + if (operator === 'Or') { + await expect(JSON.stringify(json)).toContain(searchCriteria1); + await expect(JSON.stringify(json)).toContain(searchCriteria2); + } else { + await expect(JSON.stringify(json)).toContain(searchCriteria1); + await expect(JSON.stringify(json)).not.toContain(searchCriteria2); + } + } + }); + }); +}; + +export const runRuleGroupTests = async ( + page: Page, + field: EntityFields, + operator: string, + isGroupTest: boolean, + searchCriteria: Record +) => { + 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(); + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx index 6a7f6a420a9a..565a32253dd4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx @@ -115,6 +115,7 @@ export const renderAdvanceSearchButtons: RenderSettings['renderButton'] = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any CloseCircleOutlined as React.ForwardRefExoticComponent } + data-testid="advanced-search-delete-rule" onClick={props?.onClick} /> ); @@ -123,6 +124,7 @@ export const renderAdvanceSearchButtons: RenderSettings['renderButton'] = (