|
| 1 | +You are helping the developer write or improve Playwright e2e specs for a Mendix pluggable widget in the web-widgets monorepo. |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +- E2e specs live at: `packages/pluggableWidgets/<widget>/e2e/*.spec.js` |
| 6 | +- Specs run against a live Mendix runtime (Docker CI or Studio Pro dev mode) |
| 7 | +- Playwright strict mode is ON — a locator that resolves to multiple elements throws immediately |
| 8 | +- Each test opens a new Mendix session; the license allows max 5 concurrent sessions |
| 9 | + |
| 10 | +## Mandatory boilerplate |
| 11 | + |
| 12 | +Every spec file must include a session cleanup hook: |
| 13 | + |
| 14 | +```js |
| 15 | +import { test, expect } from "@playwright/test"; |
| 16 | + |
| 17 | +test.afterEach("Cleanup session", async ({ page }) => { |
| 18 | + // Force logout after every test — Mendix limits concurrent sessions to 5 |
| 19 | + await page.evaluate(() => window.mx.session.logout()); |
| 20 | +}); |
| 21 | +``` |
| 22 | + |
| 23 | +Without this, sessions accumulate and later tests will fail with a session-limit error. |
| 24 | + |
| 25 | +## Navigating to pages |
| 26 | + |
| 27 | +**Option A — Direct URL (use when the page has a URL slug configured in Studio Pro):** |
| 28 | + |
| 29 | +```js |
| 30 | +test.beforeEach(async ({ page }) => { |
| 31 | + await page.goto("/p/my-page-slug"); |
| 32 | +}); |
| 33 | +``` |
| 34 | + |
| 35 | +**Option B — Navigation menu (use when no URL slug exists — this is the safe default):** |
| 36 | + |
| 37 | +```js |
| 38 | +async function navigateViaMenu(page, menuLabel, itemLabel) { |
| 39 | + await page.goto("/"); |
| 40 | + await page.waitForLoadState("networkidle"); |
| 41 | + await page.getByRole("menuitem", { name: menuLabel }).click(); |
| 42 | + await page.getByRole("menuitem", { name: itemLabel }).click(); |
| 43 | + await page.waitForLoadState("networkidle"); |
| 44 | +} |
| 45 | + |
| 46 | +test.beforeEach(async ({ page }) => { |
| 47 | + await navigateViaMenu(page, "Different Views", "Listen To Grid"); |
| 48 | +}); |
| 49 | +``` |
| 50 | + |
| 51 | +Get the exact menu labels from the test project: |
| 52 | + |
| 53 | +```bash |
| 54 | +bash automation/mxcli/mx-testproject.sh inspect <widget> |
| 55 | +# Navigation is included at the end of inspect output. |
| 56 | +# Or run directly: |
| 57 | +bash automation/mxcli/mx-testproject.sh exec <widget> "DESCRIBE NAVIGATION Responsive" |
| 58 | +``` |
| 59 | + |
| 60 | +Always call `waitForLoadState("networkidle")` after navigation — Mendix pages fire multiple XHR requests on load and widgets may not be ready yet. |
| 61 | + |
| 62 | +## Selectors |
| 63 | + |
| 64 | +**Use `.mx-name-<widgetName>` selectors** — they are stable across Mendix versions and match how widgets are named in Studio Pro. |
| 65 | + |
| 66 | +```js |
| 67 | +// Good |
| 68 | +page.locator(".mx-name-badgeDanger"); |
| 69 | +page.locator(".mx-name-dataGrid1"); |
| 70 | + |
| 71 | +// Avoid — brittle, tied to widget internals |
| 72 | +page.locator(".widget-badge span:first-child"); |
| 73 | +``` |
| 74 | + |
| 75 | +To discover all `.mx-name-*` widget names available on a page, describe the page via mxcli: |
| 76 | + |
| 77 | +```bash |
| 78 | +bash automation/mxcli/mx-testproject.sh exec <widget> "DESCRIBE PAGE <Module>.<PageName>" |
| 79 | +``` |
| 80 | + |
| 81 | +If you don't know the page name, first run `bash automation/mxcli/mx-testproject.sh list-pages <widget>`. |
| 82 | + |
| 83 | +### Strict mode — multiple matches |
| 84 | + |
| 85 | +A widget placed inside a list view or repeated container will produce multiple `.mx-name-*` elements. Playwright strict mode throws if your locator resolves to more than one element. Fix with `.first()` or a compound selector: |
| 86 | + |
| 87 | +```js |
| 88 | +// Throws if badgeV23 appears more than once: |
| 89 | +await expect(page.locator(".mx-name-badgeV23")).toBeVisible(); // ❌ |
| 90 | + |
| 91 | +// Safe: |
| 92 | +await expect(page.locator(".mx-name-badgeV23").first()).toBeVisible(); // ✓ |
| 93 | + |
| 94 | +// Or narrow with a compound class (badge variant only, not label variant): |
| 95 | +await expect(page.locator(".widget-badge.badge.mx-name-badgeV23")).toBeVisible(); // ✓ |
| 96 | +``` |
| 97 | + |
| 98 | +The badge widget renders two `mx-name-*` elements when both a `badge` type and a `label` type share the same Studio Pro widget name — always check with `DESCRIBE PAGE` if you get unexpected strict-mode errors. |
| 99 | + |
| 100 | +## Asserting content |
| 101 | + |
| 102 | +**Wait for visibility before asserting text** — data-bound widgets may still be loading: |
| 103 | + |
| 104 | +```js |
| 105 | +const badge = page.locator(".mx-name-badgeDanger"); |
| 106 | +await expect(badge).toBeVisible(); |
| 107 | +await expect(badge).toHaveText("Expected text"); |
| 108 | +``` |
| 109 | + |
| 110 | +**For async state updates** (e.g., a data view that reloads when a grid row is selected), use a retrying assertion with a generous timeout instead of reading text synchronously: |
| 111 | + |
| 112 | +```js |
| 113 | +// ❌ Synchronous read — may race with data view update: |
| 114 | +const text = await badge.textContent(); |
| 115 | +expect(text?.trim().length).toBeGreaterThan(0); |
| 116 | + |
| 117 | +// ✓ Auto-retrying assertion — waits up to 10 s for non-empty content: |
| 118 | +await expect(badge).toHaveText(/.+/, { timeout: 10000 }); |
| 119 | +``` |
| 120 | +
|
| 121 | +**Do not assert Atlas design classes** (e.g., `badge-danger`, `btn-primary`) — these are applied via Atlas design properties configured in Studio Pro and may or may not produce a matching CSS class depending on the Atlas version in the test project. Assert visibility and text content instead: |
| 122 | +
|
| 123 | +```js |
| 124 | +// ❌ Fragile — Atlas class may not be on the element: |
| 125 | +await expect(badge).toHaveClass(/badge-danger/); |
| 126 | + |
| 127 | +// ✓ Robust — tests what the widget actually does: |
| 128 | +await expect(badge).toBeVisible(); |
| 129 | +``` |
| 130 | +
|
| 131 | +## Structuring tests |
| 132 | +
|
| 133 | +Group tests with `test.describe` by feature area. A good split for widget specs: |
| 134 | +
|
| 135 | +``` |
| 136 | +<widget>.spec.js — core behavior: rendering, data binding, visual snapshot |
| 137 | +onClick.spec.js — microflow, nanoflow, keyboard accessibility |
| 138 | +dataTypes.spec.js — each supported Mendix attribute type |
| 139 | +layouts.spec.js — widget behavior inside list view, tab, data view, listen-to-grid |
| 140 | +``` |
| 141 | +
|
| 142 | +Keep each spec file focused. Avoid one giant file — Playwright runs files in parallel and smaller files improve isolation. |
| 143 | +
|
| 144 | +### Visual snapshots |
| 145 | +
|
| 146 | +Use sparingly — only for the main page to catch unintended rendering regressions: |
| 147 | +
|
| 148 | +```js |
| 149 | +test("visual comparison", async ({ page }) => { |
| 150 | + await expect(page.locator(".mx-name-badgeDanger")).toBeVisible(); |
| 151 | + await expect(page).toHaveScreenshot("badge.png"); |
| 152 | +}); |
| 153 | +``` |
| 154 | +
|
| 155 | +When a data mutation in a prior test changes the snapshot baseline, update it: |
| 156 | +
|
| 157 | +```bash |
| 158 | +# Re-run only the visual test and update the snapshot: |
| 159 | +pnpm --filter @mendix/<widget> run e2e --no-setup-project --no-update-project -- --update-snapshots |
| 160 | +``` |
| 161 | +
|
| 162 | +The snapshot file lives at `e2e/<spec>.spec.js-snapshots/<name>-chromium-darwin.png`. |
| 163 | +
|
| 164 | +## onClick patterns |
| 165 | +
|
| 166 | +**Nanoflow — verify via dialog:** |
| 167 | +
|
| 168 | +```js |
| 169 | +test("clicking badge calls nanoflow and shows result in dialog", async ({ page }) => { |
| 170 | + await page.locator(".mx-name-badgeCallNanoflow").click(); |
| 171 | + await expect(page.locator(".modal-body")).toBeVisible(); |
| 172 | + await expect( |
| 173 | + page |
| 174 | + .locator("div") |
| 175 | + .filter({ hasText: /^Data stringNewSuccess$/ }) |
| 176 | + .locator("div") |
| 177 | + ).toContainText("NewSuccess"); |
| 178 | +}); |
| 179 | +``` |
| 180 | +
|
| 181 | +**Keyboard accessibility:** |
| 182 | +
|
| 183 | +```js |
| 184 | +test("badge is keyboard accessible — Enter key triggers nanoflow", async ({ page }) => { |
| 185 | + await page.locator(".mx-name-badgeCallNanoflow").focus(); |
| 186 | + await page.keyboard.press("Enter"); |
| 187 | + await expect(page.locator(".modal-body")).toBeVisible(); |
| 188 | +}); |
| 189 | +``` |
| 190 | +
|
| 191 | +**Microflow smoke test (no dialog expected):** |
| 192 | +
|
| 193 | +```js |
| 194 | +test("clicking badge calls microflow without error", async ({ page }) => { |
| 195 | + const badge = page.locator(".mx-name-badgeV22"); |
| 196 | + await expect(badge).toBeVisible(); |
| 197 | + await badge.click(); |
| 198 | + // Microflow click should not throw — page stays intact after click |
| 199 | + await expect(badge).toBeVisible(); |
| 200 | +}); |
| 201 | +``` |
| 202 | +
|
| 203 | +## Running tests |
| 204 | +
|
| 205 | +```bash |
| 206 | +# Full CI run (Docker — downloads project, builds widget, runs Playwright): |
| 207 | +pnpm --filter @mendix/<widget> run e2e |
| 208 | + |
| 209 | +# Skip re-downloading the test project: |
| 210 | +pnpm --filter @mendix/<widget> run e2e --no-setup-project |
| 211 | + |
| 212 | +# Also skip rebuilding the widget MPK: |
| 213 | +pnpm --filter @mendix/<widget> run e2e --no-setup-project --no-update-project |
| 214 | +``` |
| 215 | +
|
| 216 | +⚠️ Pass flags directly — do NOT use `pnpm run e2e -- --no-setup-project`. The `--` separator causes yargs-parser to ignore the flags. |
| 217 | +
|
| 218 | +## Checklist before marking a spec done |
| 219 | +
|
| 220 | +- [ ] `test.afterEach` with `window.mx.session.logout()` is present in every file |
| 221 | +- [ ] No locator resolves to more than one element without `.first()` or a compound selector |
| 222 | +- [ ] No assertions on Atlas design classes (e.g., `.toHaveClass(/badge-danger/)`) |
| 223 | +- [ ] Async state updates use retrying assertions (`toHaveText`, `toBeVisible`) not synchronous reads |
| 224 | +- [ ] `waitForLoadState("networkidle")` called after every menu-based navigation |
| 225 | +- [ ] All 32+ tests pass in CI (`pnpm run e2e`) |
0 commit comments