mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-31 12:39:01 +00:00
playwright: migrate incident manager test to playwright (#17657)
* playwright: migrate incident manager test to playwright * migrate remaining test to playwright * added serial mode to prevent load while deploying pipeline
This commit is contained in:
parent
53cc84aef1
commit
b9311526a7
@ -1,538 +0,0 @@
|
||||
/*
|
||||
* 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 { interceptURL, verifyResponseStatusCode } from '../../common/common';
|
||||
import { triggerTestCasePipeline } from '../../common/Utils/DataQuality';
|
||||
import {
|
||||
createEntityTableViaREST,
|
||||
deleteEntityViaREST,
|
||||
visitEntityDetailsPage,
|
||||
} from '../../common/Utils/Entity';
|
||||
import { getToken } from '../../common/Utils/LocalStorage';
|
||||
import { generateRandomUser } from '../../common/Utils/Owner';
|
||||
import { uuid } from '../../constants/constants';
|
||||
import { EntityType, SidebarItem } from '../../constants/Entity.interface';
|
||||
import { DATABASE_SERVICE } from '../../constants/EntityConstant';
|
||||
const TABLE_NAME = DATABASE_SERVICE.entity.name;
|
||||
|
||||
const testSuite = {
|
||||
name: `${DATABASE_SERVICE.entity.databaseSchema}.${DATABASE_SERVICE.entity.name}.testSuite`,
|
||||
executableEntityReference: `${DATABASE_SERVICE.entity.databaseSchema}.${DATABASE_SERVICE.entity.name}`,
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
`cy_first_table_column_count_to_be_between_${uuid()}`,
|
||||
`cy_second_table_column_count_to_be_between_${uuid()}`,
|
||||
`cy_third_table_column_count_to_be_between_${uuid()}`,
|
||||
];
|
||||
const user1 = generateRandomUser();
|
||||
const user2 = generateRandomUser();
|
||||
const user3 = generateRandomUser();
|
||||
const userData1 = {
|
||||
displayName: `${user1.firstName}${user1.lastName}`,
|
||||
name: user1.email.split('@')[0],
|
||||
};
|
||||
const userData2 = {
|
||||
displayName: `${user2.firstName}${user2.lastName}`,
|
||||
name: user2.email.split('@')[0],
|
||||
};
|
||||
const userData3 = {
|
||||
displayName: `${user3.firstName}${user3.lastName}`,
|
||||
name: user3.email.split('@')[0],
|
||||
};
|
||||
const userIds: string[] = [];
|
||||
|
||||
const goToProfilerTab = () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`api/v1/tables/name/${DATABASE_SERVICE.service.name}.*.${TABLE_NAME}?fields=*&include=all`,
|
||||
'waitForPageLoad'
|
||||
);
|
||||
visitEntityDetailsPage({
|
||||
term: TABLE_NAME,
|
||||
serviceName: DATABASE_SERVICE.service.name,
|
||||
entity: EntityType.Table,
|
||||
});
|
||||
verifyResponseStatusCode('@waitForPageLoad', 200);
|
||||
|
||||
cy.get('[data-testid="profiler"]').should('be.visible').click();
|
||||
cy.get('[data-testid="profiler-tab-left-panel"]')
|
||||
.contains('Table Profile')
|
||||
.click();
|
||||
};
|
||||
|
||||
const acknowledgeTask = (testCase: string) => {
|
||||
goToProfilerTab();
|
||||
|
||||
cy.get('[data-testid="profiler-tab-left-panel"]')
|
||||
.contains('Data Quality')
|
||||
.click();
|
||||
cy.get(`[data-testid="${testCase}"]`)
|
||||
.find('.last-run-box.failed')
|
||||
.scrollIntoView()
|
||||
.should('be.visible');
|
||||
cy.get(`[data-testid="${testCase}-status"]`).should('contain', 'New');
|
||||
cy.get(`[data-testid="${testCase}"]`).contains(testCase).click();
|
||||
cy.get('[data-testid="edit-resolution-icon"]').click();
|
||||
cy.get('[data-testid="test-case-resolution-status-type"]').click();
|
||||
cy.get('[title="Ack"]').click();
|
||||
interceptURL(
|
||||
'POST',
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus',
|
||||
'updateTestCaseIncidentStatus'
|
||||
);
|
||||
cy.get('#update-status-button').click();
|
||||
verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200);
|
||||
cy.get(`[data-testid="${testCase}-status"]`).should('contain', 'Ack');
|
||||
};
|
||||
|
||||
const assignIncident = (testCaseName: string) => {
|
||||
cy.sidebarClick(SidebarItem.INCIDENT_MANAGER);
|
||||
cy.get(`[data-testid="test-case-${testCaseName}"]`).should('be.visible');
|
||||
cy.get(`[data-testid="${testCaseName}-status"]`)
|
||||
.find(`[data-testid="edit-resolution-icon"]`)
|
||||
.click();
|
||||
cy.get(`[data-testid="test-case-resolution-status-type"]`).click();
|
||||
cy.get(`[title="Assigned"]`).click();
|
||||
cy.get('#testCaseResolutionStatusDetails_assignee').should('be.visible');
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/search/suggest?q=*${user1.firstName}*${user1.lastName}*&index=user_search_index*`,
|
||||
'searchAssignee'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/users/name/*', 'userList');
|
||||
cy.get('#testCaseResolutionStatusDetails_assignee').click();
|
||||
cy.wait('@userList');
|
||||
cy.get('#testCaseResolutionStatusDetails_assignee').type(
|
||||
userData1.displayName
|
||||
);
|
||||
verifyResponseStatusCode('@searchAssignee', 200);
|
||||
cy.get(`[data-testid="${userData1.name.toLocaleLowerCase()}"]`).click();
|
||||
interceptURL(
|
||||
'POST',
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus',
|
||||
'updateTestCaseIncidentStatus'
|
||||
);
|
||||
cy.get('#update-status-button').click();
|
||||
verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200);
|
||||
cy.get(
|
||||
`[data-testid="${testCaseName}-status"] [data-testid="badge-container"]`
|
||||
).should('contain', 'Assigned');
|
||||
};
|
||||
|
||||
describe('Incident Manager', { tags: 'Observability' }, () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
||||
cy.getAllLocalStorage().then((data) => {
|
||||
const token = getToken(data);
|
||||
|
||||
// Create a new user
|
||||
for (const user of [user1, user2, user3]) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/v1/users/signup`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: user,
|
||||
}).then((response) => {
|
||||
userIds.push(response.body.id);
|
||||
});
|
||||
}
|
||||
|
||||
createEntityTableViaREST({
|
||||
token,
|
||||
...DATABASE_SERVICE,
|
||||
tables: [DATABASE_SERVICE.entity],
|
||||
});
|
||||
// create testSuite
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/v1/dataQuality/testSuites/executable`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: testSuite,
|
||||
}).then((testSuiteResponse) => {
|
||||
// creating test case
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/v1/dataQuality/testCases`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: {
|
||||
name: testCase,
|
||||
entityLink: `<#E::table::${testSuite.executableEntityReference}>`,
|
||||
parameterValues: [
|
||||
{ name: 'minColValue', value: 12 },
|
||||
{ name: 'maxColValue', value: 24 },
|
||||
],
|
||||
testDefinition: 'tableColumnCountToBeBetween',
|
||||
testSuite: testSuite.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/v1/services/ingestionPipelines`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: {
|
||||
airflowConfig: {},
|
||||
name: `${testSuite.executableEntityReference}_test_suite`,
|
||||
pipelineType: 'TestSuite',
|
||||
service: {
|
||||
id: testSuiteResponse.body.id,
|
||||
type: 'testSuite',
|
||||
},
|
||||
sourceConfig: {
|
||||
config: {
|
||||
type: 'TestSuite',
|
||||
entityFullyQualifiedName: testSuite.executableEntityReference,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).then((response) =>
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/v1/services/ingestionPipelines/deploy/${response.body.id}`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
triggerTestCasePipeline({
|
||||
serviceName: DATABASE_SERVICE.service.name,
|
||||
tableName: TABLE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.login();
|
||||
|
||||
cy.getAllLocalStorage().then((data) => {
|
||||
const token = getToken(data);
|
||||
deleteEntityViaREST({
|
||||
token,
|
||||
endPoint: EntityType.DatabaseService,
|
||||
entityName: DATABASE_SERVICE.service.name,
|
||||
});
|
||||
|
||||
// Delete created user
|
||||
userIds.forEach((userId) => {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: `/api/v1/users/${userId}?hardDelete=true&recursive=false`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic Scenario', () => {
|
||||
const testCaseName = testCases[0];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it("Acknowledge table test case's failure", () => {
|
||||
acknowledgeTask(testCaseName);
|
||||
});
|
||||
|
||||
it('Assign incident to user', () => {
|
||||
assignIncident(testCaseName);
|
||||
});
|
||||
|
||||
it('Re-assign incident to user', () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*',
|
||||
'getTestCase'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed');
|
||||
cy.sidebarClick(SidebarItem.INCIDENT_MANAGER);
|
||||
cy.get(`[data-testid="test-case-${testCaseName}"]`).click();
|
||||
verifyResponseStatusCode('@getTestCase', 200);
|
||||
cy.get('[data-testid="incident"]').click();
|
||||
verifyResponseStatusCode('@getTaskFeed', 200);
|
||||
cy.get('[data-testid="task-cta-buttons"] [role="img"]')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
cy.get('[role="menu"').find('[data-menu-id*="re-assign"]').click();
|
||||
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/search/suggest?q=*${user2.firstName}*${user2.lastName}*&index=user_search_index*`,
|
||||
'searchAssignee'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/users/name/*', 'userList');
|
||||
cy.get('[data-testid="select-assignee"]').click();
|
||||
cy.wait('@userList');
|
||||
cy.get('[data-testid="select-assignee"]').type(userData2.displayName);
|
||||
verifyResponseStatusCode('@searchAssignee', 200);
|
||||
cy.get(`[data-testid="${userData2.name.toLocaleLowerCase()}"]`).click();
|
||||
|
||||
interceptURL(
|
||||
'POST',
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus',
|
||||
'updateTestCaseIncidentStatus'
|
||||
);
|
||||
cy.get('.ant-modal-footer').contains('Submit').click();
|
||||
verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200);
|
||||
// Todo: skipping this for now as its not working from backend
|
||||
cy.clickOnLogo();
|
||||
cy.get('[id*="tab-tasks"]').click();
|
||||
cy.get('[data-testid="task-feed-card"]')
|
||||
.contains(testCaseName)
|
||||
.scrollIntoView()
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it("Re-assign incident from test case page's header", () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*',
|
||||
'getTestCase'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed');
|
||||
cy.sidebarClick(SidebarItem.INCIDENT_MANAGER);
|
||||
cy.get(`[data-testid="test-case-${testCaseName}"]`).click();
|
||||
verifyResponseStatusCode('@getTestCase', 200);
|
||||
interceptURL('GET', '/api/v1/users?*', 'getUsers');
|
||||
cy.get('[data-testid="assignee"] [data-testid="edit-owner"]').click();
|
||||
verifyResponseStatusCode('@getUsers', 200);
|
||||
cy.get('[data-testid="loader"]').should('not.exist');
|
||||
interceptURL('GET', `api/v1/search/query?q=*`, 'searchOwner');
|
||||
cy.get('[data-testid="owner-select-users-search-bar"]').type(
|
||||
userData3.displayName
|
||||
);
|
||||
verifyResponseStatusCode('@searchOwner', 200);
|
||||
interceptURL(
|
||||
'POST',
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus',
|
||||
'updateTestCaseIncidentStatus'
|
||||
);
|
||||
cy.get(`.ant-popover [title="${userData3.displayName}"]`).click();
|
||||
verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200);
|
||||
cy.get('[data-testid="assignee"] [data-testid="owner-link"]').should(
|
||||
'contain',
|
||||
userData3.displayName
|
||||
);
|
||||
});
|
||||
|
||||
it('Resolve incident', () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*',
|
||||
'getTestCase'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed');
|
||||
cy.sidebarClick(SidebarItem.INCIDENT_MANAGER);
|
||||
cy.get(`[data-testid="test-case-${testCaseName}"]`).click();
|
||||
verifyResponseStatusCode('@getTestCase', 200);
|
||||
cy.get('[data-testid="incident"]').click();
|
||||
verifyResponseStatusCode('@getTaskFeed', 200);
|
||||
cy.get('[data-testid="task-cta-buttons"]')
|
||||
.contains('Resolve')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
cy.get('#testCaseFailureReason').click();
|
||||
cy.get('[title="Missing Data"]').click();
|
||||
cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror')
|
||||
.click()
|
||||
.type('test');
|
||||
interceptURL(
|
||||
'POST',
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus',
|
||||
'updateTestCaseIncidentStatus'
|
||||
);
|
||||
cy.get('.ant-modal-footer').contains('Submit').click();
|
||||
verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resolving incident & re-run pipeline', () => {
|
||||
const testName = testCases[1];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it("Acknowledge table test case's failure", () => {
|
||||
acknowledgeTask(testName);
|
||||
});
|
||||
|
||||
it('Resolve task from incident list page', () => {
|
||||
goToProfilerTab();
|
||||
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases?fields=*&entityLink=*&includeAllTests=true&limit=*',
|
||||
'testCaseList'
|
||||
);
|
||||
cy.get('[data-testid="profiler-tab-left-panel"]')
|
||||
.contains('Data Quality')
|
||||
.click();
|
||||
verifyResponseStatusCode('@testCaseList', 200);
|
||||
cy.get(`[data-testid="${testName}"]`)
|
||||
.find('.last-run-box.failed')
|
||||
.scrollIntoView()
|
||||
.should('be.visible');
|
||||
cy.get('.ant-table-row-level-0').should('contain', 'Ack');
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*',
|
||||
'getIncidentList'
|
||||
);
|
||||
cy.sidebarClick(SidebarItem.INCIDENT_MANAGER);
|
||||
|
||||
verifyResponseStatusCode('@getIncidentList', 200);
|
||||
|
||||
cy.get(`[data-testid="test-case-${testName}"]`).should('be.visible');
|
||||
cy.get(`[data-testid="${testName}-status"]`)
|
||||
.find(`[data-testid="edit-resolution-icon"]`)
|
||||
.click();
|
||||
cy.get(`[data-testid="test-case-resolution-status-type"]`).click();
|
||||
cy.get(`[title="Resolved"]`).click();
|
||||
cy.get('#testCaseResolutionStatusDetails_testCaseFailureReason').click();
|
||||
cy.get('[title="Missing Data"]').click();
|
||||
cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror')
|
||||
.click()
|
||||
.type('test');
|
||||
interceptURL(
|
||||
'POST',
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus',
|
||||
'updateTestCaseIncidentStatus'
|
||||
);
|
||||
cy.get('.ant-modal-footer').contains('Submit').click();
|
||||
verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200);
|
||||
});
|
||||
|
||||
it('Task should be closed', () => {
|
||||
goToProfilerTab();
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*',
|
||||
'getTestCase'
|
||||
);
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases?fields=*&entityLink=*&includeAllTests=true&limit=*',
|
||||
'testCaseList'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed');
|
||||
cy.get('[data-testid="profiler-tab-left-panel"]')
|
||||
.contains('Data Quality')
|
||||
.click();
|
||||
verifyResponseStatusCode('@testCaseList', 200);
|
||||
cy.get(`[data-testid="${testName}"]`)
|
||||
.find('.last-run-box.failed')
|
||||
.scrollIntoView()
|
||||
.should('be.visible');
|
||||
|
||||
cy.get(`[data-testid="${testName}"]`).contains(testName).click();
|
||||
verifyResponseStatusCode('@getTestCase', 200);
|
||||
cy.get('[data-testid="incident"]').click();
|
||||
verifyResponseStatusCode('@getTaskFeed', 200);
|
||||
cy.get('[data-testid="closed-task"]').click();
|
||||
cy.get('[data-testid="task-feed-card"]').should('be.visible');
|
||||
cy.get('[data-testid="task-tab"]').should(
|
||||
'contain',
|
||||
'Resolved the Task.'
|
||||
);
|
||||
});
|
||||
|
||||
it('Re-run pipeline', () => {
|
||||
triggerTestCasePipeline({
|
||||
serviceName: DATABASE_SERVICE.service.name,
|
||||
tableName: TABLE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
it('Verify open and closed task', () => {
|
||||
acknowledgeTask(testName);
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*',
|
||||
'getTestCase'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed');
|
||||
cy.reload();
|
||||
verifyResponseStatusCode('@getTestCase', 200);
|
||||
cy.get('[data-testid="incident"]').click();
|
||||
verifyResponseStatusCode('@getTaskFeed', 200);
|
||||
cy.get('[data-testid="open-task"]')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text.trim()).equal('1 Open');
|
||||
});
|
||||
cy.get('[data-testid="closed-task"]')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text.trim()).equal('1 Closed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rerunning pipeline for an open incident', () => {
|
||||
const testName = testCases[2];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Ack incident and verify open task', () => {
|
||||
acknowledgeTask(testName);
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*',
|
||||
'getTestCase'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed');
|
||||
cy.reload();
|
||||
verifyResponseStatusCode('@getTestCase', 200);
|
||||
cy.get('[data-testid="incident"]').click();
|
||||
verifyResponseStatusCode('@getTaskFeed', 200);
|
||||
cy.get('[data-testid="open-task"]')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text.trim()).equal('1 Open');
|
||||
});
|
||||
});
|
||||
|
||||
it('Assign incident to user', () => {
|
||||
assignIncident(testName);
|
||||
});
|
||||
|
||||
it('Re-run pipeline', () => {
|
||||
triggerTestCasePipeline({
|
||||
serviceName: DATABASE_SERVICE.service.name,
|
||||
tableName: TABLE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
it("Verify incident's status on DQ page", () => {
|
||||
goToProfilerTab();
|
||||
|
||||
cy.get('[data-testid="profiler-tab-left-panel"]')
|
||||
.contains('Data Quality')
|
||||
.click();
|
||||
cy.get(`[data-testid="${testName}"]`)
|
||||
.find('.last-run-box.failed')
|
||||
.scrollIntoView()
|
||||
.should('be.visible');
|
||||
cy.get(`[data-testid="${testName}-status"]`).should(
|
||||
'contain',
|
||||
'Assigned'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,394 @@
|
||||
/*
|
||||
* 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, { expect } from '@playwright/test';
|
||||
import { SidebarItem } from '../../constant/sidebar';
|
||||
import { TableClass } from '../../support/entity/TableClass';
|
||||
import { UserClass } from '../../support/user/UserClass';
|
||||
import {
|
||||
createNewPage,
|
||||
descriptionBox,
|
||||
getApiContext,
|
||||
redirectToHomePage,
|
||||
} from '../../utils/common';
|
||||
import {
|
||||
acknowledgeTask,
|
||||
assignIncident,
|
||||
triggerTestSuitePipelineAndWaitForSuccess,
|
||||
visitProfilerTab,
|
||||
} from '../../utils/incidentManager';
|
||||
import { sidebarClick } from '../../utils/sidebar';
|
||||
|
||||
const user1 = new UserClass();
|
||||
const user2 = new UserClass();
|
||||
const user3 = new UserClass();
|
||||
const users = [user1, user2, user3];
|
||||
const table1 = new TableClass();
|
||||
|
||||
// use the admin user to login
|
||||
test.use({ storageState: 'playwright/.auth/admin.json' });
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.describe('Incident Manager', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
// since we need to poll for the pipeline status, we need to increase the timeout
|
||||
test.setTimeout(90000);
|
||||
|
||||
const { afterAction, apiContext, page } = await createNewPage(browser);
|
||||
|
||||
const { pipeline } = await table1.createTestSuiteAndPipelines(apiContext);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await table1.createTestCase(apiContext, {
|
||||
parameterValues: [
|
||||
{ name: 'minColValue', value: 12 },
|
||||
{ name: 'maxColValue', value: 24 },
|
||||
],
|
||||
testDefinition: 'tableColumnCountToBeBetween',
|
||||
});
|
||||
}
|
||||
await apiContext.post(
|
||||
`/api/v1/services/ingestionPipelines/deploy/${pipeline.id}`
|
||||
);
|
||||
await triggerTestSuitePipelineAndWaitForSuccess({
|
||||
page,
|
||||
table: table1,
|
||||
pipeline: { id: pipeline.id },
|
||||
apiContext,
|
||||
});
|
||||
|
||||
for (const user of users) {
|
||||
await user.create(apiContext);
|
||||
}
|
||||
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
for (const entity of [...users, table1]) {
|
||||
await entity.delete(apiContext);
|
||||
}
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.slow(true);
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await redirectToHomePage(page);
|
||||
});
|
||||
|
||||
test('Basic Scenario', async ({ page }) => {
|
||||
const testCase = table1.testCasesResponseData[0];
|
||||
const testCaseName = testCase?.['name'];
|
||||
const assignee = {
|
||||
name: user1.data.email.split('@')[0],
|
||||
displayName: user1.getUserName(),
|
||||
};
|
||||
|
||||
await test.step("Acknowledge table test case's failure", async () => {
|
||||
await acknowledgeTask({
|
||||
page,
|
||||
testCase: testCaseName,
|
||||
table: table1,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Assign incident to user', async () => {
|
||||
await assignIncident({
|
||||
page,
|
||||
testCaseName,
|
||||
user: assignee,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Re-assign incident to user', async () => {
|
||||
const assignee1 = {
|
||||
name: user2.data.email.split('@')[0],
|
||||
displayName: user2.getUserName(),
|
||||
};
|
||||
const testCaseResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*'
|
||||
);
|
||||
await page.click(`[data-testid="test-case-${testCaseName}"]`);
|
||||
|
||||
await testCaseResponse;
|
||||
|
||||
const incidentDetails = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus/stateId/*'
|
||||
);
|
||||
await page.click('[data-testid="incident"]');
|
||||
await incidentDetails;
|
||||
|
||||
await page.getByRole('button', { name: 'down' }).click();
|
||||
await page.waitForSelector('role=menuitem[name="Reassign"]', {
|
||||
state: 'visible',
|
||||
});
|
||||
await page.getByRole('menuitem', { name: 'Reassign' }).click();
|
||||
|
||||
const searchUserResponse = page.waitForResponse(
|
||||
`/api/v1/search/suggest?q=*${user2.data.firstName}*${user2.data.lastName}*&index=user_search_index*`
|
||||
);
|
||||
|
||||
await page.getByTestId('select-assignee').locator('div').click();
|
||||
await page.getByLabel('Assignee:').fill(assignee1.displayName);
|
||||
await searchUserResponse;
|
||||
|
||||
await page.click(`[data-testid="${assignee1.name.toLocaleLowerCase()}"]`);
|
||||
const updateAssignee = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus'
|
||||
);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await updateAssignee;
|
||||
});
|
||||
|
||||
await test.step(
|
||||
"Re-assign incident from test case page's header",
|
||||
async () => {
|
||||
const assignee2 = {
|
||||
name: user3.data.email.split('@')[0],
|
||||
displayName: user3.getUserName(),
|
||||
};
|
||||
const testCaseResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/name/*?fields=*'
|
||||
);
|
||||
await page.reload();
|
||||
|
||||
await testCaseResponse;
|
||||
|
||||
const listUserResponse = page.waitForResponse('/api/v1/users?*');
|
||||
await page.click('[data-testid="assignee"] [data-testid="edit-owner"]');
|
||||
listUserResponse;
|
||||
await page.waitForSelector('[data-testid="loader"]', {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
const searchUserResponse = page.waitForResponse(
|
||||
'/api/v1/search/query?q=*'
|
||||
);
|
||||
await page.fill(
|
||||
'[data-testid="owner-select-users-search-bar"]',
|
||||
assignee2.displayName
|
||||
);
|
||||
await searchUserResponse;
|
||||
|
||||
const updateIncident = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus'
|
||||
);
|
||||
await page.click(`.ant-popover [title="${assignee2.displayName}"]`);
|
||||
await updateIncident;
|
||||
|
||||
await page.waitForSelector(
|
||||
'[data-testid="assignee"] [data-testid="owner-link"]'
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator('[data-testid="assignee"] [data-testid="owner-link"]')
|
||||
).toContainText(assignee2.displayName);
|
||||
}
|
||||
);
|
||||
|
||||
await test.step('Resolve incident', async () => {
|
||||
await page.click('[data-testid="incident"]');
|
||||
await page.getByRole('button', { name: 'Resolve' }).click();
|
||||
await page.click('#testCaseFailureReason');
|
||||
await page.click('[title="Missing Data"]');
|
||||
await page.click(descriptionBox);
|
||||
await page.fill(descriptionBox, 'test');
|
||||
|
||||
const updateIncident = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus'
|
||||
);
|
||||
await page.click('.ant-modal-footer >> text=Submit');
|
||||
await updateIncident;
|
||||
});
|
||||
});
|
||||
|
||||
test('Resolving incident & re-run pipeline', async ({ page }) => {
|
||||
const testCase = table1.testCasesResponseData[1];
|
||||
const testCaseName = testCase?.['name'];
|
||||
const pipeline = table1.testSuitePipelineResponseData[0];
|
||||
const { apiContext } = await getApiContext(page);
|
||||
|
||||
await test.step("Acknowledge table test case's failure", async () => {
|
||||
await acknowledgeTask({
|
||||
page,
|
||||
testCase: testCaseName,
|
||||
table: table1,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Resolve task from incident list page', async () => {
|
||||
await visitProfilerTab(page, table1);
|
||||
const testCaseResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases?fields=*'
|
||||
);
|
||||
await page
|
||||
.getByTestId('profiler-tab-left-panel')
|
||||
.getByText('Data Quality')
|
||||
.click();
|
||||
await testCaseResponse;
|
||||
|
||||
await expect(
|
||||
page.locator(`[data-testid="${testCaseName}"] .last-run-box.failed`)
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId(`${testCaseName}-status`)).toContainText(
|
||||
'Ack'
|
||||
);
|
||||
|
||||
const incidentDetailsRes = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*'
|
||||
);
|
||||
await sidebarClick(page, SidebarItem.INCIDENT_MANAGER);
|
||||
await incidentDetailsRes;
|
||||
|
||||
await expect(
|
||||
page.locator(`[data-testid="test-case-${testCaseName}"]`)
|
||||
).toBeVisible();
|
||||
|
||||
await page.click(
|
||||
`[data-testid="${testCaseName}-status"] [data-testid="edit-resolution-icon"]`
|
||||
);
|
||||
await page.click(`[data-testid="test-case-resolution-status-type"]`);
|
||||
await page.click(`[title="Resolved"]`);
|
||||
await page.click(
|
||||
'#testCaseResolutionStatusDetails_testCaseFailureReason'
|
||||
);
|
||||
await page.click('[title="Missing Data"]');
|
||||
await page.click(descriptionBox);
|
||||
await page.fill(descriptionBox, 'test');
|
||||
const updateTestCaseIncidentStatus = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus'
|
||||
);
|
||||
await page.click('.ant-modal-footer >> text=Submit');
|
||||
await updateTestCaseIncidentStatus;
|
||||
});
|
||||
|
||||
await test.step('Task should be closed', async () => {
|
||||
await visitProfilerTab(page, table1);
|
||||
const testCaseResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases?fields=*'
|
||||
);
|
||||
await page
|
||||
.getByTestId('profiler-tab-left-panel')
|
||||
.getByText('Data Quality')
|
||||
.click();
|
||||
await testCaseResponse;
|
||||
|
||||
await expect(
|
||||
page.locator(`[data-testid="${testCaseName}"] .last-run-box.failed`)
|
||||
).toBeVisible();
|
||||
|
||||
await page.click(
|
||||
`[data-testid="${testCaseName}"] >> text=${testCaseName}`
|
||||
);
|
||||
await page.click('[data-testid="incident"]');
|
||||
await page.click('[data-testid="closed-task"]');
|
||||
await page.waitForSelector('[data-testid="task-feed-card"]');
|
||||
|
||||
await expect(page.locator('[data-testid="task-tab"]')).toContainText(
|
||||
'Resolved the Task.'
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Re-run pipeline', async () => {
|
||||
await triggerTestSuitePipelineAndWaitForSuccess({
|
||||
page,
|
||||
table: table1,
|
||||
pipeline: { id: pipeline?.['id'] },
|
||||
apiContext,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Verify open and closed task', async () => {
|
||||
await acknowledgeTask({
|
||||
page,
|
||||
testCase: testCaseName,
|
||||
table: table1,
|
||||
});
|
||||
await page.reload();
|
||||
|
||||
await page.click('[data-testid="incident"]');
|
||||
|
||||
await expect(page.locator(`[data-testid="open-task"]`)).toHaveText(
|
||||
'1 Open'
|
||||
);
|
||||
await expect(page.locator(`[data-testid="closed-task"]`)).toHaveText(
|
||||
'1 Closed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Rerunning pipeline for an open incident', async ({ page }) => {
|
||||
const testCase = table1.testCasesResponseData[2];
|
||||
const testCaseName = testCase?.['name'];
|
||||
const pipeline = table1.testSuitePipelineResponseData[0];
|
||||
const assignee = {
|
||||
name: user1.data.email.split('@')[0],
|
||||
displayName: user1.getUserName(),
|
||||
};
|
||||
const { apiContext } = await getApiContext(page);
|
||||
|
||||
await test.step('Ack incident and verify open task', async () => {
|
||||
await acknowledgeTask({
|
||||
page,
|
||||
testCase: testCaseName,
|
||||
table: table1,
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
|
||||
await page.click('[data-testid="incident"]');
|
||||
|
||||
await expect(page.locator(`[data-testid="open-task"]`)).toHaveText(
|
||||
'1 Open'
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Assign incident to user', async () => {
|
||||
await assignIncident({
|
||||
page,
|
||||
testCaseName,
|
||||
user: assignee,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Re-run pipeline', async () => {
|
||||
await triggerTestSuitePipelineAndWaitForSuccess({
|
||||
page,
|
||||
table: table1,
|
||||
pipeline: { id: pipeline?.['id'] },
|
||||
apiContext,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Verify incident's status on DQ page", async () => {
|
||||
await visitProfilerTab(page, table1);
|
||||
const testCaseResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases?fields=*'
|
||||
);
|
||||
await page
|
||||
.getByTestId('profiler-tab-left-panel')
|
||||
.getByText('Data Quality')
|
||||
.click();
|
||||
await testCaseResponse;
|
||||
|
||||
await expect(
|
||||
page.locator(`[data-testid="${testCaseName}"] .last-run-box.failed`)
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId(`${testCaseName}-status`)).toContainText(
|
||||
'Assigned'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -65,3 +65,8 @@ export enum ENTITY_PATH {
|
||||
'apiEndpoints' = 'apiEndpoint',
|
||||
'dataProducts' = 'dataProduct',
|
||||
}
|
||||
|
||||
export type TestCaseData = {
|
||||
parameterValues: unknown[];
|
||||
testDefinition: string;
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ import { APIRequestContext, Page } from '@playwright/test';
|
||||
import { SERVICE_TYPE } from '../../constant/service';
|
||||
import { uuid } from '../../utils/common';
|
||||
import { visitEntityPage } from '../../utils/entity';
|
||||
import { EntityTypeEndpoint } from './Entity.interface';
|
||||
import { EntityTypeEndpoint, TestCaseData } from './Entity.interface';
|
||||
import { EntityClass } from './EntityClass';
|
||||
|
||||
export class TableClass extends EntityClass {
|
||||
@ -169,7 +169,7 @@ export class TableClass extends EntityClass {
|
||||
|
||||
async createTestSuiteAndPipelines(apiContext: APIRequestContext) {
|
||||
if (!this.entityResponseData) {
|
||||
return this.create(apiContext);
|
||||
await this.create(apiContext);
|
||||
}
|
||||
|
||||
const testSuiteData = await apiContext
|
||||
@ -177,7 +177,7 @@ export class TableClass extends EntityClass {
|
||||
data: {
|
||||
name: `pw-test-suite-${uuid()}`,
|
||||
executableEntityReference:
|
||||
this.entityResponseData['fullyQualifiedName'],
|
||||
this.entityResponseData?.['fullyQualifiedName'],
|
||||
description: 'Playwright test suite for table',
|
||||
},
|
||||
})
|
||||
@ -221,12 +221,16 @@ export class TableClass extends EntityClass {
|
||||
},
|
||||
})
|
||||
.then((res) => res.json());
|
||||
|
||||
this.testSuitePipelineResponseData.push(pipelineData);
|
||||
|
||||
return pipelineData;
|
||||
}
|
||||
|
||||
async createTestCase(apiContext: APIRequestContext) {
|
||||
async createTestCase(
|
||||
apiContext: APIRequestContext,
|
||||
testCaseData?: TestCaseData
|
||||
) {
|
||||
if (!this.testSuiteResponseData) {
|
||||
await this.createTestSuiteAndPipelines(apiContext);
|
||||
}
|
||||
@ -236,9 +240,10 @@ export class TableClass extends EntityClass {
|
||||
data: {
|
||||
name: `pw-test-case-${uuid()}`,
|
||||
entityLink: `<#E::table::${this.entityResponseData?.['fullyQualifiedName']}>`,
|
||||
testDefinition: 'tableRowCountToBeBetween',
|
||||
testDefinition:
|
||||
testCaseData?.testDefinition ?? 'tableRowCountToBeBetween',
|
||||
testSuite: this.testSuiteResponseData?.['fullyQualifiedName'],
|
||||
parameterValues: [
|
||||
parameterValues: testCaseData?.parameterValues ?? [
|
||||
{ name: 'minValue', value: 12 },
|
||||
{ name: 'maxValue', value: 34 },
|
||||
],
|
||||
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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, expect, Page } from '@playwright/test';
|
||||
import { SidebarItem } from '../constant/sidebar';
|
||||
import { TableClass } from '../support/entity/TableClass';
|
||||
import { redirectToHomePage } from './common';
|
||||
import { sidebarClick } from './sidebar';
|
||||
|
||||
export const visitProfilerTab = async (page: Page, table: TableClass) => {
|
||||
await redirectToHomePage(page);
|
||||
await table.visitEntityPage(page);
|
||||
await page.click('[data-testid="profiler"]');
|
||||
};
|
||||
|
||||
export const acknowledgeTask = async (data: {
|
||||
testCase: string;
|
||||
page: Page;
|
||||
table: TableClass;
|
||||
}) => {
|
||||
const { testCase, page, table } = data;
|
||||
await visitProfilerTab(page, table);
|
||||
await page.click('[data-testid="profiler-tab-left-panel"]');
|
||||
await page
|
||||
.getByTestId('profiler-tab-left-panel')
|
||||
.getByText('Data Quality')
|
||||
.click();
|
||||
await page.click(`[data-testid="${testCase}"] >> .last-run-box.failed`);
|
||||
await page.waitForSelector(`[data-testid="${testCase}-status"] >> text=New`);
|
||||
await page.click(`[data-testid="${testCase}"] >> text=${testCase}`);
|
||||
await page.click('[data-testid="edit-resolution-icon"]');
|
||||
await page.click('[data-testid="test-case-resolution-status-type"]');
|
||||
await page.click('[title="Ack"]');
|
||||
const statusChangeResponse = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus'
|
||||
);
|
||||
await page.click('#update-status-button');
|
||||
await statusChangeResponse;
|
||||
await page.waitForSelector(`[data-testid="${testCase}-status"] >> text=Ack`);
|
||||
};
|
||||
|
||||
export const assignIncident = async (data: {
|
||||
testCaseName: string;
|
||||
page: Page;
|
||||
user: { name: string; displayName: string };
|
||||
}) => {
|
||||
const { testCaseName, page, user } = data;
|
||||
await sidebarClick(page, SidebarItem.INCIDENT_MANAGER);
|
||||
await page.waitForSelector(`[data-testid="test-case-${testCaseName}"]`);
|
||||
await page.click(
|
||||
`[data-testid="${testCaseName}-status"] [data-testid="edit-resolution-icon"]`
|
||||
);
|
||||
await page.click('[data-testid="test-case-resolution-status-type"]');
|
||||
await page.click('[title="Assigned"]');
|
||||
await page.waitForSelector('#testCaseResolutionStatusDetails_assignee');
|
||||
await page.fill(
|
||||
'#testCaseResolutionStatusDetails_assignee',
|
||||
user.displayName
|
||||
);
|
||||
await page.waitForResponse('/api/v1/search/suggest?q=*');
|
||||
await page.click(`[data-testid="${user.name.toLocaleLowerCase()}"]`);
|
||||
const updateIncident = page.waitForResponse(
|
||||
'/api/v1/dataQuality/testCases/testCaseIncidentStatus'
|
||||
);
|
||||
await page.click('#update-status-button');
|
||||
await updateIncident;
|
||||
await page.waitForSelector(
|
||||
`[data-testid="${testCaseName}-status"] [data-testid="badge-container"] >> text=Assigned`
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-testid="${testCaseName}-status"] [data-testid="badge-container"]`
|
||||
)
|
||||
).toContainText('Assigned');
|
||||
};
|
||||
|
||||
export const triggerTestSuitePipelineAndWaitForSuccess = async (data: {
|
||||
page: Page;
|
||||
apiContext: APIRequestContext;
|
||||
table: TableClass;
|
||||
pipeline: { id: string };
|
||||
}) => {
|
||||
const { page, apiContext, table, pipeline } = data;
|
||||
// wait for 2s before the pipeline to be run
|
||||
await page.waitForTimeout(2000);
|
||||
await apiContext
|
||||
.post(`/api/v1/services/ingestionPipelines/trigger/${pipeline.id}`)
|
||||
.then((res) => {
|
||||
if (res.status() !== 200) {
|
||||
return apiContext.post(
|
||||
`/api/v1/services/ingestionPipelines/trigger/${pipeline.id}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the run to complete
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiContext
|
||||
.get(
|
||||
`/api/v1/services/ingestionPipelines?fields=pipelineStatuses&testSuite=${table.testSuiteResponseData?.['fullyQualifiedName']}&pipelineType=TestSuite`
|
||||
)
|
||||
.then((res) => res.json());
|
||||
|
||||
return response.data?.[0]?.pipelineStatuses?.pipelineState;
|
||||
},
|
||||
{
|
||||
// Custom expect message for reporting, optional.
|
||||
message: 'Wait for the pipeline to be successful',
|
||||
timeout: 60_000,
|
||||
intervals: [5_000, 10_000],
|
||||
}
|
||||
)
|
||||
.toBe('success');
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user