diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 8a7cb36d56..2b3c64550b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -18,6 +18,7 @@ on: - "packages/nc-gui/**" - "packages/nocodb/**" - ".github/workflows/ci-cd.yml" + - "tests/playwright/**" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -97,4 +98,4 @@ jobs: uses: ./.github/workflows/playwright-test-workflow.yml with: db: pg - shard: 2 \ No newline at end of file + shard: 2 diff --git a/packages/nc-gui/components/smartsheet/header/Menu.vue b/packages/nc-gui/components/smartsheet/header/Menu.vue index 411a3877fe..bbd30a8ddb 100644 --- a/packages/nc-gui/components/smartsheet/header/Menu.vue +++ b/packages/nc-gui/components/smartsheet/header/Menu.vue @@ -225,23 +225,26 @@ const hideField = async () => { -
+
- Hide Field + + {{ $t('general.hideField') }}
@@ -251,21 +254,24 @@ const hideField = async () => { v-if="column.uidt !== UITypes.LinkToAnotherRecord && column.uidt !== UITypes.Lookup && !column.pk" @click="duplicateColumn" > -
+
- Duplicate + + {{ t('general.duplicate') }}
-
+
- Insert After + + {{ t('general.insertAfter') }}
-
+
- Insert before + + {{ t('general.insertBefore') }}
diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index 09c4984f1c..c2cb7b8909 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -69,7 +69,13 @@ "betaNote": "This feature is currently in beta.", "moreInfo": "More information can be found here", "logs": "Logs", - "groupingField": "Grouping Field" + "groupingField": "Grouping Field", + "duplicate": "Duplicate", + "insertAfter": "Insert After", + "insertBefore": "Insert Before", + "hideField": "Hide Field", + "sortAsc": "Sort Ascending", + "sortDesc": "Sort Descending" }, "objects": { "project": "Project", diff --git a/tests/playwright/pages/Dashboard/Grid/Column/index.ts b/tests/playwright/pages/Dashboard/Grid/Column/index.ts index 1116e886e5..48107c7800 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/index.ts @@ -1,4 +1,4 @@ -import { expect, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; import { GridPage } from '..'; import BasePage from '../../../Base'; import { SelectOptionColumnPageObject } from './SelectOptionColumn'; @@ -35,6 +35,8 @@ export class ColumnPageObject extends BasePage { relationType = '', rollupType = '', format = '', + insertAfterColumnTitle, + insertBeforeColumnTitle, }: { title: string; type?: string; @@ -45,8 +47,19 @@ export class ColumnPageObject extends BasePage { relationType?: string; rollupType?: string; format?: string; + insertBeforeColumnTitle?: string; + insertAfterColumnTitle?: string; }) { - await this.grid.get().locator('.nc-column-add').click(); + if (insertBeforeColumnTitle) { + await this.grid.get().locator(`th[data-title="${insertBeforeColumnTitle}"] .nc-ui-dt-dropdown`).click(); + 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}"] .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); @@ -54,8 +67,6 @@ export class ColumnPageObject extends BasePage { await this.rootPage.waitForTimeout(500); switch (type) { - case 'SingleTextLine': - break; case 'SingleSelect': case 'MultiSelect': await this.selectOption.addOption({ @@ -142,6 +153,30 @@ export class ColumnPageObject extends BasePage { } await this.save(); + + // verify column inserted after the target column + if (insertAfterColumnTitle) { + const headersText = await this.grid.get().locator(`th`).allTextContents(); + + await expect( + this.grid + .get() + .locator(`th`) + .nth(headersText.findIndex(title => title.startsWith(insertAfterColumnTitle)) + 1) + ).toHaveText(title); + } + + // verify column inserted before the target column + if (insertBeforeColumnTitle) { + const headersText = await this.grid.get().locator(`th`).allTextContents(); + + await expect( + this.grid + .get() + .locator(`th`) + .nth(headersText.findIndex(title => title.startsWith(insertBeforeColumnTitle)) - 1) + ).toHaveText(title); + } } async fillTitle({ title }: { title: string }) { @@ -213,6 +248,26 @@ export class ColumnPageObject extends BasePage { } } + 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.verifyToast({ message: 'Column duplicated successfully' }); + await this.grid.get().locator(`th[data-title="${expectedTitle}"]`).isVisible(); + } + + async hideColumn({ title }: { title: string }) { + await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click(); + + await this.waitForResponse({ + uiAction: 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 }: { isUpdated?: boolean } = {}) { await this.waitForResponse({ uiAction: this.get().locator('button:has-text("Save")').click(), @@ -245,4 +300,34 @@ export class ColumnPageObject extends BasePage { await this.grid.get().locator('.nc-ui-dt-dropdown:visible').first().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; + 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(); + } } diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index ca04a11b83..22052a2ca6 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/index.ts @@ -193,6 +193,6 @@ export class CellPageObject extends BasePage { ) { await this.get({ index, columnHeader }).click(...clickOptions); - await this.get({ index, columnHeader }).press('Control+C'); + await this.get({ index, columnHeader }).press((await this.isMacOs()) ? 'Meta+C' : 'Control+C'); } } diff --git a/tests/playwright/tests/columnMenuOperations.spec.ts b/tests/playwright/tests/columnMenuOperations.spec.ts new file mode 100644 index 0000000000..8217fd6964 --- /dev/null +++ b/tests/playwright/tests/columnMenuOperations.spec.ts @@ -0,0 +1,134 @@ +import { test } from '@playwright/test'; +import { DashboardPage } from '../pages/Dashboard'; +import setup from '../setup'; + +const columns = [ + { + title: 'SingleLineText', + type: 'SingleLineText', + }, + { + title: 'LongText', + type: 'LongText', + }, + // todo: Number column creation not works + // { + // title: 'Number', + // type: 'Number', + // }, + { + title: 'Decimal', + type: 'Decimal', + }, + { + title: 'Checkbox', + type: 'Checkbox', + }, + { + title: 'Email', + type: 'Email', + }, + { + title: 'PhoneNumber', + type: 'PhoneNumber', + }, + { + title: 'Url', + type: 'URL', + }, +]; + +test.describe('Column menu operations', () => { + let dashboard: DashboardPage; + let context: any; + + test.beforeEach(async ({ page }) => { + context = await setup({ page }); + dashboard = new DashboardPage(page, context.project); + }); + + test('Duplicate fields', async () => { + await dashboard.treeView.openTable({ title: 'Film' }); + + for (const { title, type } of columns) { + await dashboard.grid.column.create({ + title, + type, + }); + + await dashboard.grid.column.duplicateColumn({ + title, + }); + await dashboard.grid.column.duplicateColumn({ + title, + expectedTitle: `${title}_copy_1`, + }); + } + await dashboard.closeTab({ title: 'Film' }); + }); + test('Insert after', async () => { + await dashboard.treeView.openTable({ title: 'Film' }); + + await dashboard.grid.column.create({ + title: 'InsertAfterColumn', + type: 'SingleLineText', + insertAfterColumnTitle: 'Title', + }); + + await dashboard.grid.column.create({ + title: 'InsertAfterColumn1', + type: 'SingleLineText', + insertAfterColumnTitle: 'Store List', + }); + + await dashboard.closeTab({ title: 'Film' }); + }); + + test('Insert before', async () => { + await dashboard.treeView.openTable({ title: 'Film' }); + + await dashboard.grid.column.create({ + title: 'InsertBeforeColumn', + type: 'SingleLineText', + insertBeforeColumnTitle: 'Title', + }); + + await dashboard.grid.column.create({ + title: 'InsertBeforeColumn1', + type: 'SingleLineText', + insertBeforeColumnTitle: 'Store List', + }); + + await dashboard.closeTab({ title: 'Film' }); + }); + + test('Hide column', async () => { + await dashboard.treeView.openTable({ title: 'Film' }); + + await dashboard.grid.column.hideColumn({ + title: 'Title', + }); + + await dashboard.grid.column.hideColumn({ + title: 'Store List', + }); + + await dashboard.closeTab({ title: 'Film' }); + }); + + test('Sort column', async () => { + await dashboard.treeView.openTable({ title: 'Film' }); + + await dashboard.grid.column.sortColumn({ + title: 'Title', + direction: 'asc', + }); + + await dashboard.grid.column.sortColumn({ + title: 'ReleaseYear', + direction: 'desc', + }); + + await dashboard.closeTab({ title: 'Film' }); + }); +});