Browse Source

Merge pull request #5623 from nocodb/import-tests

update playwright tests for duplicate confirmation
pull/5635/head
mertmit 2 years ago committed by GitHub
parent
commit
695d1bbc5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  2. 18
      packages/nocodb/src/db/sql-client/lib/mysql/MysqlClient.ts
  3. 3
      packages/nocodb/src/modules/jobs/export-import/export.service.ts
  4. 27
      tests/playwright/pages/Dashboard/TreeView.ts
  5. 20
      tests/playwright/pages/ProjectsPage/index.ts
  6. 67
      tests/playwright/tests/db/projectOperations.spec.ts
  7. 97
      tests/playwright/tests/db/tableOperations.spec.ts
  8. 50
      tests/playwright/tests/utils/projectInfoApiUtil.ts

2
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -206,7 +206,7 @@ export class MysqlUi {
case 'datetime':
case 'timestamp':
return 6;
return '';
case 'time':
return '';

18
packages/nocodb/src/db/sql-client/lib/mysql/MysqlClient.ts

@ -653,13 +653,17 @@ class MysqlClient extends KnexClient {
if (column.dt === 'timestamp' || column.dt === 'datetime') {
if (response[0][i].cdf && response[0][i].ext) {
const str = response[0][i].ext;
// column.cdf_sequelize = response[0][i].cdf;
column.cdf =
response[0][i].cdf +
str.substring(
str.lastIndexOf('DEFAULT_GENERATED') +
'DEFAULT_GENERATED'.length,
);
if (str.includes('DEFAULT_GENERATED')) {
// column.cdf_sequelize = response[0][i].cdf;
column.cdf =
response[0][i].cdf +
str.substring(
str.lastIndexOf('DEFAULT_GENERATED') +
'DEFAULT_GENERATED'.length,
);
} else {
column.cdf = `${response[0][i].cdf} ${str}`;
}
} else {
column.cdf = response[0][i].cdf;
}

3
packages/nocodb/src/modules/jobs/export-import/export.service.ts

@ -245,6 +245,9 @@ export class ExportService {
column_name: column.column_name,
cc: column.cc,
cdf: column.cdf,
dt: column.dt,
dtxp: column.dtxp,
dtxs: column.dtxs,
meta: column.meta,
pk: column.pk,
pv: column.pv,

27
tests/playwright/pages/Dashboard/TreeView.ts

@ -174,6 +174,33 @@ 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',
});
await this.get().locator(`[data-testid="tree-view-table-${title} copy"]`).waitFor();
}
async verifyTabIcon({ title, icon }: { title: string; icon: string }) {
await new Promise(resolve => setTimeout(resolve, 1000));
await expect(

20
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({

67
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<any>;
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<ProjectInfo> = projectInfoOp.extractProjectData(testProjectId.id);
const duplicate: Promise<ProjectInfo> = projectInfoOp.extractProjectData(dupeProjectId.id);
const projectInfoOp: ProjectInfoApiUtil = new ProjectInfoApiUtil(context.token);
const orginal: Promise<ProjectInfo> = projectInfoOp.extractProjectInfo(testProjectId.id);
const duplicate: Promise<ProjectInfo> = 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<string> = 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<string> = 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);
});
});

97
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<TableInfo> = projectInfoApiUtil.extractTableInfo(context.project_id, 'Address');
// let duplicateTable: Promise<TableInfo> = await this.api.dbTable.list(projectId);.extractTableInfo(context.project_id, 'Address copy');
const api: Api<any> = 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<TableInfo> = projectInfoApiUtil.extractTableInfo(context.project_id, 'Address');
// let duplicateTable: Promise<TableInfo> = await this.api.dbTable.list(projectId);.extractTableInfo(context.project_id, 'Address copy');
const api: Api<any> = 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
});

50
tests/playwright/tests/utils/projectInfoOperator.ts → tests/playwright/tests/utils/projectInfoApiUtil.ts

@ -52,7 +52,7 @@ export class ProjectInfo {
tables: TableInfo[];
}
export class ProjectInfoOperator {
export class ProjectInfoApiUtil {
api: Api<any>;
constructor(token: string) {
@ -69,7 +69,7 @@ export class ProjectInfoOperator {
* @param projectId
* @returns
*/
async extractProjectData(projectId: string): Promise<ProjectInfo> {
async extractProjectInfo(projectId: string): Promise<ProjectInfo> {
// 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.
Loading…
Cancel
Save