Skip to content

Commit 00441bb

Browse files
E2e reliability and performance (#2201)
2 parents cbe74b4 + 663cb6a commit 00441bb

81 files changed

Lines changed: 707 additions & 732 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/RunE2ENightly.yml

Lines changed: 61 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,66 @@ name: Run E2E test nightly
22
# This workflow is used to test our widgets nightly.
33

44
on:
5-
schedule:
6-
# At 02:00 on every day-of-week.
7-
- cron: "0 02 * * 1-5"
5+
schedule:
6+
# At 02:00 on every day-of-week.
7+
- cron: "0 02 * * 1-5"
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
812

913
jobs:
10-
e2e:
11-
name: Run automated end-to-end tests nightly
12-
runs-on: ubuntu-latest
13-
14-
permissions:
15-
packages: read
16-
contents: read
17-
18-
steps:
19-
- name: Checkout
20-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21-
with:
22-
fetch-depth: 0
23-
- name: Setup pnpm
24-
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
25-
26-
- name: Setup node
27-
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
28-
with:
29-
node-version-file: ".nvmrc"
30-
cache: "pnpm"
31-
32-
- name: Install dependencies
33-
run: pnpm install
34-
35-
- name: Install Playwright Browsers
36-
run: pnpm exec playwright install --with-deps chromium
37-
38-
- name: Executing E2E tests
39-
env:
40-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41-
run: pnpm -r --workspace-concurrency=1 --no-bail run e2e
42-
43-
- name: Fixing files permissions
44-
if: failure()
45-
run: |
46-
sudo find ${{ github.workspace }}/packages/* -type d -exec chmod 755 {} \;
47-
sudo find ${{ github.workspace }}/packages/* -type f -exec chmod 644 {} \;
48-
49-
- name: Archive test screenshot diff results
50-
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
51-
if: failure()
52-
with:
53-
name: test-screenshot-results
54-
path: |
55-
${{ github.workspace }}/packages/**/**/test-results/**/*.png
56-
${{ github.workspace }}/packages/**/**/test-results/**/*.zip
57-
if-no-files-found: error
14+
e2e:
15+
name: Run automated end-to-end tests nightly
16+
runs-on: ubuntu-latest
17+
18+
permissions:
19+
packages: read
20+
contents: read
21+
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
index: [0, 1, 2, 3]
26+
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
30+
with:
31+
fetch-depth: 0
32+
- name: Setup pnpm
33+
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
34+
35+
- name: Setup node
36+
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
37+
with:
38+
node-version-file: ".nvmrc"
39+
cache: "pnpm"
40+
41+
- name: Install dependencies
42+
run: pnpm install
43+
44+
- name: Install Playwright Browsers
45+
run: pnpm exec playwright install --with-deps chromium
46+
47+
- name: Executing E2E tests
48+
env:
49+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50+
run: >-
51+
node ./automation/run-e2e/bin/run-e2e-in-chunks.mjs --chunks 4 --index ${{ matrix.index }} --event-name ${{ github.event_name }}
52+
53+
- name: Fixing files permissions
54+
if: failure()
55+
run: |
56+
sudo find ${{ github.workspace }}/packages/* -type d -exec chmod 755 {} \;
57+
sudo find ${{ github.workspace }}/packages/* -type f -exec chmod 644 {} \;
58+
59+
- name: Archive test screenshot diff results
60+
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
61+
if: failure()
62+
with:
63+
name: test-screenshot-results-${{ matrix.index }}
64+
path: |
65+
${{ github.workspace }}/packages/**/**/test-results/**/*.png
66+
${{ github.workspace }}/packages/**/**/test-results/**/*.zip
67+
if-no-files-found: error

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ docs/requirements/ -> Detailed technical requirements
3333
- Jest + RTL for unit tests (src/\*_/**tests**/_.spec.ts)
3434
- Playwright for E2E (e2e/\*.spec.js)
3535

36+
## E2E Test Rules (Playwright)
37+
38+
- docs/requirements/e2e-test-guidelines.md — Full rules + template
39+
3640
## Development Setup
3741

3842
- Node >=22, pnpm 10.x
@@ -49,6 +53,7 @@ docs/requirements/ -> Detailed technical requirements
4953
## Documentation
5054

5155
Essential reading (consult for any widget work):
56+
5257
- docs/repo-layout.md — To understand the repository
5358
- docs/requirements/tech-stack.md — Full technology stack
5459
- docs/requirements/frontend-guidelines.md — CSS/SCSS/Atlas UI guidelines
@@ -57,8 +62,10 @@ Essential reading (consult for any widget work):
5762
- docs/requirements/project-requirements-document.md — Goals and scope
5863

5964
Reference (consult on demand for specific tasks):
65+
6066
- docs/requirements/implementation-plan.md — New widget guide + PR template
6167
- docs/requirements/widget-to-module.md — Widget-to-module conversion guide
68+
- docs/requirements/e2e-test-guidelines.md — E2E test reliability rules + template
6269

6370
## Agent-Specific Instructions
6471

automation/run-e2e/eslint.config.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defineConfig } from "eslint/config";
22
import globals from "globals";
33
import js from "@eslint/js";
4+
import playwright from "eslint-plugin-playwright";
45

56
export default defineConfig([
67
{
@@ -21,5 +22,14 @@ export default defineConfig([
2122
rules: {
2223
"no-unused-vars": "warn"
2324
}
25+
},
26+
{
27+
files: ["**/e2e/**/*.spec.{,m,c}js"],
28+
plugins: { playwright },
29+
rules: {
30+
"playwright/no-wait-for-timeout": "error",
31+
"playwright/no-networkidle": "warn",
32+
"playwright/prefer-web-first-assertions": "warn"
33+
}
2434
}
2535
]);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* eslint-disable no-undef */
2+
import { test as base, expect } from "@playwright/test";
3+
4+
async function waitForMendixApp(page, timeout = 60_000) {
5+
await page.waitForLoadState("domcontentloaded");
6+
await page.waitForFunction(
7+
() =>
8+
Boolean(window.mx?.session) &&
9+
!document.querySelector(".mx-progress-indicator") &&
10+
document.querySelector(".mx-page") !== null,
11+
undefined,
12+
{ timeout }
13+
);
14+
}
15+
16+
export { expect };
17+
18+
export const test = base.extend({
19+
mendixSession: [
20+
async ({ browser }, use) => {
21+
const context = await browser.newContext();
22+
const page = await context.newPage();
23+
const originalGoto = page.goto.bind(page);
24+
page.goto = async (url, options) => {
25+
const response = await originalGoto(url, options);
26+
await waitForMendixApp(page);
27+
return response;
28+
};
29+
await use({ context, page });
30+
await page.evaluate(() => window.mx?.session?.logout?.()).catch(() => {});
31+
await context.close();
32+
},
33+
{ scope: "worker" }
34+
],
35+
page: async ({ mendixSession }, use) => {
36+
await use(mendixSession.page);
37+
}
38+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* eslint-disable no-undef */
2+
import { expect } from "@playwright/test";
3+
4+
export async function waitForMendixApp(page, timeout = 60_000) {
5+
await page.waitForLoadState("domcontentloaded");
6+
await page.waitForFunction(
7+
() =>
8+
Boolean(window.mx?.session) &&
9+
!document.querySelector(".mx-progress-indicator") &&
10+
document.querySelector(".mx-page") !== null,
11+
undefined,
12+
{ timeout }
13+
);
14+
}
15+
16+
export async function waitForDataReady(page, timeout = 60_000) {
17+
await waitForMendixApp(page, timeout);
18+
await page.waitForLoadState("networkidle");
19+
}
20+
21+
export async function waitForWidget(page, mxName, timeout = 15_000) {
22+
const locator = page.locator(`.mx-name-${mxName}`);
23+
await expect(locator).toBeVisible({ timeout });
24+
return locator;
25+
}
26+
27+
export async function waitForListData(page, mxName, minRows = 1, timeout = 15_000) {
28+
const container = page.locator(`.mx-name-${mxName}`);
29+
await expect(container).toBeVisible({ timeout });
30+
const rows = container.locator("[class*='item'], tr[class*='row'], [class*='gallery-item']");
31+
await expect(rows).toHaveCount(minRows, { timeout });
32+
return rows;
33+
}
34+
35+
export async function safeLogout(page) {
36+
await page.evaluate(() => window.mx?.session?.logout?.()).catch(() => {});
37+
}
38+
39+
export async function navigateToPage(page, path, timeout = 30_000) {
40+
await page.goto(path);
41+
await waitForMendixApp(page, timeout);
42+
}
43+
44+
export async function checkAccessibility(page, selector, options = {}) {
45+
const AxeBuilder = (await import("@axe-core/playwright")).default;
46+
let builder = new AxeBuilder({ page }).withTags(options.tags || ["wcag21aa"]);
47+
if (selector) {
48+
builder = builder.include(selector);
49+
}
50+
if (options.exclude) {
51+
for (const sel of [].concat(options.exclude)) {
52+
builder = builder.exclude(sel);
53+
}
54+
}
55+
const results = await builder.analyze();
56+
expect(results.violations).toEqual([]);
57+
}

automation/run-e2e/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"run-e2e": "bin/run-e2e.mjs"
1313
},
1414
"type": "module",
15+
"exports": {
16+
"./fixtures": "./lib/fixtures.mjs",
17+
"./mendix-helpers": "./lib/mendix-helpers.mjs",
18+
"./playwright.config.cjs": "./playwright.config.cjs"
19+
},
1520
"scripts": {
1621
"format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .",
1722
"lint": "eslint .",

automation/run-e2e/playwright.config.cjs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ module.exports = defineConfig({
99
fullyParallel: true,
1010
/* Fail the build on CI if you accidentally left test.only in the source code. */
1111
forbidOnly: !!process.env.CI,
12+
/* Filter tests by tag: E2E_SUITE=smoke runs only @smoke-tagged tests */
13+
grep: process.env.E2E_SUITE === "smoke" ? /@smoke/ : undefined,
1214
/* Retry on CI only */
1315
retries: process.env.CI ? 2 : 0,
14-
/* Use 4 workers on CI – the runner has multiple cores and each widget's tests
15-
* are independent, so parallel execution cuts per-widget runtime significantly. */
16-
workers: process.env.CI ? 4 : undefined,
16+
/* Worker-scoped session: each worker holds 1 Mendix session. Safe up to 4 workers
17+
* against the 5-session developer license (leaves 1 headroom). */
18+
workers: process.env.CI ? 4 : 2,
1719
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
1820
reporter: [
1921
["list"],
@@ -35,11 +37,20 @@ module.exports = defineConfig({
3537
reuseExistingServer: !process.env.CI
3638
}
3739
], */
40+
expect: {
41+
toHaveScreenshot: {
42+
animations: "disabled",
43+
threshold: 0.1
44+
}
45+
},
3846
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
3947
use: {
4048
/* Base URL to use in actions like `await page.goto('/')`. */
4149
baseURL: process.env.URL ? process.env.URL : "http://127.0.0.1:8080",
4250

51+
actionTimeout: 10_000,
52+
navigationTimeout: 30_000,
53+
4354
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
4455
trace: "on-first-retry",
4556

0 commit comments

Comments
 (0)