From a79a9032ceaf7b267beb6312048deab0e6bb698c Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Thu, 29 Aug 2024 15:12:32 +0530 Subject: [PATCH] migrated task cypress spec in Activity feed playwright (#17618) --- .../ui/cypress/e2e/Flow/Task.spec.ts | 386 ------------------ .../e2e/Features/ActivityFeed.spec.ts | 144 ++++++- .../support/access-control/PoliciesClass.ts | 65 +++ .../support/access-control/RolesClass.ts | 58 +++ .../ui/playwright/support/team/TeamClass.ts | 1 + .../resources/ui/playwright/utils/common.ts | 2 +- 6 files changed, 261 insertions(+), 395 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Task.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/support/access-control/RolesClass.ts diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Task.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Task.spec.ts deleted file mode 100644 index da8b6f5655e..00000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Task.spec.ts +++ /dev/null @@ -1,386 +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 { - interceptURL, - toastNotification, - verifyResponseStatusCode, -} from '../../common/common'; -import { - createEntityTable, - deleteUserEntity, - hardDeleteService, -} from '../../common/EntityUtils'; -import { - createAndUpdateDescriptionTask, - createDescriptionTask, - editAssignee, - verifyTaskDetails, -} from '../../common/TaskUtils'; -import { visitEntityDetailsPage } from '../../common/Utils/Entity'; -import { getToken } from '../../common/Utils/LocalStorage'; -import { addOwner } from '../../common/Utils/Owner'; -import { uuid } from '../../constants/constants'; -import { EntityType } from '../../constants/Entity.interface'; -import { - DATABASE_SERVICE, - USER_DETAILS, - USER_NAME, -} from '../../constants/EntityConstant'; -import { SERVICE_CATEGORIES } from '../../constants/service.constants'; - -const ENTITY_TABLE = { - term: DATABASE_SERVICE.entity.name, - displayName: DATABASE_SERVICE.entity.name, - entity: EntityType.Table, - serviceName: DATABASE_SERVICE.service.name, - schemaName: DATABASE_SERVICE.schema.name, - entityType: 'Table', -}; - -const POLICY_DETAILS = { - name: `cy-data-viewAll-policy-${uuid()}`, - rules: [ - { - name: 'viewRuleAllowed', - resources: ['All'], - operations: ['ViewAll'], - effect: 'allow', - }, - { - effect: 'deny', - name: 'editNotAllowed', - operations: ['EditAll'], - resources: ['All'], - }, - ], -}; -const ROLE_DETAILS = { - name: `cy-data-viewAll-role-${uuid()}`, - policies: [POLICY_DETAILS.name], -}; - -const TEAM_DETAILS = { - name: 'viewAllTeam', - displayName: 'viewAllTeam', - teamType: 'Group', -}; - -describe('Task flow should work', { tags: 'DataAssets' }, () => { - const data = { - user: { id: '' }, - policy: { id: '' }, - role: { id: '' }, - team: { id: '' }, - }; - - before(() => { - cy.login(); - cy.getAllLocalStorage().then((storageData) => { - const token = getToken(storageData); - - createEntityTable({ - token, - ...DATABASE_SERVICE, - tables: [DATABASE_SERVICE.entity], - }); - - // Create ViewAll Policy - cy.request({ - method: 'POST', - url: `/api/v1/policies`, - headers: { Authorization: `Bearer ${token}` }, - body: POLICY_DETAILS, - }).then((policyResponse) => { - data.policy = policyResponse.body; - - // Create ViewAll Role - cy.request({ - method: 'POST', - url: `/api/v1/roles`, - headers: { Authorization: `Bearer ${token}` }, - body: ROLE_DETAILS, - }).then((roleResponse) => { - data.role = roleResponse.body; - - // Create a new user - cy.request({ - method: 'POST', - url: `/api/v1/users/signup`, - headers: { Authorization: `Bearer ${token}` }, - body: USER_DETAILS, - }).then((userResponse) => { - data.user = userResponse.body; - - // create team - cy.request({ - method: 'GET', - url: `/api/v1/teams/name/Organization`, - headers: { Authorization: `Bearer ${token}` }, - }).then((teamResponse) => { - cy.request({ - method: 'POST', - url: `/api/v1/teams`, - headers: { Authorization: `Bearer ${token}` }, - body: { - ...TEAM_DETAILS, - parents: [teamResponse.body.id], - users: [userResponse.body.id], - defaultRoles: [roleResponse.body.id], - }, - }).then((teamResponse) => { - data.team = teamResponse.body; - }); - }); - }); - }); - }); - }); - }); - - after(() => { - cy.login(); - cy.getAllLocalStorage().then((storageData) => { - const token = getToken(storageData); - - hardDeleteService({ - token, - serviceFqn: ENTITY_TABLE.serviceName, - serviceType: SERVICE_CATEGORIES.DATABASE_SERVICES, - }); - - // Clean up for the created data - deleteUserEntity({ token, id: data.user.id }); - - cy.request({ - method: 'DELETE', - url: `/api/v1/teams/${data.team.id}?hardDelete=true&recursive=true`, - headers: { Authorization: `Bearer ${token}` }, - }); - - cy.request({ - method: 'DELETE', - url: `/api/v1/policies/${data.policy.id}?hardDelete=true&recursive=true`, - headers: { Authorization: `Bearer ${token}` }, - }); - - cy.request({ - method: 'DELETE', - url: `/api/v1/roles/${data.role.id}?hardDelete=true&recursive=true`, - headers: { Authorization: `Bearer ${token}` }, - }); - }); - }); - - beforeEach(() => { - cy.login(); - interceptURL('GET', '/api/v1/permissions/*/name/*', 'entityPermission'); - interceptURL('GET', '/api/v1/feed/count?entityLink=*', 'entityFeed'); - interceptURL('GET', '/api/v1/search/suggest?q=*', 'suggestApi'); - interceptURL('PUT', '/api/v1/feed/tasks/*/resolve', 'taskResolve'); - interceptURL( - 'GET', - `/api/v1/search/query?q=*%20AND%20disabled:false&index=tag_search_index*`, - 'suggestTag' - ); - }); - - const assignee = 'adam.matthews2'; - const tag = 'Personal'; - - const createTagTask = (value) => { - interceptURL('POST', 'api/v1/feed', 'createTask'); - - cy.get('#title').should( - 'have.value', - value.tagCount > 0 - ? `Update tags for table ${value.term}` - : `Request tags for table ${value.term}` - ); - - cy.get('[data-testid="select-assignee"] > .ant-select-selector').type( - assignee - ); - // select value from dropdown - verifyResponseStatusCode('@suggestApi', 200); - - cy.get(`[data-testid="${assignee}"]`).trigger('mouseover').trigger('click'); - - cy.clickOutside(); - if (value.tagCount > 0) { - cy.get('[data-testid="tag-selector"]') - .find('[data-testid="remove-tags"]') - .each(($btn) => { - cy.wrap($btn).click(); - }); - } - cy.get('[data-testid="tag-selector"]').click().type(tag); - - verifyResponseStatusCode('@suggestTag', 200); - cy.get('[data-testid="tag-PersonalData.Personal"]').click(); - - cy.get('[data-testid="tags-label"]').click(); - - cy.get('button[type="submit"]').click(); - verifyResponseStatusCode('@createTask', 201); - toastNotification('Task created successfully.'); - - // verify the task details - verifyTaskDetails( - value.tagCount > 0 - ? /#(\d+) Request to update tags for/ - : /#(\d+) Request tags for/ - ); - - // edit task assignees - editAssignee(); - - // Accept the description suggestion which is created - cy.get('.ant-btn-compact-first-item').contains('Accept Suggestion').click(); - - verifyResponseStatusCode('@taskResolve', 200); - - toastNotification('Task resolved successfully'); - - verifyResponseStatusCode('@entityFeed', 200); - }; - - it('Task flow for table description', () => { - interceptURL( - 'GET', - `/api/v1/${ENTITY_TABLE.entity}/name/*`, - 'getEntityDetails' - ); - - visitEntityDetailsPage({ - term: ENTITY_TABLE.term, - serviceName: ENTITY_TABLE.serviceName, - entity: ENTITY_TABLE.entity, - }); - - cy.get('[data-testid="request-description"]').click(); - - cy.wait('@getEntityDetails').then((res) => { - const entity = res.response.body; - - // create description task - - createAndUpdateDescriptionTask({ - ...ENTITY_TABLE, - term: entity.displayName ?? entity.name, - }); - }); - }); - - it('Task flow for table tags', () => { - interceptURL( - 'GET', - `/api/v1/${ENTITY_TABLE.entity}/name/*`, - 'getEntityDetails' - ); - - visitEntityDetailsPage({ - term: ENTITY_TABLE.term, - serviceName: ENTITY_TABLE.serviceName, - entity: ENTITY_TABLE.entity, - }); - - cy.get('[data-testid="request-entity-tags"]').click(); - - cy.wait('@getEntityDetails').then((res) => { - const entity = res.response.body; - - // create tag task - createTagTask({ - ...ENTITY_TABLE, - term: entity.displayName ?? entity.name, - tagCount: entity.tags.length ?? 0, - }); - }); - }); - - it('Assignee field should not be disabled for owned entity tasks', () => { - interceptURL( - 'GET', - `/api/v1/${ENTITY_TABLE.entity}/name/*`, - 'getEntityDetails' - ); - - visitEntityDetailsPage({ - term: ENTITY_TABLE.term, - serviceName: ENTITY_TABLE.serviceName, - entity: ENTITY_TABLE.entity, - }); - - addOwner('Adam Rodriguez'); - - cy.get('[data-testid="request-description"]').click(); - - cy.wait('@getEntityDetails').then((res) => { - const entity = res.response.body; - - createDescriptionTask({ - ...ENTITY_TABLE, - assignee: USER_NAME, - term: entity.displayName ?? entity.name, - }); - }); - }); - - it(`should throw error for not having edit permission for viewAll user`, () => { - // logout for the admin user - cy.logout(); - - // login to viewAll user - cy.login(USER_DETAILS.email, USER_DETAILS.password); - - interceptURL( - 'GET', - `/api/v1/${ENTITY_TABLE.entity}/name/*`, - 'getEntityDetails' - ); - - visitEntityDetailsPage({ - term: ENTITY_TABLE.term, - serviceName: ENTITY_TABLE.serviceName, - entity: ENTITY_TABLE.entity, - }); - - interceptURL( - 'GET', - '/api/v1/feed?entityLink=*type=Conversation*', - 'entityFeed' - ); - interceptURL('GET', '/api/v1/feed?entityLink=*type=Task*', 'taskFeed'); - cy.get('[data-testid="activity_feed"]').click(); - verifyResponseStatusCode('@entityFeed', 200); - - cy.get('[data-menu-id*="tasks"]').click(); - verifyResponseStatusCode('@taskFeed', 200); - - // verify the task details - verifyTaskDetails( - /#(\d+) Request to update description for/, - USER_DETAILS.firstName - ); - - // Accept the description suggestion which is created - cy.get('.ant-btn-compact-first-item').contains('Accept Suggestion').click(); - - verifyResponseStatusCode('@taskResolve', 403); - - toastNotification( - `Principal: CatalogPrincipal{name='${USER_NAME}'} operation EditDescription denied by role ${ROLE_DETAILS.name}, policy ${POLICY_DETAILS.name}, rule editNotAllowed` - ); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts index 75f2fd34002..0dc0c97c2e7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts @@ -11,7 +11,14 @@ * limitations under the License. */ import { expect, Page, test as base } from '@playwright/test'; +import { + PolicyClass, + PolicyRulesType, +} from '../../support/access-control/PoliciesClass'; +import { RolesClass } from '../../support/access-control/RolesClass'; +import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; import { TableClass } from '../../support/entity/TableClass'; +import { TeamClass } from '../../support/team/TeamClass'; import { UserClass } from '../../support/user/UserClass'; import { checkDescriptionInEditModal, @@ -22,9 +29,10 @@ import { descriptionBox, redirectToHomePage, toastNotification, + uuid, visitUserProfilePage, } from '../../utils/common'; -import { updateDescription } from '../../utils/entity'; +import { addOwner, updateDescription } from '../../utils/entity'; import { clickOnLogo } from '../../utils/sidebar'; import { checkTaskCount, @@ -38,6 +46,7 @@ import { performUserLogin } from '../../utils/user'; const entity = new TableClass(); const entity2 = new TableClass(); const entity3 = new TableClass(); +const entity4 = new TableClass(); const user1 = new UserClass(); const user2 = new UserClass(); const adminUser = new UserClass(); @@ -61,7 +70,9 @@ test.describe('Activity feed', () => { await entity.create(apiContext); await entity2.create(apiContext); await entity3.create(apiContext); + await entity4.create(apiContext); await user1.create(apiContext); + await user2.create(apiContext); await afterAction(); }); @@ -71,7 +82,9 @@ test.describe('Activity feed', () => { await entity.delete(apiContext); await entity2.delete(apiContext); await entity3.delete(apiContext); + await entity4.delete(apiContext); await user1.delete(apiContext); + await user2.delete(apiContext); await adminUser.delete(apiContext); await afterAction(); @@ -245,22 +258,22 @@ test.describe('Activity feed', () => { test('Update Description Task on Columns', async ({ page }) => { const firstTaskValue: TaskDetails = { - term: entity.entity.name, + term: entity4.entity.name, assignee: user1.responseData.name, description: 'Column Description 1', - columnName: entity.entity.columns[0].name, - oldDescription: entity.entity.columns[0].description, + columnName: entity4.entity.columns[0].name, + oldDescription: entity4.entity.columns[0].description, }; const secondTaskValue: TaskDetails = { ...firstTaskValue, description: 'Column Description 2', - columnName: entity.entity.columns[1].name, - oldDescription: entity.entity.columns[1].description, + columnName: entity4.entity.columns[1].name, + oldDescription: entity4.entity.columns[1].description, }; await redirectToHomePage(page); - await entity.visitEntityPage(page); + await entity4.visitEntityPage(page); await page .getByRole('cell', { name: 'The ID of the store. This' }) @@ -370,7 +383,7 @@ test.describe('Activity feed', () => { await checkTaskCount(page, 0, 1); }); - test('Open and Closed Task tab', async ({ page }) => { + test('Open and Closed Task Tab', async ({ page }) => { const value: TaskDetails = { term: entity3.entity.name, assignee: user1.responseData.name, @@ -439,18 +452,79 @@ test.describe('Activity feed', () => { 'Closing the task with comment' ); }); + + test('Assignee field should not be disabled for owned entity tasks', async ({ + page, + }) => { + const value: TaskDetails = { + term: entity4.entity.name, + assignee: user1.responseData.name, + }; + await redirectToHomePage(page); + + await entity4.visitEntityPage(page); + + await addOwner({ + page, + owner: user2.responseData.displayName, + type: 'Users', + endpoint: EntityTypeEndpoint.Table, + dataTestId: 'data-assets-header', + }); + + await page.getByTestId('request-description').click(); + + // create description task + await createDescriptionTask(page, value); + }); }); base.describe('Activity feed with Data Steward User', () => { base.slow(true); + const id = uuid(); + const rules: PolicyRulesType[] = [ + { + name: 'viewRuleAllowed', + resources: ['All'], + operations: ['ViewAll'], + effect: 'allow', + }, + { + effect: 'deny', + name: 'editNotAllowed', + operations: ['EditAll'], + resources: ['All'], + }, + ]; + const viewAllUser = new UserClass(); + const viewAllPolicy = new PolicyClass(); + const viewAllRoles = new RolesClass(); + let viewAllTeam: TeamClass; + base.beforeAll('Setup pre-requests', async ({ browser }) => { const { afterAction, apiContext } = await performAdminLogin(browser); await entity.create(apiContext); await entity2.create(apiContext); + await entity3.create(apiContext); await user1.create(apiContext); await user2.create(apiContext); + await viewAllUser.create(apiContext); + await viewAllPolicy.create(apiContext, rules); + await viewAllRoles.create(apiContext, [viewAllPolicy.responseData.name]); + viewAllTeam = new TeamClass({ + name: `PW%team-${id}`, + displayName: `PW Team ${id}`, + description: 'playwright team description', + teamType: 'Group', + users: [viewAllUser.responseData.id], + defaultRoles: viewAllRoles.responseData.id + ? [viewAllRoles.responseData.id] + : [], + }); + await viewAllTeam.create(apiContext); + await afterAction(); }); @@ -458,8 +532,13 @@ base.describe('Activity feed with Data Steward User', () => { const { afterAction, apiContext } = await performAdminLogin(browser); await entity.delete(apiContext); await entity2.delete(apiContext); + await entity3.delete(apiContext); await user1.delete(apiContext); await user2.delete(apiContext); + await viewAllUser.delete(apiContext); + await viewAllPolicy.delete(apiContext); + await viewAllRoles.delete(apiContext); + await viewAllTeam.delete(apiContext); await afterAction(); }); @@ -749,4 +828,53 @@ base.describe('Activity feed with Data Steward User', () => { } ); }); + + base( + 'Accepting task should throw error for not having edit permission', + async ({ browser }) => { + const { page: page1, afterAction: afterActionUser1 } = + await performUserLogin(browser, user1); + const { page: page2, afterAction: afterActionUser2 } = + await performUserLogin(browser, viewAllUser); + + const value: TaskDetails = { + term: entity3.entity.name, + assignee: viewAllUser.responseData.name, + }; + + await base.step('Create and Assign Task to user 3', async () => { + await redirectToHomePage(page1); + await entity3.visitEntityPage(page1); + + await page1.getByTestId('request-description').click(); + + await createDescriptionTask(page1, value); + + await afterActionUser1(); + }); + + await base.step( + 'Accept Task By user 2 should throw error for since it has only viewAll permission', + async () => { + await redirectToHomePage(page2); + + await entity3.visitEntityPage(page2); + + await page2.getByTestId('activity_feed').click(); + + await page2.getByRole('menuitem', { name: 'Tasks' }).click(); + + await page2.getByText('Accept Suggestion').click(); + + await toastNotification( + page2, + // eslint-disable-next-line max-len + `Principal: CatalogPrincipal{name='${viewAllUser.responseData.name}'} operation EditDescription denied by role ${viewAllRoles.responseData.name}, policy ${viewAllPolicy.responseData.name}, rule editNotAllowed` + ); + + await afterActionUser2(); + } + ); + } + ); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts new file mode 100644 index 00000000000..b9916862fe6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts @@ -0,0 +1,65 @@ +/* + * 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 { APIRequestContext } from '@playwright/test'; +import { uuid } from '../../utils/common'; + +type ResponseDataType = { + name: string; + displayName: string; + description: string; + id?: string; + fullyQualifiedName?: string; +}; + +export type PolicyRulesType = { + name: string; + resources: string[]; + operations: string[]; + effect: string; +}; + +export class PolicyClass { + id = uuid(); + data: ResponseDataType; + responseData: ResponseDataType; + + constructor(data?: ResponseDataType) { + this.data = data ?? { + name: `PW%Policy-${this.id}`, + displayName: `PW Policy ${this.id}`, + description: 'playwright for policy description', + }; + } + + get() { + return this.responseData; + } + + async create(apiContext: APIRequestContext, rules: PolicyRulesType[]) { + const response = await apiContext.post('/api/v1/policies', { + data: { ...this.data, rules }, + }); + const data = await response.json(); + this.responseData = data; + + return data; + } + + async delete(apiContext: APIRequestContext) { + const response = await apiContext.delete( + `/api/v1/policies/${this.responseData.id}?hardDelete=true&recursive=true` + ); + + return await response.json(); + } +} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/RolesClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/RolesClass.ts new file mode 100644 index 00000000000..b8448620135 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/RolesClass.ts @@ -0,0 +1,58 @@ +/* + * 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 { APIRequestContext } from '@playwright/test'; +import { uuid } from '../../utils/common'; + +type ResponseDataType = { + name: string; + displayName: string; + description: string; + id?: string; + fullyQualifiedName?: string; +}; + +export class RolesClass { + id = uuid(); + data: ResponseDataType; + responseData: ResponseDataType; + + constructor(data?: ResponseDataType) { + this.data = data ?? { + name: `PW%Roles-${this.id}`, + displayName: `PW Roles ${this.id}`, + description: 'playwright for roles description', + }; + } + + get() { + return this.responseData; + } + + async create(apiContext: APIRequestContext, policies: string[]) { + const response = await apiContext.post('/api/v1/roles', { + data: { ...this.data, policies }, + }); + const data = await response.json(); + this.responseData = data; + + return data; + } + + async delete(apiContext: APIRequestContext) { + const response = await apiContext.delete( + `/api/v1/roles/${this.responseData.id}?hardDelete=true&recursive=true` + ); + + return await response.json(); + } +} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts index 6d1483d2273..0357026d153 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts @@ -20,6 +20,7 @@ type ResponseDataType = { id?: string; fullyQualifiedName?: string; users?: string[]; + defaultRoles?: string[]; }; export class TeamClass { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 301811b89e0..48b5cd8384f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -110,7 +110,7 @@ export const toastNotification = async ( ) => { await expect(page.getByRole('alert').first()).toHaveText(message); - await page.getByLabel('close').first().click(); + await page.getByLabel('close', { exact: true }).first().click(); }; export const clickOutside = async (page: Page) => {