mirror of https://github.com/nocodb/nocodb
Muhammed Mustafa
2 years ago
22 changed files with 2132 additions and 55 deletions
File diff suppressed because it is too large
Load Diff
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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 }; |
@ -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; |
@ -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; |
@ -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…
Reference in new issue