mirror of https://github.com/nocodb/nocodb
21 changed files with 1138 additions and 5 deletions
@ -0,0 +1,4 @@ |
|||||||
|
node_modules/ |
||||||
|
/test-results/ |
||||||
|
/playwright-report/ |
||||||
|
/playwright/.cache/ |
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
} |
@ -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)); |
||||||
|
} |
||||||
|
} |
@ -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'}); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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}); |
||||||
|
} |
||||||
|
} |
@ -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'}); |
||||||
|
} |
||||||
|
} |
@ -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}`); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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; |
@ -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; |
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"cookies": [], |
||||||
|
"origins": [] |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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'}); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
@ -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'}); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
Loading…
Reference in new issue