playwright: migrate custom metric spec (#16690)

* playwright: migrate custom metric spec

* fixed customMetric failure
This commit is contained in:
Shailesh Parmar 2024-08-08 10:43:36 +05:30 committed by GitHub
parent 8ef1ab1ff8
commit f7a7529461
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 300 additions and 341 deletions

View File

@ -1,341 +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, hardDeleteService } from '../../common/EntityUtils';
import { visitEntityDetailsPage } from '../../common/Utils/Entity';
import { getToken } from '../../common/Utils/LocalStorage';
import {
INVALID_NAMES,
NAME_MIN_MAX_LENGTH_VALIDATION_ERROR_1_128,
NAME_VALIDATION_ERROR,
uuid,
} from '../../constants/constants';
import { EntityType } from '../../constants/Entity.interface';
import { DATABASE_SERVICE } from '../../constants/EntityConstant';
import { SERVICE_CATEGORIES } from '../../constants/service.constants';
const TABLE_CUSTOM_METRIC = {
name: `tableCustomMetric-${uuid()}`,
expression: `SELECT * FROM ${DATABASE_SERVICE.entity.name}`,
};
const COLUMN_CUSTOM_METRIC = {
name: `tableCustomMetric-${uuid()}`,
column: DATABASE_SERVICE.entity.columns[0].name,
expression: `SELECT * FROM ${DATABASE_SERVICE.entity.name}`,
};
const validateForm = (isColumnMetric = false) => {
// error messages
cy.get('#name_help').scrollIntoView().should('contain', 'Name is required');
cy.get('#expression_help')
.scrollIntoView()
.should('contain', 'SQL Query is required');
if (isColumnMetric) {
cy.get('#columnName_help')
.scrollIntoView()
.should('contain', 'Column is required');
}
// max length validation
cy.get('#name').scrollIntoView().type(INVALID_NAMES.MAX_LENGTH);
cy.get('#name_help').should(
'contain',
NAME_MIN_MAX_LENGTH_VALIDATION_ERROR_1_128
);
// with special char validation
cy.get('#name')
.should('be.visible')
.clear()
.type(INVALID_NAMES.WITH_SPECIAL_CHARS);
cy.get('#name_help').should('contain', NAME_VALIDATION_ERROR);
cy.get('#name').clear();
};
type CustomMetricDetails = {
term: string;
serviceName: string;
entity: EntityType.Table;
isColumnMetric?: boolean;
metric: {
name: string;
column?: string;
expression: string;
};
};
const createCustomMetric = ({
term,
serviceName,
entity,
isColumnMetric = false,
metric,
}: CustomMetricDetails) => {
interceptURL('PUT', '/api/v1/tables/*/customMetric', 'createCustomMetric');
interceptURL(
'GET',
'/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all',
'getCustomMetric'
);
visitEntityDetailsPage({
term,
serviceName,
entity,
});
// Click on create custom metric button
cy.get('[data-testid="profiler"]').click();
verifyResponseStatusCode('@getCustomMetric', 200);
cy.get('[data-testid="profiler-tab-left-panel"]')
.contains(isColumnMetric ? 'Column Profile' : 'Table Profile')
.click();
cy.get('[data-testid="profiler-add-table-test-btn"]').click();
cy.get('[data-testid="custom-metric"]').click();
// validate redirection and cancel button
cy.get('[data-testid="heading"]').first().should('be.visible');
cy.get(
`[data-testid=${
isColumnMetric
? 'profiler-tab-container'
: 'table-profiler-chart-container'
}]`
).should('be.visible');
cy.get('[data-testid="cancel-button"]').click();
verifyResponseStatusCode('@getCustomMetric', 200);
cy.url().should('include', 'profiler');
cy.get('[data-testid="heading"]')
.first()
.invoke('text')
.should('equal', isColumnMetric ? 'Column Profile' : 'Table Profile');
// Click on create custom metric button
cy.get('[data-testid="profiler-add-table-test-btn"]').click();
cy.get('[data-testid="custom-metric"]').click();
cy.get('[data-testid="submit-button"]').click();
validateForm(isColumnMetric);
// fill form and submit
cy.get('#name').type(metric.name);
if (isColumnMetric) {
cy.get('#columnName').click();
cy.get(`[title="${metric.column}"]`).click();
}
metric.expression &&
cy.get('.CodeMirror-scroll').click().type(metric.expression);
cy.get('[data-testid="submit-button"]').click();
verifyResponseStatusCode('@createCustomMetric', 200);
toastNotification(`${metric.name} created successfully.`);
verifyResponseStatusCode('@getCustomMetric', 200);
// verify the created custom metric
cy.url().should('include', 'profiler');
cy.get('[data-testid="heading"]')
.first()
.invoke('text')
.should('equal', isColumnMetric ? 'Column Profile' : 'Table Profile');
cy.get(`[data-testid="${metric.name}-custom-metrics"]`)
.scrollIntoView()
.should('be.visible');
};
const editCustomMetric = ({
term,
serviceName,
entity,
isColumnMetric = false,
metric,
}: CustomMetricDetails) => {
interceptURL(
'GET',
'/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all',
'getCustomMetric'
);
interceptURL('PUT', '/api/v1/tables/*/customMetric', 'editCustomMetric');
visitEntityDetailsPage({
term,
serviceName,
entity,
});
cy.get('[data-testid="profiler"]').click();
verifyResponseStatusCode('@getCustomMetric', 200);
cy.get('[data-testid="profiler-tab-left-panel"]')
.contains(isColumnMetric ? 'Column Profile' : 'Table Profile')
.click();
if (isColumnMetric) {
metric.column &&
cy.get('[data-row-key="user_id"]').contains(metric.column).click();
}
cy.get(`[data-testid="${metric.name}-custom-metrics"]`)
.scrollIntoView()
.should('be.visible');
cy.get(`[data-testid="${metric.name}-custom-metrics-menu"]`).click();
cy.get(`[data-menu-id*="edit"]`).click();
// validate cancel button
cy.get('.ant-modal-content').should('be.visible');
cy.get('.ant-modal-footer').contains('Cancel').click();
cy.get('.ant-modal-content').should('not.exist');
// edit expression and submit
cy.get(`[data-testid="${metric.name}-custom-metrics-menu"]`).click();
cy.get(`[data-menu-id*="edit"]`).click();
cy.get('.CodeMirror-scroll').click().type('updated');
cy.get('.ant-modal-footer').contains('Save').click();
cy.wait('@editCustomMetric').then(({ request }) => {
expect(request.body.expression).to.have.string('updated');
});
toastNotification(`${metric.name} updated successfully.`);
};
const deleteCustomMetric = ({
term,
serviceName,
entity,
metric,
isColumnMetric = false,
}: CustomMetricDetails) => {
interceptURL(
'GET',
'/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all',
'getCustomMetric'
);
interceptURL(
'DELETE',
isColumnMetric
? `/api/v1/tables/*/customMetric/${metric.column}/${metric.name}*`
: `/api/v1/tables/*/customMetric/${metric.name}*`,
'deleteCustomMetric'
);
visitEntityDetailsPage({
term,
serviceName,
entity,
});
cy.get('[data-testid="profiler"]').click();
verifyResponseStatusCode('@getCustomMetric', 200);
cy.get('[data-testid="profiler-tab-left-panel"]')
.contains(isColumnMetric ? 'Column Profile' : 'Table Profile')
.click();
if (isColumnMetric) {
metric.column &&
cy.get('[data-row-key="user_id"]').contains(metric.column).click();
}
cy.get(`[data-testid="${metric.name}-custom-metrics"]`)
.scrollIntoView()
.should('be.visible');
cy.get(`[data-testid="${metric.name}-custom-metrics-menu"]`).click();
cy.get(`[data-menu-id*="delete"]`).click();
cy.get('.ant-modal-header').should('contain', metric.name);
cy.get('[data-testid="confirmation-text-input"]').type('DELETE');
cy.get('[data-testid="confirm-button"]').click();
verifyResponseStatusCode('@deleteCustomMetric', 200);
toastNotification(`"${metric.name}" deleted successfully!`);
};
describe('Custom Metric', { tags: 'Observability' }, () => {
before(() => {
cy.login();
cy.getAllLocalStorage().then((data) => {
const token = getToken(data);
createEntityTable({
token,
...DATABASE_SERVICE,
tables: [DATABASE_SERVICE.entity],
});
});
});
after(() => {
cy.login();
cy.getAllLocalStorage().then((data) => {
const token = getToken(data);
hardDeleteService({
token,
serviceFqn: DATABASE_SERVICE.service.name,
serviceType: SERVICE_CATEGORIES.DATABASE_SERVICES,
});
});
});
beforeEach(() => {
cy.login();
});
it('Create table custom metric', () => {
createCustomMetric({
term: DATABASE_SERVICE.entity.name,
serviceName: DATABASE_SERVICE.service.name,
entity: EntityType.Table,
metric: TABLE_CUSTOM_METRIC,
});
});
it("Edit table custom metric's expression", () => {
editCustomMetric({
term: DATABASE_SERVICE.entity.name,
serviceName: DATABASE_SERVICE.service.name,
entity: EntityType.Table,
metric: TABLE_CUSTOM_METRIC,
});
});
it('Delete table custom metric', () => {
deleteCustomMetric({
term: DATABASE_SERVICE.entity.name,
serviceName: DATABASE_SERVICE.service.name,
entity: EntityType.Table,
metric: TABLE_CUSTOM_METRIC,
});
});
it('Create column custom metric', () => {
createCustomMetric({
term: DATABASE_SERVICE.entity.name,
serviceName: DATABASE_SERVICE.service.name,
entity: EntityType.Table,
metric: COLUMN_CUSTOM_METRIC,
isColumnMetric: true,
});
});
it("Edit column custom metric's expression", () => {
editCustomMetric({
term: DATABASE_SERVICE.entity.name,
serviceName: DATABASE_SERVICE.service.name,
entity: EntityType.Table,
metric: COLUMN_CUSTOM_METRIC,
isColumnMetric: true,
});
});
it('Delete column custom metric', () => {
deleteCustomMetric({
term: DATABASE_SERVICE.entity.name,
serviceName: DATABASE_SERVICE.service.name,
entity: EntityType.Table,
metric: COLUMN_CUSTOM_METRIC,
isColumnMetric: true,
});
});
});

View File

@ -0,0 +1,32 @@
/*
* 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.
*/
export const TAG_INVALID_NAMES = {
MIN_LENGTH: 'c',
MAX_LENGTH: 'a87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890ab',
WITH_SPECIAL_CHARS: '!@#$%^&*()',
};
export const INVALID_NAMES = {
MAX_LENGTH:
'a87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890aba87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890abName can be a maximum of 128 characters',
WITH_SPECIAL_CHARS: '::normalName::',
};
export const NAME_VALIDATION_ERROR =
'Name must contain only letters, numbers, underscores, hyphens, periods, parenthesis, and ampersands.';
export const NAME_MIN_MAX_LENGTH_VALIDATION_ERROR =
'Name size must be between 2 and 64';
export const NAME_MAX_LENGTH_VALIDATION_ERROR =
'Name size must be between 1 and 128';

View File

@ -0,0 +1,97 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import test from '@playwright/test';
import { TableClass } from '../../support/entity/TableClass';
import { getApiContext, redirectToHomePage, uuid } from '../../utils/common';
import {
createCustomMetric,
deleteCustomMetric,
} from '../../utils/customMetric';
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
test('Table custom metric', async ({ page }) => {
const table = new TableClass();
await redirectToHomePage(page);
const { afterAction, apiContext } = await getApiContext(page);
await table.create(apiContext);
const TABLE_CUSTOM_METRIC = {
name: `tableCustomMetric-${uuid()}`,
expression: `SELECT * FROM ${table.entity.name}`,
};
await test.step('Create', async () => {
const profilerResponse = page.waitForResponse(
`/api/v1/tables/${table.entityResponseData?.['fullyQualifiedName']}/tableProfile/latest`
);
await table.visitEntityPage(page);
await page.click('[data-testid="profiler"]');
await profilerResponse;
await page.waitForTimeout(1000);
await createCustomMetric({
page,
metric: TABLE_CUSTOM_METRIC,
});
});
await test.step('Delete', async () => {
await deleteCustomMetric({
page,
metric: TABLE_CUSTOM_METRIC,
});
});
await table.delete(apiContext);
await afterAction();
});
test('Column custom metric', async ({ page }) => {
const table = new TableClass();
await redirectToHomePage(page);
const { afterAction, apiContext } = await getApiContext(page);
await table.create(apiContext);
const COLUMN_CUSTOM_METRIC = {
name: `columnCustomMetric-${uuid()}`,
column: table.entity.columns[0].name,
expression: `SELECT * FROM ${table.entity.name}`,
};
await test.step('Create', async () => {
const profilerResponse = page.waitForResponse(
`/api/v1/tables/${table.entityResponseData?.['fullyQualifiedName']}/tableProfile/latest`
);
await table.visitEntityPage(page);
await page.click('[data-testid="profiler"]');
await profilerResponse;
await page.waitForTimeout(1000);
await createCustomMetric({
page,
metric: COLUMN_CUSTOM_METRIC,
isColumnMetric: true,
});
});
await test.step('Delete', async () => {
await deleteCustomMetric({
page,
metric: COLUMN_CUSTOM_METRIC,
isColumnMetric: true,
});
});
await table.delete(apiContext);
await afterAction();
});

View File

@ -0,0 +1,171 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page } from '@playwright/test';
import {
INVALID_NAMES,
NAME_MAX_LENGTH_VALIDATION_ERROR,
NAME_VALIDATION_ERROR,
} from '../constant/common';
type CustomMetricDetails = {
page: Page;
isColumnMetric?: boolean;
metric: {
name: string;
column?: string;
expression: string;
};
};
const validateForm = async (page: Page, isColumnMetric = false) => {
// error messages
await expect(page.locator('#name_help')).toHaveText('Name is required');
await expect(page.locator('#expression_help')).toHaveText(
'SQL Query is required.'
);
if (isColumnMetric) {
await expect(page.locator('#columnName_help')).toHaveText(
'Column is required.'
);
}
// max length validation
await page.locator('#name').fill(INVALID_NAMES.MAX_LENGTH);
await expect(page.locator('#name_help')).toHaveText(
NAME_MAX_LENGTH_VALIDATION_ERROR
);
// with special char validation
await page.locator('#name').fill(INVALID_NAMES.WITH_SPECIAL_CHARS);
await expect(page.locator('#name_help')).toHaveText(NAME_VALIDATION_ERROR);
await page.locator('#name').clear();
};
export const createCustomMetric = async ({
page,
isColumnMetric = false,
metric,
}: CustomMetricDetails) => {
await page
.getByRole('menuitem', {
name: isColumnMetric ? 'Column Profile' : 'Table Profile',
})
.click();
await page.locator('[data-testid="profiler-add-table-test-btn"]').click();
await page.locator('[data-testid="custom-metric"]').click();
const customMetricResponse = page.waitForResponse(
'/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all'
);
// validate redirection and cancel button
await expect(page.locator('[data-testid="heading"]')).toBeVisible();
await expect(
page.locator(
`[data-testid="${
isColumnMetric
? 'profiler-tab-container'
: 'table-profiler-chart-container'
}"]`
)
).toBeVisible();
await page.locator('[data-testid="cancel-button"]').click();
await customMetricResponse;
await expect(page).toHaveURL(/profiler/);
await expect(
page.getByRole('heading', {
name: isColumnMetric ? 'Column Profile' : 'Table Profile',
})
).toBeVisible();
// Click on create custom metric button
await page.click('[data-testid="profiler-add-table-test-btn"]');
await page.click('[data-testid="custom-metric"]');
await page.click('[data-testid="submit-button"]');
await validateForm(page, isColumnMetric);
// fill form and submit
await page.fill('#name', metric.name);
if (isColumnMetric) {
await page.click('#columnName');
await page.click(`[title="${metric.column}"]`);
}
if (metric.expression) {
await page.click('.CodeMirror-scroll');
await page.keyboard.type(metric.expression);
}
const createMetricResponse = page.waitForResponse(
'/api/v1/tables/*/customMetric'
);
await page.click('[data-testid="submit-button"]');
await createMetricResponse;
await expect(page.locator('.Toastify__toast-body')).toHaveText(
new RegExp(`${metric.name} created successfully.`)
);
await page.locator('.Toastify__close-button').click();
// verify the created custom metric
await expect(page).toHaveURL(/profiler/);
await expect(
page.getByRole('heading', {
name: isColumnMetric ? 'Column Profile' : 'Table Profile',
})
).toBeVisible();
await expect(
page.locator(`[data-testid="${metric.name}-custom-metrics"]`)
).toBeVisible();
};
export const deleteCustomMetric = async ({
page,
metric,
isColumnMetric = false,
}) => {
await page
.locator(`[data-testid="${metric.name}-custom-metrics"]`)
.scrollIntoViewIfNeeded();
await expect(
page.locator(`[data-testid="${metric.name}-custom-metrics"]`)
).toBeVisible();
await page.click(`[data-testid="${metric.name}-custom-metrics-menu"]`);
await page.click(`[data-menu-id*="delete"]`);
await expect(page.locator('.ant-modal-header')).toContainText(metric.name);
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
const deleteMetricResponse = page.waitForResponse(
isColumnMetric
? `/api/v1/tables/*/customMetric/${metric.column}/${metric.name}*`
: `/api/v1/tables/*/customMetric/${metric.name}*`
);
await page.click('[data-testid="confirm-button"]');
await deleteMetricResponse;
// Verifying the deletion
await expect(page.getByRole('alert').first()).toHaveText(
`"${metric.name}" deleted successfully!`
);
};