mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 05:05:44 +00:00
feat(testing): Improved flakyness
This commit is contained in:
@@ -26,42 +26,34 @@ export default abstract class BasePage {
|
||||
}
|
||||
|
||||
async waitForResponse({
|
||||
requestHttpMethod,
|
||||
uiAction,
|
||||
httpMethodsToMatch = [],
|
||||
requestUrlPathToMatch,
|
||||
responseJsonMatcher,
|
||||
}: {
|
||||
requestHttpMethod: string;
|
||||
uiAction: Promise<any>;
|
||||
requestUrlPathToMatch: string;
|
||||
httpMethodsToMatch?: string[];
|
||||
responseJsonMatcher?: ResponseSelector;
|
||||
}) {
|
||||
await this.rootPage.waitForResponse(async (res) => {
|
||||
let isResJsonMatched = true;
|
||||
if(responseJsonMatcher){
|
||||
try {
|
||||
isResJsonMatched = responseJsonMatcher(await res.json());
|
||||
} catch (e) {
|
||||
return false;
|
||||
await Promise.all([
|
||||
this.rootPage.waitForResponse(async (res) => {
|
||||
let isResJsonMatched = true;
|
||||
if(responseJsonMatcher){
|
||||
try {
|
||||
isResJsonMatched = responseJsonMatcher(await res.json());
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
res.request().method() === requestHttpMethod &&
|
||||
res.request().url().includes(requestUrlPathToMatch) &&
|
||||
isResJsonMatched
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async waitForResponseJson({
|
||||
responseSelector,
|
||||
}: {
|
||||
responseSelector: ResponseSelector;
|
||||
}) {
|
||||
await this.rootPage.waitForResponse(async (res) => {
|
||||
try {
|
||||
return responseSelector(await res.json());
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return (
|
||||
res.request().url().includes(requestUrlPathToMatch) &&
|
||||
httpMethodsToMatch.includes(res.request().method()) &&
|
||||
isResJsonMatched
|
||||
);
|
||||
}),
|
||||
uiAction,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,30 @@ export class GridPage extends BasePage {
|
||||
return expect(await this.get().locator(".nc-grid-row").count()).toBe(count);
|
||||
}
|
||||
|
||||
private async _fillRow({
|
||||
index,
|
||||
columnHeader,
|
||||
value,
|
||||
}: {
|
||||
index: number;
|
||||
columnHeader: string;
|
||||
value: string;
|
||||
}) {
|
||||
const cell = this.cell.get({ index, columnHeader });
|
||||
await this.cell.dblclick({
|
||||
index,
|
||||
columnHeader,
|
||||
});
|
||||
|
||||
await cell.locator("input").fill(value);
|
||||
}
|
||||
|
||||
async addNewRow({
|
||||
index = 0,
|
||||
columnHeader = "Title",
|
||||
value,
|
||||
}: { index?: number; columnHeader?: string; value?: string } = {}) {
|
||||
const rowValue = value ?? `Row ${index}`;
|
||||
const rowCount = await this.get().locator(".nc-grid-row").count();
|
||||
await this.get().locator(".nc-grid-add-new-cell").click();
|
||||
|
||||
@@ -51,30 +70,41 @@ export class GridPage extends BasePage {
|
||||
.poll(async () => await this.get().locator(".nc-grid-row").count())
|
||||
.toBe(rowCount + 1);
|
||||
|
||||
await this.editRow({ index, columnHeader, value });
|
||||
await this._fillRow({ index, columnHeader, value: rowValue });
|
||||
|
||||
const clickOnColumnHeaderToSave = this.cell.grid
|
||||
.get()
|
||||
.locator(`[data-title="${columnHeader}"]`)
|
||||
.locator(`span[title="${columnHeader}"]`)
|
||||
.click();
|
||||
|
||||
await this.waitForResponse({
|
||||
uiAction: clickOnColumnHeaderToSave,
|
||||
requestUrlPathToMatch: "api/v1/db/data/noco",
|
||||
httpMethodsToMatch:[ "POST"],
|
||||
responseJsonMatcher: (resJson) => resJson?.[columnHeader] === value,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async editRow({
|
||||
index = 0,
|
||||
columnHeader = "Title",
|
||||
value,
|
||||
}: { index?: number; columnHeader?: string; value?: string } = {}) {
|
||||
const cell = this.cell.get({ index, columnHeader });
|
||||
await this.cell.dblclick({
|
||||
index,
|
||||
columnHeader,
|
||||
});
|
||||
}: { index?: number; columnHeader?: string; value: string }) {
|
||||
await this._fillRow({ index, columnHeader, value });
|
||||
|
||||
await cell.locator("input").fill(value ?? `Row ${index}`);
|
||||
const clickOnColumnHeaderToSave = this.cell.grid
|
||||
.get()
|
||||
.locator(`[data-title="${columnHeader}"]`)
|
||||
.locator(`span[title="${columnHeader}"]`)
|
||||
.click();
|
||||
|
||||
await this.cell.grid
|
||||
.get()
|
||||
.locator(`[data-title="${columnHeader}"]`)
|
||||
.locator(`span[title="${columnHeader}"]`)
|
||||
.click();
|
||||
|
||||
await this.waitForResponseJson({
|
||||
responseSelector: (resJson) => resJson?.[columnHeader] === value,
|
||||
await this.waitForResponse({
|
||||
uiAction: clickOnColumnHeaderToSave,
|
||||
requestUrlPathToMatch: "api/v1/db/data/noco",
|
||||
httpMethodsToMatch: ["PATCH"],
|
||||
responseJsonMatcher: (resJson) => resJson?.[columnHeader] === value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,10 +206,12 @@ export class GridPage extends BasePage {
|
||||
|
||||
async clickPagination({ page }: { page: string }) {
|
||||
(await this.pagination({ page })).click();
|
||||
|
||||
await this.waitForResponseJson({
|
||||
responseSelector: (resJson) => resJson?.pageInfo,
|
||||
});
|
||||
await this.waitForResponse({
|
||||
uiAction: (await this.pagination({ page })).click(),
|
||||
httpMethodsToMatch: ["GET"],
|
||||
requestUrlPathToMatch: "/views/",
|
||||
responseJsonMatcher: (resJson) => resJson?.pageInfo,
|
||||
})
|
||||
|
||||
await this.waitLoading();
|
||||
}
|
||||
|
||||
@@ -30,11 +30,9 @@ export class TreeViewPage extends BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
await this.get().locator(`.nc-project-tree-tbl-${title}`).click({
|
||||
noWaitAfter: true,
|
||||
});
|
||||
await this.waitForResponse({
|
||||
requestHttpMethod: "GET",
|
||||
uiAction: this.get().locator(`.nc-project-tree-tbl-${title}`).click(),
|
||||
httpMethodsToMatch: ["GET"],
|
||||
requestUrlPathToMatch: `/api/v1/db/meta/tables/`,
|
||||
responseJsonMatcher: (json) => json.title === title,
|
||||
});
|
||||
@@ -50,9 +48,13 @@ export class TreeViewPage extends BasePage {
|
||||
.get()
|
||||
.locator('[placeholder="Enter table name"]')
|
||||
.fill(title);
|
||||
|
||||
await this.dashboard.get().locator('button:has-text("Submit")').click(),
|
||||
await this.waitForResponseJson({responseSelector:(json) => json.title === title && json.type === 'table'}),
|
||||
|
||||
await this.waitForResponse({
|
||||
uiAction: this.dashboard.get().locator('button:has-text("Submit")').click(),
|
||||
httpMethodsToMatch: ["POST"],
|
||||
requestUrlPathToMatch: `/api/v1/db/meta/projects/`,
|
||||
responseJsonMatcher: (json) => json.title === title && json.type === 'table',
|
||||
});
|
||||
|
||||
|
||||
await this.dashboard.waitForTabRender({ title });
|
||||
@@ -77,7 +79,7 @@ export class TreeViewPage extends BasePage {
|
||||
}
|
||||
|
||||
async deleteTable({ title }: { title: string }) {
|
||||
const tabCount = await this.rootPage.locator('.nc-container').count()
|
||||
const tabCount = await this.dashboard.tabBar.locator('.ant-tabs-tab').count()
|
||||
await this.get()
|
||||
.locator(`.nc-project-tree-tbl-${title}`)
|
||||
.click({ button: "right" });
|
||||
@@ -85,13 +87,13 @@ export class TreeViewPage extends BasePage {
|
||||
.get()
|
||||
.locator('div.nc-project-menu-item:has-text("Delete")')
|
||||
.click();
|
||||
await this.dashboard.get().locator('button:has-text("Yes")').click();
|
||||
// await this.toastWait({ message: "Deleted table successfully" });
|
||||
|
||||
await this.waitForResponse({
|
||||
requestHttpMethod: "DELETE",
|
||||
uiAction: this.dashboard.get().locator('button:has-text("Yes")').click(),
|
||||
httpMethodsToMatch: ["DELETE"],
|
||||
requestUrlPathToMatch: `/api/v1/db/meta/tables/`,
|
||||
});
|
||||
await expect.poll(async () => await this.rootPage.locator('.nc-container').count() === tabCount - 1).toBe(true);
|
||||
await expect.poll(async () => await this.dashboard.tabBar.locator('.ant-tabs-tab').count()).toBe(tabCount - 1);
|
||||
|
||||
(await this.rootPage.locator('.nc-container').last().elementHandle())?.waitForElementState('stable');
|
||||
}
|
||||
|
||||
@@ -34,11 +34,19 @@ export class ViewSidebarPage extends BasePage {
|
||||
await this.rootPage
|
||||
.locator('input[id="form_item_title"]:visible')
|
||||
.fill(title);
|
||||
await this.rootPage
|
||||
const submitAction = this.rootPage
|
||||
.locator(".ant-modal-content")
|
||||
.locator('button:has-text("Submit"):visible')
|
||||
.click();
|
||||
await this.waitForResponse({
|
||||
httpMethodsToMatch: ["POST"],
|
||||
requestUrlPathToMatch: "/api/v1/db/meta/tables/",
|
||||
uiAction: submitAction,
|
||||
responseJsonMatcher: (json) => json.title === title,
|
||||
});
|
||||
await this.toastWait({ message: "View created successfully" });
|
||||
// Todo: Wait for view to be rendered
|
||||
await this.rootPage.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async createGalleryView({ title }: { title: string }) {
|
||||
|
||||
@@ -13,14 +13,17 @@ export class ToolbarFilterPage extends BasePage {
|
||||
return this.rootPage.locator(`[pw-data="nc-filter-menu"]`);
|
||||
}
|
||||
|
||||
// Todo: Handle the case of operator does not need a value
|
||||
async addNew({
|
||||
columnTitle,
|
||||
opType,
|
||||
value,
|
||||
isLocallySaved,
|
||||
}: {
|
||||
columnTitle: string;
|
||||
opType: string;
|
||||
value: string;
|
||||
isLocallySaved: boolean;
|
||||
}) {
|
||||
await this.toolbar.clickFilter();
|
||||
|
||||
@@ -38,12 +41,21 @@ export class ToolbarFilterPage extends BasePage {
|
||||
.locator(`.ant-select-item:has-text("${opType}")`)
|
||||
.click();
|
||||
|
||||
await this.rootPage.locator(".nc-filter-value-select").last().fill(value);
|
||||
const fillFilter = this.rootPage.locator(".nc-filter-value-select").last().fill(value);
|
||||
|
||||
await this.waitForResponse({
|
||||
requestHttpMethod: "POST",
|
||||
requestUrlPathToMatch: "/filters",
|
||||
})
|
||||
if (isLocallySaved) {
|
||||
await this.waitForResponse({
|
||||
uiAction: fillFilter,
|
||||
httpMethodsToMatch: ["GET"],
|
||||
requestUrlPathToMatch: `${value.replace(' ', '+')}`,
|
||||
});
|
||||
} else {
|
||||
await this.waitForResponse({
|
||||
uiAction: fillFilter,
|
||||
httpMethodsToMatch: ["POST", "PATCH"],
|
||||
requestUrlPathToMatch: "/filters",
|
||||
});
|
||||
}
|
||||
|
||||
await this.toolbar.clickFilter();
|
||||
}
|
||||
@@ -57,9 +69,9 @@ export class ToolbarFilterPage extends BasePage {
|
||||
|
||||
async resetFilter() {
|
||||
await this.toolbar.clickFilter();
|
||||
await this.get().locator(".nc-filter-item-remove-btn").click();
|
||||
await this.waitForResponse({
|
||||
requestHttpMethod: "DELETE",
|
||||
uiAction: this.get().locator(".nc-filter-item-remove-btn").click(),
|
||||
httpMethodsToMatch: ["DELETE"],
|
||||
requestUrlPathToMatch: "/api/v1/db/meta/filters/",
|
||||
})
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ export class ToolbarSortPage extends BasePage {
|
||||
async addSort({
|
||||
columnTitle,
|
||||
isAscending,
|
||||
isLocallySaved,
|
||||
}: {
|
||||
columnTitle: string;
|
||||
isAscending: boolean;
|
||||
isLocallySaved: boolean;
|
||||
}) {
|
||||
// open sort menu
|
||||
await this.toolbar.clickSort();
|
||||
@@ -33,11 +35,26 @@ export class ToolbarSortPage extends BasePage {
|
||||
.click();
|
||||
|
||||
await this.rootPage.locator(".nc-sort-dir-select").last().click();
|
||||
await this.rootPage
|
||||
const selectSortDirection = this.rootPage
|
||||
.locator(".nc-dropdown-sort-dir")
|
||||
.locator(".ant-select-item")
|
||||
.nth(isAscending ? 0 : 1)
|
||||
.click();
|
||||
|
||||
if(isLocallySaved) {
|
||||
await this.waitForResponse({
|
||||
uiAction: selectSortDirection,
|
||||
httpMethodsToMatch: ["GET"],
|
||||
requestUrlPathToMatch: `${isAscending ? "asc" : "desc"}`,
|
||||
})
|
||||
} else {
|
||||
await this.waitForResponse({
|
||||
uiAction: selectSortDirection,
|
||||
httpMethodsToMatch: ["POST", "PATCH"],
|
||||
requestUrlPathToMatch: "/sorts",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// close sort menu
|
||||
await this.toolbar.clickSort();
|
||||
|
||||
@@ -75,6 +75,21 @@ export class DashboardPage extends BasePage {
|
||||
async waitForTabRender({ title }: { title: string }) {
|
||||
await this.get().locator('[pw-data="grid-id-column"]').waitFor();
|
||||
|
||||
await this.tabBar
|
||||
.locator(`.ant-tabs-tab-active:has-text("${title}")`)
|
||||
.waitFor();
|
||||
|
||||
// wait active tab animation to finish
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await this.tabBar
|
||||
.locator(`[data-pw="nc-root-tabs-${title}"]`)
|
||||
.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue("color");
|
||||
});
|
||||
})
|
||||
.toBe("rgb(67, 81, 232)"); // active tab text color
|
||||
|
||||
await this.get()
|
||||
.locator('[pw-data="grid-load-spinner"]')
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
@@ -13,7 +13,7 @@ require('dotenv').config();
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: process.env.CI ? 80 * 1000 : 50 * 1000,
|
||||
timeout: process.env.CI ? 80 * 1000 : 65 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
|
||||
@@ -11,7 +11,7 @@ test.describe("LTAR create & update", () => {
|
||||
dashboard = new DashboardPage(page, context.project);
|
||||
});
|
||||
|
||||
test("LTAR", async () => {
|
||||
test.only("LTAR", async () => {
|
||||
// close 'Team & Auth' tab
|
||||
await dashboard.closeTab({ title: "Team & Auth" });
|
||||
|
||||
|
||||
@@ -36,12 +36,14 @@ test.describe("Shared view", () => {
|
||||
await dashboard.grid.toolbar.sort.addSort({
|
||||
columnTitle: "District",
|
||||
isAscending: false,
|
||||
isLocallySaved: false
|
||||
});
|
||||
// filter
|
||||
await dashboard.grid.toolbar.filter.addNew({
|
||||
columnTitle: "Address",
|
||||
value: "Ab",
|
||||
opType: "is like",
|
||||
isLocallySaved: false
|
||||
});
|
||||
|
||||
mainPageLink = page.url();
|
||||
@@ -123,11 +125,13 @@ test.describe("Shared view", () => {
|
||||
await sharedPage.grid.toolbar.sort.addSort({
|
||||
columnTitle: "Address",
|
||||
isAscending: true,
|
||||
isLocallySaved: true,
|
||||
});
|
||||
await sharedPage.grid.toolbar.filter.addNew({
|
||||
columnTitle: "District",
|
||||
value: "Ta",
|
||||
opType: "is like",
|
||||
isLocallySaved: true,
|
||||
});
|
||||
await sharedPage.grid.toolbar.fields.toggle({ title: "LastUpdate" });
|
||||
expectedColumns[6].isVisible = false;
|
||||
@@ -228,6 +232,7 @@ test.describe("Shared view", () => {
|
||||
columnTitle: "Country",
|
||||
value: "New Country",
|
||||
opType: "is like",
|
||||
isLocallySaved: true,
|
||||
});
|
||||
await sharedPage2.grid.cell.verify({
|
||||
index: 0,
|
||||
|
||||
Reference in New Issue
Block a user