mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-26 06:17:21 +02:00
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import { expect } from '@playwright/test';
|
|
import type { Locator } from '@playwright/test';
|
|
|
|
import { BasePage } from './BasePage';
|
|
|
|
export class DataTableDetails extends BasePage {
|
|
async goto(datatableId: string) {
|
|
await this.page.goto(`/datatables/${datatableId}`);
|
|
}
|
|
|
|
getPageWrapper() {
|
|
return this.page.getByTestId('data-table-details-view');
|
|
}
|
|
|
|
getDataTableProjectBreadcrumb() {
|
|
return this.page.getByTestId('home-project');
|
|
}
|
|
|
|
getDataTableBreadcrumb() {
|
|
return this.page.getByTestId('data-table-header-name-input');
|
|
}
|
|
|
|
async renameDataTable(newName: string) {
|
|
const nameInput = this.getDataTableBreadcrumb();
|
|
await nameInput.click();
|
|
|
|
const input = nameInput.locator('input');
|
|
await input.fill('');
|
|
|
|
await input.fill(newName);
|
|
|
|
await input.press('Enter');
|
|
}
|
|
|
|
getVisibleColumns() {
|
|
return this.page.getByTestId('data-table-column-header');
|
|
}
|
|
|
|
getNoRowsMessage() {
|
|
return this.page.getByTestId('data-table-no-rows-overlay');
|
|
}
|
|
|
|
getAddColumnHeaderButton() {
|
|
return this.page.getByTestId('data-table-add-column-trigger-button').first();
|
|
}
|
|
|
|
getAddColumnTableButton() {
|
|
return this.page.getByTestId('data-table-add-column-trigger-button').last();
|
|
}
|
|
|
|
getAddColumnPopoverContent() {
|
|
return this.page.getByTestId('add-column-popover-content');
|
|
}
|
|
|
|
getAddColumnSubmitButton() {
|
|
return this.page.getByTestId('data-table-add-column-submit-button');
|
|
}
|
|
|
|
getColumnHeaderByName(name: string) {
|
|
return this.page
|
|
.getByTestId('data-table-column-header')
|
|
.filter({ has: this.page.getByTestId('data-table-column-header-text').getByText(name) });
|
|
}
|
|
|
|
async getColumnIdByName(name: string): Promise<string> {
|
|
const columnHeader = this.page
|
|
.getByRole('columnheader')
|
|
.filter({ has: this.page.getByTestId('data-table-column-header-text').getByText(name) });
|
|
|
|
const colId = await columnHeader.getAttribute('col-id');
|
|
if (!colId) {
|
|
throw new Error(`Could not find col-id for column with name: ${name}`);
|
|
}
|
|
return colId;
|
|
}
|
|
|
|
async addColumn(
|
|
name: string,
|
|
type: 'string' | 'number' | 'boolean' | 'date',
|
|
source: 'header' | 'table',
|
|
) {
|
|
if (source === 'header') {
|
|
await this.getAddColumnHeaderButton().click();
|
|
} else {
|
|
await this.getAddColumnTableButton().click();
|
|
}
|
|
await this.getAddColumnPopoverContent().waitFor({ state: 'visible' });
|
|
|
|
const nameInput = this.page
|
|
.getByTestId('add-column-popover-content')
|
|
.locator('input[type="text"]')
|
|
.first();
|
|
await nameInput.fill(name);
|
|
|
|
const typeSelect = this.page
|
|
.getByTestId('add-column-popover-content')
|
|
.locator('.n8n-select')
|
|
.first();
|
|
await typeSelect.click();
|
|
|
|
const typeLabel = type === 'date' ? 'datetime' : type;
|
|
await this.page.getByRole('option', { name: typeLabel, exact: true }).click();
|
|
|
|
await this.getAddColumnSubmitButton().click();
|
|
|
|
await this.getAddColumnPopoverContent().waitFor({ state: 'hidden' });
|
|
}
|
|
|
|
getAddRowHeaderButton() {
|
|
return this.page.getByTestId('data-table-header-add-row-button');
|
|
}
|
|
|
|
getAddRowTableButton() {
|
|
return this.page.locator('[data-test-id="data-table-grid"] .add-row-cell button');
|
|
}
|
|
|
|
async addRow() {
|
|
await this.getAddRowHeaderButton().click();
|
|
}
|
|
|
|
async addRowFromTable() {
|
|
await this.getAddRowTableButton().click();
|
|
}
|
|
|
|
getDataRows() {
|
|
return this.page.locator(
|
|
'[data-test-id="data-table-grid"] .ag-center-cols-container [row-index]',
|
|
);
|
|
}
|
|
|
|
getRowSelectionCheckbox(rowIndex: number) {
|
|
return this.page.locator(
|
|
`[data-test-id="data-table-grid"] .ag-center-cols-container [row-index="${rowIndex}"] [col-id="ag-Grid-SelectionColumn"] input[type="checkbox"]`,
|
|
);
|
|
}
|
|
|
|
async selectRow(rowIndex: number) {
|
|
const checkbox = this.getRowSelectionCheckbox(rowIndex);
|
|
await checkbox.check();
|
|
}
|
|
|
|
getSelectedItemsInfo() {
|
|
return this.page.getByTestId('selected-items-info');
|
|
}
|
|
|
|
getDeleteSelectedButton() {
|
|
return this.page.getByTestId('delete-selected-button');
|
|
}
|
|
|
|
getClearSelectionButton() {
|
|
return this.page.getByTestId('clear-selection-button');
|
|
}
|
|
|
|
async deleteSelectedRows() {
|
|
await this.getDeleteSelectedButton().click();
|
|
const confirmButton = this.page.locator('.btn--confirm');
|
|
await confirmButton.click();
|
|
}
|
|
|
|
async clearSelection() {
|
|
await this.getClearSelectionButton().click();
|
|
}
|
|
|
|
getColumnHeaderActions(columnName: string) {
|
|
const columnHeader = this.page.getByRole('columnheader').filter({
|
|
has: this.page.getByTestId('data-table-column-header-text').getByText(columnName),
|
|
});
|
|
return columnHeader.getByTestId('data-table-column-header-actions');
|
|
}
|
|
|
|
async deleteColumn(columnName: string) {
|
|
const columnHeader = this.page.getByTestId('data-table-column-header').filter({
|
|
has: this.page.getByTestId('data-table-column-header-text').getByText(columnName),
|
|
});
|
|
await columnHeader.hover();
|
|
|
|
const actionsButton = this.getColumnHeaderActions(columnName);
|
|
await actionsButton.click();
|
|
|
|
await this.page
|
|
.getByTestId('data-table-column-header-actions-item-delete')
|
|
.filter({ visible: true })
|
|
.click();
|
|
|
|
const confirmButton = this.page.locator('.btn--confirm');
|
|
await confirmButton.click();
|
|
}
|
|
|
|
async openColumnFilter(columnName: string) {
|
|
const columnHeader = this.page.getByRole('columnheader').filter({
|
|
has: this.page.getByTestId('data-table-column-header-text').getByText(columnName),
|
|
});
|
|
await columnHeader.hover();
|
|
|
|
const filterButton = columnHeader.getByTestId('data-table-column-header-filter-button');
|
|
await filterButton.click();
|
|
}
|
|
|
|
private async openFilterPanelAndWait(columnName: string) {
|
|
await this.openColumnFilter(columnName);
|
|
const filterPanel = this.page.locator('.ag-filter');
|
|
await filterPanel.waitFor({ state: 'visible' });
|
|
return filterPanel;
|
|
}
|
|
|
|
private async selectPickerOption(label: string) {
|
|
await this.page.locator('.ag-picker-field-icon').click();
|
|
await this.page.locator('.ag-select-list-item').filter({ hasText: label }).first().click();
|
|
}
|
|
|
|
private async selectFilterOperator(condition: 'equals' | 'greaterThan' | 'lessThan') {
|
|
if (condition === 'equals') return;
|
|
const label = condition === 'greaterThan' ? 'Greater than' : 'Less than';
|
|
await this.selectPickerOption(label);
|
|
}
|
|
|
|
private async fillFilterValue(
|
|
filterPanel: Locator,
|
|
value: string,
|
|
type: 'text' | 'number' | 'date',
|
|
) {
|
|
let input: Locator;
|
|
if (type === 'number') {
|
|
input = filterPanel.locator('input[type="number"]').first();
|
|
} else if (type === 'text') {
|
|
input = filterPanel.locator('input[type="text"]').first();
|
|
} else {
|
|
input = filterPanel.locator('input').first();
|
|
}
|
|
await input.fill(value);
|
|
return input;
|
|
}
|
|
|
|
async clearColumnFilter(columnName: string) {
|
|
await this.openColumnFilter(columnName);
|
|
const filterPanel = this.page.locator('.ag-filter');
|
|
await filterPanel.locator('[data-ref="resetFilterButton"]').click();
|
|
}
|
|
|
|
async setTextFilter(columnName: string, value: string) {
|
|
await this.openColumnFilter(columnName);
|
|
|
|
const filterPanel = this.page.locator('.ag-filter');
|
|
await filterPanel.waitFor({ state: 'visible' });
|
|
|
|
const filterInput = filterPanel.locator('input[type="text"]').first();
|
|
await filterInput.fill(value);
|
|
|
|
await this.page.keyboard.press('Enter');
|
|
}
|
|
|
|
async setNumberFilter(
|
|
columnName: string,
|
|
value: string,
|
|
condition: 'equals' | 'greaterThan' | 'lessThan' = 'equals',
|
|
) {
|
|
const filterPanel = await this.openFilterPanelAndWait(columnName);
|
|
await this.selectFilterOperator(condition);
|
|
const filterInput = await this.fillFilterValue(filterPanel, value, 'number');
|
|
// Wait for the value to be set before pressing Enter
|
|
await expect(filterInput).toHaveValue(value);
|
|
await this.page.keyboard.press('Enter');
|
|
}
|
|
|
|
async setBooleanFilter(columnName: string, value: boolean) {
|
|
await this.openFilterPanelAndWait(columnName);
|
|
await this.selectPickerOption(value ? 'True' : 'False');
|
|
}
|
|
|
|
async setDateFilter(
|
|
columnName: string,
|
|
value: string,
|
|
condition: 'equals' | 'greaterThan' | 'lessThan' = 'equals',
|
|
) {
|
|
const filterPanel = await this.openFilterPanelAndWait(columnName);
|
|
await this.selectFilterOperator(condition);
|
|
await this.fillFilterValue(filterPanel, value, 'date');
|
|
await this.page.keyboard.press('Enter');
|
|
}
|
|
|
|
getPagination() {
|
|
return this.page.getByTestId('data-table-content-pagination');
|
|
}
|
|
|
|
async setPageSize(size: '10' | '20' | '50') {
|
|
const pagination = this.getPagination();
|
|
const selectTrigger = pagination.locator('.el-pagination__sizes .el-select');
|
|
await selectTrigger.click();
|
|
await this.page.getByRole('option').getByText(`${size}/page`).click();
|
|
}
|
|
|
|
getCell(rowIndex: number, columnId: string) {
|
|
const cell = this.page.locator(
|
|
`[data-test-id="data-table-grid"] .ag-center-cols-container [row-index="${rowIndex}"] [col-id="${columnId}"]`,
|
|
);
|
|
return cell;
|
|
}
|
|
|
|
async getCellValue(
|
|
rowIndex: number,
|
|
columnId: string,
|
|
type: 'string' | 'number' | 'boolean' | 'date',
|
|
): Promise<string> {
|
|
const cell = this.getCell(rowIndex, columnId);
|
|
|
|
if (type === 'boolean') {
|
|
const checkbox = cell.locator('input[type="checkbox"]');
|
|
const isChecked = await checkbox.isChecked();
|
|
return isChecked ? 'true' : 'false';
|
|
} else {
|
|
const text = await cell.textContent();
|
|
return (text ?? '').trim();
|
|
}
|
|
}
|
|
|
|
async setCellValue(
|
|
rowIndex: number,
|
|
columnId: string,
|
|
value: string,
|
|
type: 'string' | 'number' | 'boolean' | 'date',
|
|
options?: { skipDoubleClick?: boolean },
|
|
) {
|
|
const cell = this.getCell(rowIndex, columnId);
|
|
|
|
if (!options?.skipDoubleClick) {
|
|
await cell.dblclick();
|
|
}
|
|
|
|
if (type === 'string') {
|
|
const input = this.page.locator('.ag-text-area-input');
|
|
await input.fill(value);
|
|
await input.press('Enter');
|
|
} else if (type === 'date') {
|
|
const input = this.page.locator('#data-table-datepicker');
|
|
await input.fill(value);
|
|
await input.press('Enter');
|
|
} else if (type === 'boolean') {
|
|
const checkbox = cell.locator('input[type="checkbox"]');
|
|
const shouldBeChecked = value === 'true' || value === '1';
|
|
|
|
if (shouldBeChecked) {
|
|
await checkbox.check();
|
|
} else {
|
|
await checkbox.uncheck();
|
|
}
|
|
await this.page.keyboard.press('Enter');
|
|
} else if (type === 'number') {
|
|
const input = cell.locator('input');
|
|
await input.fill(value);
|
|
await input.press('Enter');
|
|
}
|
|
}
|
|
|
|
async getColumnOrder(): Promise<string[]> {
|
|
const headers = this.page.getByRole('columnheader');
|
|
const count = await headers.count();
|
|
const columnData: Array<{ index: number; name: string }> = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const header = headers.nth(i);
|
|
const ariaColIndex = await header.getAttribute('aria-colindex');
|
|
const textElement = header.getByTestId('data-table-column-header-text');
|
|
const textCount = await textElement.count();
|
|
|
|
if (textCount > 0 && ariaColIndex) {
|
|
// Use innerText instead of textContent to avoid getting hidden input values
|
|
const text = await textElement.innerText();
|
|
if (text) {
|
|
columnData.push({
|
|
index: parseInt(ariaColIndex, 10),
|
|
name: text.trim(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
columnData.sort((a, b) => a.index - b.index);
|
|
return columnData.map((col) => col.name);
|
|
}
|
|
|
|
async dragColumnToPosition(sourceColumnName: string, targetColumnName: string) {
|
|
const sourceColumn = this.getColumnHeaderByName(sourceColumnName);
|
|
const targetColumn = this.getColumnHeaderByName(targetColumnName);
|
|
|
|
await sourceColumn.dragTo(targetColumn);
|
|
}
|
|
|
|
getSearchInput() {
|
|
return this.page.getByTestId('data-table-search-input');
|
|
}
|
|
|
|
async search(query: string) {
|
|
const searchInput = this.getSearchInput();
|
|
await searchInput.fill(query);
|
|
// Wait for debounce
|
|
await this.page.waitForTimeout(300);
|
|
await this.page.getByText('Loading...').waitFor({ state: 'hidden' });
|
|
}
|
|
|
|
async clearSearch() {
|
|
await this.search('');
|
|
}
|
|
}
|