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:
Shailesh Parmar 2024-09-03 12:25:16 +05:30
parent 53cc84aef1
commit b9311526a7
5 changed files with 538 additions and 544 deletions

View File

@ -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'
);
});
});
});

View File

@ -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'
);
});
});
});

View File

@ -65,3 +65,8 @@ export enum ENTITY_PATH {
'apiEndpoints' = 'apiEndpoint',
'dataProducts' = 'dataProduct',
}
export type TestCaseData = {
parameterValues: unknown[];
testDefinition: string;
};

View File

@ -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 },
],

View File

@ -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');
};