From b33f29def601ef9239ccac210e7269e8792e615d Mon Sep 17 00:00:00 2001 From: starbirdtech383 Date: Fri, 5 May 2023 03:01:57 +0530 Subject: [PATCH] added deep compare util and tests for duplicate project --- tests/playwright/package-lock.json | 55 ++++-- tests/playwright/package.json | 2 +- tests/playwright/quickTests/commonTest.ts | 10 +- .../tests/db/projectOperations.spec.ts | 52 +++++- .../tests/utils/objectCompareUtil.ts | 68 +++++++ .../tests/utils/projectInfoOperator.ts | 167 ++++++++++++++++++ 6 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 tests/playwright/tests/utils/objectCompareUtil.ts create mode 100644 tests/playwright/tests/utils/projectInfoOperator.ts diff --git a/tests/playwright/package-lock.json b/tests/playwright/package-lock.json index 18189b8a16..562a2e465a 100644 --- a/tests/playwright/package-lock.json +++ b/tests/playwright/package-lock.json @@ -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": { diff --git a/tests/playwright/package.json b/tests/playwright/package.json index 16b9708480..96372d60d0 100644 --- a/tests/playwright/package.json +++ b/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", diff --git a/tests/playwright/quickTests/commonTest.ts b/tests/playwright/quickTests/commonTest.ts index 1a5c30673c..62188bee2f 100644 --- a/tests/playwright/quickTests/commonTest.ts +++ b/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 }); + } } }; diff --git a/tests/playwright/tests/db/projectOperations.spec.ts b/tests/playwright/tests/db/projectOperations.spec.ts index fdc7480982..09ce7273ed 100644 --- a/tests/playwright/tests/db/projectOperations.spec.ts +++ b/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; 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 = projectInfoOp.extractProjectData(testProjectId.id); + const duplicate: Promise = 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 = 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 = 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(); diff --git a/tests/playwright/tests/utils/objectCompareUtil.ts b/tests/playwright/tests/utils/objectCompareUtil.ts new file mode 100644 index 0000000000..e6eb072499 --- /dev/null +++ b/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, + ignoredKeys?: Set, + 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; +} diff --git a/tests/playwright/tests/utils/projectInfoOperator.ts b/tests/playwright/tests/utils/projectInfoOperator.ts new file mode 100644 index 0000000000..950d8ebf3f --- /dev/null +++ b/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; + + 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 { + // 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))); + } + } + } + } +}