From 8e608fc2bcef2c1dae981a25667c16a8e1684e97 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Mon, 6 Jul 2026 15:59:55 +0200 Subject: [PATCH 1/4] test(datagrid-web): add page object model --- docs/requirements/e2e-test-guidelines.md | 59 +++++++ .../datagrid-web/e2e/DataGrid.spec.js | 152 ++++++++-------- .../e2e/DataGridSelection.spec.js | 71 ++++---- .../DataGridFilteringEmptyString.spec.js | 31 ++-- .../DataGridFilteringIntegration.spec.js | 55 +++--- .../filtering/DataGridFilteringMulti.spec.js | 91 ++++------ .../filtering/DataGridFilteringSingle.spec.js | 163 ++++++++---------- .../datagrid-web/e2e/pages/DataGridPage.js | 117 +++++++++++++ 8 files changed, 422 insertions(+), 317 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js diff --git a/docs/requirements/e2e-test-guidelines.md b/docs/requirements/e2e-test-guidelines.md index efbdfcc3d6..2ffa2d2423 100644 --- a/docs/requirements/e2e-test-guidelines.md +++ b/docs/requirements/e2e-test-guidelines.md @@ -67,6 +67,63 @@ page.locator(".mx-name-myForm").getByRole("button", { name: "Save" }); page.locator(".mx-name-myWidget").getByLabel("Start date"); ``` +## Page Object Model + +For widgets with more than a handful of specs — or any spec that repeats the same locators/actions — encapsulate the widget's locators and interactions in a Page Object instead of scattering raw selectors across tests. This keeps specs reading as behavior and gives one place to update when the DOM changes. + +**Where it lives:** widget-local, at `e2e/pages/Page.js`. Keep it per-widget — do not add shared POMs to `@mendix/run-e2e` (widgets are isolated; see repo-layout). Reference: `packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js`. + +**Conventions:** + +- **Scope to one widget instance by `mx-name`.** The constructor takes `(page, name)` and builds a `root` locator (`.mx-name-`); default the `name` to the most common instance. All locators derive from `root` so tests never hard-code `.mx-name-*` strings. +- **`open(path)` wraps `page.goto(path)` only.** The custom fixture already auto-waits for the app (see Imports & Setup) — do **not** add `waitForMendixApp` inside the POM. +- **Expose locators as getters/methods; keep assertions in the spec.** The POM returns locators (e.g. `get rows()`, `columnCells(n)`); the test does the `expect(...)`. This preserves auto-retrying web-first assertions and keeps the POM assertion-free. +- **Actions are verbs** (`sortByColumn(n)`, `openColumnSelector()`); **locators are nouns** (`columnHeaders`, `cells`). Prefer `.mx-name-*` and role/label composition inside the POM, following Locator Patterns above. + +```javascript +// e2e/pages/MyWidgetPage.js +export class MyWidgetPage { + constructor(page, name = "myWidget1") { + this.page = page; + this.root = page.locator(`.mx-name-${name}`); + } + + // fixture auto-waits after goto — no waitForMendixApp here + async open(path = "/") { + await this.page.goto(path); + } + + get rows() { + return this.root.locator('[role="row"]'); + } + + async submit() { + await this.root.getByRole("button", { name: "Save" }).click(); + } +} +``` + +```javascript +// e2e/MyWidget.spec.js +import { test, expect } from "@mendix/run-e2e/fixtures"; +import { MyWidgetPage } from "./pages/MyWidgetPage"; + +test.describe("MyWidget", () => { + /** @type {MyWidgetPage} */ + let widget; + + test.beforeEach(async ({ page }) => { + widget = new MyWidgetPage(page); + await widget.open("/"); + }); + + test("submits the form @smoke", async () => { + await widget.submit(); + await expect(widget.rows).toHaveCount(3); // assertion stays in the spec + }); +}); +``` + ## Screenshot Testing - No per-test `{ threshold: N }` or `{ maxDiffPixels: N }` overrides — use global config (`threshold: 0.1`) @@ -85,6 +142,8 @@ playwright/prefer-web-first-assertions: warn ## Spec File Template +Minimal template for a small widget. For widgets with many specs or repeated locators, use a Page Object instead (see Page Object Model above). + ```javascript import { test, expect } from "@mendix/run-e2e/fixtures"; diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js index 8caa1f8bad..c77c9bdf0b 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js @@ -1,16 +1,16 @@ import AxeBuilder from "@axe-core/playwright"; -import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; import { expect, test } from "@mendix/run-e2e/fixtures"; import path from "path"; import * as XLSX from "xlsx"; +import { DataGridPage } from "./pages/DataGridPage"; test.describe("datagrid-web export to Excel", () => { test("check if export to Excel generates correct output", async ({ page }) => { const downloadedFilename = path.join("./e2e/downloads/", "testFilename.xlsx"); - await page.goto("/p/export-excel"); - await waitForMendixApp(page); - await page.locator(".mx-name-dataGridExportExcel").waitFor({ state: "visible", timeout: 15000 }); + const grid = new DataGridPage(page, "dataGridExportExcel"); + await grid.open("/p/export-excel"); + await grid.root.waitFor({ state: "visible", timeout: 15000 }); // Start waiting for download before clicking. const downloadPromise = page.waitForEvent("download"); await page.locator(".mx-name-exportButton").click({ force: true }); @@ -50,66 +50,54 @@ test.describe("datagrid-web export to Excel", () => { test.describe("capabilities: sorting", () => { test("applies the default sort order from the data source option", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name"); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "arrows-alt-v" - ); + const grid = new DataGridPage(page); + await grid.open("/"); + await expect(grid.columnHeader(1)).toHaveText("First Name"); + await expect(grid.sortIcon(1)).toHaveAttribute("data-icon", "arrows-alt-v"); await expect(page.getByRole("gridcell", { name: "12" }).first()).toHaveText("12"); }); test("changes order of data to ASC when clicking sort option", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name"); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "arrows-alt-v" - ); - await page.locator(".mx-name-datagrid1 .column-header").nth(1).click(); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "long-arrow-alt-up" - ); + const grid = new DataGridPage(page); + await grid.open("/"); + await expect(grid.columnHeader(1)).toHaveText("First Name"); + await expect(grid.sortIcon(1)).toHaveAttribute("data-icon", "arrows-alt-v"); + await grid.sortByColumn(1); + await expect(grid.sortIcon(1)).toHaveAttribute("data-icon", "long-arrow-alt-up"); await expect(page.getByRole("gridcell", { name: "10" }).first()).toHaveText("10"); }); test("changes order of data to DESC when clicking sort option", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name"); - await page.locator(".mx-name-datagrid1 .column-header").nth(1).click(); - await page.locator(".mx-name-datagrid1 .column-header").nth(1).click(); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "long-arrow-alt-down" - ); + const grid = new DataGridPage(page); + await grid.open("/"); + await expect(grid.columnHeader(1)).toHaveText("First Name"); + await grid.sortByColumn(1); + await grid.sortByColumn(1); + await expect(grid.sortIcon(1)).toHaveAttribute("data-icon", "long-arrow-alt-down"); await expect(page.getByRole("gridcell", { name: "12" }).first()).toHaveText("12"); }); }); test.describe("capabilities: hiding", () => { test("hides a selected column", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid1 .column-header").first()).toHaveText("Age"); - await page.locator(".mx-name-datagrid1 .column-selector-button").click(); - await page.locator(".column-selectors > li").first().click(); - await expect(page.locator(".mx-name-datagrid1 .column-header").first()).toHaveText("First Name"); + const grid = new DataGridPage(page); + await grid.open("/"); + await expect(grid.columnHeaders.first()).toHaveText("Age"); + await grid.openColumnSelector(); + await grid.columnSelectorItems.first().click(); + await expect(grid.columnHeaders.first()).toHaveText("First Name"); }); test("hide column saved on configuration attribute capability", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); + const grid = new DataGridPage(page, "datagrid5"); + await grid.open("/"); // hide first column - await page.locator(".mx-name-datagrid5 .column-selector-button").click(); - await page.locator(".column-selectors > li").first().click(); + await grid.openColumnSelector(); + await grid.columnSelectorItems.first().click(); // check if it is really hidden - await expect(page.locator(".mx-name-datagrid5 .column-header").first()).toHaveText("Last Name"); + await expect(grid.columnHeaders.first()).toHaveText("Last Name"); // check config saved to the attribute and visible in the text area const textArea = page.locator(".mx-name-textArea1 textarea"); @@ -130,51 +118,51 @@ test.describe("capabilities: hiding", () => { }); }); test("hide column by default enabled", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid6 .column-header").first()).toHaveText("First Name"); - await page.locator(".mx-name-datagrid6 .column-selector-button").click(); - await page.locator(".column-selectors > li").first().click(); - await expect(page.locator(".mx-name-datagrid6 .column-header").first()).toHaveText("Id"); + const grid = new DataGridPage(page, "datagrid6"); + await grid.open("/"); + await expect(grid.columnHeaders.first()).toHaveText("First Name"); + await grid.openColumnSelector(); + await grid.columnSelectorItems.first().click(); + await expect(grid.columnHeaders.first()).toHaveText("Id"); }); test("do not allow to hide last visible column", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid1 .column-header").first()).toBeVisible(); - await page.locator(".mx-name-datagrid1 .column-selector-button").click(); - await expect(page.locator(".column-selectors input:checked")).toHaveCount(4); - await page.locator(".column-selectors > li").nth(3).click(); - await page.locator(".column-selectors > li").nth(2).click(); - await page.locator(".column-selectors > li").nth(1).click(); - await expect(page.locator(".column-selectors input:checked")).toHaveCount(1); - await page.locator(".column-selectors > li").nth(0).click({ force: true }); - await expect(page.locator(".column-selectors input:checked")).toHaveCount(1); + const grid = new DataGridPage(page); + await grid.open("/"); + await expect(grid.columnHeaders.first()).toBeVisible(); + await grid.openColumnSelector(); + await expect(grid.checkedColumns).toHaveCount(4); + await grid.columnSelectorItem(3).click(); + await grid.columnSelectorItem(2).click(); + await grid.columnSelectorItem(1).click(); + await expect(grid.checkedColumns).toHaveCount(1); + await grid.columnSelectorItem(0).click({ force: true }); + await expect(grid.checkedColumns).toHaveCount(1); // Trigger Enter keypress - await page.locator(".column-selectors > li").nth(0).press("Enter"); - await expect(page.locator(".column-selectors input:checked")).toHaveCount(1); + await grid.columnSelectorItem(0).press("Enter"); + await expect(grid.checkedColumns).toHaveCount(1); // Trigger Space keypress - await page.locator(".column-selectors > li").nth(0).press("Space"); - await expect(page.locator(".column-selectors input:checked")).toHaveCount(1); + await grid.columnSelectorItem(0).press("Space"); + await expect(grid.checkedColumns).toHaveCount(1); }); }); test.describe("capabilities: onClick action", () => { test("check the context", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid1 .td").first()).toHaveText("12"); - await page.locator(".mx-name-datagrid1 .td").first().click(); + const grid = new DataGridPage(page); + await grid.open("/"); + await expect(grid.cells.first()).toHaveText("12"); + await grid.cells.first().click(); await expect(page.locator(".mx-name-AgeTextBox input")).toHaveValue("12"); }); }); test.describe("manual column width", () => { test("compares with a screenshot baseline and checks the column width is with correct size", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await page.locator(".mx-name-datagrid7").scrollIntoViewIfNeeded(); - await expect(page.locator(".mx-name-datagrid7")).toHaveScreenshot(`dataGridColumnContent.png`); + const grid = new DataGridPage(page, "datagrid7"); + await grid.open("/"); + await grid.root.scrollIntoViewIfNeeded(); + await expect(grid.root).toHaveScreenshot(`dataGridColumnContent.png`); }); }); @@ -182,27 +170,27 @@ test.describe("visual testing:", () => { test("compares with a screenshot baseline and checks if all datagrid and filter elements are rendered as expected", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-datagrid1")).toBeVisible(); - await expect(page.locator(".mx-name-datagrid1")).toHaveScreenshot(`datagrid.png`); + const grid = new DataGridPage(page); + await grid.open("/"); + await expect(grid.root).toBeVisible(); + await expect(grid.root).toHaveScreenshot(`datagrid.png`); }); test("compares with a screenshot baseline and checks datagrid using virtual scrolling are rendered as expected", async ({ page }) => { - await page.goto("/p/virtual-scrolling"); - await waitForMendixApp(page); - await expect(page.locator(".mx-name-dataGrid21")).toBeVisible(); - await page.locator(".mx-name-dataGrid21 .mx-name-textFilter1 .filter-selector-content .btn").click(); + const grid = new DataGridPage(page, "dataGrid21"); + await grid.open("/p/virtual-scrolling"); + await expect(grid.root).toBeVisible(); + await grid.root.locator(".mx-name-textFilter1 .filter-selector-content .btn").click(); await expect(page.locator(".mx-page")).toHaveScreenshot(`datagrid-virtual-scrolling.png`); }); }); test.describe("a11y testing:", () => { test("checks accessibility violations", async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); + const grid = new DataGridPage(page); + await grid.open("/"); const accessibilityScanResults = await new AxeBuilder({ page }) .withTags(["wcag21aa"]) .exclude(".mx-name-navigationTree3") diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js index d4e3e14eca..502c9bf78b 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js @@ -1,71 +1,60 @@ -import { expect, test } from "@mendix/run-e2e/fixtures"; -import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; import AxeBuilder from "@axe-core/playwright"; +import { expect, test } from "@mendix/run-e2e/fixtures"; +import { DataGridPage } from "./pages/DataGridPage"; test.describe("datagrid-web selection", async () => { test("applies checkbox single selection checkbox", async ({ page }) => { - const singleSelectionCheckbox = page.locator(".mx-name-dgSingleSelectionCheckbox"); + const grid = new DataGridPage(page, "dgSingleSelectionCheckbox"); - await page.goto("/p/single-selection"); - await waitForMendixApp(page); - await expect(singleSelectionCheckbox).toBeVisible(); - await singleSelectionCheckbox.locator("input").first().click(); + await grid.open("/p/single-selection"); + await expect(grid.root).toBeVisible(); + await grid.root.locator("input").first().click(); await expect(page).toHaveScreenshot(`datagridSingleSelectionCheckbox.png`); }); test("applies checkbox single selection row click", async ({ page }) => { - const singleSelectionRowClick = page.locator(".mx-name-dgSingleSelectionRowClick"); - - await page.goto("/p/single-selection"); - await waitForMendixApp(page); - await expect(singleSelectionRowClick).toBeVisible(); - await singleSelectionRowClick - .locator(".td") - .first() - .click({ modifiers: ["Shift"] }); + const grid = new DataGridPage(page, "dgSingleSelectionRowClick"); + + await grid.open("/p/single-selection"); + await expect(grid.root).toBeVisible(); + await grid.cells.first().click({ modifiers: ["Shift"] }); await expect(page).toHaveScreenshot(`datagridSingleSelectionRowClick.png`); }); test("applies checkbox multi selection checkbox", async ({ page }) => { - const multiSelectionCheckbox = page.locator(".mx-name-dgMultiSelectionCheckbox"); + const grid = new DataGridPage(page, "dgMultiSelectionCheckbox"); - await page.goto("/p/multi-selection"); - await waitForMendixApp(page); - await expect(multiSelectionCheckbox).toBeVisible(); - await multiSelectionCheckbox.locator("input").first().click(); - await multiSelectionCheckbox.locator("input").nth(1).click(); + await grid.open("/p/multi-selection"); + await expect(grid.root).toBeVisible(); + await grid.root.locator("input").first().click(); + await grid.root.locator("input").nth(1).click(); await expect(page).toHaveScreenshot(`datagridMultiSelectionCheckbox.png`); }); test("applies checkbox multi selection row click", async ({ page }) => { - const multiSelectionRowClick = page.locator(".mx-name-dgMultiSelectionRowClick"); - - await page.goto("/p/multi-selection"); - await waitForMendixApp(page); - await expect(multiSelectionRowClick).toBeVisible(); - await multiSelectionRowClick.locator(".td").first().click({ force: true }); - await multiSelectionRowClick - .locator(".td") - .nth(4) - .click({ modifiers: ["Shift"] }); + const grid = new DataGridPage(page, "dgMultiSelectionRowClick"); + + await grid.open("/p/multi-selection"); + await expect(grid.root).toBeVisible(); + await grid.cells.first().click({ force: true }); + await grid.cells.nth(4).click({ modifiers: ["Shift"] }); await expect(page).toHaveScreenshot(`datagridMultiSelectionRowClick.png`); }); test("checks single selection accessibility with sr-only text", async ({ page }) => { - await page.goto("/p/single-selection"); - await waitForMendixApp(page); + const grid = new DataGridPage(page, "dgSingleSelectionCheckbox"); + await grid.open("/p/single-selection"); - const singleSelectionCheckbox = page.locator(".mx-name-dgSingleSelectionCheckbox"); - await singleSelectionCheckbox.waitFor(); + await grid.root.waitFor(); // Verify sr-only text is present in the selection column header - const srOnlyText = singleSelectionCheckbox.locator(".widget-datagrid-col-select .sr-only"); + const srOnlyText = grid.root.locator(".widget-datagrid-col-select .sr-only"); await expect(srOnlyText).toHaveText(/Select single row/i); // Verify sr-only text is not visible but accessible await expect(srOnlyText).toBeAttached(); const isHidden = await srOnlyText.evaluate(el => { - const style = window.getComputedStyle(el); + const style = globalThis.getComputedStyle(el); return ( style.position === "absolute" && (style.width === "1px" || style.clip === "rect(0px, 0px, 0px, 0px)") ); @@ -83,10 +72,10 @@ test.describe("datagrid-web selection", async () => { }); test("checks accessibility violations", async ({ page }) => { - await page.goto("/p/multi-selection"); - await waitForMendixApp(page); + const grid = new DataGridPage(page, "dgMultiSelectionCheckbox"); + await grid.open("/p/multi-selection"); - await page.locator(".mx-name-dgMultiSelectionCheckbox").waitFor(); + await grid.root.waitFor(); const accessibilityScanResults = await new AxeBuilder({ page }) .withTags(["wcag21aa"]) .include(".mx-name-dgMultiSelectionCheckbox") diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js index e33a2cdb21..94ab3c9177 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js @@ -1,34 +1,31 @@ import { expect, test } from "@mendix/run-e2e/fixtures"; -import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; +import { DataGridPage } from "../pages/DataGridPage"; test.describe("datagrid-web filtering empty strings", () => { + /** @type {DataGridPage} */ + let grid; + test.beforeEach(async ({ page }) => { - await page.goto("/p/filtering-empty-string"); - await waitForMendixApp(page); + grid = new DataGridPage(page, "dataGrid21"); + await grid.open("/p/filtering-empty-string"); }); - test("filter rows by Empty and Not empty", async ({ page }) => { - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const filter = n => page.locator(`[role="columnheader"]:nth-child(${n})`).locator(".filter-container"); - const filterSelectorButton = n => filter(n).getByRole("combobox"); - const filterSelectorOption = (n, name) => - filter(n).getByRole("listbox").getByRole("option", { name, exact: true }); - + test("filter rows by Empty and Not empty", async () => { // all 3 records are shown - await expect(column(1)).toHaveText(["User 1 (with value)", 'User 3 ("")', "User 3 (empty)"]); + await expect(grid.columnCells(1)).toHaveText(["User 1 (with value)", 'User 3 ("")', "User 3 (empty)"]); // select Empty option - await filterSelectorButton(2).click({ delay: 20 }); - await filterSelectorOption(2, "Empty").click({ delay: 20 }); + await grid.headerFilterButton(2).click({ delay: 20 }); + await grid.headerFilterOption(2, "Empty").click({ delay: 20 }); // both, `empty` and `""` records are visible. Record with text is filtered out. - await expect(column(1)).toHaveText(['User 3 ("")', "User 3 (empty)"]); + await expect(grid.columnCells(1)).toHaveText(['User 3 ("")', "User 3 (empty)"]); // select "Not empty" option - await filterSelectorButton(2).click({ delay: 20 }); - await filterSelectorOption(2, "Not empty").click({ delay: 20 }); + await grid.headerFilterButton(2).click({ delay: 20 }); + await grid.headerFilterOption(2, "Not empty").click({ delay: 20 }); // Record with text is visible, `empty` and `""` records are filtered out. - await expect(column(1)).toHaveText(["User 1 (with value)"]); + await expect(grid.columnCells(1)).toHaveText(["User 1 (with value)"]); }); }); diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js index c6ff286e19..1cf52e3383 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js @@ -1,49 +1,36 @@ import { test, expect } from "@mendix/run-e2e/fixtures"; -import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; +import { DataGridPage } from "../pages/DataGridPage"; test("datagrid-web filtering integration", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; + const grid = new DataGridPage(page, "dataGrid21"); - const rowCount = await rows(); + await grid.open("/p/filtering-integration"); - await page.goto("/p/filtering-integration"); - await waitForMendixApp(page); + await expect(grid.rows).toHaveCount(51); - await expect(rowCount).toHaveCount(51); + await grid.headerTextbox("First name").fill("a"); + await expect(grid.rows).toHaveCount(30); - await page.getByRole("columnheader", { name: "First name" }).getByRole("textbox").fill("a"); - //await select("First name").fill("a"); - await expect(await rows()).toHaveCount(30); + await grid.headerTextbox("Birth date").fill("1/1/1990"); + await grid.headerTextbox("First name").click(); + await expect(grid.rows).toHaveCount(14); - await page.getByRole("columnheader", { name: "Birth date" }).getByRole("textbox").fill("1/1/1990"); - //await select("Birth date").fill("1/1/1990"); - await page.getByRole("columnheader", { name: "First name" }).getByRole("textbox").click(); - await expect(await rows()).toHaveCount(14); + await grid.headerTextbox("Birth year").fill("1995"); + await expect(grid.rows).toHaveCount(9); - await page.getByRole("columnheader", { name: "Birth year" }).getByRole("textbox").fill("1995"); - await expect(await rows()).toHaveCount(9); + await grid.headerCombobox("Color (enum)").click(); + await grid.option("Black").click({ delay: 1 }); + await expect(grid.rows).toHaveCount(4); - await page.getByRole("columnheader", { name: "Color (enum)" }).getByRole("combobox").click(); - //await select("Color (enum)").click(); - await page.locator(`[role="option"]:has-text("Black")`).click({ delay: 1 }); - //await option("Black").click(); - await expect(await rows()).toHaveCount(4); + await grid.headerCombobox("Roles (ref set)").click(); + await grid.option("Careers adviser").click({ delay: 1 }); + await expect(grid.rows).toHaveCount(3); - await page.getByRole("columnheader", { name: "Roles (ref set)" }).getByRole("combobox").click(); - //await select("Roles (ref set)").click(); - await page.locator(`[role="option"]:has-text("Careers adviser")`).click({ delay: 1 }); - //await option("Careers adviser").click(); - await expect(await rows()).toHaveCount(3); + await grid.headerCombobox("Company").click(); + await grid.option("Sierra Health Services Inc").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(2); - await page.getByRole("columnheader", { name: "Company" }).getByRole("combobox").click(); - //await select("Company").click(); - await page.locator(`[role="option"]:has-text("Sierra Health Services Inc")`).click({ delay: 20 }); - //await option("Sierra Health Services Inc").click(); - await expect(await rows()).toHaveCount(2); - - const row = (await rows()).nth(1); + const row = grid.rows.nth(1); await expect(row).toHaveText( "Lina3/3/20042004BlackEnvironmental scientistCareers adviserPrison officerMarket research analystSierra Health Services Inc" ); diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js index f818cee362..f632adff2e 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js @@ -1,36 +1,29 @@ import { test, expect } from "@mendix/run-e2e/fixtures"; -import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; +import { DataGridPage } from "../pages/DataGridPage"; test.describe("datagrid-web filtering multi select", () => { + /** @type {DataGridPage} */ + let grid; + + test.beforeEach(async ({ page }) => { + grid = new DataGridPage(page, "dataGrid21"); + await grid.open("/p/filtering-multi"); + }); + test("filter rows where enum attribute equal to one of selected values", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const enumSelect = () => page.locator(".mx-name-drop_downFilter1[role=combobox]"); - const rowCount = await rows(); - await page.goto("/p/filtering-multi"); - await waitForMendixApp(page); - await expect(rowCount).toHaveCount(11); - await expect(await column(2).first()).toHaveText("Black"); - await expect(await column(2).last()).toHaveText("Blue"); - await enumSelect().click(); - await option("Pink").click({ delay: 20 }); - await expect(await rows()).toHaveCount(6); - await option("Blush").click({ delay: 20 }); - await expect(await rows()).toHaveCount(8); + await expect(grid.rows).toHaveCount(11); + await expect(grid.columnCells(2).first()).toHaveText("Black"); + await expect(grid.columnCells(2).last()).toHaveText("Blue"); + await grid.dropdownFilter(1).click(); + await grid.option("Pink").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(6); + await grid.option("Blush").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(8); await page.getByRole("columnheader", { name: "Color (enum)" }).getByRole("combobox").click({ delay: 20 }); - await expect(column(2)).toContainText(["Pink", "Pink", "Pink", "Blush", "Blush", "Pink", "Pink"]); + await expect(grid.columnCells(2)).toContainText(["Pink", "Pink", "Pink", "Blush", "Blush", "Pink", "Pink"]); }); - test("filter rows where ReferenceSet contains at least one of selected objects", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const roleSelect = () => page.locator(".mx-name-drop_downFilter3[role=combobox]"); + test("filter rows where ReferenceSet contains at least one of selected objects", async () => { const expectedColumnText = [ "EconomistArmed forces officerTraderHealth service manager", "EconomistArmed forces officerTrader", @@ -42,39 +35,27 @@ test.describe("datagrid-web filtering multi select", () => { "Homeless workerEditorial assistantPublic librarian", "Environmental scientistPublic librarianMaterials specialist" ]; - await page.goto("/p/filtering-multi"); - await waitForMendixApp(page); - await expect(await column(3).first()).toHaveText(expectedColumnText[0]); - await roleSelect().click(); - await option("Economist").click({ delay: 20 }); - await expect(await rows()).toHaveCount(6); - await option("Public librarian").click({ delay: 20 }); - await expect(await rows()).toHaveCount(10); - await roleSelect().click({ delay: 20 }); - await expect(column(3)).toHaveText(expectedColumnText); + await expect(grid.columnCells(3).first()).toHaveText(expectedColumnText[0]); + await grid.dropdownFilter(3).click(); + await grid.option("Economist").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(6); + await grid.option("Public librarian").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(10); + await grid.dropdownFilter(3).click({ delay: 20 }); + await expect(grid.columnCells(3)).toHaveText(expectedColumnText); }); test("filter rows where Reference equal to one of selected objects", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const companySelect = () => page.locator(".mx-name-drop_downFilter4[role=combobox]"); - - const rowCount = await rows(); - await page.goto("/p/filtering-multi"); - await waitForMendixApp(page); - await expect(rowCount).toHaveCount(11); - await expect(await column(4).first()).toHaveText("W.R. Berkley Corporation"); - await expect(await column(4).last()).toHaveText("PETsMART Inc"); - await companySelect().click({ delay: 20 }); - await option("FMC Corp").click({ delay: 20 }); - await expect(await rows()).toHaveCount(2); - await option("ALLETE, Inc.").click({ delay: 20 }); - await expect(await rows()).toHaveCount(6); + await expect(grid.rows).toHaveCount(11); + await expect(grid.columnCells(4).first()).toHaveText("W.R. Berkley Corporation"); + await expect(grid.columnCells(4).last()).toHaveText("PETsMART Inc"); + await grid.dropdownFilter(4).click({ delay: 20 }); + await grid.option("FMC Corp").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(2); + await grid.option("ALLETE, Inc.").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(6); await page.getByRole("columnheader", { name: "Company" }).getByRole("combobox").click({ delay: 20 }); - await expect(column(4)).toContainText([ + await expect(grid.columnCells(4)).toContainText([ "ALLETE, Inc.", "FMC Corp", "ALLETE, Inc.", diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js index 508f48e657..e39ed6183a 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js @@ -1,124 +1,111 @@ import { expect, test } from "@mendix/run-e2e/fixtures"; -import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; +import { DataGridPage } from "../pages/DataGridPage"; test.describe("datagrid-web filtering single select", () => { + /** @type {DataGridPage} */ + let grid; + test.beforeEach(async ({ page }) => { - await page.goto("/p/filtering-single"); - await waitForMendixApp(page); + grid = new DataGridPage(page, "dataGrid21"); + await grid.open("/p/filtering-single"); }); test("compares with a screenshot baseline and checks if all datagrid and filter elements are rendered as expected", async ({ page }) => { - await expect(page.locator(".mx-name-dataGrid21")).toBeVisible(); + await expect(grid.root).toBeVisible(); await expect(page).toHaveScreenshot(`datagridFilteringSingle.png`); }); - test("filter rows that have Yes in Pets column", async ({ page }) => { - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const booleanSelect = () => page.locator('.mx-name-drop_downFilter2[role="combobox"]'); - - await booleanSelect().click(); - await option("Yes").click({ delay: 1 }); - await expect(column(3)).toHaveText(["Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes"]); + test("filter rows that have Yes in Pets column", async () => { + await grid.dropdownFilter(2).click(); + await grid.option("Yes").click({ delay: 1 }); + await expect(grid.columnCells(3)).toHaveText([ + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes" + ]); }); test("filter rows that have No in Pets column", async ({ page }) => { - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const booleanSelect = () => page.locator('.mx-name-drop_downFilter2[role="combobox"]'); - - await booleanSelect().click(); + await grid.dropdownFilter(2).click(); await page.getByRole("option", { name: "No", exact: true }).click(); - await expect(column(3).first()).toHaveText("No"); - const columnTexts = await column(3).allTextContents(); + await expect(grid.columnCells(3).first()).toHaveText("No"); + const columnTexts = await grid.columnCells(3).allTextContents(); columnTexts.forEach(text => expect(text).toBe("No")); }); test("reset filter state when empty option is clicked", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const booleanSelect = () => page.locator('.mx-name-drop_downFilter2[role="combobox"]'); - - await booleanSelect().click(); - await option("Yes").click({ delay: 20 }); - const rowCount = await rows(); - await expect(rowCount).toHaveCount(11); - await expect(column(3)).toHaveText(["Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes"]); - await booleanSelect().click({ delay: 20 }); + await grid.dropdownFilter(2).click(); + await grid.option("Yes").click({ delay: 20 }); + await expect(grid.rows).toHaveCount(11); + await expect(grid.columnCells(3)).toHaveText([ + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes", + "Yes" + ]); + await grid.dropdownFilter(2).click({ delay: 20 }); await page.getByRole("row", { name: "Pets (bool)" }).getByRole("option").first().click(); - await expect(column(3)).toHaveText(["Yes", "Yes", "Yes", "No", "Yes", "No", "No", "Yes", "No", "Yes"]); + await expect(grid.columnCells(3)).toHaveText([ + "Yes", + "Yes", + "Yes", + "No", + "Yes", + "No", + "No", + "Yes", + "No", + "Yes" + ]); }); - test("filter rows that have Cyan in Color column", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const enumSelect = () => page.locator('.mx-name-drop_downFilter1[role="combobox"]'); - - await enumSelect().click(); - await option("Cyan").click({ delay: 1 }); - const rowCount = await rows(); - await expect(rowCount).toHaveCount(6); - const columnTexts = await column(2).allTextContents(); + test("filter rows that have Cyan in Color column", async () => { + await grid.dropdownFilter(1).click(); + await grid.option("Cyan").click({ delay: 1 }); + await expect(grid.rows).toHaveCount(6); + const columnTexts = await grid.columnCells(2).allTextContents(); columnTexts.forEach(text => expect(text).toBe("Cyan")); }); - test("filter rows that have Black in Color column", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const enumSelect = () => page.locator('.mx-name-drop_downFilter1[role="combobox"]'); - - await enumSelect().click(); - await option("Black").click({ delay: 1 }); - const rowCount = await rows(); - await expect(rowCount).toHaveCount(9); - const columnTexts = await column(2).allTextContents(); + test("filter rows that have Black in Color column", async () => { + await grid.dropdownFilter(1).click(); + await grid.option("Black").click({ delay: 1 }); + await expect(grid.rows).toHaveCount(9); + const columnTexts = await grid.columnCells(2).allTextContents(); columnTexts.forEach(text => expect(text).toBe("Black")); }); - test("filter rows that match selected role", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const roleSelect = () => page.locator('.mx-name-drop_downFilter3[role="combobox"]'); - - const rowCount = await rows(); - await expect(rowCount).toHaveCount(11); - await roleSelect().click(); - await option("Trader").click({ delay: 1 }); - const rowCount2 = await rows(); - await expect(rowCount2).toHaveCount(8); - const columnTexts = await column(4).allTextContents(); + test("filter rows that match selected role", async () => { + await expect(grid.rows).toHaveCount(11); + await grid.dropdownFilter(3).click(); + await grid.option("Trader").click({ delay: 1 }); + await expect(grid.rows).toHaveCount(8); + const columnTexts = await grid.columnCells(4).allTextContents(); columnTexts.forEach(text => expect(text).toContain("Trader")); }); - test("filter rows that match selected company", async ({ page }) => { - const rows = async () => { - return page.locator('.mx-name-dataGrid21 [role="row"]'); - }; - const column = n => page.locator(`[role="gridcell"]:nth-child(${n})`); - const option = label => page.locator(`[role="option"]:has-text("${label}")`); - const companySelect = () => page.locator('.mx-name-drop_downFilter4[role="combobox"]'); - - const rowCount = await rows(); - await expect(rowCount).toHaveCount(11); - await companySelect().click(); - await option("PETsMART Inc").click({ delay: 1 }); - const rowCount2 = await rows(); - await expect(rowCount2).toHaveCount(9); - const columnTexts = await column(5).allTextContents(); + test("filter rows that match selected company", async () => { + await expect(grid.rows).toHaveCount(11); + await grid.dropdownFilter(4).click(); + await grid.option("PETsMART Inc").click({ delay: 1 }); + await expect(grid.rows).toHaveCount(9); + const columnTexts = await grid.columnCells(5).allTextContents(); columnTexts.forEach(text => expect(text).toBe("PETsMART Inc")); }); }); diff --git a/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js new file mode 100644 index 0000000000..b28dae69db --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js @@ -0,0 +1,117 @@ +/** + * Page Object Model for the datagrid-web widget E2E tests. + * + * A `DataGridPage` wraps a single Data Grid instance (identified by its + * Mendix `mx-name`) and exposes the locators and actions the specs need, + * so tests read as behavior rather than raw selectors. + * + * Navigation relies on the custom fixture, which auto-waits for the Mendix + * app to be ready after every `page.goto()` — no manual `waitForMendixApp`. + */ +export class DataGridPage { + /** + * @param {import("@playwright/test").Page} page + * @param {string} [name="datagrid1"] the grid's mx-name (without the `.mx-name-` prefix) + */ + constructor(page, name = "datagrid1") { + this.page = page; + this.name = name; + this.root = page.locator(`.mx-name-${name}`); + } + + /** Navigate to a page; the fixture auto-waits for Mendix readiness. */ + async open(path = "/") { + await this.page.goto(path); + } + + // --- Columns & headers ------------------------------------------------- + + get columnHeaders() { + return this.root.locator(".column-header"); + } + + columnHeader(n) { + return this.columnHeaders.nth(n); + } + + /** Sort indicator icon inside a column header. */ + sortIcon(n) { + return this.columnHeader(n).locator("svg"); + } + + /** Click a column header to cycle its sort order. */ + async sortByColumn(n) { + await this.columnHeader(n).click(); + } + + // --- Cells & rows ------------------------------------------------------ + + get cells() { + return this.root.locator(".td"); + } + + cell(n) { + return this.cells.nth(n); + } + + get rows() { + return this.root.locator('[role="row"]'); + } + + /** All gridcells in the nth column (1-based, matches `:nth-child`). */ + columnCells(n) { + return this.page.locator(`[role="gridcell"]:nth-child(${n})`); + } + + // --- Column selector (hide/show) --------------------------------------- + + async openColumnSelector() { + await this.root.locator(".column-selector-button").click(); + } + + get columnSelectorItems() { + return this.page.locator(".column-selectors > li"); + } + + columnSelectorItem(n) { + return this.columnSelectorItems.nth(n); + } + + get checkedColumns() { + return this.page.locator(".column-selectors input:checked"); + } + + // --- Filters ----------------------------------------------------------- + + /** A named drop-down filter widget rendered as a combobox. */ + dropdownFilter(n) { + return this.page.locator(`.mx-name-drop_downFilter${n}[role="combobox"]`); + } + + /** An option in an open listbox, matched by visible text. */ + option(label) { + return this.page.locator(`[role="option"]:has-text("${label}")`); + } + + /** Column-header-scoped filter controls (used by header-embedded filters). */ + headerCombobox(name) { + return this.page.getByRole("columnheader", { name }).getByRole("combobox"); + } + + headerTextbox(name) { + return this.page.getByRole("columnheader", { name }).getByRole("textbox"); + } + + /** The `.filter-container` inside the nth column header (1-based). */ + headerFilterContainer(n) { + return this.page.locator(`[role="columnheader"]:nth-child(${n})`).locator(".filter-container"); + } + + headerFilterButton(n) { + return this.headerFilterContainer(n).getByRole("combobox"); + } + + headerFilterOption(n, name) { + return this.headerFilterContainer(n).getByRole("listbox").getByRole("option", { name, exact: true }); + } +} From f56a55bec4fc04f431dfd7c98477c8c5a1cab484 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Mon, 6 Jul 2026 16:35:17 +0200 Subject: [PATCH 2/4] test(datagrid-web): process code review feedback --- .../pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js | 1 + .../e2e/filtering/DataGridFilteringEmptyString.spec.js | 2 +- .../e2e/filtering/DataGridFilteringMulti.spec.js | 8 ++++---- .../datagrid-web/e2e/pages/DataGridPage.js | 8 +++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js index c77c9bdf0b..7f1d529e09 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js @@ -13,6 +13,7 @@ test.describe("datagrid-web export to Excel", () => { await grid.root.waitFor({ state: "visible", timeout: 15000 }); // Start waiting for download before clicking. const downloadPromise = page.waitForEvent("download"); + // Export button lives outside the grid widget boundary, so it stays a page-level selector. await page.locator(".mx-name-exportButton").click({ force: true }); const download = await downloadPromise; // Wait for the download process to complete and save the downloaded file. diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js index 94ab3c9177..ac8107dd5f 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js @@ -6,7 +6,7 @@ test.describe("datagrid-web filtering empty strings", () => { let grid; test.beforeEach(async ({ page }) => { - grid = new DataGridPage(page, "dataGrid21"); + grid = new DataGridPage(page, "dataGrid2_1"); await grid.open("/p/filtering-empty-string"); }); diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js index f632adff2e..66089084a4 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js @@ -10,7 +10,7 @@ test.describe("datagrid-web filtering multi select", () => { await grid.open("/p/filtering-multi"); }); - test("filter rows where enum attribute equal to one of selected values", async ({ page }) => { + test("filter rows where enum attribute equal to one of selected values", async () => { await expect(grid.rows).toHaveCount(11); await expect(grid.columnCells(2).first()).toHaveText("Black"); await expect(grid.columnCells(2).last()).toHaveText("Blue"); @@ -19,7 +19,7 @@ test.describe("datagrid-web filtering multi select", () => { await expect(grid.rows).toHaveCount(6); await grid.option("Blush").click({ delay: 20 }); await expect(grid.rows).toHaveCount(8); - await page.getByRole("columnheader", { name: "Color (enum)" }).getByRole("combobox").click({ delay: 20 }); + await grid.headerCombobox("Color (enum)").click({ delay: 20 }); await expect(grid.columnCells(2)).toContainText(["Pink", "Pink", "Pink", "Blush", "Blush", "Pink", "Pink"]); }); @@ -45,7 +45,7 @@ test.describe("datagrid-web filtering multi select", () => { await expect(grid.columnCells(3)).toHaveText(expectedColumnText); }); - test("filter rows where Reference equal to one of selected objects", async ({ page }) => { + test("filter rows where Reference equal to one of selected objects", async () => { await expect(grid.rows).toHaveCount(11); await expect(grid.columnCells(4).first()).toHaveText("W.R. Berkley Corporation"); await expect(grid.columnCells(4).last()).toHaveText("PETsMART Inc"); @@ -54,7 +54,7 @@ test.describe("datagrid-web filtering multi select", () => { await expect(grid.rows).toHaveCount(2); await grid.option("ALLETE, Inc.").click({ delay: 20 }); await expect(grid.rows).toHaveCount(6); - await page.getByRole("columnheader", { name: "Company" }).getByRole("combobox").click({ delay: 20 }); + await grid.headerCombobox("Company").click({ delay: 20 }); await expect(grid.columnCells(4)).toContainText([ "ALLETE, Inc.", "FMC Corp", diff --git a/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js index b28dae69db..04d5553259 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js @@ -60,7 +60,7 @@ export class DataGridPage { /** All gridcells in the nth column (1-based, matches `:nth-child`). */ columnCells(n) { - return this.page.locator(`[role="gridcell"]:nth-child(${n})`); + return this.root.locator(`[role="gridcell"]:nth-child(${n})`); } // --- Column selector (hide/show) --------------------------------------- @@ -69,8 +69,10 @@ export class DataGridPage { await this.root.locator(".column-selector-button").click(); } + // The column-selector popover is rendered inline inside the grid root + // (Floating UI without a portal), so scope its items to `this.root`. get columnSelectorItems() { - return this.page.locator(".column-selectors > li"); + return this.root.locator(".column-selectors > li"); } columnSelectorItem(n) { @@ -78,7 +80,7 @@ export class DataGridPage { } get checkedColumns() { - return this.page.locator(".column-selectors input:checked"); + return this.root.locator(".column-selectors input:checked"); } // --- Filters ----------------------------------------------------------- From ffcdf97f76d18048dc05c758d4180ccf11840ea8 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Mon, 6 Jul 2026 16:58:19 +0200 Subject: [PATCH 3/4] test(datagrid-web): code review feedback pt.2 --- .../e2e/filtering/DataGridFilteringSingle.spec.js | 4 ++-- .../datagrid-web/e2e/pages/DataGridPage.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js index e39ed6183a..374b3195c7 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js @@ -35,9 +35,9 @@ test.describe("datagrid-web filtering single select", () => { ]); }); - test("filter rows that have No in Pets column", async ({ page }) => { + test("filter rows that have No in Pets column", async () => { await grid.dropdownFilter(2).click(); - await page.getByRole("option", { name: "No", exact: true }).click(); + await grid.option("No").click(); await expect(grid.columnCells(3).first()).toHaveText("No"); const columnTexts = await grid.columnCells(3).allTextContents(); columnTexts.forEach(text => expect(text).toBe("No")); diff --git a/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js index 04d5553259..7c6a74cac8 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js @@ -90,23 +90,23 @@ export class DataGridPage { return this.page.locator(`.mx-name-drop_downFilter${n}[role="combobox"]`); } - /** An option in an open listbox, matched by visible text. */ + /** An option in an open listbox, matched by exact accessible name. */ option(label) { - return this.page.locator(`[role="option"]:has-text("${label}")`); + return this.page.getByRole("option", { name: label, exact: true }); } /** Column-header-scoped filter controls (used by header-embedded filters). */ headerCombobox(name) { - return this.page.getByRole("columnheader", { name }).getByRole("combobox"); + return this.root.getByRole("columnheader", { name }).getByRole("combobox"); } headerTextbox(name) { - return this.page.getByRole("columnheader", { name }).getByRole("textbox"); + return this.root.getByRole("columnheader", { name }).getByRole("textbox"); } /** The `.filter-container` inside the nth column header (1-based). */ headerFilterContainer(n) { - return this.page.locator(`[role="columnheader"]:nth-child(${n})`).locator(".filter-container"); + return this.root.locator(`[role="columnheader"]:nth-child(${n})`).locator(".filter-container"); } headerFilterButton(n) { From dc9d016e582f8241e142ae05a1655d09f4e46b52 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Wed, 8 Jul 2026 12:06:57 +0200 Subject: [PATCH 4/4] test(datagrid-web): code review feedback pt.3 --- docs/requirements/e2e-test-guidelines.md | 15 +++--- .../datagrid-web/e2e/DataGrid.spec.js | 26 +++++----- .../e2e/DataGridSelection.spec.js | 12 ++--- .../DataGridFilteringEmptyString.spec.js | 2 +- .../DataGridFilteringIntegration.spec.js | 9 ++-- .../filtering/DataGridFilteringMulti.spec.js | 31 ++++++----- .../filtering/DataGridFilteringSingle.spec.js | 51 +++++++++++-------- .../datagrid-web/e2e/pages/DataGridPage.js | 26 ++-------- 8 files changed, 81 insertions(+), 91 deletions(-) diff --git a/docs/requirements/e2e-test-guidelines.md b/docs/requirements/e2e-test-guidelines.md index 2ffa2d2423..53af40402d 100644 --- a/docs/requirements/e2e-test-guidelines.md +++ b/docs/requirements/e2e-test-guidelines.md @@ -75,8 +75,9 @@ For widgets with more than a handful of specs — or any spec that repeats the s **Conventions:** -- **Scope to one widget instance by `mx-name`.** The constructor takes `(page, name)` and builds a `root` locator (`.mx-name-`); default the `name` to the most common instance. All locators derive from `root` so tests never hard-code `.mx-name-*` strings. -- **`open(path)` wraps `page.goto(path)` only.** The custom fixture already auto-waits for the app (see Imports & Setup) — do **not** add `waitForMendixApp` inside the POM. +- **Scope to one widget instance by `mx-name`.** The constructor takes `(page, name)` and builds a `root` locator (`.mx-name-`); default the `name` to the most common instance. All locators derive from `root` — the POM stores no reference to `page`. +- **Navigation stays in `beforeEach`, not the POM.** Call `page.goto(path)` directly in the spec's `beforeEach`. The custom fixture auto-waits for the app after every `goto` — do **not** add `waitForMendixApp` (see Imports & Setup). +- **Page-level selectors stay in the spec.** When a widget renders outside the grid's DOM subtree (e.g. a sibling filter widget), use `page.locator(...)` inline in the test. Do not add a `this.page` property to the POM to accommodate these — it breaks instance isolation. - **Expose locators as getters/methods; keep assertions in the spec.** The POM returns locators (e.g. `get rows()`, `columnCells(n)`); the test does the `expect(...)`. This preserves auto-retrying web-first assertions and keeps the POM assertion-free. - **Actions are verbs** (`sortByColumn(n)`, `openColumnSelector()`); **locators are nouns** (`columnHeaders`, `cells`). Prefer `.mx-name-*` and role/label composition inside the POM, following Locator Patterns above. @@ -84,15 +85,10 @@ For widgets with more than a handful of specs — or any spec that repeats the s // e2e/pages/MyWidgetPage.js export class MyWidgetPage { constructor(page, name = "myWidget1") { - this.page = page; + // page is used only to build root; not stored — keeps POM dom-subtree-scoped. this.root = page.locator(`.mx-name-${name}`); } - // fixture auto-waits after goto — no waitForMendixApp here - async open(path = "/") { - await this.page.goto(path); - } - get rows() { return this.root.locator('[role="row"]'); } @@ -112,9 +108,10 @@ test.describe("MyWidget", () => { /** @type {MyWidgetPage} */ let widget; + // Navigation and page-level setup belong here, not in the POM. test.beforeEach(async ({ page }) => { widget = new MyWidgetPage(page); - await widget.open("/"); + await page.goto("/"); // fixture auto-waits for Mendix readiness }); test("submits the form @smoke", async () => { diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js index 7f1d529e09..705f821462 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js @@ -9,7 +9,7 @@ test.describe("datagrid-web export to Excel", () => { const downloadedFilename = path.join("./e2e/downloads/", "testFilename.xlsx"); const grid = new DataGridPage(page, "dataGridExportExcel"); - await grid.open("/p/export-excel"); + await page.goto("/p/export-excel"); await grid.root.waitFor({ state: "visible", timeout: 15000 }); // Start waiting for download before clicking. const downloadPromise = page.waitForEvent("download"); @@ -52,7 +52,7 @@ test.describe("datagrid-web export to Excel", () => { test.describe("capabilities: sorting", () => { test("applies the default sort order from the data source option", async ({ page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); await expect(grid.columnHeader(1)).toHaveText("First Name"); await expect(grid.sortIcon(1)).toHaveAttribute("data-icon", "arrows-alt-v"); await expect(page.getByRole("gridcell", { name: "12" }).first()).toHaveText("12"); @@ -60,7 +60,7 @@ test.describe("capabilities: sorting", () => { test("changes order of data to ASC when clicking sort option", async ({ page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); await expect(grid.columnHeader(1)).toHaveText("First Name"); await expect(grid.sortIcon(1)).toHaveAttribute("data-icon", "arrows-alt-v"); await grid.sortByColumn(1); @@ -70,7 +70,7 @@ test.describe("capabilities: sorting", () => { test("changes order of data to DESC when clicking sort option", async ({ page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); await expect(grid.columnHeader(1)).toHaveText("First Name"); await grid.sortByColumn(1); await grid.sortByColumn(1); @@ -82,7 +82,7 @@ test.describe("capabilities: sorting", () => { test.describe("capabilities: hiding", () => { test("hides a selected column", async ({ page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); await expect(grid.columnHeaders.first()).toHaveText("Age"); await grid.openColumnSelector(); await grid.columnSelectorItems.first().click(); @@ -91,7 +91,7 @@ test.describe("capabilities: hiding", () => { test("hide column saved on configuration attribute capability", async ({ page }) => { const grid = new DataGridPage(page, "datagrid5"); - await grid.open("/"); + await page.goto("/"); // hide first column await grid.openColumnSelector(); @@ -120,7 +120,7 @@ test.describe("capabilities: hiding", () => { }); test("hide column by default enabled", async ({ page }) => { const grid = new DataGridPage(page, "datagrid6"); - await grid.open("/"); + await page.goto("/"); await expect(grid.columnHeaders.first()).toHaveText("First Name"); await grid.openColumnSelector(); await grid.columnSelectorItems.first().click(); @@ -129,7 +129,7 @@ test.describe("capabilities: hiding", () => { test("do not allow to hide last visible column", async ({ page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); await expect(grid.columnHeaders.first()).toBeVisible(); await grid.openColumnSelector(); await expect(grid.checkedColumns).toHaveCount(4); @@ -151,7 +151,7 @@ test.describe("capabilities: hiding", () => { test.describe("capabilities: onClick action", () => { test("check the context", async ({ page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); await expect(grid.cells.first()).toHaveText("12"); await grid.cells.first().click(); await expect(page.locator(".mx-name-AgeTextBox input")).toHaveValue("12"); @@ -161,7 +161,7 @@ test.describe("capabilities: onClick action", () => { test.describe("manual column width", () => { test("compares with a screenshot baseline and checks the column width is with correct size", async ({ page }) => { const grid = new DataGridPage(page, "datagrid7"); - await grid.open("/"); + await page.goto("/"); await grid.root.scrollIntoViewIfNeeded(); await expect(grid.root).toHaveScreenshot(`dataGridColumnContent.png`); }); @@ -172,7 +172,7 @@ test.describe("visual testing:", () => { page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); await expect(grid.root).toBeVisible(); await expect(grid.root).toHaveScreenshot(`datagrid.png`); }); @@ -181,7 +181,7 @@ test.describe("visual testing:", () => { page }) => { const grid = new DataGridPage(page, "dataGrid21"); - await grid.open("/p/virtual-scrolling"); + await page.goto("/p/virtual-scrolling"); await expect(grid.root).toBeVisible(); await grid.root.locator(".mx-name-textFilter1 .filter-selector-content .btn").click(); await expect(page.locator(".mx-page")).toHaveScreenshot(`datagrid-virtual-scrolling.png`); @@ -191,7 +191,7 @@ test.describe("visual testing:", () => { test.describe("a11y testing:", () => { test("checks accessibility violations", async ({ page }) => { const grid = new DataGridPage(page); - await grid.open("/"); + await page.goto("/"); const accessibilityScanResults = await new AxeBuilder({ page }) .withTags(["wcag21aa"]) .exclude(".mx-name-navigationTree3") diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js index 502c9bf78b..94857c3bd0 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js @@ -6,7 +6,7 @@ test.describe("datagrid-web selection", async () => { test("applies checkbox single selection checkbox", async ({ page }) => { const grid = new DataGridPage(page, "dgSingleSelectionCheckbox"); - await grid.open("/p/single-selection"); + await page.goto("/p/single-selection"); await expect(grid.root).toBeVisible(); await grid.root.locator("input").first().click(); await expect(page).toHaveScreenshot(`datagridSingleSelectionCheckbox.png`); @@ -15,7 +15,7 @@ test.describe("datagrid-web selection", async () => { test("applies checkbox single selection row click", async ({ page }) => { const grid = new DataGridPage(page, "dgSingleSelectionRowClick"); - await grid.open("/p/single-selection"); + await page.goto("/p/single-selection"); await expect(grid.root).toBeVisible(); await grid.cells.first().click({ modifiers: ["Shift"] }); await expect(page).toHaveScreenshot(`datagridSingleSelectionRowClick.png`); @@ -24,7 +24,7 @@ test.describe("datagrid-web selection", async () => { test("applies checkbox multi selection checkbox", async ({ page }) => { const grid = new DataGridPage(page, "dgMultiSelectionCheckbox"); - await grid.open("/p/multi-selection"); + await page.goto("/p/multi-selection"); await expect(grid.root).toBeVisible(); await grid.root.locator("input").first().click(); await grid.root.locator("input").nth(1).click(); @@ -34,7 +34,7 @@ test.describe("datagrid-web selection", async () => { test("applies checkbox multi selection row click", async ({ page }) => { const grid = new DataGridPage(page, "dgMultiSelectionRowClick"); - await grid.open("/p/multi-selection"); + await page.goto("/p/multi-selection"); await expect(grid.root).toBeVisible(); await grid.cells.first().click({ force: true }); await grid.cells.nth(4).click({ modifiers: ["Shift"] }); @@ -43,7 +43,7 @@ test.describe("datagrid-web selection", async () => { test("checks single selection accessibility with sr-only text", async ({ page }) => { const grid = new DataGridPage(page, "dgSingleSelectionCheckbox"); - await grid.open("/p/single-selection"); + await page.goto("/p/single-selection"); await grid.root.waitFor(); @@ -73,7 +73,7 @@ test.describe("datagrid-web selection", async () => { test("checks accessibility violations", async ({ page }) => { const grid = new DataGridPage(page, "dgMultiSelectionCheckbox"); - await grid.open("/p/multi-selection"); + await page.goto("/p/multi-selection"); await grid.root.waitFor(); const accessibilityScanResults = await new AxeBuilder({ page }) diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js index ac8107dd5f..175d304712 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringEmptyString.spec.js @@ -7,7 +7,7 @@ test.describe("datagrid-web filtering empty strings", () => { test.beforeEach(async ({ page }) => { grid = new DataGridPage(page, "dataGrid2_1"); - await grid.open("/p/filtering-empty-string"); + await page.goto("/p/filtering-empty-string"); }); test("filter rows by Empty and Not empty", async () => { diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js index 1cf52e3383..b472edf348 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringIntegration.spec.js @@ -4,7 +4,7 @@ import { DataGridPage } from "../pages/DataGridPage"; test("datagrid-web filtering integration", async ({ page }) => { const grid = new DataGridPage(page, "dataGrid21"); - await grid.open("/p/filtering-integration"); + await page.goto("/p/filtering-integration"); await expect(grid.rows).toHaveCount(51); @@ -18,16 +18,17 @@ test("datagrid-web filtering integration", async ({ page }) => { await grid.headerTextbox("Birth year").fill("1995"); await expect(grid.rows).toHaveCount(9); + // option() is page-scoped: the listbox is rendered by a sibling filter widget, not inside the grid root. await grid.headerCombobox("Color (enum)").click(); - await grid.option("Black").click({ delay: 1 }); + await page.getByRole("option", { name: "Black", exact: true }).click({ delay: 1 }); await expect(grid.rows).toHaveCount(4); await grid.headerCombobox("Roles (ref set)").click(); - await grid.option("Careers adviser").click({ delay: 1 }); + await page.getByRole("option", { name: "Careers adviser", exact: true }).click({ delay: 1 }); await expect(grid.rows).toHaveCount(3); await grid.headerCombobox("Company").click(); - await grid.option("Sierra Health Services Inc").click({ delay: 20 }); + await page.getByRole("option", { name: "Sierra Health Services Inc", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(2); const row = grid.rows.nth(1); diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js index 66089084a4..83c9c4e5dc 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringMulti.spec.js @@ -7,23 +7,24 @@ test.describe("datagrid-web filtering multi select", () => { test.beforeEach(async ({ page }) => { grid = new DataGridPage(page, "dataGrid21"); - await grid.open("/p/filtering-multi"); + await page.goto("/p/filtering-multi"); }); - test("filter rows where enum attribute equal to one of selected values", async () => { + test("filter rows where enum attribute equal to one of selected values", async ({ page }) => { await expect(grid.rows).toHaveCount(11); await expect(grid.columnCells(2).first()).toHaveText("Black"); await expect(grid.columnCells(2).last()).toHaveText("Blue"); - await grid.dropdownFilter(1).click(); - await grid.option("Pink").click({ delay: 20 }); + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter1[role="combobox"]').click(); + await page.getByRole("option", { name: "Pink", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(6); - await grid.option("Blush").click({ delay: 20 }); + await page.getByRole("option", { name: "Blush", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(8); await grid.headerCombobox("Color (enum)").click({ delay: 20 }); await expect(grid.columnCells(2)).toContainText(["Pink", "Pink", "Pink", "Blush", "Blush", "Pink", "Pink"]); }); - test("filter rows where ReferenceSet contains at least one of selected objects", async () => { + test("filter rows where ReferenceSet contains at least one of selected objects", async ({ page }) => { const expectedColumnText = [ "EconomistArmed forces officerTraderHealth service manager", "EconomistArmed forces officerTrader", @@ -36,23 +37,25 @@ test.describe("datagrid-web filtering multi select", () => { "Environmental scientistPublic librarianMaterials specialist" ]; await expect(grid.columnCells(3).first()).toHaveText(expectedColumnText[0]); - await grid.dropdownFilter(3).click(); - await grid.option("Economist").click({ delay: 20 }); + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter3[role="combobox"]').click(); + await page.getByRole("option", { name: "Economist", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(6); - await grid.option("Public librarian").click({ delay: 20 }); + await page.getByRole("option", { name: "Public librarian", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(10); - await grid.dropdownFilter(3).click({ delay: 20 }); + await page.locator('.mx-name-drop_downFilter3[role="combobox"]').click({ delay: 20 }); await expect(grid.columnCells(3)).toHaveText(expectedColumnText); }); - test("filter rows where Reference equal to one of selected objects", async () => { + test("filter rows where Reference equal to one of selected objects", async ({ page }) => { await expect(grid.rows).toHaveCount(11); await expect(grid.columnCells(4).first()).toHaveText("W.R. Berkley Corporation"); await expect(grid.columnCells(4).last()).toHaveText("PETsMART Inc"); - await grid.dropdownFilter(4).click({ delay: 20 }); - await grid.option("FMC Corp").click({ delay: 20 }); + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter4[role="combobox"]').click({ delay: 20 }); + await page.getByRole("option", { name: "FMC Corp", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(2); - await grid.option("ALLETE, Inc.").click({ delay: 20 }); + await page.getByRole("option", { name: "ALLETE, Inc.", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(6); await grid.headerCombobox("Company").click({ delay: 20 }); await expect(grid.columnCells(4)).toContainText([ diff --git a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js index 374b3195c7..ffadb7af51 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/filtering/DataGridFilteringSingle.spec.js @@ -7,7 +7,7 @@ test.describe("datagrid-web filtering single select", () => { test.beforeEach(async ({ page }) => { grid = new DataGridPage(page, "dataGrid21"); - await grid.open("/p/filtering-single"); + await page.goto("/p/filtering-single"); }); test("compares with a screenshot baseline and checks if all datagrid and filter elements are rendered as expected", async ({ @@ -18,9 +18,10 @@ test.describe("datagrid-web filtering single select", () => { await expect(page).toHaveScreenshot(`datagridFilteringSingle.png`); }); - test("filter rows that have Yes in Pets column", async () => { - await grid.dropdownFilter(2).click(); - await grid.option("Yes").click({ delay: 1 }); + test("filter rows that have Yes in Pets column", async ({ page }) => { + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter2[role="combobox"]').click(); + await page.getByRole("option", { name: "Yes", exact: true }).click({ delay: 1 }); await expect(grid.columnCells(3)).toHaveText([ "Yes", "Yes", @@ -35,17 +36,19 @@ test.describe("datagrid-web filtering single select", () => { ]); }); - test("filter rows that have No in Pets column", async () => { - await grid.dropdownFilter(2).click(); - await grid.option("No").click(); + test("filter rows that have No in Pets column", async ({ page }) => { + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter2[role="combobox"]').click(); + await page.getByRole("option", { name: "No", exact: true }).click(); await expect(grid.columnCells(3).first()).toHaveText("No"); const columnTexts = await grid.columnCells(3).allTextContents(); columnTexts.forEach(text => expect(text).toBe("No")); }); test("reset filter state when empty option is clicked", async ({ page }) => { - await grid.dropdownFilter(2).click(); - await grid.option("Yes").click({ delay: 20 }); + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter2[role="combobox"]').click(); + await page.getByRole("option", { name: "Yes", exact: true }).click({ delay: 20 }); await expect(grid.rows).toHaveCount(11); await expect(grid.columnCells(3)).toHaveText([ "Yes", @@ -59,7 +62,7 @@ test.describe("datagrid-web filtering single select", () => { "Yes", "Yes" ]); - await grid.dropdownFilter(2).click({ delay: 20 }); + await page.locator('.mx-name-drop_downFilter2[role="combobox"]').click({ delay: 20 }); await page.getByRole("row", { name: "Pets (bool)" }).getByRole("option").first().click(); await expect(grid.columnCells(3)).toHaveText([ "Yes", @@ -75,35 +78,39 @@ test.describe("datagrid-web filtering single select", () => { ]); }); - test("filter rows that have Cyan in Color column", async () => { - await grid.dropdownFilter(1).click(); - await grid.option("Cyan").click({ delay: 1 }); + test("filter rows that have Cyan in Color column", async ({ page }) => { + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter1[role="combobox"]').click(); + await page.getByRole("option", { name: "Cyan", exact: true }).click({ delay: 1 }); await expect(grid.rows).toHaveCount(6); const columnTexts = await grid.columnCells(2).allTextContents(); columnTexts.forEach(text => expect(text).toBe("Cyan")); }); - test("filter rows that have Black in Color column", async () => { - await grid.dropdownFilter(1).click(); - await grid.option("Black").click({ delay: 1 }); + test("filter rows that have Black in Color column", async ({ page }) => { + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter1[role="combobox"]').click(); + await page.getByRole("option", { name: "Black", exact: true }).click({ delay: 1 }); await expect(grid.rows).toHaveCount(9); const columnTexts = await grid.columnCells(2).allTextContents(); columnTexts.forEach(text => expect(text).toBe("Black")); }); - test("filter rows that match selected role", async () => { + test("filter rows that match selected role", async ({ page }) => { await expect(grid.rows).toHaveCount(11); - await grid.dropdownFilter(3).click(); - await grid.option("Trader").click({ delay: 1 }); + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter3[role="combobox"]').click(); + await page.getByRole("option", { name: "Trader", exact: true }).click({ delay: 1 }); await expect(grid.rows).toHaveCount(8); const columnTexts = await grid.columnCells(4).allTextContents(); columnTexts.forEach(text => expect(text).toContain("Trader")); }); - test("filter rows that match selected company", async () => { + test("filter rows that match selected company", async ({ page }) => { await expect(grid.rows).toHaveCount(11); - await grid.dropdownFilter(4).click(); - await grid.option("PETsMART Inc").click({ delay: 1 }); + // drop_downFilter widgets are siblings of the grid, not children — page-scoped selector is required. + await page.locator('.mx-name-drop_downFilter4[role="combobox"]').click(); + await page.getByRole("option", { name: "PETsMART Inc", exact: true }).click({ delay: 1 }); await expect(grid.rows).toHaveCount(9); const columnTexts = await grid.columnCells(5).allTextContents(); columnTexts.forEach(text => expect(text).toBe("PETsMART Inc")); diff --git a/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js index 7c6a74cac8..8493faf079 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/pages/DataGridPage.js @@ -2,11 +2,10 @@ * Page Object Model for the datagrid-web widget E2E tests. * * A `DataGridPage` wraps a single Data Grid instance (identified by its - * Mendix `mx-name`) and exposes the locators and actions the specs need, - * so tests read as behavior rather than raw selectors. - * - * Navigation relies on the custom fixture, which auto-waits for the Mendix - * app to be ready after every `page.goto()` — no manual `waitForMendixApp`. + * Mendix `mx-name`) and exposes locators and actions scoped to that widget's + * DOM subtree. Navigation (`page.goto`) and page-level selectors (e.g. + * drop-down filter widgets that are siblings of the grid, not children) stay + * in the spec's `beforeEach` / test body — this class has no `this.page`. */ export class DataGridPage { /** @@ -14,16 +13,9 @@ export class DataGridPage { * @param {string} [name="datagrid1"] the grid's mx-name (without the `.mx-name-` prefix) */ constructor(page, name = "datagrid1") { - this.page = page; - this.name = name; this.root = page.locator(`.mx-name-${name}`); } - /** Navigate to a page; the fixture auto-waits for Mendix readiness. */ - async open(path = "/") { - await this.page.goto(path); - } - // --- Columns & headers ------------------------------------------------- get columnHeaders() { @@ -85,16 +77,6 @@ export class DataGridPage { // --- Filters ----------------------------------------------------------- - /** A named drop-down filter widget rendered as a combobox. */ - dropdownFilter(n) { - return this.page.locator(`.mx-name-drop_downFilter${n}[role="combobox"]`); - } - - /** An option in an open listbox, matched by exact accessible name. */ - option(label) { - return this.page.getByRole("option", { name: label, exact: true }); - } - /** Column-header-scoped filter controls (used by header-embedded filters). */ headerCombobox(name) { return this.root.getByRole("columnheader", { name }).getByRole("combobox");