mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 03:45:41 +00:00
feat(testing): Added meta sync test, toolbar page objects and improved multi db support
This commit is contained in:
@@ -82,6 +82,7 @@ const filterAutoSaveLoc = computed({
|
||||
ref="filterComp"
|
||||
class="nc-table-toolbar-menu shadow-lg"
|
||||
:auto-save="filterAutoSave"
|
||||
pw-data="grid-filter-menu"
|
||||
@update:filters-length="filtersLength = $event"
|
||||
>
|
||||
<div v-if="!isPublic" class="flex items-end mt-2 min-h-[30px]" @click.stop>
|
||||
|
||||
@@ -139,6 +139,7 @@ const getIcon = (c: ColumnType) =>
|
||||
<template #overlay>
|
||||
<div
|
||||
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
|
||||
>
|
||||
<a-card
|
||||
@@ -162,7 +163,13 @@ const getIcon = (c: ColumnType) =>
|
||||
<div class="nc-fields-list py-1">
|
||||
<Draggable v-model="fields" item-key="id" @change="onMove($event)">
|
||||
<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
|
||||
v-model:checked="field.show"
|
||||
v-e="['a:fields:show-hide']"
|
||||
|
||||
@@ -53,7 +53,10 @@ watch(
|
||||
</a-button>
|
||||
</div>
|
||||
<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>
|
||||
<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)" />
|
||||
|
||||
@@ -5,6 +5,7 @@ export async function reset(req: Request<any, any>, res) {
|
||||
console.log('resetting id', req.body);
|
||||
const service = new TestResetService({
|
||||
parallelId: req.body.parallelId,
|
||||
dbType: req.body.dbType,
|
||||
});
|
||||
|
||||
res.json(await service.process());
|
||||
|
||||
@@ -6,6 +6,7 @@ import Project from '../../../models/Project';
|
||||
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
|
||||
import resetMetaSakilaSqliteProject from './resetMetaSakilaSqliteProject';
|
||||
import resetMysqlSakilaProject from './resetMysqlSakilaProject';
|
||||
import Model from '../../../models/Model';
|
||||
|
||||
const loginRootUser = async () => {
|
||||
const response = await axios.post(
|
||||
@@ -17,16 +18,19 @@ const loginRootUser = async () => {
|
||||
};
|
||||
|
||||
const projectTitleByType = {
|
||||
sqlite3: 'sampleREST',
|
||||
sqlite: 'sampleREST',
|
||||
mysql: 'externalREST',
|
||||
};
|
||||
|
||||
export class TestResetService {
|
||||
private knex: Knex | null = null;
|
||||
private readonly parallelId;
|
||||
constructor({ parallelId }: { parallelId: string }) {
|
||||
private readonly dbType;
|
||||
|
||||
constructor({ parallelId, dbType }: { parallelId: string; dbType: string }) {
|
||||
this.knex = Noco.ncMeta.knex;
|
||||
this.parallelId = parallelId;
|
||||
this.dbType = dbType;
|
||||
}
|
||||
|
||||
async process() {
|
||||
@@ -36,7 +40,7 @@ export class TestResetService {
|
||||
const { project } = await this.resetProject({
|
||||
metaKnex: this.knex,
|
||||
token,
|
||||
type: 'mysql',
|
||||
dbType: this.dbType,
|
||||
parallelId: this.parallelId,
|
||||
});
|
||||
|
||||
@@ -50,28 +54,34 @@ export class TestResetService {
|
||||
async resetProject({
|
||||
metaKnex,
|
||||
token,
|
||||
type,
|
||||
dbType,
|
||||
parallelId,
|
||||
}: {
|
||||
metaKnex: Knex;
|
||||
token: string;
|
||||
type: string;
|
||||
dbType: string;
|
||||
parallelId: string;
|
||||
}) {
|
||||
const title = `${projectTitleByType[type]}${parallelId}`;
|
||||
const title = `${projectTitleByType[dbType]}${parallelId}`;
|
||||
const project: Project | undefined = await Project.getByTitle(title);
|
||||
|
||||
if (project) {
|
||||
const bases = await project.getBases();
|
||||
if (dbType == 'sqlite') await dropTablesOfProject(metaKnex, project);
|
||||
await Project.delete(project.id);
|
||||
|
||||
if (bases.length > 0) await NcConnectionMgrv2.deleteAwait(bases[0]);
|
||||
}
|
||||
|
||||
if (type == 'sqlite3') {
|
||||
if (dbType == 'sqlite') {
|
||||
await resetMetaSakilaSqliteProject({ token, metaKnex, title });
|
||||
} else if (type == 'mysql') {
|
||||
await resetMysqlSakilaProject({ token, title, parallelId, oldProject: project });
|
||||
} else if (dbType == 'mysql') {
|
||||
await resetMysqlSakilaProject({
|
||||
token,
|
||||
title,
|
||||
parallelId,
|
||||
oldProject: project,
|
||||
});
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,29 +42,37 @@ const extMysqlProject = (title, parallelId) => ({
|
||||
});
|
||||
|
||||
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) => {
|
||||
for (const view of mysqlSakilaSqlViews) {
|
||||
try {
|
||||
await knex.raw(`DROP VIEW ${view}`);
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
for (const table of sakilaTableNames) {
|
||||
try {
|
||||
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(
|
||||
`SELECT table_name FROM information_schema.tables WHERE table_schema = 'test_sakila_${parallelId}'`
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
tablesInDb.length === 0 ||
|
||||
@@ -73,14 +81,14 @@ const isSakilaMysqlToBeReset = async (knex: Knex, parallelId: string, project?:
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!project) return false;
|
||||
if (!project) return false;
|
||||
|
||||
const audits = await Audit.projectAuditList(project.id, {});
|
||||
|
||||
return audits?.length > 0;
|
||||
};
|
||||
|
||||
const resetSakilaMysql = async (knex:Knex, parallelId: string) => {
|
||||
const resetSakilaMysql = async (knex: Knex, parallelId: string) => {
|
||||
await dropTablesAndViews(knex);
|
||||
|
||||
const testsDir = __dirname.replace(
|
||||
@@ -99,16 +107,12 @@ const resetSakilaMysql = async (knex:Knex, parallelId: string) => {
|
||||
|
||||
try {
|
||||
const schemaFile = fs
|
||||
.readFileSync(
|
||||
`${testsDir}/mysql-sakila-db/03-test-sakila-schema.sql`
|
||||
)
|
||||
.readFileSync(`${testsDir}/mysql-sakila-db/03-test-sakila-schema.sql`)
|
||||
.toString()
|
||||
.replace(/test_sakila/g, `test_sakila_${parallelId}`);
|
||||
|
||||
const dataFile = fs
|
||||
.readFileSync(
|
||||
`${testsDir}/mysql-sakila-db/04-test-sakila-data.sql`
|
||||
)
|
||||
.readFileSync(`${testsDir}/mysql-sakila-db/04-test-sakila-data.sql`)
|
||||
.toString()
|
||||
.replace(/test_sakila/g, `test_sakila_${parallelId}`);
|
||||
|
||||
@@ -135,7 +139,7 @@ const resetMysqlSakilaProject = async ({
|
||||
}) => {
|
||||
const knex = Knex(config);
|
||||
|
||||
try{
|
||||
try {
|
||||
await knex.raw(`USE test_sakila_${parallelId}`);
|
||||
} catch (e) {
|
||||
await knex.raw(`CREATE DATABASE test_sakila_${parallelId}`);
|
||||
|
||||
1610
scripts/playwright/package-lock.json
generated
1610
scripts/playwright/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,8 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.26.1",
|
||||
"axios": "^0.24.0"
|
||||
"axios": "^0.24.0",
|
||||
"mysql2": "^2.3.3",
|
||||
"promised-sqlite3": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ export default abstract class BasePage {
|
||||
}
|
||||
|
||||
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
|
||||
await this.rootPage.locator('.ant-message .ant-message-notice-content', {hasText: message}).last().textContent()
|
||||
.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
Normal file
19
scripts/playwright/pages/Dashboard/Grid/Toolbar/Fields.ts
Normal file
@@ -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
Normal file
41
scripts/playwright/pages/Dashboard/Grid/Toolbar/Filter.ts
Normal file
@@ -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
Normal file
36
scripts/playwright/pages/Dashboard/Grid/Toolbar/Sort.ts
Normal file
@@ -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
Normal file
37
scripts/playwright/pages/Dashboard/Grid/Toolbar/index.ts
Normal file
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { DashboardPage } from '..';
|
||||
import BasePage from '../../Base';
|
||||
import { CellPageObject } from './Cell';
|
||||
import { ColumnPageObject } from './Column';
|
||||
import { ToolbarPage } from './Toolbar';
|
||||
|
||||
export class GridPage extends BasePage {
|
||||
readonly dashboard: DashboardPage;
|
||||
@@ -11,6 +12,7 @@ export class GridPage extends BasePage {
|
||||
readonly dashboardPage: DashboardPage;
|
||||
readonly column: ColumnPageObject;
|
||||
readonly cell: CellPageObject;
|
||||
readonly toolbar: ToolbarPage;
|
||||
|
||||
constructor(dashboardPage: DashboardPage) {
|
||||
super(dashboardPage.rootPage);
|
||||
@@ -18,6 +20,7 @@ export class GridPage extends BasePage {
|
||||
this.addNewTableButton = dashboardPage.get().locator('.nc-add-new-table');
|
||||
this.column = new ColumnPageObject(this);
|
||||
this.cell = new CellPageObject(this);
|
||||
this.toolbar = new ToolbarPage(this);
|
||||
}
|
||||
|
||||
get() {
|
||||
@@ -28,6 +31,14 @@ export class GridPage extends BasePage {
|
||||
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} = {}) {
|
||||
const rowCount = await this.get().locator('.nc-grid-row').count();
|
||||
await this.get().locator('.nc-grid-add-new-cell').click();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// playwright-dev-page.ts
|
||||
import { expect } from '@playwright/test';
|
||||
import { SettingsPage } from '.';
|
||||
import BasePage from '../../Base';
|
||||
|
||||
39
scripts/playwright/pages/Dashboard/Settings/Metadata.ts
Normal file
39
scripts/playwright/pages/Dashboard/Settings/Metadata.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
// playwright-dev-page.ts
|
||||
import { Page } from '@playwright/test';
|
||||
import { DashboardPage } from '..';
|
||||
import BasePage from '../../Base';
|
||||
import { AuditSettingsPage } from './Audit';
|
||||
import { MetaDataPage } from './Metadata';
|
||||
|
||||
const tabInfo = {
|
||||
'Team & Auth': 'teamAndAuth',
|
||||
@@ -15,11 +14,13 @@ const tabInfo = {
|
||||
export class SettingsPage extends BasePage {
|
||||
private readonly dashboard: DashboardPage;
|
||||
readonly audit: AuditSettingsPage;
|
||||
readonly metaData: MetaDataPage;
|
||||
|
||||
constructor(dashboard: DashboardPage) {
|
||||
super(dashboard.rootPage);
|
||||
this.dashboard = dashboard;
|
||||
this.audit = new AuditSettingsPage(this);
|
||||
this.metaData = new MetaDataPage(this);
|
||||
}
|
||||
|
||||
get() {
|
||||
|
||||
44
scripts/playwright/setup/db.ts
Normal file
44
scripts/playwright/setup/db.ts
Normal file
@@ -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 };
|
||||
@@ -1,11 +1,19 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import axios from 'axios';
|
||||
|
||||
const setup = async ({page, typeOnLocalSetup}: {page: Page, typeOnLocalSetup?: string}) => {
|
||||
const type = process.env.CI ? process.env.E2E_TYPE : typeOnLocalSetup;
|
||||
export interface NcContext {
|
||||
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`, {
|
||||
parallelId: process.env.TEST_PARALLEL_INDEX,
|
||||
type: type ?? 'sqlite',
|
||||
dbType,
|
||||
});
|
||||
|
||||
if(response.status !== 200) {
|
||||
@@ -28,7 +36,7 @@ const setup = async ({page, typeOnLocalSetup}: {page: Page, typeOnLocalSetup?: s
|
||||
|
||||
await page.goto(`/#/nc/${project.id}/auth`);
|
||||
|
||||
return { project, token };
|
||||
return { project, token, dbType } as NcContext;
|
||||
}
|
||||
|
||||
export default setup;
|
||||
25
scripts/playwright/setup/mysqlExec.ts
Normal file
25
scripts/playwright/setup/mysqlExec.ts
Normal file
@@ -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
Normal file
11
scripts/playwright/setup/sqliteExec.ts
Normal file
@@ -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
Normal file
155
scripts/playwright/tests/metaSync.spec.ts
Normal file
@@ -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});
|
||||
})
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user