diff --git a/packages/nc-gui/components/dlg/TableCreate.vue b/packages/nc-gui/components/dlg/TableCreate.vue
index 77718878af..0725e124a7 100644
--- a/packages/nc-gui/components/dlg/TableCreate.vue
+++ b/packages/nc-gui/components/dlg/TableCreate.vue
@@ -105,6 +105,7 @@ onMounted(() => {
v-model:value="table.title"
size="large"
hide-details
+ data-pw="create-table-title-input"
:placeholder="$t('msg.info.enterTableName')"
/>
diff --git a/packages/nc-gui/components/smartsheet/Cell.vue b/packages/nc-gui/components/smartsheet/Cell.vue
index ac4d5d017d..ddaf84f9ce 100644
--- a/packages/nc-gui/components/smartsheet/Cell.vue
+++ b/packages/nc-gui/components/smartsheet/Cell.vue
@@ -126,6 +126,7 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue
index 2f5e1499ce..80892f7eab 100644
--- a/packages/nc-gui/components/smartsheet/Grid.vue
+++ b/packages/nc-gui/components/smartsheet/Grid.vue
@@ -607,6 +607,7 @@ 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)"
diff --git a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
index 3f7f342274..b2ac24d492 100644
--- a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
+++ b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
@@ -124,7 +124,7 @@ onMounted(() => {
:class="{ '!w-[600px]': formState.uidt === UITypes.Formula }"
@click.stop
>
-
+
{
diff --git a/scripts/playwright/.gitignore b/scripts/playwright/.gitignore
new file mode 100644
index 0000000000..75e854d8dc
--- /dev/null
+++ b/scripts/playwright/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+/test-results/
+/playwright-report/
+/playwright/.cache/
diff --git a/scripts/playwright/package-lock.json b/scripts/playwright/package-lock.json
new file mode 100644
index 0000000000..44031ded44
--- /dev/null
+++ b/scripts/playwright/package-lock.json
@@ -0,0 +1,119 @@
+{
+ "name": "playwright",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "playwright",
+ "version": "1.0.0",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.26.1",
+ "axios": "^0.24.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.26.1.tgz",
+ "integrity": "sha512-bNxyZASVt2adSZ9gbD7NCydzcb5JaI0OR9hc7s+nmPeH604gwp0zp17NNpwXY4c8nvuBGQQ9oGDx72LE+cUWvw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "playwright-core": "1.26.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "18.8.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.1.tgz",
+ "integrity": "sha512-vuYaNuEIbOYLTLUAJh50ezEbvxrD43iby+lpUA2aa148Nh5kX/AVO/9m1Ahmbux2iU5uxJTNF9g2Y+31uml7RQ==",
+ "dev": true
+ },
+ "node_modules/axios": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
+ "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
+ "dev": true,
+ "dependencies": {
+ "follow-redirects": "^1.14.4"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
+ "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
+ "dev": true,
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ }
+ },
+ "dependencies": {
+ "@playwright/test": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.26.1.tgz",
+ "integrity": "sha512-bNxyZASVt2adSZ9gbD7NCydzcb5JaI0OR9hc7s+nmPeH604gwp0zp17NNpwXY4c8nvuBGQQ9oGDx72LE+cUWvw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "playwright-core": "1.26.1"
+ }
+ },
+ "@types/node": {
+ "version": "18.8.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.1.tgz",
+ "integrity": "sha512-vuYaNuEIbOYLTLUAJh50ezEbvxrD43iby+lpUA2aa148Nh5kX/AVO/9m1Ahmbux2iU5uxJTNF9g2Y+31uml7RQ==",
+ "dev": true
+ },
+ "axios": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
+ "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
+ "dev": true,
+ "requires": {
+ "follow-redirects": "^1.14.4"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "dev": true
+ },
+ "playwright-core": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
+ "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
+ "dev": true
+ }
+ }
+}
diff --git a/scripts/playwright/package.json b/scripts/playwright/package.json
new file mode 100644
index 0000000000..d393256f62
--- /dev/null
+++ b/scripts/playwright/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "playwright",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {},
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.26.1",
+ "axios": "^0.24.0"
+ }
+}
diff --git a/scripts/playwright/pages/Base.ts b/scripts/playwright/pages/Base.ts
new file mode 100644
index 0000000000..4315e29b22
--- /dev/null
+++ b/scripts/playwright/pages/Base.ts
@@ -0,0 +1,19 @@
+// playwright-dev-page.ts
+import { Page, expect } from '@playwright/test';
+
+export class BasePage {
+ readonly page: Page;
+
+ constructor(page: Page) {
+ this.page = page;
+ }
+
+ async toastWait({message}: {message: string}) {
+ const toast = await this.page.locator('.ant-message .ant-message-notice-content', {hasText: message}).last();
+ await toast.waitFor({state: 'visible'});
+
+ // todo: text of toastr shows old one in the test assertion
+ await toast.last().textContent()
+ .then((text) => expect(text).toContain(message));
+ }
+}
\ No newline at end of file
diff --git a/scripts/playwright/pages/Cell/SelectOptionCell.ts b/scripts/playwright/pages/Cell/SelectOptionCell.ts
new file mode 100644
index 0000000000..29dfe3ccb6
--- /dev/null
+++ b/scripts/playwright/pages/Cell/SelectOptionCell.ts
@@ -0,0 +1,70 @@
+import { expect } from "@playwright/test";
+import { CellPageObject } from ".";
+
+export class SelectOptionCellPageObject {
+ readonly cell: CellPageObject;
+ constructor(cell: CellPageObject) {
+ this.cell = cell;
+ }
+
+ 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();
+ }
+ }
+
+ if(multiSelect) await this.cell.get({index, columnHeader}).click();
+
+ await this.cell.page.locator(`.nc-dropdown-single-select-cell`).nth(index).waitFor({state: 'hidden'});
+ }
+
+ async clear({index, columnHeader, multiSelect}: {index: number, columnHeader: string, multiSelect?: boolean}) {
+ if(multiSelect){
+ await this.cell.get({index, columnHeader}).click();
+ await this.cell.get({index, columnHeader}).click();
+
+ const optionCount = await this.cell.get({index, columnHeader}).locator('.ant-tag').count();
+
+ for(let i = 0; i < optionCount; i++) {
+ await this.cell.get({index, columnHeader}).locator('.ant-tag > .ant-tag-close-icon').first().click();
+ await this.cell.page.waitForTimeout(200);
+ }
+ return
+ }
+
+ await this.cell.get({index, columnHeader}).click();
+ await this.cell.page.locator('.ant-select-single > .ant-select-clear').click();
+ await this.cell.get({index, columnHeader}).click();
+ await this.cell.page.locator(`.nc-dropdown-single-select-cell`).waitFor({state: 'hidden'});
+ }
+
+ async verify({index, columnHeader, option, multiSelect}: {index: number, columnHeader: string, option: string, multiSelect?: boolean}) {
+ if(multiSelect) {
+ return expect(
+ this.cell.get({index, columnHeader})).toContainText(option, {useInnerText: true});
+ }
+ return expect(this.cell.get({index, columnHeader}).locator('.ant-select-selection-item > .ant-tag')).toHaveText(option, {useInnerText: true});
+ }
+
+ async verifyNoOptionsSelected({index, columnHeader}: {index: number, columnHeader: string}) {
+ return expect(this.cell.get({index, columnHeader}).locator('.ant-select-selection-item > .ant-tag')).toBeHidden();
+ }
+
+ async verifyOptions({index, columnHeader, options}: {index: number, columnHeader: string, options: string[]}) {
+ await this.cell.get({index, columnHeader}).click();
+
+ let counter = 0;
+ for (const option of options) {
+ const optionInDom = await this.cell.page.locator(`div.ant-select-item-option`).nth(counter)
+ .evaluate((node) => (node as HTMLElement).innerText)
+ expect(optionInDom).toBe(option);
+ counter++;
+ }
+ await this.cell.click({index, columnHeader});
+ await this.cell.page.locator(`.nc-dropdown-single-select-cell`).nth(index).waitFor({state: 'hidden'});
+ }
+}
\ No newline at end of file
diff --git a/scripts/playwright/pages/Cell/index.ts b/scripts/playwright/pages/Cell/index.ts
new file mode 100644
index 0000000000..3acb3a6574
--- /dev/null
+++ b/scripts/playwright/pages/Cell/index.ts
@@ -0,0 +1,20 @@
+import { Page, Locator } from "@playwright/test";
+import { SelectOptionCellPageObject } from "./SelectOptionCell";
+
+export class CellPageObject {
+ readonly page: Page;
+ readonly selectOption: SelectOptionCellPageObject;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.selectOption = new SelectOptionCellPageObject(this);
+ }
+
+ get({index, columnHeader}: {index: number, columnHeader: string}): Locator {
+ return this.page.locator(`tr.nc-grid-row:nth-child(${index + 1}) > [data-title="${columnHeader}"]`);
+ }
+
+ async click({index, columnHeader}: {index: number, columnHeader: string}) {
+ return this.get({index, columnHeader}).click();
+ }
+}
\ No newline at end of file
diff --git a/scripts/playwright/pages/Column/SelectOptionColumn.ts b/scripts/playwright/pages/Column/SelectOptionColumn.ts
new file mode 100644
index 0000000000..e2f15a94a3
--- /dev/null
+++ b/scripts/playwright/pages/Column/SelectOptionColumn.ts
@@ -0,0 +1,53 @@
+import { Page } from "@playwright/test";
+import { ColumnPageObject } from ".";
+
+export class SelectOptionColumnPageObject {
+ readonly column: ColumnPageObject;
+
+ constructor(column: ColumnPageObject) {
+ this.column = column;
+ }
+
+ async addOption({index, columnTitle,option, skipColumnModal}: {index: number, option: string, skipColumnModal?: boolean, columnTitle?: string}) {
+ if(!skipColumnModal && columnTitle) await this.column.openEdit({title: columnTitle});
+
+ await this.column.page.locator('button:has-text("Add option")').click();
+
+ // Fill text=Select options can't be nullAdd option >> input[type="text"]
+ await this.column.page.locator(`input[data-pw="select-column-option-input-${index}"]`).click();
+ await this.column.page.locator(`input[data-pw="select-column-option-input-${index}"]`).fill(option);
+
+ if(!skipColumnModal && columnTitle) await this.column.save({isUpdated: true});
+ }
+
+ async editOption({columnTitle, index, newOption}: {index: number, columnTitle: string, newOption: string}) {
+ await this.column.openEdit({title: columnTitle});
+
+ await this.column.page.locator(`input[data-pw="select-column-option-input-${index}"]`).click();
+ await this.column.page.locator(`input[data-pw="select-column-option-input-${index}"]`).fill(newOption);
+
+ await this.column.save({isUpdated: true});
+ }
+
+ async deleteOption({columnTitle, index}: {index: number, columnTitle: string}) {
+ await this.column.openEdit({title: columnTitle});
+
+ await this.column.page.locator(`svg[data-pw="select-column-option-remove-${index}"]`).click();
+
+ await this.column.save({isUpdated: true});
+ }
+
+ async reorderOption({columnTitle, sourceOption, destinationOption}: {columnTitle: string, sourceOption: string, destinationOption: string}) {
+ await this.column.openEdit({title: columnTitle});
+
+ await this.column.page.waitForTimeout(150);
+
+ await this.column.page.dragAndDrop(`svg[data-pw="select-option-column-handle-icon-${sourceOption}"]`, `svg[data-pw="select-option-column-handle-icon-${destinationOption}"]`, {
+ force: true,
+ });
+
+ await this.column.page.waitForTimeout(150);
+
+ await this.column.save({isUpdated: true});
+ }
+}
\ No newline at end of file
diff --git a/scripts/playwright/pages/Column/index.ts b/scripts/playwright/pages/Column/index.ts
new file mode 100644
index 0000000000..844952a3d3
--- /dev/null
+++ b/scripts/playwright/pages/Column/index.ts
@@ -0,0 +1,69 @@
+import { Page, expect } from "@playwright/test";
+import { BasePage } from "../Base";
+import {SelectOptionColumnPageObject} from "./SelectOptionColumn";
+
+export class ColumnPageObject {
+ readonly page: Page;
+ readonly basePage: BasePage;
+ readonly selectOption: SelectOptionColumnPageObject;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.selectOption = new SelectOptionColumnPageObject(this);
+ this.basePage = new BasePage(this.page);
+ }
+
+ async create({title, type}: {title: string, type: string}) {
+ await this.page.locator('.nc-column-add').click();
+
+ await this.page.locator('form[data-pw="add-or-edit-column"]').waitFor();
+
+ // Click span:has-text("SingleLineText") >> nth=1
+ await this.page.locator('span:has-text("SingleLineText")').click();
+
+ // Fill text=Column Type SingleLineText >> input[role="combobox"]
+ await this.page.locator('text=Column Type SingleLineText >> input[role="combobox"]').fill(type);
+
+ // Select column type
+ await this.page.locator(`text=${type}`).nth(1).click();
+
+ switch (type) {
+ case 'SingleSelect':
+ case 'MultiSelect':
+ await this.selectOption.addOption({index: 0, option: 'Option 1', skipColumnModal: true});
+ await this.selectOption.addOption({index: 1, option: 'Option 2', skipColumnModal: true});
+ break;
+ default:
+ break;
+ }
+
+ await this.page.locator('.nc-column-name-input').fill(title);
+
+ await this.save();
+ }
+
+ async delete({title}: {title: string}) {
+ await this.page.locator(`text=#Title${title} >> svg >> nth=3`).click();
+ await this.page.locator('li[role="menuitem"]:has-text("Delete")').waitFor()
+ await this.page.locator('li[role="menuitem"]:has-text("Delete")').click();
+
+ await this.page.locator('button:has-text("Delete")').click();
+
+ // wait till modal is closed
+ await this.page.locator('.nc-modal-column-delete').waitFor({state: 'hidden'});
+ }
+
+ async openEdit({title}: {title: string}) {
+ await this.page.locator(`text=#Title${title} >> svg >> nth=3`).click();
+ await this.page.locator('li[role="menuitem"]:has-text("Edit")').waitFor()
+ await this.page.locator('li[role="menuitem"]:has-text("Edit")').click();
+
+ await this.page.locator('form[data-pw="add-or-edit-column"]').waitFor();
+ }
+
+ async save({isUpdated}: {isUpdated?: boolean} = {}) {
+ await this.page.locator('button:has-text("Save")').click();
+
+ await this.basePage.toastWait({message: isUpdated ? 'Column updated' : 'Column created'});
+ }
+}
\ No newline at end of file
diff --git a/scripts/playwright/pages/Dashboard.ts b/scripts/playwright/pages/Dashboard.ts
new file mode 100644
index 0000000000..53fd1d332d
--- /dev/null
+++ b/scripts/playwright/pages/Dashboard.ts
@@ -0,0 +1,36 @@
+// playwright-dev-page.ts
+import { expect, Locator, Page } from '@playwright/test';
+
+export class DashboardPage {
+ readonly project: any;
+ readonly page: Page;
+ readonly tablesSideBar: Locator;
+ readonly tabBar: Locator;
+
+ constructor(page: Page, project: any) {
+ this.page = page;
+ this.project = project;
+ this.tablesSideBar = page.locator('.nc-treeview-container');
+ this.tabBar = page.locator('.nc-tab-bar');
+ }
+
+ async goto() {
+ await this.page.goto(`http://localhost:3000/#/nc/${this.project.id}/auth`);
+ }
+
+ async openTable({ title }: { title: string }) {
+ await this.tablesSideBar.locator(`.nc-project-tree-tbl-${title}`).click();
+ await this.tabBar.textContent().then((text) => expect(text).toContain(title));
+ }
+
+ async createTable({ title }: { title: string }) {
+ await this.tablesSideBar.locator('.nc-add-new-table').click();
+
+ await this.page.locator('.ant-modal-body').waitFor()
+
+ await this.page.locator('[placeholder="Enter table name"]').fill(title);
+ await this.page.locator('button:has-text("Submit")').click();
+
+ await expect(this.page).toHaveURL(`http://localhost:3000/#/nc/${this.project.id}/table/${title}`);
+ }
+}
\ No newline at end of file
diff --git a/scripts/playwright/pages/Grid.ts b/scripts/playwright/pages/Grid.ts
new file mode 100644
index 0000000000..6f48e212d7
--- /dev/null
+++ b/scripts/playwright/pages/Grid.ts
@@ -0,0 +1,46 @@
+// playwright-dev-page.ts
+import { Locator, Page, expect } from '@playwright/test';
+import { CellPageObject } from './Cell';
+import { ColumnPageObject } from './Column';
+
+export class GridPage {
+ readonly page: Page;
+ readonly addNewTableButton: Locator;
+ readonly column: ColumnPageObject;
+ readonly cell: CellPageObject;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.addNewTableButton = page.locator('.nc-add-new-table');
+ this.column = new ColumnPageObject(page);
+ this.cell = new CellPageObject(page);
+ }
+
+ async addNewRow({index = 0, title}: {index?: number, title?: string} = {}) {
+ 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();
+
+
+ // 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();
+ }
+
+ async verifyRowDoesNotExist({index}: {index: number}) {
+ return expect(await this.page.locator(`td[data-pw="cell-Title-${index}"]`)).toBeHidden();
+ }
+
+ async deleteRow(index: number) {
+ await this.page.locator(`td[data-pw="cell-Title-${index}"]`).click({
+ button: 'right'
+ });
+
+ // Click text=Delete Row
+ await this.page.locator('text=Delete Row').click();
+ }
+
+}
\ No newline at end of file
diff --git a/scripts/playwright/playwright.config.ts b/scripts/playwright/playwright.config.ts
new file mode 100644
index 0000000000..2ddeaa2273
--- /dev/null
+++ b/scripts/playwright/playwright.config.ts
@@ -0,0 +1,107 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+import { devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: './tests',
+ /* Maximum time one test can run for. */
+ timeout: 30 * 1000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* 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,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: 'http://localhost:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+
+ {
+ name: 'firefox',
+ use: {
+ ...devices['Desktop Firefox'],
+ },
+ },
+
+ {
+ name: 'webkit',
+ use: {
+ ...devices['Desktop Safari'],
+ },
+ },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: {
+ // ...devices['Pixel 5'],
+ // },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: {
+ // ...devices['iPhone 12'],
+ // },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: {
+ // channel: 'msedge',
+ // },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: {
+ // channel: 'chrome',
+ // },
+ // },
+ ],
+
+ /* Folder for test artifacts such as screenshots, videos, traces, etc. */
+ // outputDir: 'test-results/',
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // port: 3000,
+ // },
+};
+
+export default config;
diff --git a/scripts/playwright/setup/index.ts b/scripts/playwright/setup/index.ts
new file mode 100644
index 0000000000..08b33d49f2
--- /dev/null
+++ b/scripts/playwright/setup/index.ts
@@ -0,0 +1,29 @@
+import { Page } from '@playwright/test';
+import axios from 'axios';
+import { DashboardPage } from '../pages/Dashboard';
+
+const setup = async ({page}: {page: Page}) => {
+ const response = await axios.get('http://localhost:8080/api/v1/meta/test/reset');
+ const token = response.data.token;
+
+ await page.addInitScript(async ({token}) => {
+ try {
+ window.localStorage.setItem('nocodb-gui-v2', JSON.stringify({
+ token: token,
+ }));
+ } catch (e) {
+ window.console.log(e);
+ }
+ }, { token: token });
+
+ const project = response.data.projects.find((project) => project.title === 'externalREST');
+
+ await page.goto(`/#/nc/${project.id}/auth`);
+
+ const dashboardPage = new DashboardPage(page, project);
+ await dashboardPage.openTable({title: "Country"})
+
+ return { project, token };
+}
+
+export default setup;
\ No newline at end of file
diff --git a/scripts/playwright/storageState.json b/scripts/playwright/storageState.json
new file mode 100644
index 0000000000..f4ec35503c
--- /dev/null
+++ b/scripts/playwright/storageState.json
@@ -0,0 +1,4 @@
+{
+ "cookies": [],
+ "origins": []
+}
\ No newline at end of file
diff --git a/scripts/playwright/tests-examples/demo-todo-app.spec.ts b/scripts/playwright/tests-examples/demo-todo-app.spec.ts
new file mode 100644
index 0000000000..5bba0ada68
--- /dev/null
+++ b/scripts/playwright/tests-examples/demo-todo-app.spec.ts
@@ -0,0 +1,398 @@
+import { test, expect, type Page } from '@playwright/test';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('https://demo.playwright.dev/todomvc');
+});
+
+const TODO_ITEMS = [
+ 'buy some cheese',
+ 'feed the cat',
+ 'book a doctors appointment'
+];
+
+test.describe('New Todo', () => {
+ test('should allow me to add todo items', async ({ page }) => {
+ // Create 1st todo.
+ await page.locator('.new-todo').fill(TODO_ITEMS[0]);
+ await page.locator('.new-todo').press('Enter');
+
+ // Make sure the list only has one todo item.
+ await expect(page.locator('.view label')).toHaveText([
+ TODO_ITEMS[0]
+ ]);
+
+ // Create 2nd todo.
+ await page.locator('.new-todo').fill(TODO_ITEMS[1]);
+ await page.locator('.new-todo').press('Enter');
+
+ // Make sure the list now has two todo items.
+ await expect(page.locator('.view label')).toHaveText([
+ TODO_ITEMS[0],
+ TODO_ITEMS[1]
+ ]);
+
+ await checkNumberOfTodosInLocalStorage(page, 2);
+ });
+
+ test('should clear text input field when an item is added', async ({ page }) => {
+ // Create one todo item.
+ await page.locator('.new-todo').fill(TODO_ITEMS[0]);
+ await page.locator('.new-todo').press('Enter');
+
+ // Check that input is empty.
+ await expect(page.locator('.new-todo')).toBeEmpty();
+ await checkNumberOfTodosInLocalStorage(page, 1);
+ });
+
+ test('should append new items to the bottom of the list', async ({ page }) => {
+ // Create 3 items.
+ await createDefaultTodos(page);
+
+ // Check test using different methods.
+ await expect(page.locator('.todo-count')).toHaveText('3 items left');
+ await expect(page.locator('.todo-count')).toContainText('3');
+ await expect(page.locator('.todo-count')).toHaveText(/3/);
+
+ // Check all items in one call.
+ await expect(page.locator('.view label')).toHaveText(TODO_ITEMS);
+ await checkNumberOfTodosInLocalStorage(page, 3);
+ });
+
+ test('should show #main and #footer when items added', async ({ page }) => {
+ await page.locator('.new-todo').fill(TODO_ITEMS[0]);
+ await page.locator('.new-todo').press('Enter');
+
+ await expect(page.locator('.main')).toBeVisible();
+ await expect(page.locator('.footer')).toBeVisible();
+ await checkNumberOfTodosInLocalStorage(page, 1);
+ });
+});
+
+test.describe('Mark all as completed', () => {
+ test.beforeEach(async ({ page }) => {
+ await createDefaultTodos(page);
+ await checkNumberOfTodosInLocalStorage(page, 3);
+ });
+
+ test.afterEach(async ({ page }) => {
+ await checkNumberOfTodosInLocalStorage(page, 3);
+ });
+
+ test('should allow me to mark all items as completed', async ({ page }) => {
+ // Complete all todos.
+ await page.locator('.toggle-all').check();
+
+ // Ensure all todos have 'completed' class.
+ await expect(page.locator('.todo-list li')).toHaveClass(['completed', 'completed', 'completed']);
+ await checkNumberOfCompletedTodosInLocalStorage(page, 3);
+ });
+
+ test('should allow me to clear the complete state of all items', async ({ page }) => {
+ // Check and then immediately uncheck.
+ await page.locator('.toggle-all').check();
+ await page.locator('.toggle-all').uncheck();
+
+ // Should be no completed classes.
+ await expect(page.locator('.todo-list li')).toHaveClass(['', '', '']);
+ });
+
+ test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
+ const toggleAll = page.locator('.toggle-all');
+ await toggleAll.check();
+ await expect(toggleAll).toBeChecked();
+ await checkNumberOfCompletedTodosInLocalStorage(page, 3);
+
+ // Uncheck first todo.
+ const firstTodo = page.locator('.todo-list li').nth(0);
+ await firstTodo.locator('.toggle').uncheck();
+
+ // Reuse toggleAll locator and make sure its not checked.
+ await expect(toggleAll).not.toBeChecked();
+
+ await firstTodo.locator('.toggle').check();
+ await checkNumberOfCompletedTodosInLocalStorage(page, 3);
+
+ // Assert the toggle all is checked again.
+ await expect(toggleAll).toBeChecked();
+ });
+});
+
+test.describe('Item', () => {
+
+ test('should allow me to mark items as complete', async ({ page }) => {
+ // Create two items.
+ for (const item of TODO_ITEMS.slice(0, 2)) {
+ await page.locator('.new-todo').fill(item);
+ await page.locator('.new-todo').press('Enter');
+ }
+
+ // Check first item.
+ const firstTodo = page.locator('.todo-list li').nth(0);
+ await firstTodo.locator('.toggle').check();
+ await expect(firstTodo).toHaveClass('completed');
+
+ // Check second item.
+ const secondTodo = page.locator('.todo-list li').nth(1);
+ await expect(secondTodo).not.toHaveClass('completed');
+ await secondTodo.locator('.toggle').check();
+
+ // Assert completed class.
+ await expect(firstTodo).toHaveClass('completed');
+ await expect(secondTodo).toHaveClass('completed');
+ });
+
+ test('should allow me to un-mark items as complete', async ({ page }) => {
+ // Create two items.
+ for (const item of TODO_ITEMS.slice(0, 2)) {
+ await page.locator('.new-todo').fill(item);
+ await page.locator('.new-todo').press('Enter');
+ }
+
+ const firstTodo = page.locator('.todo-list li').nth(0);
+ const secondTodo = page.locator('.todo-list li').nth(1);
+ await firstTodo.locator('.toggle').check();
+ await expect(firstTodo).toHaveClass('completed');
+ await expect(secondTodo).not.toHaveClass('completed');
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
+
+ await firstTodo.locator('.toggle').uncheck();
+ await expect(firstTodo).not.toHaveClass('completed');
+ await expect(secondTodo).not.toHaveClass('completed');
+ await checkNumberOfCompletedTodosInLocalStorage(page, 0);
+ });
+
+ test('should allow me to edit an item', async ({ page }) => {
+ await createDefaultTodos(page);
+
+ const todoItems = page.locator('.todo-list li');
+ const secondTodo = todoItems.nth(1);
+ await secondTodo.dblclick();
+ await expect(secondTodo.locator('.edit')).toHaveValue(TODO_ITEMS[1]);
+ await secondTodo.locator('.edit').fill('buy some sausages');
+ await secondTodo.locator('.edit').press('Enter');
+
+ // Explicitly assert the new text value.
+ await expect(todoItems).toHaveText([
+ TODO_ITEMS[0],
+ 'buy some sausages',
+ TODO_ITEMS[2]
+ ]);
+ await checkTodosInLocalStorage(page, 'buy some sausages');
+ });
+});
+
+test.describe('Editing', () => {
+ test.beforeEach(async ({ page }) => {
+ await createDefaultTodos(page);
+ await checkNumberOfTodosInLocalStorage(page, 3);
+ });
+
+ test('should hide other controls when editing', async ({ page }) => {
+ const todoItem = page.locator('.todo-list li').nth(1);
+ await todoItem.dblclick();
+ await expect(todoItem.locator('.toggle')).not.toBeVisible();
+ await expect(todoItem.locator('label')).not.toBeVisible();
+ await checkNumberOfTodosInLocalStorage(page, 3);
+ });
+
+ test('should save edits on blur', async ({ page }) => {
+ const todoItems = page.locator('.todo-list li');
+ await todoItems.nth(1).dblclick();
+ await todoItems.nth(1).locator('.edit').fill('buy some sausages');
+ await todoItems.nth(1).locator('.edit').dispatchEvent('blur');
+
+ await expect(todoItems).toHaveText([
+ TODO_ITEMS[0],
+ 'buy some sausages',
+ TODO_ITEMS[2],
+ ]);
+ await checkTodosInLocalStorage(page, 'buy some sausages');
+ });
+
+ test('should trim entered text', async ({ page }) => {
+ const todoItems = page.locator('.todo-list li');
+ await todoItems.nth(1).dblclick();
+ await todoItems.nth(1).locator('.edit').fill(' buy some sausages ');
+ await todoItems.nth(1).locator('.edit').press('Enter');
+
+ await expect(todoItems).toHaveText([
+ TODO_ITEMS[0],
+ 'buy some sausages',
+ TODO_ITEMS[2],
+ ]);
+ await checkTodosInLocalStorage(page, 'buy some sausages');
+ });
+
+ test('should remove the item if an empty text string was entered', async ({ page }) => {
+ const todoItems = page.locator('.todo-list li');
+ await todoItems.nth(1).dblclick();
+ await todoItems.nth(1).locator('.edit').fill('');
+ await todoItems.nth(1).locator('.edit').press('Enter');
+
+ await expect(todoItems).toHaveText([
+ TODO_ITEMS[0],
+ TODO_ITEMS[2],
+ ]);
+ });
+
+ test('should cancel edits on escape', async ({ page }) => {
+ const todoItems = page.locator('.todo-list li');
+ await todoItems.nth(1).dblclick();
+ await todoItems.nth(1).locator('.edit').press('Escape');
+ await expect(todoItems).toHaveText(TODO_ITEMS);
+ });
+});
+
+test.describe('Counter', () => {
+ test('should display the current number of todo items', async ({ page }) => {
+ await page.locator('.new-todo').fill(TODO_ITEMS[0]);
+ await page.locator('.new-todo').press('Enter');
+ await expect(page.locator('.todo-count')).toContainText('1');
+
+ await page.locator('.new-todo').fill(TODO_ITEMS[1]);
+ await page.locator('.new-todo').press('Enter');
+ await expect(page.locator('.todo-count')).toContainText('2');
+
+ await checkNumberOfTodosInLocalStorage(page, 2);
+ });
+});
+
+test.describe('Clear completed button', () => {
+ test.beforeEach(async ({ page }) => {
+ await createDefaultTodos(page);
+ });
+
+ test('should display the correct text', async ({ page }) => {
+ await page.locator('.todo-list li .toggle').first().check();
+ await expect(page.locator('.clear-completed')).toHaveText('Clear completed');
+ });
+
+ test('should remove completed items when clicked', async ({ page }) => {
+ const todoItems = page.locator('.todo-list li');
+ await todoItems.nth(1).locator('.toggle').check();
+ await page.locator('.clear-completed').click();
+ await expect(todoItems).toHaveCount(2);
+ await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
+ });
+
+ test('should be hidden when there are no items that are completed', async ({ page }) => {
+ await page.locator('.todo-list li .toggle').first().check();
+ await page.locator('.clear-completed').click();
+ await expect(page.locator('.clear-completed')).toBeHidden();
+ });
+});
+
+test.describe('Persistence', () => {
+ test('should persist its data', async ({ page }) => {
+ for (const item of TODO_ITEMS.slice(0, 2)) {
+ await page.locator('.new-todo').fill(item);
+ await page.locator('.new-todo').press('Enter');
+ }
+
+ const todoItems = page.locator('.todo-list li');
+ await todoItems.nth(0).locator('.toggle').check();
+ await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
+ await expect(todoItems).toHaveClass(['completed', '']);
+
+ // Ensure there is 1 completed item.
+ checkNumberOfCompletedTodosInLocalStorage(page, 1);
+
+ // Now reload.
+ await page.reload();
+ await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
+ await expect(todoItems).toHaveClass(['completed', '']);
+ });
+});
+
+test.describe('Routing', () => {
+ test.beforeEach(async ({ page }) => {
+ await createDefaultTodos(page);
+ // make sure the app had a chance to save updated todos in storage
+ // before navigating to a new view, otherwise the items can get lost :(
+ // in some frameworks like Durandal
+ await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
+ });
+
+ test('should allow me to display active items', async ({ page }) => {
+ await page.locator('.todo-list li .toggle').nth(1).check();
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
+ await page.locator('.filters >> text=Active').click();
+ await expect(page.locator('.todo-list li')).toHaveCount(2);
+ await expect(page.locator('.todo-list li')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
+ });
+
+ test('should respect the back button', async ({ page }) => {
+ await page.locator('.todo-list li .toggle').nth(1).check();
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
+
+ await test.step('Showing all items', async () => {
+ await page.locator('.filters >> text=All').click();
+ await expect(page.locator('.todo-list li')).toHaveCount(3);
+ });
+
+ await test.step('Showing active items', async () => {
+ await page.locator('.filters >> text=Active').click();
+ });
+
+ await test.step('Showing completed items', async () => {
+ await page.locator('.filters >> text=Completed').click();
+ });
+
+ await expect(page.locator('.todo-list li')).toHaveCount(1);
+ await page.goBack();
+ await expect(page.locator('.todo-list li')).toHaveCount(2);
+ await page.goBack();
+ await expect(page.locator('.todo-list li')).toHaveCount(3);
+ });
+
+ test('should allow me to display completed items', async ({ page }) => {
+ await page.locator('.todo-list li .toggle').nth(1).check();
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
+ await page.locator('.filters >> text=Completed').click();
+ await expect(page.locator('.todo-list li')).toHaveCount(1);
+ });
+
+ test('should allow me to display all items', async ({ page }) => {
+ await page.locator('.todo-list li .toggle').nth(1).check();
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
+ await page.locator('.filters >> text=Active').click();
+ await page.locator('.filters >> text=Completed').click();
+ await page.locator('.filters >> text=All').click();
+ await expect(page.locator('.todo-list li')).toHaveCount(3);
+ });
+
+ test('should highlight the currently applied filter', async ({ page }) => {
+ await expect(page.locator('.filters >> text=All')).toHaveClass('selected');
+ await page.locator('.filters >> text=Active').click();
+ // Page change - active items.
+ await expect(page.locator('.filters >> text=Active')).toHaveClass('selected');
+ await page.locator('.filters >> text=Completed').click();
+ // Page change - completed items.
+ await expect(page.locator('.filters >> text=Completed')).toHaveClass('selected');
+ });
+});
+
+async function createDefaultTodos(page: Page) {
+ for (const item of TODO_ITEMS) {
+ await page.locator('.new-todo').fill(item);
+ await page.locator('.new-todo').press('Enter');
+ }
+}
+
+async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
+ return await page.waitForFunction(e => {
+ return JSON.parse(localStorage['react-todos']).length === e;
+ }, expected);
+}
+
+async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
+ return await page.waitForFunction(e => {
+ return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
+ }, expected);
+}
+
+async function checkTodosInLocalStorage(page: Page, title: string) {
+ return await page.waitForFunction(t => {
+ return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
+ }, title);
+}
diff --git a/scripts/playwright/tests/multiSelect.spec.ts b/scripts/playwright/tests/multiSelect.spec.ts
new file mode 100644
index 0000000000..eddeb78cb2
--- /dev/null
+++ b/scripts/playwright/tests/multiSelect.spec.ts
@@ -0,0 +1,65 @@
+import { Page, test } from '@playwright/test';
+import { DashboardPage } from '../pages/Dashboard';
+import { GridPage } from '../pages/Grid';
+import setup from '../setup';
+
+
+test.describe.serial('Multi select', () => {
+ let dashboard: DashboardPage, grid: GridPage;
+ let context: any;
+
+ test.beforeEach(async ({page}) => {
+ context = await setup({ page });
+ dashboard = new DashboardPage(page, context.project);
+ await dashboard.createTable({ title: 'sheet1' });
+
+ grid = new GridPage(page);
+ await grid.column.create({ title: 'MultiSelect', type: 'MultiSelect' });
+ await grid.addNewRow({index: 0, title: "Row 0"});
+ })
+
+ test('Select and clear options and rename options', async () => {
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'MultiSelect', option: 'Option 1', multiSelect: true});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'MultiSelect', option: 'Option 1', multiSelect: true});
+
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'MultiSelect', option: 'Option 2', multiSelect: true});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'MultiSelect', option: 'Option 2', multiSelect: true});
+
+ await grid.addNewRow({index: 0, title: "Row 0"});
+ await grid.cell.selectOption.select({index: 1, columnHeader: 'MultiSelect', option: 'Option 1', multiSelect: true});
+
+ await grid.cell.selectOption.clear({index: 0, columnHeader: 'MultiSelect', multiSelect: true});
+ await grid.cell.click({index: 0, columnHeader: 'MultiSelect'});
+
+ await grid.column.selectOption.addOption({index: 2, option: 'Option 3', columnTitle: 'MultiSelect'});
+
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'MultiSelect', option: 'Option 3', multiSelect: true});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'MultiSelect', option: 'Option 3', multiSelect: true});
+
+ await grid.column.selectOption.editOption({index: 2, columnTitle: 'MultiSelect', newOption: 'New Option 3'});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'MultiSelect', option: 'New Option 3', multiSelect: true});
+
+ await grid.cell.selectOption.verifyOptions({index: 0, columnHeader: 'MultiSelect', options: ['Option 1', 'Option 2', 'New Option 3']});
+
+ await grid.deleteRow(0);
+ await grid.deleteRow(0);
+ await grid.verifyRowDoesNotExist({index: 0});
+ });
+
+ test('Remove a option, reorder option and delete the column', async () => {
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'MultiSelect', option: 'Option 1', multiSelect: true});
+ await grid.column.selectOption.addOption({index: 2, option: 'Option 3', columnTitle: 'MultiSelect'});
+
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'MultiSelect', option: 'Option 3', multiSelect: true});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'MultiSelect', option: 'Option 3', multiSelect: true});
+
+ await grid.column.selectOption.deleteOption({index: 2, columnTitle: 'MultiSelect'});
+ await grid.cell.selectOption.verifyNoOptionsSelected({index: 0, columnHeader: 'MultiSelect'});
+
+ await grid.column.selectOption.reorderOption({sourceOption: "Option 1", columnTitle: 'MultiSelect', destinationOption: "Option 2"});
+ await grid.cell.selectOption.verifyOptions({index: 0, columnHeader: 'MultiSelect', options: ['Option 2', 'Option 1']});
+
+ await grid.column.delete({title: 'MultiSelect'});
+ });
+
+});
diff --git a/scripts/playwright/tests/singleSelect.spec.ts b/scripts/playwright/tests/singleSelect.spec.ts
new file mode 100644
index 0000000000..e081198531
--- /dev/null
+++ b/scripts/playwright/tests/singleSelect.spec.ts
@@ -0,0 +1,61 @@
+import { Page, test } from '@playwright/test';
+import { DashboardPage } from '../pages/Dashboard';
+import { GridPage } from '../pages/Grid';
+import setup from '../setup';
+
+
+test.describe.serial('Single select', () => {
+ let dashboard: DashboardPage, grid: GridPage;
+ let context: any;
+
+ test.beforeEach(async ({page}) => {
+ context = await setup({ page });
+ dashboard = new DashboardPage(page, context.project);
+ await dashboard.createTable({ title: 'sheet1' });
+
+ grid = new GridPage(page);
+ await grid.column.create({ title: 'SingleSelect', type: 'SingleSelect' });
+ await grid.addNewRow({index: 0, title: "Row 0"});
+ })
+
+ test('Select and clear options and rename options', async () => {
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'SingleSelect', option: 'Option 1'});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'SingleSelect', option: 'Option 1'});
+
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'SingleSelect', option: 'Option 2'});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'SingleSelect', option: 'Option 2'});
+
+ await grid.cell.selectOption.clear({index: 0, columnHeader: 'SingleSelect'});
+ await grid.cell.click({index: 0, columnHeader: 'SingleSelect'});
+
+ await grid.column.selectOption.addOption({index: 2, option: 'Option 3', columnTitle: 'SingleSelect'});
+
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'SingleSelect', option: 'Option 3'});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'SingleSelect', option: 'Option 3'});
+
+ await grid.column.selectOption.editOption({index: 2, columnTitle: 'SingleSelect', newOption: 'New Option 3'});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'SingleSelect', option: 'New Option 3'});
+
+ await grid.cell.selectOption.verifyOptions({index: 0, columnHeader: 'SingleSelect', options: ['Option 1', 'Option 2', 'New Option 3']});
+
+ await grid.deleteRow(0);
+ await grid.verifyRowDoesNotExist({index: 0});
+ });
+
+ test('Remove a option, reorder option and delete the column', async () => {
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'SingleSelect', option: 'Option 1'});
+ await grid.column.selectOption.addOption({index: 2, option: 'Option 3', columnTitle: 'SingleSelect'});
+
+ await grid.cell.selectOption.select({index: 0, columnHeader: 'SingleSelect', option: 'Option 3'});
+ await grid.cell.selectOption.verify({index: 0, columnHeader: 'SingleSelect', option: 'Option 3'});
+
+ await grid.column.selectOption.deleteOption({index: 2, columnTitle: 'SingleSelect'});
+ await grid.cell.selectOption.verifyNoOptionsSelected({index: 0, columnHeader: 'SingleSelect'});
+
+ await grid.column.selectOption.reorderOption({sourceOption: "Option 1", columnTitle: 'SingleSelect', destinationOption: "Option 2"});
+ await grid.cell.selectOption.verifyOptions({index: 0, columnHeader: 'SingleSelect', options: ['Option 2', 'Option 1']});
+
+ await grid.column.delete({title: 'SingleSelect'});
+ });
+
+});