Browse Source

feat(testing): Added quick test, added isEmptyProject option to test setup to have project created with no sakila tables

pull/3848/head
Muhammed Mustafa 2 years ago
parent
commit
6b0d0fcb04
  1. 2
      packages/nc-gui/components/webhook/Drawer.vue
  2. 6
      packages/nc-gui/pages/index/index/index.vue
  3. 1
      packages/nocodb/src/lib/meta/api/testApis.ts
  4. 15
      packages/nocodb/src/lib/services/test/TestResetService/index.ts
  5. 6
      packages/nocodb/src/lib/services/test/TestResetService/resetMetaSakilaSqliteProject.ts
  6. 17
      packages/nocodb/src/lib/services/test/TestResetService/resetMysqlSakilaProject.ts
  7. 14
      packages/nocodb/src/lib/services/test/TestResetService/resetPgSakilaProject.ts
  8. 2
      scripts/playwright/package.json
  9. 38
      scripts/playwright/pages/Dashboard/WebhookForm/index.ts
  10. 1
      scripts/playwright/pages/Dashboard/common/Toolbar/Actions/index.ts
  11. 10
      scripts/playwright/pages/Dashboard/common/Toolbar/Fields.ts
  12. 32
      scripts/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  13. 19
      scripts/playwright/pages/Dashboard/common/Toolbar/Sort.ts
  14. 8
      scripts/playwright/pages/Dashboard/common/Toolbar/index.ts
  15. 5
      scripts/playwright/pages/ProjectsPage/index.ts
  16. 5
      scripts/playwright/playwright.config.ts
  17. 249
      scripts/playwright/quickTests/commonTest.ts
  18. 120
      scripts/playwright/quickTests/quickTests.spec.ts
  19. 3
      scripts/playwright/setup/index.ts
  20. 27
      scripts/playwright/tests/import.spec.ts
  21. 2
      scripts/playwright/tests/rolesCreate.spec.ts
  22. 2
      scripts/playwright/tests/views.spec.ts
  23. 2
      scripts/playwright/tests/webhook.spec.ts

2
packages/nc-gui/components/webhook/Drawer.vue

@ -31,7 +31,7 @@ async function editHook(hook: Record<string, any>) {
class="nc-drawer-webhook" class="nc-drawer-webhook"
@keydown.esc="vModel = false" @keydown.esc="vModel = false"
> >
<a-layout> <a-layout class="nc-drawer-webhook-body">
<a-layout-content class="px-10 py-5 scrollbar-thin-primary"> <a-layout-content class="px-10 py-5 scrollbar-thin-primary">
<LazyWebhookEditor v-if="editOrAdd" :hook="currentHook" @back-to-list="editOrAdd = false" /> <LazyWebhookEditor v-if="editOrAdd" :hook="currentHook" @back-to-list="editOrAdd = false" />

6
packages/nc-gui/pages/index/index/index.vue

@ -284,7 +284,11 @@ const copyProjectMeta = async () => {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<MdiEditOutline v-e="['c:project:edit:rename']" class="nc-action-btn" @click.stop="navigateTo(`/${text}`)" /> <MdiEditOutline v-e="['c:project:edit:rename']" class="nc-action-btn" @click.stop="navigateTo(`/${text}`)" />
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)" /> <MdiDeleteOutline
class="nc-action-btn"
:pw-data="`delete-project-${record.title}`"
@click.stop="deleteProject(record)"
/>
</div> </div>
</template> </template>
</a-table-column> </a-table-column>

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

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

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

@ -28,11 +28,21 @@ export class TestResetService {
private knex: Knex | null = null; private knex: Knex | null = null;
private readonly parallelId; private readonly parallelId;
private readonly dbType; private readonly dbType;
private readonly isEmptyProject: boolean;
constructor({ parallelId, dbType }: { parallelId: string; dbType: string }) { constructor({
parallelId,
dbType,
isEmptyProject,
}: {
parallelId: string;
dbType: string;
isEmptyProject: boolean;
}) {
this.knex = Noco.ncMeta.knex; this.knex = Noco.ncMeta.knex;
this.parallelId = parallelId; this.parallelId = parallelId;
this.dbType = dbType; this.dbType = dbType;
this.isEmptyProject = isEmptyProject;
} }
async process() { async process() {
@ -81,6 +91,7 @@ export class TestResetService {
metaKnex, metaKnex,
title, title,
oldProject: project, oldProject: project,
isEmptyProject: this.isEmptyProject,
}); });
} else if (dbType == 'mysql') { } else if (dbType == 'mysql') {
await resetMysqlSakilaProject({ await resetMysqlSakilaProject({
@ -88,6 +99,7 @@ export class TestResetService {
title, title,
parallelId, parallelId,
oldProject: project, oldProject: project,
isEmptyProject: this.isEmptyProject,
}); });
} else if (dbType == 'pg') { } else if (dbType == 'pg') {
await resetPgSakilaProject({ await resetPgSakilaProject({
@ -95,6 +107,7 @@ export class TestResetService {
title, title,
parallelId, parallelId,
oldProject: project, oldProject: project,
isEmptyProject: this.isEmptyProject,
}); });
} }

6
packages/nocodb/src/lib/services/test/TestResetService/resetMetaSakilaSqliteProject.ts

@ -20,17 +20,21 @@ const resetMetaSakilaSqliteProject = async ({
token, token,
title, title,
oldProject, oldProject,
isEmptyProject,
}: { }: {
metaKnex: Knex; metaKnex: Knex;
token: string; token: string;
title: string; title: string;
oldProject: Project; oldProject: Project;
isEmptyProject: boolean;
}) => { }) => {
const project = await createProject(token, title); const project = await createProject(token, title);
await dropTablesAndViews(metaKnex, oldProject.prefix); if (oldProject) await dropTablesAndViews(metaKnex, oldProject.prefix);
await dropTablesAndViews(metaKnex, project.prefix); await dropTablesAndViews(metaKnex, project.prefix);
if (isEmptyProject) return;
await resetMetaSakilaSqlite(metaKnex, project.prefix, oldProject); await resetMetaSakilaSqlite(metaKnex, project.prefix, oldProject);
await syncMeta(project, token); await syncMeta(project, token);

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

@ -63,7 +63,11 @@ const isSakilaMysqlToBeReset = async (
return audits?.length > 0; return audits?.length > 0;
}; };
const resetSakilaMysql = async (knex: Knex, parallelId: string) => { const resetSakilaMysql = async (
knex: Knex,
parallelId: string,
isEmptyProject: boolean
) => {
const testsDir = __dirname.replace( const testsDir = __dirname.replace(
'/src/lib/services/test/TestResetService', '/src/lib/services/test/TestResetService',
'/tests' '/tests'
@ -76,6 +80,8 @@ const resetSakilaMysql = async (knex: Knex, parallelId: string) => {
} }
await knex.raw(`CREATE DATABASE test_sakila_${parallelId}`); await knex.raw(`CREATE DATABASE test_sakila_${parallelId}`);
if (isEmptyProject) return;
const trx = await knex.transaction(); const trx = await knex.transaction();
try { try {
@ -105,11 +111,13 @@ const resetMysqlSakilaProject = async ({
title, title,
parallelId, parallelId,
oldProject, oldProject,
isEmptyProject,
}: { }: {
token: string; token: string;
title: string; title: string;
parallelId: string; parallelId: string;
oldProject?: Project | undefined; oldProject?: Project | undefined;
isEmptyProject: boolean;
}) => { }) => {
const knex = Knex(config); const knex = Knex(config);
@ -120,8 +128,11 @@ const resetMysqlSakilaProject = async ({
await knex.raw(`USE test_sakila_${parallelId}`); await knex.raw(`USE test_sakila_${parallelId}`);
} }
if (await isSakilaMysqlToBeReset(knex, parallelId, oldProject)) { if (
await resetSakilaMysql(knex, parallelId); isEmptyProject ||
(await isSakilaMysqlToBeReset(knex, parallelId, oldProject))
) {
await resetSakilaMysql(knex, parallelId, isEmptyProject);
} }
const response = await axios.post( const response = await axios.post(

14
packages/nocodb/src/lib/services/test/TestResetService/resetPgSakilaProject.ts

@ -66,7 +66,11 @@ const isSakilaPgToBeReset = async (knex: Knex, project?: Project) => {
return audits?.length > 0; return audits?.length > 0;
}; };
const resetSakilaPg = async (pgknex: Knex, parallelId: string) => { const resetSakilaPg = async (
pgknex: Knex,
parallelId: string,
isEmptyProject: boolean
) => {
const testsDir = __dirname.replace( const testsDir = __dirname.replace(
'/src/lib/services/test/TestResetService', '/src/lib/services/test/TestResetService',
'/tests' '/tests'
@ -75,6 +79,8 @@ const resetSakilaPg = async (pgknex: Knex, parallelId: string) => {
await pgknex.raw(`DROP DATABASE IF EXISTS sakila_${parallelId}`); await pgknex.raw(`DROP DATABASE IF EXISTS sakila_${parallelId}`);
await pgknex.raw(`CREATE DATABASE sakila_${parallelId}`); await pgknex.raw(`CREATE DATABASE sakila_${parallelId}`);
if (isEmptyProject) return;
const sakilaKnex = Knex(sakilaKnexConfig(parallelId)); const sakilaKnex = Knex(sakilaKnexConfig(parallelId));
const schemaFile = await fs.readFile( const schemaFile = await fs.readFile(
@ -103,11 +109,13 @@ const resetPgSakilaProject = async ({
title, title,
parallelId, parallelId,
oldProject, oldProject,
isEmptyProject,
}: { }: {
token: string; token: string;
title: string; title: string;
parallelId: string; parallelId: string;
oldProject?: Project | undefined; oldProject?: Project | undefined;
isEmptyProject: boolean;
}) => { }) => {
const pgknex = Knex(config); const pgknex = Knex(config);
@ -117,9 +125,9 @@ const resetPgSakilaProject = async ({
const sakilaKnex = Knex(sakilaKnexConfig(parallelId)); const sakilaKnex = Knex(sakilaKnexConfig(parallelId));
if (await isSakilaPgToBeReset(sakilaKnex, oldProject)) { if (isEmptyProject || (await isSakilaPgToBeReset(sakilaKnex, oldProject))) {
await sakilaKnex.destroy(); await sakilaKnex.destroy();
await resetSakilaPg(pgknex, parallelId); await resetSakilaPg(pgknex, parallelId, isEmptyProject);
} else { } else {
await sakilaKnex.destroy(); await sakilaKnex.destroy();
} }

2
scripts/playwright/package.json

@ -5,7 +5,9 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "TRACE=true npx playwright test --workers=4", "test": "TRACE=true npx playwright test --workers=4",
"quick-test": "TRACE=true PW_QUICK_TEST=1 npx playwright test --workers=4",
"test-debug": "./startPlayWrightServer.sh; PW_TEST_REUSE_CONTEXT=1 PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:31000/ PWDEBUG=console npx playwright test -c playwright.config.ts --headed --project=chromium --retries 0 --timeout 0 --workers 1 --max-failures=1", "test-debug": "./startPlayWrightServer.sh; PW_TEST_REUSE_CONTEXT=1 PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:31000/ PWDEBUG=console npx playwright test -c playwright.config.ts --headed --project=chromium --retries 0 --timeout 0 --workers 1 --max-failures=1",
"test-debug-quick-sqlite": "./startPlayWrightServer.sh; PW_QUICK_TEST=1 PW_TEST_REUSE_CONTEXT=1 PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:31000/ PWDEBUG=console npx playwright test -c playwright.config.ts --headed --project=chromium --retries 0 --timeout 5 --workers 1 --max-failures=1",
"ci-test-mysql": "E2E_DB_TYPE=mysql npx playwright test --workers=2" "ci-test-mysql": "E2E_DB_TYPE=mysql npx playwright test --workers=2"
}, },
"keywords": [], "keywords": [],

38
scripts/playwright/pages/Dashboard/WebhookForm/index.ts

@ -22,9 +22,10 @@ export class WebhookFormPage extends BasePage {
} }
get() { get() {
return this.dashboard.get().locator(`.nc-drawer-webhook`); return this.dashboard.get().locator(`.nc-drawer-webhook-body`);
} }
// todo: Removing opening webhook drawer logic as it belongs to `Toolbar` page
async create({ async create({
title, title,
event, event,
@ -147,6 +148,14 @@ export class WebhookFormPage extends BasePage {
await this.dashboard.get().locator(`.nc-hook`).nth(index).click(); await this.dashboard.get().locator(`.nc-hook`).nth(index).click();
} }
async openForm({ index }: { index: number }) {
await this.dashboard.get().locator(`.nc-hook`).nth(index).click();
}
async click({ index }: { index: number }) {
await this.dashboard.get().locator(`.nc-hook`).nth(index).click();
}
async configureHeader({ key, value }: { key: string; value: string }) { async configureHeader({ key, value }: { key: string; value: string }) {
// hardcode "Content-type: application/json" // hardcode "Content-type: application/json"
await this.get().locator(`.ant-tabs-tab-btn:has-text("Headers")`).click(); await this.get().locator(`.ant-tabs-tab-btn:has-text("Headers")`).click();
@ -163,4 +172,31 @@ export class WebhookFormPage extends BasePage {
.locator("input.ant-checkbox-input") .locator("input.ant-checkbox-input")
.click(); .click();
} }
async verifyForm({
title,
hookEvent,
url,
notificationType,
urlMethod,
condition,
}: {
title: string;
hookEvent: string;
url: string;
notificationType: string;
urlMethod: string;
condition: boolean;
}) {
await expect(await this.get().locator('input.nc-text-field-hook-title').inputValue()).toBe(title);
await expect(await this.get().locator('.nc-text-field-hook-event >> .ant-select-selection-item').innerText()).toBe(hookEvent);
await expect(await this.get().locator('.nc-select-hook-notification-type >> .ant-select-selection-item').innerText()).toBe(notificationType);
await expect(await this.get().locator('.nc-select-hook-url-method >> .ant-select-selection-item').innerText()).toBe(urlMethod);
await expect(await this.get().locator('input.nc-text-field-hook-url-path').inputValue()).toBe(url);
await expect(await this.get().locator('label.nc-check-box-hook-condition >> input[type="checkbox"]').isChecked()).toBe(condition);
}
async goBackFromForm() {
await this.get().locator('svg.nc-icon-hook-navigate-left').click();
}
} }

1
scripts/playwright/pages/Dashboard/common/Toolbar/Actions/index.ts

@ -16,6 +16,7 @@ export class ToolbarActionsPage extends BasePage {
return this.rootPage.locator(`[pw-data="toolbar-actions"]`); return this.rootPage.locator(`[pw-data="toolbar-actions"]`);
} }
// todo: use enum
async click(label: string) { async click(label: string) {
await this.get().locator(`span:has-text("${label}")`).click(); await this.get().locator(`span:has-text("${label}")`).click();
} }

10
scripts/playwright/pages/Dashboard/common/Toolbar/Fields.ts

@ -1,3 +1,4 @@
import { expect } from "@playwright/test";
import BasePage from "../../../Base"; import BasePage from "../../../Base";
import { ToolbarPage } from "./index"; import { ToolbarPage } from "./index";
@ -22,6 +23,15 @@ export class ToolbarFieldsPage extends BasePage {
await this.toolbar.clickFields(); await this.toolbar.clickFields();
} }
async verify({ title, checked }: { title: string, checked: boolean }) {
await expect(
await this.get()
.locator(`[pw-data="nc-fields-menu-${title}"]`)
.locator('input[type="checkbox"]')
.isChecked()
).toBe(checked);
}
async click({ title }: { title: string }) { async click({ title }: { title: string }) {
await this.get() await this.get()
.locator(`[pw-data="nc-fields-menu-${title}"]`) .locator(`[pw-data="nc-fields-menu-${title}"]`)

32
scripts/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -1,3 +1,4 @@
import { expect } from "@playwright/test";
import BasePage from "../../../Base"; import BasePage from "../../../Base";
import { ToolbarPage } from "./index"; import { ToolbarPage } from "./index";
@ -13,6 +14,37 @@ export class ToolbarFilterPage extends BasePage {
return this.rootPage.locator(`[pw-data="nc-filter-menu"]`); return this.rootPage.locator(`[pw-data="nc-filter-menu"]`);
} }
async verify({
index,
column,
operator,
value,
}: {
index: number;
column: string;
operator: string;
value: string;
}) {
await expect(
await this.get().locator('.nc-filter-field-select').nth(index).innerText()
).toBe(column);
await expect(
await this.get().locator('.nc-filter-operation-select').nth(index).innerText()
).toBe(operator);
await expect(
await this.get().locator('input.nc-filter-value-select').nth(index).inputValue()
).toBe(value);
}
async verifyFilter({ title }: { title: string }) {
await expect(
await this.get()
.locator(`[pw-data="nc-fields-menu-${title}"]`)
.locator('input[type="checkbox"]')
.isChecked()
).toBe(true);
}
// Todo: Handle the case of operator does not need a value // Todo: Handle the case of operator does not need a value
async addNew({ async addNew({
columnTitle, columnTitle,

19
scripts/playwright/pages/Dashboard/common/Toolbar/Sort.ts

@ -1,3 +1,4 @@
import { expect } from "@playwright/test";
import BasePage from "../../../Base"; import BasePage from "../../../Base";
import { ToolbarPage } from "./index"; import { ToolbarPage } from "./index";
@ -13,6 +14,23 @@ export class ToolbarSortPage extends BasePage {
return this.rootPage.locator(`[pw-data="nc-sorts-menu"]`); return this.rootPage.locator(`[pw-data="nc-sorts-menu"]`);
} }
async verify({
index,
column,
direction,
}: {
index: number;
column: string;
direction: string;
}) {
await expect(
await this.get().locator('.nc-sort-field-select').nth(index).innerText()
).toBe(column);
await expect(
await this.get().locator('.nc-sort-dir-select >> span.ant-select-selection-item').nth(index).innerText()
).toBe(direction);
}
async addSort({ async addSort({
columnTitle, columnTitle,
isAscending, isAscending,
@ -61,6 +79,7 @@ export class ToolbarSortPage extends BasePage {
await this.toolbar.parent.waitLoading(); await this.toolbar.parent.waitLoading();
} }
// todo: remove this opening sort menu logic
async resetSort() { async resetSort() {
// open sort menu // open sort menu
await this.toolbar.clickSort(); await this.toolbar.clickSort();

8
scripts/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -54,7 +54,7 @@ export class ToolbarPage extends BasePage {
async clickFields() { async clickFields() {
const menuOpen = await this.fields.get().isVisible(); const menuOpen = await this.fields.get().isVisible();
await this.get().locator(`button:has-text("Fields")`).click(); await this.get().locator(`button.nc-fields-menu-btn`).click();
// Wait for the menu to close // Wait for the menu to close
if (menuOpen) await this.fields.get().waitFor({ state: "hidden" }); if (menuOpen) await this.fields.get().waitFor({ state: "hidden" });
@ -63,7 +63,7 @@ export class ToolbarPage extends BasePage {
async clickSort() { async clickSort() {
const menuOpen = await this.sort.get().isVisible(); const menuOpen = await this.sort.get().isVisible();
await this.get().locator(`button:has-text("Sort")`).click(); await this.get().locator(`button.nc-sort-menu-btn`).click();
// Wait for the menu to close // Wait for the menu to close
if (menuOpen) await this.sort.get().waitFor({ state: "hidden" }); if (menuOpen) await this.sort.get().waitFor({ state: "hidden" });
@ -72,7 +72,7 @@ export class ToolbarPage extends BasePage {
async clickFilter() { async clickFilter() {
const menuOpen = await this.filter.get().isVisible(); const menuOpen = await this.filter.get().isVisible();
await this.get().locator(`button:has-text("Filter")`).click(); await this.get().locator(`button.nc-filter-menu-btn`).click();
// Wait for the menu to close // Wait for the menu to close
if (menuOpen) await this.filter.get().waitFor({ state: "hidden" }); if (menuOpen) await this.filter.get().waitFor({ state: "hidden" });
@ -80,7 +80,7 @@ export class ToolbarPage extends BasePage {
async clickShareView() { async clickShareView() {
const menuOpen = await this.shareView.get().isVisible(); const menuOpen = await this.shareView.get().isVisible();
await this.get().locator(`button:has-text("Share View")`).click(); await this.get().locator(`button.nc-btn-share-view `).click();
// Wait for the menu to close // Wait for the menu to close
if (menuOpen) await this.shareView.get().waitFor({ state: "hidden" }); if (menuOpen) await this.shareView.get().waitFor({ state: "hidden" });

5
scripts/playwright/pages/ProjectsPage/index.ts

@ -41,4 +41,9 @@ export class ProjectsPage extends BasePage {
return project; return project;
} }
async delete({title}: {title: string}) {
await this.get().locator(`[pw-data="delete-project-${title}"]`).click();
await this.rootPage.locator(`button:has-text("Yes")`).click();
}
} }

5
scripts/playwright/playwright.config.ts

@ -1,6 +1,9 @@
import type { PlaywrightTestConfig } from '@playwright/test'; import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test'; import { devices } from '@playwright/test';
process.env.E2E_AIRTABLE_API_KEY = "keyn1MR87qgyUsYg4";
process.env.E2E_AIRTABLE_BASE_ID = "https://airtable.com/shr4z0qmh6dg5s3eB";
/** /**
* Read environment variables from file. * Read environment variables from file.
* https://github.com/motdotla/dotenv * https://github.com/motdotla/dotenv
@ -11,7 +14,7 @@ require('dotenv').config();
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
testDir: './tests', testDir: process.env.PW_QUICK_TEST ? './quickTests' : './tests',
/* Maximum time one test can run for. */ /* Maximum time one test can run for. */
timeout: process.env.CI ? 80 * 1000 : 65 * 1000, timeout: process.env.CI ? 80 * 1000 : 65 * 1000,
expect: { expect: {

249
scripts/playwright/quickTests/commonTest.ts

@ -0,0 +1,249 @@
import { DashboardPage } from "../pages/Dashboard";
import { ProjectsPage } from "../pages/ProjectsPage";
import { NcContext } from "../setup";
import { isMysql } from "../setup/db";
// normal fields
let recordCells = {
Name: "Movie-1",
Notes: "Good",
Status: "Todo",
Tags: "Jan",
Phone: "123123123",
Email: "a@b.com",
URL: "www.a.com",
Number: "1",
Value: "$1.00",
Percent: "0.01",
};
// links/ computed fields
let recordsVirtualCells = {
Duration: "00:01",
Done: true,
Date: "2022-05-31",
Rating: 1,
Actor: ["Actor1", "Actor2"],
"Status (from Actor)": ["Todo", "In progress"],
RollUp: "128",
Computation: "4.04",
Producer: ["P1", "P2"],
};
let tn = ["Film", "Actor", "Producer"];
let cn = [
"Name",
"Notes",
"Status",
"Tags",
"Done",
"Date",
"Phone",
"Email",
"URL",
"Number",
"Percent",
"Duration",
"Rating",
"Actor",
"Status (from Actor)",
"RollUp",
"Computation",
"Producer",
];
const quickVerify = async (
{
dashboard,
airtableImport,
context
}:
{
dashboard: DashboardPage,
airtableImport?: boolean,
context: NcContext
}) => {
await dashboard.treeView.openTable({ title: "Film" });
// Verify tables
for (let i = 0; i < tn.length; i++) {
await dashboard.treeView.verifyTable({ title: tn[i] });
}
let cellIndex = 0;
let columnCount = cn.length;
if (airtableImport) {
cellIndex = 2;
columnCount -= 3;
}
for (let i = 0; i < columnCount; i++) {
await dashboard.grid.column.verify({ title: cn[i] });
}
// Verify cells
// normal cells
for (let [key, value] of Object.entries(recordCells)) {
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: key, value });
}
// checkbox
await dashboard.grid.cell.checkbox.verifyChecked({ index: cellIndex, columnHeader: "Done" });
// duration
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "Duration", value: recordsVirtualCells.Duration });
// rating
await dashboard.grid.cell.rating.verify({ index: cellIndex, columnHeader: "Rating", rating: recordsVirtualCells.Rating });
// LinkToAnotherRecord
await dashboard.grid.cell.verifyVirtualCell({ index: cellIndex, columnHeader: "Actor", value: isMysql(context) ? ['Actor1'] : recordsVirtualCells.Actor });
// Status (from Actor)
// todo: Find a way to verify only the elements that are passed in
// await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "Status (from Actor)", value: recordsVirtualCells["Status (from Actor)"][0] });
if(!airtableImport){
// RollUp
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "RollUp", value: recordsVirtualCells.RollUp });
// Computation
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "Computation", value: recordsVirtualCells.Computation });
// LinkToAnotherRecord
await dashboard.grid.cell.verifyVirtualCell({ index: cellIndex, columnHeader: "Producer", value: recordsVirtualCells.Producer });
}
// Verify form
await dashboard.viewSidebar.openView({ title: "FormTitle" });
await dashboard.form.verifyHeader({ title: "FormTitle", subtitle: "FormDescription" });
await dashboard.form.verifyFormFieldLabel({ index: 0, label: "DisplayName" });
await dashboard.form.verifyFormFieldHelpText({ index: 0, helpText: "HelpText" });
await dashboard.form.verifyFieldsIsEditable({ index: 0 });
await dashboard.form.verifyAfterSubmitMsg({ msg: "Thank you for submitting the form!" });
await dashboard.form.verifyAfterSubmitMenuState({
emailMe: false,
showBlankForm: true,
submitAnotherForm: true,
});
await dashboard.treeView.openTable({ title: "Actor" });
if(!airtableImport){
// Verify webhooks
await dashboard.grid.toolbar.clickActions();
await dashboard.grid.toolbar.actions.click("Webhooks");
await dashboard.webhookForm.openForm({
index: 0,
});
await dashboard.webhookForm.verifyForm(
{
title: "Webhook-1",
hookEvent: "After Insert",
notificationType: "URL",
urlMethod: "POST",
url: "http://localhost:9090/hook",
condition: false,
}
);
await dashboard.webhookForm.goBackFromForm();
await dashboard.webhookForm.openForm({
index: 1,
});
await dashboard.webhookForm.verifyForm(
{
title: "Webhook-2",
hookEvent: "After Update",
notificationType: "URL",
urlMethod: "POST",
url: "http://localhost:9090/hook",
condition: false,
}
);
await dashboard.webhookForm.goBackFromForm();
await dashboard.webhookForm.openForm({
index: 2,
});
await dashboard.webhookForm.verifyForm(
{
title: "Webhook-3",
hookEvent: "After Delete",
notificationType: "URL",
urlMethod: "POST",
url: "http://localhost:9090/hook",
condition: false,
}
);
await dashboard.webhookForm.close();
}
// Verify pagination
await dashboard.grid.verifyActivePage({ page: '1' });
(await dashboard.grid.clickPagination({ page: ">" }));
await dashboard.grid.verifyActivePage({ page: '2' });
(await dashboard.grid.clickPagination({ page: "<" }));
await dashboard.grid.verifyActivePage({ page: '1' });
await dashboard.viewSidebar.openView({ title: "Filter&Sort" });
// Verify Fields, Filter & Sort
await dashboard.grid.column.verify({ title: "Name" });
await dashboard.grid.column.verify({ title: "Notes" });
await dashboard.grid.column.verify({ title: "Attachments", isVisible: false });
await dashboard.grid.column.verify({ title: "Status" });
await dashboard.grid.column.verify({ title: "Film" });
// Verify Fields
await dashboard.grid.toolbar.clickFields();
await dashboard.grid.toolbar.fields.verify({ title: "Name", checked: true });
await dashboard.grid.toolbar.fields.verify({ title: "Notes", checked: true });
await dashboard.grid.toolbar.fields.verify({ title: "Attachments", checked: false });
await dashboard.grid.toolbar.fields.verify({ title: "Status", checked: true });
await dashboard.grid.toolbar.fields.verify({ title: "Film", checked: true });
// Verify Sort
await dashboard.grid.toolbar.clickSort();
await dashboard.grid.toolbar.sort.verify({ index:0, column: "Name", direction: "A → Z" });
// Verify Filter
await dashboard.grid.toolbar.clickFilter();
await dashboard.grid.toolbar.filter.verify({ index: 0, column: "Name", operator: "is like", value: "1" });
await dashboard.grid.toolbar.filter.verify({ index: 1, column: "Name", operator: "is like", value: "2" });
if(!airtableImport){
// Verify views
// todo: Wait for 800ms, issue related to vue router
await dashboard.rootPage.waitForTimeout(800);
await dashboard.treeView.openTable({ title: "Producer" });
await dashboard.viewSidebar.verifyView({ index: 0, title: "Grid view" });
await dashboard.viewSidebar.verifyView({ index: 1, title: "Grid 2" });
await dashboard.viewSidebar.verifyView({ index: 2, title: "Grid 3" });
await dashboard.viewSidebar.verifyView({ index: 3, title: "Grid 4" });
await dashboard.viewSidebar.verifyView({ index: 4, title: "Form" });
await dashboard.viewSidebar.verifyView({ index: 5, title: "Form 2" });
await dashboard.viewSidebar.verifyView({ index: 6, title: "Form 3" });
await dashboard.viewSidebar.verifyView({ index: 7, title: "Form 4" });
await dashboard.viewSidebar.verifyView({ index: 8, title: "Gallery" });
await dashboard.viewSidebar.verifyView({ index: 9, title: "Gallery 2" });
await dashboard.viewSidebar.verifyView({ index: 10, title: "Gallery 3" });
// verify BT relation
await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: "FilmRead", value: ['Movie-1'] });
}
if(airtableImport){
// Delete project
await dashboard.clickHome();
const projectsPage = new ProjectsPage(dashboard.rootPage);
await projectsPage.delete({ title: context.project.title });
}
}
export { quickVerify };

120
scripts/playwright/quickTests/quickTests.spec.ts

@ -3,67 +3,13 @@ import { expect, test } from "@playwright/test";
import { DashboardPage } from "../pages/Dashboard"; import { DashboardPage } from "../pages/Dashboard";
import { LoginPage } from "../pages/LoginPage"; import { LoginPage } from "../pages/LoginPage";
import { ProjectsPage } from "../pages/ProjectsPage"; import { ProjectsPage } from "../pages/ProjectsPage";
import { quickVerify } from "../quickTests/commonTest";
// normal fields import { NcContext } from "../setup";
let recordCells = {
Name: "Movie-1",
Notes: "Good",
Status: "Todo",
Tags: "Jan",
Phone: "123123123",
Email: "a@b.com",
URL: "www.a.com",
Number: "1",
Value: "$1.00",
Percent: "0.01",
};
// links/ computed fields
let recordsVirtualCells = {
Duration: "00:01",
Done: true,
Date: "2022-05-31",
Rating: 1,
Actor: ["Actor1", "Actor2"],
"Status (from Actor)": ["Todo", "In progress"],
RollUp: "128",
Computation: "4.04",
Producer: ["P1", "P2"],
};
let tn = ["Film", "Actor", "Producer"];
let cn = [
"Name",
"Notes",
"Status",
"Tags",
"Done",
"Date",
"Phone",
"Email",
"URL",
"Number",
"Percent",
"Duration",
"Rating",
"Actor",
"Status (from Actor)",
"RollUp",
"Computation",
"Producer",
];
test.describe("Quick tests", () => { test.describe("Quick tests", () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;
// test.beforeEach(async ({ page }) => {
// });
test("Quick tests test", async ({page}) => { test("Quick tests test", async ({page}) => {
let cellIndex = 0;
let columnCount = cn.length;
const loginPage = new LoginPage(page); const loginPage = new LoginPage(page);
await loginPage.goto(); await loginPage.goto();
await loginPage.fillEmail("user@nocodb.com"); await loginPage.fillEmail("user@nocodb.com");
@ -74,63 +20,11 @@ test.describe("Quick tests", () => {
const project = await projectsPage.selectAndGetProject("sample"); const project = await projectsPage.selectAndGetProject("sample");
dashboard = new DashboardPage(page, project); dashboard = new DashboardPage(page, project);
// verify if all tables exist const context: NcContext = {
for (let i = 0; i < tn.length; i++) { project,
await dashboard.treeView.verifyTable({ title: tn[i] }); token: '',
dbType: (process.env.CI ? process.env.E2E_DB_TYPE : process.env.E2E_DEV_DB_TYPE) || 'mysql'
} }
await quickVerify({dashboard, context});
await dashboard.treeView.openTable({ title: "Film" });
// for Film table, verify columns
for (let i = 0; i < columnCount; i++) {
await dashboard.grid.column.verify({ title: cn[i] });
}
// normal cells
for (let [key, value] of Object.entries(recordCells)) {
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: key, value });
}
// checkbox
await dashboard.grid.cell.checkbox.verifyChecked({ index: cellIndex, columnHeader: "Done" });
// duration
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "Duration", value: recordsVirtualCells.Duration });
// rating
await dashboard.grid.cell.rating.verify({ index: cellIndex, columnHeader: "Rating", rating: recordsVirtualCells.Rating });
// LinkToAnotherRecord
await dashboard.grid.cell.verifyVirtualCell({ index: cellIndex, columnHeader: "Actor", value: recordsVirtualCells.Actor });
// Status (from Actor)
// todo: Find a way to verify only the elements that are passed in
// await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "Status (from Actor)", value: recordsVirtualCells["Status (from Actor)"][0] });
// RollUp
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "RollUp", value: recordsVirtualCells.RollUp });
// Computation
await dashboard.grid.cell.verify({ index: cellIndex, columnHeader: "Computation", value: recordsVirtualCells.Computation });
// LinkToAnotherRecord
await dashboard.grid.cell.verifyVirtualCell({ index: cellIndex, columnHeader: "Producer", value: recordsVirtualCells.Producer });
// Verify form
await dashboard.viewSidebar.openView({ title: "FormTitle" });
await dashboard.form.verifyHeader({ title: "FormTitle", subtitle: "FormDescription" });
await dashboard.form.verifyFormFieldLabel({ index: 0, label: "DisplayName" });
await dashboard.form.verifyFormFieldHelpText({ index: 0, helpText: "HelpText" });
await dashboard.form.verifyFieldsIsEditable({ index: 0 });
await dashboard.form.verifyAfterSubmitMsg({ msg: "Thank you for submitting the form!" });
await dashboard.form.verifyAfterSubmitMenuState({
emailMe: false,
showBlankForm: true,
submitAnotherForm: true,
});
// Verify webhooks
await dashboard.treeView.openTable({ title: "Actor" });
// await dashboard.webhookForm.open({})
}); });
}); });

3
scripts/playwright/setup/index.ts

@ -7,13 +7,14 @@ export interface NcContext {
dbType?: string; dbType?: string;
} }
const setup = async ({page}: {page: Page}): Promise<NcContext> => { const setup = async ({page, isEmptyProject}: {page: Page, isEmptyProject?: boolean}): Promise<NcContext> => {
let dbType = process.env.CI ? process.env.E2E_DB_TYPE : process.env.E2E_DEV_DB_TYPE; let dbType = process.env.CI ? process.env.E2E_DB_TYPE : process.env.E2E_DEV_DB_TYPE;
dbType = dbType || 'mysql'; dbType = dbType || 'mysql';
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,
dbType, dbType,
isEmptyProject
}); });
if(response.status !== 200) { if(response.status !== 200) {

27
scripts/playwright/tests/import.spec.ts

@ -1,5 +1,6 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { DashboardPage } from "../pages/Dashboard"; import { DashboardPage } from "../pages/Dashboard";
import { quickVerify } from "../quickTests/commonTest";
import setup from "../setup"; import setup from "../setup";
const apiKey = process.env.E2E_AIRTABLE_API_KEY; const apiKey = process.env.E2E_AIRTABLE_API_KEY;
@ -9,45 +10,33 @@ test.describe("Import", () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;
let context: any; let context: any;
test.setTimeout(150000);
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
context = await setup({ page }); page.setDefaultTimeout(50000);
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project); dashboard = new DashboardPage(page, context.project);
}); });
test("Airtable", async () => { test("Airtable", async () => {
// create empty project
await dashboard.clickHome();
await dashboard.createProject({ name: "airtable", type: "xcdb" });
await dashboard.treeView.quickImport({ title: "Airtable" }); await dashboard.treeView.quickImport({ title: "Airtable" });
await dashboard.importAirtable.import({ await dashboard.importAirtable.import({
key: apiKey, key: apiKey!,
baseId: apiBase, baseId: apiBase!,
}); });
await dashboard.rootPage.waitForTimeout(1000); await dashboard.rootPage.waitForTimeout(1000);
await quickVerify({dashboard, airtableImport: true, context});
}); });
test("Excel", async () => { test("Excel", async () => {
// create empty project
await dashboard.clickHome();
await dashboard.createProject({ name: "excel", type: "xcdb" });
await dashboard.treeView.quickImport({ title: "Microsoft Excel" }); await dashboard.treeView.quickImport({ title: "Microsoft Excel" });
}); });
test("CSV", async () => { test("CSV", async () => {
// create empty project
await dashboard.clickHome();
await dashboard.createProject({ name: "CSV", type: "xcdb" });
await dashboard.treeView.quickImport({ title: "CSV file" }); await dashboard.treeView.quickImport({ title: "CSV file" });
}); });
test("JSON", async () => { test("JSON", async () => {
// create empty project
await dashboard.clickHome();
await dashboard.createProject({ name: "JSON", type: "xcdb" });
await dashboard.treeView.quickImport({ title: "JSON file" }); await dashboard.treeView.quickImport({ title: "JSON file" });
}); });
}); });

2
scripts/playwright/tests/rolesCreate.spec.ts

@ -39,7 +39,7 @@ test.describe("User roles", () => {
let settings: SettingsPage; let settings: SettingsPage;
let context: any; let context: any;
test.beforeAll(async ({ page }) => { test.beforeEach(async ({ page }) => {
context = await setup({ page }); context = await setup({ page });
dashboard = new DashboardPage(page, context.project); dashboard = new DashboardPage(page, context.project);
toolbar = dashboard.grid.toolbar; toolbar = dashboard.grid.toolbar;

2
scripts/playwright/tests/views.spec.ts

@ -70,7 +70,7 @@ test.describe("Views CRUD Operations", () => {
await dashboard.viewSidebar.deleteView({ title: "CityGallery2" }); await dashboard.viewSidebar.deleteView({ title: "CityGallery2" });
await dashboard.viewSidebar.verifyViewNotPresent({ await dashboard.viewSidebar.verifyViewNotPresent({
title: "CityGallery2", title: "CityGallery2",
index: 2, index: 1,
}); });
}); });
}); });

2
scripts/playwright/tests/webhook.spec.ts

@ -32,7 +32,7 @@ test.describe("Webhook", () => {
let dashboard: DashboardPage, toolbar: ToolbarPage, webhook: WebhookFormPage; let dashboard: DashboardPage, toolbar: ToolbarPage, webhook: WebhookFormPage;
let context: any; let context: any;
test.beforeAll(async () => { test.beforeEach(async () => {
// start a server locally for webhook tests // start a server locally for webhook tests
await makeServer(); await makeServer();
}); });

Loading…
Cancel
Save