playwright: migrate entity, database and service spec (#16584)

* playwright: migrating entity spec

* added test for customProperty

* added custom property test

* add boolean for slow test

* add comment
This commit is contained in:
Shailesh Parmar 2024-06-11 16:57:09 +05:30 committed by GitHub
parent 0b205faefc
commit 8d81c0068d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1100 additions and 38 deletions

View File

@ -0,0 +1,33 @@
/*
* 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 { EntityTypeEndpoint } from '../support/entity/Entity.interface';
export const CustomPropertySupportedEntityList = [
EntityTypeEndpoint.Database,
EntityTypeEndpoint.DatabaseSchema,
EntityTypeEndpoint.Table,
EntityTypeEndpoint.StoreProcedure,
EntityTypeEndpoint.Topic,
EntityTypeEndpoint.Dashboard,
EntityTypeEndpoint.Pipeline,
EntityTypeEndpoint.Container,
EntityTypeEndpoint.MlModel,
EntityTypeEndpoint.GlossaryTerm,
EntityTypeEndpoint.SearchIndex,
];
export const ENTITY_REFERENCE_PROPERTIES = [
'Entity Reference',
'Entity Reference List',
];

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { test } from '@playwright/test';
import { ENTITIES_WITHOUT_FOLLOWING_BUTTON } from '../../constant/delete';
import { CustomPropertySupportedEntityList } from '../../constant/customProperty';
import { ContainerClass } from '../../support/entity/ContainerClass';
import { DashboardClass } from '../../support/entity/DashboardClass';
import { DashboardDataModelClass } from '../../support/entity/DashboardDataModelClass';
@ -19,13 +19,7 @@ import { EntityDataClass } from '../../support/entity/EntityDataClass';
import { MlModelClass } from '../../support/entity/MlModelClass';
import { PipelineClass } from '../../support/entity/PipelineClass';
import { SearchIndexClass } from '../../support/entity/SearchIndexClass';
import { DashboardServiceClass } from '../../support/entity/service/DashboardServiceClass';
import { DatabaseServiceClass } from '../../support/entity/service/DatabaseServiceClass';
import { MessagingServiceClass } from '../../support/entity/service/MessagingServiceClass';
import { MlmodelServiceClass } from '../../support/entity/service/MlmodelServiceClass';
import { PipelineServiceClass } from '../../support/entity/service/PipelineServiceClass';
import { SearchIndexServiceClass } from '../../support/entity/service/SearchIndexServiceClass';
import { StorageServiceClass } from '../../support/entity/service/StorageServiceClass';
import { StoredProcedureClass } from '../../support/entity/StoredProcedureClass';
import { TableClass } from '../../support/entity/TableClass';
import { TopicClass } from '../../support/entity/TopicClass';
import {
@ -34,16 +28,11 @@ import {
getToken,
redirectToHomePage,
} from '../../utils/common';
import { CustomPropertyTypeByName } from '../../utils/customProperty';
const entities = [
DatabaseServiceClass,
DashboardServiceClass,
MessagingServiceClass,
MlmodelServiceClass,
PipelineServiceClass,
SearchIndexServiceClass,
StorageServiceClass,
TableClass,
StoredProcedureClass,
DashboardClass,
PipelineClass,
TopicClass,
@ -59,9 +48,6 @@ test.use({ storageState: 'playwright/.auth/admin.json' });
entities.forEach((EntityClass) => {
const entity = new EntityClass();
const deleteEntity = new EntityClass();
const allowFollowUnfollowTest = !ENTITIES_WITHOUT_FOLLOWING_BUTTON.includes(
entity.endpoint
);
test.describe(entity.getType(), () => {
test.beforeAll('Setup pre-requests', async ({ browser }) => {
@ -69,6 +55,7 @@ entities.forEach((EntityClass) => {
await EntityDataClass.preRequisitesForTests(apiContext);
await entity.create(apiContext);
await entity.prepareForTests(apiContext);
await afterAction();
});
@ -128,15 +115,44 @@ entities.forEach((EntityClass) => {
await entity.inactiveAnnouncement(page);
});
if (allowFollowUnfollowTest) {
test(`UpVote & DownVote entity`, async ({ page }) => {
await entity.upVote(page);
await entity.downVote(page);
});
test(`UpVote & DownVote entity`, async ({ page }) => {
await entity.upVote(page);
await entity.downVote(page);
});
test(`Follow & Un-follow entity`, async ({ page }) => {
const entityName = entity.entityResponseData?.['displayName'];
await entity.followUnfollowEntity(page, entityName);
test(`Follow & Un-follow entity`, async ({ page }) => {
const entityName = entity.entityResponseData?.['displayName'];
await entity.followUnfollowEntity(page, entityName);
});
// Create custom property only for supported entities
if (CustomPropertySupportedEntityList.includes(entity.endpoint)) {
const properties = Object.values(CustomPropertyTypeByName);
const titleText = properties.join(', ');
test(`Set & Update ${titleText} Custom Property `, async ({ page }) => {
// increase timeout as it using single test for multiple steps
test.slow(true);
await test.step(`Set ${titleText} Custom Property`, async () => {
for (const type of properties) {
await entity.setCustomProperty(
page,
entity.customPropertyValue[type].property,
entity.customPropertyValue[type].value
);
}
});
await test.step(`Update ${titleText} Custom Property`, async () => {
for (const type of properties) {
await entity.updateCustomProperty(
page,
entity.customPropertyValue[type].property,
entity.customPropertyValue[type].newValue
);
}
});
});
}
@ -146,6 +162,7 @@ entities.forEach((EntityClass) => {
test.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await entity.cleanup(apiContext);
await entity.delete(apiContext);
await EntityDataClass.postRequisitesForTests(apiContext);
await afterAction();
@ -153,6 +170,9 @@ entities.forEach((EntityClass) => {
});
test(`Delete ${deleteEntity.getType()}`, async ({ page }) => {
// increase timeout as it using single test for multiple steps
test.slow(true);
await redirectToHomePage(page);
// get the token from localStorage
const token = await getToken(page);

View File

@ -0,0 +1,157 @@
/*
* 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 { DatabaseClass } from '../../support/entity/DatabaseClass';
import { DatabaseSchemaClass } from '../../support/entity/DatabaseSchemaClass';
import { EntityDataClass } from '../../support/entity/EntityDataClass';
import { DashboardServiceClass } from '../../support/entity/service/DashboardServiceClass';
import { DatabaseServiceClass } from '../../support/entity/service/DatabaseServiceClass';
import { MessagingServiceClass } from '../../support/entity/service/MessagingServiceClass';
import { MlmodelServiceClass } from '../../support/entity/service/MlmodelServiceClass';
import { PipelineServiceClass } from '../../support/entity/service/PipelineServiceClass';
import { SearchIndexServiceClass } from '../../support/entity/service/SearchIndexServiceClass';
import { StorageServiceClass } from '../../support/entity/service/StorageServiceClass';
import {
createNewPage,
getAuthContext,
getToken,
redirectToHomePage,
} from '../../utils/common';
const entities = [
DatabaseServiceClass,
DashboardServiceClass,
MessagingServiceClass,
MlmodelServiceClass,
PipelineServiceClass,
SearchIndexServiceClass,
StorageServiceClass,
DatabaseClass,
DatabaseSchemaClass,
] as const;
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
entities.forEach((EntityClass) => {
const entity = new EntityClass();
const deleteEntity = new EntityClass();
test.describe(entity.getType(), () => {
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await EntityDataClass.preRequisitesForTests(apiContext);
await entity.create(apiContext);
await afterAction();
});
test.beforeEach('Visit entity details page', async ({ page }) => {
await redirectToHomePage(page);
await entity.visitEntityPage(page);
});
test('Domain Add, Update and Remove', async ({ page }) => {
await entity.domain(
page,
EntityDataClass.domain1.responseData,
EntityDataClass.domain2.responseData
);
});
test('User as Owner Add, Update and Remove', async ({ page }) => {
const OWNER1 = EntityDataClass.user1.getUserName();
const OWNER2 = EntityDataClass.user2.getUserName();
await entity.owner(page, OWNER1, OWNER2);
});
test('Team as Owner Add, Update and Remove', async ({ page }) => {
const OWNER1 = EntityDataClass.team1.data.displayName;
const OWNER2 = EntityDataClass.team2.data.displayName;
await entity.owner(page, OWNER1, OWNER2, 'Teams');
});
test('Tier Add, Update and Remove', async ({ page }) => {
await entity.tier(page, 'Tier1', 'Tier5');
});
test('Update description', async ({ page }) => {
await entity.descriptionUpdate(page);
});
test('Tag Add, Update and Remove', async ({ page }) => {
await entity.tag(page, 'PersonalData.Personal', 'PII.None');
});
test('Glossary Term Add, Update and Remove', async ({ page }) => {
await entity.glossaryTerm(
page,
EntityDataClass.glossaryTerm1.responseData,
EntityDataClass.glossaryTerm2.responseData
);
});
test(`Announcement create & delete`, async ({ page }) => {
await entity.announcement(
page,
entity.entityResponseData?.['fullyQualifiedName']
);
});
test(`Inactive Announcement create & delete`, async ({ page }) => {
await entity.inactiveAnnouncement(page);
});
test(`Update displayName`, async ({ page }) => {
await entity.renameEntity(page, entity.entity.name);
});
test.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await entity.delete(apiContext);
await EntityDataClass.postRequisitesForTests(apiContext);
await afterAction();
});
});
test(`Delete ${deleteEntity.getType()}`, async ({ page }) => {
// increase timeout as it using single test for multiple steps
test.slow(true);
await redirectToHomePage(page);
// get the token from localStorage
const token = await getToken(page);
// create a new context with the token
const apiContext = await getAuthContext(token);
await deleteEntity.create(apiContext);
await redirectToHomePage(page);
await deleteEntity.visitEntityPage(page);
await test.step('Soft delete', async () => {
await deleteEntity.softDeleteEntity(
page,
deleteEntity.entity.name,
deleteEntity.entityResponseData?.['displayName']
);
});
await test.step('Hard delete', async () => {
await deleteEntity.hardDeleteEntity(
page,
deleteEntity.entity.name,
deleteEntity.entityResponseData?.['displayName']
);
});
});
});

View File

@ -0,0 +1,113 @@
/*
* 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, Page } from '@playwright/test';
import { SERVICE_TYPE } from '../../constant/service';
import { uuid } from '../../utils/common';
import { visitServiceDetailsPage } from '../../utils/service';
import { EntityTypeEndpoint } from './Entity.interface';
import { EntityClass } from './EntityClass';
export class DatabaseClass extends EntityClass {
service = {
name: `pw-database-service-${uuid()}`,
serviceType: 'Mysql',
connection: {
config: {
type: 'Mysql',
scheme: 'mysql+pymysql',
username: 'username',
authType: {
password: 'password',
},
hostPort: 'mysql:3306',
supportsMetadataExtraction: true,
supportsDBTExtraction: true,
supportsProfiler: true,
supportsQueryComment: true,
},
},
};
entity = {
name: `pw-database-${uuid()}`,
service: this.service.name,
};
serviceResponseData: unknown;
entityResponseData: unknown;
constructor(name?: string) {
super(EntityTypeEndpoint.Database);
this.service.name = name ?? this.service.name;
this.type = 'Database';
}
async create(apiContext: APIRequestContext) {
const serviceResponse = await apiContext.post(
'/api/v1/services/databaseServices',
{
data: this.service,
}
);
const entityResponse = await apiContext.post('/api/v1/databases', {
data: this.entity,
});
const service = await serviceResponse.json();
const entity = await entityResponse.json();
this.serviceResponseData = service;
this.entityResponseData = entity;
return {
service,
entity,
};
}
get() {
return {
service: this.serviceResponseData,
entity: this.entityResponseData,
};
}
async visitEntityPage(page: Page) {
await visitServiceDetailsPage(
page,
{
name: this.service.name,
type: SERVICE_TYPE.Database,
},
false
);
const databaseResponse = page.waitForResponse(
`/api/v1/databases/name/*${this.entity.name}?**`
);
await page.getByTestId(this.entity.name).click();
await databaseResponse;
}
async delete(apiContext: APIRequestContext) {
const serviceResponse = await apiContext.delete(
`/api/v1/services/databaseServices/name/${encodeURIComponent(
this.serviceResponseData?.['fullyQualifiedName']
)}?recursive=true&hardDelete=true`
);
return {
service: serviceResponse.body,
entity: this.entityResponseData,
};
}
}

View File

@ -0,0 +1,130 @@
/*
* 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, Page } from '@playwright/test';
import { SERVICE_TYPE } from '../../constant/service';
import { uuid } from '../../utils/common';
import { visitServiceDetailsPage } from '../../utils/service';
import { EntityTypeEndpoint } from './Entity.interface';
import { EntityClass } from './EntityClass';
export class DatabaseSchemaClass extends EntityClass {
service = {
name: `pw-database-service-${uuid()}`,
serviceType: 'Mysql',
connection: {
config: {
type: 'Mysql',
scheme: 'mysql+pymysql',
username: 'username',
authType: {
password: 'password',
},
hostPort: 'mysql:3306',
supportsMetadataExtraction: true,
supportsDBTExtraction: true,
supportsProfiler: true,
supportsQueryComment: true,
},
},
};
database = {
name: `pw-database-${uuid()}`,
service: this.service.name,
};
entity = {
name: `pw-database-schema-${uuid()}`,
database: `${this.service.name}.${this.database.name}`,
};
serviceResponseData: unknown;
databaseResponseData: unknown;
entityResponseData: unknown;
constructor(name?: string) {
super(EntityTypeEndpoint.DatabaseSchema);
this.service.name = name ?? this.service.name;
this.type = 'Database Schema';
}
async create(apiContext: APIRequestContext) {
const serviceResponse = await apiContext.post(
'/api/v1/services/databaseServices',
{
data: this.service,
}
);
const databaseResponse = await apiContext.post('/api/v1/databases', {
data: this.database,
});
const entityResponse = await apiContext.post('/api/v1/databaseSchemas', {
data: this.entity,
});
const service = await serviceResponse.json();
const database = await databaseResponse.json();
const entity = await entityResponse.json();
this.serviceResponseData = service;
this.databaseResponseData = database;
this.entityResponseData = entity;
return {
service,
database,
entity,
};
}
get() {
return {
service: this.serviceResponseData,
database: this.databaseResponseData,
entity: this.entityResponseData,
};
}
async visitEntityPage(page: Page) {
await visitServiceDetailsPage(
page,
{
name: this.service.name,
type: SERVICE_TYPE.Database,
},
false
);
const databaseResponse = page.waitForResponse(
`/api/v1/databases/name/*${this.database.name}?**`
);
await page.getByTestId(this.database.name).click();
await databaseResponse;
const databaseSchemaResponse = page.waitForResponse(
`/api/v1/databaseSchemas/name/*${this.entity}?*`
);
await page.getByTestId(this.entity.name).click();
await databaseSchemaResponse;
}
async delete(apiContext: APIRequestContext) {
const serviceResponse = await apiContext.delete(
`/api/v1/services/databaseServices/name/${encodeURIComponent(
this.serviceResponseData?.['fullyQualifiedName']
)}?recursive=true&hardDelete=true`
);
return {
service: serviceResponse.body,
entity: this.entityResponseData,
};
}
}

View File

@ -41,3 +41,19 @@ export type EntityDataType = {
entityDetails: unknown;
endPoint: EntityTypeEndpoint;
};
export enum ENTITY_PATH {
tables = 'table',
topics = 'topic',
dashboards = 'dashboard',
pipelines = 'pipeline',
mlmodels = 'mlmodel',
containers = 'container',
tags = 'tag',
glossaries = 'glossary',
searchIndexes = 'searchIndex',
storedProcedures = 'storedProcedure',
glossaryTerm = 'glossaryTerm',
databases = 'database',
databaseSchemas = 'databaseSchema',
}

View File

@ -10,7 +10,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page } from '@playwright/test';
import { APIRequestContext, Page } from '@playwright/test';
import { CustomPropertySupportedEntityList } from '../../constant/customProperty';
import {
createCustomPropertyForEntity,
CustomProperty,
setValueForProperty,
validateValueForProperty,
} from '../../utils/customProperty';
import { assignDomain, removeDomain, updateDomain } from '../../utils/domain';
import {
addOwner,
@ -38,12 +45,17 @@ import {
} from '../../utils/entity';
import { Domain } from '../domain/Domain';
import { GlossaryTerm } from '../glossary/GlossaryTerm';
import { EntityTypeEndpoint } from './Entity.interface';
import { EntityTypeEndpoint, ENTITY_PATH } from './Entity.interface';
export class EntityClass {
type: string;
endpoint: EntityTypeEndpoint;
customPropertyValue: Record<
string,
{ value: string; newValue: string; property: CustomProperty }
>;
constructor(endpoint: EntityTypeEndpoint) {
this.endpoint = endpoint;
}
@ -57,6 +69,40 @@ export class EntityClass {
// Override for entity visit
}
// Prepare for tests
async prepareForTests(apiContext: APIRequestContext) {
// Create custom property only for supported entities
if (CustomPropertySupportedEntityList.includes(this.endpoint)) {
const data = await createCustomPropertyForEntity(
apiContext,
this.endpoint
);
this.customPropertyValue = data;
}
}
async cleanup(apiContext: APIRequestContext) {
// Delete custom property only for supported entities
if (CustomPropertySupportedEntityList.includes(this.endpoint)) {
const entitySchemaResponse = await apiContext.get(
`/api/v1/metadata/types/name/${ENTITY_PATH[this.endpoint]}`
);
const entitySchema = await entitySchemaResponse.json();
await apiContext.patch(`/api/v1/metadata/types/${entitySchema.id}`, {
data: [
{
op: 'remove',
path: '/customProperties',
},
],
headers: {
'Content-Type': 'application/json-patch+json',
},
});
}
}
async domain(
page: Page,
domain1: Domain['responseData'],
@ -173,4 +219,42 @@ export class EntityClass {
async hardDeleteEntity(page: Page, entityName: string, displayName?: string) {
await hardDeleteEntity(page, displayName ?? entityName, this.endpoint);
}
async setCustomProperty(
page: Page,
propertydetails: CustomProperty,
value: string
) {
await setValueForProperty({
page,
propertyName: propertydetails.name,
value,
propertyType: propertydetails.propertyType.name,
});
await validateValueForProperty({
page,
propertyName: propertydetails.name,
value,
propertyType: propertydetails.propertyType.name,
});
}
async updateCustomProperty(
page: Page,
propertydetails: CustomProperty,
value: string
) {
await setValueForProperty({
page,
propertyName: propertydetails.name,
value,
propertyType: propertydetails.propertyType.name,
});
await validateValueForProperty({
page,
propertyName: propertydetails.name,
value,
propertyType: propertydetails.propertyType.name,
});
}
}

View File

@ -0,0 +1,130 @@
/*
* 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, Page } from '@playwright/test';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import { EntityTypeEndpoint } from './Entity.interface';
import { EntityClass } from './EntityClass';
export class StoredProcedureClass extends EntityClass {
service = {
name: `pw-database-service-${uuid()}`,
serviceType: 'Mysql',
connection: {
config: {
type: 'Mysql',
scheme: 'mysql+pymysql',
username: 'username',
authType: {
password: 'password',
},
hostPort: 'mysql:3306',
supportsMetadataExtraction: true,
supportsDBTExtraction: true,
supportsProfiler: true,
supportsQueryComment: true,
},
},
};
database = {
name: `pw-database-${uuid()}`,
service: this.service.name,
};
schema = {
name: `pw-database-schema-${uuid()}`,
database: `${this.service.name}.${this.database.name}`,
};
entity = {
name: `pw-stored-procedure-${uuid()}`,
databaseSchema: `${this.service.name}.${this.database.name}.${this.schema.name}`,
storedProcedureCode: {
code: 'CREATE OR REPLACE PROCEDURE output_message(message VARCHAR)\nRETURNS VARCHAR NOT NULL\nLANGUAGE SQL\nAS\n$$\nBEGIN\n RETURN message;\nEND;\n$$\n;',
},
};
serviceResponseData: unknown;
databaseResponseData: unknown;
schemaResponseData: unknown;
entityResponseData: unknown;
constructor(name?: string) {
super(EntityTypeEndpoint.StoreProcedure);
this.service.name = name ?? this.service.name;
this.type = 'Store Procedure';
}
async create(apiContext: APIRequestContext) {
const serviceResponse = await apiContext.post(
'/api/v1/services/databaseServices',
{
data: this.service,
}
);
const databaseResponse = await apiContext.post('/api/v1/databases', {
data: this.database,
});
const schemaResponse = await apiContext.post('/api/v1/databaseSchemas', {
data: this.schema,
});
const entityResponse = await apiContext.post('/api/v1/storedProcedures', {
data: this.entity,
});
const service = await serviceResponse.json();
const database = await databaseResponse.json();
const schema = await schemaResponse.json();
const entity = await entityResponse.json();
this.serviceResponseData = service;
this.databaseResponseData = database;
this.schemaResponseData = schema;
this.entityResponseData = entity;
return {
service,
database,
schema,
entity,
};
}
get() {
return {
service: this.serviceResponseData,
database: this.databaseResponseData,
schema: this.schemaResponseData,
entity: this.entityResponseData,
};
}
async visitEntityPage(page: Page) {
await visitEntityPage({
page,
searchTerm: this.entityResponseData?.['fullyQualifiedName'],
dataTestId: `${this.service.name}-${this.entity.name}`,
});
}
async delete(apiContext: APIRequestContext) {
const serviceResponse = await apiContext.delete(
`/api/v1/services/databaseServices/name/${encodeURIComponent(
this.serviceResponseData?.['fullyQualifiedName']
)}?recursive=true&hardDelete=true`
);
return {
service: serviceResponse.body,
entity: this.entityResponseData,
};
}
}

View File

@ -12,6 +12,7 @@
*/
import { APIRequestContext } from '@playwright/test';
import { uuid } from '../../utils/common';
import { getRandomFirstName } from '../../utils/user';
type ResponseDataType = {
name: string;
@ -25,9 +26,10 @@ type ResponseDataType = {
};
export class Glossary {
randomName = getRandomFirstName();
data = {
name: `PW%General.${uuid()}`,
displayName: `PW % General ${uuid()}`,
name: `PW%${uuid()}.${this.randomName}`,
displayName: `PW % ${uuid()} ${this.randomName}`,
description:
'Glossary terms that describe general conceptual terms. **Note that these conceptual terms are used for automatically labeling the data.**',
reviewers: [],
@ -48,10 +50,10 @@ export class Glossary {
this.responseData = await response.json();
return response.body;
return await response.json();
}
async get() {
get() {
return this.responseData;
}
@ -62,6 +64,6 @@ export class Glossary {
)}?recursive=true&hardDelete=true`
);
return response.body;
return await response.json();
}
}

View File

@ -12,6 +12,7 @@
*/
import { APIRequestContext } from '@playwright/test';
import { uuid } from '../../utils/common';
import { getRandomLastName } from '../../utils/user';
type ResponseDataType = {
name: string;
@ -28,9 +29,10 @@ type ResponseDataType = {
};
export class GlossaryTerm {
randomName = getRandomLastName();
data = {
name: `PW.Bank%Number-${uuid()}`,
displayName: `PW BankNumber ${uuid()}`,
name: `PW.${uuid()}%${this.randomName}`,
displayName: `PW ${uuid()}%${this.randomName}`,
description: 'A bank account number.',
mutuallyExclusive: false,
glossary: '',
@ -50,10 +52,10 @@ export class GlossaryTerm {
this.responseData = await response.json();
return response.body;
return await response.json();
}
async get() {
get() {
return this.responseData;
}
@ -64,6 +66,6 @@ export class GlossaryTerm {
)}?recursive=true&hardDelete=true`
);
return response.body;
return await response.json();
}
}

View File

@ -0,0 +1,368 @@
/*
* 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 {
EntityTypeEndpoint,
ENTITY_PATH,
} from '../support/entity/Entity.interface';
import { uuid } from './common';
export enum CustomPropertyType {
STRING = 'String',
INTEGER = 'Integer',
MARKDOWN = 'Markdown',
}
export enum CustomPropertyTypeByName {
STRING = 'string',
INTEGER = 'integer',
MARKDOWN = 'markdown',
NUMBER = 'number',
DURATION = 'duration',
EMAIL = 'email',
ENUM = 'enum',
SQL_QUERY = 'sqlQuery',
TIMESTAMP = 'timestamp',
ENTITY_REFERENCE = 'entityReference',
ENTITY_REFERENCE_LIST = 'entityReferenceList',
}
export interface CustomProperty {
name: string;
type: CustomPropertyType;
description: string;
propertyType: {
name: string;
type: string;
};
}
export const setValueForProperty = async (data: {
page: Page;
propertyName: string;
value: string;
propertyType: string;
}) => {
const { page, propertyName, value, propertyType } = data;
await page.click('[data-testid="custom_properties"]');
await expect(page.getByRole('cell', { name: propertyName })).toContainText(
propertyName
);
const editButton = page.locator(
`[data-row-key="${propertyName}"] [data-testid="edit-icon"]`
);
await editButton.scrollIntoViewIfNeeded();
await editButton.click({ force: true });
switch (propertyType) {
case 'markdown':
await page
.locator(
'.toastui-editor-md-container > .toastui-editor > .ProseMirror'
)
.isVisible();
await page
.locator(
'.toastui-editor-md-container > .toastui-editor > .ProseMirror'
)
.fill(value);
await page.locator('[data-testid="save"]').click();
break;
case 'email':
await page.locator('[data-testid="email-input"]').isVisible();
await page.locator('[data-testid="email-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click();
break;
case 'duration':
await page.locator('[data-testid="duration-input"]').isVisible();
await page.locator('[data-testid="duration-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click();
break;
case 'enum':
await page.locator('#enumValues').click();
await page.locator('#enumValues').fill(value);
await page.locator('#enumValues').press('Enter');
await page.mouse.click(0, 0);
await page.locator('[data-testid="inline-save-btn"]').click();
break;
case 'sqlQuery':
await page.locator("pre[role='presentation']").last().click();
await page.keyboard.type(value);
await page.locator('[data-testid="inline-save-btn"]').click();
break;
case 'timestamp':
await page.locator('[data-testid="timestamp-input"]').isVisible();
await page.locator('[data-testid="timestamp-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click();
break;
case 'timeInterval': {
const [startValue, endValue] = value.split(',');
await page.locator('[data-testid="start-input"]').isVisible();
await page.locator('[data-testid="start-input"]').fill(startValue);
await page.locator('[data-testid="end-input"]').isVisible();
await page.locator('[data-testid="end-input"]').fill(endValue);
await page.locator('[data-testid="inline-save-btn"]').click();
break;
}
case 'string':
case 'integer':
case 'number':
await page.locator('[data-testid="value-input"]').isVisible();
await page.locator('[data-testid="value-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click();
break;
case 'entityReference':
case 'entityReferenceList': {
const refValues = value.split(',');
for (const val of refValues) {
const searchApi = `**/api/v1/search/query?q=*${encodeURIComponent(
val
)}*`;
await page.route(searchApi, (route) => route.continue());
await page.locator('#entityReference').clear();
await page.locator('#entityReference').fill(val);
await page.waitForResponse(searchApi);
await page.locator(`[data-testid="${val}"]`).click();
}
await page.locator('[data-testid="inline-save-btn"]').click();
break;
}
}
await page.waitForResponse('/api/v1/*/*');
if (propertyType === 'enum') {
await expect(
page.getByLabel('Custom Properties').getByTestId('enum-value')
).toContainText(value);
} else if (propertyType === 'timeInterval') {
const [startValue, endValue] = value.split(',');
await expect(
page.getByLabel('Custom Properties').getByTestId('time-interval-value')
).toContainText(startValue);
await expect(
page.getByLabel('Custom Properties').getByTestId('time-interval-value')
).toContainText(endValue);
} else if (propertyType === 'sqlQuery') {
await expect(
page.getByLabel('Custom Properties').locator('.CodeMirror-scroll')
).toContainText(value);
} else if (
!['entityReference', 'entityReferenceList'].includes(propertyType)
) {
await expect(page.getByRole('row', { name: propertyName })).toContainText(
value.replace(/\*|_/gi, '')
);
}
};
export const validateValueForProperty = async (data: {
page: Page;
propertyName: string;
value: string;
propertyType: string;
}) => {
const { page, propertyName, value, propertyType } = data;
await page.click('[data-testid="custom_properties"]');
if (propertyType === 'enum') {
await expect(
page.getByLabel('Custom Properties').getByTestId('enum-value')
).toContainText(value);
} else if (propertyType === 'timeInterval') {
const [startValue, endValue] = value.split(',');
await expect(
page.getByLabel('Custom Properties').getByTestId('time-interval-value')
).toContainText(startValue);
await expect(
page.getByLabel('Custom Properties').getByTestId('time-interval-value')
).toContainText(endValue);
} else if (propertyType === 'sqlQuery') {
await expect(
page.getByLabel('Custom Properties').locator('.CodeMirror-scroll')
).toContainText(value);
} else if (
!['entityReference', 'entityReferenceList'].includes(propertyType)
) {
await expect(page.getByRole('row', { name: propertyName })).toContainText(
value.replace(/\*|_/gi, '')
);
}
};
export const getPropertyValues = (type: string) => {
switch (type) {
case 'integer':
return {
value: '123',
newValue: '456',
};
case 'string':
return {
value: 'string value',
newValue: 'new string value',
};
case 'markdown':
return {
value: '**Bold statement**',
newValue: '__Italic statement__',
};
case 'number':
return {
value: '123',
newValue: '456',
};
case 'duration':
return {
value: 'PT1H',
newValue: 'PT2H',
};
case 'email':
return {
value: 'john@gamil.com',
newValue: 'user@getcollate.io',
};
case 'enum':
return {
value: 'small',
newValue: 'medium',
};
case 'sqlQuery':
return {
value: 'Select * from table',
newValue: 'Select * from table where id = 1',
};
case 'timestamp':
return {
value: '1710831125922',
newValue: '1710831125923',
};
case 'entityReference':
return {
value: 'Adam Matthews',
newValue: 'Aaron Singh',
};
case 'entityReferenceList':
return {
value: 'Aaron Johnson,Organization',
newValue: 'Aaron Warren',
};
default:
return {
value: '',
newValue: '',
};
}
};
export const createCustomPropertyForEntity = async (
apiContext: APIRequestContext,
endpoint: EntityTypeEndpoint
) => {
const propertiesResponse = await apiContext.get(
'/api/v1/metadata/types?category=field&limit=20'
);
const properties = await propertiesResponse.json();
const propertyList = properties.data.filter((item) =>
Object.values(CustomPropertyTypeByName).includes(item.name)
);
const entitySchemaResponse = await apiContext.get(
`/api/v1/metadata/types/name/${ENTITY_PATH[endpoint]}`
);
const entitySchema = await entitySchemaResponse.json();
let customProperties = {} as Record<
string,
{
value: string;
newValue: string;
property: CustomProperty;
}
>;
for (const item of propertyList) {
const customPropertyResponse = await apiContext.put(
`/api/v1/metadata/types/${entitySchema.id}`,
{
data: {
name: `cyCustomProperty${uuid()}`,
description: `cyCustomProperty${uuid()}`,
propertyType: {
id: item.id ?? '',
type: 'type',
},
...(item.name === 'enum'
? {
customPropertyConfig: {
config: {
multiSelect: true,
values: ['small', 'medium', 'large'],
},
},
}
: {}),
...(['entityReference', 'entityReferenceList'].includes(item.name)
? {
customPropertyConfig: {
config: ['user', 'team'],
},
}
: {}),
},
}
);
const customProperty = await customPropertyResponse.json();
// Process the custom properties
customProperties = customProperty.customProperties.reduce((prev, curr) => {
const propertyTypeName = curr.propertyType.name;
return {
...prev,
[propertyTypeName]: {
...getPropertyValues(propertyTypeName),
property: curr,
},
};
}, {});
}
return customProperties;
};

View File

@ -15,6 +15,7 @@ import { Domain } from '../support/domain/Domain';
export const assignDomain = async (page: Page, domain: Domain['data']) => {
await page.getByTestId('add-domain').click();
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await page
.getByTestId('selectable-list')
.getByTestId('searchbar')
@ -31,6 +32,7 @@ export const assignDomain = async (page: Page, domain: Domain['data']) => {
export const updateDomain = async (page: Page, domain: Domain['data']) => {
await page.getByTestId('add-domain').click();
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await page.getByTestId('selectable-list').getByTestId('searchbar').clear();
await page
.getByTestId('selectable-list')
@ -48,6 +50,7 @@ export const updateDomain = async (page: Page, domain: Domain['data']) => {
export const removeDomain = async (page: Page) => {
await page.getByTestId('add-domain').click();
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await expect(page.getByTestId('remove-owner').locator('path')).toBeVisible();

View File

@ -33,6 +33,7 @@ export const visitEntityPage = async (data: {
const { page, searchTerm, dataTestId } = data;
await page.getByTestId('searchBox').fill(searchTerm);
await page.waitForResponse('/api/v1/search/query?q=*index=dataAsset*');
await page.getByTestId(dataTestId).getByTestId('data-name').click();
await page.getByTestId('searchBox').clear();
};
@ -98,6 +99,7 @@ export const updateOwner = async (
export const removeOwner = async (page: Page, dataTestId?: string) => {
await page.getByTestId('edit-owner').click();
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await expect(page.getByTestId('remove-owner').locator('svg')).toBeVisible();
@ -110,6 +112,7 @@ export const removeOwner = async (page: Page, dataTestId?: string) => {
export const assignTier = async (page: Page, tier: string) => {
await page.getByTestId('edit-tier').click();
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await page.getByTestId(`radio-btn-${tier}`).click();
await page.getByTestId('Tier').click();
@ -118,6 +121,7 @@ export const assignTier = async (page: Page, tier: string) => {
export const removeTier = async (page: Page) => {
await page.getByTestId('edit-tier').click();
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await page.getByTestId('clear-tier').click();
await page.getByTestId('Tier').click();