Browse Source

feat(testing): Added meta sync test, toolbar page objects and improved multi db support

pull/3848/head
Muhammed Mustafa 2 years ago
parent
commit
c7c1c5eeb6
  1. 1
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  2. 9
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  3. 5
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  4. 1
      packages/nocodb/src/lib/meta/api/testApis.ts
  5. 43
      packages/nocodb/src/lib/services/test/TestResetService/index.ts
  6. 32
      packages/nocodb/src/lib/services/test/TestResetService/resetMysqlSakilaProject.ts
  7. 1672
      scripts/playwright/package-lock.json
  8. 4
      scripts/playwright/package.json
  9. 6
      scripts/playwright/pages/Base.ts
  10. 19
      scripts/playwright/pages/Dashboard/Grid/Toolbar/Fields.ts
  11. 41
      scripts/playwright/pages/Dashboard/Grid/Toolbar/Filter.ts
  12. 36
      scripts/playwright/pages/Dashboard/Grid/Toolbar/Sort.ts
  13. 37
      scripts/playwright/pages/Dashboard/Grid/Toolbar/index.ts
  14. 11
      scripts/playwright/pages/Dashboard/Grid/index.ts
  15. 1
      scripts/playwright/pages/Dashboard/Settings/Audit.ts
  16. 39
      scripts/playwright/pages/Dashboard/Settings/Metadata.ts
  17. 5
      scripts/playwright/pages/Dashboard/Settings/index.ts
  18. 44
      scripts/playwright/setup/db.ts
  19. 16
      scripts/playwright/setup/index.ts
  20. 25
      scripts/playwright/setup/mysqlExec.ts
  21. 11
      scripts/playwright/setup/sqliteExec.ts
  22. 155
      scripts/playwright/tests/metaSync.spec.ts

1
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -82,6 +82,7 @@ const filterAutoSaveLoc = computed({
ref="filterComp" ref="filterComp"
class="nc-table-toolbar-menu shadow-lg" class="nc-table-toolbar-menu shadow-lg"
:auto-save="filterAutoSave" :auto-save="filterAutoSave"
pw-data="grid-filter-menu"
@update:filters-length="filtersLength = $event" @update:filters-length="filtersLength = $event"
> >
<div v-if="!isPublic" class="flex items-end mt-2 min-h-[30px]" @click.stop> <div v-if="!isPublic" class="flex items-end mt-2 min-h-[30px]" @click.stop>

9
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -139,6 +139,7 @@ const getIcon = (c: ColumnType) =>
<template #overlay> <template #overlay>
<div <div
class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border" class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border"
pw-data="grid-fields-menu"
@click.stop @click.stop
> >
<a-card <a-card
@ -162,7 +163,13 @@ const getIcon = (c: ColumnType) =>
<div class="nc-fields-list py-1"> <div class="nc-fields-list py-1">
<Draggable v-model="fields" item-key="id" @change="onMove($event)"> <Draggable v-model="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field, index: index }"> <template #item="{ element: field, index: index }">
<div v-show="filteredFieldList.includes(field)" :key="field.id" class="px-2 py-1 flex items-center" @click.stop> <div
v-show="filteredFieldList.includes(field)"
:key="field.id"
class="px-2 py-1 flex items-center"
:pw-data="`grid-fields-menu-${field.title}`"
@click.stop
>
<a-checkbox <a-checkbox
v-model:checked="field.show" v-model:checked="field.show"
v-e="['a:fields:show-hide']" v-e="['a:fields:show-hide']"

5
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -53,7 +53,10 @@ watch(
</a-button> </a-button>
</div> </div>
<template #overlay> <template #overlay>
<div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border"> <div
class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border"
pw-data="grid-sorts-menu"
>
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop> <div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i"> <template v-for="(sort, i) in sorts || []" :key="i">
<MdiCloseBox class="nc-sort-item-remove-btn text-grey self-center" small @click.stop="deleteSort(sort, i)" /> <MdiCloseBox class="nc-sort-item-remove-btn text-grey self-center" small @click.stop="deleteSort(sort, i)" />

1
packages/nocodb/src/lib/meta/api/testApis.ts

@ -5,6 +5,7 @@ export async function reset(req: Request<any, any>, res) {
console.log('resetting id', req.body); console.log('resetting id', req.body);
const service = new TestResetService({ const service = new TestResetService({
parallelId: req.body.parallelId, parallelId: req.body.parallelId,
dbType: req.body.dbType,
}); });
res.json(await service.process()); res.json(await service.process());

43
packages/nocodb/src/lib/services/test/TestResetService/index.ts

@ -6,6 +6,7 @@ import Project from '../../../models/Project';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import resetMetaSakilaSqliteProject from './resetMetaSakilaSqliteProject'; import resetMetaSakilaSqliteProject from './resetMetaSakilaSqliteProject';
import resetMysqlSakilaProject from './resetMysqlSakilaProject'; import resetMysqlSakilaProject from './resetMysqlSakilaProject';
import Model from '../../../models/Model';
const loginRootUser = async () => { const loginRootUser = async () => {
const response = await axios.post( const response = await axios.post(
@ -17,16 +18,19 @@ const loginRootUser = async () => {
}; };
const projectTitleByType = { const projectTitleByType = {
sqlite3: 'sampleREST', sqlite: 'sampleREST',
mysql: 'externalREST', mysql: 'externalREST',
}; };
export class TestResetService { export class TestResetService {
private knex: Knex | null = null; private knex: Knex | null = null;
private readonly parallelId; private readonly parallelId;
constructor({ parallelId }: { parallelId: string }) { private readonly dbType;
constructor({ parallelId, dbType }: { parallelId: string; dbType: string }) {
this.knex = Noco.ncMeta.knex; this.knex = Noco.ncMeta.knex;
this.parallelId = parallelId; this.parallelId = parallelId;
this.dbType = dbType;
} }
async process() { async process() {
@ -36,7 +40,7 @@ export class TestResetService {
const { project } = await this.resetProject({ const { project } = await this.resetProject({
metaKnex: this.knex, metaKnex: this.knex,
token, token,
type: 'mysql', dbType: this.dbType,
parallelId: this.parallelId, parallelId: this.parallelId,
}); });
@ -50,28 +54,34 @@ export class TestResetService {
async resetProject({ async resetProject({
metaKnex, metaKnex,
token, token,
type, dbType,
parallelId, parallelId,
}: { }: {
metaKnex: Knex; metaKnex: Knex;
token: string; token: string;
type: string; dbType: string;
parallelId: string; parallelId: string;
}) { }) {
const title = `${projectTitleByType[type]}${parallelId}`; const title = `${projectTitleByType[dbType]}${parallelId}`;
const project: Project | undefined = await Project.getByTitle(title); const project: Project | undefined = await Project.getByTitle(title);
if (project) { if (project) {
const bases = await project.getBases(); const bases = await project.getBases();
if (dbType == 'sqlite') await dropTablesOfProject(metaKnex, project);
await Project.delete(project.id); await Project.delete(project.id);
if (bases.length > 0) await NcConnectionMgrv2.deleteAwait(bases[0]); if (bases.length > 0) await NcConnectionMgrv2.deleteAwait(bases[0]);
} }
if (type == 'sqlite3') { if (dbType == 'sqlite') {
await resetMetaSakilaSqliteProject({ token, metaKnex, title }); await resetMetaSakilaSqliteProject({ token, metaKnex, title });
} else if (type == 'mysql') { } else if (dbType == 'mysql') {
await resetMysqlSakilaProject({ token, title, parallelId, oldProject: project }); await resetMysqlSakilaProject({
token,
title,
parallelId,
oldProject: project,
});
} }
return { return {
@ -79,3 +89,18 @@ export class TestResetService {
}; };
} }
} }
const dropTablesOfProject = async (knex: Knex, project: Project) => {
const tables = await Model.list({
project_id: project.id,
base_id: (await project.getBases())[0].id,
});
for (const table of tables) {
if (table.type == 'table') {
await knex.raw(`DROP TABLE IF EXISTS ${table.table_name}`);
} else {
await knex.raw(`DROP VIEW IF EXISTS ${table.table_name}`);
}
}
};

32
packages/nocodb/src/lib/services/test/TestResetService/resetMysqlSakilaProject.ts

@ -42,29 +42,37 @@ const extMysqlProject = (title, parallelId) => ({
}); });
const mysqlSakilaSqlViews = [ const mysqlSakilaSqlViews = [
'actor_info', 'customer_list', 'film_list', 'nicer_but_slower_film_list', 'sales_by_film_category', 'sales_by_store', 'staff_list' 'actor_info',
] 'customer_list',
'film_list',
'nicer_but_slower_film_list',
'sales_by_film_category',
'sales_by_store',
'staff_list',
];
const dropTablesAndViews = async (knex: Knex) => { const dropTablesAndViews = async (knex: Knex) => {
for (const view of mysqlSakilaSqlViews) { for (const view of mysqlSakilaSqlViews) {
try { try {
await knex.raw(`DROP VIEW ${view}`); await knex.raw(`DROP VIEW ${view}`);
} catch (e) { } catch (e) {}
}
} }
for (const table of sakilaTableNames) { for (const table of sakilaTableNames) {
try { try {
await knex.raw(`DROP TABLE ${table}`); await knex.raw(`DROP TABLE ${table}`);
} catch (e) { } catch (e) {}
}
} }
}; };
const isSakilaMysqlToBeReset = async (knex: Knex, parallelId: string, project?: Project, ) => { const isSakilaMysqlToBeReset = async (
knex: Knex,
parallelId: string,
project?: Project
) => {
const tablesInDb: Array<string> = await knex.raw( const tablesInDb: Array<string> = await knex.raw(
`SELECT table_name FROM information_schema.tables WHERE table_schema = 'test_sakila_${parallelId}'` `SELECT table_name FROM information_schema.tables WHERE table_schema = 'test_sakila_${parallelId}'`
) );
if ( if (
tablesInDb.length === 0 || tablesInDb.length === 0 ||
@ -99,16 +107,12 @@ const resetSakilaMysql = async (knex:Knex, parallelId: string) => {
try { try {
const schemaFile = fs const schemaFile = fs
.readFileSync( .readFileSync(`${testsDir}/mysql-sakila-db/03-test-sakila-schema.sql`)
`${testsDir}/mysql-sakila-db/03-test-sakila-schema.sql`
)
.toString() .toString()
.replace(/test_sakila/g, `test_sakila_${parallelId}`); .replace(/test_sakila/g, `test_sakila_${parallelId}`);
const dataFile = fs const dataFile = fs
.readFileSync( .readFileSync(`${testsDir}/mysql-sakila-db/04-test-sakila-data.sql`)
`${testsDir}/mysql-sakila-db/04-test-sakila-data.sql`
)
.toString() .toString()
.replace(/test_sakila/g, `test_sakila_${parallelId}`); .replace(/test_sakila/g, `test_sakila_${parallelId}`);

1672
scripts/playwright/package-lock.json generated

File diff suppressed because it is too large Load Diff

4
scripts/playwright/package.json

@ -13,6 +13,8 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.26.1", "@playwright/test": "^1.26.1",
"axios": "^0.24.0" "axios": "^0.24.0",
"mysql2": "^2.3.3",
"promised-sqlite3": "^1.2.0"
} }
} }

6
scripts/playwright/pages/Base.ts

@ -10,11 +10,11 @@ export default abstract class BasePage {
} }
async toastWait ({message}: {message: string}){ async toastWait ({message}: {message: string}){
// const toast = await this.page.locator('.ant-message .ant-message-notice-content', {hasText: message}).last();
// await toast.waitFor({state: 'visible'});
// todo: text of toastr shows old one in the test assertion // todo: text of toastr shows old one in the test assertion
await this.rootPage.locator('.ant-message .ant-message-notice-content', {hasText: message}).last().textContent() await this.rootPage.locator('.ant-message .ant-message-notice-content', {hasText: message}).last().textContent()
.then((text) => expect(text).toContain(message)); .then((text) => expect(text).toContain(message));
// await this.rootPage.locator('.ant-message .ant-message-notice-content', {hasText: message}).last().waitFor({state: 'detached'});
} }
} }

19
scripts/playwright/pages/Dashboard/Grid/Toolbar/Fields.ts

@ -0,0 +1,19 @@
import BasePage from "../../../Base";
import { ToolbarPage } from ".";
export class ToolbarFieldsPage extends BasePage {
readonly toolbar: ToolbarPage;
constructor(toolbar: ToolbarPage) {
super(toolbar.rootPage);
this.toolbar = toolbar;
}
get() {
return this.rootPage.locator(`[pw-data="grid-fields-menu"]`);
}
click({ title}: { title: string }) {
return this.get().locator(`[pw-data="grid-fields-menu-${title}"]`).locator('input[type="checkbox"]').click();
}
}

41
scripts/playwright/pages/Dashboard/Grid/Toolbar/Filter.ts

@ -0,0 +1,41 @@
import BasePage from "../../../Base";
import { ToolbarPage } from ".";
export class ToolbarFilterPage extends BasePage {
readonly toolbar: ToolbarPage;
constructor(toolbar: ToolbarPage) {
super(toolbar.rootPage);
this.toolbar = toolbar;
}
get() {
return this.rootPage.locator(`[pw-data="grid-filter-menu"]`);
}
async addNew({
columnTitle,
opType,
value
}: {
columnTitle: string;
opType: string;
value: string;
}) {
await this.get().locator(`button:has-text("Add Filter")`).first().click();
await this.rootPage.locator('.nc-filter-field-select').last().click();
await this.rootPage.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list').locator(`div[label="${columnTitle}"][aria-selected="false"]`).click();
await this.rootPage.locator('.nc-filter-operation-select').last().click();
// await this.rootPage.locator('.nc-dropdown-filter-comp-op').locator(`.ant-select-item:has-text("${opType}")`).scrollIntoViewIfNeeded();
await this.rootPage.locator('.nc-dropdown-filter-comp-op').locator(`.ant-select-item:has-text("${opType}")`).click();
await this.rootPage.locator('.nc-filter-value-select').last().fill(value);
}
click({ title}: { title: string }) {
return this.get().locator(`[pw-data="grid-fields-menu-${title}"]`).locator('input[type="checkbox"]').click();
}
}

36
scripts/playwright/pages/Dashboard/Grid/Toolbar/Sort.ts

@ -0,0 +1,36 @@
import BasePage from "../../../Base";
import { ToolbarPage } from ".";
export class ToolbarSortPage extends BasePage {
readonly toolbar: ToolbarPage;
constructor(toolbar: ToolbarPage) {
super(toolbar.rootPage);
this.toolbar = toolbar;
}
get() {
return this.rootPage.locator(`[pw-data="grid-sorts-menu"]`);
}
async addNew({
columnTitle,
isAscending,
}: {
columnTitle: string;
isAscending: boolean;
}) {
await this.get().locator(`button:has-text("Add Sort Option")`).click();
await this.rootPage.locator('.nc-sort-field-select').click();
await this.rootPage.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list').locator(`div[label="${columnTitle}"]`).click();
await this.rootPage.locator('.nc-sort-dir-select').click();
await this.rootPage.locator('.nc-dropdown-sort-dir').locator('.ant-select-item').nth(isAscending ? 0 : 1).click();
}
click({ title}: { title: string }) {
return this.get().locator(`[pw-data="grid-fields-menu-${title}"]`).locator('input[type="checkbox"]').click();
}
}

37
scripts/playwright/pages/Dashboard/Grid/Toolbar/index.ts

@ -0,0 +1,37 @@
import BasePage from "../../../Base";
import { GridPage } from "..";
import { ToolbarFieldsPage } from "./Fields";
import { ToolbarSortPage } from "./Sort";
import { ToolbarFilterPage } from "./Filter";
export class ToolbarPage extends BasePage {
readonly grid: GridPage;
readonly fields: ToolbarFieldsPage;
readonly sort: ToolbarSortPage;
readonly filter: ToolbarFilterPage;
constructor(grid: GridPage) {
super(grid.rootPage);
this.grid = grid;
this.fields = new ToolbarFieldsPage(this);
this.sort = new ToolbarSortPage(this);
this.filter = new ToolbarFilterPage(this);
}
get() {
return this.rootPage.locator(`.nc-table-toolbar`);
}
async clickFields() {
await this.get().locator(`button:has-text("Fields")`).click();
}
async clickSort() {
await this.get().locator(`button:has-text("Sort")`).click();
}
async clickFilter() {
await this.get().locator(`button:has-text("Filter")`).click();
}
}

11
scripts/playwright/pages/Dashboard/Grid/index.ts

@ -4,6 +4,7 @@ import { DashboardPage } from '..';
import BasePage from '../../Base'; import BasePage from '../../Base';
import { CellPageObject } from './Cell'; import { CellPageObject } from './Cell';
import { ColumnPageObject } from './Column'; import { ColumnPageObject } from './Column';
import { ToolbarPage } from './Toolbar';
export class GridPage extends BasePage { export class GridPage extends BasePage {
readonly dashboard: DashboardPage; readonly dashboard: DashboardPage;
@ -11,6 +12,7 @@ export class GridPage extends BasePage {
readonly dashboardPage: DashboardPage; readonly dashboardPage: DashboardPage;
readonly column: ColumnPageObject; readonly column: ColumnPageObject;
readonly cell: CellPageObject; readonly cell: CellPageObject;
readonly toolbar: ToolbarPage;
constructor(dashboardPage: DashboardPage) { constructor(dashboardPage: DashboardPage) {
super(dashboardPage.rootPage); super(dashboardPage.rootPage);
@ -18,6 +20,7 @@ export class GridPage extends BasePage {
this.addNewTableButton = dashboardPage.get().locator('.nc-add-new-table'); this.addNewTableButton = dashboardPage.get().locator('.nc-add-new-table');
this.column = new ColumnPageObject(this); this.column = new ColumnPageObject(this);
this.cell = new CellPageObject(this); this.cell = new CellPageObject(this);
this.toolbar = new ToolbarPage(this);
} }
get() { get() {
@ -28,6 +31,14 @@ export class GridPage extends BasePage {
return this.get().locator(`tr[data-pw="grid-row-${index}"]`); return this.get().locator(`tr[data-pw="grid-row-${index}"]`);
} }
async rowCount() {
await this.get().locator('.nc-grid-row').count();
}
async verifyRowCount({count}: {count: number}) {
return expect(await this.get().locator('.nc-grid-row').count()).toBe(count);
}
async addNewRow({index = 0, title}: {index?: number, title?: string} = {}) { async addNewRow({index = 0, title}: {index?: number, title?: string} = {}) {
const rowCount = await this.get().locator('.nc-grid-row').count(); const rowCount = await this.get().locator('.nc-grid-row').count();
await this.get().locator('.nc-grid-add-new-cell').click(); await this.get().locator('.nc-grid-add-new-cell').click();

1
scripts/playwright/pages/Dashboard/Settings/Audit.ts

@ -1,4 +1,3 @@
// playwright-dev-page.ts
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
import { SettingsPage } from '.'; import { SettingsPage } from '.';
import BasePage from '../../Base'; import BasePage from '../../Base';

39
scripts/playwright/pages/Dashboard/Settings/Metadata.ts

@ -0,0 +1,39 @@
import { expect } from '@playwright/test';
import { SettingsPage } from '.';
import BasePage from '../../Base';
export class MetaDataPage extends BasePage {
private readonly settings: SettingsPage;
constructor(settings: SettingsPage) {
super(settings.rootPage);
this.settings = settings;
}
get() {
return this.settings.get().locator(`[pw-data="nc-settings-subtab-Metadata"]`);
}
async clickReload(){
await this.get().locator(`button:has-text("Reload")`).click();
await this.get().locator(`.animate-spin`).waitFor({state: 'visible'});
await this.get().locator(`.animate-spin`).waitFor({state: 'detached'});
}
async sync(){
await this.get().locator(`button:has-text("Sync Now")`).click();
await this.toastWait({message: 'Table metadata recreated successfully'});
await this.get().locator(`.animate-spin`).waitFor({state: 'visible'});
await this.get().locator(`.animate-spin`).waitFor({state: 'detached'});
}
async verifyRow(
{index, model, state}:
{index: number,model: string, state: string}
) {
await expect.poll(async () => {
return await this.get().locator(`tr.ant-table-row`).nth(index).locator(`td.ant-table-cell`).nth(0).textContent();
}).toContain(model);
expect(await this.get().locator(`tr.ant-table-row`).nth(index).locator(`td.ant-table-cell`).nth(1).textContent()).toContain(state);
}
}

5
scripts/playwright/pages/Dashboard/Settings/index.ts

@ -1,8 +1,7 @@
// playwright-dev-page.ts
import { Page } from '@playwright/test';
import { DashboardPage } from '..'; import { DashboardPage } from '..';
import BasePage from '../../Base'; import BasePage from '../../Base';
import { AuditSettingsPage } from './Audit'; import { AuditSettingsPage } from './Audit';
import { MetaDataPage } from './Metadata';
const tabInfo = { const tabInfo = {
'Team & Auth': 'teamAndAuth', 'Team & Auth': 'teamAndAuth',
@ -15,11 +14,13 @@ const tabInfo = {
export class SettingsPage extends BasePage { export class SettingsPage extends BasePage {
private readonly dashboard: DashboardPage; private readonly dashboard: DashboardPage;
readonly audit: AuditSettingsPage; readonly audit: AuditSettingsPage;
readonly metaData: MetaDataPage;
constructor(dashboard: DashboardPage) { constructor(dashboard: DashboardPage) {
super(dashboard.rootPage); super(dashboard.rootPage);
this.dashboard = dashboard; this.dashboard = dashboard;
this.audit = new AuditSettingsPage(this); this.audit = new AuditSettingsPage(this);
this.metaData = new MetaDataPage(this);
} }
get() { get() {

44
scripts/playwright/setup/db.ts

@ -0,0 +1,44 @@
import { NcContext } from ".";
import { PromisedDatabase } from "promised-sqlite3";
const sqliteDb = new PromisedDatabase();
const isMysql = (context: NcContext) => context.dbType === 'mysql';
const isSqlite = (context: NcContext) => context.dbType === 'sqlite';
const isPg = (context: NcContext) => context.dbType === 'pg';
const mysql = require("mysql2");
const mysqlExec = async (query) => {
// creates a new mysql connection using credentials from cypress.json env's
const connection = mysql.createConnection({
"host": "localhost",
"user": "root",
"password": "password",
"database": `test_sakila_${process.env.TEST_PARALLEL_INDEX}`
});
// start connection to db
connection.connect();
// exec query + disconnect to db as a Promise
return new Promise((resolve, reject) => {
connection.query(query, (error, results) => {
if (error) reject(error);
else {
connection.end();
// console.log(results)
return resolve(results);
}
});
});
}
async function sqliteExec(query) {
const rootProjectDir = __dirname.replace("/scripts/playwright/setup", "");
await sqliteDb.open(`${rootProjectDir}/packages/nocodb/test_noco.db`);
await sqliteDb.run(query);
}
export { sqliteExec, mysqlExec, isMysql, isSqlite, isPg };

16
scripts/playwright/setup/index.ts

@ -1,11 +1,19 @@
import { Page } from '@playwright/test'; import { Page } from '@playwright/test';
import axios from 'axios'; import axios from 'axios';
const setup = async ({page, typeOnLocalSetup}: {page: Page, typeOnLocalSetup?: string}) => { export interface NcContext {
const type = process.env.CI ? process.env.E2E_TYPE : typeOnLocalSetup; project: any;
token: string;
dbType?: string;
}
const setup = async ({page, typeOnLocalSetup}: {page: Page, typeOnLocalSetup?: string}): Promise<NcContext> => {
let dbType = process.env.CI ? process.env.E2E_TYPE : typeOnLocalSetup;
dbType = dbType || 'sqlite';
const response = await axios.post(`http://localhost:8080/api/v1/meta/test/reset`, { const response = await axios.post(`http://localhost:8080/api/v1/meta/test/reset`, {
parallelId: process.env.TEST_PARALLEL_INDEX, parallelId: process.env.TEST_PARALLEL_INDEX,
type: type ?? 'sqlite', dbType,
}); });
if(response.status !== 200) { if(response.status !== 200) {
@ -28,7 +36,7 @@ const setup = async ({page, typeOnLocalSetup}: {page: Page, typeOnLocalSetup?: s
await page.goto(`/#/nc/${project.id}/auth`); await page.goto(`/#/nc/${project.id}/auth`);
return { project, token }; return { project, token, dbType } as NcContext;
} }
export default setup; export default setup;

25
scripts/playwright/setup/mysqlExec.ts

@ -0,0 +1,25 @@
const mysql = require("mysql2");
const mysqlExec = async (query) => {
// creates a new mysql connection using credentials from cypress.json env's
const connection = mysql.createConnection({
"host": "127.0.0.1",
"user": "root",
"password": "password"
});
// start connection to db
connection.connect();
// exec query + disconnect to db as a Promise
return new Promise((resolve, reject) => {
connection.query(query, (error, results) => {
if (error) reject(error);
else {
connection.end();
// console.log(results)
return resolve(results);
}
});
});
}
export default mysqlExec;

11
scripts/playwright/setup/sqliteExec.ts

@ -0,0 +1,11 @@
const { PromisedDatabase } = require("promised-sqlite3");
const sqliteDb = new PromisedDatabase();
async function sqliteExec(query) {
const rootProjectDir = __dirname.replace("/scripts/playwright/setup", "");
await sqliteDb.open(`${rootProjectDir}/packages/nocodb/test_noco.db`);
await sqliteDb.run(query);
}
export default sqliteExec;

155
scripts/playwright/tests/metaSync.spec.ts

@ -0,0 +1,155 @@
import { test } from '@playwright/test';
import { DashboardPage } from '../pages/Dashboard';
import { SettingsPage } from '../pages/Dashboard/Settings';
import setup, { NcContext } from '../setup';
import { isSqlite, mysqlExec, sqliteExec } from '../setup/db';
// todo: Enable when view bug is fixed
test.describe('Meta sync', () => {
let dashboard: DashboardPage;
let settings: SettingsPage;
let context: NcContext;
let dbExec;
let projectPrefix;
test.beforeEach(async ({page}) => {
context = await setup({ page });
dashboard = new DashboardPage(page, context.project);
settings = dashboard.settings;
switch (context.dbType) {
case 'sqlite':
dbExec = sqliteExec;
break;
case 'mysql':
dbExec = mysqlExec;
break;
}
projectPrefix = isSqlite(context) ? context.project.prefix: '';
})
test('Meta sync', async () => {
await dashboard.gotoSettings();
await settings.selectTab({title: 'Project Metadata'});
await dbExec(`CREATE TABLE ${projectPrefix}table1 (id INT NOT NULL, col1 INT NULL, PRIMARY KEY (id))`);
await dbExec(`CREATE TABLE ${projectPrefix}table2 (id INT NOT NULL, col1 INT NULL, PRIMARY KEY (id))`);
await settings.metaData.clickReload();
await settings.metaData.verifyRow({index: 16, model: `${projectPrefix}table1`, state: 'New table'});
await settings.metaData.verifyRow({index: 17, model: `${projectPrefix}table2`, state: 'New table'});
await settings.metaData.sync();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: 'No change identified'});
await settings.metaData.verifyRow({index: 17, model: 'Table2', state: 'No change identified'});
if(!isSqlite(context)) {
// Add relation
await dbExec(`ALTER TABLE ${projectPrefix}table1 ADD INDEX fk1_idx (col1 ASC) VISIBLE`);
await dbExec(`ALTER TABLE ${projectPrefix}table1 ADD CONSTRAINT fk1 FOREIGN KEY (col1) REFERENCES ${projectPrefix}table2 (id) ON DELETE NO ACTION ON UPDATE NO ACTION`);
await settings.metaData.clickReload();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: 'New relation added'});
//verify after sync
await settings.metaData.sync();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: 'No change identified'});
// Remove relation
await dbExec(`ALTER TABLE ${projectPrefix}table1 DROP FOREIGN KEY fk1`);
await dbExec(`ALTER TABLE ${projectPrefix}table1 DROP INDEX fk1_idx`);
await settings.metaData.clickReload();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: "Relation removed"});
//verify after sync
await settings.metaData.sync();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: 'No change identified'});
}
// Add column
await dbExec(
isSqlite(context)
? `ALTER TABLE ${projectPrefix}table1 ADD COLUMN newCol TEXT NULL`
: `ALTER TABLE ${projectPrefix}table1 ADD COLUMN newCol VARCHAR(45) NULL AFTER id`
);
await settings.metaData.clickReload();
await settings.metaData.verifyRow({index: 16, model: `Table1`, state: 'New column(newCol)'});
//verify after sync
await settings.metaData.sync();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: 'No change identified'});
// Edit column
await dbExec(
isSqlite(context)
? `ALTER TABLE ${projectPrefix}table1 RENAME COLUMN newCol TO newColName`
: `ALTER TABLE ${projectPrefix}table1 CHANGE COLUMN newCol newColName VARCHAR(45) NULL DEFAULT NULL`
);
await settings.metaData.clickReload();
await settings.metaData.verifyRow({index: 16, model: `Table1`, state: 'New column(newColName), Column removed(newCol)'});
//verify after sync
await settings.metaData.sync();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: 'No change identified'});
// Delete column
// todo: Add for sqlite
if(!isSqlite(context)) {
await dbExec(`ALTER TABLE ${projectPrefix}table1 DROP COLUMN newColName`);
await settings.metaData.clickReload();
await settings.metaData.verifyRow({index: 16, model: `Table1`, state: 'Column removed(newColName)'});
//verify after sync
await settings.metaData.sync();
await settings.metaData.verifyRow({index: 16, model: 'Table1', state: 'No change identified'});
}
// Delete table
await dbExec(`DROP TABLE ${projectPrefix}table1`);
await dbExec(`DROP TABLE ${projectPrefix}table2`);
await settings.metaData.clickReload();
await settings.metaData.verifyRow({index: 16, model: `${projectPrefix}table1`, state: "Table removed"});
await settings.metaData.verifyRow({index: 17, model: `${projectPrefix}table2`, state: "Table removed"});
//verify after sync
await settings.metaData.sync();
if(isSqlite(context)) {
await settings.metaData.verifyRow({index: 16, model: 'CustomerList', state: 'No change identified'});
await settings.metaData.verifyRow({index: 17, model: 'FilmList', state: 'No change identified'});
} else {
await settings.metaData.verifyRow({index: 16, model: 'ActorInfo', state: 'No change identified'});
await settings.metaData.verifyRow({index: 17, model: 'CustomerList', state: 'No change identified'});
}
});
test('Hide, filter, sort', async() => {
await dbExec(`CREATE TABLE ${projectPrefix}table1 (id INT NOT NULL, col1 INT NULL, col2 INT NULL, col3 INT NULL, col4 INT NULL, PRIMARY KEY (id))`);
await dbExec(`INSERT INTO ${projectPrefix}table1 (id, col1, col2, col3, col4) VALUES (1,1,1,1,1), (2,2,2,2,2), (3,3,3,3,3), (4,4,4,4,4), (5,5,5,5,5), (6,6,6,6,6), (7,7,7,7,7), (8,8,8,8,8), (9,9,9,9,9);`);
await dashboard.gotoSettings();
await settings.selectTab({title: 'Project Metadata'});
await settings.metaData.clickReload();
await settings.metaData.sync();
await settings.close();
await dashboard.treeView.openTable({title: 'Table1'});
await dashboard.grid.toolbar.clickFields();
await dashboard.grid.toolbar.fields.click({title: 'Col1'});
await dashboard.grid.toolbar.clickFields();
await dashboard.grid.toolbar.clickSort();
await dashboard.grid.toolbar.sort.addNew({columnTitle: 'Col1', isAscending: false});
await dashboard.grid.toolbar.clickSort();
await dashboard.grid.toolbar.clickFilter();
await dashboard.grid.toolbar.filter.addNew({columnTitle: 'Col1', opType: '>=', value: '5'});
await dashboard.grid.toolbar.clickFilter();
await dashboard.grid.verifyRowCount({count: 5});
})
});
Loading…
Cancel
Save