Browse Source

added deep compare util and tests for duplicate project

feat/export-nest-dir-restructure
starbirdtech383 2 years ago
parent
commit
b33f29def6
  1. 55
      tests/playwright/package-lock.json
  2. 2
      tests/playwright/package.json
  3. 10
      tests/playwright/quickTests/commonTest.ts
  4. 52
      tests/playwright/tests/db/projectOperations.spec.ts
  5. 68
      tests/playwright/tests/utils/objectCompareUtil.ts
  6. 167
      tests/playwright/tests/utils/projectInfoOperator.ts

55
tests/playwright/package-lock.json generated

@ -16,7 +16,7 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"@playwright/test": "1.27.1",
"@playwright/test": "1.32.2",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.24.0",
@ -304,19 +304,22 @@
}
},
"node_modules/@playwright/test": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
"integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.2.tgz",
"integrity": "sha512-nhaTSDpEdTTttdkDE8Z6K3icuG1DVRxrl98Qq0Lfc63SS9a2sjc9+x8ezysh7MzCKz6Y+nArml3/mmt+gqRmQQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.27.1"
"playwright-core": "1.32.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/@types/json-schema": {
@ -2107,6 +2110,20 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -3841,9 +3858,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
"integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.2.tgz",
"integrity": "sha512-zD7aonO+07kOTthsrCR3YCVnDcqSHIJpdFUtZEMOb6//1Rc7/6mZDRdw+nlzcQiQltOOsiqI3rrSyn/SlyjnJQ==",
"dev": true,
"bin": {
"playwright": "cli.js"
@ -5192,13 +5209,14 @@
}
},
"@playwright/test": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
"integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.2.tgz",
"integrity": "sha512-nhaTSDpEdTTttdkDE8Z6K3icuG1DVRxrl98Qq0Lfc63SS9a2sjc9+x8ezysh7MzCKz6Y+nArml3/mmt+gqRmQQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.27.1"
"fsevents": "2.3.2",
"playwright-core": "1.32.2"
}
},
"@types/json-schema": {
@ -6493,6 +6511,13 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -7800,9 +7825,9 @@
"dev": true
},
"playwright-core": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
"integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.2.tgz",
"integrity": "sha512-zD7aonO+07kOTthsrCR3YCVnDcqSHIJpdFUtZEMOb6//1Rc7/6mZDRdw+nlzcQiQltOOsiqI3rrSyn/SlyjnJQ==",
"dev": true
},
"postgres-array": {

2
tests/playwright/package.json

@ -23,7 +23,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "1.27.1",
"@playwright/test": "1.32.2",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.24.0",

10
tests/playwright/quickTests/commonTest.ts

@ -246,10 +246,16 @@ const quickVerify = async ({
}
if (airtableImport) {
// Delete project
// Delete default context project
await dashboard.clickHome();
const projectsPage = new ProjectsPage(dashboard.rootPage);
await projectsPage.deleteProject({ title: context.project.title, withoutPrefix: true });
const projExists: boolean = await projectsPage
.get()
.locator(`[data-testid="delete-project-${context.project.title}"]`)
.isVisible();
if (projExists) {
await projectsPage.deleteProject({ title: context.project.title, withoutPrefix: true });
}
}
};

52
tests/playwright/tests/db/projectOperations.spec.ts

@ -1,4 +1,4 @@
import { test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard';
import { airtableApiBase, airtableApiKey } from '../../constants';
import { quickVerify } from '../../quickTests/commonTest';
@ -6,6 +6,8 @@ import setup from '../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar';
import { ProjectsPage } from '../../pages/ProjectsPage';
import { Api } from 'nocodb-sdk';
import { ProjectInfo, ProjectInfoOperator } from '../utils/projectInfoOperator';
import { deepCompare } from '../utils/objectCompareUtil';
test.describe('Project operations', () => {
let dashboard: DashboardPage;
@ -13,7 +15,7 @@ test.describe('Project operations', () => {
let context: any;
let api: Api<any>;
let projectPage: ProjectsPage;
test.setTimeout(150000);
test.setTimeout(70000);
async function deleteIfExists(name: string) {
try {
@ -77,6 +79,52 @@ test.describe('Project operations', () => {
await projectPage.openProject({ title: dupeProjectName, withoutPrefix: true });
await quickVerify({ dashboard, airtableImport: true, context });
// compare
const projectList = await api.project.list();
const testProjectId = await projectList.list.find((p: any) => p.title === testProjectName);
const dupeProjectId = await projectList.list.find((p: any) => p.title === dupeProjectName);
const projectInfoOp: ProjectInfoOperator = new ProjectInfoOperator(context.token);
const orginal: Promise<ProjectInfo> = projectInfoOp.extractProjectData(testProjectId.id);
const duplicate: Promise<ProjectInfo> = projectInfoOp.extractProjectData(dupeProjectId.id);
await Promise.all([orginal, duplicate]).then(arr => {
// TODO: support providing full json path instead of just last field name
const ignoredFields: Set<string> = new Set([
'id',
'prefix',
'project_id',
'fk_view_id',
'ptn',
'base_id',
'table_name',
'fk_model_id',
'fk_column_id',
'fk_cover_image_col_id',
// potential bugs
'created_at',
'updated_at',
]);
const ignoredKeys: Set<string> = new Set([
'.project.is_meta.title',
// below are potential bugs
'.project.is_meta.title.status',
'.project.tables.0.table.shares.views.0.view._ptn.ptype.tn',
'.project.tables.0.table.shares.views.0.view._ptn.ptype.tn._tn',
'.project.tables.0.table.shares.views.0.view._ptn.ptype.tn._tn.table_meta.title',
'.project.tables.0.1.table.shares.views.0.view._ptn.ptype.tn',
'.project.tables.0.1.table.shares.views.0.view._ptn.ptype.tn._tn',
'.project.tables.0.1.table.shares.views.0.view._ptn.ptype.tn._tn.table_meta.title',
'.project.tables.0.1.2.table.shares.views.0.view._ptn.ptype.tn',
'.project.tables.0.1.2.table.shares.views.0.view._ptn.ptype.tn._tn',
'.project.tables.0.1.2.table.shares.views.0.view._ptn.ptype.tn._tn.table_meta.title',
'.project.tables.bases.0.alias.config',
'.project.tables.bases.users.0.1.email.invite_token.main_roles.roles',
'.project.tables.bases.users.0.1.2.email.invite_token.main_roles.roles',
]);
const orginalProjectInfo: ProjectInfo = arr[0];
const duplicateProjectInfo: ProjectInfo = arr[1];
expect(deepCompare(orginalProjectInfo, duplicateProjectInfo, ignoredFields, ignoredKeys)).toBeTruthy();
});
// cleanup test-data
await cleanupTestData();

68
tests/playwright/tests/utils/objectCompareUtil.ts

@ -0,0 +1,68 @@
/**
* Compare obj1 and obj2 conditionally based on ignoredFields set
* Ignore the field names which are passed in the ignoredFields.
* optionally keyId will be use to prefix the keys mismatched
*
*
* use utility boolean param breakAtFirstMismatch to print diff for
* all the fields instead of breaking at first mismatch
*
* @param obj1
* @param obj2
* @param ignoredFields : filed names ex: title
* @param ignoredKeys : json path for the filed ex: ".project.is_meta.title"
* @param keyId : starts with ""
* @param breakAtFirstMismatch : default true. returns false on first field mismatch
* @returns
*/
export function deepCompare(
obj1: any,
obj2: any,
ignoredFields?: Set<string>,
ignoredKeys?: Set<string>,
keyId = '',
breakAtFirstMismatch = true
): boolean {
if (ignoredKeys !== undefined && ignoredKeys.has(keyId)) {
return true;
}
// If the objects are the same instance, they are equal
if (obj1 === obj2) {
return true;
}
// If one of the objects is null or not an object, they are not equal
if (!obj1 || !obj2 || typeof obj1 !== 'object' || typeof obj2 !== 'object') {
console.log(`Mismatch key: ${keyId} value1: "${obj1}" value2: "${obj2}"`);
return !breakAtFirstMismatch;
// return false;
}
// If the objects have different numbers of properties, they are not equal
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
console.log(`Mismatch length key: ${keyId} value1: "${obj1}" value2: "${obj2}"`);
return !breakAtFirstMismatch;
// return false;
}
// Recursively compare each property of the objects
for (const key of keys1) {
if (
(ignoredFields !== undefined && ignoredFields.has(key)) ||
key.endsWith(' List') /* temp hack to avoid fields like */
) {
// console.log(`${keyId} ignored in comparison`)
} else {
keyId = keyId + '.' + key;
if (!deepCompare(obj1[key], obj2[key], ignoredFields, ignoredKeys, keyId, breakAtFirstMismatch)) {
return !breakAtFirstMismatch;
// return false;
}
}
}
// If all properties match, the objects are equal
return true;
}

167
tests/playwright/tests/utils/projectInfoOperator.ts

@ -0,0 +1,167 @@
import {
Api,
BaseListType,
BaseType,
FilterListType,
FilterType,
HookListType,
HookType,
PaginatedType,
ProjectType,
SharedViewListType,
SharedViewType,
SignInReqType,
SortListType,
SortType,
TableListType,
TableType,
UserType,
ViewListType,
ViewType,
} from 'nocodb-sdk';
export class ViewInfo {
view: ViewType;
filters: FilterType[];
sorts: SortType[];
firstPageData?: {
/** List of data objects */
list: any[];
/** Paginated Info */
pageInfo: PaginatedType;
};
}
export class TableInfo {
table: TableType;
views: ViewInfo[];
shares: SharedViewType[];
webhooks: HookType[];
firstPageData?: {
/** List of data objects */
list: any[];
/** Paginated Info */
pageInfo: PaginatedType;
};
}
export class ProjectInfo {
project: ProjectType;
bases: BaseType[];
users: UserType[];
tables: TableInfo[];
}
export class ProjectInfoOperator {
api: Api<any>;
constructor(token: string) {
this.api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': token,
},
});
}
/**
* extracts the projectInfo using sdk via apis
*
* @param projectId
* @returns
*/
async extractProjectData(projectId: string): Promise<ProjectInfo> {
// TODO: capture apiTokens, projectSettings, ACLVisibilityRules, UI ACL (discuss before adding)
const project: ProjectType = await this.api.project.read(projectId);
// bases
const bases: BaseListType = await this.api.base.list(projectId);
// users
const usersWrapper: any = await this.api.auth.projectUserList(projectId);
// SET project, users and bases
const projectInfo: ProjectInfo = { project: project, tables: [], bases: [], users: [] };
projectInfo.bases = bases.list;
if (usersWrapper.users) {
projectInfo.users = usersWrapper.users.list as UserType[];
}
const tables: TableListType = await this.api.dbTable.list(projectId);
for (const table of tables.list) {
const tableInfo: TableInfo = { table: table, shares: [], views: [], webhooks: [] };
const views: ViewListType = await this.api.dbView.list(table.id);
for (const v of views.list) {
const filters: FilterListType = await this.api.dbTableFilter.read(v.id);
const sorts: SortListType = await this.api.dbTableSort.list(v.id);
// create ViewData and push to array
const viewInfo: ViewInfo = { view: v, filters: [], sorts: [] };
viewInfo.firstPageData = await this.api.dbViewRow.list('noco', projectId, table.id, v.id);
viewInfo.filters = filters.list;
viewInfo.sorts = sorts.list;
tableInfo.views.push(viewInfo);
}
const shares: SharedViewListType = await this.api.dbViewShare.list(table.id);
const webhooks: HookListType = await this.api.dbTableWebhook.list(table.id);
tableInfo.shares = shares.list;
tableInfo.webhooks = webhooks.list;
projectInfo.tables.push(tableInfo);
tableInfo.firstPageData = await this.api.dbTableRow.list('noco', projectId, table.id);
}
return projectInfo;
}
/**
* helper function to print projectInfo
* do not use this function to assert anything.
* this is only helper function to debug and should
* be allowed to modify without any test failures.
*
* @param projectData
*/
async printProjectData(projectData: ProjectInfo) {
console.log('project.title : ' + projectData.project.title);
// bases
console.log('Bases:');
for (const base of projectData.bases) {
console.log(base.id);
}
// users
console.log('Users:');
if (projectData.users) {
for (const user of projectData.users) {
console.log(user.email);
}
}
console.log('Tables: ');
if (projectData.tables) {
for (const tableData of projectData.tables) {
console.log('Table: ' + tableData.table.title);
console.log('Views: ');
console.log('Filters: ');
for (const viewData of tableData.views) {
const v: ViewType = viewData.view;
console.log(`${v.title} ${v.id}`);
if (viewData.filters.length > 0) {
console.log('======= Filters =======');
console.log(viewData.filters);
}
if (viewData.sorts.length > 0) {
console.log('======= Sorts =======');
console.log(viewData.sorts);
}
}
if (tableData.shares.length > 0) {
console.log('======= Shares =======');
console.log(tableData.shares.forEach(s => console.log(s.uuid)));
}
if (tableData.webhooks.length > 0) {
console.log('======= Webhooks =======');
console.log(tableData.webhooks.forEach(w => console.log(w.id)));
}
}
}
}
}
Loading…
Cancel
Save