mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-02 02:26:57 +00:00
feat(testing): Added Table Column Operations test suite
This commit is contained in:
@@ -20,6 +20,7 @@ import MdiCloseCircle from '~icons/mdi/close-circle'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | string[]
|
||||
rowIndex?: number
|
||||
}
|
||||
|
||||
const { modelValue } = defineProps<Props>()
|
||||
@@ -150,7 +151,13 @@ watch(isOpen, (n, _o) => {
|
||||
@keydown="handleKeys"
|
||||
@click="isOpen = !isOpen"
|
||||
>
|
||||
<a-select-option v-for="op of options" :key="op.id" :value="op.title" @click.stop>
|
||||
<a-select-option
|
||||
v-for="op of options"
|
||||
:key="op.id"
|
||||
:value="op.title"
|
||||
:pw-data="`select-option-${column.title}-${rowIndex}`"
|
||||
@click.stop
|
||||
>
|
||||
<a-tag class="rounded-tag" :color="op.color">
|
||||
<span
|
||||
:style="{
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ActiveCellInj, ColumnInj, IsKanbanInj, ReadonlyInj, computed, inject, r
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | undefined
|
||||
rowIndex?: number
|
||||
}
|
||||
|
||||
const { modelValue } = defineProps<Props>()
|
||||
@@ -81,7 +82,13 @@ watch(isOpen, (n, _o) => {
|
||||
@keydown="handleKeys"
|
||||
@click="isOpen = !isOpen"
|
||||
>
|
||||
<a-select-option v-for="op of options" :key="op.title" :value="op.title" @click.stop>
|
||||
<a-select-option
|
||||
v-for="op of options"
|
||||
:key="op.title"
|
||||
:value="op.title"
|
||||
:pw-data="`select-option-${column.title}-${rowIndex}`"
|
||||
@click.stop
|
||||
>
|
||||
<a-tag class="rounded-tag" :color="op.color">
|
||||
<span
|
||||
:style="{
|
||||
|
||||
@@ -126,15 +126,14 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
|
||||
<div
|
||||
class="nc-cell w-full"
|
||||
:class="[`nc-cell-${(column?.uidt || 'default').toLowerCase()}`, { 'text-blue-600': isPrimary && !virtual && !isForm }]"
|
||||
:data-pw="`cell-${column?.title}-${rowIndex}`"
|
||||
@keydown.enter.exact="syncAndNavigate(NavigateDir.NEXT, $event)"
|
||||
@keydown.shift.enter.exact="syncAndNavigate(NavigateDir.PREV, $event)"
|
||||
>
|
||||
<LazyCellTextArea v-if="isTextArea" v-model="vModel" />
|
||||
<LazyCellCheckbox v-else-if="isBoolean" v-model="vModel" />
|
||||
<LazyCellAttachment v-else-if="isAttachment" v-model="vModel" :row-index="props.rowIndex" />
|
||||
<LazyCellSingleSelect v-else-if="isSingleSelect" v-model="vModel" />
|
||||
<LazyCellMultiSelect v-else-if="isMultiSelect" v-model="vModel" />
|
||||
<LazyCellSingleSelect v-else-if="isSingleSelect" v-model="vModel" :row-index="props.rowIndex" />
|
||||
<LazyCellMultiSelect v-else-if="isMultiSelect" v-model="vModel" :row-index="props.rowIndex" />
|
||||
<LazyCellDatePicker v-else-if="isDate" v-model="vModel" />
|
||||
<LazyCellYearPicker v-else-if="isYear" v-model="vModel" />
|
||||
<LazyCellDateTimePicker v-else-if="isDateTime" v-model="vModel" />
|
||||
|
||||
@@ -461,7 +461,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col h-full min-h-0 w-full">
|
||||
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15" pw-data="grid-load-spinner">
|
||||
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15">
|
||||
<div class="flex items-center justify-center h-full w-full !bg-white !bg-opacity-85 z-1000">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
@@ -568,6 +568,7 @@ watch(
|
||||
<div
|
||||
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)"
|
||||
class="nc-expand"
|
||||
:pw-data="`nc-expand-${rowIndex}`"
|
||||
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
|
||||
>
|
||||
<a-spin v-if="row.rowMeta.saving" class="!flex items-center" />
|
||||
@@ -607,7 +608,6 @@ watch(
|
||||
:data-key="rowIndex + columnObj.id"
|
||||
:data-col="columnObj.id"
|
||||
:data-title="columnObj.title"
|
||||
:data-pw="`cell-${columnObj.title}-${rowIndex}`"
|
||||
@click="selectCell(rowIndex, colIndex)"
|
||||
@dblclick="makeEditable(row, columnObj)"
|
||||
@mousedown="startSelectRange($event, rowIndex, colIndex)"
|
||||
|
||||
@@ -150,6 +150,7 @@ export default {
|
||||
:key="col.title"
|
||||
class="mt-2 py-2"
|
||||
:class="`nc-expand-col-${col.title}`"
|
||||
:pw-data="`nc-expand-col-${col.title}`"
|
||||
>
|
||||
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
|
||||
|
||||
|
||||
@@ -9,19 +9,12 @@ export class SelectOptionCellPageObject {
|
||||
|
||||
async select({index, columnHeader, option, multiSelect}: {index: number, columnHeader: string, option: string, multiSelect?: boolean}) {
|
||||
await this.cell.get({index, columnHeader}).click();
|
||||
const count = await this.cell.page.locator('.rc-virtual-list-holder .ant-select-item-option-content', {hasText: option}).count();
|
||||
|
||||
for(let i = 0; i < count; i++) {
|
||||
if(await this.cell.page.locator('.rc-virtual-list-holder .ant-select-item-option-content', {hasText: option}).nth(i).isVisible()) {
|
||||
await this.cell.page.locator('.rc-virtual-list-holder .ant-select-item-option-content', {hasText: option}).nth(i).click();
|
||||
}
|
||||
}
|
||||
await this.cell.page.locator(`[pw-data="select-option-${columnHeader}-${index}"]`, {hasText: option}).click();
|
||||
|
||||
if(multiSelect) await this.cell.get({index, columnHeader}).click();
|
||||
|
||||
await this.cell.page.locator(`.nc-dropdown-single-select-cell`).nth(index).waitFor({state: 'hidden'});
|
||||
// todo: Remove this wait. Should be solved by adding pw-data-attribute with cell info to the a-select-option of the cell
|
||||
// await this.cell.page.waitForTimeout(200);
|
||||
await this.cell.page.locator(`[pw-data="select-option-${columnHeader}-${index}"]`, {hasText: option}).waitFor({state: 'hidden'});
|
||||
}
|
||||
|
||||
async clear({index, columnHeader, multiSelect}: {index: number, columnHeader: string, multiSelect?: boolean}) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator,expect } from "@playwright/test";
|
||||
import { SelectOptionCellPageObject } from "./SelectOptionCell";
|
||||
|
||||
export class CellPageObject {
|
||||
@@ -11,10 +11,18 @@ export class CellPageObject {
|
||||
}
|
||||
|
||||
get({index, columnHeader}: {index: number, columnHeader: string}): Locator {
|
||||
return this.page.locator(`td[data-pw="cell-${columnHeader}-${index}"]`);
|
||||
return this.page.locator(`td[data-pw=cell-${columnHeader}-${index}]`);
|
||||
}
|
||||
|
||||
async click({index, columnHeader}: {index: number, columnHeader: string}) {
|
||||
return this.get({index, columnHeader}).click();
|
||||
}
|
||||
|
||||
async dblclick({index, columnHeader}: {index: number, columnHeader: string}) {
|
||||
return this.get({index, columnHeader}).dblclick();
|
||||
}
|
||||
|
||||
async verify({index, columnHeader, value}: {index: number, columnHeader: string, value: string}) {
|
||||
return expect(await this.get({index, columnHeader}).innerText()).toBe(value);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class ColumnPageObject {
|
||||
}
|
||||
|
||||
async delete({title}: {title: string}) {
|
||||
await this.page.locator(`text=#Title${title} >> svg >> nth=3`).click();
|
||||
await this.page.locator(`th[data-title="${title}"] >> svg.ant-dropdown-trigger`).click();
|
||||
await this.page.locator('li[role="menuitem"]:has-text("Delete")').waitFor()
|
||||
await this.page.locator('li[role="menuitem"]:has-text("Delete")').click();
|
||||
|
||||
@@ -81,4 +81,11 @@ export class ColumnPageObject {
|
||||
await this.page.locator('form[data-pw="add-or-edit-column"]').waitFor({state: 'hidden'});
|
||||
await this.page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
async verify({title, isDeleted}: {title: string, isDeleted?: boolean}) {
|
||||
if(isDeleted) {
|
||||
return expect(await this.page.locator(`th[data-title="${title}"]`).count()).toBe(0);
|
||||
}
|
||||
await expect(this.page.locator(`th[data-title="${title}"]`)).toHaveText(title);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// playwright-dev-page.ts
|
||||
import { expect, Locator, Page } from '@playwright/test';
|
||||
import { BasePage } from './Base';
|
||||
import { ExpandedFormPage } from './ExpandedForm';
|
||||
|
||||
export class DashboardPage {
|
||||
readonly project: any;
|
||||
@@ -8,6 +9,7 @@ export class DashboardPage {
|
||||
readonly tablesSideBar: Locator;
|
||||
readonly tabBar: Locator;
|
||||
readonly base: BasePage;
|
||||
readonly expandedForm: ExpandedFormPage;
|
||||
|
||||
constructor(page: Page, project: any) {
|
||||
this.page = page;
|
||||
@@ -15,6 +17,7 @@ export class DashboardPage {
|
||||
this.project = project;
|
||||
this.tablesSideBar = page.locator('.nc-treeview-container');
|
||||
this.tabBar = page.locator('.nc-tab-bar');
|
||||
this.expandedForm = new ExpandedFormPage(page);
|
||||
}
|
||||
|
||||
async goto() {
|
||||
|
||||
38
scripts/playwright/pages/ExpandedForm/index.ts
Normal file
38
scripts/playwright/pages/ExpandedForm/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// playwright-dev-page.ts
|
||||
import { Locator, Page, expect } from '@playwright/test';
|
||||
import { BasePage } from '../Base';
|
||||
import { CellPageObject } from '../Cell';
|
||||
import { ColumnPageObject } from '../Column';
|
||||
|
||||
export class ExpandedFormPage {
|
||||
readonly page: Page;
|
||||
readonly addNewTableButton: Locator;
|
||||
readonly column: ColumnPageObject;
|
||||
readonly cell: CellPageObject;
|
||||
readonly base: BasePage;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.addNewTableButton = page.locator('.nc-add-new-table');
|
||||
this.column = new ColumnPageObject(page);
|
||||
this.cell = new CellPageObject(page);
|
||||
this.base = new BasePage(page);
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.page.locator(`.nc-drawer-expanded-form`);
|
||||
}
|
||||
|
||||
async fillField({columnTitle, value}: {columnTitle: string, value: string}) {
|
||||
const field = this.get().locator(`[pw-data="nc-expand-col-${columnTitle}"]`);
|
||||
await field.locator('input').fill(value);
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.get().locator('button:has-text("Save Row")').click();
|
||||
await this.get().press('Escape');
|
||||
await this.get().waitFor({state: 'hidden'});
|
||||
await this.base.toastWait({message: `updated successfully.`});
|
||||
await this.page.waitForTimeout(400);
|
||||
}
|
||||
}
|
||||
@@ -16,21 +16,31 @@ export class GridPage {
|
||||
this.cell = new CellPageObject(page);
|
||||
}
|
||||
|
||||
row(index: number) {
|
||||
return this.page.locator(`tr[data-pw="grid-row-${index}"]`);
|
||||
}
|
||||
|
||||
async addNewRow({index = 0, title}: {index?: number, title?: string} = {}) {
|
||||
const rowCount = await this.page.locator('.nc-grid-row').count();
|
||||
await this.page.locator('.nc-grid-add-new-cell').click();
|
||||
if(rowCount + 1 !== await this.page.locator('.nc-grid-row').count()) {
|
||||
await this.page.locator('.nc-grid-add-new-cell').click();
|
||||
}
|
||||
// Double click td >> nth=1
|
||||
await this.page.locator('td[data-title="Title"]').nth(index).dblclick();
|
||||
|
||||
const cell = this.cell.get({index, columnHeader: 'Title'});
|
||||
await this.cell.dblclick({
|
||||
index,
|
||||
columnHeader: 'Title'
|
||||
});
|
||||
|
||||
|
||||
// Fill text=1Add new row >> input >> nth=1
|
||||
await this.page.locator(`div[data-pw="cell-Title-${index}"] >> input`).fill(title ?? `Row ${index}`);
|
||||
|
||||
await this.page.locator('span[title="Title"]').click();
|
||||
await this.page.locator('.nc-grid-wrapper').click();
|
||||
await cell.locator('input').fill(title ?? `Row ${index}`);
|
||||
await cell.locator('input').press('Enter');
|
||||
}
|
||||
|
||||
async verifyRow({index}: {index: number}) {
|
||||
await this.page.locator(`td[data-pw="cell-Title-${index}"]`).waitFor({state: 'visible'});
|
||||
expect(await this.page.locator(`td[data-pw="cell-Title-${index}"]`).count()).toBe(1);
|
||||
}
|
||||
|
||||
async verifyRowDoesNotExist({index}: {index: number}) {
|
||||
@@ -48,4 +58,21 @@ export class GridPage {
|
||||
await this.page.locator('span.ant-dropdown-menu-title-content > nc-project-menu-item').waitFor({state: 'hidden'});
|
||||
}
|
||||
|
||||
async openExpandedRow({index}:{index: number}) {
|
||||
await this.row(index).locator(`td[pw-data="cell-id-${index}"]`).hover();
|
||||
await this.row(index).locator(`div[pw-data="nc-expand-${index}"]`).click();
|
||||
}
|
||||
|
||||
async selectAll() {
|
||||
await this.page.locator('[pw-data="nc-check-all"]').hover();
|
||||
await this.page.locator('[pw-data="nc-check-all"]').locator('input[type="checkbox"]').click();
|
||||
}
|
||||
|
||||
async deleteAll() {
|
||||
await this.selectAll();
|
||||
await this.page.locator('[pw-data="nc-check-all"]').locator('input[type="checkbox"]').click({
|
||||
button: 'right'
|
||||
});
|
||||
await this.page.locator('text=Delete Selected Rows').click();
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ const config: PlaywrightTestConfig = {
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
actionTimeout: process.env.CI ? 0: 10000,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
|
||||
53
scripts/playwright/tests/tableColumnOperation.spec.ts
Normal file
53
scripts/playwright/tests/tableColumnOperation.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { test } from '@playwright/test';
|
||||
import { DashboardPage } from '../pages/Dashboard';
|
||||
import { GridPage } from '../pages/Grid';
|
||||
import setup from '../setup';
|
||||
|
||||
|
||||
test.describe('Table Column Operations', () => {
|
||||
let grid: GridPage, dashboard: DashboardPage;
|
||||
let context: any;
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
context = await setup({ page });
|
||||
dashboard = new DashboardPage(page, context.project);
|
||||
grid = new GridPage(page);
|
||||
|
||||
await dashboard.createTable({title: "sheet1"});
|
||||
})
|
||||
|
||||
test('Create column', async () => {
|
||||
await grid.column.create({title: "column_name_a"});
|
||||
await grid.column.verify({title: "column_name_a"});
|
||||
|
||||
await grid.column.openEdit({title: "column_name_a"});
|
||||
await grid.column.fillTitle({title: "column_name_b"});
|
||||
await grid.column.selectType({type: "LongText"});
|
||||
await grid.column.save({isUpdated: true});
|
||||
await grid.column.verify({title: "column_name_b"});
|
||||
|
||||
await grid.column.delete({title: "column_name_b"});
|
||||
await grid.column.verify({title: "column_name_b", isDeleted: true});
|
||||
|
||||
await grid.addNewRow({index: 0});
|
||||
await grid.verifyRow({index: 0})
|
||||
|
||||
await grid.openExpandedRow({index: 0});
|
||||
await dashboard.expandedForm.fillField({columnTitle: "Title", value: "value_a"});
|
||||
await dashboard.expandedForm.save();
|
||||
await grid.cell.verify({index: 0, columnHeader: "Title", value: "value_a"});
|
||||
|
||||
await grid.deleteRow(0);
|
||||
await grid.verifyRowDoesNotExist({index: 0});
|
||||
|
||||
await grid.addNewRow({index: 0});
|
||||
await grid.addNewRow({index: 1});
|
||||
await grid.addNewRow({index: 2});
|
||||
await grid.addNewRow({index: 3});
|
||||
await grid.addNewRow({index: 4});
|
||||
await grid.deleteAll();
|
||||
|
||||
await grid.verifyRowDoesNotExist({index: 0});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user