Files
nocodb/tests/playwright/pages/Dashboard/TreeView.ts
2025-10-11 06:35:44 +00:00

536 lines
19 KiB
TypeScript

import { expect, Locator } from '@playwright/test';
import { DashboardPage } from '.';
import BasePage from '../Base';
import { NcContext } from '../../setup';
import { isEE } from '../../setup/db';
export class TreeViewPage extends BasePage {
readonly dashboard: DashboardPage;
readonly base: any;
readonly quickImportButton: Locator;
readonly createNewButton: Locator;
readonly miniSidebar: Locator;
constructor(dashboard: DashboardPage, base: any) {
super(dashboard.rootPage);
this.dashboard = dashboard;
this.base = base;
this.quickImportButton = dashboard.get().locator('.nc-import-menu');
this.createNewButton = this.get().locator('.nc-home-create-new-btn');
this.miniSidebar = this.dashboard.get().getByTestId('nc-mini-sidebar');
}
get() {
return this.dashboard.get().locator('.nc-treeview-container');
}
getAddNewTableBtn({ baseTitle }: { baseTitle: string }) {
return this.dashboard
.get()
.getByTestId(`nc-sidebar-base-title-${baseTitle}`)
.locator('[data-testid="nc-sidebar-add-base-entity"]');
}
private async openProjectContextMenu({ baseTitle }: { baseTitle: string }) {
await this.dashboard.leftSidebar.verifyBaseListOpen(true);
await this.dashboard.get().getByTestId(`nc-sidebar-base-title-${baseTitle}`).hover();
const baseTitleElement = this.dashboard.get().getByTestId(`nc-sidebar-base-title-${baseTitle}`);
if (
(await baseTitleElement.isVisible()) &&
!(await baseTitleElement.locator('[data-testid="nc-sidebar-context-menu"]').isVisible())
) {
return await baseTitleElement.click();
}
await baseTitleElement.locator('[data-testid="nc-sidebar-context-menu"]').click();
}
async isVisible() {
return await this.get().isVisible();
}
async verifyVisibility({ isVisible }: { isVisible: boolean }) {
await this.rootPage.waitForTimeout(1000);
const domElement = await this.get();
// get width of treeview dom element
const width = (await domElement.boundingBox()).width;
// if (isVisible) {
// await expect(this.get()).toBeVisible();
// } else {
// await expect(this.get()).not.toBeVisible();
// }
// border for treeview is 1px
// if not-visible, width should be < 5;
if (!isVisible) {
expect(width).toBeLessThan(5);
} else {
expect(width).toBeGreaterThan(5);
}
}
async focusTable({ title }: { title: string }) {
await this.get()
.locator(`.nc-base-tree-tbl-${title.replace(/ /g, '')}`)
.focus();
}
async openBase({ title }: { title: string }) {
await this.dashboard.leftSidebar.verifyBaseListOpen(true);
const nodes = this.get().locator(`[data-testid="nc-sidebar-base-${title.toLowerCase()}"]`);
await nodes.waitFor();
await nodes.click();
return;
}
async getTable({ index, tableTitle }: { index: number; tableTitle?: string }) {
if (tableTitle) {
return this.get().getByTestId(`nc-tbl-side-node-${tableTitle}`);
}
return this.get().locator('.nc-tree-item').nth(index);
}
async waitForTableOptions({ title }: { title: string }) {
const tableTitle = title.replace(/ /g, '');
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).waitFor({ state: 'visible' });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).scrollIntoViewIfNeeded();
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).getByTestId(`nc-tbl-title-${title}`).hover();
}
// assumption: first view rendered is always GRID
//
async openTable({
title,
mode = 'standard',
networkResponse = false,
mobileMode = false,
baseTitle,
sourceTitle,
}: {
title: string;
mode?: string;
networkResponse?: boolean;
mobileMode?: boolean;
baseTitle?: string;
sourceTitle?: string;
}) {
await this.dashboard.leftSidebar.verifyBaseListOpen(!!baseTitle);
if (baseTitle) {
await this.dashboard.sidebar.baseNode.verifyActiveProject({ baseTitle, open: true });
}
if (sourceTitle) {
await this.dashboard.treeView.openSource({ title: sourceTitle });
}
if (mobileMode) {
await this.rootPage.locator('.h-full > div > .nc-sidebar-left-toggle-icon').click();
}
await this.get().getByTestId(`nc-tbl-title-${title}`).waitFor({ state: 'visible' });
if (networkResponse === true) {
await this.waitForResponse({
uiAction: () => this.get().getByTestId(`nc-tbl-title-${title}`).closest('.nc-base-tree-tbl').click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: `/api/v1/db/data/noco`,
responseJsonMatcher: json => json.pageInfo,
});
} else {
await this.get().locator(`[data-testid="nc-tbl-title-${title}"]`).click({
// x:10, y:10
});
// todo: remove this after fixing the issue
await this.rootPage.waitForTimeout(1000);
await this.get().locator(`[data-testid="nc-tbl-title-${title}"]`).click({
// x:10, y:10
});
await this.rootPage.waitForLoadState('networkidle');
}
}
async openScript({ title, baseTitle }: { title: string; baseTitle?: string }) {
if (baseTitle) {
await this.dashboard.sidebar.baseNode.verifyActiveProject({ baseTitle, open: true });
}
const scriptNode = this.get().getByTestId(`view-sidebar-script-${title}`);
await scriptNode.waitFor({ state: 'visible' });
await scriptNode.click({});
// todo: remove this after fixing the issue
await this.rootPage.waitForTimeout(1000);
await scriptNode.click({
// x:10, y:10
});
}
async createEntity({
type,
skipOpeningModal,
baseTitle,
}: {
type: 'table' | 'script';
skipOpeningModal?: boolean;
mode?: string;
baseTitle: string;
}) {
if (skipOpeningModal) return;
await this.dashboard.leftSidebar.miniSidebarActionClick({ type: 'base' });
await this.rootPage.waitForTimeout(500);
await this.dashboard.leftSidebar.verifyBaseListOpen(true);
const verifyBaseListOpen = true;
switch (type) {
case 'table': {
if (verifyBaseListOpen) {
await this.get().getByTestId(`nc-sidebar-base-title-${baseTitle}`).hover();
await this.get()
.getByTestId(`nc-sidebar-base-${baseTitle}`)
.getByTestId('nc-sidebar-add-base-entity')
.click();
} else {
const isCreateNewDropdown = (await this.createNewButton.getAttribute('class')).includes(
'nc-home-create-new-dropdown-btn'
);
if (!isCreateNewDropdown) {
return await this.createNewButton.click();
} else {
await this.createNewButton.click();
await this.dashboard.get().locator('.nc-dropdown.active').waitFor();
await this.dashboard.get().locator('.nc-dropdown.active').getByTestId(`create-new-${type}`).click();
}
}
break;
}
case 'script': {
// Todo:
}
}
}
async createTable({
title,
skipOpeningModal,
baseTitle,
}: {
title: string;
skipOpeningModal?: boolean;
mode?: string;
baseTitle: string;
}) {
await this.createEntity({ type: 'table', skipOpeningModal, baseTitle });
await this.dashboard.get().locator('.ant-modal.active').locator('.ant-modal-body').waitFor();
await this.dashboard.get().getByPlaceholder('Enter table name').fill(title);
await this.waitForResponse({
uiAction: () =>
this.dashboard.get().locator('.ant-modal.active').locator('button:has-text("Create Table")').click(),
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: `/api/v1/db/meta/projects/`,
responseJsonMatcher: json => json.title === title && json.type === 'table',
});
// After table create we navigate to that table and sidebar will be base homepage instead of baselist, so we have to wait for that
await this.dashboard.leftSidebar.active_base.waitFor({ state: 'visible' });
}
async verifyTable({
title,
index,
exists = true,
baseTitle,
sourceTitle,
}: {
title: string;
index?: number;
exists?: boolean;
baseTitle?: string;
sourceTitle?: string;
}) {
await this.dashboard.leftSidebar.verifyBaseListOpen(!!baseTitle);
if (baseTitle) {
await this.dashboard.sidebar.baseNode.verifyActiveProject({ baseTitle, open: true });
}
if (sourceTitle) {
await this.dashboard.treeView.openSource({ title: sourceTitle });
}
if (exists) {
await expect(this.get().getByTestId(`nc-tbl-title-${title}`)).toHaveCount(1);
if (index) {
await expect(this.get().locator('.nc-tbl-title').nth(index)).toHaveText(title);
}
} else {
await expect(this.get().getByTestId(`nc-tbl-title-${title}`)).toHaveCount(0);
}
}
async deleteTable({ title, baseTitle }: { title: string; baseTitle?: string }) {
if (baseTitle) {
await this.dashboard.sidebar.baseNode.verifyActiveProject({ baseTitle, open: true });
}
const tableTitle = title.replace(/ /g, '');
await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item.nc-table-delete:has-text("Delete")').click();
await this.waitForResponse({
uiAction: async () => {
// Create a promise that resolves after 1 second
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// Returning a promise that resolves with the result after the 1-second delay
await delay(100);
return await this.dashboard.get().locator('button:has-text("Delete Table")').click();
},
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: `/api/v1/db/meta/tables/`,
});
await (await this.rootPage.locator('.nc-container').last().elementHandle())?.waitForElementState('stable');
}
async renameTable({ title, newTitle, baseTitle }: { title: string; newTitle: string; baseTitle?: string }) {
if (baseTitle) {
await this.dashboard.sidebar.baseNode.verifyActiveProject({ baseTitle, open: true });
}
const tableTitle = title.replace(/ /g, '');
await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-table-rename.nc-menu-item:has-text("Rename")').click();
const tableNodeInput = this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('input');
await tableNodeInput.clear();
await tableNodeInput.fill(newTitle);
await tableNodeInput.press('Enter');
}
async reorderTables({ sourceTable, destinationTable }: { sourceTable: string; destinationTable: string }) {
await this.dashboard
.get()
.locator(`[data-testid="tree-view-table-draggable-handle-${sourceTable}"]`)
.dragTo(this.get().locator(`[data-testid="nc-tbl-title-${destinationTable}"]`));
}
async baseSettings({ title }: { title?: string }) {
await this.openProjectContextMenu({ baseTitle: title });
const settingsMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md');
await settingsMenu.locator(`.nc-sidebar-base-base-settings`).click();
}
async quickImport({ title, baseTitle, context }: { title: string; baseTitle: string; context: NcContext }) {
baseTitle = this.scopedProjectTitle({ title: baseTitle, context });
await this.openProjectContextMenu({ baseTitle });
const importMenu = this.dashboard.get().locator('.ant-dropdown-menu');
await importMenu.locator(`.nc-sub-menu:has-text("Import Data")`).click();
await this.rootPage.locator(`.ant-dropdown-menu-item:has-text("${title}")`).waitFor();
await this.rootPage.locator(`.ant-dropdown-menu-item:has-text("${title}")`).click();
}
async changeTableIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) {
const tableTitle = title.replace(/ /g, '');
await this.get().locator(`.nc-base-tree-tbl-${tableTitle} .nc-table-icon`).click();
await this.rootPage.locator('.emoji-mart-search > input').fill(icon);
const emojiList = this.rootPage.locator('[id="emoji-mart-list"]');
await emojiList.locator('button').first().click();
await expect(
this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator(`.nc-table-icon:has-text("${iconDisplay}")`)
).toHaveCount(1);
}
async duplicateTable(title: string, includeData = true, includeViews = true) {
const tableTitle = title.replace(/ /g, '');
await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-icon.ant-dropdown-trigger').click();
await this.dashboard.get().locator('div.nc-base-menu-item:has-text("Duplicate")').click();
// Find the checkbox element with the label "Include data"
const includeDataCheckbox = this.dashboard.get().getByText('Include data', { exact: true });
// Check the checkbox if it is not already checked
if ((await includeDataCheckbox.isChecked()) && !includeData) {
await includeDataCheckbox.click(); // click the checkbox to check it
}
// Find the checkbox element with the label "Include data"
const includeViewsCheckbox = this.dashboard.get().getByText('Include views', { exact: true });
// Check the checkbox if it is not already checked
if ((await includeViewsCheckbox.isChecked()) && !includeViews) {
await includeViewsCheckbox.click(); // click the checkbox to check it
}
await this.waitForResponse({
uiAction: async () => await this.rootPage.getByRole('button', { name: 'Confirm' }).click(),
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: `/api/v1/db/meta/duplicate/`,
});
await this.get().locator(`[data-testid="nc-tbl-title-${title} copy"]`).waitFor();
}
async verifyTabIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) {
await new Promise(resolve => setTimeout(resolve, 1000));
const tableTitle = title.replace(/ /g, '');
await this.rootPage.locator(`.nc-base-tree-tbl-${tableTitle}`).waitFor({ state: 'visible' });
await expect(
this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator(`.nc-table-icon:has-text("${iconDisplay}")`)
).toHaveCount(1);
}
async validateRoleAccess(param: {
role: string;
baseTitle?: string;
tableTitle?: string;
mode?: string;
context: NcContext;
}) {
const context = param.context;
param.baseTitle = param.baseTitle ?? context.base.title;
const count = param.role.toLowerCase() === 'creator' || param.role.toLowerCase() === 'owner' ? 1 : 0;
if (param.mode !== 'shareBase') {
await this.dashboard.leftSidebar.verifyBaseListOpen(true);
const pjtNode = await this.getProject({ title: param.baseTitle });
await pjtNode.hover();
// add new table button & context menu is visible only for owner & creator
await expect(pjtNode.locator('[data-testid="nc-sidebar-add-base-entity"]')).toHaveCount(count);
await expect(pjtNode.locator('[data-testid="nc-sidebar-context-menu"]')).toHaveCount(1);
await this.openProject({ title: param.baseTitle, context });
// table context menu
await this.dashboard.sidebar.tableNode.verifyTableOptions({
tableTitle: param.tableTitle,
isVisible: !!count,
checkMenuOptions: false,
});
}
}
async openProject({ title, context }: { title: string; context: NcContext }) {
title = this.scopedProjectTitle({ title, context });
await this.dashboard.leftSidebar.verifyBaseListOpen(true);
await this.get().getByTestId(`nc-sidebar-base-title-${title}`).click();
await this.dashboard.leftSidebar.active_base.waitFor({ state: 'visible' });
await this.rootPage.waitForTimeout(1000);
// // TODO: FIx why base click is not always registering
// await this.get().getByTestId(`nc-sidebar-base-title-${title}`).click();
// await this.rootPage.waitForTimeout(1000);
}
scopedProjectTitle({ title, context }: { title: string; context: NcContext }) {
if (isEE()) return title;
if (title.toLowerCase().startsWith('xcdb')) return `${title}`;
return title === context.base.title ? context.base.title : `nc-${context.workerId}-${title}`;
}
private async getProject(param: { title?: string }) {
return this.get().getByTestId(`nc-sidebar-base-title-${param.title}`);
}
async renameProject(param: { newTitle: string; title: string; context: NcContext }) {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
param.newTitle = this.scopedProjectTitle({ title: param.newTitle, context: param.context });
await this.dashboard.leftSidebar.verifyBaseListOpen(true);
await this.openProjectContextMenu({ baseTitle: param.title });
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last();
await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Rename")`).click();
const baseNodeInput = (await this.getProject({ title: param.title })).locator('input');
await baseNodeInput.clear();
await baseNodeInput.fill(param.newTitle);
await baseNodeInput.press('Enter');
}
async deleteProject(param: { title: string; context: NcContext }) {
await this.dashboard.leftSidebar.verifyBaseListOpen(true);
param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
await this.openProjectContextMenu({ baseTitle: param.title });
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible').last();
await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Delete")`).click();
await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Delete")`).click();
}
async duplicateProject(param: { title: string; context: NcContext }) {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
await this.openProjectContextMenu({ baseTitle: param.title });
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible');
await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Duplicate")`).click();
await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Duplicate Base")`).click();
await this.rootPage.waitForTimeout(10000);
await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Go to Base")`).click();
// Wait for sidebar transition to complete
await this.rootPage.waitForTimeout(2000);
}
async openProjectSourceSettings(param: { title: string; context: NcContext }) {
param.title = this.scopedProjectTitle({ title: param.title, context: param.context });
await this.openProjectContextMenu({ baseTitle: param.title });
const contextMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md:visible');
await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Settings")`).click();
}
async openSource(param: { title: string }) {
await this.get().getByTestId(`nc-sidebar-base-${param.title}`).click();
}
}