diff --git a/tests/playwright/pages/Dashboard/TreeView.ts b/tests/playwright/pages/Dashboard/TreeView.ts index 804cc1d2fe..9a8bb1870f 100644 --- a/tests/playwright/pages/Dashboard/TreeView.ts +++ b/tests/playwright/pages/Dashboard/TreeView.ts @@ -174,6 +174,32 @@ export class TreeViewPage extends BasePage { ).toHaveCount(1); } + async duplicateTable(title: string, includeData = true, includeViews = true) { + await this.get().locator(`.nc-project-tree-tbl-${title}`).click({ button: 'right' }); + await this.dashboard.get().locator('div.nc-project-menu-item:has-text("Duplicate")').click(); + + // Find the checkbox element with the label "Include data" + const includeDataCheckbox = await this.dashboard.get().getByText('Include data', { exact: true }); + // Check the checkbox if it is not already checked + if ((await includeDataCheckbox.isChecked()) && !includeData) { + await includeDataCheckbox.click(); // click the checkbox to check it + } + + // Find the checkbox element with the label "Include data" + const includeViewsCheckbox = await this.dashboard.get().getByText('Include views', { exact: true }); + // Check the checkbox if it is not already checked + if ((await includeViewsCheckbox.isChecked()) && !includeViews) { + await includeViewsCheckbox.click(); // click the checkbox to check it + } + + await this.waitForResponse({ + uiAction: () => this.rootPage.getByRole('button', { name: 'Confirm' }).click(), + httpMethodsToMatch: ['POST'], + requestUrlPathToMatch: `/api/v1/db/meta/duplicate/`, + responseJsonMatcher: json => json.name === 'duplicate-model', + }); + } + async verifyTabIcon({ title, icon }: { title: string; icon: string }) { await new Promise(resolve => setTimeout(resolve, 1000)); await expect( diff --git a/tests/playwright/pages/ProjectsPage/index.ts b/tests/playwright/pages/ProjectsPage/index.ts index 2413ba36f4..7543d3e87c 100644 --- a/tests/playwright/pages/ProjectsPage/index.ts +++ b/tests/playwright/pages/ProjectsPage/index.ts @@ -41,10 +41,14 @@ export class ProjectsPage extends BasePage { async duplicateProject({ name = 'sample', withoutPrefix, + includeData = true, + includeViews = true, }: { name?: string; type?: string; withoutPrefix?: boolean; + includeData: boolean; + includeViews: boolean; }) { if (!withoutPrefix) name = this.prefixTitle(name); // click three-dot @@ -53,8 +57,22 @@ export class ProjectsPage extends BasePage { await expect(this.rootPage.getByTestId('dupe-project-' + name)).toBeVisible(); // click duplicate await this.rootPage.getByTestId('dupe-project-' + name).click(); + + // Find the checkbox element with the label "Include data" + const includeDataCheckbox = await this.rootPage.getByText('Include data', { exact: true }); + // Check the checkbox if it is not already checked + if ((await includeDataCheckbox.isChecked()) && !includeData) { + await includeDataCheckbox.click(); // click the checkbox to check it + } + + // Find the checkbox element with the label "Include data" + const includeViewsCheckbox = await this.rootPage.getByText('Include views', { exact: true }); + // Check the checkbox if it is not already checked + if ((await includeViewsCheckbox.isChecked()) && !includeViews) { + await includeViewsCheckbox.click(); // click the checkbox to check it + } + // click duplicate confirmation "Do you want to duplicate 'sampleREST0' project?" - // assert message on duplicate confirmation page const dupeProjectSubmitAction = () => this.rootPage.getByRole('button', { name: 'Confirm' }).click(); await this.waitForResponse({ diff --git a/tests/playwright/tests/db/projectOperations.spec.ts b/tests/playwright/tests/db/projectOperations.spec.ts index b06cca7a5f..942ce6e61b 100644 --- a/tests/playwright/tests/db/projectOperations.spec.ts +++ b/tests/playwright/tests/db/projectOperations.spec.ts @@ -6,7 +6,7 @@ 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 { ProjectInfo, ProjectInfoApiUtil } from '../utils/projectInfoApiUtil'; import { deepCompare } from '../utils/objectCompareUtil'; test.describe('Project operations', () => { @@ -15,7 +15,7 @@ test.describe('Project operations', () => { let context: any; let api: Api; let projectPage: ProjectsPage; - test.setTimeout(70000); + test.setTimeout(100000); async function deleteIfExists(name: string) { try { @@ -30,6 +30,24 @@ test.describe('Project operations', () => { } } + async function createTestProjectWithData(testProjectName: string) { + await dashboard.clickHome(); + await projectPage.createProject({ name: testProjectName, withoutPrefix: true }); + await dashboard.treeView.quickImport({ title: 'Airtable' }); + await dashboard.importAirtable.import({ + key: airtableApiKey, + baseId: airtableApiBase, + }); + await dashboard.rootPage.waitForTimeout(1000); + // await quickVerify({ dashboard, airtableImport: true, context }); + } + + async function cleanupTestData(dupeProjectName: string, testProjectName: string) { + await dashboard.clickHome(); + await projectPage.deleteProject({ title: dupeProjectName, withoutPrefix: true }); + await projectPage.deleteProject({ title: testProjectName, withoutPrefix: true }); + } + test.beforeEach(async ({ page }) => { page.setDefaultTimeout(70000); context = await setup({ page }); @@ -71,23 +89,27 @@ test.describe('Project operations', () => { await deleteIfExists(dupeProjectName); // // data creation for orginial test project - await createTestProjectWithData(); + await createTestProjectWithData(testProjectName); // create duplicate await dashboard.clickHome(); - await projectPage.duplicateProject({ name: testProjectName, withoutPrefix: true }); + await projectPage.duplicateProject({ + name: testProjectName, + withoutPrefix: true, + includeData: true, + includeViews: true, + }); await projectPage.openProject({ title: dupeProjectName, withoutPrefix: true }); - await quickVerify({ dashboard, airtableImport: true, context }); + // 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); + const projectInfoOp: ProjectInfoApiUtil = new ProjectInfoApiUtil(context.token); + const orginal: Promise = projectInfoOp.extractProjectInfo(testProjectId.id); + const duplicate: Promise = projectInfoOp.extractProjectInfo(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', @@ -99,12 +121,16 @@ test.describe('Project operations', () => { 'fk_model_id', 'fk_column_id', 'fk_cover_image_col_id', - // potential bugs + // // potential bugs 'created_at', 'updated_at', ]); const ignoredKeys: Set = new Set([ + '.project.is_meta.id', '.project.is_meta.title', + '.project.tables.0.table.id', + '.project.tables.0.table.id.base_id', + // below are potential bugs '.project.is_meta.title.status', '.project.tables.0.table.shares.views.0.view._ptn.ptype.tn', @@ -119,6 +145,7 @@ test.describe('Project operations', () => { '.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', + '.project.tables.bases.users.0.1.2.3.email.invite_token.main_roles.roles', ]); const orginalProjectInfo: ProjectInfo = arr[0]; const duplicateProjectInfo: ProjectInfo = arr[1]; @@ -126,24 +153,6 @@ test.describe('Project operations', () => { }); // cleanup test-data - await cleanupTestData(); - - async function createTestProjectWithData() { - await dashboard.clickHome(); - await projectPage.createProject({ name: testProjectName, withoutPrefix: true }); - await dashboard.treeView.quickImport({ title: 'Airtable' }); - await dashboard.importAirtable.import({ - key: airtableApiKey, - baseId: airtableApiBase, - }); - await dashboard.rootPage.waitForTimeout(1000); - await quickVerify({ dashboard, airtableImport: true, context }); - } - - async function cleanupTestData() { - await dashboard.clickHome(); - await projectPage.deleteProject({ title: dupeProjectName, withoutPrefix: true }); - await projectPage.deleteProject({ title: testProjectName, withoutPrefix: true }); - } + await cleanupTestData(dupeProjectName, testProjectName); }); }); diff --git a/tests/playwright/tests/db/tableOperations.spec.ts b/tests/playwright/tests/db/tableOperations.spec.ts index 2751ffc5cb..8151b00279 100644 --- a/tests/playwright/tests/db/tableOperations.spec.ts +++ b/tests/playwright/tests/db/tableOperations.spec.ts @@ -1,7 +1,10 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; +import { Api, TableListType, TableType } from 'nocodb-sdk'; import { DashboardPage } from '../../pages/Dashboard'; import { SettingsPage, SettingTab } from '../../pages/Dashboard/Settings'; +import { deepCompare } from '../utils/objectCompareUtil'; import setup from '../../setup'; +import { ProjectInfoApiUtil, TableInfo } from '../utils/projectInfoApiUtil'; test.describe('Table Operations', () => { let dashboard: DashboardPage, settings: SettingsPage; @@ -52,4 +55,96 @@ test.describe('Table Operations', () => { await dashboard.treeView.changeTableIcon({ title: 'Address', icon: 'american-football' }); await dashboard.treeView.verifyTabIcon({ title: 'Address', icon: 'american-football' }); }); + + test('duplicate_table', async () => { + const orginalTableName = 'Actor'; + const dupTableName = 'Actor copy'; + // verify table icon customization + await dashboard.treeView.duplicateTable(orginalTableName, true, true); + await dashboard.treeView.verifyTable({ title: dupTableName }); + // let projectInfoApiUtil: ProjectInfoApiUtil = new ProjectInfoApiUtil(context.token); + // let orginalTable: Promise = projectInfoApiUtil.extractTableInfo(context.project_id, 'Address'); + // let duplicateTable: Promise = await this.api.dbTable.list(projectId);.extractTableInfo(context.project_id, 'Address copy'); + const api: Api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + const tables: TableListType = await api.dbTable.list(context.project.id); + const orginalTable: TableType = await tables.list.filter(t => t.title === orginalTableName)[0]; + const duplicateTable: TableType = await tables.list.filter(t => t.title === dupTableName)[0]; + expect( + deepCompare( + orginalTable, + duplicateTable, + undefined, + new Set([ + '.id', + '.id.base_id.project_id.table_name', + '.id.base_id.project_id.table_name.title', + '.id.base_id.project_id.table_name.title.type.meta.schema.enabled.mm.tags.pinned.deleted.order', + '.id.base_id.project_id.table_name.title.type.meta.schema.enabled.mm.tags.pinned.deleted.order.created_at', + '.id.base_id.project_id.table_name.title.type.meta.schema.enabled.mm.tags.pinned.deleted.order.created_at.updated_at', + ]) + ) + ).toBeTruthy(); + // check individual field values where values does not match as per design + }); + + test('duplicate_table_with_no_data_views', async () => { + const orginalTableName = 'Actor'; + const dupTableName = 'Actor copy'; + // verify table icon customization + await dashboard.treeView.duplicateTable(orginalTableName, false, false); + await dashboard.treeView.verifyTable({ title: dupTableName }); + // let projectInfoApiUtil: ProjectInfoApiUtil = new ProjectInfoApiUtil(context.token); + // let orginalTable: Promise = projectInfoApiUtil.extractTableInfo(context.project_id, 'Address'); + // let duplicateTable: Promise = await this.api.dbTable.list(projectId);.extractTableInfo(context.project_id, 'Address copy'); + const api: Api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + const tables: TableListType = await api.dbTable.list(context.project.id); + const orginalTable: TableType = await tables.list.filter(t => t.title === orginalTableName)[0]; + const duplicateTable: TableType = await tables.list.filter(t => t.title === dupTableName)[0]; + const p: ProjectInfoApiUtil = new ProjectInfoApiUtil(context.token); + const orginalTableInfo: TableInfo = await p.extractTableInfo(orginalTable, context.project.id); + const duplicateTableInfo: TableInfo = await p.extractTableInfo(duplicateTable, context.project.id); + expect( + deepCompare( + orginalTableInfo, + duplicateTableInfo, + new Set(['created_at']), + new Set([ + '.table.id', + '.table.id.base_id.project_id.table_name', + '.table.id.base_id.project_id.table_name.title', + '.table.id.base_id.project_id.table_name.title.type.meta.schema.enabled.mm.tags.pinned.deleted.order', + '.table.id.base_id.project_id.table_name.title.type.meta.schema.enabled.mm.tags.pinned.deleted.order.created_at', + '.table.id.base_id.project_id.table_name.title.type.meta.schema.enabled.mm.tags.pinned.deleted.order.created_at.updated_at', + '.table.shares.views.0.view.filters.sorts.firstPageData', + '.table.shares.views.webhooks.firstPageData.list.pageInfo.totalRows', + '.table.shares.views.0.view.ptn', + '.table.id.base_id.project_id.table_name.title.type.meta.schema.enabled.mm.tags.pinned.deleted.order.updated_at', + '.table.shares.views.0.view.ptn._ptn', + '.table.shares.views.0.view.ptn._ptn.ptype.tn', + '.table.shares.views.0.view.ptn._ptn.ptype.tn._tn', + '.table.shares.views.0.view.ptn._ptn.ptype.tn._tn.table_meta.id', + '.table.shares.views.0.view.ptn._ptn.ptype.tn._tn.table_meta.id.base_id.project_id.fk_model_id', + '.table.shares.views.0.view.ptn._ptn.ptype.tn._tn.table_meta.id.base_id.project_id.fk_model_id.title', + '.table.shares.views.0.view.ptn._ptn.ptype.tn._tn.table_meta.id.base_id.project_id.fk_model_id.title.type.is_default.show_system_fields.lock_type.uuid.password.show.order.updated_at', + '.table.shares.views.0.view.ptn._ptn.ptype.tn._tn.table_meta.id.base_id.project_id.fk_model_id.title.type.is_default.show_system_fields.lock_type.uuid.password.show.order.updated_at.meta.description.view.fk_view_id', + '.table.shares.views.0.view.ptn._ptn.ptype.tn._tn.table_meta.id.base_id.project_id.fk_model_id.title.type.is_default.show_system_fields.lock_type.uuid.password.show.order.updated_at.meta.description.view.fk_view_id.base_id.project_id.uuid.updated_at', + '.table.shares.views.webhooks.firstPageData.list.pageInfo.totalRows.page.pageSize.isFirstPage.isLastPage', + + // Mismatch length key: + '.table.shares.views.webhooks.firstPageData.list', + ]) + ) + ).toBeTruthy(); + }); + // check individual field values where values does not match as per design }); diff --git a/tests/playwright/tests/utils/projectInfoOperator.ts b/tests/playwright/tests/utils/projectInfoApiUtil.ts similarity index 71% rename from tests/playwright/tests/utils/projectInfoOperator.ts rename to tests/playwright/tests/utils/projectInfoApiUtil.ts index 950d8ebf3f..111bcd1fa6 100644 --- a/tests/playwright/tests/utils/projectInfoOperator.ts +++ b/tests/playwright/tests/utils/projectInfoApiUtil.ts @@ -52,7 +52,7 @@ export class ProjectInfo { tables: TableInfo[]; } -export class ProjectInfoOperator { +export class ProjectInfoApiUtil { api: Api; constructor(token: string) { @@ -69,7 +69,7 @@ export class ProjectInfoOperator { * @param projectId * @returns */ - async extractProjectData(projectId: string): Promise { + async extractProjectInfo(projectId: string): Promise { // TODO: capture apiTokens, projectSettings, ACLVisibilityRules, UI ACL (discuss before adding) const project: ProjectType = await this.api.project.read(projectId); // bases @@ -86,29 +86,39 @@ export class ProjectInfoOperator { 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; + const tableInfo: TableInfo = await this.extractTableInfo(table, projectId); projectInfo.tables.push(tableInfo); - tableInfo.firstPageData = await this.api.dbTableRow.list('noco', projectId, table.id); } return projectInfo; } + async extractTableInfo(table: TableType, projectId: string) { + 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 viewInfo: ViewInfo = await this.extractViewInfo(v, projectId, table.id); + 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; + tableInfo.firstPageData = await this.api.dbTableRow.list('noco', projectId, table.id); + return tableInfo; + } + + private async extractViewInfo(v: ViewType, projectId: string, tableId: string) { + 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, tableId, v.id); + viewInfo.filters = filters.list; + viewInfo.sorts = sorts.list; + return viewInfo; + } + /** * helper function to print projectInfo * do not use this function to assert anything.