mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-02 02:57:23 +00:00
feat(testing): Added and integrated playwright
This commit is contained in:
@@ -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')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
@@ -126,6 +126,7 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
|
||||
<div
|
||||
class="nc-cell w-full"
|
||||
:class="[`nc-cell-${(column?.uidt || 'default').toLowerCase()}`, { 'text-blue-600': isPrimary && !virtual && !isForm }]"
|
||||
:data-pw="`cell-${column?.title}-${rowIndex}`"
|
||||
@keydown.enter.exact="syncAndNavigate(NavigateDir.NEXT, $event)"
|
||||
@keydown.shift.enter.exact="syncAndNavigate(NavigateDir.PREV, $event)"
|
||||
>
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -124,7 +124,7 @@ onMounted(() => {
|
||||
:class="{ '!w-[600px]': formState.uidt === UITypes.Formula }"
|
||||
@click.stop
|
||||
>
|
||||
<a-form v-model="formState" no-style name="column-create-or-edit" layout="vertical">
|
||||
<a-form v-model="formState" no-style name="column-create-or-edit" layout="vertical" data-pw="add-or-edit-column">
|
||||
<div class="flex flex-col gap-2">
|
||||
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
|
||||
<a-input
|
||||
|
||||
@@ -133,7 +133,12 @@ watch(inputs, () => {
|
||||
<Draggable :list="options" item-key="id" handle=".nc-child-draggable-icon">
|
||||
<template #item="{ element, index }">
|
||||
<div class="flex py-1 items-center nc-select-option">
|
||||
<MdiDragVertical v-if="!isKanban" small class="nc-child-draggable-icon handle" />
|
||||
<MdiDragVertical
|
||||
v-if="!isKanban"
|
||||
small
|
||||
class="nc-child-draggable-icon handle"
|
||||
:data-pw="`select-option-column-handle-icon-${element.title}`"
|
||||
/>
|
||||
<a-dropdown
|
||||
v-model:visible="colorMenus[index]"
|
||||
:trigger="['click']"
|
||||
@@ -153,9 +158,20 @@ watch(inputs, () => {
|
||||
/>
|
||||
</a-dropdown>
|
||||
|
||||
<a-input ref="inputs" v-model:value="element.title" class="caption" @change="optionChanged(element.id)" />
|
||||
<a-input
|
||||
ref="inputs"
|
||||
v-model:value="element.title"
|
||||
class="caption"
|
||||
:data-pw="`select-column-option-input-${index}`"
|
||||
@change="optionChanged(element.id)"
|
||||
/>
|
||||
|
||||
<MdiClose class="ml-2 hover:!text-black" :style="{ color: 'red' }" @click="removeOption(index)" />
|
||||
<MdiClose
|
||||
class="ml-2 hover:!text-black"
|
||||
:style="{ color: 'red' }"
|
||||
:data-pw="`select-column-option-remove-${index}`"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
|
||||
4
scripts/playwright/.gitignore
vendored
Normal file
4
scripts/playwright/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
119
scripts/playwright/package-lock.json
generated
Normal file
119
scripts/playwright/package-lock.json
generated
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
14
scripts/playwright/package.json
Normal file
14
scripts/playwright/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
19
scripts/playwright/pages/Base.ts
Normal file
19
scripts/playwright/pages/Base.ts
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
70
scripts/playwright/pages/Cell/SelectOptionCell.ts
Normal file
70
scripts/playwright/pages/Cell/SelectOptionCell.ts
Normal file
@@ -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'});
|
||||
}
|
||||
}
|
||||
20
scripts/playwright/pages/Cell/index.ts
Normal file
20
scripts/playwright/pages/Cell/index.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
53
scripts/playwright/pages/Column/SelectOptionColumn.ts
Normal file
53
scripts/playwright/pages/Column/SelectOptionColumn.ts
Normal file
@@ -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});
|
||||
}
|
||||
}
|
||||
69
scripts/playwright/pages/Column/index.ts
Normal file
69
scripts/playwright/pages/Column/index.ts
Normal file
@@ -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'});
|
||||
}
|
||||
}
|
||||
36
scripts/playwright/pages/Dashboard.ts
Normal file
36
scripts/playwright/pages/Dashboard.ts
Normal file
@@ -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}`);
|
||||
}
|
||||
}
|
||||
46
scripts/playwright/pages/Grid.ts
Normal file
46
scripts/playwright/pages/Grid.ts
Normal file
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
107
scripts/playwright/playwright.config.ts
Normal file
107
scripts/playwright/playwright.config.ts
Normal file
@@ -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;
|
||||
29
scripts/playwright/setup/index.ts
Normal file
29
scripts/playwright/setup/index.ts
Normal file
@@ -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;
|
||||
4
scripts/playwright/storageState.json
Normal file
4
scripts/playwright/storageState.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": []
|
||||
}
|
||||
398
scripts/playwright/tests-examples/demo-todo-app.spec.ts
Normal file
398
scripts/playwright/tests-examples/demo-todo-app.spec.ts
Normal file
@@ -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);
|
||||
}
|
||||
65
scripts/playwright/tests/multiSelect.spec.ts
Normal file
65
scripts/playwright/tests/multiSelect.spec.ts
Normal file
@@ -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'});
|
||||
});
|
||||
|
||||
});
|
||||
61
scripts/playwright/tests/singleSelect.spec.ts
Normal file
61
scripts/playwright/tests/singleSelect.spec.ts
Normal file
@@ -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'});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user