From f620a4af3c986467a188733acfaf1be4cdc9b69e Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Wed, 12 Aug 2026 13:35:48 -0300 Subject: [PATCH 01/18] Migrate wrappers e2e tests from TestCafe to Playwright --- .github/workflows/wrapper_tests_e2e.yml | 10 +- apps/react/package.json | 1 - apps/react/project.json | 1 - apps/react/runner.js | 17 --- apps/react/test.js | 8 -- apps/vue/package.json | 1 - apps/vue/project.json | 1 - apps/vue/runner.js | 17 --- apps/vue/test.js | 8 -- e2e/wrappers/.gitignore | 2 + e2e/wrappers/README.md | 44 ++++++++ e2e/wrappers/docker/Dockerfile | 14 +++ e2e/wrappers/docker/run.sh | 31 +++++ e2e/wrappers/fixtures.ts | 13 +++ e2e/wrappers/package.json | 9 +- e2e/wrappers/playwright.config.ts | 46 ++++++++ e2e/wrappers/runner.js | 106 ------------------ e2e/wrappers/serve.js | 38 +++++++ e2e/wrappers/test-helpers.js | 22 ---- e2e/wrappers/tests/button.spec.ts | 11 ++ e2e/wrappers/tests/button.test.js | 11 -- .../tests/chat-template-rerender.spec.ts | 25 +++++ .../tests/chat-template-rerender.test.js | 21 ---- .../tests/gantt-template-state-update.spec.ts | 19 ++++ .../tests/gantt-template-state-update.test.js | 17 --- .../tests/inputs-list-in-form.spec.ts | 22 ++++ .../tests/inputs-list-in-form.test.js | 22 ---- .../tests/select-box-nested-validator.spec.ts | 39 +++++++ .../tests/select-box-nested-validator.test.js | 41 ------- e2e/wrappers/tests/textbox.spec.ts | 27 +++++ e2e/wrappers/tests/textbox.test.js | 25 ----- pnpm-lock.yaml | 47 +++++++- pnpm-workspace.yaml | 1 + 33 files changed, 384 insertions(+), 333 deletions(-) delete mode 100644 apps/react/runner.js delete mode 100644 apps/react/test.js delete mode 100644 apps/vue/runner.js delete mode 100644 apps/vue/test.js create mode 100644 e2e/wrappers/.gitignore create mode 100644 e2e/wrappers/README.md create mode 100644 e2e/wrappers/docker/Dockerfile create mode 100755 e2e/wrappers/docker/run.sh create mode 100644 e2e/wrappers/fixtures.ts create mode 100644 e2e/wrappers/playwright.config.ts delete mode 100644 e2e/wrappers/runner.js create mode 100644 e2e/wrappers/serve.js delete mode 100644 e2e/wrappers/test-helpers.js create mode 100644 e2e/wrappers/tests/button.spec.ts delete mode 100644 e2e/wrappers/tests/button.test.js create mode 100644 e2e/wrappers/tests/chat-template-rerender.spec.ts delete mode 100644 e2e/wrappers/tests/chat-template-rerender.test.js create mode 100644 e2e/wrappers/tests/gantt-template-state-update.spec.ts delete mode 100644 e2e/wrappers/tests/gantt-template-state-update.test.js create mode 100644 e2e/wrappers/tests/inputs-list-in-form.spec.ts delete mode 100644 e2e/wrappers/tests/inputs-list-in-form.test.js create mode 100644 e2e/wrappers/tests/select-box-nested-validator.spec.ts delete mode 100644 e2e/wrappers/tests/select-box-nested-validator.test.js create mode 100644 e2e/wrappers/tests/textbox.spec.ts delete mode 100644 e2e/wrappers/tests/textbox.test.js diff --git a/.github/workflows/wrapper_tests_e2e.yml b/.github/workflows/wrapper_tests_e2e.yml index fff07fe970d1..336fedc81790 100644 --- a/.github/workflows/wrapper_tests_e2e.yml +++ b/.github/workflows/wrapper_tests_e2e.yml @@ -170,12 +170,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Start server for ${{ matrix.framework }} - working-directory: e2e/wrappers - run: | - pnpm run start:${{ matrix.framework }} & - sleep 10 - - name: Run tests for ${{ matrix.framework }} working-directory: e2e/wrappers run: pnpm run test:${{ matrix.framework }} @@ -185,5 +179,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: test-fails-${{matrix.framework}} - path: e2e/wrappers/screenshots + path: | + e2e/wrappers/playwright-report + e2e/wrappers/test-results if-no-files-found: ignore diff --git a/apps/react/package.json b/apps/react/package.json index 626d1c0e6cbf..096562318540 100644 --- a/apps/react/package.json +++ b/apps/react/package.json @@ -27,7 +27,6 @@ "webpack-dev-server": "5.2.6" }, "scripts": { - "test": "node runner.js", "start": "webpack-dev-server", "build": "webpack" }, diff --git a/apps/react/project.json b/apps/react/project.json index 42973c8ad028..5dfa1a454156 100644 --- a/apps/react/project.json +++ b/apps/react/project.json @@ -15,7 +15,6 @@ "!{projectRoot}/public/js/app/bundle*", "{projectRoot}/*.tsx", "{projectRoot}/*.js", - "!{projectRoot}/test.js", "{projectRoot}/tsconfig.json", "{workspaceRoot}/tsconfig.json" ], diff --git a/apps/react/runner.js b/apps/react/runner.js deleted file mode 100644 index ed29b45f45c2..000000000000 --- a/apps/react/runner.js +++ /dev/null @@ -1,17 +0,0 @@ -const process = require('process'); -const createTestCafe = require('testcafe'); - -let testCafe; -createTestCafe('127.0.0.1') - .then(tsc => { - testCafe = tsc; - - const runner = tsc.createRunner(); - return runner - .src('test.js') - .browsers('chrome:headless --disable-gpu --window-size=1200,800') - .run(); - }).then(failedCount => { - testCafe.close(); - process.exit(failedCount); - }); diff --git a/apps/react/test.js b/apps/react/test.js deleted file mode 100644 index e7d5cbb051bb..000000000000 --- a/apps/react/test.js +++ /dev/null @@ -1,8 +0,0 @@ -const path = require('path'); - -fixture('DevExtreme React Playground') - .page(path.resolve(__dirname, 'public/index.html')); - -test('App has no errors', async (t) => { - -}); diff --git a/apps/vue/package.json b/apps/vue/package.json index c20c2aa1cf35..b314f15ab54f 100644 --- a/apps/vue/package.json +++ b/apps/vue/package.json @@ -33,7 +33,6 @@ "webpack-dev-server": "5.2.6" }, "scripts": { - "test": "node runner.js", "start": "webpack-dev-server", "build": "webpack" } diff --git a/apps/vue/project.json b/apps/vue/project.json index 0a42ced30f0c..14a07d7f08d6 100644 --- a/apps/vue/project.json +++ b/apps/vue/project.json @@ -16,7 +16,6 @@ "{projectRoot}/*.ts", "{projectRoot}/*.vue", "{projectRoot}/*.js", - "!{projectRoot}/test.js", "{projectRoot}/tsconfig.json" ], "outputs": [ diff --git a/apps/vue/runner.js b/apps/vue/runner.js deleted file mode 100644 index 953758023537..000000000000 --- a/apps/vue/runner.js +++ /dev/null @@ -1,17 +0,0 @@ -const process = require('process'); -const createTestCafe = require('testcafe'); - -let testCafe; -createTestCafe('localhost') - .then(tsc => { - testCafe = tsc; - - const runner = tsc.createRunner(); - return runner - .src('test.js') - .browsers('chrome:headless --disable-gpu --window-size=1200,800') - .run(); - }).then(failedCount => { - testCafe.close(); - process.exit(failedCount); - }); diff --git a/apps/vue/test.js b/apps/vue/test.js deleted file mode 100644 index 5cf6e3127730..000000000000 --- a/apps/vue/test.js +++ /dev/null @@ -1,8 +0,0 @@ -const path = require('path'); - -fixture('DevExtreme Vue Playground') - .page(path.resolve(__dirname, 'public/index.html')); - -test('App has no errors', async (t) => { - -}); diff --git a/e2e/wrappers/.gitignore b/e2e/wrappers/.gitignore new file mode 100644 index 000000000000..5c4ffa21fa5e --- /dev/null +++ b/e2e/wrappers/.gitignore @@ -0,0 +1,2 @@ +playwright-report/ +test-results/ diff --git a/e2e/wrappers/README.md b/e2e/wrappers/README.md new file mode 100644 index 000000000000..3e2de13330db --- /dev/null +++ b/e2e/wrappers/README.md @@ -0,0 +1,44 @@ +# Wrappers E2E tests + +End-to-end tests that check DevExtreme components inside the React, Vue and Angular wrappers. +The tests are written with [Playwright](https://playwright.dev/) and use the system Google Chrome +(`channel: 'chrome'`), so no browser download is needed. + +## Layout + +| Path | Purpose | +|------------------------|----------------------------------------------------------------------| +| `builders/*` | Host applications that render the examples for each framework | +| `examples/*` | Examples under test, one folder per scenario and framework | +| `tests/*.spec.ts` | Playwright specs | +| `fixtures.ts` | `test`/`expect` with the `framework` option added | +| `playwright.config.ts` | Per-framework port, base URL and dev server | +| `serve.js` | Static server for a built application | +| `docker/` | Container that matches the CI environment | + +## Run locally + +```bash +pnpm install +pnpm nx all:build-testing workflows + +cd e2e/wrappers +pnpm run build:react19 # or build:vue3 / build:angular / build:all + +pnpm run test:react19 # or test:vue3 / test:angular +``` + +`playwright test` starts `serve.js` on its own, so no separate server is needed. +Useful flags: `--headed`, `--debug`, `--ui`, `--reporter=html`. + +Examples that exist for a single framework only (Chat, Gantt) are skipped in the other frameworks. + +## Run in the CI environment + +The container pins Node and Google Chrome to the versions used by the `Wrappers E2E Tests` +workflow. Dependencies and application builds are taken from the host: + +```bash +cd e2e/wrappers +docker/run.sh react19 +``` diff --git a/e2e/wrappers/docker/Dockerfile b/e2e/wrappers/docker/Dockerfile new file mode 100644 index 000000000000..bcde5274689c --- /dev/null +++ b/e2e/wrappers/docker/Dockerfile @@ -0,0 +1,14 @@ +# Reproduces the "Wrappers E2E Tests" CI environment: Node from .node-version +# and Google Chrome pinned to the version used by .github/actions/setup-chrome. +FROM node:24.16.0-bookworm-slim + +ARG CHROME_VERSION=150.0.7871.181 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl \ + && curl -fsSL "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}-1_amd64.deb" -o /tmp/chrome.deb \ + && apt-get install -y --no-install-recommends /tmp/chrome.deb \ + && rm /tmp/chrome.deb \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /repo/e2e/wrappers diff --git a/e2e/wrappers/docker/run.sh b/e2e/wrappers/docker/run.sh new file mode 100755 index 000000000000..8e080ac76a51 --- /dev/null +++ b/e2e/wrappers/docker/run.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Runs the wrappers e2e tests in a container that matches the CI environment. +# The repository is mounted as is, so dependencies must be installed and +# the app under test built on the host first: +# +# pnpm install +# pnpm nx all:build-testing workflows +# cd e2e/wrappers && pnpm run build:react19 +# +# Usage: docker/run.sh [react19|vue3|angular] [extra playwright args] + +set -euo pipefail + +FRAMEWORK="${1:-react19}" +shift || true + +DOCKER_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$DOCKER_DIR/../../.." && pwd)" + +# Google Chrome for Linux ships for amd64 only, so the platform is pinned as on CI. +PLATFORM=linux/amd64 + +docker build --platform "$PLATFORM" -t devextreme-wrappers-e2e "$DOCKER_DIR" + +docker run --rm --ipc=host --platform "$PLATFORM" \ + -v "$REPO_ROOT:/repo" \ + -w /repo/e2e/wrappers \ + -e CI=true \ + -e "FRAMEWORK=$FRAMEWORK" \ + devextreme-wrappers-e2e \ + node_modules/.bin/playwright test "$@" diff --git a/e2e/wrappers/fixtures.ts b/e2e/wrappers/fixtures.ts new file mode 100644 index 000000000000..3ce3100c6f9d --- /dev/null +++ b/e2e/wrappers/fixtures.ts @@ -0,0 +1,13 @@ +import { test as base } from '@playwright/test'; + +export type Framework = 'react19' | 'vue3' | 'angular'; + +export interface TestOptions { + framework: Framework; +} + +export const test = base.extend({ + framework: ['react19', { option: true }], +}); + +export { expect } from '@playwright/test'; diff --git a/e2e/wrappers/package.json b/e2e/wrappers/package.json index 7ba5c5c8644e..e8f3b42f52b3 100644 --- a/e2e/wrappers/package.json +++ b/e2e/wrappers/package.json @@ -13,9 +13,9 @@ "start:vue3": "pnpm run dev:vue3 -- --host", "start:angular": "cd builders/angular && pnpm run start", "build:all": "pnpm run build:react19 && pnpm run build:vue3 && pnpm run build:angular", - "test:react19": "node ./runner.js --framework=react", - "test:vue3": "node ./runner.js --framework=vue", - "test:angular": "node ./runner.js --framework=angular" + "test:react19": "cross-env FRAMEWORK=react19 playwright test", + "test:vue3": "cross-env FRAMEWORK=vue3 playwright test", + "test:angular": "cross-env FRAMEWORK=angular playwright test" }, "dependencies": { "@angular/common": "catalog:angular", @@ -59,11 +59,13 @@ "@angular/cli": "catalog:angular", "@angular/compiler-cli": "catalog:angular", "@eslint/js": "catalog:", + "@playwright/test": "catalog:", "@types/jasmine": "5.1.4", "@types/react": "19.1.2", "@types/react-dom": "19.1.3", "@vitejs/plugin-react": "4.7.0", "@vitejs/plugin-vue": "5.2.4", + "cross-env": "7.0.3", "eslint": "9.39.4", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-react-refresh": "0.5.2", @@ -74,7 +76,6 @@ "karma-coverage": "2.2.1", "karma-jasmine": "5.1.0", "karma-jasmine-html-reporter": "2.1.0", - "testcafe": "catalog:", "typescript": "5.8.3", "vite": "8.0.16" }, diff --git a/e2e/wrappers/playwright.config.ts b/e2e/wrappers/playwright.config.ts new file mode 100644 index 000000000000..dc115d9d7962 --- /dev/null +++ b/e2e/wrappers/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig } from '@playwright/test'; + +import type { Framework, TestOptions } from './fixtures'; + +const FRAMEWORK_PORTS: Record = { + react19: 3030, + angular: 3031, + vue3: 3032, +}; + +const framework = (process.env.FRAMEWORK ?? 'react19') as Framework; +const port = FRAMEWORK_PORTS[framework]; + +if(!port) { + throw new Error(`Unsupported framework: ${framework}. Expected one of: ${Object.keys(FRAMEWORK_PORTS).join(', ')}.`); +} + +const baseURL = `http://localhost:${port}`; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + workers: 1, + reporter: process.env.CI + ? [['list'], ['html', { open: 'never' }]] + : [['list']], + timeout: 30000, + expect: { timeout: 5000 }, + use: { + framework, + baseURL, + channel: 'chrome', + headless: true, + viewport: { width: 1200, height: 800 }, + launchOptions: { args: ['--no-sandbox', '--disable-gpu'] }, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: { + command: `node ./serve.js --framework=${framework} --port=${port}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + timeout: 30000, + }, +}); diff --git a/e2e/wrappers/runner.js b/e2e/wrappers/runner.js deleted file mode 100644 index 13e6d6cec50f..000000000000 --- a/e2e/wrappers/runner.js +++ /dev/null @@ -1,106 +0,0 @@ -const path = require('path'); -const process = require('process'); -const minimist = require('minimist'); -const express = require('express'); -const createTestCafe = require('testcafe'); - -const argv = minimist(process.argv.slice(2)); -const framework = argv.framework || 'react'; - -const http = require('http'); - -function waitForServerReady(port, timeout = 5000) { - const deadline = Date.now() + timeout; - - return new Promise((resolve, reject) => { - const check = () => { - http.get(`http://localhost:${port}`, res => { - res.destroy(); - resolve(); - }).on('error', () => { - if (Date.now() > deadline) { - reject(new Error(`Timeout waiting for http://localhost:${port}`)); - } else { - setTimeout(check, 100); - } - }); - }; - - check(); - }); -} - -const frameworkConfig = { - react: { - staticPath: path.resolve(__dirname, 'builders/react19/dist/index.html'), - port: 3030, - }, - angular: { - staticPath: path.resolve(__dirname, 'builders/angular/dist/angular/browser/index.html'), - port: 3031, - }, - vue: { - staticPath: path.resolve(__dirname, 'builders/vue3/dist/index.html'), - port: 3032, - }, -}; - -if (!frameworkConfig[framework]) { - console.error(`❌ Unsupported framework: ${framework}`); - process.exit(1); -} - -const { staticPath, port } = frameworkConfig[framework]; -process.env.FRAMEWORK = framework; -process.env.E2E_TEST_PORT = port; - -const startStaticServer = () => - new Promise((resolve, reject) => { - const app = express(); - app.use(express.static(path.dirname(staticPath))); - app.get('*', (_, res) => res.sendFile(staticPath)); - - const server = app.listen(port, () => { - console.log(`✅ Server for ${framework} running at http://localhost:${port}`); - resolve(server); - }); - - server.on('error', reject); - }); - -(async () => { - let testcafe; - let server; - - try { - server = await startStaticServer(); - - await waitForServerReady(port, 5000); - - testcafe = await createTestCafe(); - - const runner = testcafe.createRunner(); - - const failedCount = await runner - .src('./tests/**/*.js') - .browsers(process.env.BROWSER || 'chrome:headless --no-sandbox --disable-gpu --window-size=1200,800') - .concurrency(1) - .run({ - skipJsErrors: true, - selectorTimeout: 3000, - assertionTimeout: 1000, - pageLoadTimeout: 5000, - }); - - console.log(`✅ E2E complete. Failed tests: ${failedCount}`); - process.exit(failedCount); - - } catch (err) { - console.error('❌ E2E test run failed:', err); - process.exit(1); - - } finally { - if (testcafe) await testcafe.close(); - if (server && server.close) await new Promise((res) => server.close(res)); - } -})(); diff --git a/e2e/wrappers/serve.js b/e2e/wrappers/serve.js new file mode 100644 index 000000000000..37b8ca11cab6 --- /dev/null +++ b/e2e/wrappers/serve.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); +const process = require('process'); +const minimist = require('minimist'); +const express = require('express'); + +const DIST_DIRS = { + react19: 'builders/react19/dist', + angular: 'builders/angular/dist/angular/browser', + vue3: 'builders/vue3/dist', +}; + +const argv = minimist(process.argv.slice(2)); +const framework = argv.framework; +const port = Number(argv.port); +const distDir = DIST_DIRS[framework]; + +if(!distDir) { + console.error(`❌ Unsupported framework: ${framework}. Expected one of: ${Object.keys(DIST_DIRS).join(', ')}.`); + process.exit(1); +} + +const root = path.resolve(__dirname, distDir); +const indexPath = path.join(root, 'index.html'); + +if(!fs.existsSync(indexPath)) { + console.error(`❌ Build for ${framework} is not found at ${root}. Run "pnpm run build:${framework}" first.`); + process.exit(1); +} + +const app = express(); + +app.use(express.static(root)); +app.get('*', (_, res) => res.sendFile(indexPath)); + +app.listen(port, () => { + console.log(`✅ Server for ${framework} running at http://localhost:${port}`); +}); diff --git a/e2e/wrappers/test-helpers.js b/e2e/wrappers/test-helpers.js deleted file mode 100644 index be9fce8d132a..000000000000 --- a/e2e/wrappers/test-helpers.js +++ /dev/null @@ -1,22 +0,0 @@ -import { fixture, test } from 'testcafe'; -const process = require('process'); - -export function navigateToComponent(t, baseUrl, examplePath) { - return t.navigateTo(`${baseUrl}/examples/${examplePath}`); -} - -export function testInFramework(fixtureName, examplePath, ...testData) { - const frameworkName = process.env.FRAMEWORK || 'react'; - const port = process.env.E2E_TEST_PORT || 3030; - const baseUrl = `http://localhost:${port}`; - - fixture(fixtureName).page(baseUrl); - - for(let [testName, testFn] of testData) { - test(testName, async t => { - await navigateToComponent(t, baseUrl, examplePath); - - await testFn(t); - }); - } -} diff --git a/e2e/wrappers/tests/button.spec.ts b/e2e/wrappers/tests/button.spec.ts new file mode 100644 index 000000000000..a50dc9b01d58 --- /dev/null +++ b/e2e/wrappers/tests/button.spec.ts @@ -0,0 +1,11 @@ +import { expect, test } from '../fixtures'; + +test.describe('Button scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/examples/button'); + }); + + test('Button should exist', async ({ page }) => { + await expect(page.locator('.dx-button-text')).toBeAttached(); + }); +}); diff --git a/e2e/wrappers/tests/button.test.js b/e2e/wrappers/tests/button.test.js deleted file mode 100644 index 7966bf3799bc..000000000000 --- a/e2e/wrappers/tests/button.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Selector } from 'testcafe'; -import { testInFramework } from '../test-helpers'; - -testInFramework('Button scenarios', 'button', [ - 'Button should exist', - async (t) => { - const button = Selector('.dx-button-text'); - - await t.expect(button.exists).ok('Button should exist'); - } -]); \ No newline at end of file diff --git a/e2e/wrappers/tests/chat-template-rerender.spec.ts b/e2e/wrappers/tests/chat-template-rerender.spec.ts new file mode 100644 index 000000000000..025341454d66 --- /dev/null +++ b/e2e/wrappers/tests/chat-template-rerender.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '../fixtures'; + +test.describe('Chat template re-rendering', () => { + test.skip(({ framework }) => framework !== 'react19', 'The example is implemented for React only'); + + test.beforeEach(async ({ page }) => { + await page.goto('/examples/chat-template-rerender'); + }); + + test('Chat should be able to re-render its messages', async ({ page }) => { + const textarea = page.locator('.dx-chat-messagebox textarea'); + const sendButton = page.locator('.dx-chat-messagebox .dx-chat-textarea-toolbar .dx-button'); + const assistantBubble = page.locator('.chat-messagebubble-text').nth(1); + const regenerateButton = page.locator('.dx-icon-refresh').nth(1); + + await textarea.pressSequentially('Hi there!'); + await sendButton.click(); + + await expect(assistantBubble).toHaveText('How can I help you?'); + + await regenerateButton.click(); + + await expect(assistantBubble).toHaveText('In other words, what do you want?'); + }); +}); diff --git a/e2e/wrappers/tests/chat-template-rerender.test.js b/e2e/wrappers/tests/chat-template-rerender.test.js deleted file mode 100644 index aa327c8f73ce..000000000000 --- a/e2e/wrappers/tests/chat-template-rerender.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Selector } from 'testcafe'; -import { testInFramework } from '../test-helpers'; - -if(process.env.FRAMEWORK === 'react') { - testInFramework('Chat template re-rendering', 'chat-template-rerender', [ - 'Chat should be able to re-render its messages', - async (t) => { - const textarea = Selector('.dx-chat-messagebox textarea'); - const sendButton = Selector('.dx-chat-messagebox .dx-chat-textarea-toolbar .dx-button'); - const assistantBubble = Selector('.chat-messagebubble-text').nth(1); - const regenerateButton = Selector('.dx-icon-refresh').nth(1); - - await t - .typeText(textarea, 'Hi there!') - .click(sendButton) - .expect(assistantBubble.textContent).eql('How can I help you?') - .click(regenerateButton) - .expect(assistantBubble.textContent).eql('In other words, what do you want?'); - } - ]); -} diff --git a/e2e/wrappers/tests/gantt-template-state-update.spec.ts b/e2e/wrappers/tests/gantt-template-state-update.spec.ts new file mode 100644 index 000000000000..cddc05d042b1 --- /dev/null +++ b/e2e/wrappers/tests/gantt-template-state-update.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '../fixtures'; + +test.describe('Gantt template state update', () => { + test.skip(({ framework }) => framework !== 'react19', 'The example is implemented for React only'); + + test.beforeEach(async ({ page }) => { + await page.goto('/examples/gantt-template-state-update'); + }); + + test('Gantt should be able to unmount its template when a parent component\'s state update happens', async ({ page }) => { + const hideButton = page.locator('button', { hasText: 'Hide Data' }); + const noDataPlaceholder = page.locator('.dx-treelist-nodata'); + + await hideButton.click(); + + await expect(noDataPlaceholder).toBeVisible(); + await expect(noDataPlaceholder).toHaveText('No data'); + }); +}); diff --git a/e2e/wrappers/tests/gantt-template-state-update.test.js b/e2e/wrappers/tests/gantt-template-state-update.test.js deleted file mode 100644 index 7d402aaeab4b..000000000000 --- a/e2e/wrappers/tests/gantt-template-state-update.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Selector } from 'testcafe'; -import { testInFramework } from '../test-helpers'; - -if(process.env.FRAMEWORK === 'react') { - testInFramework('Gantt template state update', 'gantt-template-state-update', [ - 'Gantt should be able to ummount its template when a parent component\'s state update happens', - async (t) => { - const hideButton = Selector('button').withText('Hide Data'); - const noDataPlaceholder = Selector('.dx-treelist-nodata'); - - await t - .click(hideButton) - .expect(noDataPlaceholder.visible).ok() - .expect(noDataPlaceholder.textContent).eql('No data'); - } - ]); -} diff --git a/e2e/wrappers/tests/inputs-list-in-form.spec.ts b/e2e/wrappers/tests/inputs-list-in-form.spec.ts new file mode 100644 index 000000000000..430b228820e7 --- /dev/null +++ b/e2e/wrappers/tests/inputs-list-in-form.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from '../fixtures'; + +test.describe('inputs-list-in-form scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/examples/inputs-list-in-form'); + }); + + test('Phone inputs should adding and deleting correctly', async ({ page }) => { + const addButton = page.locator('.dx-button-text', { hasText: 'Add phone' }); + const deleteButton = page.locator('.dx-button', { has: page.locator('.dx-icon-trash') }); + const phoneGroup = page.locator('[aria-labelledby$="_phones-container"]'); + const phoneInputs = phoneGroup.locator('.dx-texteditor-container'); + + await expect(phoneInputs).toHaveCount(0); + + await addButton.click(); + await expect(phoneInputs).toHaveCount(1); + + await deleteButton.click(); + await expect(phoneInputs).toHaveCount(0); + }); +}); diff --git a/e2e/wrappers/tests/inputs-list-in-form.test.js b/e2e/wrappers/tests/inputs-list-in-form.test.js deleted file mode 100644 index ae61e713564a..000000000000 --- a/e2e/wrappers/tests/inputs-list-in-form.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Selector } from 'testcafe'; -import { testInFramework } from '../test-helpers'; - -testInFramework('inputs-list-in-form scenarios', 'inputs-list-in-form', [ - 'Phone inputs should adding and deleting correctly', - async (t) => { - const addButton = Selector('.dx-button-text').withText('Add phone'); - const deleteButton = Selector('.dx-icon-trash').parent('.dx-button'); - - const phoneGroup = Selector('[aria-labelledby$="_phones-container"]'); - - const phoneInputs = phoneGroup.find('.dx-texteditor-container'); - - await t.expect(phoneInputs.count).eql(0, 'Phone inputs should not exist initially'); - - await t.click(addButton); - await t.expect(phoneInputs.count).eql(1, 'There should be exactly one phone input after adding'); - - await t.click(deleteButton); - await t.expect(phoneInputs.count).eql(0, 'Phone inputs should be removed after deletion'); - } -]); diff --git a/e2e/wrappers/tests/select-box-nested-validator.spec.ts b/e2e/wrappers/tests/select-box-nested-validator.spec.ts new file mode 100644 index 000000000000..df26be374309 --- /dev/null +++ b/e2e/wrappers/tests/select-box-nested-validator.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '../fixtures'; + +test.describe('SelectBox nested validator scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/examples/select-box-nested-validator'); + }); + + test('SelectBox with nested Validator component should not render double errors', async ({ page }) => { + const validateButton = page.locator('.dx-button-text', { hasText: 'Validate' }); + const validationSummary = page.locator('.dx-validationsummary'); + + await expect(validateButton).toBeAttached(); + await expect(validationSummary).toBeAttached(); + + await validateButton.click(); + + const validationSummaryItems = validationSummary.locator('.dx-validationsummary-item'); + + await expect(validationSummaryItems).toHaveCount(1); + await expect(validationSummaryItems.first()).toHaveText('Type is required'); + }); + + test('SelectBox validation should pass when value is selected', async ({ page }) => { + const validateButton = page.locator('.dx-button-text', { hasText: 'Validate' }); + const validationSummary = page.locator('.dx-validationsummary'); + const selectBoxArrow = page.locator('.dx-selectbox .dx-dropdowneditor-button'); + + await selectBoxArrow.click(); + + const firstItem = page.locator('.dx-item', { hasText: 'One' }); + + await expect(firstItem).toBeVisible(); + await firstItem.click(); + + await validateButton.click(); + + await expect(validationSummary.locator('.dx-validationsummary-item')).toHaveCount(0); + }); +}); diff --git a/e2e/wrappers/tests/select-box-nested-validator.test.js b/e2e/wrappers/tests/select-box-nested-validator.test.js deleted file mode 100644 index 3a7f7f83f328..000000000000 --- a/e2e/wrappers/tests/select-box-nested-validator.test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Selector } from 'testcafe'; -import { testInFramework } from '../test-helpers'; - -testInFramework('SelectBox nested validator scenarios', 'select-box-nested-validator', [ - 'SelectBox with nested Validator component should not render double errors', - async (t) => { - const validateButton = Selector('.dx-button-text').withText('Validate'); - const validationSummary = Selector('.dx-validationsummary'); - - await t - .expect(validateButton.exists).ok('Validate button should exist') - .expect(validationSummary.exists).ok('Validation summary should exist'); - - await t.click(validateButton); - - const updatedValidationSummaryItems = validationSummary.find('.dx-validationsummary-item'); - await t.expect(updatedValidationSummaryItems.count).eql(1, 'Should have exactly one validation error in summary'); - const errorMessage = await updatedValidationSummaryItems.nth(0).innerText; - await t.expect(errorMessage).eql('Type is required', 'Error message should be "Type is required"'); - }, - - 'SelectBox validation should pass when value is selected', - async (t) => { - const validateButton = Selector('.dx-button-text').withText('Validate'); - const validationSummary = Selector('.dx-validationsummary'); - const selectBox = Selector('.dx-selectbox'); - const selectBoxArrow = selectBox.find('.dx-dropdowneditor-button'); - - await t.click(selectBoxArrow); - const firstItem = Selector('.dx-item').withText('One'); - - await t - .expect(firstItem.exists).ok('First item should exist in dropdown') - .click(firstItem); - - await t.click(validateButton); - - const updatedValidationSummaryItems = validationSummary.find('.dx-validationsummary-item'); - await t.expect(updatedValidationSummaryItems.count).eql(0, 'Should have no validation errors when value is selected'); - } -]); diff --git a/e2e/wrappers/tests/textbox.spec.ts b/e2e/wrappers/tests/textbox.spec.ts new file mode 100644 index 000000000000..26c84d61c172 --- /dev/null +++ b/e2e/wrappers/tests/textbox.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from '../fixtures'; + +test.describe('TextBox Dynamic Styles scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/examples/text-box-dynamic-styles'); + }); + + test('TextBox should update inline styles', async ({ page }) => { + const textboxWrapper = page.locator('.dx-textbox'); + const textboxInput = page.locator('.dx-texteditor-input'); + + await expect(textboxWrapper).toBeAttached(); + await expect(textboxInput).toBeAttached(); + + await textboxInput.pressSequentially('trigger'); + await textboxInput.press('Enter'); + + await expect(textboxInput).toHaveValue('trigger'); + await expect(textboxWrapper).toHaveCSS('background-color', 'rgb(255, 99, 132)'); + + await textboxInput.pressSequentially(' again'); + await textboxInput.press('Enter'); + + await expect(textboxInput).toHaveValue('trigger again'); + await expect(textboxWrapper).toHaveCSS('background-color', 'rgb(54, 162, 235)'); + }); +}); diff --git a/e2e/wrappers/tests/textbox.test.js b/e2e/wrappers/tests/textbox.test.js deleted file mode 100644 index 5dc2fcbdedfe..000000000000 --- a/e2e/wrappers/tests/textbox.test.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Selector } from 'testcafe'; -import { testInFramework } from '../test-helpers'; - -testInFramework('TextBox Dynamic Styles scenarios', 'text-box-dynamic-styles', [ - 'TextBox should update inline styles', - async (t) => { - const textboxWrapper = Selector('.dx-textbox'); - const textboxInput = Selector('.dx-texteditor-input'); - await t.expect(textboxWrapper.exists).ok('TextBox Wrapper should exist'); - await t.expect(textboxInput.exists).ok('TextBox Input should exist'); - await t - .typeText(textboxInput, 'trigger') - .pressKey('enter') - .expect(textboxInput.value) - .eql('trigger') - .expect(textboxWrapper.getStyleProperty('background-color')) - .eql('rgb(255, 99, 132)') - .typeText(textboxInput, ' again') - .pressKey('enter') - .expect(textboxInput.value) - .eql('trigger again') - .expect(textboxWrapper.getStyleProperty('background-color')) - .eql('rgb(54, 162, 235)'); - }, -]); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cab23141008..66e94d5547df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ catalogs: '@eslint/js': specifier: 9.39.4 version: 9.39.4 + '@playwright/test': + specifier: 1.62.1 + version: 1.62.1 '@stylistic/eslint-plugin': specifier: 5.10.0 version: 5.10.0 @@ -1306,6 +1309,9 @@ importers: '@eslint/js': specifier: 'catalog:' version: 9.39.4 + '@playwright/test': + specifier: 'catalog:' + version: 1.62.1 '@types/jasmine': specifier: 5.1.4 version: 5.1.4 @@ -1321,6 +1327,9 @@ importers: '@vitejs/plugin-vue': specifier: 5.2.4 version: 5.2.4(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.8.0)(sass-embedded@1.93.3)(sass@1.101.0)(terser@5.49.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.8.3)) + cross-env: + specifier: 7.0.3 + version: 7.0.3 eslint: specifier: 9.39.4 version: 9.39.4(jiti@2.6.1) @@ -1351,9 +1360,6 @@ importers: karma-jasmine-html-reporter: specifier: 2.1.0 version: 2.1.0(jasmine-core@5.6.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4) - testcafe: - specifier: 'catalog:' - version: 3.7.5(supports-color@7.2.0) typescript: specifier: 5.8.3 version: 5.8.3 @@ -6583,6 +6589,11 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -11299,6 +11310,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -14377,6 +14393,16 @@ packages: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + plimit-lit@1.6.1: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} @@ -23613,6 +23639,10 @@ snapshots: '@pkgr/core@0.3.6': {} + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + '@popperjs/core@2.11.8': {} '@preact/signals-core@1.14.1': {} @@ -30036,6 +30066,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -34489,6 +34522,14 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + plimit-lit@1.6.1: dependencies: queue-lit: 1.5.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0494ceb08b2d..7cc132bd4434 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -91,6 +91,7 @@ catalog: zod: 3.24.4 zod-to-json-schema: 3.24.6 "@babel/eslint-parser": 7.29.7 + "@playwright/test": 1.62.1 "@eslint-stylistic/metadata": ^2.13.0 "@eslint/eslintrc": 3.3.5 "@stylistic/eslint-plugin": 5.10.0 From 6213e22b286b819734c355f264ef90b284948108 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Wed, 12 Aug 2026 14:15:43 -0300 Subject: [PATCH 02/18] Address review: validate port, read CI versions in docker script, docs and test title fixes --- e2e/wrappers/README.md | 2 +- e2e/wrappers/docker/Dockerfile | 7 +++---- e2e/wrappers/docker/run.sh | 21 ++++++++++++++++--- e2e/wrappers/serve.js | 5 +++++ .../tests/inputs-list-in-form.spec.ts | 2 +- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/e2e/wrappers/README.md b/e2e/wrappers/README.md index 3e2de13330db..6f819e956485 100644 --- a/e2e/wrappers/README.md +++ b/e2e/wrappers/README.md @@ -19,7 +19,7 @@ The tests are written with [Playwright](https://playwright.dev/) and use the sys ## Run locally ```bash -pnpm install +pnpm install --frozen-lockfile pnpm nx all:build-testing workflows cd e2e/wrappers diff --git a/e2e/wrappers/docker/Dockerfile b/e2e/wrappers/docker/Dockerfile index bcde5274689c..7ca3317cfc3a 100644 --- a/e2e/wrappers/docker/Dockerfile +++ b/e2e/wrappers/docker/Dockerfile @@ -1,8 +1,7 @@ -# Reproduces the "Wrappers E2E Tests" CI environment: Node from .node-version -# and Google Chrome pinned to the version used by .github/actions/setup-chrome. -FROM node:24.16.0-bookworm-slim +ARG NODE_IMAGE=node:lts-bookworm-slim +FROM ${NODE_IMAGE} -ARG CHROME_VERSION=150.0.7871.181 +ARG CHROME_VERSION RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl \ diff --git a/e2e/wrappers/docker/run.sh b/e2e/wrappers/docker/run.sh index 8e080ac76a51..f74e834b879e 100755 --- a/e2e/wrappers/docker/run.sh +++ b/e2e/wrappers/docker/run.sh @@ -1,9 +1,10 @@ #!/bin/bash -# Runs the wrappers e2e tests in a container that matches the CI environment. +# Runs the wrappers e2e tests in a container that matches the CI environment: Node and Google +# Chrome are taken from .node-version and from the workflow that runs these tests on CI. # The repository is mounted as is, so dependencies must be installed and # the app under test built on the host first: # -# pnpm install +# pnpm install --frozen-lockfile # pnpm nx all:build-testing workflows # cd e2e/wrappers && pnpm run build:react19 # @@ -16,11 +17,25 @@ shift || true DOCKER_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$DOCKER_DIR/../../.." && pwd)" +WORKFLOW="$REPO_ROOT/.github/workflows/wrapper_tests_e2e.yml" + +NODE_VERSION="$(cat "$REPO_ROOT/.node-version")" +CHROME_VERSION="$(grep -m1 'chrome-version:' "$WORKFLOW" | cut -d: -f2 | tr -d " '\"")" + +if [ -z "$NODE_VERSION" ] || [ -z "$CHROME_VERSION" ]; then + echo "❌ Cannot read the Node version from .node-version or the Chrome version from $WORKFLOW." >&2 + exit 1 +fi + +echo "Node $NODE_VERSION, Google Chrome $CHROME_VERSION" # Google Chrome for Linux ships for amd64 only, so the platform is pinned as on CI. PLATFORM=linux/amd64 -docker build --platform "$PLATFORM" -t devextreme-wrappers-e2e "$DOCKER_DIR" +docker build --platform "$PLATFORM" \ + --build-arg "NODE_IMAGE=node:$NODE_VERSION-bookworm-slim" \ + --build-arg "CHROME_VERSION=$CHROME_VERSION" \ + -t devextreme-wrappers-e2e "$DOCKER_DIR" docker run --rm --ipc=host --platform "$PLATFORM" \ -v "$REPO_ROOT:/repo" \ diff --git a/e2e/wrappers/serve.js b/e2e/wrappers/serve.js index 37b8ca11cab6..43caca16ee89 100644 --- a/e2e/wrappers/serve.js +++ b/e2e/wrappers/serve.js @@ -20,6 +20,11 @@ if(!distDir) { process.exit(1); } +if(!Number.isInteger(port) || port <= 0) { + console.error(`❌ Invalid port: ${argv.port}. Pass a positive integer, for example --port=3030.`); + process.exit(1); +} + const root = path.resolve(__dirname, distDir); const indexPath = path.join(root, 'index.html'); diff --git a/e2e/wrappers/tests/inputs-list-in-form.spec.ts b/e2e/wrappers/tests/inputs-list-in-form.spec.ts index 430b228820e7..71f6a1e6cf7e 100644 --- a/e2e/wrappers/tests/inputs-list-in-form.spec.ts +++ b/e2e/wrappers/tests/inputs-list-in-form.spec.ts @@ -5,7 +5,7 @@ test.describe('inputs-list-in-form scenarios', () => { await page.goto('/examples/inputs-list-in-form'); }); - test('Phone inputs should adding and deleting correctly', async ({ page }) => { + test('Phone inputs should be added and deleted correctly', async ({ page }) => { const addButton = page.locator('.dx-button-text', { hasText: 'Add phone' }); const deleteButton = page.locator('.dx-button', { has: page.locator('.dx-icon-trash') }); const phoneGroup = page.locator('[aria-labelledby$="_phones-container"]'); From 8c224ababb6b727801cc7243679895ce672757cd Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 13 Aug 2026 10:58:36 -0300 Subject: [PATCH 03/18] Keep Chrome scrollbars visible and align the local container with the CI environment --- e2e/wrappers/README.md | 33 +++++++++++++++++++++++++++++-- e2e/wrappers/docker/Dockerfile | 18 +++++++++++------ e2e/wrappers/docker/run.sh | 5 +++-- e2e/wrappers/playwright.config.ts | 7 ++++++- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/e2e/wrappers/README.md b/e2e/wrappers/README.md index 6f819e956485..e62fb0ef1a70 100644 --- a/e2e/wrappers/README.md +++ b/e2e/wrappers/README.md @@ -35,10 +35,39 @@ Examples that exist for a single framework only (Chat, Gantt) are skipped in the ## Run in the CI environment -The container pins Node and Google Chrome to the versions used by the `Wrappers E2E Tests` -workflow. Dependencies and application builds are taken from the host: +The container mirrors the OS, Node and Google Chrome the tests get on CI. Dependencies and +application builds are taken from the host: ```bash cd e2e/wrappers docker/run.sh react19 ``` + +To drive the run from the browser on your machine, start the UI mode inside the container and +open `http://localhost:9323`: + +```bash +docker run --rm --platform linux/amd64 --shm-size=2gb -p 9323:9323 \ + -v "$(git rev-parse --show-toplevel):/repo" -w /repo/e2e/wrappers -e FRAMEWORK=react19 \ + devextreme-wrappers-e2e \ + node_modules/.bin/playwright test --ui-host=0.0.0.0 --ui-port=9323 +``` + +The repository is mounted, so edits made on the host are picked up and the watch mode re-runs +the affected tests. Plain `--ui` is not used on purpose: it opens the interface in the bundled +Chromium, which is deliberately not installed. + +Artifacts from a failed CI run are read with the same tooling: + +```bash +pnpm exec playwright show-report /playwright-report +pnpm exec playwright show-trace /test-results//trace.zip --port 0 +``` + +## Rendering + +The default Chrome scrollbars must stay visible: they take layout space, so a page rendered +without them differs from what a user sees and from the etalons the TestCafe tests produce. +Playwright hides scrollbars in headless mode by default, so `--hide-scrollbars` is removed in +`playwright.config.ts`. Measured in the CI-like container, the scrollbar is 15px wide both here +and in TestCafe. diff --git a/e2e/wrappers/docker/Dockerfile b/e2e/wrappers/docker/Dockerfile index 7ca3317cfc3a..5b8a844edcd2 100644 --- a/e2e/wrappers/docker/Dockerfile +++ b/e2e/wrappers/docker/Dockerfile @@ -1,13 +1,19 @@ -ARG NODE_IMAGE=node:lts-bookworm-slim -FROM ${NODE_IMAGE} +# Mirrors the environment the tests get on CI: Ubuntu 24.04, Node from .node-version and +# Google Chrome pinned to the version the workflow installs. Keep the font packages in sync +# with the CI runner image — they affect text rendering and, later, screenshot comparison. +FROM ubuntu:24.04 +ARG NODE_VERSION ARG CHROME_VERSION -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl \ +RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/dx-no-recommends \ + && apt-get update \ + && apt-get install -y ca-certificates curl fonts-noto-color-emoji \ && curl -fsSL "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}-1_amd64.deb" -o /tmp/chrome.deb \ - && apt-get install -y --no-install-recommends /tmp/chrome.deb \ - && rm /tmp/chrome.deb \ + && apt-get install -y /tmp/chrome.deb \ + && curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" -o /tmp/node.tar.gz \ + && tar -xzf /tmp/node.tar.gz -C /usr/local --strip-components=1 \ + && rm /tmp/chrome.deb /tmp/node.tar.gz \ && rm -rf /var/lib/apt/lists/* WORKDIR /repo/e2e/wrappers diff --git a/e2e/wrappers/docker/run.sh b/e2e/wrappers/docker/run.sh index f74e834b879e..426fbcb76cc3 100755 --- a/e2e/wrappers/docker/run.sh +++ b/e2e/wrappers/docker/run.sh @@ -33,11 +33,12 @@ echo "Node $NODE_VERSION, Google Chrome $CHROME_VERSION" PLATFORM=linux/amd64 docker build --platform "$PLATFORM" \ - --build-arg "NODE_IMAGE=node:$NODE_VERSION-bookworm-slim" \ + --build-arg "NODE_VERSION=$NODE_VERSION" \ --build-arg "CHROME_VERSION=$CHROME_VERSION" \ -t devextreme-wrappers-e2e "$DOCKER_DIR" -docker run --rm --ipc=host --platform "$PLATFORM" \ +# Shared memory and seccomp are set as the CI runner container gets them. +docker run --rm --platform "$PLATFORM" --shm-size=2gb --security-opt seccomp=unconfined \ -v "$REPO_ROOT:/repo" \ -w /repo/e2e/wrappers \ -e CI=true \ diff --git a/e2e/wrappers/playwright.config.ts b/e2e/wrappers/playwright.config.ts index dc115d9d7962..4acd82a80950 100644 --- a/e2e/wrappers/playwright.config.ts +++ b/e2e/wrappers/playwright.config.ts @@ -33,7 +33,12 @@ export default defineConfig({ channel: 'chrome', headless: true, viewport: { width: 1200, height: 800 }, - launchOptions: { args: ['--no-sandbox', '--disable-gpu'] }, + launchOptions: { + args: ['--no-sandbox', '--disable-gpu'], + // Playwright hides scrollbars in headless by default, so the page gets no scrollbar + // gutter and its layout differs from a real browser and from the TestCafe etalons. + ignoreDefaultArgs: ['--hide-scrollbars'], + }, trace: 'retain-on-failure', screenshot: 'only-on-failure', }, From 06324205f2f204991417335cc7616605928f88ea Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 16 Aug 2026 13:51:35 -0300 Subject: [PATCH 04/18] Upload the Playwright report and traces as separate CI artifacts, add the UI mode to the docker script --- .github/workflows/wrapper_tests_e2e.yml | 18 +++++++--- e2e/wrappers/README.md | 29 +++++++++------ e2e/wrappers/docker/run.sh | 47 ++++++++++++++++++------- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/.github/workflows/wrapper_tests_e2e.yml b/.github/workflows/wrapper_tests_e2e.yml index 336fedc81790..c39e9440a83a 100644 --- a/.github/workflows/wrapper_tests_e2e.yml +++ b/.github/workflows/wrapper_tests_e2e.yml @@ -174,12 +174,20 @@ jobs: working-directory: e2e/wrappers run: pnpm run test:${{ matrix.framework }} - - name: Upload test artifacts on failure + # The report and the traces are uploaded separately: each artifact then unpacks into the + # layout "playwright show-report" and "playwright show-trace" expect. + - name: Upload test report on failure if: ${{ failure() }} uses: actions/upload-artifact@v7 with: - name: test-fails-${{matrix.framework}} - path: | - e2e/wrappers/playwright-report - e2e/wrappers/test-results + name: playwright-report-${{matrix.framework}} + path: e2e/wrappers/playwright-report + if-no-files-found: ignore + + - name: Upload test traces on failure + if: ${{ failure() }} + uses: actions/upload-artifact@v7 + with: + name: playwright-traces-${{matrix.framework}} + path: e2e/wrappers/test-results if-no-files-found: ignore diff --git a/e2e/wrappers/README.md b/e2e/wrappers/README.md index e62fb0ef1a70..734d4a0e75d8 100644 --- a/e2e/wrappers/README.md +++ b/e2e/wrappers/README.md @@ -43,27 +43,34 @@ cd e2e/wrappers docker/run.sh react19 ``` -To drive the run from the browser on your machine, start the UI mode inside the container and -open `http://localhost:9323`: +The browser runs in the container while you watch and re-run the tests from the browser on your +machine — start the UI mode and open `http://localhost:9323`: ```bash -docker run --rm --platform linux/amd64 --shm-size=2gb -p 9323:9323 \ - -v "$(git rev-parse --show-toplevel):/repo" -w /repo/e2e/wrappers -e FRAMEWORK=react19 \ - devextreme-wrappers-e2e \ - node_modules/.bin/playwright test --ui-host=0.0.0.0 --ui-port=9323 +docker/run.sh --ui react19 ``` The repository is mounted, so edits made on the host are picked up and the watch mode re-runs -the affected tests. Plain `--ui` is not used on purpose: it opens the interface in the bundled -Chromium, which is deliberately not installed. +the affected tests. Plain `playwright test --ui` is not used on purpose: it opens the interface +in the bundled Chromium, which is deliberately not installed. -Artifacts from a failed CI run are read with the same tooling: +## Investigate a failed CI run + +The `Wrappers E2E Tests` workflow uploads two artifacts per framework when tests fail: the HTML +report and the traces. Download them and open with the tools Playwright ships: ```bash -pnpm exec playwright show-report /playwright-report -pnpm exec playwright show-trace /test-results//trace.zip --port 0 +gh run download -n playwright-report-react19 -D ci-report +gh run download -n playwright-traces-react19 -D ci-traces + +pnpm exec playwright show-report ci-report +pnpm exec playwright show-trace ci-traces//trace.zip --port 0 ``` +The trace is a step-by-step timeline: the DOM before and after every action, network, console +output and the error, so a failure that only happens on CI can be inspected without reproducing +it. To reproduce it anyway, run the same test in the container — the environment matches. + ## Rendering The default Chrome scrollbars must stay visible: they take layout space, so a page rendered diff --git a/e2e/wrappers/docker/run.sh b/e2e/wrappers/docker/run.sh index 426fbcb76cc3..f4befae6f160 100755 --- a/e2e/wrappers/docker/run.sh +++ b/e2e/wrappers/docker/run.sh @@ -1,17 +1,26 @@ #!/bin/bash -# Runs the wrappers e2e tests in a container that matches the CI environment: Node and Google -# Chrome are taken from .node-version and from the workflow that runs these tests on CI. -# The repository is mounted as is, so dependencies must be installed and -# the app under test built on the host first: +# Runs the wrappers e2e tests in a container that matches the CI environment: the OS, Node and +# Google Chrome are the ones the tests get on CI. The repository is mounted as is, so +# dependencies must be installed and the app under test built on the host first: # # pnpm install --frozen-lockfile # pnpm nx all:build-testing workflows # cd e2e/wrappers && pnpm run build:react19 # -# Usage: docker/run.sh [react19|vue3|angular] [extra playwright args] +# Usage: +# docker/run.sh [react19|vue3|angular] [extra playwright args] +# docker/run.sh --ui [react19|vue3|angular] open the UI mode at http://localhost:9323 set -euo pipefail +UI_PORT=9323 +UI=false + +if [ "${1:-}" = "--ui" ]; then + UI=true + shift +fi + FRAMEWORK="${1:-react19}" shift || true @@ -38,10 +47,24 @@ docker build --platform "$PLATFORM" \ -t devextreme-wrappers-e2e "$DOCKER_DIR" # Shared memory and seccomp are set as the CI runner container gets them. -docker run --rm --platform "$PLATFORM" --shm-size=2gb --security-opt seccomp=unconfined \ - -v "$REPO_ROOT:/repo" \ - -w /repo/e2e/wrappers \ - -e CI=true \ - -e "FRAMEWORK=$FRAMEWORK" \ - devextreme-wrappers-e2e \ - node_modules/.bin/playwright test "$@" +DOCKER_ARGS=( + --rm + --platform "$PLATFORM" + --shm-size=2gb + --security-opt seccomp=unconfined + -v "$REPO_ROOT:/repo" + -w /repo/e2e/wrappers + -e "FRAMEWORK=$FRAMEWORK" +) +PLAYWRIGHT_ARGS=(test) + +if [ "$UI" = true ]; then + DOCKER_ARGS+=(-p "$UI_PORT:$UI_PORT") + PLAYWRIGHT_ARGS+=(--ui-host=0.0.0.0 "--ui-port=$UI_PORT") + echo "UI mode for $FRAMEWORK: open http://localhost:$UI_PORT" +else + DOCKER_ARGS+=(-e CI=true) + PLAYWRIGHT_ARGS+=("$@") +fi + +docker run "${DOCKER_ARGS[@]}" devextreme-wrappers-e2e node_modules/.bin/playwright "${PLAYWRIGHT_ARGS[@]}" From 8734d8821859e0f8e25e4c05125655a31631e95f Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Tue, 25 Aug 2026 07:28:12 -0300 Subject: [PATCH 05/18] Shorten the artifact upload comment to the reason only --- .github/workflows/wrapper_tests_e2e.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wrapper_tests_e2e.yml b/.github/workflows/wrapper_tests_e2e.yml index c39e9440a83a..acce47596123 100644 --- a/.github/workflows/wrapper_tests_e2e.yml +++ b/.github/workflows/wrapper_tests_e2e.yml @@ -174,8 +174,7 @@ jobs: working-directory: e2e/wrappers run: pnpm run test:${{ matrix.framework }} - # The report and the traces are uploaded separately: each artifact then unpacks into the - # layout "playwright show-report" and "playwright show-trace" expect. + # Kept as two artifacts: merged into one, the report no longer opens from the download. - name: Upload test report on failure if: ${{ failure() }} uses: actions/upload-artifact@v7 From ca516221c5a1e8d775cc8b41269f8010f094f650 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Tue, 25 Aug 2026 07:33:06 -0300 Subject: [PATCH 06/18] Drop the artifact upload comment --- .github/workflows/wrapper_tests_e2e.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/wrapper_tests_e2e.yml b/.github/workflows/wrapper_tests_e2e.yml index acce47596123..d647c75b75f0 100644 --- a/.github/workflows/wrapper_tests_e2e.yml +++ b/.github/workflows/wrapper_tests_e2e.yml @@ -174,7 +174,6 @@ jobs: working-directory: e2e/wrappers run: pnpm run test:${{ matrix.framework }} - # Kept as two artifacts: merged into one, the report no longer opens from the download. - name: Upload test report on failure if: ${{ failure() }} uses: actions/upload-artifact@v7 From 15af875ee848fea31f7947a592f2a43cbc1c3016 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 09:54:55 -0300 Subject: [PATCH 07/18] Rewrite the wrappers static server as an ES module and drop minimist --- e2e/wrappers/.gitignore | 2 ++ e2e/wrappers/docker/Dockerfile | 4 +--- e2e/wrappers/package.json | 1 - e2e/wrappers/playwright.config.ts | 2 +- e2e/wrappers/{serve.js => serve.mjs} | 26 ++++++++++++++++---------- pnpm-lock.yaml | 3 --- 6 files changed, 20 insertions(+), 18 deletions(-) rename e2e/wrappers/{serve.js => serve.mjs} (61%) diff --git a/e2e/wrappers/.gitignore b/e2e/wrappers/.gitignore index 5c4ffa21fa5e..8d5a8b9a498e 100644 --- a/e2e/wrappers/.gitignore +++ b/e2e/wrappers/.gitignore @@ -1,2 +1,4 @@ playwright-report/ test-results/ +ci-report/ +ci-traces/ diff --git a/e2e/wrappers/docker/Dockerfile b/e2e/wrappers/docker/Dockerfile index 5b8a844edcd2..c2b440c8f26b 100644 --- a/e2e/wrappers/docker/Dockerfile +++ b/e2e/wrappers/docker/Dockerfile @@ -1,6 +1,4 @@ -# Mirrors the environment the tests get on CI: Ubuntu 24.04, Node from .node-version and -# Google Chrome pinned to the version the workflow installs. Keep the font packages in sync -# with the CI runner image — they affect text rendering and, later, screenshot comparison. +# Mirrors the environment the tests get on CI. FROM ubuntu:24.04 ARG NODE_VERSION diff --git a/e2e/wrappers/package.json b/e2e/wrappers/package.json index e8f3b42f52b3..81e9a5b89486 100644 --- a/e2e/wrappers/package.json +++ b/e2e/wrappers/package.json @@ -40,7 +40,6 @@ "inferno-create-element": "^8.2.0", "jszip": "^3.10.1", "luxon": "3.4.4", - "minimist": "1.2.8", "react": "^19.2.8", "react-dom": "^19.2.8", "react-router": "^8.3.0", diff --git a/e2e/wrappers/playwright.config.ts b/e2e/wrappers/playwright.config.ts index 4acd82a80950..d1a19573546b 100644 --- a/e2e/wrappers/playwright.config.ts +++ b/e2e/wrappers/playwright.config.ts @@ -43,7 +43,7 @@ export default defineConfig({ screenshot: 'only-on-failure', }, webServer: { - command: `node ./serve.js --framework=${framework} --port=${port}`, + command: `node ./serve.mjs --framework=${framework} --port=${port}`, url: baseURL, reuseExistingServer: !process.env.CI, timeout: 30000, diff --git a/e2e/wrappers/serve.js b/e2e/wrappers/serve.mjs similarity index 61% rename from e2e/wrappers/serve.js rename to e2e/wrappers/serve.mjs index 43caca16ee89..7949d8078f9d 100644 --- a/e2e/wrappers/serve.js +++ b/e2e/wrappers/serve.mjs @@ -1,8 +1,8 @@ -const fs = require('fs'); -const path = require('path'); -const process = require('process'); -const minimist = require('minimist'); -const express = require('express'); +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { parseArgs } from 'node:util'; +import express from 'express'; const DIST_DIRS = { react19: 'builders/react19/dist', @@ -10,9 +10,15 @@ const DIST_DIRS = { vue3: 'builders/vue3/dist', }; -const argv = minimist(process.argv.slice(2)); -const framework = argv.framework; -const port = Number(argv.port); +const { values } = parseArgs({ + options: { + framework: { type: 'string' }, + port: { type: 'string' }, + }, +}); + +const framework = values.framework; +const port = Number(values.port); const distDir = DIST_DIRS[framework]; if(!distDir) { @@ -21,11 +27,11 @@ if(!distDir) { } if(!Number.isInteger(port) || port <= 0) { - console.error(`❌ Invalid port: ${argv.port}. Pass a positive integer, for example --port=3030.`); + console.error(`❌ Invalid port: ${values.port}. Pass a positive integer, for example --port=3030.`); process.exit(1); } -const root = path.resolve(__dirname, distDir); +const root = path.resolve(import.meta.dirname, distDir); const indexPath = path.join(root, 'index.html'); if(!fs.existsSync(indexPath)) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66e94d5547df..57cb806b54ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1257,9 +1257,6 @@ importers: luxon: specifier: 3.4.4 version: 3.4.4 - minimist: - specifier: 1.2.8 - version: 1.2.8 react: specifier: ^19.2.8 version: 19.2.8 From e62c4075ddefcd367939efbf6ce9e19f5d1c40ad Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 10:02:47 -0300 Subject: [PATCH 08/18] Trim the wrappers README to the three things it needs to answer --- e2e/wrappers/README.md | 57 ++++++++---------------------------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/e2e/wrappers/README.md b/e2e/wrappers/README.md index 734d4a0e75d8..6e92bc6525b3 100644 --- a/e2e/wrappers/README.md +++ b/e2e/wrappers/README.md @@ -1,21 +1,9 @@ # Wrappers E2E tests End-to-end tests that check DevExtreme components inside the React, Vue and Angular wrappers. -The tests are written with [Playwright](https://playwright.dev/) and use the system Google Chrome +They run on [Playwright](https://playwright.dev/) and use the system Google Chrome (`channel: 'chrome'`), so no browser download is needed. -## Layout - -| Path | Purpose | -|------------------------|----------------------------------------------------------------------| -| `builders/*` | Host applications that render the examples for each framework | -| `examples/*` | Examples under test, one folder per scenario and framework | -| `tests/*.spec.ts` | Playwright specs | -| `fixtures.ts` | `test`/`expect` with the `framework` option added | -| `playwright.config.ts` | Per-framework port, base URL and dev server | -| `serve.js` | Static server for a built application | -| `docker/` | Container that matches the CI environment | - ## Run locally ```bash @@ -24,40 +12,26 @@ pnpm nx all:build-testing workflows cd e2e/wrappers pnpm run build:react19 # or build:vue3 / build:angular / build:all - pnpm run test:react19 # or test:vue3 / test:angular ``` -`playwright test` starts `serve.js` on its own, so no separate server is needed. -Useful flags: `--headed`, `--debug`, `--ui`, `--reporter=html`. - -Examples that exist for a single framework only (Chat, Gantt) are skipped in the other frameworks. +`playwright test` starts the server for the built application itself. The Chat and Gantt examples +exist for React only, so they are reported as skipped for the other frameworks. ## Run in the CI environment -The container mirrors the OS, Node and Google Chrome the tests get on CI. Dependencies and -application builds are taken from the host: +The container takes Node from `.node-version` and Chrome from the version the workflow installs; +dependencies and application builds come from the host. ```bash -cd e2e/wrappers docker/run.sh react19 +docker/run.sh --ui react19 # then open http://localhost:9323 ``` -The browser runs in the container while you watch and re-run the tests from the browser on your -machine — start the UI mode and open `http://localhost:9323`: - -```bash -docker/run.sh --ui react19 -``` - -The repository is mounted, so edits made on the host are picked up and the watch mode re-runs -the affected tests. Plain `playwright test --ui` is not used on purpose: it opens the interface -in the bundled Chromium, which is deliberately not installed. +Open the UI on `localhost` — the address Playwright prints, `0.0.0.0`, is not a secure origin and +its trace viewer will not load there. -## Investigate a failed CI run - -The `Wrappers E2E Tests` workflow uploads two artifacts per framework when tests fail: the HTML -report and the traces. Download them and open with the tools Playwright ships: +## Read a failed CI run ```bash gh run download -n playwright-report-react19 -D ci-report @@ -67,14 +41,5 @@ pnpm exec playwright show-report ci-report pnpm exec playwright show-trace ci-traces//trace.zip --port 0 ``` -The trace is a step-by-step timeline: the DOM before and after every action, network, console -output and the error, so a failure that only happens on CI can be inspected without reproducing -it. To reproduce it anyway, run the same test in the container — the environment matches. - -## Rendering - -The default Chrome scrollbars must stay visible: they take layout space, so a page rendered -without them differs from what a user sees and from the etalons the TestCafe tests produce. -Playwright hides scrollbars in headless mode by default, so `--hide-scrollbars` is removed in -`playwright.config.ts`. Measured in the CI-like container, the scrollbar is 15px wide both here -and in TestCafe. +The trace holds every action with the DOM before and after it, the network and the console, so a +failure that only happens on CI can be diagnosed without reproducing it. From f827903fd2a33fb04c212901c40e2ef106af93bd Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 10:05:40 -0300 Subject: [PATCH 09/18] Trim the docker script to its usage line and drop redundant guards --- e2e/wrappers/README.md | 3 +-- e2e/wrappers/docker/run.sh | 53 +++++++------------------------------- 2 files changed, 11 insertions(+), 45 deletions(-) diff --git a/e2e/wrappers/README.md b/e2e/wrappers/README.md index 6e92bc6525b3..8916e6d6db8b 100644 --- a/e2e/wrappers/README.md +++ b/e2e/wrappers/README.md @@ -15,8 +15,7 @@ pnpm run build:react19 # or build:vue3 / build:angular / build:all pnpm run test:react19 # or test:vue3 / test:angular ``` -`playwright test` starts the server for the built application itself. The Chat and Gantt examples -exist for React only, so they are reported as skipped for the other frameworks. +`playwright test` starts the server for the built application itself. ## Run in the CI environment diff --git a/e2e/wrappers/docker/run.sh b/e2e/wrappers/docker/run.sh index f4befae6f160..0cd917bc92fb 100755 --- a/e2e/wrappers/docker/run.sh +++ b/e2e/wrappers/docker/run.sh @@ -1,15 +1,5 @@ #!/bin/bash -# Runs the wrappers e2e tests in a container that matches the CI environment: the OS, Node and -# Google Chrome are the ones the tests get on CI. The repository is mounted as is, so -# dependencies must be installed and the app under test built on the host first: -# -# pnpm install --frozen-lockfile -# pnpm nx all:build-testing workflows -# cd e2e/wrappers && pnpm run build:react19 -# -# Usage: -# docker/run.sh [react19|vue3|angular] [extra playwright args] -# docker/run.sh --ui [react19|vue3|angular] open the UI mode at http://localhost:9323 +# Usage: docker/run.sh [--ui] [react19|vue3|angular] [playwright args] set -euo pipefail @@ -24,47 +14,24 @@ fi FRAMEWORK="${1:-react19}" shift || true -DOCKER_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$DOCKER_DIR/../../.." && pwd)" -WORKFLOW="$REPO_ROOT/.github/workflows/wrapper_tests_e2e.yml" - +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" NODE_VERSION="$(cat "$REPO_ROOT/.node-version")" -CHROME_VERSION="$(grep -m1 'chrome-version:' "$WORKFLOW" | cut -d: -f2 | tr -d " '\"")" - -if [ -z "$NODE_VERSION" ] || [ -z "$CHROME_VERSION" ]; then - echo "❌ Cannot read the Node version from .node-version or the Chrome version from $WORKFLOW." >&2 - exit 1 -fi +CHROME_VERSION="$(grep -m1 'chrome-version:' "$REPO_ROOT/.github/workflows/wrapper_tests_e2e.yml" | cut -d: -f2 | tr -d " '\"")" echo "Node $NODE_VERSION, Google Chrome $CHROME_VERSION" -# Google Chrome for Linux ships for amd64 only, so the platform is pinned as on CI. -PLATFORM=linux/amd64 - -docker build --platform "$PLATFORM" \ +docker build --platform linux/amd64 \ --build-arg "NODE_VERSION=$NODE_VERSION" \ --build-arg "CHROME_VERSION=$CHROME_VERSION" \ - -t devextreme-wrappers-e2e "$DOCKER_DIR" + -t devextreme-wrappers-e2e "$REPO_ROOT/e2e/wrappers/docker" -# Shared memory and seccomp are set as the CI runner container gets them. -DOCKER_ARGS=( - --rm - --platform "$PLATFORM" - --shm-size=2gb - --security-opt seccomp=unconfined - -v "$REPO_ROOT:/repo" - -w /repo/e2e/wrappers - -e "FRAMEWORK=$FRAMEWORK" -) -PLAYWRIGHT_ARGS=(test) +RUN=(docker run --rm --platform linux/amd64 --shm-size=2gb --security-opt seccomp=unconfined + -v "$REPO_ROOT:/repo" -w /repo/e2e/wrappers -e "FRAMEWORK=$FRAMEWORK") if [ "$UI" = true ]; then - DOCKER_ARGS+=(-p "$UI_PORT:$UI_PORT") - PLAYWRIGHT_ARGS+=(--ui-host=0.0.0.0 "--ui-port=$UI_PORT") echo "UI mode for $FRAMEWORK: open http://localhost:$UI_PORT" -else - DOCKER_ARGS+=(-e CI=true) - PLAYWRIGHT_ARGS+=("$@") + exec "${RUN[@]}" -p "$UI_PORT:$UI_PORT" devextreme-wrappers-e2e \ + node_modules/.bin/playwright test --ui-host=0.0.0.0 --ui-port="$UI_PORT" fi -docker run "${DOCKER_ARGS[@]}" devextreme-wrappers-e2e node_modules/.bin/playwright "${PLAYWRIGHT_ARGS[@]}" +exec "${RUN[@]}" -e CI=true devextreme-wrappers-e2e node_modules/.bin/playwright test "$@" From ff9c1468dbbeb813fd8a4055117ab357cce6d236 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 10:28:12 -0300 Subject: [PATCH 10/18] Forward playwright arguments in the docker UI mode --- e2e/wrappers/docker/run.sh | 2 +- e2e/wrappers/playwright.config.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e/wrappers/docker/run.sh b/e2e/wrappers/docker/run.sh index 0cd917bc92fb..d8b76f397015 100755 --- a/e2e/wrappers/docker/run.sh +++ b/e2e/wrappers/docker/run.sh @@ -31,7 +31,7 @@ RUN=(docker run --rm --platform linux/amd64 --shm-size=2gb --security-opt seccom if [ "$UI" = true ]; then echo "UI mode for $FRAMEWORK: open http://localhost:$UI_PORT" exec "${RUN[@]}" -p "$UI_PORT:$UI_PORT" devextreme-wrappers-e2e \ - node_modules/.bin/playwright test --ui-host=0.0.0.0 --ui-port="$UI_PORT" + node_modules/.bin/playwright test --ui-host=0.0.0.0 --ui-port="$UI_PORT" "$@" fi exec "${RUN[@]}" -e CI=true devextreme-wrappers-e2e node_modules/.bin/playwright test "$@" diff --git a/e2e/wrappers/playwright.config.ts b/e2e/wrappers/playwright.config.ts index d1a19573546b..738b4b2183a8 100644 --- a/e2e/wrappers/playwright.config.ts +++ b/e2e/wrappers/playwright.config.ts @@ -35,8 +35,7 @@ export default defineConfig({ viewport: { width: 1200, height: 800 }, launchOptions: { args: ['--no-sandbox', '--disable-gpu'], - // Playwright hides scrollbars in headless by default, so the page gets no scrollbar - // gutter and its layout differs from a real browser and from the TestCafe etalons. + // Playwright hides scrollbars in headless by default, which changes the page layout. ignoreDefaultArgs: ['--hide-scrollbars'], }, trace: 'retain-on-failure', From 8b908e3918f7831296e0fbd1683ee5e06d91d44e Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 13:34:22 -0300 Subject: [PATCH 11/18] Add the e2e/playwright package with the component test infrastructure --- .../workflows/testcafe_tests_playwright.yml | 321 ++++++++++++++++++ e2e/playwright/.gitignore | 1 + e2e/playwright/README.md | 100 ++++++ e2e/playwright/docker/Dockerfile | 17 + e2e/playwright/docker/run.sh | 33 ++ e2e/playwright/eslint.config.mjs | 79 +++++ e2e/playwright/fixtures.ts | 35 ++ e2e/playwright/helpers/apiMock.ts | 23 ++ e2e/playwright/helpers/const.ts | 9 + e2e/playwright/helpers/createWidget.ts | 32 ++ e2e/playwright/helpers/domUtils.ts | 126 +++++++ e2e/playwright/helpers/mockDate.ts | 10 + e2e/playwright/helpers/screenshots.ts | 74 ++++ e2e/playwright/helpers/testPageUtils.ts | 82 +++++ e2e/playwright/helpers/themeUtils.ts | 15 + e2e/playwright/models/button.ts | 24 ++ e2e/playwright/models/checkBox.ts | 23 ++ e2e/playwright/models/internal/widget.ts | 85 +++++ e2e/playwright/models/types.ts | 80 +++++ e2e/playwright/package.json | 24 ++ e2e/playwright/playwright.config.ts | 73 ++++ e2e/playwright/project.json | 34 ++ e2e/playwright/runner.mjs | 63 ++++ e2e/playwright/serve.mjs | 61 ++++ e2e/playwright/tests/container.html | 34 ++ .../tests/editors/checkBox/common.spec.ts | 24 ++ .../tests/navigation/button/common.spec.ts | 71 ++++ ...lingMode=contained (fluent.blue.light).png | Bin 0 -> 36994 bytes ...ylingMode=outlined (fluent.blue.light).png | Bin 0 -> 37094 bytes ..., stylingMode=text (fluent.blue.light).png | Bin 0 -> 33951 bytes e2e/playwright/tsconfig.json | 21 ++ pnpm-lock.yaml | 45 +++ 32 files changed, 1619 insertions(+) create mode 100644 .github/workflows/testcafe_tests_playwright.yml create mode 100644 e2e/playwright/.gitignore create mode 100644 e2e/playwright/README.md create mode 100644 e2e/playwright/docker/Dockerfile create mode 100755 e2e/playwright/docker/run.sh create mode 100644 e2e/playwright/eslint.config.mjs create mode 100644 e2e/playwright/fixtures.ts create mode 100644 e2e/playwright/helpers/apiMock.ts create mode 100644 e2e/playwright/helpers/const.ts create mode 100644 e2e/playwright/helpers/createWidget.ts create mode 100644 e2e/playwright/helpers/domUtils.ts create mode 100644 e2e/playwright/helpers/mockDate.ts create mode 100644 e2e/playwright/helpers/screenshots.ts create mode 100644 e2e/playwright/helpers/testPageUtils.ts create mode 100644 e2e/playwright/helpers/themeUtils.ts create mode 100644 e2e/playwright/models/button.ts create mode 100644 e2e/playwright/models/checkBox.ts create mode 100644 e2e/playwright/models/internal/widget.ts create mode 100644 e2e/playwright/models/types.ts create mode 100644 e2e/playwright/package.json create mode 100644 e2e/playwright/playwright.config.ts create mode 100644 e2e/playwright/project.json create mode 100644 e2e/playwright/runner.mjs create mode 100644 e2e/playwright/serve.mjs create mode 100644 e2e/playwright/tests/container.html create mode 100644 e2e/playwright/tests/editors/checkBox/common.spec.ts create mode 100644 e2e/playwright/tests/navigation/button/common.spec.ts create mode 100644 e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=contained (fluent.blue.light).png create mode 100644 e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=outlined (fluent.blue.light).png create mode 100644 e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=text (fluent.blue.light).png create mode 100644 e2e/playwright/tsconfig.json diff --git a/.github/workflows/testcafe_tests_playwright.yml b/.github/workflows/testcafe_tests_playwright.yml new file mode 100644 index 000000000000..67882266e690 --- /dev/null +++ b/.github/workflows/testcafe_tests_playwright.yml @@ -0,0 +1,321 @@ +name: Playwright tests + +concurrency: + group: wf-${{github.event.pull_request.number || github.sha}}-${{github.workflow}} + cancel-in-progress: true + +on: + pull_request: + paths-ignore: + - 'apps/**/*.md' + merge_group: + workflow_dispatch: + +env: + NX_SKIP_NX_CACHE: ${{ contains(github.event.pull_request.labels.*.name, 'skip-cache') && 'true' || 'false' }} + RUN_TESTS: true + +jobs: + check-should-run: + name: Check if tests should run + runs-on: ubuntu-latest + outputs: + should-run: ${{ steps.check.outputs.should-run }} + styles-changed: ${{ steps.changes.outputs.styles }} + steps: + - uses: actions/checkout@v6 + - name: Check RUN_TESTS flag + id: check + run: echo "should-run=${{ env.RUN_TESTS }}" >> $GITHUB_OUTPUT + + - uses: dorny/paths-filter@v4 + id: changes + with: + filters: | + styles: + - 'packages/devextreme-scss/scss/**' + - 'packages/devextreme-scss/icons/**' + - 'packages/devextreme-scss/images/**' + - 'packages/devextreme-scss/fonts/**' + - 'packages/devextreme/js/**/themes/**' + - 'e2e/playwright/helpers/accessibility/**' + + build: + name: Build DevExtreme + needs: check-should-run + if: needs.check-should-run.outputs.should-run == 'true' + + runs-on: devextreme-shr2 + timeout-minutes: 15 + + steps: + - name: Get sources + uses: actions/checkout@v6 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: '.node-version' + + - uses: pnpm/action-setup@v6 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v5 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-cache + + - uses: actions/cache@v5 + name: Setup nx cache + with: + path: | + .nx/cache + .nx/workspace-data/*.db + .nx/workspace-data/*.db-wal + .nx/workspace-data/*.db-shm + key: ${{ runner.os }}-nx-v2-${{ github.workflow }}-${{ github.job }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-nx-v2-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}- + ${{ runner.os }}-nx-v2-${{ github.workflow }}-${{ github.job }}- + ${{ runner.os }}-nx-v2- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + shell: bash + env: + NODE_OPTIONS: --max-old-space-size=8192 + run: | + pnpm exec nx build devextreme-scss + pnpm exec nx build devextreme -c testing + + - name: Zip artifacts + working-directory: ./packages/devextreme + run: | + 7z a -tzip -mx3 -mmt2 artifacts.zip artifacts ../devextreme-scss/scss/bundles + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: devextreme-artifacts-playwright + path: ./packages/devextreme/artifacts.zip + retention-days: 1 + + determine-playwright-matrix: + runs-on: ubuntu-latest + name: Determine Playwright matrix + needs: check-should-run + if: needs.check-should-run.outputs.should-run == 'true' + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + + steps: + - name: Build matrix + id: matrix + run: | + base_matrix_start=$(cat <<'JSON' + [ + { "componentFolder": "accessibility", "name": "accessibility", "concurrency": 6 } + ] + JSON + ) + + base_matrix_end=$(cat <<'JSON' + [ + { "componentFolder": "common", "name": "common", "concurrency": 3 }, + + { "name": "generic", "theme": "generic.light", "concurrency": 2 }, + { "name": "material", "theme": "material.blue.light", "concurrency": 2 }, + { "name": "material - compact", "theme": "material.blue.light.compact", "concurrency": 2 }, + + { "componentFolder": "cardView", "name": "cardView" }, + { "componentFolder": "dataGrid", "name": "dataGrid (1/4)", "indices": "1/4", "concurrency": 5 }, + { "componentFolder": "dataGrid", "name": "dataGrid (2/4)", "indices": "2/4", "concurrency": 5 }, + { "componentFolder": "dataGrid", "name": "dataGrid (3/4)", "indices": "3/4", "concurrency": 5 }, + { "componentFolder": "dataGrid", "name": "dataGrid (4/4)", "indices": "4/4", "concurrency": 5 }, + + { "componentFolder": "editors", "name": "editors" }, + { "componentFolder": "navigation", "name": "navigation", "concurrency": 6 }, + + { "componentFolder": "scheduler/common", "name": "scheduler / common (1/3)", "indices": "1/3", "concurrency": 6 }, + { "componentFolder": "scheduler/common", "name": "scheduler / common (2/3)", "indices": "2/3", "concurrency": 6 }, + { "componentFolder": "scheduler/common", "name": "scheduler / common (3/3)", "indices": "3/3", "concurrency": 6 }, + { "componentFolder": "scheduler/viewOffset", "name": "scheduler / viewOffset (1/2)", "indices": "1/2", "concurrency": 4 }, + { "componentFolder": "scheduler/viewOffset", "name": "scheduler / viewOffset (2/2)", "indices": "2/2", "concurrency": 4 }, + { "componentFolder": "scheduler/timezones", "name": "scheduler / timezones (Europe/Berlin)", "timezone": "Europe/Berlin" }, + { "componentFolder": "scheduler/timezones", "name": "scheduler / timezones (America/Los_Angeles)", "timezone": "America/Los_Angeles" } + ] + JSON + ) + + style_dependent_matrix=$(cat <<'JSON' + [ + { "componentFolder": "accessibility", "name": "accessibility - fluent.dark", "theme": "fluent.blue.dark", "concurrency": 6 }, + { "componentFolder": "accessibility", "name": "accessibility - material.light", "theme": "material.blue.light", "concurrency": 6 }, + { "componentFolder": "accessibility", "name": "accessibility - material.dark", "theme": "material.blue.dark", "concurrency": 6 } + ] + JSON + ) + + matrix=$(jq -c -n \ + --argjson baseStart "$base_matrix_start" \ + --argjson baseEnd "$base_matrix_end" \ + --argjson styleDependent "$style_dependent_matrix" \ + --arg stylesChanged "${{ needs.check-should-run.outputs.styles-changed }}" \ + '{ include: ($baseStart + (if $stylesChanged == "true" then $styleDependent else [] end) + $baseEnd) }') + + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + playwright: + name: ${{ matrix.name }}${{ !matrix.theme && ' - fluent' || '' }} + if: | + needs.check-should-run.outputs.should-run == 'true' && + needs.determine-playwright-matrix.result == 'success' && + needs.build.result == 'success' + needs: [check-should-run, build, determine-playwright-matrix] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.determine-playwright-matrix.outputs.matrix) }} + runs-on: devextreme-shr2 + timeout-minutes: 45 + + steps: + - name: Get sources + uses: actions/checkout@v6 + + - name: Download artifacts + uses: actions/download-artifact@v8 + with: + name: devextreme-artifacts-playwright + path: ./packages/devextreme + + - name: Unpack artifacts + working-directory: ./packages/devextreme + run: 7z x artifacts.zip -aoa + + - name: Setup Chrome + uses: ./.github/actions/setup-chrome + with: + chrome-version: '150.0.7871.181' + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: '.node-version' + + - uses: pnpm/action-setup@v6 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache/restore@v5 + name: Restore pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-cache + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run Playwright tests + working-directory: ./e2e/playwright + env: + NODE_OPTIONS: --max-old-space-size=8192 + # The browser context takes the timezone, so the agent clock is left alone. + TIMEZONE: ${{ matrix.timezone }} + run: | + THEME="--theme ${{ matrix.theme || 'fluent.blue.light' }}" + [ "${{ matrix.componentFolder }}" != "" ] && COMPONENT_FOLDER="--componentFolder ${{ matrix.componentFolder }}" + [ "${{ matrix.indices }}" != "" ] && INDICES="--indices ${{ matrix.indices }}" + [ "${{ matrix.concurrency }}" != "" ] && CONCURRENCY="--concurrency ${{ matrix.concurrency }}" + all_args="$COMPONENT_FOLDER $CONCURRENCY $INDICES $THEME" + echo "$all_args" + pnpm run test $all_args + + - name: Sanitize job name + if: ${{ always() }} + run: echo "JOB_NAME=$(echo "${{ matrix.name }}" | tr '/' '-')" >> $GITHUB_ENV + + - name: Upload test report + if: ${{ failure() }} + uses: actions/upload-artifact@v7 + with: + name: playwright-report-${{ env.JOB_NAME }} + path: ${{ github.workspace }}/e2e/playwright/artifacts/playwright-report + if-no-files-found: ignore + + - name: Upload test traces and screenshot diffs + if: ${{ failure() }} + uses: actions/upload-artifact@v7 + with: + name: playwright-traces-${{ env.JOB_NAME }} + path: ${{ github.workspace }}/e2e/playwright/artifacts/test-results + if-no-files-found: ignore + + merge-artifacts: + runs-on: ubuntu-latest + needs: [playwright] + if: ${{ always() }} + + steps: + - name: Detect mergeable artifacts + id: mergeable-artifacts + uses: actions/github-script@v9 + with: + script: | + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId, + per_page: 100, + }); + + const artifactGroups = { + reports: /^playwright-report-/, + traces: /^playwright-traces-/, + }; + + for (const [outputName, pattern] of Object.entries(artifactGroups)) { + const names = artifacts + .map((artifact) => artifact.name) + .filter((name) => pattern.test(name)); + + core.setOutput(outputName, names.length > 0 ? 'true' : 'false'); + core.info(`${outputName}: ${names.length ? names.join(', ') : '(none)'}`); + } + + - name: Merge test reports + if: steps.mergeable-artifacts.outputs.reports == 'true' + uses: actions/upload-artifact/merge@v7 + continue-on-error: true + with: + name: playwright-reports + pattern: playwright-report-* + separate-directories: true + delete-merged: true + + - name: Merge test traces + if: steps.mergeable-artifacts.outputs.traces == 'true' + uses: actions/upload-artifact/merge@v7 + continue-on-error: true + with: + name: playwright-traces + pattern: playwright-traces-* + separate-directories: true + delete-merged: true diff --git a/e2e/playwright/.gitignore b/e2e/playwright/.gitignore new file mode 100644 index 000000000000..d4f588edfef5 --- /dev/null +++ b/e2e/playwright/.gitignore @@ -0,0 +1 @@ +artifacts/ diff --git a/e2e/playwright/README.md b/e2e/playwright/README.md new file mode 100644 index 000000000000..35b78a12809b --- /dev/null +++ b/e2e/playwright/README.md @@ -0,0 +1,100 @@ +# Component E2E tests (Playwright) + +End-to-end tests for DevExtreme components. They run on [Playwright](https://playwright.dev/) and +use the system Google Chrome (`channel: 'chrome'`), so no browser download is needed. + +While the migration is in progress, `e2e/testcafe-devextreme` stays the source of truth: this +package holds the tests that are already migrated, and its workflow runs the same matrix so both +runs stay comparable. + +## Run locally + +```bash +pnpm install --frozen-lockfile +pnpm exec nx build devextreme-scss +pnpm exec nx build devextreme -c testing + +cd e2e/playwright +pnpm run test # everything, fluent.blue.light +pnpm run test --componentFolder navigation # one component folder +pnpm run test --theme material.blue.light # another theme +pnpm run test --indices 1/4 --concurrency 5 # one shard, five workers +pnpm run test --test "Buttons, stylingMode=text" # one test by name +``` + +The runner takes the flags the TestCafe one takes (`--componentFolder`, `--indices`, `--theme`, +`--concurrency`, `--test`) and passes everything else straight to `playwright test`, so +`pnpm run test --headed --debug` works too. The static server for the test page starts on its own. + +## Screenshots + +Etalons live in `etalons/` next to the test and carry the theme in the file name — +`Buttons, stylingMode=text (fluent.blue.light).png` — the same convention the TestCafe run uses. + +The pixel budget is asymmetric on purpose: **CI is the source of truth** (`maxDiffPixelRatio` +0.001, `threshold` 0.1) and a local run is a sanity check (0.05 / 0.2). + +Run the screenshot tests in the container and nowhere else. The etalons are Linux renders, and a +macOS or Windows one differs in text metrics — the image comes out a different size, which no +tolerance can absorb. The same goes for writing them: + +```bash +docker/run.sh # verify +docker/run.sh --update-snapshots # rewrite the etalons of the tests that ran +``` + +## Run in the CI environment + +The container takes Node from `.node-version` and Chrome from the version the workflow installs; +dependencies and the DevExtreme build come from the host. + +```bash +docker/run.sh # the whole suite, as CI runs it +docker/run.sh --ui # then open http://localhost:9323 +``` + +Open the UI on `localhost` — the address Playwright prints, `0.0.0.0`, is not a secure origin and +its trace viewer will not load there. + +## Read a failed CI run + +```bash +gh run download -n playwright-report-navigation -D ci-report +gh run download -n playwright-traces-navigation -D ci-traces +pnpm exec playwright show-report ci-report +pnpm exec playwright show-trace ci-traces//trace.zip --port 0 +``` + +The trace holds every action with the DOM before and after it, the network and the console, and a +failed screenshot comparison uploads its `-expected`, `-actual` and `-diff` images alongside, so a +failure that only happens on CI can be diagnosed without reproducing it. + +## Writing a test + +```ts +import { expect, test } from '../../../fixtures'; +import { createWidget } from '../../../helpers/createWidget'; +import { testScreenshot } from '../../../helpers/screenshots'; +import Button from '../../../models/button'; + +test('Button reports its text', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Find' }); + + const button = new Button(page, '#container .dx-button'); + + await expect(button.text).toHaveText('Find'); + await testScreenshot(page, 'Button with text.png'); +}); +``` + +`page` arrives ready: the container page is open, the theme requested by `--theme` is applied, the +viewport is 1200×800 and the widgets of the previous test are disposed. + +| TestCafe | here | +|---------------------------------|-----------------------------------------------| +| `Selector` | `Locator` (`models/`) | +| `ClientFunction` | `page.evaluate` / `locator.evaluate` | +| `devextreme-screenshot-comparer`| `testScreenshot` on top of `toHaveScreenshot` | +| `RequestMock` | `mockApi` on top of `page.route` | +| `mockdate` | `mockDate` on top of `page.clock` | +| machine timezone on the agent | `TIMEZONE` → the browser context | diff --git a/e2e/playwright/docker/Dockerfile b/e2e/playwright/docker/Dockerfile new file mode 100644 index 000000000000..296435b9cc75 --- /dev/null +++ b/e2e/playwright/docker/Dockerfile @@ -0,0 +1,17 @@ +# Mirrors the environment the tests get on CI. +FROM ubuntu:24.04 + +ARG NODE_VERSION +ARG CHROME_VERSION + +RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/dx-no-recommends \ + && apt-get update \ + && apt-get install -y ca-certificates curl fonts-noto-color-emoji \ + && curl -fsSL "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}-1_amd64.deb" -o /tmp/chrome.deb \ + && apt-get install -y /tmp/chrome.deb \ + && curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" -o /tmp/node.tar.gz \ + && tar -xzf /tmp/node.tar.gz -C /usr/local --strip-components=1 \ + && rm /tmp/chrome.deb /tmp/node.tar.gz \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /repo/e2e/playwright diff --git a/e2e/playwright/docker/run.sh b/e2e/playwright/docker/run.sh new file mode 100755 index 000000000000..006ed53f6020 --- /dev/null +++ b/e2e/playwright/docker/run.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Usage: docker/run.sh [--ui] [playwright args] +set -euo pipefail + +UI_PORT=9323 +UI=false + +if [ "${1:-}" = "--ui" ]; then + UI=true + shift +fi + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +NODE_VERSION="$(cat "$REPO_ROOT/.node-version")" +CHROME_VERSION="$(grep -m1 "chrome-version:" "$REPO_ROOT/.github/workflows/testcafe_tests_playwright.yml" | cut -d: -f2 | tr -d " '\"")" + +echo "Node $NODE_VERSION, Google Chrome $CHROME_VERSION" + +docker build --platform linux/amd64 \ + --build-arg "NODE_VERSION=$NODE_VERSION" \ + --build-arg "CHROME_VERSION=$CHROME_VERSION" \ + -t devextreme-playwright-e2e "$REPO_ROOT/e2e/playwright/docker" + +RUN=(docker run --rm --platform linux/amd64 --shm-size=2gb --security-opt seccomp=unconfined + -v "$REPO_ROOT:/repo" -w /repo/e2e/playwright) + +if [ "$UI" = true ]; then + echo "UI mode: open http://localhost:$UI_PORT" + exec "${RUN[@]}" -p "$UI_PORT:$UI_PORT" devextreme-playwright-e2e \ + node_modules/.bin/playwright test --ui-host=0.0.0.0 --ui-port="$UI_PORT" "$@" +fi + +exec "${RUN[@]}" -e CI=true devextreme-playwright-e2e node_modules/.bin/playwright test "$@" diff --git a/e2e/playwright/eslint.config.mjs b/e2e/playwright/eslint.config.mjs new file mode 100644 index 000000000000..704158b41664 --- /dev/null +++ b/e2e/playwright/eslint.config.mjs @@ -0,0 +1,79 @@ +/* eslint-disable spellcheck/spell-checker */ +import noOnlyTests from 'eslint-plugin-no-only-tests'; +import tsParser from '@typescript-eslint/parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import stylistic from '@stylistic/eslint-plugin'; +import { changeRulesToStylistic } from 'eslint-migration-utils'; +import spellCheckConfig from 'eslint-config-devextreme/spell-check'; +import typescriptConfig from 'eslint-config-devextreme/typescript'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default [ + { + ignores: [ + 'artifacts/**', + 'node_modules/**', + ], + }, + ...spellCheckConfig, + { + plugins: { + 'no-only-tests': noOnlyTests, + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.mjs', '.ts'], + }, + }, + }, + }, + ...typescriptConfig.map((config) => { + const newConfig = { + ...config, + files: ['**/*.ts'], + }; + + if (config.rules) { + newConfig.rules = changeRulesToStylistic(config.rules); + } + + return newConfig; + }), + { + files: ['**/*.ts'], + plugins: { + '@stylistic': stylistic, + }, + languageOptions: { + parser: tsParser, + sourceType: 'module', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + rules: { + '@typescript-eslint/no-unused-vars': ['error', { + ignoreRestSiblings: true, + caughtErrors: 'none', + }], + 'no-only-tests/no-only-tests': 'error', + 'no-await-in-loop': 'off', + 'no-restricted-globals': 'off', + 'no-restricted-syntax': 'off', + 'require-await': 'off', + '@typescript-eslint/require-await': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/prefer-default-export': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@stylistic/no-extra-parens': 'off', + }, + }, +]; diff --git a/e2e/playwright/fixtures.ts b/e2e/playwright/fixtures.ts new file mode 100644 index 000000000000..42e03d50e0db --- /dev/null +++ b/e2e/playwright/fixtures.ts @@ -0,0 +1,35 @@ +import { test as base } from '@playwright/test'; +import { DEFAULT_BROWSER_SIZE, DEFAULT_THEME } from './helpers/const'; +import { + changeTheme, + clearTestPage, + openTestPage, + resetPageState, +} from './helpers/testPageUtils'; + +export interface TestOptions { + theme: string; + browserSize: [number, number]; +} + +// Overriding "page" gives every test the prepared container page, the way the TestCafe runner +// did it from its global test hooks. +export const test = base.extend({ + theme: [DEFAULT_THEME, { option: true }], + browserSize: [DEFAULT_BROWSER_SIZE, { option: true }], + + page: async ({ page, theme, browserSize }, use) => { + const [width, height] = browserSize; + + await page.setViewportSize({ width, height }); + await openTestPage(page); + await changeTheme(page, theme); + await resetPageState(page); + + await use(page); + + await clearTestPage(page); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/e2e/playwright/helpers/apiMock.ts b/e2e/playwright/helpers/apiMock.ts new file mode 100644 index 000000000000..85cef2471226 --- /dev/null +++ b/e2e/playwright/helpers/apiMock.ts @@ -0,0 +1,23 @@ +import type { Page } from '@playwright/test'; + +export interface MockedRequest { + url: RegExp | string; + body: unknown; + status?: number; + headers?: Record; +} + +// Replaces the TestCafe RequestMock: the rules are matched in the order they are declared, +// so a narrower rule has to come before the wider one, just as it did there. +export async function mockApi(page: Page, requests: MockedRequest[]): Promise { + await Promise.all(requests.map(({ + url, body, status = 200, headers, + }) => page.route(url, async (route) => { + await route.fulfill({ + status, + contentType: 'application/json', + headers: { 'access-control-allow-origin': '*', ...headers }, + body: JSON.stringify(body), + }); + }))); +} diff --git a/e2e/playwright/helpers/const.ts b/e2e/playwright/helpers/const.ts new file mode 100644 index 000000000000..7dafa3374eca --- /dev/null +++ b/e2e/playwright/helpers/const.ts @@ -0,0 +1,9 @@ +export const DEFAULT_BROWSER_SIZE: [number, number] = [1200, 800]; + +export const DEFAULT_THEME = 'fluent.blue.light'; + +export const DEFAULT_SELECTOR = '#container'; + +export const SERVER_PORT = 8080; + +export const TEST_PAGE_URL = '/e2e/playwright/tests/container.html'; diff --git a/e2e/playwright/helpers/createWidget.ts b/e2e/playwright/helpers/createWidget.ts new file mode 100644 index 000000000000..4559a84ff72a --- /dev/null +++ b/e2e/playwright/helpers/createWidget.ts @@ -0,0 +1,32 @@ +import type { Page } from '@playwright/test'; +import type { WidgetName, WidgetOptions } from '../models/types'; +import { DEFAULT_SELECTOR } from './const'; + +export interface CreateWidgetOptions { + disableFxAnimation: boolean; +} + +const DEFAULT_OPTIONS: CreateWidgetOptions = { + disableFxAnimation: true, +}; + +export const createWidget = async ( + page: Page, + widgetName: TWidgetName, + widgetOptions: TWidgetName extends keyof WidgetOptions ? WidgetOptions[TWidgetName] : unknown, + selector: string = DEFAULT_SELECTOR, + { disableFxAnimation } = DEFAULT_OPTIONS, +): Promise => page.evaluate( + ({ + name, options, elementSelector, disableAnimation, + }) => { + (window as any).DevExpress.fx.off = disableAnimation; + (window as any).widget = ($(elementSelector) as any)[name](options)[name]('instance'); + }, + { + name: widgetName, + options: widgetOptions, + elementSelector: selector, + disableAnimation: disableFxAnimation, + }, +); diff --git a/e2e/playwright/helpers/domUtils.ts b/e2e/playwright/helpers/domUtils.ts new file mode 100644 index 000000000000..e9c35622cadd --- /dev/null +++ b/e2e/playwright/helpers/domUtils.ts @@ -0,0 +1,126 @@ +import type { Page } from '@playwright/test'; + +const STYLESHEET_RULES_ID = 'stylesheetRules'; + +export const setAttribute = async ( + page: Page, + selector: string, + attribute: string, + value: string, +): Promise => page.evaluate( + ({ elementSelector, name, attributeValue }) => { + document.querySelector(elementSelector)?.setAttribute(name, attributeValue); + }, + { elementSelector: selector, name: attribute, attributeValue: value }, +); + +export const removeAttribute = async ( + page: Page, + selector: string, + attribute: string, +): Promise => page.evaluate( + ({ elementSelector, name }) => { + document.querySelector(elementSelector)?.removeAttribute(name); + }, + { elementSelector: selector, name: attribute }, +); + +export const appendAttributeValue = async ( + page: Page, + selector: string, + attribute: string, + value: string, +): Promise => page.evaluate( + ({ elementSelector, name, attributeValue }) => { + const element = document.querySelector(elementSelector); + const current = element?.getAttribute(name) ?? ''; + + element?.setAttribute(name, `${current} ${attributeValue}`); + }, + { elementSelector: selector, name: attribute, attributeValue: value }, +); + +export const setStyleAttribute = async ( + page: Page, + selector: string, + value: string, +): Promise => appendAttributeValue(page, selector, 'style', value); + +export const setClassAttribute = async ( + page: Page, + selector: string, + value: string, +): Promise => appendAttributeValue(page, selector, 'class', value); + +export const removeClassAttribute = async ( + page: Page, + selector: string, + value: string, +): Promise => page.evaluate( + ({ elementSelector, className }) => { + const element = document.querySelector(elementSelector); + const classes = element?.getAttribute('class') ?? ''; + + element?.setAttribute('class', classes.replace(className, '')); + }, + { elementSelector: selector, className: value }, +); + +export const appendElementTo = async ( + page: Page, + parentSelector: string, + tagName: string, + id: string, + style: Record = {}, +): Promise => page.evaluate( + ({ + parent, tag, elementId, elementStyle, + }) => { + const element = document.createElement(tag); + + element.setAttribute('id', elementId); + Object.entries(elementStyle).forEach(([key, value]) => { element.style[key] = value; }); + + document.querySelector(parent)?.appendChild(element); + }, + { + parent: parentSelector, tag: tagName, elementId: id, elementStyle: style, + }, +); + +export const addCaptionTo = async ( + page: Page, + parentSelector: string, + caption: string, +): Promise => page.evaluate( + ({ parent, text }) => { + const element = document.createElement('div'); + + element.textContent = text; + document.querySelector(parent)?.appendChild(element); + }, + { parent: parentSelector, text: caption }, +); + +export const insertStylesheetRulesToPage = async ( + page: Page, + rules: string, +): Promise => page.evaluate( + ({ stylesheetId, cssRules }) => { + let style = document.getElementById(stylesheetId) as HTMLStyleElement | null; + + if (!style) { + style = document.createElement('style'); + style.id = stylesheetId; + document.head.appendChild(style); + } + + style.sheet?.insertRule(cssRules, style.sheet.cssRules.length); + }, + { stylesheetId: STYLESHEET_RULES_ID, cssRules: rules }, +); + +export const removeStylesheetRulesFromPage = async (page: Page): Promise => page.evaluate( + (stylesheetId) => { document.getElementById(stylesheetId)?.remove(); }, + STYLESHEET_RULES_ID, +); diff --git a/e2e/playwright/helpers/mockDate.ts b/e2e/playwright/helpers/mockDate.ts new file mode 100644 index 000000000000..d32cdb8a6b5e --- /dev/null +++ b/e2e/playwright/helpers/mockDate.ts @@ -0,0 +1,10 @@ +import type { Page } from '@playwright/test'; + +// Runs before any page script, so the widgets see the mocked date from their very first render. +export async function mockDate(page: Page, date: string): Promise { + await page.clock.install({ time: new Date(date) }); +} + +export async function setSystemTime(page: Page, date: string): Promise { + await page.clock.setSystemTime(new Date(date)); +} diff --git a/e2e/playwright/helpers/screenshots.ts b/e2e/playwright/helpers/screenshots.ts new file mode 100644 index 000000000000..01971f99f69d --- /dev/null +++ b/e2e/playwright/helpers/screenshots.ts @@ -0,0 +1,74 @@ +import type { Locator, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { DEFAULT_SELECTOR } from './const'; +import { getFullThemeName, getThemePostfix } from './themeUtils'; +import { changeTheme } from './testPageUtils'; + +export interface ScreenshotOptions { + element?: Locator | string | null; + theme?: string; + shouldTestInCompact?: boolean; + compactCallBack?: () => Promise; + themeChanged?: () => Promise; +} + +// The etalons carry the theme in their file name — the same convention the TestCafe run uses, +// so the existing " (fluent.blue.light).png" files are reused as is. +const getScreenshotName = (baseName: string, theme?: string): string => { + const themePostfix = getThemePostfix(theme); + + return baseName.endsWith('.png') + ? baseName.replace('.png', `${themePostfix}.png`) + : `${baseName}${themePostfix}.png`; +}; + +const resolveTarget = ( + page: Page, + element: Locator | string | null | undefined, +): Locator | Page => { + if (element === null) { + return page; + } + + if (typeof element === 'string') { + return page.locator(element); + } + + return element ?? page.locator(DEFAULT_SELECTOR); +}; + +export async function testScreenshot( + page: Page, + screenshotName: string, + options?: ScreenshotOptions, +): Promise { + const { + element, + theme, + shouldTestInCompact = false, + compactCallBack, + themeChanged, + } = options ?? {}; + + if (theme) { + await changeTheme(page, theme); + await themeChanged?.(); + } + + const target = resolveTarget(page, element); + + await expect(target).toHaveScreenshot([getScreenshotName(screenshotName, theme)]); + + if (shouldTestInCompact) { + const themeName = theme ?? getFullThemeName(); + + await changeTheme(page, `${themeName}.compact`); + await compactCallBack?.(); + + await expect(target).toHaveScreenshot([getScreenshotName(screenshotName, `${themeName}.compact`)]); + } + + if (theme || shouldTestInCompact) { + await changeTheme(page, getFullThemeName()); + } +} diff --git a/e2e/playwright/helpers/testPageUtils.ts b/e2e/playwright/helpers/testPageUtils.ts new file mode 100644 index 000000000000..3fe744e79eaf --- /dev/null +++ b/e2e/playwright/helpers/testPageUtils.ts @@ -0,0 +1,82 @@ +import type { Page } from '@playwright/test'; +import { DEFAULT_THEME, TEST_PAGE_URL } from './const'; + +export async function openTestPage(page: Page): Promise { + await page.goto(TEST_PAGE_URL); + await page.waitForFunction(() => !!(window as any).DevExpress); +} + +export async function changeTheme(page: Page, themeName: string): Promise { + await page.evaluate(async (theme) => { + const { themes } = (window as any).DevExpress.ui; + + if (themes.current() === theme) { + return; + } + + await new Promise((resolve) => { + themes.ready(resolve); + themes.current(theme); + }); + }, themeName); +} + +export async function getCurrentTheme(page: Page): Promise { + const theme = await page.evaluate(() => (window as any).DevExpress?.ui.themes.current()); + + return theme ?? DEFAULT_THEME; +} + +export async function clearTestPage(page: Page): Promise { + await page.evaluate(() => { + const widgetSelector = '.dx-widget'; + const $elements = $(widgetSelector) + .filter((_, element) => $(element).parents(widgetSelector).length === 0); + + $elements.each((_, element) => { + const $widgetElement = $(element); + const widgetNames = $widgetElement.data().dxComponents; + + widgetNames?.forEach((name) => { + if ($widgetElement.hasClass('dx-widget')) { + ($widgetElement as any)[name]?.('dispose'); + } + }); + $widgetElement.empty(); + }); + + document.getElementById('focusable-start')?.remove(); + document.getElementById('stylesheetRules')?.remove(); + + const body = document.querySelector('body'); + + if (body) { + body.innerHTML = ''; + body.className = 'dx-surface'; + } + + const temp = document.createElement('div'); + + temp.innerHTML = ` +
+

Test header

+
+
+
+ `; + + body?.prepend(temp.firstElementChild!); + }); +} + +export async function resetPageState(page: Page): Promise { + await page.evaluate(() => { + if (document.activeElement && document.activeElement !== document.body) { + (document.activeElement as HTMLElement).blur(); + } + + window.getSelection()?.removeAllRanges(); + }); + + await page.mouse.move(1, 1); +} diff --git a/e2e/playwright/helpers/themeUtils.ts b/e2e/playwright/helpers/themeUtils.ts new file mode 100644 index 000000000000..143c2d98f2ef --- /dev/null +++ b/e2e/playwright/helpers/themeUtils.ts @@ -0,0 +1,15 @@ +import { DEFAULT_THEME } from './const'; + +export const getFullThemeName = (): string => process.env.THEME ?? DEFAULT_THEME; + +export const getThemeName = (): string => getFullThemeName().split('.')[0]; + +export const getDarkThemeName = (): string => getFullThemeName().replace('light', 'dark'); + +export const isMaterial = (): boolean => getFullThemeName().startsWith('material'); + +export const isFluent = (): boolean => getFullThemeName().startsWith('fluent'); + +export const isMaterialBased = (): boolean => isMaterial() || isFluent(); + +export const getThemePostfix = (theme?: string): string => ` (${theme ?? getFullThemeName()})`; diff --git a/e2e/playwright/models/button.ts b/e2e/playwright/models/button.ts new file mode 100644 index 000000000000..81df6f59b1d2 --- /dev/null +++ b/e2e/playwright/models/button.ts @@ -0,0 +1,24 @@ +import type { Locator } from '@playwright/test'; +import type { WidgetName } from './types'; +import Widget from './internal/widget'; + +export const CLASS = { + text: '.dx-button-text', + selected: 'dx-state-selected', + itemSelected: 'dx-item-selected', +}; + +export default class Button extends Widget { + public static className = 'dx-button'; + + public get text(): Locator { + return this.element.locator(CLASS.text); + } + + public isSelected(): Promise { + return this.hasClass(CLASS.selected); + } + + // eslint-disable-next-line class-methods-use-this + public getName(): WidgetName { return 'dxButton'; } +} diff --git a/e2e/playwright/models/checkBox.ts b/e2e/playwright/models/checkBox.ts new file mode 100644 index 000000000000..0ba7c189b705 --- /dev/null +++ b/e2e/playwright/models/checkBox.ts @@ -0,0 +1,23 @@ +import type { WidgetName } from './types'; +import Widget from './internal/widget'; + +export const CLASS = { + checked: 'dx-checkbox-checked', + indeterminate: 'dx-checkbox-indeterminate', + icon: '.dx-checkbox-icon', +}; + +export default class CheckBox extends Widget { + public static className = 'dx-checkbox'; + + public isChecked(): Promise { + return this.hasClass(CLASS.checked); + } + + public isIndeterminate(): Promise { + return this.hasClass(CLASS.indeterminate); + } + + // eslint-disable-next-line class-methods-use-this + public getName(): WidgetName { return 'dxCheckBox'; } +} diff --git a/e2e/playwright/models/internal/widget.ts b/e2e/playwright/models/internal/widget.ts new file mode 100644 index 000000000000..5248e39ae630 --- /dev/null +++ b/e2e/playwright/models/internal/widget.ts @@ -0,0 +1,85 @@ +import type { Locator, Page } from '@playwright/test'; +import type { WidgetName } from '../types'; + +const CLASS = { + focused: 'dx-state-focused', + hovered: 'dx-state-hover', + active: 'dx-state-active', + disabled: 'dx-state-disabled', +}; + +export default abstract class Widget { + public static className = ''; + + public readonly page: Page; + + public readonly element: Locator; + + constructor(page: Page, selector: Locator | string) { + this.page = page; + this.element = typeof selector === 'string' ? page.locator(selector) : selector; + } + + static addClassPrefix(widgetName: string, className: string): string { + return `dx-${widgetName.slice(2).toLowerCase() + (className ? `-${className}` : '')}`; + } + + public isFocused(): Promise { + return this.hasClass(CLASS.focused); + } + + public isHovered(): Promise { + return this.hasClass(CLASS.hovered); + } + + public isActive(): Promise { + return this.hasClass(CLASS.active); + } + + public isDisabled(): Promise { + return this.hasClass(CLASS.disabled); + } + + public async hasClass(className: string): Promise { + const classes = await this.element.getAttribute('class'); + + return classes?.split(' ').includes(className) ?? false; + } + + public async option(option: string | Record, ...value: unknown[]): Promise { + const isSetter = value.length > 0 || typeof option === 'object'; + + return isSetter + ? this.invoke('option', option, ...value) + : this.invoke('option', option); + } + + public async focus(): Promise { + await this.invoke('focus'); + } + + public async repaint(): Promise { + await this.invoke('repaint'); + } + + public async invoke(method: string, ...args: unknown[]): Promise { + return this.element.evaluate( + (element, { name, methodArgs, widgetName }) => { + const $element = $(element); + const data = $element.data(); + const widgetNames = data.dxComponents ?? []; + + if (!widgetNames.length) { + throw new Error(`jQuery widget not found for element ${element.outerHTML.slice(0, 100)}`); + } + + const instance = data[widgetName] ?? data[widgetNames[0]]; + + return instance[name](...methodArgs); + }, + { name: method, methodArgs: args, widgetName: this.getName() }, + ); + } + + abstract getName(): WidgetName; +} diff --git a/e2e/playwright/models/types.ts b/e2e/playwright/models/types.ts new file mode 100644 index 000000000000..95664ba3611f --- /dev/null +++ b/e2e/playwright/models/types.ts @@ -0,0 +1,80 @@ +import type { Properties as DataGridProperties } from 'devextreme/ui/data_grid'; +import type { Properties as FilterBuilderProperties } from 'devextreme/ui/filter_builder'; +import type { Properties as GanttProperties } from 'devextreme/ui/gantt'; + +export type WidgetName = 'dxAccordion' + | 'dxActionSheet' + | 'dxAutocomplete' + | 'dxButton' + | 'dxButtonGroup' + | 'dxCalendar' + | 'dxCalendarView' + | 'dxCardView' + | 'dxChart' + | 'dxChat' + | 'dxCheckBox' + | 'dxColorBox' + | 'dxContextMenu' + | 'dxDataGrid' + | 'dxDateBox' + | 'dxDateRangeBox' + | 'dxDrawer' + | 'dxDropDownBox' + | 'dxDropDownButton' + | 'dxDropDownMenu' + | 'dxDraggable' + | 'dxFileManager' + | 'dxFileUploader' + | 'dxFilterBuilder' + | 'dxForm' + | 'dxGallery' + | 'dxHtmlEditor' + | 'dxList' + | 'dxLoadIndicator' + | 'dxLoadPanel' + | 'dxLookup' + | 'dxMap' + | 'dxMenu' + | 'dxMultiView' + | 'dxNumberBox' + | 'dxOverlay' + | 'dxPagination' + | 'dxPivotGrid' + | 'dxPivotGridFieldChooser' + | 'dxPopover' + | 'dxPopup' + | 'dxProgressBar' + | 'dxRadioGroup' + | 'dxRangeSlider' + | 'dxScheduler' + | 'dxScrollable' + | 'dxScrollView' + | 'dxSelectBox' + | 'dxSlider' + | 'dxSortable' + | 'dxSpeechToText' + | 'dxSpeedDialAction' + | 'dxSplitter' + | 'dxStepper' + | 'dxSwitch' + | 'dxTabPanel' + | 'dxTabs' + | 'dxTagBox' + | 'dxTextArea' + | 'dxTextBox' + | 'dxTileView' + | 'dxToast' + | 'dxToolbar' + | 'dxTooltip' + | 'dxTreeList' + | 'dxTreeView' + | 'dxValidationSummary' + | 'dxValidator' + | 'dxGantt'; + +export interface WidgetOptions { + dxDataGrid: DataGridProperties; + dxFilterBuilder: FilterBuilderProperties; + dxGantt: GanttProperties; + // TODO: write other widgets +} diff --git a/e2e/playwright/package.json b/e2e/playwright/package.json new file mode 100644 index 000000000000..810adaae49d9 --- /dev/null +++ b/e2e/playwright/package.json @@ -0,0 +1,24 @@ +{ + "name": "devextreme-playwright-tests", + "version": "26.2.0", + "scripts": { + "test": "node ./runner.mjs", + "lint": "eslint" + }, + "devDependencies": { + "@playwright/test": "catalog:", + "@types/jquery": "catalog:", + "devextreme": "workspace:*", + "eslint": "catalog:", + "@eslint/js": "catalog:", + "@stylistic/eslint-plugin": "catalog:", + "@typescript-eslint/eslint-plugin": "catalog:", + "@typescript-eslint/parser": "catalog:", + "eslint-config-devextreme": "catalog:", + "eslint-plugin-import": "catalog:", + "eslint-plugin-no-only-tests": "catalog:", + "typescript": "5.8.3", + "eslint-migration-utils": "workspace:*", + "@types/node": "catalog:tools" + } +} diff --git a/e2e/playwright/playwright.config.ts b/e2e/playwright/playwright.config.ts new file mode 100644 index 000000000000..00f9c7c3bc1e --- /dev/null +++ b/e2e/playwright/playwright.config.ts @@ -0,0 +1,73 @@ +import { defineConfig } from '@playwright/test'; +import type { TestOptions } from './fixtures'; +import { + DEFAULT_BROWSER_SIZE, DEFAULT_THEME, SERVER_PORT, TEST_PAGE_URL, +} from './helpers/const'; + +const baseURL = `http://localhost:${SERVER_PORT}`; +const [width, height] = DEFAULT_BROWSER_SIZE; + +// The pixel budget is asymmetric on purpose: CI is the source of truth and the etalons are +// generated there, a local run only says whether the change is in the right ballpark. +const screenshotBudget = process.env.CI + ? { maxDiffPixelRatio: 0.001, threshold: 0.1 } + : { maxDiffPixelRatio: 0.05, threshold: 0.2 }; + +export default defineConfig({ + testDir: './tests', + snapshotPathTemplate: '{testDir}/{testFileDir}/etalons/{arg}{ext}', + outputDir: './artifacts/test-results', + fullyParallel: true, + forbidOnly: !!process.env.CI, + workers: process.env.CI ? 8 : 4, + reporter: process.env.CI + ? [['list'], ['html', { open: 'never', outputFolder: './artifacts/playwright-report' }]] + : [['list']], + timeout: 60000, + expect: { + timeout: 5000, + toHaveScreenshot: screenshotBudget, + }, + use: { + theme: process.env.THEME ?? DEFAULT_THEME, + // The TestCafe run changes the machine timezone on the CI agent; the browser context + // takes it per run instead, so the same agent can serve any timezone job. + timezoneId: process.env.TIMEZONE ?? 'GMT', + baseURL, + channel: 'chrome', + headless: true, + viewport: { width, height }, + launchOptions: { + // The same flags the TestCafe runner passes as the "chrome:devextreme-shr2" alias: + // they pin the rendering pipeline so screenshots stay comparable between runs. + args: [ + '--no-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--disable-partial-raster', + '--disable-skia-runtime-opts', + '--run-all-compositor-stages-before-draw', + '--disable-new-content-rendering-timeout', + '--disable-threaded-animation', + '--disable-threaded-scrolling', + '--disable-checker-imaging', + '--disable-image-animation-resync', + '--use-gl=swiftshader', + '--disable-features=PaintHolding', + '--js-flags=--random-seed=2147483647', + '--font-render-hinting=none', + '--disable-font-subpixel-positioning', + ], + // Playwright hides scrollbars in headless by default, which changes the page layout. + ignoreDefaultArgs: ['--hide-scrollbars'], + }, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: { + command: `node ./serve.mjs --port=${SERVER_PORT}`, + url: `${baseURL}${TEST_PAGE_URL}`, + reuseExistingServer: !process.env.CI, + timeout: 30000, + }, +}); diff --git a/e2e/playwright/project.json b/e2e/playwright/project.json new file mode 100644 index 000000000000..8eb0a9e08b1a --- /dev/null +++ b/e2e/playwright/project.json @@ -0,0 +1,34 @@ +{ + "name": "devextreme-playwright-tests", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/playwright", + "projectType": "application", + "targets": { + "lint": { + "executor": "nx:run-script", + "options": { + "script": "lint" + }, + "inputs": [ + "{projectRoot}/**/*.mjs", + "{projectRoot}/**/*.ts" + ] + }, + "test": { + "executor": "nx:run-script", + "options": { + "script": "test" + }, + "inputs": [ + "^default", + "{projectRoot}/helpers/**/*", + "{projectRoot}/models/**/*", + "{projectRoot}/tests/**/*", + "{projectRoot}/playwright.config.ts", + "{projectRoot}/runner.mjs", + "{projectRoot}/tsconfig.json" + ] + } + }, + "tags": [] +} diff --git a/e2e/playwright/runner.mjs b/e2e/playwright/runner.mjs new file mode 100644 index 000000000000..078da053d01e --- /dev/null +++ b/e2e/playwright/runner.mjs @@ -0,0 +1,63 @@ +/* eslint-disable spellcheck/spell-checker */ +import { spawnSync } from 'node:child_process'; +import process from 'node:process'; +import { parseArgs } from 'node:util'; + +const { values, positionals } = parseArgs({ + args: process.argv.slice(2), + allowPositionals: true, + strict: false, + options: { + componentFolder: { type: 'string', default: '' }, + indices: { type: 'string', default: '' }, + theme: { type: 'string', default: 'fluent.blue.light' }, + concurrency: { type: 'string', default: '' }, + test: { type: 'string', default: '' }, + updateSnapshots: { type: 'boolean', default: false }, + }, +}); + +// The matrix already covers every component folder, but the tests are migrated squad by squad, +// so a job whose folder is still TestCafe-only has nothing to run and must not fail. +const args = ['--pass-with-no-tests']; + +if(values.componentFolder) { + args.push(`tests/${values.componentFolder}/`); +} + +if(values.indices) { + const [current, total] = values.indices.split(/[_/\\]|of/i); + + if(!current || !total) { + console.error(`❌ Invalid --indices: ${values.indices}. Pass it as "current/total", for example --indices 1/4.`); + process.exit(1); + } + + args.push(`--shard=${current}/${total}`); +} + +if(values.concurrency) { + args.push(`--workers=${values.concurrency}`); +} + +if(values.test) { + args.push('--grep', values.test); +} + +if(values.updateSnapshots) { + args.push('--update-snapshots'); +} + +args.push(...positionals); + +const { status } = spawnSync( + 'playwright', + ['test', ...args], + { + stdio: 'inherit', + shell: process.platform === 'win32', + env: { ...process.env, THEME: values.theme }, + }, +); + +process.exit(status ?? 1); diff --git a/e2e/playwright/serve.mjs b/e2e/playwright/serve.mjs new file mode 100644 index 000000000000..daa16055c056 --- /dev/null +++ b/e2e/playwright/serve.mjs @@ -0,0 +1,61 @@ +import fs from 'node:fs'; +import http from 'node:http'; +import path from 'node:path'; +import process from 'node:process'; +import { parseArgs } from 'node:util'; + +const MIME_TYPES = { + '.css': 'text/css', + '.html': 'text/html', + '.js': 'text/javascript', + '.json': 'application/json', + '.mjs': 'text/javascript', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ttf': 'font/ttf', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +}; + +const { values } = parseArgs({ + options: { + port: { type: 'string' }, + }, +}); + +const port = Number(values.port); + +if(!Number.isInteger(port) || port <= 0) { + console.error(`❌ Invalid port: ${values.port}. Pass a positive integer, for example --port=8080.`); + process.exit(1); +} + +// The test page pulls the built library and the themes straight out of the repository, +// so the whole repository is the document root. +const root = path.resolve(import.meta.dirname, '../..'); +const bundlePath = path.join(root, 'packages/devextreme/artifacts/js/dx.all.js'); + +if(!fs.existsSync(bundlePath)) { + console.error(`❌ DevExtreme build is not found at ${bundlePath}. Run "pnpm exec nx build devextreme -c testing" first.`); + process.exit(1); +} + +const server = http.createServer((request, response) => { + const { pathname } = new URL(request.url, `http://localhost:${port}`); + const filePath = path.join(root, decodeURIComponent(pathname)); + + if(!filePath.startsWith(root) || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + response.writeHead(404).end('Not found'); + return; + } + + response.writeHead(200, { + 'Content-Type': MIME_TYPES[path.extname(filePath)] ?? 'application/octet-stream', + 'Cache-Control': 'no-store', + }); + fs.createReadStream(filePath).pipe(response); +}); + +server.listen(port, () => { + console.log(`✅ Test pages are served at http://localhost:${port}`); +}); diff --git a/e2e/playwright/tests/container.html b/e2e/playwright/tests/container.html new file mode 100644 index 000000000000..32c6aa58fa88 --- /dev/null +++ b/e2e/playwright/tests/container.html @@ -0,0 +1,34 @@ + + + + Playwright Tests Container + + + + + + + + + + + + + + + + + +
+

Test header

+ +
+
+
+
+
+ + diff --git a/e2e/playwright/tests/editors/checkBox/common.spec.ts b/e2e/playwright/tests/editors/checkBox/common.spec.ts new file mode 100644 index 000000000000..b34ce4af6178 --- /dev/null +++ b/e2e/playwright/tests/editors/checkBox/common.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '../../../fixtures'; +import { createWidget } from '../../../helpers/createWidget'; +import CheckBox from '../../../models/checkBox'; + +test('CheckBox switches its state on click', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: false }); + + const checkBox = new CheckBox(page, '#container'); + + expect(await checkBox.isChecked()).toBe(false); + + await checkBox.element.click(); + + expect(await checkBox.isChecked()).toBe(true); + expect(await checkBox.option('value')).toBe(true); +}); + +test('CheckBox renders the indeterminate state', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: null }); + + const checkBox = new CheckBox(page, '#container'); + + expect(await checkBox.isIndeterminate()).toBe(true); +}); diff --git a/e2e/playwright/tests/navigation/button/common.spec.ts b/e2e/playwright/tests/navigation/button/common.spec.ts new file mode 100644 index 000000000000..60af235c7580 --- /dev/null +++ b/e2e/playwright/tests/navigation/button/common.spec.ts @@ -0,0 +1,71 @@ +import type { ButtonType } from 'devextreme/common'; +import { expect, test } from '../../../fixtures'; +import { createWidget } from '../../../helpers/createWidget'; +import { + addCaptionTo, + appendElementTo, + setAttribute, + setClassAttribute, +} from '../../../helpers/domUtils'; +import { testScreenshot } from '../../../helpers/screenshots'; +import Button from '../../../models/button'; + +const STATES = ['default', 'focused', 'hover', 'active', 'selected', 'disabled']; + +const BUTTONS = [ + ...['danger', 'default', 'normal', 'success'].map((type: ButtonType) => ({ + type, + text: `${type[0].toUpperCase()}${type.slice(1)}`, + })), + { icon: 'find', text: 'Find' }, + { icon: 'find' }, +]; + +['text', 'outlined', 'contained'].forEach((stylingMode) => { + const testName = `Buttons, stylingMode=${stylingMode}`; + + test(testName, async ({ page }) => { + await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); + await setAttribute(page, '#container', 'style', 'width: fit-content; padding: 8px;'); + + for (const state of STATES) { + await appendElementTo(page, '#container', 'div', `mode${state}`); + await setAttribute(page, `#mode${state}`, 'style', 'display: flex; gap: 8px; margin-bottom: 16px;'); + await addCaptionTo(page, `#mode${state}`, state); + + for (const [index] of BUTTONS.entries()) { + await appendElementTo(page, `#mode${state}`, 'div', `button-${state}-${index}`); + } + + for (const [index, config] of BUTTONS.entries()) { + await createWidget(page, 'dxButton', { + ...config, + stylingMode, + disabled: state === 'disabled', + }, `#button-${state}-${index}`); + } + + if (state !== 'default' && state !== 'disabled') { + for (const [index] of BUTTONS.entries()) { + await setClassAttribute(page, `#button-${state}-${index}`, `dx-state-${state}`); + } + } + } + + await testScreenshot(page, `${testName}.png`); + }); +}); + +test('Button reports its text, selected and disabled state', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Find', icon: 'find' }); + + const button = new Button(page, '#container'); + + await expect(button.text).toHaveText('Find'); + expect(await button.isSelected()).toBe(false); + expect(await button.isDisabled()).toBe(false); + + await button.option('disabled', true); + + expect(await button.isDisabled()).toBe(true); +}); diff --git a/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=contained (fluent.blue.light).png b/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=contained (fluent.blue.light).png new file mode 100644 index 0000000000000000000000000000000000000000..ce153a4cb49496b3c485784f0ae60312c72cd297 GIT binary patch literal 36994 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 b#0LWARw_uY<#sJQ8-R_agGHs8_lvKze literal 0 HcmV?d00001 diff --git a/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=outlined (fluent.blue.light).png b/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=outlined (fluent.blue.light).png new file mode 100644 index 0000000000000000000000000000000000000000..2ee920bbf97b063fa99e5412e3e25b8eac21d229 GIT binary patch literal 37094 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 v|99i|zMw%szP@N52 Date: Thu, 27 Aug 2026 13:37:51 -0300 Subject: [PATCH 12/18] Fall back to the default theme and timezone when the matrix leaves them empty --- e2e/playwright/helpers/const.ts | 5 +++++ e2e/playwright/helpers/themeUtils.ts | 4 ++-- e2e/playwright/playwright.config.ts | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/e2e/playwright/helpers/const.ts b/e2e/playwright/helpers/const.ts index 7dafa3374eca..be1b7a492d81 100644 --- a/e2e/playwright/helpers/const.ts +++ b/e2e/playwright/helpers/const.ts @@ -7,3 +7,8 @@ export const DEFAULT_SELECTOR = '#container'; export const SERVER_PORT = 8080; export const TEST_PAGE_URL = '/e2e/playwright/tests/container.html'; + +// A matrix input that is not set for the job arrives as an empty string, not as undefined. +export const readEnv = (value: string | undefined, fallback: string): string => ( + value === undefined || value === '' ? fallback : value +); diff --git a/e2e/playwright/helpers/themeUtils.ts b/e2e/playwright/helpers/themeUtils.ts index 143c2d98f2ef..6eb3002ee79a 100644 --- a/e2e/playwright/helpers/themeUtils.ts +++ b/e2e/playwright/helpers/themeUtils.ts @@ -1,6 +1,6 @@ -import { DEFAULT_THEME } from './const'; +import { DEFAULT_THEME, readEnv } from './const'; -export const getFullThemeName = (): string => process.env.THEME ?? DEFAULT_THEME; +export const getFullThemeName = (): string => readEnv(process.env.THEME, DEFAULT_THEME); export const getThemeName = (): string => getFullThemeName().split('.')[0]; diff --git a/e2e/playwright/playwright.config.ts b/e2e/playwright/playwright.config.ts index 00f9c7c3bc1e..6c5034a2eef1 100644 --- a/e2e/playwright/playwright.config.ts +++ b/e2e/playwright/playwright.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from '@playwright/test'; import type { TestOptions } from './fixtures'; import { - DEFAULT_BROWSER_SIZE, DEFAULT_THEME, SERVER_PORT, TEST_PAGE_URL, + DEFAULT_BROWSER_SIZE, DEFAULT_THEME, readEnv, SERVER_PORT, TEST_PAGE_URL, } from './helpers/const'; const baseURL = `http://localhost:${SERVER_PORT}`; @@ -29,10 +29,10 @@ export default defineConfig({ toHaveScreenshot: screenshotBudget, }, use: { - theme: process.env.THEME ?? DEFAULT_THEME, + theme: readEnv(process.env.THEME, DEFAULT_THEME), // The TestCafe run changes the machine timezone on the CI agent; the browser context // takes it per run instead, so the same agent can serve any timezone job. - timezoneId: process.env.TIMEZONE ?? 'GMT', + timezoneId: readEnv(process.env.TIMEZONE, 'GMT'), baseURL, channel: 'chrome', headless: true, From 2c842402820ef125a7bce95f3451cac55fcf2ec9 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 13:39:35 -0300 Subject: [PATCH 13/18] Lint the new package in the lint workflow and keep its e2e run out of the default one --- .github/workflows/default_workflow.yml | 1 + .github/workflows/lint.yml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/default_workflow.yml b/.github/workflows/default_workflow.yml index c49d07553117..0c0581db5a7e 100644 --- a/.github/workflows/default_workflow.yml +++ b/.github/workflows/default_workflow.yml @@ -108,6 +108,7 @@ jobs: devextreme-react-storybook devextreme-angular-playground devextreme-testcafe-tests + devextreme-playwright-tests devextreme-demos devextreme-react-playground devextreme-vue-playground diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 786e7ae848fb..525ce9fa82c8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -115,6 +115,12 @@ jobs: DEBUG: eslint:cli-engine run: pnpm exec nx lint + - name: Lint Playwright tests + working-directory: ./e2e/playwright + env: + DEBUG: eslint:cli-engine + run: pnpm exec nx lint + JS: runs-on: devextreme-shr2 timeout-minutes: 60 From c8772f5173105c1fe61168ca865734e79d4132a8 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 13:53:35 -0300 Subject: [PATCH 14/18] Run a theme job only on the tests that opted into that theme --- e2e/playwright/README.md | 12 ++++++++++++ e2e/playwright/runner.mjs | 11 +++++++++-- e2e/playwright/tests/editors/checkBox/common.spec.ts | 7 +++++-- .../tests/navigation/button/common.spec.ts | 5 ++++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/e2e/playwright/README.md b/e2e/playwright/README.md index 35b78a12809b..82282dd62fb0 100644 --- a/e2e/playwright/README.md +++ b/e2e/playwright/README.md @@ -43,6 +43,18 @@ docker/run.sh # verify docker/run.sh --update-snapshots # rewrite the etalons of the tests that ran ``` +## Themes + +A test runs in the default theme unless it says otherwise. The jobs that run the whole suite in +another theme (`generic`, `material`, `material - compact`) take only the tests tagged with that +theme — the same opt-in the TestCafe `meta.themes` gives: + +```ts +test('CheckBox switches its state on click', { tag: ['@generic.light'] }, async ({ page }) => { +``` + +Tag a test when its result does not depend on the theme, or when it has an etalon for that theme. + ## Run in the CI environment The container takes Node from `.node-version` and Chrome from the version the workflow installs; diff --git a/e2e/playwright/runner.mjs b/e2e/playwright/runner.mjs index 078da053d01e..e1a20198bac6 100644 --- a/e2e/playwright/runner.mjs +++ b/e2e/playwright/runner.mjs @@ -3,6 +3,8 @@ import { spawnSync } from 'node:child_process'; import process from 'node:process'; import { parseArgs } from 'node:util'; +const DEFAULT_THEME = 'fluent.blue.light'; + const { values, positionals } = parseArgs({ args: process.argv.slice(2), allowPositionals: true, @@ -10,7 +12,7 @@ const { values, positionals } = parseArgs({ options: { componentFolder: { type: 'string', default: '' }, indices: { type: 'string', default: '' }, - theme: { type: 'string', default: 'fluent.blue.light' }, + theme: { type: 'string' }, concurrency: { type: 'string', default: '' }, test: { type: 'string', default: '' }, updateSnapshots: { type: 'boolean', default: false }, @@ -42,6 +44,11 @@ if(values.concurrency) { if(values.test) { args.push('--grep', values.test); +} else if(values.theme && !values.componentFolder) { + // A job that names a theme instead of a folder runs the whole suite in that theme, so it + // takes only the tests that opted into it with a "@" tag — the etalons of the rest + // are recorded in the default theme. + args.push('--grep', `@${values.theme}`); } if(values.updateSnapshots) { @@ -56,7 +63,7 @@ const { status } = spawnSync( { stdio: 'inherit', shell: process.platform === 'win32', - env: { ...process.env, THEME: values.theme }, + env: { ...process.env, THEME: values.theme ?? DEFAULT_THEME }, }, ); diff --git a/e2e/playwright/tests/editors/checkBox/common.spec.ts b/e2e/playwright/tests/editors/checkBox/common.spec.ts index b34ce4af6178..e49f1ec699ab 100644 --- a/e2e/playwright/tests/editors/checkBox/common.spec.ts +++ b/e2e/playwright/tests/editors/checkBox/common.spec.ts @@ -2,7 +2,10 @@ import { expect, test } from '../../../fixtures'; import { createWidget } from '../../../helpers/createWidget'; import CheckBox from '../../../models/checkBox'; -test('CheckBox switches its state on click', async ({ page }) => { +// The tags list the themes this test also runs in; a test without them runs in the default one. +const THEMES = { tag: ['@generic.light', '@material.blue.light', '@material.blue.light.compact'] }; + +test('CheckBox switches its state on click', THEMES, async ({ page }) => { await createWidget(page, 'dxCheckBox', { value: false }); const checkBox = new CheckBox(page, '#container'); @@ -15,7 +18,7 @@ test('CheckBox switches its state on click', async ({ page }) => { expect(await checkBox.option('value')).toBe(true); }); -test('CheckBox renders the indeterminate state', async ({ page }) => { +test('CheckBox renders the indeterminate state', THEMES, async ({ page }) => { await createWidget(page, 'dxCheckBox', { value: null }); const checkBox = new CheckBox(page, '#container'); diff --git a/e2e/playwright/tests/navigation/button/common.spec.ts b/e2e/playwright/tests/navigation/button/common.spec.ts index 60af235c7580..db45035d09e0 100644 --- a/e2e/playwright/tests/navigation/button/common.spec.ts +++ b/e2e/playwright/tests/navigation/button/common.spec.ts @@ -10,6 +10,9 @@ import { import { testScreenshot } from '../../../helpers/screenshots'; import Button from '../../../models/button'; +// The tags list the themes this test also runs in; a test without them runs in the default one. +const THEMES = { tag: ['@generic.light', '@material.blue.light', '@material.blue.light.compact'] }; + const STATES = ['default', 'focused', 'hover', 'active', 'selected', 'disabled']; const BUTTONS = [ @@ -56,7 +59,7 @@ const BUTTONS = [ }); }); -test('Button reports its text, selected and disabled state', async ({ page }) => { +test('Button reports its text, selected and disabled state', THEMES, async ({ page }) => { await createWidget(page, 'dxButton', { text: 'Find', icon: 'find' }); const button = new Button(page, '#container'); From 2ab615bdbc575386d1e00c713d4fa6363a22b73d Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 14:31:53 -0300 Subject: [PATCH 15/18] Match the TestCafe semantics in the runner flags, the request mocks and the screenshot target --- e2e/playwright/README.md | 8 ++- e2e/playwright/fixtures.ts | 14 ++-- e2e/playwright/helpers/apiMock.ts | 23 ++++--- e2e/playwright/helpers/screenshots.ts | 8 +-- e2e/playwright/helpers/testPageUtils.ts | 42 ------------ e2e/playwright/models/internal/widget.ts | 6 +- e2e/playwright/runner.mjs | 66 ++++++++++++------- e2e/playwright/serve.mjs | 7 +- .../tests/navigation/button/common.spec.ts | 2 +- 9 files changed, 72 insertions(+), 104 deletions(-) diff --git a/e2e/playwright/README.md b/e2e/playwright/README.md index 82282dd62fb0..a657f2ef5733 100644 --- a/e2e/playwright/README.md +++ b/e2e/playwright/README.md @@ -24,11 +24,13 @@ pnpm run test --test "Buttons, stylingMode=text" # one test by name The runner takes the flags the TestCafe one takes (`--componentFolder`, `--indices`, `--theme`, `--concurrency`, `--test`) and passes everything else straight to `playwright test`, so -`pnpm run test --headed --debug` works too. The static server for the test page starts on its own. +`pnpm run test --headed --repeat-each 3` works too. `--test` matches the name as a whole word, not +as a regular expression. The static server for the test page starts on its own. ## Screenshots -Etalons live in `etalons/` next to the test and carry the theme in the file name — +`testScreenshot` shoots the whole viewport unless the call names an `element`, the way the TestCafe +comparer did. Etalons live in `etalons/` next to the test and carry the theme in the file name — `Buttons, stylingMode=text (fluent.blue.light).png` — the same convention the TestCafe run uses. The pixel budget is asymmetric on purpose: **CI is the source of truth** (`maxDiffPixelRatio` @@ -95,7 +97,7 @@ test('Button reports its text', async ({ page }) => { const button = new Button(page, '#container .dx-button'); await expect(button.text).toHaveText('Find'); - await testScreenshot(page, 'Button with text.png'); + await testScreenshot(page, 'Button with text.png', { element: '#container' }); }); ``` diff --git a/e2e/playwright/fixtures.ts b/e2e/playwright/fixtures.ts index 42e03d50e0db..715d8fab0e83 100644 --- a/e2e/playwright/fixtures.ts +++ b/e2e/playwright/fixtures.ts @@ -1,19 +1,15 @@ import { test as base } from '@playwright/test'; import { DEFAULT_BROWSER_SIZE, DEFAULT_THEME } from './helpers/const'; -import { - changeTheme, - clearTestPage, - openTestPage, - resetPageState, -} from './helpers/testPageUtils'; +import { changeTheme, openTestPage, resetPageState } from './helpers/testPageUtils'; export interface TestOptions { theme: string; browserSize: [number, number]; } -// Overriding "page" gives every test the prepared container page, the way the TestCafe runner -// did it from its global test hooks. +// Overriding "page" gives every test the prepared container page, the way the TestCafe runner did +// it from its global test hooks. Nothing needs cleaning up afterwards: every test gets its own +// context, and the widgets of the previous one die with its page. export const test = base.extend({ theme: [DEFAULT_THEME, { option: true }], browserSize: [DEFAULT_BROWSER_SIZE, { option: true }], @@ -27,8 +23,6 @@ export const test = base.extend({ await resetPageState(page); await use(page); - - await clearTestPage(page); }, }); diff --git a/e2e/playwright/helpers/apiMock.ts b/e2e/playwright/helpers/apiMock.ts index 85cef2471226..8e367461c9e8 100644 --- a/e2e/playwright/helpers/apiMock.ts +++ b/e2e/playwright/helpers/apiMock.ts @@ -7,17 +7,20 @@ export interface MockedRequest { headers?: Record; } -// Replaces the TestCafe RequestMock: the rules are matched in the order they are declared, -// so a narrower rule has to come before the wider one, just as it did there. +// Replaces the TestCafe RequestMock: the rules are matched in the order they are declared, so a +// narrower rule has to come before the wider one, just as it did there. Registration runs backwards +// because "page.route" gives the last registered handler priority. export async function mockApi(page: Page, requests: MockedRequest[]): Promise { - await Promise.all(requests.map(({ + for (const { url, body, status = 200, headers, - }) => page.route(url, async (route) => { - await route.fulfill({ - status, - contentType: 'application/json', - headers: { 'access-control-allow-origin': '*', ...headers }, - body: JSON.stringify(body), + } of [...requests].reverse()) { + await page.route(url, async (route) => { + await route.fulfill({ + status, + contentType: 'application/json', + headers: { 'access-control-allow-origin': '*', ...headers }, + body: JSON.stringify(body), + }); }); - }))); + } } diff --git a/e2e/playwright/helpers/screenshots.ts b/e2e/playwright/helpers/screenshots.ts index 01971f99f69d..1ca4fd6ea844 100644 --- a/e2e/playwright/helpers/screenshots.ts +++ b/e2e/playwright/helpers/screenshots.ts @@ -1,6 +1,5 @@ import type { Locator, Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import { DEFAULT_SELECTOR } from './const'; import { getFullThemeName, getThemePostfix } from './themeUtils'; import { changeTheme } from './testPageUtils'; @@ -22,19 +21,16 @@ const getScreenshotName = (baseName: string, theme?: string): string => { : `${baseName}${themePostfix}.png`; }; +// No element means the whole viewport, the way the TestCafe comparer read a missing element. const resolveTarget = ( page: Page, element: Locator | string | null | undefined, ): Locator | Page => { - if (element === null) { - return page; - } - if (typeof element === 'string') { return page.locator(element); } - return element ?? page.locator(DEFAULT_SELECTOR); + return element ?? page; }; export async function testScreenshot( diff --git a/e2e/playwright/helpers/testPageUtils.ts b/e2e/playwright/helpers/testPageUtils.ts index 3fe744e79eaf..a944a3671625 100644 --- a/e2e/playwright/helpers/testPageUtils.ts +++ b/e2e/playwright/helpers/testPageUtils.ts @@ -27,48 +27,6 @@ export async function getCurrentTheme(page: Page): Promise { return theme ?? DEFAULT_THEME; } -export async function clearTestPage(page: Page): Promise { - await page.evaluate(() => { - const widgetSelector = '.dx-widget'; - const $elements = $(widgetSelector) - .filter((_, element) => $(element).parents(widgetSelector).length === 0); - - $elements.each((_, element) => { - const $widgetElement = $(element); - const widgetNames = $widgetElement.data().dxComponents; - - widgetNames?.forEach((name) => { - if ($widgetElement.hasClass('dx-widget')) { - ($widgetElement as any)[name]?.('dispose'); - } - }); - $widgetElement.empty(); - }); - - document.getElementById('focusable-start')?.remove(); - document.getElementById('stylesheetRules')?.remove(); - - const body = document.querySelector('body'); - - if (body) { - body.innerHTML = ''; - body.className = 'dx-surface'; - } - - const temp = document.createElement('div'); - - temp.innerHTML = ` -
-

Test header

-
-
-
- `; - - body?.prepend(temp.firstElementChild!); - }); -} - export async function resetPageState(page: Page): Promise { await page.evaluate(() => { if (document.activeElement && document.activeElement !== document.body) { diff --git a/e2e/playwright/models/internal/widget.ts b/e2e/playwright/models/internal/widget.ts index 5248e39ae630..9fe9f20177d8 100644 --- a/e2e/playwright/models/internal/widget.ts +++ b/e2e/playwright/models/internal/widget.ts @@ -47,11 +47,7 @@ export default abstract class Widget { } public async option(option: string | Record, ...value: unknown[]): Promise { - const isSetter = value.length > 0 || typeof option === 'object'; - - return isSetter - ? this.invoke('option', option, ...value) - : this.invoke('option', option); + return this.invoke('option', option, ...value); } public async focus(): Promise { diff --git a/e2e/playwright/runner.mjs b/e2e/playwright/runner.mjs index e1a20198bac6..f42c06f597eb 100644 --- a/e2e/playwright/runner.mjs +++ b/e2e/playwright/runner.mjs @@ -1,27 +1,44 @@ -/* eslint-disable spellcheck/spell-checker */ + import { spawnSync } from 'node:child_process'; import process from 'node:process'; -import { parseArgs } from 'node:util'; const DEFAULT_THEME = 'fluent.blue.light'; -const { values, positionals } = parseArgs({ - args: process.argv.slice(2), - allowPositionals: true, - strict: false, - options: { - componentFolder: { type: 'string', default: '' }, - indices: { type: 'string', default: '' }, - theme: { type: 'string' }, - concurrency: { type: 'string', default: '' }, - test: { type: 'string', default: '' }, - updateSnapshots: { type: 'boolean', default: false }, - }, -}); +const OPTIONS = ['componentFolder', 'indices', 'theme', 'concurrency', 'test']; + +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +// Everything this runner does not know about goes to "playwright test" untouched, so +// "pnpm run test --headed --repeat-each 3" behaves exactly as the Playwright CLI would. +function parseArgs(argv) { + const values = {}; + const rest = []; + + for(let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + const [flag, inlineValue] = arg.startsWith('--') ? arg.slice(2).split(/=(.*)/s) : []; + + if(OPTIONS.includes(flag)) { + if(inlineValue === undefined) { + i += 1; + values[flag] = argv[i]; + } else { + values[flag] = inlineValue; + } + } else { + rest.push(arg); + } + } + + return { values, rest }; +} + +const { values, rest } = parseArgs(process.argv.slice(2)); // The matrix already covers every component folder, but the tests are migrated squad by squad, -// so a job whose folder is still TestCafe-only has nothing to run and must not fail. -const args = ['--pass-with-no-tests']; +// so a job whose folder is still TestCafe-only has nothing to run and must not fail. A run asking +// for one test by name is the opposite case: matching nothing there means the name is wrong. +const args = values.test ? [] : ['--pass-with-no-tests']; if(values.componentFolder) { args.push(`tests/${values.componentFolder}/`); @@ -43,19 +60,18 @@ if(values.concurrency) { } if(values.test) { - args.push('--grep', values.test); + // Playwright greps the title with the tags appended, so the name can only be bounded by + // whitespace, not anchored. Escaping matters: test names here carry ticket numbers in braces. + args.push('--grep', `(^|\\s)${escapeRegExp(values.test)}(\\s|$)`); } else if(values.theme && !values.componentFolder) { // A job that names a theme instead of a folder runs the whole suite in that theme, so it // takes only the tests that opted into it with a "@" tag — the etalons of the rest - // are recorded in the default theme. - args.push('--grep', `@${values.theme}`); -} - -if(values.updateSnapshots) { - args.push('--update-snapshots'); + // are recorded in the default theme. The tag ends at a space, otherwise "@material.blue.light" + // would also pick up the tests tagged "@material.blue.light.compact". + args.push('--grep', `@${escapeRegExp(values.theme)}(\\s|$)`); } -args.push(...positionals); +args.push(...rest); const { status } = spawnSync( 'playwright', diff --git a/e2e/playwright/serve.mjs b/e2e/playwright/serve.mjs index daa16055c056..57fe135789c1 100644 --- a/e2e/playwright/serve.mjs +++ b/e2e/playwright/serve.mjs @@ -43,8 +43,11 @@ if(!fs.existsSync(bundlePath)) { const server = http.createServer((request, response) => { const { pathname } = new URL(request.url, `http://localhost:${port}`); const filePath = path.join(root, decodeURIComponent(pathname)); + // A prefix check would also accept a sibling directory whose name starts with the root one. + const relative = path.relative(root, filePath); + const isInsideRoot = relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative); - if(!filePath.startsWith(root) || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + if(!isInsideRoot || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { response.writeHead(404).end('Not found'); return; } @@ -56,6 +59,6 @@ const server = http.createServer((request, response) => { fs.createReadStream(filePath).pipe(response); }); -server.listen(port, () => { +server.listen(port, '127.0.0.1', () => { console.log(`✅ Test pages are served at http://localhost:${port}`); }); diff --git a/e2e/playwright/tests/navigation/button/common.spec.ts b/e2e/playwright/tests/navigation/button/common.spec.ts index db45035d09e0..e192ae28a3d0 100644 --- a/e2e/playwright/tests/navigation/button/common.spec.ts +++ b/e2e/playwright/tests/navigation/button/common.spec.ts @@ -55,7 +55,7 @@ const BUTTONS = [ } } - await testScreenshot(page, `${testName}.png`); + await testScreenshot(page, `${testName}.png`, { element: '#container' }); }); }); From 9f0bf21fa4a3a7259483debb6500e56ed46672f3 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 15:35:29 -0300 Subject: [PATCH 16/18] Name the workflow after Playwright, accept the "1 of 4" shard form and stop doubling the compact suffix --- ...testcafe_tests_playwright.yml => playwright_tests.yml} | 0 e2e/playwright/docker/run.sh | 2 +- e2e/playwright/helpers/screenshots.ts | 8 +++++--- e2e/playwright/runner.mjs | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) rename .github/workflows/{testcafe_tests_playwright.yml => playwright_tests.yml} (100%) diff --git a/.github/workflows/testcafe_tests_playwright.yml b/.github/workflows/playwright_tests.yml similarity index 100% rename from .github/workflows/testcafe_tests_playwright.yml rename to .github/workflows/playwright_tests.yml diff --git a/e2e/playwright/docker/run.sh b/e2e/playwright/docker/run.sh index 006ed53f6020..7334c2cf1ab0 100755 --- a/e2e/playwright/docker/run.sh +++ b/e2e/playwright/docker/run.sh @@ -12,7 +12,7 @@ fi REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" NODE_VERSION="$(cat "$REPO_ROOT/.node-version")" -CHROME_VERSION="$(grep -m1 "chrome-version:" "$REPO_ROOT/.github/workflows/testcafe_tests_playwright.yml" | cut -d: -f2 | tr -d " '\"")" +CHROME_VERSION="$(grep -m1 "chrome-version:" "$REPO_ROOT/.github/workflows/playwright_tests.yml" | cut -d: -f2 | tr -d " '\"")" echo "Node $NODE_VERSION, Google Chrome $CHROME_VERSION" diff --git a/e2e/playwright/helpers/screenshots.ts b/e2e/playwright/helpers/screenshots.ts index 1ca4fd6ea844..cf8a2aa3a19b 100644 --- a/e2e/playwright/helpers/screenshots.ts +++ b/e2e/playwright/helpers/screenshots.ts @@ -56,12 +56,14 @@ export async function testScreenshot( await expect(target).toHaveScreenshot([getScreenshotName(screenshotName, theme)]); if (shouldTestInCompact) { - const themeName = theme ?? getFullThemeName(); + // The theme of a "- compact" job already ends with the suffix; appending it twice would ask + // for a theme that does not exist. + const compactTheme = `${(theme ?? getFullThemeName()).replace(/\.compact$/, '')}.compact`; - await changeTheme(page, `${themeName}.compact`); + await changeTheme(page, compactTheme); await compactCallBack?.(); - await expect(target).toHaveScreenshot([getScreenshotName(screenshotName, `${themeName}.compact`)]); + await expect(target).toHaveScreenshot([getScreenshotName(screenshotName, compactTheme)]); } if (theme || shouldTestInCompact) { diff --git a/e2e/playwright/runner.mjs b/e2e/playwright/runner.mjs index f42c06f597eb..57ba4af981d3 100644 --- a/e2e/playwright/runner.mjs +++ b/e2e/playwright/runner.mjs @@ -45,9 +45,10 @@ if(values.componentFolder) { } if(values.indices) { - const [current, total] = values.indices.split(/[_/\\]|of/i); + // "1/4", "1_4" and "1 of 4" all mean the same shard, the way the TestCafe runner read them. + const [current, total] = values.indices.split(/[_/\\]|of/i).map((part) => Number(part)); - if(!current || !total) { + if(!Number.isInteger(current) || !Number.isInteger(total) || current < 1 || total < 1) { console.error(`❌ Invalid --indices: ${values.indices}. Pass it as "current/total", for example --indices 1/4.`); process.exit(1); } From db2db0f8745f9f77e6ad8b3af03c6ef19ad60d23 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 27 Aug 2026 23:04:01 -0300 Subject: [PATCH 17/18] Use PivotGrid as the reference component instead of widgets another squad owns --- e2e/playwright/README.md | 24 +++--- e2e/playwright/models/button.ts | 24 ------ e2e/playwright/models/checkBox.ts | 23 ------ e2e/playwright/models/pivotGrid.ts | 26 ++++++ .../PivotGrid markup (fluent.blue.light).png | Bin 0 -> 11619 bytes .../tests/common/pivotGrid/reference.spec.ts | 42 ++++++++++ .../tests/editors/checkBox/common.spec.ts | 27 ------- .../tests/navigation/button/common.spec.ts | 74 ------------------ ...lingMode=contained (fluent.blue.light).png | Bin 36994 -> 0 bytes ...ylingMode=outlined (fluent.blue.light).png | Bin 37094 -> 0 bytes ..., stylingMode=text (fluent.blue.light).png | Bin 33951 -> 0 bytes 11 files changed, 80 insertions(+), 160 deletions(-) delete mode 100644 e2e/playwright/models/button.ts delete mode 100644 e2e/playwright/models/checkBox.ts create mode 100644 e2e/playwright/models/pivotGrid.ts create mode 100644 e2e/playwright/tests/common/pivotGrid/etalons/PivotGrid markup (fluent.blue.light).png create mode 100644 e2e/playwright/tests/common/pivotGrid/reference.spec.ts delete mode 100644 e2e/playwright/tests/editors/checkBox/common.spec.ts delete mode 100644 e2e/playwright/tests/navigation/button/common.spec.ts delete mode 100644 e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=contained (fluent.blue.light).png delete mode 100644 e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=outlined (fluent.blue.light).png delete mode 100644 e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=text (fluent.blue.light).png diff --git a/e2e/playwright/README.md b/e2e/playwright/README.md index a657f2ef5733..65401290f81b 100644 --- a/e2e/playwright/README.md +++ b/e2e/playwright/README.md @@ -16,10 +16,10 @@ pnpm exec nx build devextreme -c testing cd e2e/playwright pnpm run test # everything, fluent.blue.light -pnpm run test --componentFolder navigation # one component folder +pnpm run test --componentFolder common/pivotGrid # one component folder pnpm run test --theme material.blue.light # another theme pnpm run test --indices 1/4 --concurrency 5 # one shard, five workers -pnpm run test --test "Buttons, stylingMode=text" # one test by name +pnpm run test --test "PivotGrid markup" # one test by name ``` The runner takes the flags the TestCafe one takes (`--componentFolder`, `--indices`, `--theme`, @@ -31,7 +31,7 @@ as a regular expression. The static server for the test page starts on its own. `testScreenshot` shoots the whole viewport unless the call names an `element`, the way the TestCafe comparer did. Etalons live in `etalons/` next to the test and carry the theme in the file name — -`Buttons, stylingMode=text (fluent.blue.light).png` — the same convention the TestCafe run uses. +`PivotGrid markup (fluent.blue.light).png` — the same convention the TestCafe run uses. The pixel budget is asymmetric on purpose: **CI is the source of truth** (`maxDiffPixelRatio` 0.001, `threshold` 0.1) and a local run is a sanity check (0.05 / 0.2). @@ -52,7 +52,7 @@ another theme (`generic`, `material`, `material - compact`) take only the tests theme — the same opt-in the TestCafe `meta.themes` gives: ```ts -test('CheckBox switches its state on click', { tag: ['@generic.light'] }, async ({ page }) => { +test('PivotGrid renders its areas', { tag: ['@generic.light'] }, async ({ page }) => { ``` Tag a test when its result does not depend on the theme, or when it has an etalon for that theme. @@ -73,8 +73,8 @@ its trace viewer will not load there. ## Read a failed CI run ```bash -gh run download -n playwright-report-navigation -D ci-report -gh run download -n playwright-traces-navigation -D ci-traces +gh run download -n playwright-report-common -D ci-report +gh run download -n playwright-traces-common -D ci-traces pnpm exec playwright show-report ci-report pnpm exec playwright show-trace ci-traces//trace.zip --port 0 ``` @@ -89,15 +89,15 @@ failure that only happens on CI can be diagnosed without reproducing it. import { expect, test } from '../../../fixtures'; import { createWidget } from '../../../helpers/createWidget'; import { testScreenshot } from '../../../helpers/screenshots'; -import Button from '../../../models/button'; +import PivotGrid from '../../../models/pivotGrid'; -test('Button reports its text', async ({ page }) => { - await createWidget(page, 'dxButton', { text: 'Find' }); +test('PivotGrid renders its data area', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { dataSource: { store: [] } }); - const button = new Button(page, '#container .dx-button'); + const pivotGrid = new PivotGrid(page, '#container'); - await expect(button.text).toHaveText('Find'); - await testScreenshot(page, 'Button with text.png', { element: '#container' }); + await expect(pivotGrid.dataArea).toBeVisible(); + await testScreenshot(page, 'PivotGrid markup.png', { element: '#container' }); }); ``` diff --git a/e2e/playwright/models/button.ts b/e2e/playwright/models/button.ts deleted file mode 100644 index 81df6f59b1d2..000000000000 --- a/e2e/playwright/models/button.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Locator } from '@playwright/test'; -import type { WidgetName } from './types'; -import Widget from './internal/widget'; - -export const CLASS = { - text: '.dx-button-text', - selected: 'dx-state-selected', - itemSelected: 'dx-item-selected', -}; - -export default class Button extends Widget { - public static className = 'dx-button'; - - public get text(): Locator { - return this.element.locator(CLASS.text); - } - - public isSelected(): Promise { - return this.hasClass(CLASS.selected); - } - - // eslint-disable-next-line class-methods-use-this - public getName(): WidgetName { return 'dxButton'; } -} diff --git a/e2e/playwright/models/checkBox.ts b/e2e/playwright/models/checkBox.ts deleted file mode 100644 index 0ba7c189b705..000000000000 --- a/e2e/playwright/models/checkBox.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { WidgetName } from './types'; -import Widget from './internal/widget'; - -export const CLASS = { - checked: 'dx-checkbox-checked', - indeterminate: 'dx-checkbox-indeterminate', - icon: '.dx-checkbox-icon', -}; - -export default class CheckBox extends Widget { - public static className = 'dx-checkbox'; - - public isChecked(): Promise { - return this.hasClass(CLASS.checked); - } - - public isIndeterminate(): Promise { - return this.hasClass(CLASS.indeterminate); - } - - // eslint-disable-next-line class-methods-use-this - public getName(): WidgetName { return 'dxCheckBox'; } -} diff --git a/e2e/playwright/models/pivotGrid.ts b/e2e/playwright/models/pivotGrid.ts new file mode 100644 index 000000000000..c32e74e9a93e --- /dev/null +++ b/e2e/playwright/models/pivotGrid.ts @@ -0,0 +1,26 @@ +import type { Locator } from '@playwright/test'; +import type { WidgetName } from './types'; +import Widget from './internal/widget'; + +export const CLASS = { + area: '.dx-pivotgrid-area', + dataArea: '.dx-pivotgrid-area-data', + fieldChooserButton: '.dx-pivotgrid-field-chooser-button', +}; + +// The reference model of the package: it shows what a squad writes on top of Widget — a Locator +// per part of the markup and the widget API through option()/invoke(). +export default class PivotGrid extends Widget { + public static className = 'dx-pivotgrid'; + + public get dataArea(): Locator { + return this.element.locator(CLASS.dataArea); + } + + public getAreaCell(area: string, index: number): Locator { + return this.element.locator(`${CLASS.area}-${area} td`).nth(index); + } + + // eslint-disable-next-line class-methods-use-this + public getName(): WidgetName { return 'dxPivotGrid'; } +} diff --git a/e2e/playwright/tests/common/pivotGrid/etalons/PivotGrid markup (fluent.blue.light).png b/e2e/playwright/tests/common/pivotGrid/etalons/PivotGrid markup (fluent.blue.light).png new file mode 100644 index 0000000000000000000000000000000000000000..7c0e1a974479537c85e809e923381e2783796f07 GIT binary patch literal 11619 zcmcJVcT^MIzOYeLL_|cIh=78qfHXs|FVa!Egc3p&q=sGt7*G)q5RfiidJhPM5(uD3 zFQJ9rdngGl)BqRWbIx7se(O8mU3abf{ga(!_RQWhduGq`dwx4uTT_Ml#=RS4WMtH8 zs*1W~WLJKif3LoN`TP^KMShEn>^7O2qWo)E^7HXY7cu^ z=h;O!n;o?iwMi$6WgmZiPDqkiZ`uYSJw7_K-}uOeWfMcIUj$=e>R~bLqVJpmBTi z;%DRI>#&RR4c!&#i?WpRGTTKta^(`uMQJxDW;WS%I2$R0W6+nM)8YR! z>S8k)>E00g=5Bi^JDkoyY%Jv%Fxzc%51MZ+BbQ>7hPZxFrLpAA2}sx-44C9rlWP!u z()`%saZP~D#YH{eHqTvYEz4|W2{*{O_=AjO?Cab(kPCWWA5kylyXx+>Offkb%WXG! z<$U7@t92$hB|V67Z!&@S{v@LH5VedpwD&M&YQ*#rDi#{H_L{wRW@#>u6#Z_paI7;D zVPyPpqCM5PlX=LyM<}U51Y5Nov5X!=AE)A`8_V}Y4K`y8tI9Uj+6i)p`Q&p`p%#Zd z__+Gb#WOF`ujdGbu$O8WZW&(=F<&%|j{aQeX}^18dy~P|8!I^EobZb?paH7mOsB20U0?;iq^QSwNFfgqQa*pdh z+5Ql(E3+G6`!PNxi_Q9;Eymp#iW)HBH=dv6kr71{TBB*TLMBr=J}@c6igFeYqNqq&Uw=GNP317D! z?d-P|JvQd(+o`&P1|rwNE?j|fL4<$Z}r}IiEW>C{3H)n z=a$s-e3W8jPAb^N?Szrff^)CchTB2b{@h}d{N`2f4)c)LQq$SR)wjfjnh(vT(pOb& z9wc=555*|Ms-HFJ+pIdO4KrxU6EJvA-6@Ci^2&xW;Li_k=k>$JIHpA}nY8EI zv=`>?P5w#@bUhtU|FS;zv$mS#rdBp|f=`oN`uzQ>rvu zw8-t;X@gh(Iv+(ROW0AzJ8{t`00)a|r=LYdT85oInVD!S58N_uL127F)risj3@%C8 z;dt>B)zyk|O!)P)esmXrR)`m~i8xdg`3#f<`Ny&b6G$6(5$Z=w`iiz7C z`2?e>+Yi%GyQP=LT9Xk|>F0Y$P{>W54-0F08Bymz+z4Ba*X`LoJ6UVkPgbW%Es@lc znYVZsn4`>;J!s~*ooUY8o&t78{ZV(|ZMeR%7ZZuyUCds+slOvOy!Xs1x!!GKEPEi| z*i*kryBFmmexKGHzn!PsKL|Le!gQEB2#X#24+VRynP{|rU0%s9IrW&$TdO>4*^ZXU z`YXv=V)Xg^Huzw5%pgT}cq4plX-zq!q})D@*Ag;-Jj3%ityM|dN-o{$5Fl-g#k(8P z`YC7-z^NbJuuS*|g}t@qZ1sqcKG2vV${X11zQh;PZmS9+@({uRNs0SD87_O4qm9^X z|A61CO3AhAR0EldTL(HH7rtP;92eobChAcxLe?4#rHo%8k@?WK+=!?6Ck2{nCF{)3 zV-0ilGi$O{5aF*iFoDf`L3BTcXma%H0g)fk`q)=XTFML0-OWaZE|=! zInQ?I;zeeH)%e=ATua>cjj-&5FM0Tt4KJ@|(}uH&u8dh^&$A%+Z(@F2k2@%B`1|Cl ziq<+a={i$MIiz=MXou;({62Hvtt%DzrP z`0Y|g&3Pb9`ZerL>1^@-rFr7f6a9ZVsWDxVuFHF*(a6otIU_iCqa6mI2hW8N)MS^wFQZGI@3o+Xsmo3x+GjG0UA}i)Vq)<5Dvxq(@yqGV@Ug-~%5HZP)Ndi5QX?UjbJCq_*7HRxQ*tn9^d z>F3Ap(Iu3BvfkbgH%?t8tgt112OS5Adr*zE9FeDHCjFQVQdt3j^+nP;o3Zg@9#2rsirs0p2ewyOjLu|W>fSh?p2(
W(=MhC5^qxy;G8WbckA9_fDfZhZW(IfoF2ZSA3XPuRn38Y*m! zix|Z19*&j*_EmAeHiqot>xHFQPHV0+flC>U-5l%!*pjIeLz1Z1Vfg#~oC7qLh68C% zJil)W1ZcD~r+KZY>Jb9;oPR~Ob6E;)ZbzErk5ZZy)LPk!u&d$!2)|UeotqcsP8c(3 z++E*6r$pwdzo3~+1o+}Bobw7eGXjRf`xIOs&a3r2SM#X3ul%&M^2m%oh><#|lZY0u z7rgA6n7GK>{?d)NPb+nR)d@P zfcP2~Mhz*FG}&tZ1mJ4t<{nb&yWxe>-!X@?hZ#i|9cj^oXOEyMqIY?Vmy7itO3Mum zKNn=TPxt)e$~ejQ>{B%F4&BjiW3$jku%xxC?~e|UF0r}eCPMbC`<-^RegK#WMyZwx zkF8(mU2dJ?$;YKt7utLTou(d@HHpx1h;YiE&i0kj_hqcX`$~G&@Zbtd!<+L@^peU% zcsVT*%M-n?w5d$a>U@NE-*La1jEsNw7=@uWCQwbC-{`54fbEQ`eWP+hq=m+S+X;h_ zDW%sC*Q=z1I-lRD&&-|I<^i=cY|?gLRoKhswCDR>3u?8Bq}`>mD+*s0*xT*;y6CzP zx}Ft4dB*rDfm@{#;(#?D?>QD0%qfzH@k8<_I@dlA9Xkm~=o64MP=6WyzW8c`BSUx! z!CeHKT?G`5If&VrYW0nxhm1|}-GdhE$(PGMDX%EZk2xl6k|bQUQW6j+n@*=eIp6g5 zmLBs-x4$si4J9V-4wbh~k#;^I)n`};j(@_vYqj9P&ncex|dGplAtwEdm_JbILy z<2|mmYz6Z@xG&Kjz1YcKlm~|z6n_~7Pnm*KCbY3m)#|q0-XxXW!_`g&5mv(KPxCtP#i_(Z$h1)^EByV-7pB0jpK)?%5o36Me%7N|!`8Gt zkL#`Bb9+at72yR-`w{I{(|1Zg8sMiJ1RnfB`|MZRjdGABW&D22LJwH~Q(%=F?Rfi2 zX{7Jg{@-y0nd!}c6G(jeKyktMqVkR87iH4_Fb?^5yVsRfS3kWtAX(Bh?>=Fg>Gs?d z+6Po=NBcoTXSS3&gxSLkTRwoJCo6Rx$)LQb_`ltSe??NF;xh>^4Q!`;&Bvj~#yLa+ z60^_H@Uvw0IP9X?57fK6mjtAN#HC7m$JaN58lZq>m7a*Z2`TYGJbE!IG;wCGJ#NU@ z7VgVrrCzul$m_+1y|Mkr>$_5LhC|1?O8arBV;vY!_YP4Rm@5#s;R7G9v@dDEL773d zy5DDiGEOJ)79V%;KgR_vx0~FPf-n|O3{SO{h|4jX7Mr;GjwI%raxarY1n@=J70HnM zjhz~4xDxKsv`W|I)K6uWes79vdo8_rZtSgB81Kg;wX6%nazy(~EB&&OKQgQ=iyMT< zPmiMP*2w$IrX^%x`!s7W! z5y;-`^gjW06d*dJqGMDfz2HahHx4(aQ0XjUN0>+O0b?maN9AQQ%+OwN2BME8b5bRy z@Pglle!eug$I==*KM8~yM6MLtGjq<(O7!Il00+aHQ&ta;hy5BhKMHH>Gj!W8k^VAY zv+GYdWf2Qp-3yc}+6YyyBhpKJh)-a?TU3nmjHx)We^2ViRn&wRAgmn*9;5CZ1ypSm zLLu%)3w=wLe!Y_7)+bI1vST*LN-rfph1Sn(C!f=o!fQSc#3R7RMc(_{HG!BkjyV~N zM!<6GwVdqa`6suYmXjM8(9OTdJ^hypNR>5;CY-#JuQf^{*24M398>(p6PIjDb8M$K z;XgOq7&Rsn@ALWsB&qi-otrjEwvvOn#|0t#N>5r%ralob|Lx6LMJ6czX zGckTW`X;8|?2tK&$7T1cPV^F)?-GM_;TB%O1*`3QhaM^?j_i~h46S{fR{b1lBNW?fS8&_Pmg9Lib}862faU$7x^u|C~?fJ zEd~rt`9eRht7Ogp?z82K-`O37j<3z`=yR6l@d->}7w!%l7cOkKDL00ES=(jVqTe-w zDtE$XUPWbmK~qdJ9bdByt;{{MKC@6%RZ|KWS}=6GzPVWpF#)Ud*z>*$j#bnFCrRcQ zogvk|Eim8m|0o}~PxNyds1)Q_HJSMok&)H)z5+9a%)ecA6=z-gbXwxa&-~&LU+_Sp z2%6%s63CrT1L9iKP#!5&{u4!(%5tI!(cgeg2@@I`Z|<{z@6REGFdpE zzF%CS=$tA*(>mLAmpEc}RF{f>JbZUJ+Zk@{`Sv|ju_XBXaD~|3Q=2wrMP2s27q<$b zLUF_J?DvTbb=Im>t*l+aRi{z(4g1gS=>u;haHsZBSJVNQtR5dt;k+fQ=v|e z$8uA0qn8tT?~&vSp^=;|O{rXFiX6aBM*~FgG*N8mQrQuE8N(Iojau_=Yhe>l%r^b` zJT2kmE?+zPeZSWF{xR5eqqpmaZ<3ANPv%yV*mj#X+#NOSJ30Q9mGS!#w`jR_$z1{{;fP9W4;-8ozx0Q9 zOG1fz%xT&Uwn#~Yi!CPizuKFdKNKXG6xm{LtQF9-_=>sdM%@UVW4wk?W>2x{fv|qp zrwY}Lai^EKL<6f)x_lZQ7>Eb;eXzV{g}?+C+pvwM%uez->}n~2kol);k%M#3BIeq+ z@x7!Ifbp*On!ZFKtO}&|bkl+QX4n+i>&MbcZ?f^sE8^sS`ewEP-Vo*X^vRItR@28AfKT(nVc^i#0g7BABzCRnTdoz)lHs(9xtRq< zYTtsaG4#>u;$wy}zg{r$A-UIa)WiYEKDhX@e~+<=;V)4dVlDXLV3qO`Jf&#fNu zJ_su)1)z6dTiN6ccy>w|F+vNb&YJv;QbM3AKxZ(#zI9dL zxn@=HRJi#lGBMFucM6!CZcI_TvpUWSW)0d;{%Y6FgRtArQ95RcsLF5e>)XPojz@lm zt0axxxHIgR{eJOW8Lx>?)VbDIX9?7!8*o08-(`W(5FyauK&-5`t|(}18cfey#7Pse zV7ek#1`b*JEemOH&O)57(krF0JLKE5)-VfC*4~In%de2sI)ASxln`@hHz3n%wvu=4 zimku98GzDu3N->J|8HJeEs#vKUy?i(E@bur{ZCkX(MVo8o4Os^b785;1n&JGI{M#X z;J+B`zp>f>-R`DFCMNWM!&)~X_IRH_b={dTh2{fQUT@6Fa~iqUd{gS@>L||b2HA+X zUEhV0{$B&xe{%-GL<(s*>g{wTd#9XVD?OR-s|LV?b{&CeKW^$%UcD45b>u%(A3_o{ zEjONq)mp-AHg+ksAkRYCP#Z)sIG_0}Rv92YH0R@*?G71jwgI1adFE;sW_LJdU2D9? zRZHHeT&`5Kap3;}e)b=%u91jbqeB!~77ec6oA`m7$EgliF|)GTp6Sazr!Egn?~wUE zeV@Ix5M=yzpm!Joall|@rAO~}+aK*7j_u5%-4(J!(%sSBuTwVUMEN-+zckY#}oR2u(8B6?1#F(x-LT7XSUFI2<>I z#1wc6q(Kiz+#FK_SEbd|#u859 zh3d894@(gkuR?kJ#0(48ZC`^I*`^^aoqgSPnvC7hO!wK~CfTE1THM!<}Mr>TRJ zQf6OkKD_0drk;wU3ilqED3Fq9DEcF^oOWaN7x!6mVRhYuQCp49eG3VP<6L2Y;3`e_ zQOEWjeQfl`&c?Z2W*km|`^NfgJ_^dhX!R7d`tW-)G>1yT^%c+Td_mZIHXO zg->qN&2XAmImx)QS@UI=DRVFX%?Ce-m*z_Nz_;Yia-IH`7V()B~dOaO?rO5&9 zun)az9%)p8%X{JxhP0-Jz1c8#dn_nepUYnI?k$052_fS(V>}X*nzYH(_kQ7iI7!7K z6|j$T*}}eDw37pdk5B!^bY0nPF#0=vLaUZy zyucjbr55}f0>V1;_>tZ7SzZQ5k}Sb#BkQ;=A!8#%&>j5r7VcN2WzfJ|Bxx<}WuSIU zkYm(+;QkL5)LZrbCSbdFG_C00QHNV18_@4hYIsK7x8l3yPLB{jkiO|oo$R7CRu0E! z@8R#a)u%^?%o*lh9P=0Y62I6Wy8NyK?t~k*1`@GR-#P%NW`YRI6=#5W- zdy_vch3_GJ2CL-vXR_@)Pm|>Mf+;Az%duYRZms8NMGx-~L3A^F98 zqed!M30DG8l|PtHEQowLNN+2i0zLf+2kyGfNj?>CSnzm6Y4G>4*oXpKjH%Hf+h-@? ziJIs+1}k-)aOp`g#>u22O!&%paJPJsis=c#7bI7rLEq{FlTu(`_d(ljkO9!1UYjB^3=IkE}=6E zcETTV1LRDt6|IUwT>N5BJZ!QSNh9yY-&-B5kJ8HEW~Q)QHL6Yv>!Z9ov)QrkAFE za7Z((BLUH|>z=l9F|;j%>)WV1UMnYZiVfrV69>))gzPjYrO3P+sHWBLnUB`qPhU?X zkb9*q>v~V2Ze)lXqSj&3B5!-{gGizwU(@{m9T@!&xe8|C?5uOq5}9a&LH)@d=+eXAv!&-2?mwm63mNher~l6g zmBc%f@bR>1lpu^*xp=FPZIZJvZtm9l6N9MA_SY-2EST;itl1s~UD7!M`|ZqLUYk%I z_pA2ZM4bLWp6yDF+Vt&YXHBsS8-S1rsUa)rQv2%y(6nA4i-vs$+U`spDz8NIgfaDr zjG*C;rm^v_On^N-K$Ht0-Up^Fo~T&&)byP02l5WzJXm`P>^w{v&>Ao&hqdE)WfT&z zLiZ848sre5Xg=}az|}Q6lXu;Db)52XY&Kcne=_3$XnkrrP^SRN zp5&M!Yg1bJIB~qIQQNiE(+>Q{}}j5Sj36+~t>! zVUPgQ|&6OdZ=@F>d=CJ-*tNj^0T{DdTk(NBzO3yj*$htoB`%i8) zAt`kpUN|$Gz7CyU`5i@T+de_^%v0@xl4k-s`ZA-k)A%ob);br`O!bnfgXK)iOmbn? z`)n)K9e3)z*N=G3Scm_#?!Pcg5q&U&3mty)!UgDTnz5}~C~_-Ld_x16DJOOR7N?*{ zZXBFsaXgOb)mCV-(COX9AxrpUe3p8!q7VqWQKce3og35FgSpmFr_B*)+@r)KS+Ukl z4Nf0i)oM5bSp`eGAMQOzKM`f%5|R?==2NPw)VS%sA7nC`&;PUTuHYk#zfRe@E|>k| ziXy(it`!V#*ndSRxbdqD+bfJBn&i?nQLE0pQM=aBYC`6&Q4=;^qfl}t8A0Sz>3ood z`4-}FYxR)rYZtF~oVN{4RU-9z8aumDy?n|Xbln8yDnmSXo^{$62WrDs5$8i^^tXpY zHsqI06{(C^A!RD3`Q$}siC?}|E-|T;=VidQllb8#@$4Ff?Ow73psVxaihHlP4B|W? z6;-h&l#~U`=NV*CG4F9z{;fqWRIv?!x1~k=N-55ISj&n4Uo&iq=`=X3)Su6Jdwp~k zCaCR^7yjk#-Lsp<#nUOOw}P--cc{?a)+MB$@<8ZcJs8tB4NKa(+>lIEtNIcM*|B3GPE{S2e zy?H-MBn>J{YC2NX*z2KSn+PH}wb9TO2r@b*OC9!f%o}>3u`WKNUyABSAHMVK8uR<6 z;QU4Pk@rrzC70>&*a=%1Zyy)sn*p8jq>>1~LuYVYpL+#7YE8cQUygUM1c)bL3? zso)tXGmwH1YEj@l5}I}C$pBvme)##8~j`ZbBfTC7*0xP5toV; zD_h-tp-lhh?OO{zNz;EjwD)Bkb7a9lW8nT|$KRyj2jhLzy4P3h- z&D~;LN7XHDDD&FmzWv!qztR3)=KieIALA0HU9s1~m_IQUaIcLeF|mds%@YTWa9`Rz z^J|WWB^X7|hrFp3pE4J{wkL?pO^HY+Yd|_Sy5%c>J!p+6B*X^oKE;oHszj!;-*(C1 zsg<^UCsbM~W>ht_8?b8j>MK)#qL6ucSwGm-zGM^rKK(JK9Az-@w)-hd=trcKLRjP1 zbCM=}ebz;#j*g^ z&_F4JPbxr9SouQ2Y4D2w$Zk!iLi!a)`A-Wyn|MaCBZE}QbPXaTB8$<=i(V_vGiZqr z5vT~TM)O!P5Im@6uF9W3GS3NnLmtnT4bpmC{mxa_of~x^a)nG!12zjWN#R^ZnCH{A zr!JS-HeI5njl2-er&B9{N5tAp8M?qH=7nY9(7Ab^hd(tNXd9BFzC&TtX-urVhET*n&*c)h(CUqnN^lAJ}Su@#XGikEM+@oYvi`xCIBr7 z#oxN4+kzAA8+U$I3)Itl*)bjh$cW%}S;kgLp(Jk+B$!7N^-V_c8le5)TXt7Aj&Pou zrlzJRi<4;diz)EFjlrenuNtapJ2aYn{F-QFp+|A^&&=&|DlRrhz39x$`AL~7PRiqO zQ6xi=bdUwB(yf{7$6j^V*z~}$Dv!Xf@1KjJj<$BWAb(l%uz+>-P5;G-paLA*->$5D!Q%I{39~H8#Q2qy+|J<%^ID9wUJD4&1MXjw(pyXNS?b9YltftbR6;SOp-t)K29lCqc!xL+1iJ&-> z{Nr$798+oO-|1T|T*9P(F2+q?(AWY%aquw!RKkFI6|ijGr+a;A1gzp!yxraJ6m71+hSzc+&7e^Mv?jqnV05|x4H+H z_&wc-)77lcfh-=1ZJrnHY{sPa-AHBrDSdQ2A5OVD40IQGe1C&vOIi|}k3R*-GhS5s zkW2h}IrUMkNg3^RYiN7Vbw1k{8sFIAE;+Q{DJ68_^`G8VaU^uQRw+E2z8I;-0UzN0 zOg^PozK|<=`N;7qa;9sh20GE9b0%q)nhLJL%UG8)?A_#Les!(d}#zd5MrRB0cHEH?4vH@(dr;Qr5q{AcfL=k|2;A8[0]): Promise => createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [ + { dataField: 'region', area: 'row' }, + { dataField: 'city', area: 'column' }, + { dataField: 'amount', area: 'data', summaryType: 'sum' }, + ], + store: sales, + }, +}); + +test('PivotGrid renders its areas and answers its options', THEMES, async ({ page }) => { + await createPivotGrid(page); + + const pivotGrid = new PivotGrid(page, '#container'); + + await expect(pivotGrid.dataArea).toBeVisible(); + expect(await pivotGrid.option('rowHeaderLayout')).toBe('standard'); + + await pivotGrid.option('rowHeaderLayout', 'tree'); + + expect(await pivotGrid.option('rowHeaderLayout')).toBe('tree'); +}); + +test('PivotGrid markup', async ({ page }) => { + await createPivotGrid(page); + + await testScreenshot(page, 'PivotGrid markup.png', { element: '#container' }); +}); diff --git a/e2e/playwright/tests/editors/checkBox/common.spec.ts b/e2e/playwright/tests/editors/checkBox/common.spec.ts deleted file mode 100644 index e49f1ec699ab..000000000000 --- a/e2e/playwright/tests/editors/checkBox/common.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expect, test } from '../../../fixtures'; -import { createWidget } from '../../../helpers/createWidget'; -import CheckBox from '../../../models/checkBox'; - -// The tags list the themes this test also runs in; a test without them runs in the default one. -const THEMES = { tag: ['@generic.light', '@material.blue.light', '@material.blue.light.compact'] }; - -test('CheckBox switches its state on click', THEMES, async ({ page }) => { - await createWidget(page, 'dxCheckBox', { value: false }); - - const checkBox = new CheckBox(page, '#container'); - - expect(await checkBox.isChecked()).toBe(false); - - await checkBox.element.click(); - - expect(await checkBox.isChecked()).toBe(true); - expect(await checkBox.option('value')).toBe(true); -}); - -test('CheckBox renders the indeterminate state', THEMES, async ({ page }) => { - await createWidget(page, 'dxCheckBox', { value: null }); - - const checkBox = new CheckBox(page, '#container'); - - expect(await checkBox.isIndeterminate()).toBe(true); -}); diff --git a/e2e/playwright/tests/navigation/button/common.spec.ts b/e2e/playwright/tests/navigation/button/common.spec.ts deleted file mode 100644 index e192ae28a3d0..000000000000 --- a/e2e/playwright/tests/navigation/button/common.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { ButtonType } from 'devextreme/common'; -import { expect, test } from '../../../fixtures'; -import { createWidget } from '../../../helpers/createWidget'; -import { - addCaptionTo, - appendElementTo, - setAttribute, - setClassAttribute, -} from '../../../helpers/domUtils'; -import { testScreenshot } from '../../../helpers/screenshots'; -import Button from '../../../models/button'; - -// The tags list the themes this test also runs in; a test without them runs in the default one. -const THEMES = { tag: ['@generic.light', '@material.blue.light', '@material.blue.light.compact'] }; - -const STATES = ['default', 'focused', 'hover', 'active', 'selected', 'disabled']; - -const BUTTONS = [ - ...['danger', 'default', 'normal', 'success'].map((type: ButtonType) => ({ - type, - text: `${type[0].toUpperCase()}${type.slice(1)}`, - })), - { icon: 'find', text: 'Find' }, - { icon: 'find' }, -]; - -['text', 'outlined', 'contained'].forEach((stylingMode) => { - const testName = `Buttons, stylingMode=${stylingMode}`; - - test(testName, async ({ page }) => { - await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); - await setAttribute(page, '#container', 'style', 'width: fit-content; padding: 8px;'); - - for (const state of STATES) { - await appendElementTo(page, '#container', 'div', `mode${state}`); - await setAttribute(page, `#mode${state}`, 'style', 'display: flex; gap: 8px; margin-bottom: 16px;'); - await addCaptionTo(page, `#mode${state}`, state); - - for (const [index] of BUTTONS.entries()) { - await appendElementTo(page, `#mode${state}`, 'div', `button-${state}-${index}`); - } - - for (const [index, config] of BUTTONS.entries()) { - await createWidget(page, 'dxButton', { - ...config, - stylingMode, - disabled: state === 'disabled', - }, `#button-${state}-${index}`); - } - - if (state !== 'default' && state !== 'disabled') { - for (const [index] of BUTTONS.entries()) { - await setClassAttribute(page, `#button-${state}-${index}`, `dx-state-${state}`); - } - } - } - - await testScreenshot(page, `${testName}.png`, { element: '#container' }); - }); -}); - -test('Button reports its text, selected and disabled state', THEMES, async ({ page }) => { - await createWidget(page, 'dxButton', { text: 'Find', icon: 'find' }); - - const button = new Button(page, '#container'); - - await expect(button.text).toHaveText('Find'); - expect(await button.isSelected()).toBe(false); - expect(await button.isDisabled()).toBe(false); - - await button.option('disabled', true); - - expect(await button.isDisabled()).toBe(true); -}); diff --git a/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=contained (fluent.blue.light).png b/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=contained (fluent.blue.light).png deleted file mode 100644 index ce153a4cb49496b3c485784f0ae60312c72cd297..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36994 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 b#0LWARw_uY<#sJQ8-R_agGHs8_lvKze diff --git a/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=outlined (fluent.blue.light).png b/e2e/playwright/tests/navigation/button/etalons/Buttons, stylingMode=outlined (fluent.blue.light).png deleted file mode 100644 index 2ee920bbf97b063fa99e5412e3e25b8eac21d229..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37094 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 v|99i|zMw%szP@N52 Date: Thu, 27 Aug 2026 23:06:12 -0300 Subject: [PATCH 18/18] Keep the Button and CheckBox models: the composite models build on them --- e2e/playwright/models/button.ts | 24 ++++++++++++++++++++++++ e2e/playwright/models/checkBox.ts | 23 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 e2e/playwright/models/button.ts create mode 100644 e2e/playwright/models/checkBox.ts diff --git a/e2e/playwright/models/button.ts b/e2e/playwright/models/button.ts new file mode 100644 index 000000000000..81df6f59b1d2 --- /dev/null +++ b/e2e/playwright/models/button.ts @@ -0,0 +1,24 @@ +import type { Locator } from '@playwright/test'; +import type { WidgetName } from './types'; +import Widget from './internal/widget'; + +export const CLASS = { + text: '.dx-button-text', + selected: 'dx-state-selected', + itemSelected: 'dx-item-selected', +}; + +export default class Button extends Widget { + public static className = 'dx-button'; + + public get text(): Locator { + return this.element.locator(CLASS.text); + } + + public isSelected(): Promise { + return this.hasClass(CLASS.selected); + } + + // eslint-disable-next-line class-methods-use-this + public getName(): WidgetName { return 'dxButton'; } +} diff --git a/e2e/playwright/models/checkBox.ts b/e2e/playwright/models/checkBox.ts new file mode 100644 index 000000000000..0ba7c189b705 --- /dev/null +++ b/e2e/playwright/models/checkBox.ts @@ -0,0 +1,23 @@ +import type { WidgetName } from './types'; +import Widget from './internal/widget'; + +export const CLASS = { + checked: 'dx-checkbox-checked', + indeterminate: 'dx-checkbox-indeterminate', + icon: '.dx-checkbox-icon', +}; + +export default class CheckBox extends Widget { + public static className = 'dx-checkbox'; + + public isChecked(): Promise { + return this.hasClass(CLASS.checked); + } + + public isIndeterminate(): Promise { + return this.hasClass(CLASS.indeterminate); + } + + // eslint-disable-next-line class-methods-use-this + public getName(): WidgetName { return 'dxCheckBox'; } +}