多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

567 lines
21 KiB

import { expect, Locator } from '@playwright/test';
import { GridPage } from '..';
import BasePage from '../../../Base';
import { SelectOptionColumnPageObject } from './SelectOptionColumn';
import { AttachmentColumnPageObject } from './Attachment';
import { getTextExcludeIconText } from '../../../../tests/utils/general';
import { UserOptionColumnPageObject } from './UserOptionColumn';
import { LTAROptionColumnPageObject } from './LTAROptionColumn';
export class ColumnPageObject extends BasePage {
readonly grid: GridPage;
readonly selectOption: SelectOptionColumnPageObject;
readonly attachmentColumnPageObject: AttachmentColumnPageObject;
readonly userOption: UserOptionColumnPageObject;
readonly ltarOption: LTAROptionColumnPageObject;
constructor(grid: GridPage) {
super(grid.rootPage);
this.grid = grid;
this.selectOption = new SelectOptionColumnPageObject(this);
this.attachmentColumnPageObject = new AttachmentColumnPageObject(this);
this.userOption = new UserOptionColumnPageObject(this);
this.ltarOption = new LTAROptionColumnPageObject(this);
}
get() {
return this.rootPage.locator('form[data-testid="add-or-edit-column"]');
}
async getColumnHeaderByIndex({ index }: { index: number }) {
return this.grid.get().locator(`.nc-grid-header > th`).nth(index);
}
async clickColumnHeader({ title }: { title: string }) {
await this.getColumnHeader(title).click();
}
defaultValueBtn() {
const showDefautlValueBtn = this.get().getByTestId('nc-show-default-value-btn');
return {
locator: showDefautlValueBtn,
isVisible: async () => {
return await showDefautlValueBtn.isVisible();
},
click: async () => {
if (await showDefautlValueBtn.isVisible()) {
await showDefautlValueBtn.waitFor();
await showDefautlValueBtn.click({ force: true });
await showDefautlValueBtn.waitFor({ state: 'hidden' });
await this.get().locator('.nc-default-value-wrapper').waitFor({ state: 'visible' });
}
},
};
}
async create({
title,
type = 'SingleLineText',
formula = '',
qrCodeValueColumnTitle = '',
barcodeValueColumnTitle = '',
childTable = '',
childColumn = '',
relationType = '',
rollupType = '',
format = '',
dateFormat = '',
timeFormat = '',
insertAfterColumnTitle,
insertBeforeColumnTitle,
isDisplayValue = false,
ltarFilters,
ltarView,
}: {
title: string;
type?: string;
formula?: string;
qrCodeValueColumnTitle?: string;
barcodeValueColumnTitle?: string;
barcodeFormat?: string;
childTable?: string;
childColumn?: string;
relationType?: string;
rollupType?: string;
format?: string;
dateFormat?: string;
timeFormat?: string;
insertBeforeColumnTitle?: string;
insertAfterColumnTitle?: string;
isDisplayValue?: boolean;
ltarFilters?: any[];
ltarView?: string;
}) {
if (insertBeforeColumnTitle) {
await this.grid.get().locator(`th[data-title="${insertBeforeColumnTitle}"]`).scrollIntoViewIfNeeded();
await this.grid.get().locator(`th[data-title="${insertBeforeColumnTitle}"] .nc-ui-dt-dropdown`).click();
if (isDisplayValue) {
await expect(this.rootPage.locator('li[role="menuitem"]:has-text("Insert Before")')).toHaveCount(0);
return;
}
await this.rootPage.locator('li[role="menuitem"]:has-text("Insert Before"):visible').click();
} else if (insertAfterColumnTitle) {
await this.grid.get().locator(`th[data-title="${insertAfterColumnTitle}"]`).scrollIntoViewIfNeeded();
await this.grid.get().locator(`th[data-title="${insertAfterColumnTitle}"] .nc-ui-dt-dropdown`).click();
await this.rootPage.locator('li[role="menuitem"]:has-text("Insert After"):visible').click();
} else {
await this.grid.get().locator('.nc-column-add').click();
}
await this.rootPage.waitForTimeout(500);
await this.fillTitle({ title });
await this.rootPage.waitForTimeout(500);
await this.selectType({ type, isCreateColumn: true });
await this.rootPage.waitForTimeout(500);
switch (type) {
case 'SingleSelect':
case 'MultiSelect':
break;
case 'Duration':
if (format) {
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage.locator(`.ant-select-item .ant-select-item-option-content`).getByTestId(format).click();
}
break;
case 'Date':
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.nc-date-select').pressSequentially(dateFormat);
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
break;
case 'DateTime':
// Date Format
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
// Time Format
await this.get().locator('.nc-time-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click();
break;
case 'Formula':
await this.get().locator('.nc-formula-input').fill(formula);
break;
case 'QrCode':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`)
.locator(`[data-testid="nc-qr-${qrCodeValueColumnTitle}"]`)
.click();
break;
case 'Barcode':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: new RegExp(`^${barcodeValueColumnTitle}$`),
})
.click();
break;
case 'Lookup':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: childTable,
})
.click();
await this.get().locator('.ant-select-single').nth(2).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: childColumn,
})
.last()
.click();
break;
case 'Rollup':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: childTable,
})
.click();
await this.get().locator('.ant-select-single').nth(2).click();
await this.rootPage
.locator(`.nc-dropdown-relation-column >> .ant-select-item`, {
hasText: childColumn,
})
.click();
await this.get().locator('.ant-select-single').nth(3).click();
await this.rootPage
.locator(`.nc-dropdown-rollup-function >> .ant-select-item`, {
hasText: rollupType,
})
.nth(0)
.click();
break;
case 'Links':
// kludge, fix me
await this.rootPage.waitForTimeout(2000);
await this.get().locator('.nc-ltar-relation-type').getByTestId(relationType).click();
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).fill(childTable);
await this.rootPage
.locator(`.nc-dropdown-ltar-child-table >> .ant-select-item`, {
hasText: childTable,
})
.nth(0)
.click();
if (ltarView) {
await this.ltarOption.selectView({ ltarView: ltarView });
}
if (ltarFilters) {
await this.ltarOption.addFilters(ltarFilters);
}
break;
case 'User':
break;
default:
break;
}
await this.save();
const headersText = [];
const locator = this.grid.get().locator('th.nc-grid-column-header');
await locator.first().waitFor({ state: 'visible' });
const count = await locator.count();
for (let i = 0; i < count; i++) {
const header = locator.nth(i);
const text = await getTextExcludeIconText(header);
headersText.push(text);
}
// verify column inserted after the target column
if (insertAfterColumnTitle) {
expect(headersText[headersText.findIndex(title => title.startsWith(insertAfterColumnTitle)) + 1]).toBe(title);
}
// verify column inserted before the target column
if (insertBeforeColumnTitle) {
expect(headersText[headersText.findIndex(title => title.startsWith(insertBeforeColumnTitle)) - 1]).toBe(title);
}
}
async fillTitle({ title }: { title: string }) {
await this.get().locator('.nc-column-name-input').fill(title);
}
async selectType({ type, first, isCreateColumn }: { type: string; first?: boolean; isCreateColumn?: boolean }) {
if (isCreateColumn || (await this.get().getByTestId('nc-column-uitypes-options-list-wrapper').isVisible())) {
const searchInput = this.get().locator('.nc-column-type-search-input >> input');
await searchInput.waitFor({ state: 'visible' });
await searchInput.click();
await searchInput.fill(type);
await this.get().locator('.nc-column-list-wrapper').getByTestId(type).waitFor();
await this.get().locator('.nc-column-list-wrapper').getByTestId(type).click();
await this.get().locator('.nc-column-type-input').waitFor();
} else {
if (first) {
await this.get().locator('.ant-select-selector > .ant-select-selection-item').first().click();
} else {
await this.get().locator('.ant-select-selector > .ant-select-selection-item').click();
}
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').waitFor();
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').fill(type);
// Select column type
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').getByTestId(type).click();
}
}
async changeReferencedColumnForQrCode({ titleOfReferencedColumn }: { titleOfReferencedColumn: string }) {
await this.get().locator('.nc-qr-code-value-column-select .ant-select-single').click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: titleOfReferencedColumn,
})
.click();
await this.save({ isUpdated: true });
}
async changeReferencedColumnForBarcode({ titleOfReferencedColumn }: { titleOfReferencedColumn: string }) {
await this.get().locator('.nc-barcode-value-column-select .ant-select-single').click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: titleOfReferencedColumn,
})
.click();
await this.save({ isUpdated: true });
}
async changeBarcodeFormat({ barcodeFormatName }: { barcodeFormatName: string }) {
await this.get().locator('.nc-barcode-format-select .ant-select-single').click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: barcodeFormatName,
})
.click();
await this.save({ isUpdated: true });
}
async delete({ title }: { title: string }) {
await this.getColumnHeader(title).locator('div.ant-dropdown-trigger').locator('.nc-ui-dt-dropdown').click();
// await this.rootPage.locator('li[role="menuitem"]:has-text("Delete")').waitFor();
await this.rootPage.locator('li[role="menuitem"]:has-text("Delete"):visible').click();
// pressing on delete column button
await this.rootPage.locator('.ant-modal.active button:has-text("Delete Field")').click();
// wait till modal is closed
await this.rootPage.locator('.ant-modal.active').waitFor({ state: 'hidden' });
}
// or in the dropdown edit click
async openEdit({
title,
type = 'SingleLineText',
formula = '',
format,
dateFormat = '',
timeFormat = '',
selectType = false,
}: {
title: string;
type?: string;
formula?: string;
format?: string;
dateFormat?: string;
timeFormat?: string;
selectType?: boolean;
}) {
// when clicked on the dropdown cell header
await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').scrollIntoViewIfNeeded();
await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').click();
await expect(await this.rootPage.locator('li[role="menuitem"]:has-text("Edit"):visible').last()).toBeVisible();
await this.rootPage.locator('li[role="menuitem"]:has-text("Edit"):visible').last().click();
await this.get().waitFor({ state: 'visible' });
if (selectType) {
await this.selectType({ type, first: true });
}
// Click set default value to show default value input, on close field modal it will automacally hide input if value is not set
await this.defaultValueBtn().click();
switch (type) {
case 'Formula':
await this.get().locator('.nc-formula-input').fill(formula);
break;
case 'Duration':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage.locator(`.ant-select-item`).getByTestId(format).click();
break;
case 'DateTime':
// Date Format
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
// allow UI to update
await this.rootPage.waitForTimeout(500);
// Time Format
await this.get().locator('.nc-time-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click();
// allow UI to update
await this.rootPage.waitForTimeout(500);
break;
case 'Date':
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.nc-date-select').pressSequentially(dateFormat, { delay: 100 });
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
// allow UI to update
await this.rootPage.waitForTimeout(500);
break;
default:
break;
}
}
// opening edit modal in table header double click
async editMenuShowMore() {
await this.rootPage.locator('.nc-more-options').click();
}
async duplicateColumn({ title, expectedTitle = `${title} copy` }: { title: string; expectedTitle?: string }) {
await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click();
await this.rootPage.locator('li[role="menuitem"]:has-text("Duplicate"):visible').click();
await this.rootPage.locator('.nc-modal-column-duplicate .nc-button:has-text("Confirm"):visible').click();
// await this.verifyToast({ message: 'Column duplicated successfully' });
await this.grid.get().locator(`th[data-title="${expectedTitle}"]`).waitFor({ state: 'visible', timeout: 10000 });
}
async hideColumn({ title, isDisplayValue = false }: { title: string; isDisplayValue?: boolean }) {
await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click();
if (isDisplayValue) {
await expect(this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field")')).toHaveCount(0);
return;
}
await this.waitForResponse({
uiAction: async () => await this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field"):visible').click(),
requestUrlPathToMatch: '/api/v1/db/meta/views',
httpMethodsToMatch: ['PATCH'],
});
await expect(this.grid.get().locator(`th[data-title="${title}"]`)).toHaveCount(0);
}
async save({ isUpdated, typeChange }: { isUpdated?: boolean; typeChange?: boolean } = {}) {
// if type is changed, then we need to click the update button during the warning popup
if (!typeChange) {
const buttonText = isUpdated ? 'Update' : 'Save';
await this.waitForResponse({
uiAction: async () => await this.get().locator(`button:has-text("${buttonText}")`).click(),
requestUrlPathToMatch: 'api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
responseJsonMatcher: json => json['pageInfo'],
});
} else {
await this.get().locator('button:has-text("Update Field")').click();
// click on update button on warning popup
await this.waitForResponse({
uiAction: async () => await this.rootPage.locator('button:has-text("Update")').click(),
requestUrlPathToMatch: 'api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
responseJsonMatcher: json => json['pageInfo'],
});
}
await this.verifyToast({
message: isUpdated ? 'Column updated' : 'Column created',
});
await this.get().waitFor({ state: 'hidden' });
await this.rootPage.waitForTimeout(200);
}
async checkMessageAndClose({ errorMessage }: { errorMessage?: RegExp } = {}) {
await this.verifyErrorMessage({
message: errorMessage,
});
await this.get().locator('button:has-text("Cancel")').click();
await this.get().waitFor({ state: 'hidden' });
await this.rootPage.waitForTimeout(200);
}
async verify({ title, isVisible = true, scroll = false }: { title: string; isVisible?: boolean; scroll?: boolean }) {
if (!isVisible) {
return await expect(this.getColumnHeader(title)).not.toBeVisible();
}
if (scroll) {
await this.getColumnHeader(title).scrollIntoViewIfNeeded();
}
await expect(this.getColumnHeader(title)).toContainText(title);
}
async verifyRoleAccess(param: { role: string }) {
const role = param.role.toLowerCase();
const count = role.toLowerCase() === 'creator' || role.toLowerCase() === 'owner' ? 1 : 0;
await expect(this.grid.get().locator('.nc-column-add:visible')).toHaveCount(count);
// verify for first column, if edit dropdown exists
const columnHdr = await this.getColumnHeaderByIndex({ index: 1 });
await expect(await columnHdr.locator('.nc-ui-dt-dropdown:visible')).toHaveCount(count);
if (role === 'creator' || role === 'owner') {
// open edit dropdown menu
await columnHdr.locator('.nc-ui-dt-dropdown:visible').click();
await expect(this.rootPage.locator('.nc-dropdown-column-operations')).toHaveCount(1);
// close edit dropdown menu
await columnHdr.locator('.nc-ui-dt-dropdown:visible').click();
}
// select all menu access
await expect(
await this.grid.get().locator('[data-testid="nc-check-all"]').locator('input[type="checkbox"]')
).toHaveCount(role === 'creator' || role === 'owner' || role === 'editor' ? 1 : 0);
if (role === 'creator' || role === 'owner' || role === 'editor') {
await this.grid.selectAll();
await this.grid.openAllRowContextMenu();
await this.rootPage.locator('.nc-dropdown-grid-context-menu').waitFor({ state: 'visible' });
await expect(this.rootPage.locator('.nc-dropdown-grid-context-menu')).toHaveCount(1);
await this.rootPage.keyboard.press('Escape');
await (await this.getColumnHeaderByIndex({ index: 2 })).click();
}
}
async sortColumn({ title, direction = 'asc' }: { title: string; direction: 'asc' | 'desc' }) {
await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click();
let menuOption: { (): Promise<void>; (): Promise<void> };
if (direction === 'desc') {
menuOption = () => this.rootPage.locator('li[role="menuitem"]:has-text("Sort Descending"):visible').click();
} else {
menuOption = () => this.rootPage.locator('li[role="menuitem"]:has-text("Sort Ascending"):visible').click();
}
await this.waitForResponse({
uiAction: menuOption,
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: `/sorts`,
});
await this.grid.toolbar.parent.dashboard.waitForLoaderToDisappear();
await this.grid.toolbar.clickSort();
await this.rootPage.locator(`.ant-select-selection-item:has-text("${title}")`).first().isVisible();
await this.rootPage
.locator(
`.nc-sort-dir-select:has-text("${direction === 'asc' ? '1 → 9' : '9 → 1'}"),.nc-sort-dir-select:has-text("${
direction === 'asc' ? 'A → Z' : 'Z → A'
}")`
)
.first()
.isVisible();
// close sort menu
await this.grid.toolbar.clickSort();
await this.rootPage.waitForTimeout(100);
}
async resize(param: { src: string; dst: string }) {
const { src, dst } = param;
const [fromStack, toStack] = await Promise.all([
this.rootPage.locator(`[data-title="${src}"] >> .resizer`),
this.rootPage.locator(`[data-title="${dst}"] >> .resizer`),
]);
await fromStack.scrollIntoViewIfNeeded();
await fromStack.hover();
await fromStack.dragTo(toStack);
await this.rootPage.waitForTimeout(500);
await fromStack.click({
force: true,
});
}
async getWidth(param: { title: string }) {
const { title } = param;
const cell = this.rootPage.locator(`th[data-title="${title}"]`);
return await cell.evaluate(el => el.getBoundingClientRect().width);
}
private getColumnHeader(title: string) {
return this.grid.get().locator(`th[data-title="${title}"]`).first();
}
}