diff --git a/.github/workflows/playwright_tests.yml b/.github/workflows/playwright_tests.yml new file mode 100644 index 000000000000..33cc9cc9f04c --- /dev/null +++ b/.github/workflows/playwright_tests.yml @@ -0,0 +1,177 @@ +name: Playwright tests (POC) + +concurrency: + group: wf-${{github.event.pull_request.number || github.sha}}-playwright + cancel-in-progress: true + +on: + pull_request: + workflow_dispatch: + inputs: + repeat_count: + description: 'Number of times to run tests (for stability check)' + required: false + default: '1' + type: string + +env: + NX_SKIP_NX_CACHE: ${{ contains(github.event.pull_request.labels.*.name, 'skip-cache') && 'true' || 'false' }} + +jobs: + build: + name: Build DevExtreme + runs-on: devextreme-shr2 + timeout-minutes: 15 + + steps: + - name: Get sources + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup 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: Build + shell: bash + env: + NODE_OPTIONS: --max-old-space-size=8192 + run: | + pnpx nx build devextreme-scss + pnpx 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@v4 + with: + name: devextreme-artifacts + path: ./packages/devextreme/artifacts.zip + retention-days: 1 + + playwright: + name: ${{ matrix.ARGS.name }} + needs: build + strategy: + fail-fast: false + matrix: + ARGS: [ + { componentFolder: "scheduler/common", name: "scheduler / common (1/3)", shard: "1/3" }, + { componentFolder: "scheduler/common", name: "scheduler / common (2/3)", shard: "2/3" }, + { componentFolder: "scheduler/common", name: "scheduler / common (3/3)", shard: "3/3" }, + { componentFolder: "scheduler/timezones", name: "scheduler / timezones" }, + { componentFolder: "scheduler/viewOffset", name: "scheduler / viewOffset", project: "chromium-1185" }, + { componentFolder: "dataGrid/common", name: "dataGrid / common (1/2)", shard: "1/2" }, + { componentFolder: "dataGrid/common", name: "dataGrid / common (2/2)", shard: "2/2" }, + { componentFolder: "dataGrid/sticky", name: "dataGrid / sticky" }, + { componentFolder: "common", name: "common (1/2)", shard: "1/2" }, + { componentFolder: "common", name: "common (2/2)", shard: "2/2" }, + { componentFolder: "editors", name: "editors (1/2)", shard: "1/2" }, + { componentFolder: "editors", name: "editors (2/2)", shard: "2/2" }, + { componentFolder: "navigation", name: "navigation" }, + { componentFolder: "cardView", name: "cardView" }, + { componentFolder: "accessibility", name: "accessibility (1/2)", shard: "1/2" }, + { componentFolder: "accessibility", name: "accessibility (2/2)", shard: "2/2" }, + ] + runs-on: devextreme-shr2 + timeout-minutes: 45 + + steps: + - name: Get sources + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: devextreme-artifacts + path: ./packages/devextreme + + - name: Unpack artifacts + working-directory: ./packages/devextreme + run: 7z x artifacts.zip -aoa + + - uses: pnpm/action-setup@v4 + 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@v4 + 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: Install Playwright browsers + working-directory: ./e2e/testcafe-devextreme + run: npx playwright install chromium + + - name: Run Playwright tests + working-directory: ./e2e/testcafe-devextreme + env: + NODE_OPTIONS: --max-old-space-size=8192 + THEME: fluent.blue.light + run: | + REPEAT_COUNT="${{ github.event.inputs.repeat_count || '1' }}" + SHARD_ARG="" + if [ "${{ matrix.ARGS.shard }}" != "" ]; then + SHARD_ARG="--shard=${{ matrix.ARGS.shard }}" + fi + PROJECT="${{ matrix.ARGS.project || 'chromium' }}" + PROJECT_ARG="--project=$PROJECT" + + set -o pipefail + for i in $(seq 1 $REPEAT_COUNT); do + echo "=== Run $i / $REPEAT_COUNT ===" + npx playwright test \ + --config playwright.config.ts \ + playwright-tests/${{ matrix.ARGS.componentFolder }} \ + $SHARD_ARG \ + $PROJECT_ARG \ + --reporter=list \ + 2>&1 | tee -a playwright-output-run-$i.log + echo "" + done + + - name: Sanitize job name + if: always() + run: echo "JOB_NAME=$(echo "${{ matrix.ARGS.name }}" | tr '/' '-' | tr ' ' '-')" >> $GITHUB_ENV + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-results-${{ env.JOB_NAME }} + path: | + e2e/testcafe-devextreme/playwright-results/ + e2e/testcafe-devextreme/playwright-output-*.log + e2e/testcafe-devextreme/test-results/ + if-no-files-found: ignore diff --git a/FINISH_REVIEW.md b/FINISH_REVIEW.md new file mode 100644 index 000000000000..01bdb5858770 --- /dev/null +++ b/FINISH_REVIEW.md @@ -0,0 +1,80 @@ +# Playwright Migration — Final Review Checklist + +## Goal +Every Playwright test must have a 1:1 correspondence with TestCafe: +- Same test name +- Same test count per file +- Same behavior tested + +## Key Rules + +### 1. forEach Tests Must Match TestCafe Structure +TestCafe allows duplicate test names inside forEach loops: +```typescript +// TestCafe: 5 iterations = 5 tests with SAME name "Usual appointments render" +[0, 735, 1440, -735, -1440].forEach((offset) => { + test('Usual appointments render', async (t) => { ... }); +}); +``` + +Playwright does NOT allow duplicate test names. Solution: **keep forEach inside ONE test**: +```typescript +// CORRECT: 1 test with 5 iterations inside (matches TC count) +test('Usual appointments render', async ({ page }) => { + for (const offset of [0, 735, 1440, -735, -1440]) { + await clearTestPage(page); + // ... test with this offset + } +}); +``` + +**WRONG** (creates extra tests): +```typescript +// WRONG: 5 separate tests with different names +test('Usual appointments render (offset: 0)', ...) +test('Usual appointments render (offset: 735)', ...) +``` + +### 2. Test Names Must Be Identical +- Copy test name from TestCafe exactly +- No ticket numbers added/removed unless TC has them +- No parameterization suffixes unless TC has them + +### 3. No Extra Screenshots / No Missing Screenshots +- All etalons are from TestCafe CI +- Playwright must not generate new etalons +- Playwright must not delete any etalons + +### 4. Verification Steps + +For each component folder: +1. Run TestCafe test names from CI: check TEST_NAMES_COMPARISON.md +2. Run `npx playwright test --list --project=chromium playwright-tests//` +3. Compare counts and names +4. If PW has more tests — check for forEach expansion (collapse them) +5. If PW has fewer — check for missing tests (add them) + +### 5. CI Must Pass +- `common (1/2)` and `common (2/2)` — must be SUCCESS +- `scheduler/viewOffset` — must be SUCCESS +- Other jobs — screenshot pixel differences expected (macOS vs Ubuntu rendering) + +## Known Issues + +### forEach Expansion (needs collapsing) +These components have expanded forEach that need to be collapsed: +- `scheduler/viewOffset/` — TC: ~28 tests, PW: ~537 (massive expansion) +- `scheduler/timezones/` — TC: ~36 tests, PW: ~105 +- `scheduler/common/` — various files with expanded forEach +- `accessibility/*.matrix.spec.ts` — matrix expansion (OK — mirrors TC testAccessibility pattern) + +### Screenshot Dimension Mismatches (CI vs Local) +- TestCafe headless Chrome has 15px scrollbar, Playwright headless has 0px +- ViewOffset uses viewport 1185 (project chromium-1185) to match TC etalons +- Other components use viewport 1200 + `::-webkit-scrollbar` CSS +- Some tests fail on CI but pass locally due to font rendering differences + +### Accessibility Matrix Tests +TC uses `testAccessibility()` which generates N tests per option combination at runtime. +PW uses `testAccessibilityMatrix()` helper that does the same. +These are expected to have different counts but same coverage. diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 000000000000..8a2bb8047726 --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,187 @@ +# Playwright Migration - Implementation Plan + +## Goal + +Prove that Playwright can fully replace TestCafe for screenshot/e2e tests. Both must run in CI simultaneously until Playwright is proven stable. + +## Rules + +- **No new screenshots** — Playwright tests must use existing TestCafe etalons from `tests/*/etalons/` +- **No deleted screenshots** — every existing etalon must be referenced +- **No deleted tests** — every TestCafe test must have a Playwright equivalent +- **No changed test logic** — page objects may differ in syntax but must verify the same behavior +- **Threshold adjustments only** — if a test doesn't pass locally due to cross-platform rendering, increase `maxDiffPixelRatio` per-test (not globally) +- **CI must report failures clearly** — if a screenshot doesn't match, the diff must appear in artifacts + +## Current State + +- **565 Playwright spec files** vs **620 TestCafe test files** +- **7 skipped tests** (all in scheduler/timezones) +- CI workflow exists (`playwright_tests.yml`) but only runs **scheduler** tests (common, timezones, viewOffset) +- CI runs on `devextreme-shr2` self-hosted runners +- Playwright config: viewport 1185x800, `maxDiffPixelRatio: 0.07`, `threshold: 0.2` +- Etalons are read directly from TestCafe `tests/` directory via `snapshotDir: './tests'` + +## Missing Work + +### 1. Expand CI to all components +Currently CI only runs scheduler tests. Need to add matrix entries for: +- `dataGrid/common`, `dataGrid/sticky` +- `editors/*` +- `navigation/*` +- `common/*` (draggable, filterBuilder, gantt, pivotGrid, treeList, etc.) +- `cardView/*` +- `accessibility/*` + +### 2. Fix failing tests per scope (iterative) +Work scope-by-scope. For each scope: +1. Run tests locally: `npx playwright test playwright-tests//` +2. Fix failures — adjust page objects, waitFor conditions, thresholds +3. Commit when scope passes locally +4. Push, verify on CI +5. While CI runs, start next scope + +**Scope order** (largest/most critical first): +1. `scheduler/` — already in CI, mostly working +2. `dataGrid/` — largest component +3. `common/` — many sub-components +4. `editors/` +5. `navigation/` +6. `cardView/` +7. `accessibility/` + +### 3. Verify CI failure reporting +- Intentionally break one etalon and push +- Confirm CI fails with clear error +- Confirm diff artifacts are uploaded and viewable + +### 4. Run all tests together +After each scope passes individually, run full suite: +```bash +npx playwright test playwright-tests/ --reporter=list +``` +Verify no cross-scope interference. + +### 5. Handle stuck tests +If a test cannot be fixed after 5 attempts: +- Mark with `test.skip()` and add comment: `// TODO: Playwright migration - ` +- Log the test path and failure reason in this file under "Stuck Tests" section + +## How to Run + +### Locally +```bash +cd e2e/testcafe-devextreme + +# Single scope +npx playwright test playwright-tests/scheduler/common/ --reporter=list + +# All tests +npx playwright test playwright-tests/ --reporter=list + +# With UI for debugging +npx playwright test playwright-tests/scheduler/common/ --ui +``` + +### CI +Push to `playwright-poc` branch — workflow triggers automatically. + +### CI Monitoring (gh cli) + +```bash +# Check PR checks status +gh pr checks --repo DevExpress/DevExtreme + +# View failed job logs +gh run view --repo DevExpress/DevExtreme --log-failed + +# Re-run only failed jobs +gh run rerun --repo DevExpress/DevExtreme --failed + +# List workflow runs for the branch +gh run list --repo DevExpress/DevExtreme --branch playwright-poc --workflow "Playwright tests (POC)" + +# Watch a run in real-time +gh run watch --repo DevExpress/DevExtreme +``` + +- CI runs on `devextreme-shr2` self-hosted runners +- Concurrency group with `cancel-in-progress: true` — new push cancels previous run +- Playwright workflow: `.github/workflows/playwright_tests.yml` +- Artifacts: screenshot diffs are uploaded on failure for inspection + +## Reporting + +After each scope is completed (or if stuck), send a status update to Telegram (chat_id: 253383754) with this format: + +``` +Playwright Migration Status + +Scope: +Status: + +Tests: passing, failing, skipped +CI: + +Progress: +✅ scheduler/common — tests passing +✅ scheduler/viewOffset — tests passing +🔧 dataGrid/common — fixing ( failing) +⬜ editors — not started +... + +Stuck tests: +- +``` + +Send this report: +- After completing each scope +- When all scopes are done +- If stuck on a scope for more than 30 minutes + +## Ralph Loop Prompt + +``` +Working directory: /Users/alekseisemikozov/Projects/DevExtreme/.claude/worktrees/playwright-poc/e2e/testcafe-devextreme + +Task: Fix failing Playwright tests scope by scope. + +Rules: +- Do NOT add/delete/modify any screenshot etalon files +- Do NOT change test logic — only fix page objects, selectors, waitFor, thresholds +- Do NOT skip tests unless they fail after 5 fix attempts +- For each scope: run tests, fix failures, run again until all pass +- If a test fails 5+ times, mark test.skip() with "// TODO: Playwright migration - " and move on +- Commit after each scope is fixed with message: "Playwright - fix tests" + +Current scope order: +1. scheduler/common (verify still passes) +2. scheduler/timezones (7 skipped — try to unskip) +3. scheduler/viewOffset +4. dataGrid/common +5. dataGrid/sticky +6. common/* (each subfolder) +7. editors/* +8. navigation/* +9. cardView/* +10. accessibility/* + +For each scope: +1. Run: npx playwright test playwright-tests// --reporter=list +2. If failures: read test code + page object, read TestCafe equivalent, fix +3. Re-run. Repeat up to 5 times per failing test. +4. When scope passes: git add + commit +5. Move to next scope + +After all scopes done: +- Run full suite: npx playwright test playwright-tests/ --reporter=list +- Report results +``` + +## Stuck Tests + +(Will be filled as tests are discovered that cannot be fixed) + +| Test file | Reason | Attempts | +|-----------|--------|----------| +| | | | diff --git a/e2e/testcafe-devextreme/.gitignore b/e2e/testcafe-devextreme/.gitignore index 8c2eaf550a10..2109b29f5032 100644 --- a/e2e/testcafe-devextreme/.gitignore +++ b/e2e/testcafe-devextreme/.gitignore @@ -1,2 +1,4 @@ /screenshots -/artifacts \ No newline at end of file +/artifactsplaywright-results/ +playwright-report/ +pw-browsers/ diff --git a/e2e/testcafe-devextreme/docker/Dockerfile.playwright b/e2e/testcafe-devextreme/docker/Dockerfile.playwright new file mode 100644 index 000000000000..8931390e813f --- /dev/null +++ b/e2e/testcafe-devextreme/docker/Dockerfile.playwright @@ -0,0 +1,29 @@ +FROM node:20 + +RUN apt-get update && apt-get install -y \ + chromium \ + fonts-liberation \ + fonts-noto-color-emoji \ + fonts-freefont-ttf \ + libatk-bridge2.0-0 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libgbm1 \ + libasound2 \ + libpango-1.0-0 \ + libcairo2 \ + libcups2 \ + libnspr4 \ + libnss3 \ + libx11-xcb1 \ + libxss1 \ + libxtst6 \ + && rm -rf /var/lib/apt/lists/* + +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium +ENV CHROME_BIN=/usr/bin/chromium + +WORKDIR /work diff --git a/e2e/testcafe-devextreme/eslint.config.mjs b/e2e/testcafe-devextreme/eslint.config.mjs index 232738cff5e5..16c308fde676 100644 --- a/e2e/testcafe-devextreme/eslint.config.mjs +++ b/e2e/testcafe-devextreme/eslint.config.mjs @@ -25,6 +25,10 @@ export default [ { ignores: [ 'node_modules/**', + 'playwright-tests/**', + 'playwright-helpers/**', + 'playwright-results/**', + 'playwright-report/**', ], }, ...spellCheckConfig, diff --git a/e2e/testcafe-devextreme/images/test-image-1.png b/e2e/testcafe-devextreme/images/test-image-1.png new file mode 100644 index 000000000000..c9cf573551d3 Binary files /dev/null and b/e2e/testcafe-devextreme/images/test-image-1.png differ diff --git a/e2e/testcafe-devextreme/images/test-image-2.png b/e2e/testcafe-devextreme/images/test-image-2.png new file mode 100644 index 000000000000..61fb45e996e4 Binary files /dev/null and b/e2e/testcafe-devextreme/images/test-image-2.png differ diff --git a/e2e/testcafe-devextreme/package.json b/e2e/testcafe-devextreme/package.json index f4b479abb1b9..dd9225e6b1a5 100644 --- a/e2e/testcafe-devextreme/package.json +++ b/e2e/testcafe-devextreme/package.json @@ -3,34 +3,39 @@ "version": "26.1.0", "scripts": { "test": "ts-node ./runner.ts", + "posttest": "echo '=== PLAYWRIGHT POC ===' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright install chromium 2>&1 || true; echo '--- PASS 1: generate baselines ---' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list --update-snapshots 2>&1; echo '--- PASS 2: compare against baselines ---' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list 2>&1 | tee playwright-run.log; echo \"--- PASS 2 exit code: $? ---\"; echo '=== PLAYWRIGHT DONE ==='", + "test:playwright": "npx playwright test --config playwright.config.ts --reporter=list", "lint": "eslint", "update-failed-etalons": "node update_failed_etalons.mjs" }, "devDependencies": { "@babel/eslint-parser": "catalog:eslint8", "@babel/plugin-transform-runtime": "7.29.0", + "@eslint/eslintrc": "catalog:", + "@playwright/test": "^1.58.2", + "@stylistic/eslint-plugin": "catalog:", "@testcafe-community/axe": "3.5.0", "@types/jquery": "catalog:", + "@typescript-eslint/eslint-plugin": "catalog:", + "@typescript-eslint/parser": "catalog:", "axe-core": "catalog:", "devextreme": "workspace:*", "devextreme-screenshot-comparer": "2.0.17", "devextreme-testcafe-models": "workspace:*", + "eslint": "catalog:", + "eslint-config-devextreme": "catalog:", + "eslint-migration-utils": "workspace:*", + "eslint-plugin-i18n": "catalog:", + "eslint-plugin-import": "catalog:", + "eslint-plugin-no-only-tests": "catalog:", "glob": "11.1.0", "minimist": "1.2.8", "mockdate": "3.0.5", "nconf": "0.12.1", + "pixelmatch": "^7.1.0", + "pngjs": "^7.0.0", "testcafe": "3.7.4", "testcafe-reporter-spec-time": "4.0.0", - "ts-node": "10.9.2", - "eslint": "catalog:", - "@eslint/eslintrc": "catalog:", - "@stylistic/eslint-plugin": "catalog:", - "@typescript-eslint/eslint-plugin": "catalog:", - "@typescript-eslint/parser": "catalog:", - "eslint-config-devextreme": "catalog:", - "eslint-migration-utils": "workspace:*", - "eslint-plugin-i18n": "catalog:", - "eslint-plugin-import": "catalog:", - "eslint-plugin-no-only-tests": "catalog:" + "ts-node": "10.9.2" } } diff --git a/e2e/testcafe-devextreme/playwright-helpers/accessibility.ts b/e2e/testcafe-devextreme/playwright-helpers/accessibility.ts new file mode 100644 index 000000000000..d4eb58b5b86a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/accessibility.ts @@ -0,0 +1,149 @@ +import type { Page } from '@playwright/test'; +import { expect, test } from '@playwright/test'; +import { generateOptionMatrix } from './generateOptionMatrix'; +import { createWidget } from './createWidget'; + +export interface A11yCheckOptions { + runOnly?: string; + rules?: Record; +} + +async function injectAxeIfNeeded(page: Page): Promise { + const axeLoaded = await page.evaluate(() => !!(window as any).axe); + if (!axeLoaded) { + const axePath = require.resolve('axe-core'); + await page.addScriptTag({ path: axePath }); + await page.waitForFunction(() => !!(window as any).axe); + } +} + +export async function a11yCheck( + page: Page, + options: A11yCheckOptions = {}, + selector?: string, +): Promise { + await injectAxeIfNeeded(page); + + const results = await page.evaluate( + ({ opts, sel }) => { + const axeOptions: any = { rules: {} }; + if (opts.rules) { + Object.entries(opts.rules).forEach(([rule, config]) => { + axeOptions.rules[rule] = config; + }); + } + if (opts.runOnly) { + axeOptions.runOnly = opts.runOnly; + } + const context = sel || document; + return (window as any).axe.run(context, axeOptions); + }, + { opts: options, sel: selector }, + ); + + const violations = results.violations as any[]; + + if (violations.length > 0) { + const report = violations + .map((v: any) => { + const nodes = v.nodes + .map((n: any) => ` - ${n.html}`) + .join('\n'); + return `${v.id} (${v.impact}): ${v.description}\n${nodes}`; + }) + .join('\n\n'); + + expect(violations.length, `Accessibility violations found:\n${report}`).toBe(0); + } +} + +export interface TestAccessibilityConfig { + widgetName: string; + widgetOptions?: Record; + a11yCheckConfig?: A11yCheckOptions; + selector?: string; +} + +export async function testAccessibility( + page: Page, + config: TestAccessibilityConfig, +): Promise { + const { + widgetName, + widgetOptions = {}, + a11yCheckConfig = {}, + selector = '#container', + } = config; + + await page.evaluate(({ name, opts, sel }) => { + (window as any).DevExpress.fx.off = true; + const options = typeof opts === 'function' ? opts() : opts; + ($(sel) as any)[name](options); + }, { name: widgetName, opts: widgetOptions, sel: selector }); + + await a11yCheck(page, a11yCheckConfig, selector); +} + +type OptionMatrix = { [K in keyof T]: T[K][] }; + +export interface MatrixAccessibilityConfig> { + component: string; + options?: OptionMatrix; + a11yCheckConfig?: A11yCheckOptions; + selector?: string; + containerUrl: string; + created?: (page: Page, optionConfiguration: TOptions) => Promise; +} + +const componentsWithDisabledColorContrastIssues = ['dxTagBox', 'dxFileUploader', 'dxDateRangeBox']; + +export function testAccessibilityMatrix = Record>( + config: MatrixAccessibilityConfig, +): void { + const { + component, + options, + selector = '#container', + a11yCheckConfig = {}, + containerUrl, + created, + } = config; + + const optionConfigurations: TOptions[] = options && Object.keys(options).length + ? generateOptionMatrix(options as OptionMatrix) + : [{} as TOptions]; + + optionConfigurations.forEach((optionConfiguration, index) => { + test(`${component}: test with axe #${index}`, async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + + const currentA11yCheckConfig = { ...a11yCheckConfig } as A11yCheckOptions; + const isComponentDisabled = (optionConfiguration as Record).disabled; + const shouldIgnoreColorContrast = componentsWithDisabledColorContrastIssues + .includes(component) && isComponentDisabled; + + if (shouldIgnoreColorContrast) { + if (currentA11yCheckConfig.runOnly === 'color-contrast') { + return; + } + currentA11yCheckConfig.rules = { + ...currentA11yCheckConfig.rules, + 'color-contrast': { enabled: false }, + }; + } + + await createWidget(page, component, optionConfiguration as Record, selector); + + if (created) { + await created(page, optionConfiguration); + } + + await a11yCheck(page, currentA11yCheckConfig, selector); + }); + }); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/chat.ts b/e2e/testcafe-devextreme/playwright-helpers/chat.ts new file mode 100644 index 000000000000..e85158d2e6c4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/chat.ts @@ -0,0 +1,110 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + input: 'dx-texteditor-input', + messageList: 'dx-chat-messagelist', + messageBoxButton: 'dx-button', + scrollable: 'dx-scrollable', + textArea: 'dx-textarea', + messageBubble: 'dx-chat-messagebubble', + contextMenuContent: 'dx-messagelist-context-menu-content', + menuItem: 'dx-menu-item', +} as const; + +export class Chat { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + readonly messageList: Locator; + readonly messageBoxButton: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.messageList = this.element.locator(`.${CLASS.messageList}`); + this.messageBoxButton = this.element.locator(`.${CLASS.messageBoxButton}`); + } + + getInput(): Locator { + return this.element.locator(`.${CLASS.textArea} .${CLASS.input}`); + } + + getScrollable(): Locator { + return this.element.locator(`.${CLASS.scrollable}`); + } + + getMessage(index: number): Locator { + return this.element.locator(`.${CLASS.messageBubble}`).nth(index); + } + + getContextMenuContent(): Locator { + return this.page.locator(`.${CLASS.contextMenuContent}`); + } + + getContextMenuItem(index: number): Locator { + return this.getContextMenuContent().locator(`.${CLASS.menuItem}`).nth(index); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxChat('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxChat('instance').option(n), + { sel, name }, + ); + } + + async focus(): Promise { + await this.page.evaluate( + (sel) => { + ($(sel) as any).dxChat('instance').focus(); + }, + this.selector, + ); + } + + async repaint(): Promise { + await this.page.evaluate( + (sel) => { + ($(sel) as any).dxChat('instance').repaint(); + }, + this.selector, + ); + } + + async renderMessage(message: unknown): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ sel: s, msg }) => { + ($(s) as any).dxChat('instance').renderMessage(msg); + }, + { sel, msg: message }, + ); + } + + async scrollOffset(): Promise<{ top: number; left: number }> { + const sel = this.selector; + return this.page.evaluate( + (s) => { + const scrollable = ($(s) as any).find('.dx-scrollable').dxScrollable('instance'); + return { + top: scrollable.scrollTop(), + left: scrollable.scrollLeft(), + }; + }, + sel, + ); + } + + async rightClick(locator: Locator): Promise { + await locator.click({ button: 'right' }); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts b/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts new file mode 100644 index 000000000000..850221094686 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts @@ -0,0 +1,52 @@ +import type { Page } from '@playwright/test'; + +function serializeValue(value: unknown, depth = 0): string { + if (depth > 10) return 'undefined'; + if (value === undefined) return 'undefined'; + if (value === null) return 'null'; + if (typeof value === 'function') { + const str = value.toString(); + if (/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/.test(str) && !str.startsWith('function') && !str.startsWith('(') && !str.startsWith('async')) { + return `function ${str}`; + } + return str; + } + if (value instanceof Date) return `new Date(${value.getTime()})`; + if (Array.isArray(value)) { + return `[${value.map((v) => serializeValue(v, depth + 1)).join(',')}]`; + } + if (typeof value === 'object') { + const entries = Object.entries(value as Record) + .map(([k, v]) => `${JSON.stringify(k)}:${serializeValue(v, depth + 1)}`); + return `{${entries.join(',')}}`; + } + return JSON.stringify(value); +} + +export async function createWidget( + page: Page, + widgetName: string, + widgetOptions: Record | (() => Record), + selector = '#container', + disableFxAnimation = true, +): Promise { + const optionsStr = typeof widgetOptions === 'function' + ? `(${widgetOptions.toString()})()` + : serializeValue(widgetOptions); + + const script = ` + DevExpress.fx.off = ${disableFxAnimation}; + $('${selector}')['${widgetName}'](${optionsStr}); + `; + await page.evaluate(script); + + await page.evaluate(() => { + document.querySelectorAll('dx-license').forEach((el) => { + const btn = el.querySelector('div[style*="cursor: pointer"]') as HTMLElement | null; + if (btn) btn.click(); + }); + if (document.activeElement && document.activeElement !== document.body) { + (document.activeElement as HTMLElement).blur(); + } + }); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/dataGrid.ts b/e2e/testcafe-devextreme/playwright-helpers/dataGrid.ts new file mode 100644 index 000000000000..28529d328442 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/dataGrid.ts @@ -0,0 +1,977 @@ +import type { Page, Locator } from '@playwright/test'; + +type LocatorWithElement = Locator & { element: Locator }; + +function locatorWithElement(locator: Locator): LocatorWithElement { + return new Proxy(locator, { + get(target, prop) { + if (prop === 'element') return target; + const value = (target as any)[prop]; + if (prop === 'constructor') return value; + return typeof value === 'function' ? value.bind(target) : value; + }, + }) as LocatorWithElement; +} + +const CLASS = { + dataGrid: 'dx-datagrid', + headers: 'dx-datagrid-headers', + headerRow: 'dx-header-row', + headerPanel: 'dx-datagrid-header-panel', + filterRow: 'dx-datagrid-filter-row', + row: 'dx-row', + dataRow: 'dx-data-row', + groupRow: 'dx-group-row', + focusedRow: 'dx-row-focused', + errorRow: 'dx-error-row', + masterDetailRow: 'dx-master-detail-row', + adaptiveDetailRow: 'dx-adaptive-detail-row', + adaptiveCommandCellHidden: 'dx-command-adaptive-hidden', + adaptiveColumnButton: 'dx-datagrid-adaptive-more', + freeSpaceRow: 'dx-freespace-row', + footerRow: 'dx-footer-row', + groupFooterRow: 'dx-datagrid-group-footer', + editFormRow: 'dx-datagrid-edit-form', + formButtonsContainer: 'dx-datagrid-form-buttons-container', + popupEdit: 'dx-datagrid-edit-popup', + rowsView: 'dx-datagrid-rowsview', + fixedGridView: 'dx-datagrid-content-fixed', + scrollableContainer: 'dx-scrollable-container', + scrollContainer: 'dx-datagrid-scroll-container', + overlayContent: 'dx-overlay-content', + overlayWrapper: 'dx-overlay-wrapper', + loadPanel: 'dx-loadpanel', + loadPanelContent: 'dx-loadpanel-content', + toolbar: 'dx-toolbar', + contextMenu: 'dx-context-menu', + columnChooser: 'dx-datagrid-column-chooser', + columnChooserButton: 'dx-datagrid-column-chooser-button', + groupPanel: 'dx-datagrid-group-panel', + searchBox: 'dx-searchbox', + filterPanel: 'dx-datagrid-filter-panel', + filterRangeOverlay: 'dx-datagrid-filter-range-overlay', + filterRangeStartEditor: 'dx-datagrid-filter-range-start', + filterRangeEndEditor: 'dx-datagrid-filter-range-end', + focusOverlay: 'dx-datagrid-focus-overlay', + revertTooltip: 'dx-datagrid-revert-tooltip', + invalidMessage: 'dx-invalid-message', + dialogWrapper: 'dx-dialog-wrapper', + summaryTotal: 'dx-datagrid-summary-item', + button: 'dx-button', + fieldItemContent: 'dx-field-item-content', + textEditorInput: 'dx-texteditor-input', + commandDrag: 'dx-command-drag', + revertButton: 'dx-revert-button', + columnsSeparator: 'dx-datagrid-columns-separator', + toast: 'dx-toast-wrapper', + dragHeader: 'dx-datagrid-drag-header', + sortableDragging: 'dx-sortable-dragging', + pager: 'dx-datagrid-pager', + pagination: 'dx-pagination', + noDataText: 'dx-datagrid-nodata', +} as const; + +export class DataGridHeaders { + readonly element: Locator; + + constructor(container: Locator) { + this.element = container.locator(`.${CLASS.headers}`); + } + + getHeaderRow(index = 0): Locator { + return this.element.locator(`.${CLASS.headerRow}`).nth(index); + } + + getHeaderCell(rowIndex: number, cellIndex: number): Locator { + return this.getHeaderRow(rowIndex).locator('td').nth(cellIndex); + } + + getFilterRow(): Locator { + return this.element.locator(`.${CLASS.filterRow}`); + } + + getFilterCell(columnIndex: number): Locator { + return this.getFilterRow().locator('td').nth(columnIndex); + } +} + +export class DataGridCommandCell { + readonly element: Locator; + + constructor(row: Locator, columnIndex: number) { + this.element = row.locator('td').nth(columnIndex); + } + + getButton(buttonIndex: number): Locator { + return this.element.locator('.dx-link').nth(buttonIndex); + } + + getSelectCheckBox(): Locator { + return this.element.locator('.dx-checkbox-container'); + } +} + +export class DataGridDataRow { + readonly element: Locator; + + constructor(container: Locator, index: number) { + this.element = container.locator(`.${CLASS.dataRow}[aria-rowindex='${index + 1}']`); + } + + getDataCell(columnIndex: number): LocatorWithElement { + return locatorWithElement(this.element.locator('td').nth(columnIndex)); + } + + getCommandCell(columnIndex: number): DataGridCommandCell { + return new DataGridCommandCell(this.element, columnIndex); + } +} + +export class DataGridEditForm { + readonly element: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + + constructor(element: Locator, buttons: Locator) { + this.element = element; + this.saveButton = buttons.first(); + this.cancelButton = buttons.last(); + } +} + +export class DataGridGroupRow { + readonly element: Locator; + + constructor(container: Locator, index: number) { + this.element = container.locator(`.${CLASS.groupRow}`).nth(index); + } + + async isExpanded(): Promise { + return this.element.evaluate( + (el) => el.getAttribute('aria-expanded') === 'true', + ); + } +} + +export class DataGridAdaptiveDetailRow { + readonly element: Locator; + + constructor(container: Locator, index: number) { + this.element = container.locator(`.${CLASS.adaptiveDetailRow}`).nth(index); + } + + getAdaptiveCell(cellIndex: number): LocatorWithElement { + return locatorWithElement(this.element.locator('.dx-field-item-content').nth(cellIndex)); + } +} + +export class DataGridHeaderPanel { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + getAddRowButton(): Locator { + return this.element.locator('.dx-datagrid-addrow-button'); + } + + getSaveButton(): Locator { + return this.element.locator('.dx-datagrid-save-button'); + } + + getCancelButton(): Locator { + return this.element.locator('.dx-datagrid-cancel-button'); + } + + getColumnChooserButton(): Locator { + return this.element.locator('.dx-datagrid-column-chooser-button'); + } + + getDropDownMenuButton(): Locator { + return this.element.locator('.dx-dropdownmenu-button'); + } + + getApplyFilterButton(): Locator { + return this.element.locator('.dx-apply-button'); + } + + getExportButton(): Locator { + return this.element.locator('.dx-datagrid-export-button'); + } +} + +export class DataGridContextMenu { + readonly element: Locator; + + constructor(page: Page) { + this.element = page.locator(`.${CLASS.contextMenu}.dx-datagrid`); + } + + getItemByText(text: string): Locator { + return this.element.locator('.dx-menu-item').filter({ hasText: text }); + } +} + +export class DataGrid { + readonly page: Page; + readonly element: Locator; + readonly selector: string; + + readonly dataRows: Locator; + readonly rowsView: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.dataRows = this.element.locator(`.${CLASS.dataRow}`); + this.rowsView = this.element.locator(`.${CLASS.rowsView}`); + } + + getContainer(): Locator { + return this.element.locator(`.${CLASS.dataGrid}`); + } + + getHeaders(): DataGridHeaders { + return new DataGridHeaders(this.element); + } + + getHeaderRow(index = 0): Locator { + return this.element.locator(`.${CLASS.headers} .${CLASS.headerRow}`).nth(index); + } + + getFilterRow(): Locator { + return this.element.locator(`.${CLASS.headers} .${CLASS.filterRow}`); + } + + getFilterCell(columnIndex: number): Locator { + return this.getFilterRow().locator('td').nth(columnIndex); + } + + getFilterRangeOverlay(): Locator { + return this.element.locator(`.${CLASS.headers}`).locator(`.${CLASS.filterRangeOverlay}`); + } + + getFilterRangeStartEditor(): Locator { + return this.page.locator(`.${CLASS.filterRangeStartEditor}`); + } + + getFilterRangeEndEditor(): Locator { + return this.page.locator(`.${CLASS.filterRangeEndEditor}`); + } + + getFilterPanel(): Locator { + return this.element.locator(`.${CLASS.filterPanel}`); + } + + getRows(): Locator { + return this.rowsView.locator(`.${CLASS.row}`); + } + + getDataRow(index: number): DataGridDataRow { + return new DataGridDataRow(this.element, index); + } + + getDataCell(rowIndex: number, columnIndex: number): LocatorWithElement { + return this.getDataRow(rowIndex).getDataCell(columnIndex); + } + + getFixedDataRow(index: number): DataGridDataRow { + return new DataGridDataRow( + this.element.locator(`.${CLASS.fixedGridView}`), + index, + ); + } + + getFixedDataCell(rowIndex: number, columnIndex: number): LocatorWithElement { + return this.getFixedDataRow(rowIndex).getDataCell(columnIndex); + } + + getGroupRow(index: number): DataGridGroupRow { + return new DataGridGroupRow(this.element, index); + } + + getGroupRowSelector(): Locator { + return this.element.locator(`.${CLASS.groupRow}`); + } + + getFocusedRow(): Locator { + return this.element.locator(`.${CLASS.dataRow}.${CLASS.focusedRow}`); + } + + getErrorRow(): Locator { + return this.element.locator(`.${CLASS.errorRow}`); + } + + getAdaptiveRow(index: number): DataGridAdaptiveDetailRow { + return new DataGridAdaptiveDetailRow(this.element, index); + } + + getAdaptiveButton(nth = 0): Locator { + return this.element.locator(`.${CLASS.adaptiveColumnButton}`).nth(nth); + } + + async isAdaptiveColumnHidden(): Promise { + return this.element.locator(`.${CLASS.adaptiveCommandCellHidden}`).first().isVisible(); + } + + getMasterRow(index: number): Locator { + return this.element.locator(`.${CLASS.masterDetailRow}`).nth(index); + } + + getEditForm(): DataGridEditForm { + const element = this.element.locator(`.${CLASS.editFormRow}`); + const buttons = element.locator(`.${CLASS.formButtonsContainer} .${CLASS.button}`); + return new DataGridEditForm(element, buttons); + } + + getPopupEditForm(): DataGridEditForm { + const element = this.page.locator(`.${CLASS.popupEdit} .${CLASS.overlayContent}`); + const buttons = element.locator(`.${CLASS.toolbar} .${CLASS.button}`); + return new DataGridEditForm(element, buttons); + } + + getFormItemElement(index: number): Locator { + return this.element.locator(`.${CLASS.fieldItemContent}`).nth(index); + } + + getFormItemEditor(index: number): Locator { + return this.getFormItemElement(index).locator(`.${CLASS.textEditorInput}`); + } + + getFooterRow(): Locator { + return this.element.locator(`.${CLASS.footerRow}`); + } + + getGroupFooterRow(): Locator { + return this.element.locator(`.${CLASS.groupFooterRow}`); + } + + getFreeSpaceRow(): Locator { + return this.element.locator(`.${CLASS.freeSpaceRow}`); + } + + getLoadPanel(): { getContent: () => Locator } { + const panel = this.element.locator(`.${CLASS.loadPanel}`); + return { + getContent: () => panel.locator(`.${CLASS.loadPanelContent}`), + }; + } + + getToolbar(): Locator { + return this.element.locator(`.${CLASS.toolbar}`); + } + + getHeaderPanel(): DataGridHeaderPanel { + return new DataGridHeaderPanel(this.element.locator(`.${CLASS.headerPanel}`)); + } + + getGroupPanel(): Locator { + return this.page.locator(`.${CLASS.groupPanel}`); + } + + getColumnChooser(): Locator { + return this.page.locator(`.${CLASS.columnChooser}`).last(); + } + + getColumnChooserButton(): Locator { + return this.element.locator(`.${CLASS.columnChooserButton}`); + } + + getContextMenu(): DataGridContextMenu { + return new DataGridContextMenu(this.page); + } + + getSearchBox(): Locator { + return this.element.locator(`.${CLASS.searchBox}`); + } + + getRevertButton(): Locator { + return this.element.locator(`.${CLASS.revertButton}`); + } + + getRevertTooltip(): Locator { + return this.page.locator('.dx-datagrid-revert-tooltip'); + } + + getInvalidMessageTooltip(): Locator { + return this.page.locator('.dx-invalid-message.dx-invalid-message-always.dx-datagrid-invalid-message'); + } + + getConfirmDeletionButton(): Locator { + return this.page.locator('[aria-label="Yes"]'); + } + + getCancelDeletionButton(): Locator { + return this.page.locator('[aria-label="No"]'); + } + + getDialog(): Locator { + return this.page.locator(`.${CLASS.dialogWrapper}`); + } + + getToast(): Locator { + return this.page.locator(`.${CLASS.toast}`); + } + + getFocusOverlay(): Locator { + return this.page.locator(`.${CLASS.focusOverlay}`); + } + + getDraggableHeader(): Locator { + return this.page.locator(`.${CLASS.dragHeader}`); + } + + getColumnsSeparator(): Locator { + return this.element.locator(`.${CLASS.columnsSeparator}`); + } + + getPager(): Locator { + return this.element.locator(`.${CLASS.pager}, .${CLASS.pagination}`); + } + + getNoDataText(): Locator { + return this.element.locator(`.${CLASS.noDataText}`); + } + + getScrollContainer(): Locator { + return this.rowsView.locator(`.${CLASS.scrollableContainer}`); + } + + getSummaryTotalElement(nth = 0): Locator { + return this.element.locator(`.${CLASS.summaryTotal}`).nth(nth); + } + + getScrollBarThumbTrack(scrollbarPosition: string): Locator { + return this.rowsView.locator(`.dx-scrollbar-${scrollbarPosition.toLowerCase()} .dx-scrollable-scroll`); + } + + async option(name: string, value?: unknown): Promise; + async option(options: Record): Promise; + async option(nameOrOptions: string | Record, value?: unknown): Promise { + const sel = this.selector; + if (typeof nameOrOptions === 'object') { + return this.page.evaluate( + ({ sel: s, opts }) => { + ($(s) as any).dxDataGrid('instance').option(opts); + }, + { sel, opts: nameOrOptions }, + ); + } + if (arguments.length >= 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxDataGrid('instance').option(n, v); + }, + { sel, name: nameOrOptions, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxDataGrid('instance').option(n), + { sel, name: nameOrOptions }, + ); + } + + async repaint(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').repaint(); + }, sel); + } + + async focus(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').focus(); + }, sel); + } + + async scrollTo(options: { x?: number; y?: number; top?: number; left?: number }): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, opts }) => { + ($(s) as any).dxDataGrid('instance').getScrollable().scrollTo(opts); + }, + { s: sel, opts: options }, + ); + } + + async scrollBy(options: { x?: number; y?: number; top?: number; left?: number }): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, opts }) => { + ($(s) as any).dxDataGrid('instance').getScrollable().scrollBy(opts); + }, + { s: sel, opts: options }, + ); + } + + async getScrollLeft(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + return ($(s) as any).dxDataGrid('instance').getScrollable().scrollLeft(); + }, sel); + } + + async getScrollTop(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + return ($(s) as any).dxDataGrid('instance').getScrollable().scrollTop(); + }, sel); + } + + async getScrollWidth(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + return ($(s) as any).dxDataGrid('instance').getScrollable().scrollWidth(); + }, sel); + } + + async getScrollRight(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + const scrollable = ($(s) as any).dxDataGrid('instance').getScrollable(); + return scrollable.scrollWidth() - scrollable.clientWidth() - scrollable.scrollLeft(); + }, sel); + } + + async apiAddRow(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').addRow(); + }, sel); + } + + async apiEditRow(rowIndex: number): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, ri }) => ($(s) as any).dxDataGrid('instance').editRow(ri), + { s: sel, ri: rowIndex }, + ); + } + + async apiDeleteRow(rowIndex: number): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, ri }) => { ($(s) as any).dxDataGrid('instance').deleteRow(ri); }, + { s: sel, ri: rowIndex }, + ); + } + + async apiEditCell(rowIndex: number, columnIndex: number): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, ri, ci }) => { ($(s) as any).dxDataGrid('instance').editCell(ri, ci); }, + { s: sel, ri: rowIndex, ci: columnIndex }, + ); + } + + async apiCellValue(rowIndex: number, columnIndex: number, value: T): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, ri, ci, v }) => { ($(s) as any).dxDataGrid('instance').cellValue(ri, ci, v); }, + { s: sel, ri: rowIndex, ci: columnIndex, v: value }, + ); + } + + async apiGetCellValue(rowIndex: number, columnIndex: number): Promise { + const sel = this.selector; + return this.page.evaluate( + ({ s, ri, ci }) => ($(s) as any).dxDataGrid('instance').cellValue(ri, ci), + { s: sel, ri: rowIndex, ci: columnIndex }, + ); + } + + async apiSaveEditData(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').saveEditData(); + }, sel); + } + + async apiCancelEditData(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').cancelEditData(); + }, sel); + } + + async apiExpandRow(key: unknown): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, k }) => { ($(s) as any).dxDataGrid('instance').expandRow(k); }, + { s: sel, k: key }, + ); + } + + async apiCollapseRow(key: unknown): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, k }) => { ($(s) as any).dxDataGrid('instance').collapseRow(k); }, + { s: sel, k: key }, + ); + } + + async apiExpandAdaptiveDetailRow(key: unknown): Promise { + const sel = this.selector; + const expanded = await this.page.evaluate( + ({ s, k }) => { + const instance = ($(s) as any).dxDataGrid('instance'); + const items = instance.getDataSource().items(); + const storeKey = instance.getDataSource().store().key(); + if (storeKey) { + instance.expandAdaptiveDetailRow(k); + return true; + } + const matchingItemIndex = items.findIndex((item: any) => Object.values(item).includes(k)); + if (matchingItemIndex !== -1) { + const rowKey = items[matchingItemIndex]; + instance.expandAdaptiveDetailRow(rowKey); + return true; + } + return false; + }, + { s: sel, k: key }, + ); + if (!expanded) { + const adaptiveBtn = this.element.locator('.dx-datagrid-adaptive-more').first(); + await adaptiveBtn.click(); + } + } + + async apiExpandAllGroups(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').option('grouping.autoExpandAll', true); + }, sel); + } + + async apiCollapseAllGroups(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').option('grouping.autoExpandAll', false); + }, sel); + } + + async apiExpandAll(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').expandAll(); + }, sel); + } + + async apiFilter(filterExpr: unknown): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, f }) => { ($(s) as any).dxDataGrid('instance').filter(f); }, + { s: sel, f: filterExpr }, + ); + } + + async apiColumnOption(id: string | number, name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length >= 3) { + return this.page.evaluate( + ({ s, id: i, n, v }) => { + ($(s) as any).dxDataGrid('instance').columnOption(i, n, v); + }, + { s: sel, id, n: name, v: value }, + ); + } + return this.page.evaluate( + ({ s, id: i, n }) => ($(s) as any).dxDataGrid('instance').columnOption(i, n), + { s: sel, id, n: name }, + ); + } + + async apiBeginCustomLoading(messageText: string): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, msg }) => { ($(s) as any).dxDataGrid('instance').beginCustomLoading(msg); }, + { s: sel, msg: messageText }, + ); + } + + async apiEndCustomLoading(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').endCustomLoading(); + }, sel); + } + + async apiRefresh(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').refresh().catch(() => {}); + }, sel); + } + + async apiUpdateDimensions(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxDataGrid('instance').updateDimensions(); + }, sel); + } + + async apiPageIndex(pageIndex?: number): Promise { + const sel = this.selector; + if (pageIndex === undefined) { + return this.page.evaluate((s) => { + return ($(s) as any).dxDataGrid('instance').pageIndex(); + }, sel); + } + await this.page.evaluate( + ({ s, pi }) => { ($(s) as any).dxDataGrid('instance').pageIndex(pi); }, + { s: sel, pi: pageIndex }, + ); + } + + async apiNavigateToRow(key: unknown): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, k }) => { ($(s) as any).dxDataGrid('instance').navigateToRow(k); }, + { s: sel, k: key }, + ); + } + + async apiSearchByText(text: string): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, t }) => { ($(s) as any).dxDataGrid('instance').searchByText(t); }, + { s: sel, t: text }, + ); + } + + async apiAddColumn(config: unknown): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, c }) => { ($(s) as any).dxDataGrid('instance').addColumn(c); }, + { s: sel, c: config }, + ); + } + + async apiPush(values: unknown[]): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, v }) => { ($(s) as any).dxDataGrid('instance').getDataSource().store().push(v); }, + { s: sel, v: values }, + ); + } + + async apiGetVisibleRows(): Promise> { + const sel = this.selector; + return this.page.evaluate((s) => { + const instance = ($(s) as any).dxDataGrid('instance'); + return instance.getVisibleRows().map((r: any) => ({ + key: r.key, + data: r.data, + dataIndex: r.dataIndex, + rowType: r.rowType, + rowIndex: r.rowIndex, + })); + }, sel); + } + + async apiGetVisibleColumns(): Promise> { + const sel = this.selector; + return this.page.evaluate((s) => { + const instance = ($(s) as any).dxDataGrid('instance'); + return instance.getVisibleColumns().map((c: any) => ({ + dataField: c.dataField, + name: c.name, + visibleIndex: c.visibleIndex, + })); + }, sel); + } + + async apiGetTopVisibleRowData(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + return ($(s) as any).dxDataGrid('instance').getTopVisibleRowData(); + }, sel); + } + + async isReady(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + return ($(s) as any).dxDataGrid('instance').isReady(); + }, sel); + } + + async hasScrollable(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + return Boolean(($(s) as any).dxDataGrid('instance').getScrollable()); + }, sel); + } + + async getView(viewName: string): Promise { + const sel = this.selector; + return this.page.evaluate( + ({ s, vn }) => ($(s) as any).dxDataGrid('instance').getView(vn), + { s: sel, vn: viewName }, + ); + } + + async moveRow(rowIndex: number, x: number, y: number, isStart = false): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, ri, px, py, start }) => { + const instance = ($(s) as any).dxDataGrid('instance'); + const $row = $(instance.getRowElement(ri)); + let $dragEl = $row.children('.dx-command-drag'); + $dragEl = $dragEl.length ? $dragEl : $row; + const offset = ($dragEl as any).offset(); + if (offset) { + if (start) { + $dragEl.trigger($.Event('dxpointerdown', { + pageX: offset.left, + pageY: offset.top, + pointers: [{ pointerId: 1 }], + })); + } + $dragEl.trigger($.Event('dxpointermove', { + pageX: offset.left + px, + pageY: offset.top + py, + pointers: [{ pointerId: 1 }], + })); + } + }, + { s: sel, ri: rowIndex, px: x, py: y, start: isStart }, + ); + } + + async dropRow(): Promise { + await this.page.evaluate( + (cls) => { + const $dragEl = $(`.${cls}`); + const offset = ($dragEl as any).offset(); + $dragEl.trigger($.Event('dxpointerup', { + pageX: offset.left, + pageY: offset.top, + pointers: [{ pointerId: 1 }], + })); + }, + CLASS.sortableDragging, + ); + } + + async resizeHeader(columnIndex: number, offset: number, needToTriggerPointerUp = true): Promise { + const sel = this.selector; + const headerInfo = await this.page.evaluate( + ({ s, ci }) => { + const instance = ($(s) as any).dxDataGrid('instance'); + const columnsController = instance.getController('columns'); + const visualIndex = columnsController.getVisibleIndex(ci); + const columnHeadersView = instance.getView('columnHeadersView'); + const $header = $(columnHeadersView.getHeaderElement(visualIndex)); + const rect = $header[0].getBoundingClientRect(); + return { + x: rect.right, + y: rect.top + rect.height / 2, + }; + }, + { s: sel, ci: columnIndex }, + ); + + await this.page.mouse.move(headerInfo.x - 2, headerInfo.y); + await this.page.waitForTimeout(100); + await this.page.mouse.down(); + await this.page.mouse.move(headerInfo.x - 2 + offset, headerInfo.y, { steps: 5 }); + if (needToTriggerPointerUp) { + await this.page.mouse.up(); + } + } + + async moveHeader(columnIndex: number, x: number, y: number, isStart = false): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, ci, px, py, start }) => { + const instance = ($(s) as any).dxDataGrid('instance'); + const columnHeadersView = instance.getView('columnHeadersView'); + const $header = $(columnHeadersView.getHeaderElement(ci)); + const offset = ($header as any).offset(); + if (offset) { + if (start) { + $header.trigger($.Event('dxpointerdown', { + pageX: offset.left, + pageY: offset.top, + pointers: [{ pointerId: 1 }], + })); + } + $header.trigger($.Event('dxpointermove', { + pageX: offset.left + px, + pageY: offset.top + py, + pointers: [{ pointerId: 1 }], + })); + } + }, + { s: sel, ci: columnIndex, px: x, py: y, start: isStart }, + ); + } + + async dropHeader(columnIndex: number): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ s, ci }) => { + const instance = ($(s) as any).dxDataGrid('instance'); + const columnHeadersView = instance.getView('columnHeadersView'); + const $header = $(columnHeadersView.getHeaderElement(ci)); + const headerOffset = ($header as any).offset(); + $(document).trigger($.Event('dxpointerup', { + pageX: headerOffset.left, + pageY: headerOffset.top, + pointers: [{ pointerId: 1 }], + })); + }, + { s: sel, ci: columnIndex }, + ); + } + + async apiShowErrorToast(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + const instance = ($(s) as any).dxDataGrid('instance'); + instance.getController('errorHandling').showToastError('Error'); + }, sel); + } + + async isFocusedRowInViewport(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + const instance = ($(s) as any).dxDataGrid('instance'); + const rowsViewElement = instance.getView('rowsView').element(); + const rowsViewRect = rowsViewElement[0].getBoundingClientRect(); + const rowElement = rowsViewElement.find('.dx-row-focused'); + if (rowElement?.length) { + const elementRect = rowElement[0].getBoundingClientRect(); + return elementRect.top >= rowsViewRect.top && elementRect.bottom <= rowsViewRect.bottom; + } + return false; + }, sel); + } + + async isVirtualRowIntersectViewport(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + const instance = ($(s) as any).dxDataGrid('instance'); + const rowsViewElement = instance.getView('rowsView').element(); + const rowsViewRect = rowsViewElement[0].getBoundingClientRect(); + const virtualRowElements = rowsViewElement.find('.dx-virtual-row'); + for (let i = 0; i < virtualRowElements.length; i += 1) { + const elRect = virtualRowElements[i].getBoundingClientRect(); + if ((elRect.top > rowsViewRect.top && elRect.top < rowsViewRect.bottom) + || (elRect.bottom > rowsViewRect.top && elRect.bottom < rowsViewRect.bottom) + || (elRect.top <= rowsViewRect.top && elRect.bottom >= rowsViewRect.bottom)) { + return true; + } + } + return false; + }, sel); + } + + async getFilterEditor(columnIndex: number): Promise { + return this.getFilterCell(columnIndex).locator(`.${CLASS.textEditorInput}`); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/dateRangeBox.ts b/e2e/testcafe-devextreme/playwright-helpers/dateRangeBox.ts new file mode 100644 index 000000000000..10caeb5994a4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/dateRangeBox.ts @@ -0,0 +1,308 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + popup: 'dx-popup', + calendar: 'dx-calendar', + calendarCell: 'dx-calendar-cell', + calendarWidget: 'dx-widget', + calendarViewsWrapper: 'dx-calendar-views-wrapper', + cellInRange: 'dx-calendar-cell-in-range', + cellInRangeStart: 'dx-calendar-range-start-date', + cellInRangeEnd: 'dx-calendar-range-end-date', + cellInHoveredRange: 'dx-calendar-cell-range-hover', + cellInHoveredRangeStart: 'dx-calendar-cell-range-hover-start', + cellInHoveredRangeEnd: 'dx-calendar-cell-range-hover-end', + otherMonth: 'dx-calendar-other-month', + startDateDateBox: 'dx-start-datebox', + endDateDateBox: 'dx-end-datebox', + dropDownButton: 'dx-dropdowneditor-button', + clearButton: 'dx-clear-button-area', + buttonsContainer: 'dx-texteditor-buttons-container', + separator: 'dx-daterangebox-separator', + input: 'dx-texteditor-input', + focused: 'dx-state-focused', + doneButton: 'dx-popup-done', + cancelButton: 'dx-popup-cancel', + todayButton: 'dx-button-today', + navigatorNextView: 'dx-calendar-navigator-next-view', + navigatorPrevView: 'dx-calendar-navigator-previous-view', + navigatorCaption: 'dx-calendar-caption-button', + button: 'dx-button', +} as const; + +function serializeDateToCalendarFormat(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}/${m}/${d}`; +} + +export class CalendarViewHelper { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + getCellByDate(date: Date): Locator { + const dateStr = serializeDateToCalendarFormat(date); + return this.element.locator(`td[data-value='${dateStr}']`); + } +} + +export class CalendarHelper { + readonly element: Locator; + private readonly page: Page; + + constructor(page: Page, element: Locator) { + this.page = page; + this.element = element; + } + + getSelectedRangeCells(): Locator { + return this.element.locator(`.${CLASS.cellInRange}`); + } + + getSelectedRangeStartCell(): Locator { + return this.element.locator(`.${CLASS.cellInRangeStart}:not(.${CLASS.otherMonth})`); + } + + getSelectedRangeEndCell(): Locator { + return this.element.locator(`.${CLASS.cellInRangeEnd}`); + } + + getHoveredRangeCells(): Locator { + return this.element.locator(`.${CLASS.cellInHoveredRange}`); + } + + getHoveredRangeStartCell(): Locator { + return this.element.locator(`.${CLASS.cellInHoveredRangeStart}`); + } + + getHoveredRangeEndCell(): Locator { + return this.element.locator(`.${CLASS.cellInHoveredRangeEnd}`); + } + + getCellByDate(dateStr: string): Locator { + return this.element.locator(`[data-value="${dateStr}"]:not(.${CLASS.otherMonth})`); + } + + getView(): CalendarViewHelper { + const viewEl = this.element.locator(`.${CLASS.calendarViewsWrapper} .${CLASS.calendarWidget}`).first(); + return new CalendarViewHelper(viewEl); + } + + async option(name: string, value?: unknown): Promise { + const elementHandle = await this.element.elementHandle(); + if (!elementHandle) throw new Error('Calendar element not found'); + if (value !== undefined) { + return this.page.evaluate( + ({ el, name: n, value: v }) => { + const instance = (window as any).DevExpress.ui.dxCalendar.getInstance(el); + if (instance) instance.option(n, v); + }, + { el: elementHandle, name, value }, + ); + } + return this.page.evaluate( + ({ el, name: n }) => { + const instance = (window as any).DevExpress.ui.dxCalendar.getInstance(el); + return instance ? instance.option(n) : undefined; + }, + { el: elementHandle, name }, + ); + } +} + +export class DateBoxHelper { + readonly element: Locator; + readonly input: Locator; + + constructor(private readonly page: Page, locator: Locator) { + this.element = locator; + this.input = locator.locator(`.${CLASS.input}`); + } + + async isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } + + async option(name: string, value?: unknown): Promise { + const elementHandle = await this.element.elementHandle(); + if (!elementHandle) throw new Error('DateBox element not found'); + if (arguments.length === 2) { + return this.page.evaluate( + ({ el, name: n, value: v }) => { + const instance = (window as any).DevExpress.ui.dxDateBox.getInstance(el); + if (instance) instance.option(n, v); + }, + { el: elementHandle, name, value }, + ); + } + return this.page.evaluate( + ({ el, name: n }) => { + const instance = (window as any).DevExpress.ui.dxDateBox.getInstance(el); + return instance ? instance.option(n) : undefined; + }, + { el: elementHandle, name }, + ); + } +} + +export class DateRangeBoxPopup { + readonly page: Page; + readonly container: Locator; + + constructor(page: Page, container: Locator) { + this.page = page; + this.container = container; + } + + private getWrapper(): Locator { + return this.page.locator('.dx-popup-wrapper'); + } + + getApplyButton(): { element: Locator; isFocused: () => Promise } { + const el = this.getWrapper().locator(`.${CLASS.button}.${CLASS.doneButton}`); + return { + element: el, + isFocused: () => el.evaluate((e, cls) => e.classList.contains(cls), CLASS.focused), + }; + } + + getCancelButton(): { element: Locator; isFocused: () => Promise } { + const el = this.getWrapper().locator(`.${CLASS.button}.${CLASS.cancelButton}`); + return { + element: el, + isFocused: () => el.evaluate((e, cls) => e.classList.contains(cls), CLASS.focused), + }; + } + + getTodayButton(): { element: Locator; isFocused: () => Promise } { + const el = this.getWrapper().locator(`.${CLASS.todayButton}`); + return { + element: el, + isFocused: () => el.evaluate((e, cls) => e.classList.contains(cls), CLASS.focused), + }; + } + + getNavigatorPrevButton(): { element: Locator; isFocused: () => Promise } { + const el = this.getWrapper().locator(`.${CLASS.navigatorPrevView}`); + return { + element: el, + isFocused: () => el.evaluate((e, cls) => e.classList.contains(cls), CLASS.focused), + }; + } + + getNavigatorNextButton(): { element: Locator; isFocused: () => Promise } { + const el = this.getWrapper().locator(`.${CLASS.navigatorNextView}`); + return { + element: el, + isFocused: () => el.evaluate((e, cls) => e.classList.contains(cls), CLASS.focused), + }; + } + + getNavigatorCaption(): { element: Locator; isFocused: () => Promise } { + const el = this.getWrapper().locator(`.${CLASS.navigatorCaption}`); + return { + element: el, + isFocused: () => el.evaluate((e, cls) => e.classList.contains(cls), CLASS.focused), + }; + } + + getViewsWrapper(): { element: Locator; isFocused: () => Promise } { + const el = this.getWrapper().locator('.dx-calendar-views-wrapper'); + return { + element: el, + isFocused: () => el.evaluate((e) => e === document.activeElement || e.contains(document.activeElement)), + }; + } +} + +export class DateRangeBox { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + readonly dropDownButton: Locator; + readonly clearButton: Locator; + readonly separator: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.dropDownButton = this.element.locator(`.${CLASS.dropDownButton}`); + this.separator = this.element.locator(`.${CLASS.separator}`); + this.clearButton = this.element + .locator(`.${CLASS.buttonsContainer}`) + .locator(`.${CLASS.clearButton}`); + } + + async isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } + + getStartDateBox(): DateBoxHelper { + return new DateBoxHelper( + this.page, + this.element.locator(`.${CLASS.startDateDateBox}`), + ); + } + + getEndDateBox(): DateBoxHelper { + return new DateBoxHelper( + this.page, + this.element.locator(`.${CLASS.endDateDateBox}`), + ); + } + + getPopup(): DateRangeBoxPopup { + return new DateRangeBoxPopup(this.page, this.element); + } + + getCalendar(): CalendarHelper { + const calendarLocator = this.page.locator(`.${CLASS.calendar}`); + return new CalendarHelper(this.page, calendarLocator); + } + + getCalendarCell(index: number): Locator { + return this.page.locator(`.${CLASS.calendar} .${CLASS.calendarCell}`).nth(index); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxDateRangeBox('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => { + const result = ($(s) as any).dxDateRangeBox('instance').option(n); + if (Array.isArray(result)) { + return result.map((v: unknown) => (v instanceof Date ? v.toISOString() : v)); + } + return result instanceof Date ? result.toISOString() : result; + }, + { sel, name }, + ); + } + + async focus(): Promise { + await this.page.evaluate( + (sel) => { + ($(sel) as any).dxDateRangeBox('instance').focus(); + }, + this.selector, + ); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts new file mode 100644 index 000000000000..4a83da30457f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts @@ -0,0 +1,138 @@ +import type { Page } from '@playwright/test'; + +export async function setAttribute( + page: Page, + selector: string, + attribute: string, + value: string, +): Promise { + await page.evaluate(({ sel, attr, val }) => { + document.querySelector(sel)?.setAttribute(attr, val); + }, { sel: selector, attr: attribute, val: value }); +} + +export async function getStyleAttribute(page: Page, selector: string): Promise { + return page.evaluate( + (sel) => document.querySelector(sel)?.getAttribute('style') ?? '', + selector, + ); +} + +export async function setStyleAttribute( + page: Page, + selector: string, + styleValue: string, +): Promise { + await page.evaluate(({ sel, style }) => new Promise((resolve) => { + const element = document.querySelector(sel); + const styles = element?.getAttribute('style') ?? ''; + element?.setAttribute('style', `${styles} ${style}`); + window.dispatchEvent(new Event('resize')); + requestAnimationFrame(() => resolve()); + }), { sel: selector, style: styleValue }); +} + +export async function insertStylesheetRulesToPage( + page: Page, + rules: string, +): Promise { + await page.evaluate((css) => { + const styleTag = document.createElement('style'); + styleTag.setAttribute('data-playwright-style', 'true'); + styleTag.textContent = css; + document.head.appendChild(styleTag); + }, rules); +} + +export async function removeStylesheetRulesFromPage(page: Page): Promise { + await page.evaluate(() => { + document.querySelectorAll('style[data-playwright-style]').forEach((el) => el.remove()); + }); +} + +export async function appendElementTo( + page: Page, + parentSelector: string, + childSelector: string, + idOrStyles?: string | Record, + additionalStyles?: Record, +): Promise { + const id = typeof idOrStyles === 'string' ? idOrStyles : undefined; + const styles = additionalStyles ?? (typeof idOrStyles === 'object' ? idOrStyles : undefined); + await page.evaluate(({ parent, tag, elemId, elemStyles }) => { + const el = document.createElement(tag); + if (elemId) el.setAttribute('id', elemId); + if (elemStyles) { + Object.entries(elemStyles).forEach(([key, val]) => { + if (key === 'id') { + el.setAttribute('id', val as string); + } else { + (el.style as any)[key] = val; + } + }); + } + document.querySelector(parent)?.appendChild(el); + }, { + parent: parentSelector, + tag: childSelector, + elemId: id, + elemStyles: styles, + }); +} + +export async function setClassAttribute( + page: Page, + selector: string, + className: string, +): Promise { + await page.evaluate(({ sel, cls }) => { + const el = document.querySelector(sel); + if (el) { + const existing = el.getAttribute('class') ?? ''; + el.setAttribute('class', `${existing} ${cls}`.trim()); + } + }, { sel: selector, cls: className }); +} + +export async function removeAttribute( + page: Page, + selector: string, + attribute: string, +): Promise { + await page.evaluate(({ sel, attr }) => { + document.querySelector(sel)?.removeAttribute(attr); + }, { sel: selector, attr: attribute }); +} + +export async function addFocusableElementBefore( + page: Page, + targetSelector: string, + elementId = 'focusable-start', +): Promise { + await page.evaluate(({ target, id }) => { + const existing = document.getElementById(id); + existing?.remove(); + const targetEl = document.querySelector(target); + const button = document.createElement('button'); + button.id = id; + button.textContent = 'Start'; + button.style.position = 'fixed'; + button.style.top = '0'; + button.style.left = '0'; + button.style.zIndex = '-1'; + button.style.opacity = '0'; + targetEl?.parentElement?.insertBefore(button, targetEl); + }, { target: targetSelector, id: elementId }); +} + +export async function addCaptionTo( + page: Page, + selector: string, + caption: string, + where: InsertPosition = 'beforebegin', +): Promise { + await page.evaluate(({ sel, cap, pos }) => { + const element = document.querySelector(sel); + element?.insertAdjacentText(pos as InsertPosition, cap); + }, { sel: selector, cap: caption, pos: where }); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts b/e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts new file mode 100644 index 000000000000..65d9808c7886 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts @@ -0,0 +1,28 @@ +type OptionMatrix = { + [K in keyof T]: T[K][]; +}; + +export function generateOptionMatrix>( + matrix: OptionMatrix, +): T[] { + const keys = Object.keys(matrix) as (keyof T)[]; + const combinations: T[] = []; + + function generate(index: number, current: Partial): void { + if (index === keys.length) { + combinations.push({ ...current } as T); + return; + } + + const key = keys[index]; + const values = matrix[key]; + + for (const value of values) { + current[key] = value; + generate(index + 1, current); + } + } + + generate(0, {}); + return combinations; +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/htmlEditor.ts b/e2e/testcafe-devextreme/playwright-helpers/htmlEditor.ts new file mode 100644 index 000000000000..1594db750b13 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/htmlEditor.ts @@ -0,0 +1,263 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + toolbar: 'dx-htmleditor-toolbar', + content: 'dx-htmleditor-content', + addImagePopup: 'dx-htmleditor-add-image-popup', + formDialog: 'dx-formdialog', + form: 'dx-formdialog-form', + popup: 'dx-popup', + popupContent: 'dx-popup-content', + popupBottom: 'dx-popup-bottom', + overlayContent: 'dx-overlay-content', + tabs: 'dx-tabs', + tabItem: 'dx-tab', + button: 'dx-button', + buttonGroup: 'dx-buttongroup', + textBox: 'dx-textbox', + textEditorInput: 'dx-texteditor-input', + fileUploader: 'dx-fileuploader', + fileUploaderInput: 'dx-fileuploader-input', + fileUploaderFile: 'dx-fileuploader-file', + fileUploaderFileName: 'dx-fileuploader-file-name', + fileUploaderFileSize: 'dx-fileuploader-file-size', + fileUploaderFileStatusMessage: 'dx-fileuploader-file-status-message', + fileUploaderFileCancelButton: 'dx-fileuploader-cancel-button', + fileUploaderValidationMessage: 'dx-fileuploader-file-status-message', + invalidState: 'dx-invalid', + stateDisabled: 'dx-state-disabled', +} as const; + +type ToolbarItemName = 'image' | 'color' | 'link' | 'ai'; + +export class HtmlEditorDialogFooterToolbar { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + get addButton(): HtmlEditorButton { + return new HtmlEditorButton(this.element.locator(`.${CLASS.button}`).nth(0)); + } + + get cancelButton(): HtmlEditorButton { + return new HtmlEditorButton(this.element.locator(`.${CLASS.button}`).nth(1)); + } +} + +export class HtmlEditorButton { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + get isDisabled(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.stateDisabled, + ); + } + + get text(): Promise { + return this.element.textContent().then((t) => t ?? ''); + } +} + +export class HtmlEditorDialogTabs { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + getItem(index: number): { element: Locator } { + return { element: this.element.locator(`.${CLASS.tabItem}`).nth(index) }; + } +} + +export class HtmlEditorAddImageUrlForm { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + get url(): HtmlEditorTextBox { + return new HtmlEditorTextBox(this.element.locator(`.${CLASS.textBox}`).first()); + } + + get lockButton(): HtmlEditorButton { + return new HtmlEditorButton( + this.element.locator(`.${CLASS.buttonGroup} .${CLASS.button}`).first(), + ); + } +} + +export class HtmlEditorTextBox { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + get isInvalid(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.invalidState, + ); + } + + get input(): Locator { + return this.element.locator(`.${CLASS.textEditorInput}`); + } +} + +export class HtmlEditorFileUploader { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + get input(): Locator { + return this.element.locator('input[type="file"]'); + } + + get fileCount(): Promise { + return this.element.locator(`.${CLASS.fileUploaderFile}`).count(); + } + + getFile(index = 0): HtmlEditorFileUploaderFile { + return new HtmlEditorFileUploaderFile( + this.element.locator(`.${CLASS.fileUploaderFile}`).nth(index), + ); + } +} + +export class HtmlEditorFileUploaderFile { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + get fileName(): Promise { + return this.element.locator(`.${CLASS.fileUploaderFileName}`).textContent() + .then((t) => t ?? ''); + } + + get fileSize(): Promise { + return this.element.locator(`.${CLASS.fileUploaderFileSize}`).textContent() + .then((t) => t ?? ''); + } + + get statusMessage(): Promise { + return this.element.locator(`.${CLASS.fileUploaderFileStatusMessage}`).textContent() + .then((t) => t ?? ''); + } + + get validationMessage(): Promise { + return this.element.locator(`.${CLASS.fileUploaderValidationMessage}`).textContent() + .then((t) => t ?? ''); + } + + get cancelButton(): HtmlEditorButton { + return new HtmlEditorButton( + this.element.locator('xpath=..').locator(`.${CLASS.fileUploaderFileCancelButton}`), + ); + } +} + +export class HtmlEditorAddImageFileForm { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + get fileUploader(): HtmlEditorFileUploader { + return new HtmlEditorFileUploader(this.element.locator(`.${CLASS.fileUploader}`)); + } +} + +export class HtmlEditorDialog { + readonly page: Page; + readonly element: Locator; + + constructor(page: Page) { + this.page = page; + this.element = page.locator(`.dx-overlay-wrapper.${CLASS.formDialog}`); + } + + get footerToolbar(): HtmlEditorDialogFooterToolbar { + return new HtmlEditorDialogFooterToolbar(this.element.locator(`.${CLASS.popupBottom}`)); + } + + get tabs(): HtmlEditorDialogTabs { + return new HtmlEditorDialogTabs(this.element.locator(`.${CLASS.tabs}`)); + } + + get addImageUrlForm(): HtmlEditorAddImageUrlForm { + return new HtmlEditorAddImageUrlForm(this.element.locator(`.${CLASS.form}`)); + } + + get addImageFileForm(): HtmlEditorAddImageFileForm { + return new HtmlEditorAddImageFileForm(this.element.locator(`.${CLASS.form}`)); + } +} + +export class HtmlEditorToolbar { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + getItemByName(itemName: ToolbarItemName): Locator { + return this.element.locator(`.dx-${itemName}-format`).locator('..').locator('..'); + } +} + +export class HtmlEditor { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + readonly toolbar: HtmlEditorToolbar; + readonly dialog: HtmlEditorDialog; + readonly content: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.toolbar = new HtmlEditorToolbar(this.element.locator(`.${CLASS.toolbar}`)); + this.dialog = new HtmlEditorDialog(page); + this.content = this.element.locator(`.${CLASS.content}`); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxHtmlEditor('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxHtmlEditor('instance').option(n), + { sel, name }, + ); + } + + async focus(): Promise { + const sel = this.selector; + await this.page.evaluate( + (s) => { ($(s) as any).dxHtmlEditor('instance').focus(); }, + sel, + ); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/index.ts b/e2e/testcafe-devextreme/playwright-helpers/index.ts new file mode 100644 index 000000000000..a3afedea2dc6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/index.ts @@ -0,0 +1,114 @@ +export { createWidget } from './createWidget'; +export { + changeTheme, + getCurrentTheme, + getFullThemeName, + getThemePostfix, + isFluent, + isMaterial, + isMaterialBased, + testScreenshot, +} from './themeUtils'; +export { + addCaptionTo, + addFocusableElementBefore, + appendElementTo, + getStyleAttribute, + insertStylesheetRulesToPage, + removeAttribute, + removeStylesheetRulesFromPage, + setAttribute, + setClassAttribute, + setStyleAttribute, +} from './domUtils'; +export { + adjustViewportForContent, + clearTestPage, + getContainerUrl, + setupTestPage, +} from './testPageUtils'; +export { generateOptionMatrix } from './generateOptionMatrix'; +export { a11yCheck, testAccessibility, testAccessibilityMatrix } from './accessibility'; +export type { A11yCheckOptions, TestAccessibilityConfig, MatrixAccessibilityConfig } from './accessibility'; +export { + Scheduler, + SchedulerAppointment, + SchedulerAppointmentPopup, + SchedulerAppointmentTooltip, + SchedulerAppointmentDialog, + SchedulerToolbar, + SchedulerNavigator, + SchedulerTooltipListItem, +} from './scheduler'; +export { + DataGrid, + DataGridHeaders, + DataGridDataRow, + DataGridCommandCell, + DataGridEditForm, + DataGridGroupRow, + DataGridAdaptiveDetailRow, + DataGridContextMenu, + DataGridHeaderPanel, +} from './dataGrid'; +export { Widget } from './widget'; +export { Menu } from './menu'; +export { + TreeList, + TreeListDataRow, + ExpandableCell, +} from './treeList'; +export { + PivotGrid, + PivotGridHeaderArea, + PivotGridFieldPanel, + HeaderFilter, + HeaderFilterList, + HeaderFilterSelectAll, +} from './pivotGrid'; +export { + Scrollable, + ScrollView, +} from './scrollable'; +export { + DateRangeBox, + DateRangeBoxPopup, + DateBoxHelper, + CalendarHelper, + CalendarViewHelper, +} from './dateRangeBox'; +export { Chat } from './chat'; +export { + TabPanel, + TabsHelper, + MultiViewHelper, + TabItem, + MultiViewItem, +} from './tabPanel'; +export { + HtmlEditor, + HtmlEditorToolbar, + HtmlEditorDialog, + HtmlEditorDialogFooterToolbar, + HtmlEditorDialogTabs, + HtmlEditorAddImageUrlForm, + HtmlEditorAddImageFileForm, + HtmlEditorButton, + HtmlEditorTextBox, + HtmlEditorFileUploader, + HtmlEditorFileUploaderFile, +} from './htmlEditor'; +export { + List, + ListItem, + ListGroup, + ListItemCheckBox, + ListItemRadioButton, +} from './list'; +export { + Toolbar, + ToolbarDropDownMenu, + ToolbarDropDownMenuPopup, +} from './toolbar'; +export { SelectBox } from './selectBox'; +export { Lookup } from './lookup'; diff --git a/e2e/testcafe-devextreme/playwright-helpers/list.ts b/e2e/testcafe-devextreme/playwright-helpers/list.ts new file mode 100644 index 000000000000..99cc5f4906d7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/list.ts @@ -0,0 +1,216 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + item: 'dx-list-item', + group: 'dx-list-group', + groupHeader: 'dx-list-group-header', + search: 'dx-list-search', + selectAllItem: 'dx-list-select-all', + invisible: 'dx-state-invisible', + focused: 'dx-state-focused', + selected: 'dx-list-item-selected', + hovered: 'dx-state-hover', + disabled: 'dx-state-disabled', + checkbox: 'dx-checkbox', + checkboxChecked: 'dx-checkbox-checked', + checkboxIndeterminate: 'dx-checkbox-indeterminate', + radioButton: 'dx-radiobutton', + radioButtonChecked: 'dx-radiobutton-checked', + reorderHandle: 'dx-list-reorder-handle', + nestedItem: 'nested-item', +} as const; + +export class ListItemCheckBox { + readonly element: Locator; + + constructor(itemElement: Locator) { + this.element = itemElement.locator(`.${CLASS.checkbox}`); + } + + get isChecked(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.checkboxChecked, + ); + } + + get isFocused(): Promise { + return this.element.evaluate( + (el, focusCls) => { + let node: Element | null = el; + while (node) { + if (node.classList.contains(focusCls)) return true; + if (node.classList.contains('dx-list-item') || node.classList.contains('dx-list-select-all')) break; + node = node.parentElement; + } + return false; + }, + CLASS.focused, + ); + } + + get isIndeterminate(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.checkboxIndeterminate, + ); + } +} + +export class ListItemRadioButton { + readonly element: Locator; + + constructor(itemElement: Locator) { + this.element = itemElement.locator(`.${CLASS.radioButton}`); + } + + get isChecked(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.radioButtonChecked, + ); + } + + get isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } +} + +export class ListItem { + readonly element: Locator; + readonly checkBox: ListItemCheckBox; + readonly radioButton: ListItemRadioButton; + readonly reorderHandle: Locator; + + constructor(element: Locator) { + this.element = element; + this.checkBox = new ListItemCheckBox(element); + this.radioButton = new ListItemRadioButton(element); + this.reorderHandle = element.locator(`.${CLASS.reorderHandle}`); + } + + get isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } + + get isSelected(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.selected, + ); + } + + get isHovered(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.hovered, + ); + } + + get isDisabled(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.disabled, + ); + } + + get text(): Promise { + return this.element.textContent().then((t) => t ?? ''); + } +} + +export class ListGroup { + readonly element: Locator; + readonly header: Locator; + readonly items: Locator; + + constructor(element: Locator) { + this.element = element; + this.header = element.locator(`.${CLASS.groupHeader}`); + this.items = element.locator(`.${CLASS.item}:not(.${CLASS.nestedItem})`); + } + + getItem(index = 0): ListItem { + return new ListItem(this.items.nth(index)); + } +} + +export class List { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + readonly searchInput: Locator; + readonly selectAll: ListItem; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.searchInput = this.element.locator(`.${CLASS.search} input`); + this.selectAll = new ListItem(this.element.locator(`.${CLASS.selectAllItem}`)); + } + + getItem(index = 0): ListItem { + return new ListItem(this.getItems().nth(index)); + } + + getItems(): Locator { + return this.element.locator(`.${CLASS.item}:not(.${CLASS.nestedItem})`); + } + + getVisibleItems(): Locator { + return this.element.locator(`.${CLASS.item}:not(.${CLASS.invisible})`); + } + + getGroup(index = 0): ListGroup { + return new ListGroup(this.element.locator(`.${CLASS.group}`).nth(index)); + } + + async scrollTo(value: number): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ sel: s, val }) => { + ($(s) as any).dxList('instance').scrollTo(val); + }, + { sel, val: value }, + ); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxList('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxList('instance').option(n), + { sel, name }, + ); + } + + async focus(): Promise { + const sel = this.selector; + await this.page.evaluate( + (s) => { ($(s) as any).dxList('instance').focus(); }, + sel, + ); + } + + async repaint(): Promise { + const sel = this.selector; + await this.page.evaluate( + (s) => { ($(s) as any).dxList('instance').repaint(); }, + sel, + ); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/lookup.ts b/e2e/testcafe-devextreme/playwright-helpers/lookup.ts new file mode 100644 index 000000000000..bd651e2f2c99 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/lookup.ts @@ -0,0 +1,87 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + inputField: 'dx-lookup-field', + list: 'dx-list', + focused: 'dx-state-focused', + invisible: 'dx-state-invisible', + popupWrapper: 'dx-popup-wrapper', + overlayContent: 'dx-overlay-content', + popupContent: 'dx-popup-content', + search: 'dx-lookup-search', +} as const; + +const ATTR = { + popupId: 'aria-owns', +} as const; + +export class Lookup { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + readonly field: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.field = this.element.locator(`.${CLASS.inputField}`); + } + + async open(): Promise { + const sel = this.selector; + await this.page.evaluate( + (s) => { ($(s) as any).dxLookup('instance').open(); }, + sel, + ); + } + + async isOpened(): Promise { + const sel = this.selector; + return this.page.evaluate( + (s) => { + const instance = ($(s) as any).dxLookup('instance'); + const popup = instance._popup; + return popup ? popup.option('visible') : false; + }, + sel, + ); + } + + async getPopup(): Promise { + return this.page.locator(`.${CLASS.popupWrapper}`); + } + + async getList(): Promise { + const popup = await this.getPopup(); + return popup.locator(`.${CLASS.list}`); + } + + getSearchInput(): Locator { + return this.page.locator(`.${CLASS.search} input`); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxLookup('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxLookup('instance').option(n), + { sel, name }, + ); + } + + async focus(): Promise { + const sel = this.selector; + await this.page.evaluate( + (s) => { ($(s) as any).dxLookup('instance').focus(); }, + sel, + ); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/menu.ts b/e2e/testcafe-devextreme/playwright-helpers/menu.ts new file mode 100644 index 000000000000..5d212d0dc5a8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/menu.ts @@ -0,0 +1,34 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + menu: 'dx-menu', + item: 'dx-menu-item', + adaptiveItem: 'dx-treeview-item', + contextMenu: 'dx-context-menu', + hamburgerButton: 'dx-menu-hamburger-button', +} as const; + +export class Menu { + readonly page: Page; + readonly element: Locator; + readonly items: Locator; + + constructor(page: Page, adaptivityEnabled = false) { + this.page = page; + const itemClass = adaptivityEnabled ? CLASS.adaptiveItem : CLASS.item; + this.element = page.locator(`.${CLASS.menu}`); + this.items = page.locator(`.${itemClass}`).filter({ hasNot: page.locator('[style*="display: none"]') }); + } + + getItem(index: number): Locator { + return this.items.nth(index); + } + + getHamburgerButton(): Locator { + return this.element.locator(`.${CLASS.hamburgerButton}`); + } + + async isElementFocused(element: Locator): Promise { + return element.evaluate((el) => el.classList.contains('dx-state-focused')); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/pivotGrid.ts b/e2e/testcafe-devextreme/playwright-helpers/pivotGrid.ts new file mode 100644 index 000000000000..a8849e9326d0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/pivotGrid.ts @@ -0,0 +1,188 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + pivotGrid: 'dx-pivotgrid', + fieldArea: 'dx-pivotgrid-area', + fieldAreaColumn: 'dx-pivotgrid-horizontal-headers', + fieldAreaRow: 'dx-pivotgrid-vertical-headers', + fieldAreaData: 'dx-pivotgrid-area-data', + headerFilterIcon: 'dx-header-filter', + scrollable: 'dx-scrollable', + fieldPanel: 'dx-pivotgridfieldchooser-container', + fieldChooser: 'dx-pivotgridfieldchooser', + fieldChooserButton: 'dx-pivotgrid-field-chooser-button', + sortUpIcon: 'dx-sort-up', + sortDownIcon: 'dx-sort-down', + fieldPanelColumn: 'dx-area-column-cell', + fieldPanelRow: 'dx-area-row-cell', + fieldPanelData: 'dx-area-data-cell', + fieldPanelFilter: 'dx-area-filter-cell', + fieldItem: 'dx-area-field', +} as const; + +export class PivotGrid { + readonly page: Page | null; + readonly selector: string; + readonly element: Locator | string; + + constructor(pageOrSelector: Page | string, selector = '#container') { + if (typeof pageOrSelector === 'string') { + this.page = null; + this.selector = pageOrSelector; + this.element = pageOrSelector; + } else { + this.page = pageOrSelector; + this.selector = selector; + this.element = pageOrSelector.locator(selector); + } + } + + private getPage(): Page { + if (!this.page) { + throw new Error('PivotGrid: page is required for this operation'); + } + return this.page; + } + + private getLocator(): Locator { + if (typeof this.element === 'string') { + throw new Error('PivotGrid: page is required for this operation'); + } + return this.element; + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + const page = this.getPage(); + if (arguments.length === 2) { + return page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxPivotGrid('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxPivotGrid('instance').option(n), + { sel, name }, + ); + } + + getColumnHeaderArea(): PivotGridHeaderArea { + return new PivotGridHeaderArea(this.getLocator().locator(`.${CLASS.fieldAreaColumn}`)); + } + + getRowHeaderArea(): PivotGridHeaderArea { + return new PivotGridHeaderArea(this.getLocator().locator(`.${CLASS.fieldAreaRow}`)); + } + + getDataArea(): Locator { + return this.getLocator().locator(`.${CLASS.fieldAreaData}`); + } + + getFieldPanel(): PivotGridFieldPanel { + return new PivotGridFieldPanel(this.getPage(), this.getLocator()); + } + + getFieldChooserButton(): Locator { + return this.getLocator().locator(`.${CLASS.fieldChooserButton}`); + } +} + +export class PivotGridHeaderArea { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + getHeaderFilterIcon(index = 0): Locator { + return this.element.locator(`.${CLASS.headerFilterIcon}`).nth(index); + } + + getHeaderFilterScrollable(): Locator { + return this.element.locator(`.${CLASS.scrollable}`); + } + + getSortUpIcon(index = 0): Locator { + return this.element.locator(`.${CLASS.sortUpIcon}`).nth(index); + } + + getSortDownIcon(index = 0): Locator { + return this.element.locator(`.${CLASS.sortDownIcon}`).nth(index); + } +} + +export class PivotGridFieldPanel { + readonly page: Page; + readonly element: Locator; + + constructor(page: Page, container: Locator) { + this.page = page; + this.element = container; + } + + getColumnArea(): Locator { + return this.element.locator(`.${CLASS.fieldPanelColumn}`); + } + + getRowArea(): Locator { + return this.element.locator(`.${CLASS.fieldPanelRow}`); + } + + getDataArea(): Locator { + return this.element.locator(`.${CLASS.fieldPanelData}`); + } + + getFilterArea(): Locator { + return this.element.locator(`.${CLASS.fieldPanelFilter}`); + } + + getFieldItem(areaLocator: Locator, index = 0): Locator { + return areaLocator.locator(`.${CLASS.fieldItem}`).nth(index); + } +} + +export class HeaderFilter { + readonly page: Page; + readonly element: Locator; + + constructor(page: Page) { + this.page = page; + this.element = page.locator('.dx-header-filter-menu'); + } + + getList(): HeaderFilterList { + return new HeaderFilterList(this.element); + } +} + +export class HeaderFilterList { + readonly element: Locator; + + constructor(container: Locator) { + this.element = container.locator('.dx-list'); + } + + getItem(index: number): Locator { + return this.element.locator('.dx-list-item').nth(index); + } + + getSelectAll(): HeaderFilterSelectAll { + return new HeaderFilterSelectAll(this.element); + } +} + +export class HeaderFilterSelectAll { + readonly element: Locator; + readonly checkBox: Locator; + + constructor(container: Locator) { + this.element = container.locator('.dx-list-select-all'); + this.checkBox = this.element.locator('.dx-checkbox'); + } + + async isChecked(): Promise { + return this.checkBox.evaluate((el) => el.classList.contains('dx-checkbox-checked')); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/scheduler.ts b/e2e/testcafe-devextreme/playwright-helpers/scheduler.ts new file mode 100644 index 000000000000..010174d216b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/scheduler.ts @@ -0,0 +1,594 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + appointment: 'dx-scheduler-appointment', + appointmentCollector: 'dx-scheduler-appointment-collector', + dateTable: 'dx-scheduler-date-table', + dateTableCell: 'dx-scheduler-date-table-cell', + allDayTableCell: 'dx-scheduler-all-day-table-cell', + allDayTitle: 'dx-scheduler-all-day-title', + allDayRow: 'dx-scheduler-all-day-table-row', + allDayCollapsed: 'dx-scheduler-work-space-all-day-collapsed', + focusedCell: 'dx-scheduler-focused-cell', + selectedCell: 'dx-state-focused', + hoverCell: 'dx-state-hover', + activeCell: 'dx-state-active', + droppableCell: 'dx-scheduler-date-table-droppable-cell', + dateTableRow: 'dx-scheduler-date-table-row', + dateTableScrollable: 'dx-scheduler-date-table-scrollable', + dateTableScrollableContainer: 'dx-scrollable-container', + headerScrollable: 'dx-scheduler-header-scrollable', + scrollableContainer: 'dx-scrollable-container', + workspaceBothScrollbar: 'dx-scheduler-work-space-both-scrollbar', + workSpace: 'dx-scheduler-work-space', + statusContainer: 'dx-screen-reader-only', +} as const; + +const APPOINTMENT_CLASS = { + appointment: 'dx-scheduler-appointment', + appointmentContentDate: 'dx-scheduler-appointment-content-date', + recurrenceIcon: 'dx-scheduler-appointment-recurrence-icon', + resizableHandleBottom: 'dx-resizable-handle-bottom', + resizableHandleLeft: 'dx-resizable-handle-left', + resizableHandleRight: 'dx-resizable-handle-right', + resizableHandleTop: 'dx-resizable-handle-top', + stateFocused: 'dx-state-focused', + allDay: 'dx-scheduler-all-day-appointment', + title: 'dx-scheduler-appointment-title', + resourceItem: 'dx-scheduler-appointment-resource-item', + resourceValue: 'dx-scheduler-appointment-resource-item-value', + reduced: 'dx-scheduler-appointment-reduced', + reducedIcon: 'dx-scheduler-appointment-reduced-icon', + reducedHead: 'dx-scheduler-appointment-head', + reducedBody: 'dx-scheduler-appointment-body', + reducedTail: 'dx-scheduler-appointment-tail', + draggableSource: 'dx-draggable-source', +} as const; + +const POPUP_SELECTORS = { + appointmentPopup: '.dx-scheduler-appointment-popup.dx-popup.dx-widget', + appointmentPopupContent: '.dx-scheduler-appointment-popup .dx-overlay-content', + appointmentPopupToolbar: '.dx-scheduler-appointment-popup .dx-popup-title', + form: '.dx-scheduler-form', + doneButton: '.dx-popup-done.dx-button.dx-widget', + cancelButton: '.dx-popup-cancel.dx-button.dx-widget', + textEditor: '.dx-textbox.dx-widget', + allDaySwitch: '.dx-scheduler-form-all-day-switch .dx-switch.dx-widget', + startDateEditor: '.dx-scheduler-form-start-date-editor .dx-datebox.dx-datebox-date.dx-widget', + startTimeEditor: '.dx-scheduler-form-start-time-editor .dx-datebox.dx-datebox-time.dx-widget', + endDateEditor: '.dx-scheduler-form-end-date-editor .dx-datebox.dx-datebox-date.dx-widget', + endTimeEditor: '.dx-scheduler-form-end-time-editor .dx-datebox.dx-datebox-time.dx-widget', + descriptionEditor: '.dx-scheduler-form-description-editor .dx-textarea.dx-widget', + recurrenceGroup: '.dx-scheduler-form-recurrence-group', + listItem: '.dx-list-item', +} as const; + +const TOOLTIP_CLASS = { + tooltip: 'dx-tooltip', + appointmentTooltipWrapper: 'dx-scheduler-appointment-tooltip-wrapper', + tooltipWrapper: 'dx-tooltip-wrapper', + tooltipDeleteButton: 'dx-tooltip-appointment-item-delete-button', + mobileTooltip: '.dx-scheduler-overlay-panel > .dx-overlay-content', + listItem: 'dx-list-item', + contentDate: 'dx-tooltip-appointment-item-content-date', + contentSubject: 'dx-tooltip-appointment-item-content-subject', + stateInvisible: 'dx-state-invisible', + popupContent: 'dx-popup-content', +} as const; + +const TOOLBAR_CLASS = { + toolbar: 'dx-scheduler-header', + todayButton: 'dx-scheduler-today', + menuButton: 'dx-toolbar-menu-container', + invisible: 'dx-state-invisible', +} as const; + +const NAVIGATOR_CLASS = { + navigator: 'dx-scheduler-navigator', + nextButton: 'dx-scheduler-navigator-next', + prevButton: 'dx-scheduler-navigator-previous', + caption: 'dx-scheduler-navigator-caption', +} as const; + +const DIALOG_CLASS = { + dialog: 'dx-dialog.dx-popup', + dialogButton: 'dx-dialog-button', +} as const; + +const VIEW_TYPE_CLASSES: Record = { + day: 'dx-scheduler-work-space-day', + week: 'dx-scheduler-work-space-week', + workWeek: 'dx-scheduler-work-space-work-week', + month: 'dx-scheduler-work-space-month', + timelineDay: 'dx-scheduler-timeline-day', + timelineWeek: 'dx-scheduler-timeline-week', + timelineWorkWeek: 'dx-scheduler-timeline-work-week', + timelineMonth: 'dx-scheduler-timeline-month', +}; + +export class SchedulerAppointment { + readonly element: Locator; + + readonly resizableHandle: { + left: Locator; + right: Locator; + top: Locator; + bottom: Locator; + }; + + readonly reducedIcon: Locator; + + readonly resourcesItems: Locator; + + constructor( + private readonly page: Page, + container: Locator, + index: number, + title?: string, + ) { + const appointments = container.locator(`.${APPOINTMENT_CLASS.appointment}`); + this.element = title + ? appointments.filter({ hasText: title }).nth(index) + : appointments.nth(index); + + this.resizableHandle = { + left: this.element.locator(`.${APPOINTMENT_CLASS.resizableHandleLeft}`), + right: this.element.locator(`.${APPOINTMENT_CLASS.resizableHandleRight}`), + top: this.element.locator(`.${APPOINTMENT_CLASS.resizableHandleTop}`), + bottom: this.element.locator(`.${APPOINTMENT_CLASS.resizableHandleBottom}`), + }; + + this.reducedIcon = this.element.locator(`.${APPOINTMENT_CLASS.reducedIcon}`); + this.resourcesItems = this.element.locator(`.${APPOINTMENT_CLASS.resourceItem}`); + } + + async getDateTimeText(): Promise { + return this.element + .locator(`.${APPOINTMENT_CLASS.appointmentContentDate}`) + .first() + .innerText(); + } + + async getTitle(): Promise { + return this.element.locator(`.${APPOINTMENT_CLASS.title}`).innerText(); + } + + async getColor(): Promise { + return this.element.evaluate( + (el) => getComputedStyle(el).backgroundColor, + ); + } + + async getWidth(): Promise { + return this.element.evaluate( + (el) => getComputedStyle(el).width, + ); + } + + async getHeight(): Promise { + return this.element.evaluate( + (el) => getComputedStyle(el).height, + ); + } + + async isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + APPOINTMENT_CLASS.stateFocused, + ); + } + + async isAllDay(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + APPOINTMENT_CLASS.allDay, + ); + } + + async isReduced(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + APPOINTMENT_CLASS.reduced, + ); + } + + async isDraggableSource(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + APPOINTMENT_CLASS.draggableSource, + ); + } + + async getAriaLabel(): Promise { + return this.element.getAttribute('aria-label'); + } + + getRecurrenceElement(): Locator { + return this.element.locator(`.${APPOINTMENT_CLASS.recurrenceIcon}`); + } + + getResourceElement(label: string): Locator { + return this.resourcesItems + .filter({ has: this.page.locator('div', { hasText: label }) }) + .locator(`.${APPOINTMENT_CLASS.resourceValue}`); + } + + async getResource(label: string): Promise { + return this.getResourceElement(label).innerText(); + } +} + +export class SchedulerAppointmentPopup { + readonly element: Locator; + readonly contentElement: Locator; + readonly toolbarElement: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly form: Locator; + readonly textEditor: Locator; + readonly allDaySwitch: Locator; + readonly startDateEditor: Locator; + readonly startTimeEditor: Locator; + readonly endDateEditor: Locator; + readonly endTimeEditor: Locator; + readonly descriptionEditor: Locator; + readonly recurrenceGroup: Locator; + + constructor(private readonly page: Page) { + this.element = page.locator(POPUP_SELECTORS.appointmentPopup); + this.contentElement = page.locator(POPUP_SELECTORS.appointmentPopupContent); + this.toolbarElement = page.locator(POPUP_SELECTORS.appointmentPopupToolbar); + this.saveButton = this.toolbarElement.locator(POPUP_SELECTORS.doneButton); + this.cancelButton = this.toolbarElement.locator(POPUP_SELECTORS.cancelButton); + this.form = this.contentElement.locator(POPUP_SELECTORS.form); + this.textEditor = this.contentElement.locator(POPUP_SELECTORS.textEditor); + this.allDaySwitch = this.contentElement.locator(POPUP_SELECTORS.allDaySwitch); + this.startDateEditor = this.contentElement.locator(POPUP_SELECTORS.startDateEditor); + this.startTimeEditor = this.contentElement.locator(POPUP_SELECTORS.startTimeEditor); + this.endDateEditor = this.contentElement.locator(POPUP_SELECTORS.endDateEditor); + this.endTimeEditor = this.contentElement.locator(POPUP_SELECTORS.endTimeEditor); + this.descriptionEditor = this.contentElement.locator(POPUP_SELECTORS.descriptionEditor); + this.recurrenceGroup = this.contentElement.locator(POPUP_SELECTORS.recurrenceGroup); + } + + async isVisible(): Promise { + return this.element.isVisible(); + } +} + +export class SchedulerTooltipListItem { + readonly element: Locator; + readonly date: Locator; + readonly subject: Locator; + + constructor(wrapper: Locator, title?: string, index = 0) { + const items = wrapper.locator(`.${TOOLTIP_CLASS.listItem}`); + this.element = title + ? items.filter({ hasText: title }).nth(index) + : items.nth(index); + this.date = this.element.locator(`.${TOOLTIP_CLASS.contentDate}`); + this.subject = this.element.locator(`.${TOOLTIP_CLASS.contentSubject}`); + } + + async isFocused(): Promise { + return this.element.evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + } +} + +export class SchedulerAppointmentTooltip { + readonly element: Locator; + readonly mobileElement: Locator; + readonly deleteButton: Locator; + readonly wrapper: Locator; + readonly content: Locator; + + constructor(private readonly page: Page, container: Locator) { + this.element = container.locator( + `.${TOOLTIP_CLASS.tooltip}.${TOOLTIP_CLASS.appointmentTooltipWrapper}`, + ); + this.mobileElement = page.locator(TOOLTIP_CLASS.mobileTooltip); + this.deleteButton = page.locator(`.${TOOLTIP_CLASS.tooltipDeleteButton}`); + this.wrapper = page.locator( + `.${TOOLTIP_CLASS.tooltipWrapper}.${TOOLTIP_CLASS.appointmentTooltipWrapper}`, + ); + this.content = this.element.locator(`.${TOOLTIP_CLASS.popupContent}`); + } + + getListItem(title?: string, index = 0): SchedulerTooltipListItem { + return new SchedulerTooltipListItem(this.wrapper, title, index); + } + + async isVisible(): Promise { + const count = await this.element.count(); + if (count === 0) return false; + return this.element.evaluate( + (el, cls) => !el.classList.contains(cls), + TOOLTIP_CLASS.stateInvisible, + ); + } +} + +export class SchedulerNavigator { + readonly element: Locator; + readonly nextButton: Locator; + readonly prevButton: Locator; + readonly caption: Locator; + + constructor(private readonly page: Page, toolbar: Locator) { + this.element = toolbar.locator(`.${NAVIGATOR_CLASS.navigator}`); + this.nextButton = page.locator(`.${NAVIGATOR_CLASS.nextButton}`); + this.prevButton = page.locator(`.${NAVIGATOR_CLASS.prevButton}`); + this.caption = page.locator(`.${NAVIGATOR_CLASS.caption}`); + } +} + +export class SchedulerToolbar { + readonly element: Locator; + readonly todayButton: Locator; + readonly navigator: SchedulerNavigator; + readonly menuButton: Locator; + + constructor(page: Page, container: Locator) { + this.element = container.locator(`.${TOOLBAR_CLASS.toolbar}`); + this.todayButton = this.element.locator(`.${TOOLBAR_CLASS.todayButton}`); + this.navigator = new SchedulerNavigator(page, this.element); + this.menuButton = this.element.locator(`.${TOOLBAR_CLASS.menuButton}`); + } + + async isInvisible(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + TOOLBAR_CLASS.invisible, + ); + } +} + +export class SchedulerAppointmentDialog { + readonly element: Locator; + readonly series: Locator; + readonly appointment: Locator; + + constructor(page: Page) { + this.element = page.locator(`.${DIALOG_CLASS.dialog}`); + this.series = this.element.locator(`.${DIALOG_CLASS.dialogButton}`).nth(0); + this.appointment = this.element.locator(`.${DIALOG_CLASS.dialogButton}`).nth(1); + } +} + +export class Scheduler { + readonly page: Page; + readonly element: Locator; + readonly selector: string; + + readonly workSpace: Locator; + readonly dateTable: Locator; + readonly dateTableCells: Locator; + readonly dateTableRows: Locator; + readonly dateTableScrollable: Locator; + readonly dateTableScrollableContainer: Locator; + readonly workspaceScrollable: Locator; + readonly allDayTableCells: Locator; + readonly allDayRow: Locator; + readonly allDayTitle: Locator; + + readonly toolbar: SchedulerToolbar; + readonly appointmentPopup: SchedulerAppointmentPopup; + readonly appointmentTooltip: SchedulerAppointmentTooltip; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + + this.workSpace = this.element.locator(`.${CLASS.workSpace}`); + this.dateTable = this.element.locator(`.${CLASS.dateTable}`); + this.dateTableCells = this.element.locator(`.${CLASS.dateTableCell}`); + this.dateTableRows = this.element.locator(`.${CLASS.dateTableRow}`); + this.allDayTableCells = this.element.locator(`.${CLASS.allDayTableCell}`); + this.allDayRow = this.element.locator(`.${CLASS.allDayRow}`); + this.allDayTitle = this.element.locator(`.${CLASS.allDayTitle}`); + this.dateTableScrollable = this.element.locator(`.${CLASS.dateTableScrollable}`); + this.dateTableScrollableContainer = this.dateTableScrollable.locator( + `.${CLASS.dateTableScrollableContainer}`, + ); + this.workspaceScrollable = this.dateTableScrollable.locator( + `.${CLASS.scrollableContainer}`, + ); + + this.toolbar = new SchedulerToolbar(page, this.element); + this.appointmentPopup = new SchedulerAppointmentPopup(page); + this.appointmentTooltip = new SchedulerAppointmentTooltip(page, this.element); + } + + getAppointment(title: string, index = 0): SchedulerAppointment { + return new SchedulerAppointment(this.page, this.element, index, title); + } + + getAppointmentByIndex(index = 0): SchedulerAppointment { + return new SchedulerAppointment(this.page, this.element, index); + } + + async getAppointmentCount(): Promise { + return this.element.locator(`.${CLASS.appointment}`).count(); + } + + getDateTableCell(rowIndex = 0, cellIndex = 0): Locator { + return this.dateTableRows + .nth(rowIndex) + .locator(`.${CLASS.dateTableCell}`) + .nth(cellIndex); + } + + getAllDayTableCell(cellIndex = 0): Locator { + return this.allDayTableCells.nth(cellIndex); + } + + getSelectedCells(isAllDay = false): Locator { + const cellClass = isAllDay ? CLASS.allDayTableCell : CLASS.dateTableCell; + return this.element.locator(`.${cellClass}.${CLASS.selectedCell}`); + } + + getFocusedCell(isAllDay = false): Locator { + const base = isAllDay + ? `.${CLASS.allDayTableCell}.${CLASS.focusedCell}` + : `.${CLASS.dateTableCell}.${CLASS.focusedCell}`; + return this.element.locator(base); + } + + getDroppableCell(isAllDay = false): Locator { + const base = isAllDay + ? `.${CLASS.allDayTableCell}.${CLASS.droppableCell}` + : `.${CLASS.dateTableCell}.${CLASS.droppableCell}`; + return this.element.locator(base); + } + + getGeneralStatusContainer(): Locator { + return this.element.locator(`.${CLASS.statusContainer}`); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxScheduler('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxScheduler('instance').option(n), + { sel, name }, + ); + } + + async optionObject(options: Record): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ sel: s, opts }) => { + ($(s) as any).dxScheduler('instance').option(opts); + }, + { sel, opts: options }, + ); + } + + async scrollTo( + date: Date, + group?: Record, + allDay?: boolean, + ): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ sel: s, d, g, a }) => { + const instance = ($(s) as any).dxScheduler('instance'); + instance.scrollTo(new Date(d), g, a); + }, + { sel, d: date.toISOString(), g: group, a: allDay }, + ); + } + + async hideAppointmentTooltip(): Promise { + const sel = this.selector; + await this.page.evaluate( + (s) => { + ($(s) as any).dxScheduler('instance').hideAppointmentTooltip(); + }, + sel, + ); + } + + async showAppointmentPopup(appointmentData: unknown): Promise { + const sel = this.selector; + await this.page.evaluate( + ({ sel: s, data }) => { + ($(s) as any).dxScheduler('instance').showAppointmentPopup(data); + }, + { sel, data: appointmentData }, + ); + } + + async checkViewType(type: string): Promise { + const viewClass = VIEW_TYPE_CLASSES[type]; + if (!viewClass) return false; + return this.workSpace.evaluate( + (el, cls) => el.classList.contains(cls), + viewClass, + ); + } + + async isAllDayPanelCollapsed(): Promise { + return this.workSpace.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.allDayCollapsed, + ); + } + + async workspaceHasBothScrollbar(): Promise { + return this.workSpace.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.workspaceBothScrollbar, + ); + } + + async getWorkSpaceScrollLeft(): Promise { + return this.workspaceScrollable.evaluate((el) => el.scrollLeft); + } + + async getWorkSpaceScrollTop(): Promise { + return this.workspaceScrollable.evaluate((el) => el.scrollTop); + } + + async getHeaderSpaceScrollLeft(): Promise { + return this.element + .locator(`.${CLASS.headerScrollable} .${CLASS.scrollableContainer}`) + .evaluate((el) => el.scrollLeft); + } + + async getHeaderSpaceScrollTop(): Promise { + return this.element + .locator(`.${CLASS.headerScrollable} .${CLASS.scrollableContainer}`) + .evaluate((el) => el.scrollTop); + } + + async getCellDataAtViewportCenter(): Promise { + const sel = this.selector; + return this.page.evaluate((s) => { + const instance = ($(s) as any).dxScheduler('instance'); + const workSpace = instance.getWorkSpace(); + const scrollable = workSpace.getScrollable(); + const scrollLeft = scrollable.scrollLeft(); + const scrollTop = scrollable.scrollTop(); + const centerX = scrollLeft + scrollable.$element().width() / 2; + const centerY = scrollTop + scrollable.$element().height() / 2; + const cellElement = workSpace.getCellByCoordinates( + { top: centerY, left: centerX }, + false, + ); + return workSpace.getCellData(cellElement); + }, sel); + } + + async focus(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxScheduler('instance').focus(); + }, sel); + } + + async repaint(): Promise { + const sel = this.selector; + await this.page.evaluate((s) => { + ($(s) as any).dxScheduler('instance').repaint(); + }, sel); + } + + getDeleteRecurrenceDialog(): SchedulerAppointmentDialog { + return new SchedulerAppointmentDialog(this.page); + } + + getEditRecurrenceDialog(): SchedulerAppointmentDialog { + return new SchedulerAppointmentDialog(this.page); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/screenshotUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/screenshotUtils.ts new file mode 100644 index 000000000000..11fcac35b2d1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/screenshotUtils.ts @@ -0,0 +1,79 @@ +import type { Page, Locator } from '@playwright/test'; + +export async function getLocatorScrollClip( + locator: Locator, +): Promise<{ x: number; y: number; width: number; height: number } | null> { + return locator.evaluate((el) => { + const r = el.getBoundingClientRect(); + const width = Math.max(el.scrollWidth, el.offsetWidth); + const height = Math.max(el.scrollHeight, el.offsetHeight); + if (width === Math.round(r.width) && height === Math.round(r.height)) { + return null; + } + return { + x: Math.round(r.x), + y: Math.round(r.y), + width, + height, + }; + }); +} + +export async function getVisualClip( + page: Page, + selector: string, +): Promise<{ x: number; y: number; width: number; height: number } | null> { + return page.evaluate((sel) => { + const root = document.querySelector(sel); + if (!root) return null; + const rootRect = root.getBoundingClientRect(); + const SHADOW_THRESHOLD = 4; + const vw = window.innerWidth; + const vh = window.innerHeight; + + const isClippedByAncestor = (el: Element): boolean => { + let ancestor = el.parentElement; + while (ancestor && ancestor !== root) { + const style = getComputedStyle(ancestor); + if (style.overflow === 'hidden' || style.overflowX === 'hidden') return true; + ancestor = ancestor.parentElement; + } + return false; + }; + + const all = [root as Element, ...Array.from(root.querySelectorAll('*'))]; + let maxX = rootRect.right; + let maxY = rootRect.bottom; + let hasOverflowX = false; + let hasOverflowY = false; + + all.forEach((el) => { + const r = el.getBoundingClientRect(); + if (r.width <= 0 || r.height <= 0) return; + const overflowX = r.right - rootRect.right; + const overflowY = r.bottom - rootRect.bottom; + + if (overflowX > 0 && overflowX <= SHADOW_THRESHOLD) maxX = Math.max(maxX, r.right); + if (overflowX > SHADOW_THRESHOLD && r.right <= vw && !isClippedByAncestor(el)) { + maxX = Math.max(maxX, r.right); + hasOverflowX = true; + } + if (overflowY > 0 && r.bottom <= vh && r.left >= rootRect.left - 1 && !isClippedByAncestor(el)) { + maxY = Math.max(maxY, r.bottom); + hasOverflowY = true; + } + }); + + const minX = Math.max(0, rootRect.left); + const minY = Math.max(0, rootRect.top); + + const clampedMaxY = Math.min(maxY, vh); + + return { + x: Math.floor(minX), + y: Math.floor(minY), + width: Math.round(maxX - minX), + height: Math.round(clampedMaxY - minY), + }; + }, selector); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/scrollable.ts b/e2e/testcafe-devextreme/playwright-helpers/scrollable.ts new file mode 100644 index 000000000000..653f3a59281e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/scrollable.ts @@ -0,0 +1,213 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + scrollable: 'dx-scrollable', + scrollableContainer: 'dx-scrollable-container', + scrollableContent: 'dx-scrollable-content', + scrollbar: 'dx-scrollbar-horizontal', + scrollbarVertical: 'dx-scrollbar-vertical', + simulatedScrollbar: 'dx-scrollable-scrollbar', +} as const; + +export class Scrollable { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + + constructor(page: Page, selector = '#scrollable', options?: Record) { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + if (options) { + this._options = options; + } + } + + private _options?: Record; + + getContainer(): Locator { + return this.element.locator(`.${CLASS.scrollableContainer}`); + } + + getContent(): Locator { + return this.element.locator(`.${CLASS.scrollableContent}`); + } + + getScrollbar(direction: 'horizontal' | 'vertical' = 'vertical'): Locator { + const cls = direction === 'horizontal' ? CLASS.scrollbar : CLASS.scrollbarVertical; + return this.element.locator(`.${cls}`); + } + + async scrollTo(position: { top?: number; left?: number }): Promise { + await this.page.evaluate( + ({ sel, pos }) => { + ($(sel) as any).dxScrollable('instance').scrollTo(pos); + }, + { sel: this.selector, pos: position }, + ); + } + + async scrollToElement(elementSelector: string): Promise { + await this.page.evaluate( + ({ sel, elemSel }) => { + ($(sel) as any).dxScrollable('instance').scrollToElement($(elemSel)[0]); + }, + { sel: this.selector, elemSel: elementSelector }, + ); + } + + async scrollTop(): Promise { + return this.page.evaluate( + ({ sel }) => ($(sel) as any).dxScrollable('instance').scrollTop(), + { sel: this.selector }, + ); + } + + async scrollLeft(): Promise { + return this.page.evaluate( + ({ sel }) => ($(sel) as any).dxScrollable('instance').scrollLeft(), + { sel: this.selector }, + ); + } + + async isScrollbarVisible(direction: 'horizontal' | 'vertical' = 'vertical'): Promise { + const cls = direction === 'horizontal' ? CLASS.scrollbar : CLASS.scrollbarVertical; + return this.page.evaluate( + ({ sel, scrollbarCls }) => { + const scrollbarTrack = document.querySelector(`${sel} .${scrollbarCls}`); + if (!scrollbarTrack) return false; + const scrollThumb = scrollbarTrack.querySelector('.dx-scrollable-scroll'); + if (!scrollThumb) return false; + return !scrollThumb.classList.contains('dx-state-invisible'); + }, + { sel: this.selector, scrollbarCls: cls }, + ); + } + + readonly hScrollbar: null = null; + + async setContainerCssWidth(width: number): Promise { + await this.page.evaluate( + ({ sel, w }) => { + const el = document.querySelector(sel) as HTMLElement | null; + if (el) el.style.width = `${w}px`; + const inst = ($(sel) as any).dxScrollable('instance'); + inst.option('width', w); + inst.update(); + }, + { sel: this.selector, w: width }, + ); + } + + async update(): Promise { + await this.page.evaluate( + ({ sel }) => { + ($(sel) as any).dxScrollable('instance').update(); + }, + { sel: this.selector }, + ); + } + + async scrollOffset(): Promise<{ left: number; top: number }> { + return this.page.evaluate( + ({ sel }) => { + const inst = ($(sel) as any).dxScrollable('instance'); + return inst.scrollOffset(); + }, + { sel: this.selector }, + ); + } + + async getMaxScrollOffset(): Promise<{ horizontal: number; vertical: number }> { + return this.page.evaluate( + ({ sel }) => { + const inst = ($(sel) as any).dxScrollable('instance'); + const scrollWidth = inst.scrollWidth(); + const clientWidth = inst.clientWidth(); + const scrollHeight = inst.scrollHeight(); + const clientHeight = inst.clientHeight(); + return { + horizontal: Math.max(0, scrollWidth - clientWidth), + vertical: Math.max(0, scrollHeight - clientHeight), + }; + }, + { sel: this.selector }, + ); + } + + async hide(): Promise { + await this.page.evaluate( + ({ sel }) => { + const el = document.querySelector(sel) as HTMLElement | null; + if (el) el.style.display = 'none'; + }, + { sel: this.selector }, + ); + } + + async show(): Promise { + await this.page.evaluate( + ({ sel }) => { + const el = document.querySelector(sel) as HTMLElement | null; + if (el) el.style.display = 'block'; + }, + { sel: this.selector }, + ); + } + + async triggerHidingEvent(): Promise { + await this.page.evaluate( + ({ sel }) => { + ($(sel) as any).dxScrollable('instance')._visibilityChanged(false); + }, + { sel: this.selector }, + ); + } + + async triggerShownEvent(): Promise { + await this.page.evaluate( + ({ sel }) => { + ($(sel) as any).dxScrollable('instance')._visibilityChanged(true); + }, + { sel: this.selector }, + ); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxScrollable('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxScrollable('instance').option(n), + { sel, name }, + ); + } +} + +export class ScrollView extends Scrollable { + constructor(page: Page, selector = '#scrollView', options?: Record) { + super(page, selector, options); + } + + async scrollViewOption(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxScrollView('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxScrollView('instance').option(n), + { sel, name }, + ); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/selectBox.ts b/e2e/testcafe-devextreme/playwright-helpers/selectBox.ts new file mode 100644 index 000000000000..accaaaca5614 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/selectBox.ts @@ -0,0 +1,95 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + dropDownButton: 'dx-dropdowneditor-button', + textEditorInput: 'dx-texteditor-input', + list: 'dx-list', + focused: 'dx-state-focused', + invisible: 'dx-state-invisible', + actionButton: 'dx-texteditor-buttons-container', + button: 'dx-button', +} as const; + +const ATTR = { + popupId: 'aria-owns', +} as const; + +export class SelectBox { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + readonly input: Locator; + readonly dropDownButton: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.input = this.element.locator(`.${CLASS.textEditorInput}`); + this.dropDownButton = this.element.locator(`.${CLASS.dropDownButton}`); + } + + async click(): Promise { + await this.element.click(); + } + + get isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } + + get value(): Promise { + return this.input.inputValue(); + } + + async isOpened(): Promise { + const popupId = await this.input.getAttribute(ATTR.popupId); + if (!popupId) return false; + const overlayContent = this.page.locator(`#${popupId}`).locator('..'); + const isInvisible = await overlayContent.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.invisible, + ); + return !isInvisible; + } + + async getPopup(): Promise { + const popupId = await this.input.getAttribute(ATTR.popupId); + return this.page.locator(`#${popupId}`); + } + + async getList(): Promise { + const popup = await this.getPopup(); + return popup.locator(`.${CLASS.list}`); + } + + getButton(index: number): Locator { + return this.element.locator(`.${CLASS.actionButton} .${CLASS.button}`).nth(index); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxSelectBox('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxSelectBox('instance').option(n), + { sel, name }, + ); + } + + async focus(): Promise { + const sel = this.selector; + await this.page.evaluate( + (s) => { ($(s) as any).dxSelectBox('instance').focus(); }, + sel, + ); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/tabPanel.ts b/e2e/testcafe-devextreme/playwright-helpers/tabPanel.ts new file mode 100644 index 000000000000..682307e3118b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/tabPanel.ts @@ -0,0 +1,115 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + tabs: 'dx-tabs', + multiView: 'dx-multiview-wrapper', + tab: 'dx-tab', + multiViewItem: 'dx-multiview-item', + focused: 'dx-state-focused', +} as const; + +export class TabItem { + readonly element: Locator; + + constructor(locator: Locator) { + this.element = locator; + } + + async isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } +} + +export class MultiViewItem { + readonly element: Locator; + + constructor(locator: Locator) { + this.element = locator; + } + + async isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } +} + +export class TabsHelper { + readonly element: Locator; + + constructor(container: Locator) { + this.element = container.locator(`.${CLASS.tabs}`); + } + + async isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } + + getItem(index = 0): TabItem { + return new TabItem(this.element.locator(`.${CLASS.tab}`).nth(index)); + } +} + +export class MultiViewHelper { + readonly element: Locator; + + constructor(container: Locator) { + this.element = container.locator(`.${CLASS.multiView}`); + } + + getItem(index = 0): MultiViewItem { + return new MultiViewItem( + this.element.locator(`.${CLASS.multiViewItem}`).nth(index), + ); + } +} + +export class TabPanel { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + readonly tabs: TabsHelper; + readonly multiView: MultiViewHelper; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.tabs = new TabsHelper(this.element); + this.multiView = new MultiViewHelper(this.element); + } + + async isFocused(): Promise { + return this.element.evaluate( + (el, cls) => el.classList.contains(cls), + CLASS.focused, + ); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxTabPanel('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxTabPanel('instance').option(n), + { sel, name }, + ); + } + + getItem(index = 0): TabItem { + return new TabItem(this.element.locator(`.${CLASS.tab}`).nth(index)); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts new file mode 100644 index 000000000000..cd44138a6180 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts @@ -0,0 +1,75 @@ +import type { Page } from '@playwright/test'; +import path from 'path'; +import { removeStylesheetRulesFromPage } from './domUtils'; + +export function getContainerUrl(dirname: string, relativePath = '../../../tests/container.html'): string { + return `file://${path.resolve(dirname, relativePath)}`; +} + +export async function clearTestPage(page: Page): Promise { + await page.evaluate(() => { + const widgetSelector = '.dx-widget'; + const elements = document.querySelectorAll(widgetSelector); + elements.forEach((element) => { + if (element.closest(widgetSelector) === element) { + const $el = $(element) as any; + const widgetNames = $el.data()?.dxComponents; + widgetNames?.forEach((name: string) => { + if ($el.hasClass('dx-widget')) { + $el[name]?.('dispose'); + } + }); + $el.empty(); + } + }); + + const body = document.querySelector('body'); + if (body) { + body.innerHTML = ''; + body.className = 'dx-surface'; + + const parent = document.createElement('div'); + parent.id = 'parentContainer'; + parent.setAttribute('role', 'main'); + parent.innerHTML = '

Test header

'; + body.appendChild(parent); + } + }); + + await removeStylesheetRulesFromPage(page); +} + +const TARGET_CONTENT_WIDTH = 1184; + +export async function adjustViewportForContent(page: Page): Promise { + const currentContentWidth = await page.evaluate( + () => document.body.clientWidth, + ); + + if (currentContentWidth === TARGET_CONTENT_WIDTH) return; + + const current = page.viewportSize(); + if (!current) return; + + const diff = TARGET_CONTENT_WIDTH - currentContentWidth; + await page.setViewportSize({ width: current.width + diff, height: current.height }); + await page.evaluate(() => new Promise((resolve) => { + window.dispatchEvent(new Event('resize')); + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + })); +} + +export async function setupTestPage(page: Page, containerUrl: string, theme = 'fluent.blue.light'): Promise { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((themeName) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(themeName); + }), theme); + + await page.addStyleTag({ + content: '*, *::before, *::after { caret-color: transparent !important; }', + }); + + await adjustViewportForContent(page); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts new file mode 100644 index 000000000000..9c5068d10087 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts @@ -0,0 +1,175 @@ +import type { Page, Locator } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { getVisualClip, getLocatorScrollClip } from './screenshotUtils'; + +const defaultThemeName = 'fluent.blue.light'; + +export const getThemePostfix = (theme?: string): string => { + const themeName = (theme ?? process.env.theme) ?? defaultThemeName; + return ` (${themeName})`; +}; + +export const getFullThemeName = (): string => process.env.theme ?? defaultThemeName; + +export const isMaterial = (): boolean => getFullThemeName().startsWith('material'); + +export const isFluent = (): boolean => getFullThemeName().startsWith('fluent'); + +export const isMaterialBased = (): boolean => isMaterial() || isFluent(); + +export async function changeTheme(page: Page, themeName: string): Promise { + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), themeName); +} + +export async function getCurrentTheme(page: Page): Promise { + return page.evaluate(() => (window as any).DevExpress?.ui.themes.current()); +} + +const getScreenshotName = (baseName: string, theme?: string): string => { + const themePostfix = getThemePostfix(theme); + return baseName.endsWith('.png') + ? baseName.replace('.png', `${themePostfix}.png`) + : `${baseName}${themePostfix}.png`; +}; + +async function takePageScreenshot( + page: Page, + name: string, + screenshotOptions?: { maxDiffPixelRatio?: number }, +): Promise { + const viewport = page.viewportSize() ?? { width: 1200, height: 800 }; + const hasPadding = await page.evaluate(() => !!document.documentElement.style.paddingRight); + const width = hasPadding ? viewport.width - 15 : viewport.width; + const clip = { x: 0, y: 0, width, height: viewport.height }; + await expect(page).toHaveScreenshot([name], { maxDiffPixelRatio: 0.20, clip, ...screenshotOptions }); +} + +async function takeElementScreenshot( + page: Page, + element: Locator | string, + name: string, + screenshotOptions?: { maxDiffPixelRatio?: number }, +): Promise { + const locator = typeof element === 'string' ? page.locator(element) : element; + const selector = typeof element === 'string' ? element : null; + + const overflowClip = selector + ? await getVisualClip(page, selector) + : await getLocatorScrollClip(locator); + + const clip = overflowClip ?? await locator.evaluate((el) => { + const r = el.getBoundingClientRect(); + return { + x: Math.round(r.x), + y: Math.round(r.y), + width: Math.round(r.width), + height: Math.floor(r.height), + }; + }); + + await expect(page).toHaveScreenshot([name], { maxDiffPixelRatio: 0.15, clip, ...screenshotOptions }); +} + +async function simulateTestCafeScrollbar(page: Page): Promise { + const viewport = page.viewportSize(); + if (viewport && viewport.width !== 1200) return false; + + return page.evaluate(() => { + if (document.documentElement.style.paddingRight) return false; + + document.documentElement.style.paddingRight = '15px'; + document.documentElement.style.boxSizing = 'border-box'; + + void document.body.offsetHeight; + + const hasOverflow = document.body.scrollHeight > window.innerHeight; + if (!hasOverflow) { + document.documentElement.style.paddingRight = ''; + document.documentElement.style.boxSizing = ''; + return false; + } + return true; + }); +} + +async function removeSimulatedScrollbar(page: Page): Promise { + await page.evaluate(() => { + document.documentElement.style.paddingRight = ''; + document.documentElement.style.boxSizing = ''; + }); +} + +async function takeScreenshotForTarget( + page: Page, + element: Locator | string | null | undefined, + name: string, + screenshotOptions?: { maxDiffPixelRatio?: number }, +): Promise { + await page.evaluate(() => { + (document.activeElement as HTMLElement)?.blur(); + const licenseEls = document.querySelectorAll('dx-license'); + licenseEls.forEach((el) => { + const btn = el.querySelector('div[style*="cursor: pointer"]') as HTMLElement | null; + if (btn) btn.click(); + }); + }); + + const addedPadding = await simulateTestCafeScrollbar(page); + if (addedPadding) { + await page.evaluate(() => new Promise((resolve) => { + window.dispatchEvent(new Event('resize')); + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + })); + } + + try { + if (element === undefined || element === null) { + await takePageScreenshot(page, name, screenshotOptions); + } else { + await takeElementScreenshot(page, element, name, screenshotOptions); + } + } finally { + if (addedPadding) { + await removeSimulatedScrollbar(page); + } + } +} + +export async function testScreenshot( + page: Page, + screenshotName: string, + options?: { + element?: Locator | string | null; + theme?: string; + shouldTestInCompact?: boolean; + maxDiffPixelRatio?: number; + }, +): Promise { + const { + element, + theme, + shouldTestInCompact = false, + maxDiffPixelRatio, + } = options ?? {}; + + const screenshotOptions = maxDiffPixelRatio !== undefined ? { maxDiffPixelRatio } : undefined; + + if (theme) { + await changeTheme(page, theme); + } + + await takeScreenshotForTarget(page, element, getScreenshotName(screenshotName, theme), screenshotOptions); + + if (shouldTestInCompact) { + const themeName = (theme ?? process.env.theme) ?? defaultThemeName; + await changeTheme(page, `${themeName}.compact`); + await takeScreenshotForTarget(page, element, getScreenshotName(screenshotName, `${themeName}.compact`), screenshotOptions); + } + + if (theme || shouldTestInCompact) { + await changeTheme(page, process.env.theme ?? defaultThemeName); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/toolbar.ts b/e2e/testcafe-devextreme/playwright-helpers/toolbar.ts new file mode 100644 index 000000000000..b8543a9b6630 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/toolbar.ts @@ -0,0 +1,87 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + overflowMenu: 'dx-dropdownmenu', + item: 'dx-toolbar-item', + popup: 'dx-popup', + popupWrapper: 'dx-popup-wrapper', + popupContent: 'dx-popup-content', + overlayContent: 'dx-overlay-content', + list: 'dx-list', +} as const; + +export class ToolbarDropDownMenu { + readonly element: Locator; + readonly page: Page; + + constructor(page: Page, element: Locator) { + this.page = page; + this.element = element; + } + + async click(): Promise { + await this.element.click(); + } + + getPopup(): ToolbarDropDownMenuPopup { + return new ToolbarDropDownMenuPopup(this.page); + } + + getList(): Locator { + return this.page.locator(`.${CLASS.popupWrapper} .${CLASS.list}`); + } +} + +export class ToolbarDropDownMenuPopup { + readonly element: Locator; + readonly page: Page; + + constructor(page: Page) { + this.page = page; + this.element = page.locator(`.${CLASS.popupWrapper}`).locator(`.${CLASS.overlayContent}`).filter({ has: page.locator(`.${CLASS.popupContent}`) }); + } + + getContent(): Locator { + return this.element.locator(`.${CLASS.popupContent}`); + } + + async isVisible(): Promise { + return this.element.isVisible(); + } +} + +export class Toolbar { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + } + + getOverflowMenu(): ToolbarDropDownMenu { + return new ToolbarDropDownMenu(this.page, this.element.locator(`.${CLASS.overflowMenu}`)); + } + + getItem(index = 0): Locator { + return this.element.locator(`.${CLASS.item}`).nth(index); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxToolbar('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxToolbar('instance').option(n), + { sel, name }, + ); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/treeList.ts b/e2e/testcafe-devextreme/playwright-helpers/treeList.ts new file mode 100644 index 000000000000..37f1219dc9bd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/treeList.ts @@ -0,0 +1,132 @@ +import type { Page, Locator } from '@playwright/test'; + +const CLASS = { + dataRow: 'dx-data-row', + headerRow: 'dx-header-row', + focusedRow: 'dx-row-focused', + treeListExpandedRow: 'dx-treelist-expanded', + treeListCollapsedRow: 'dx-treelist-collapsed', + commandDrag: 'dx-command-drag', + rowsView: 'dx-treelist-rowsview', + headers: 'dx-treelist-headers', + scrollableContainer: 'dx-scrollable-container', + expandButton: 'dx-treelist-icon-container', + aiColumn: 'dx-ai-column', + aiColumnLoading: 'dx-ai-loading', + dropDownButton: 'dx-dropdownbutton', +} as const; + +export class TreeList { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + + constructor(page: Page, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + } + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v }) => { + ($(s) as any).dxTreeList('instance').option(n, v); + }, + { sel, name, value }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n }) => ($(s) as any).dxTreeList('instance').option(n), + { sel, name }, + ); + } + + async isReady(): Promise { + return this.page.evaluate( + ({ sel }) => { + const instance = ($(sel) as any).dxTreeList('instance'); + return instance && instance.getDataSource() !== undefined; + }, + { sel: this.selector }, + ); + } + + async apiFocus(): Promise { + await this.page.evaluate( + ({ sel }) => { ($(sel) as any).dxTreeList('instance').focus(); }, + { sel: this.selector }, + ); + } + + getDataRow(index: number): TreeListDataRow { + return new TreeListDataRow(this.element.locator(`.${CLASS.dataRow}`).nth(index)); + } + + getDataCell(rowIndex: number, cellIndex: number): Locator { + return this.getDataRow(rowIndex).getDataCell(cellIndex); + } + + getHeaderRow(index = 0): Locator { + return this.element.locator(`.${CLASS.headers} .${CLASS.headerRow}`).nth(index); + } + + getHeaderCell(rowIndex: number, cellIndex: number): Locator { + return this.getHeaderRow(rowIndex).locator('td').nth(cellIndex); + } + + getRowsView(): Locator { + return this.element.locator(`.${CLASS.rowsView}`); + } + + getScrollableContainer(): Locator { + return this.getRowsView().locator(`.${CLASS.scrollableContainer}`); + } + + getAIColumn(rowIndex: number): Locator { + return this.getDataRow(rowIndex).element.locator(`.${CLASS.aiColumn}`); + } + + getDropDownButton(rowIndex: number): Locator { + return this.getAIColumn(rowIndex).locator(`.${CLASS.dropDownButton}`); + } + + async scrollTo(options: { top?: number; left?: number }): Promise { + await this.page.evaluate( + ({ sel, opts }) => { + const scrollable = ($(sel) as any).dxTreeList('instance').getScrollable(); + scrollable.scrollTo(opts); + }, + { sel: this.selector, opts: options }, + ); + } +} + +export class TreeListDataRow { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + getDataCell(cellIndex: number): Locator { + return this.element.locator('td').nth(cellIndex); + } + + getExpandButton(): Locator { + return this.element.locator(`.${CLASS.expandButton}`); + } +} + +export class ExpandableCell { + readonly element: Locator; + + constructor(element: Locator) { + this.element = element; + } + + getExpandButton(): Locator { + return this.element.locator(`.${CLASS.expandButton}`); + } +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/widget.ts b/e2e/testcafe-devextreme/playwright-helpers/widget.ts new file mode 100644 index 000000000000..02ee904ccb01 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/widget.ts @@ -0,0 +1,74 @@ +import type { Page, Locator } from '@playwright/test'; + +export class Widget { + readonly page: Page; + readonly selector: string; + readonly element: Locator; + + constructor(page: Page, widgetName: string, selector = '#container') { + this.page = page; + this.selector = selector; + this.element = page.locator(selector); + this.widgetName = widgetName; + } + + private readonly widgetName: string; + + async option(name: string, value?: unknown): Promise { + const sel = this.selector; + const wn = this.widgetName; + if (arguments.length === 2) { + return this.page.evaluate( + ({ sel: s, name: n, value: v, wn: w }) => { + ($(s) as any)[w]('instance').option(n, v); + }, + { sel, name, value, wn }, + ); + } + return this.page.evaluate( + ({ sel: s, name: n, wn: w }) => ($(s) as any)[w]('instance').option(n), + { sel, name, wn }, + ); + } + + async optionObject(options: Record): Promise { + const sel = this.selector; + const wn = this.widgetName; + await this.page.evaluate( + ({ sel: s, opts, wn: w }) => { + ($(s) as any)[w]('instance').option(opts); + }, + { sel, opts: options, wn }, + ); + } + + async getInstance(): Promise { + return this.page.evaluate( + ({ sel: s, wn: w }) => ($(s) as any)[w]('instance'), + { sel: this.selector, wn: this.widgetName }, + ); + } + + async focus(): Promise { + const sel = this.selector; + const wn = this.widgetName; + await this.page.evaluate( + ({ sel: s, wn: w }) => { + ($(s) as any)[w]('instance').focus(); + }, + { sel, wn }, + ); + } + + get isFocused(): Locator { + return this.element.locator('.dx-state-focused'); + } + + async hasFocusClass(): Promise { + return this.element.evaluate((el) => el.classList.contains('dx-state-focused')); + } + + async hasClass(className: string): Promise { + return this.element.evaluate((el, cls) => el.classList.contains(cls), className); + } +} diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.matrix.spec.ts new file mode 100644 index 000000000000..71832d2ecfca --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.matrix.spec.ts @@ -0,0 +1,22 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = ['Item_1', 'Item_2', 'Item_3']; + +test.describe('Accessibility - accordion matrix', () => { + testAccessibilityMatrix({ + component: 'dxAccordion', + containerUrl, + a11yCheckConfig: {}, + options: { + dataSource: [items], + disabled: [true, false], + deferRendering: [true, false], + multiple: [true, false], + focusStateEnabled: [true], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts new file mode 100644 index 000000000000..2ce6b5faef96 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts @@ -0,0 +1,68 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - accordion', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('disabled accordion', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion with multiple mode and expanded items', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], multiple: true, focusStateEnabled: true }); + await page.locator('.dx-accordion-item-title').nth(0).click(); + await page.locator('.dx-accordion-item-title').nth(1).click(); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion without deferRendering', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], deferRendering: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion disabled with multiple', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], disabled: true, multiple: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion with deferRendering false and multiple', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], deferRendering: false, multiple: true, focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion with single item', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Only Item'], focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion with collapsible false', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], collapsible: false, focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion empty datasource', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: [] }); + await a11yCheck(page, {}, '#container'); + }); + + test('accordion with selected index', async ({ page }) => { + await createWidget(page, 'dxAccordion', { dataSource: ['Item_1', 'Item_2', 'Item_3'], selectedIndex: 1, focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.matrix.spec.ts new file mode 100644 index 000000000000..278986b82756 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.matrix.spec.ts @@ -0,0 +1,26 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = [ + { text: 'Call' }, + { text: 'Send message' }, + { text: 'Edit' }, + { text: 'Delete' }, +]; + +test.describe('Accessibility - actionSheet matrix', () => { + testAccessibilityMatrix({ + component: 'dxActionSheet', + containerUrl, + options: { + dataSource: [[], items], + title: [undefined, 'title'], + cancelText: [undefined, 'Cancel'], + showTitle: [true, false], + showCancelButton: [true, false], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts new file mode 100644 index 000000000000..48201435751e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts @@ -0,0 +1,77 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - actionSheet', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { dataSource: [{ text: 'Call' }, { text: 'Send message' }, { text: 'Edit' }] }); + await a11yCheck(page, {}, '#container'); + }); + + test('action sheet with title', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { + dataSource: [{ text: 'Call' }, { text: 'Send message' }], + title: 'Actions', + showTitle: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('action sheet without cancel button', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { + dataSource: [{ text: 'Call' }, { text: 'Send message' }], + showCancelButton: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('empty action sheet', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { dataSource: [] }); + await a11yCheck(page, {}, '#container'); + }); + + test('action sheet with cancelText', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { dataSource: [{ text: 'Call' }, { text: 'Send message' }], showCancelButton: true, cancelText: 'Cancel' }); + await a11yCheck(page, {}, '#container'); + }); + + test('action sheet empty with title', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { dataSource: [], title: 'title', showTitle: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('action sheet with disabled item', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { + dataSource: [{ text: 'Call' }, { text: 'Delete', disabled: true }], + title: 'Actions', + showTitle: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('action sheet with single item', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { dataSource: [{ text: 'Confirm' }], showCancelButton: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('action sheet without cancel and with title', async ({ page }) => { + await createWidget(page, 'dxActionSheet', { + dataSource: [{ text: 'Call' }, { text: 'Send message' }], + showCancelButton: false, + showTitle: true, + title: 'Choose action', + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.matrix.spec.ts new file mode 100644 index 000000000000..6bea5873692f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.matrix.spec.ts @@ -0,0 +1,63 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = ['Item_1', 'Item_2', 'Item_3']; + +const baseOptions = { + dataSource: [[], items], + placeholder: [undefined, 'placeholder'], + value: [undefined, 'Item_1'], + disabled: [true, false], + readOnly: [true, false], + searchTimeout: [0], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +const buttonsOptions = { + dataSource: [[], items], + value: ['Item_1'], + label: [undefined, 'label'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +test.describe('Accessibility - autocomplete matrix (deferred)', () => { + testAccessibilityMatrix({ + component: 'dxAutocomplete', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [true, false], + deferRendering: [true], + }, + }); +}); + +test.describe('Accessibility - autocomplete matrix (no deferred)', () => { + testAccessibilityMatrix({ + component: 'dxAutocomplete', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [false], + deferRendering: [false], + }, + }); +}); + +test.describe('Accessibility - autocomplete matrix (buttons)', () => { + testAccessibilityMatrix({ + component: 'dxAutocomplete', + containerUrl, + a11yCheckConfig: {}, + options: { + ...buttonsOptions, + showClearButton: [true, false], + showDropDownButton: [true, false], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts new file mode 100644 index 000000000000..77588e0340a3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts @@ -0,0 +1,77 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - autocomplete', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete empty datasource', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: [], inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete disabled', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], disabled: true, inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete readOnly', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], readOnly: true, inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete with value', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], value: 'Item_1', inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete with placeholder', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete opened with deferRendering', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], opened: true, deferRendering: true, inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete with label', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], value: 'Item_1', label: 'label', inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete with showClearButton', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], value: 'Item_1', showClearButton: true, inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete with showDropDownButton', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { dataSource: ['Item_1', 'Item_2', 'Item_3'], value: 'Item_1', showDropDownButton: true, inputAttr: { 'aria-label': 'aria-label' }, searchTimeout: 0 }); + await a11yCheck(page, {}, '#container'); + }); + + test('autocomplete with custom button', async ({ page }) => { + await createWidget(page, 'dxAutocomplete', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + value: 'Item_1', + inputAttr: { 'aria-label': 'aria-label' }, + searchTimeout: 0, + buttons: [{ name: 'custom', location: 'before', options: { text: 'Custom', stylingMode: 'text' } }], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/button.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/button.matrix.spec.ts new file mode 100644 index 000000000000..baf484f8e3c8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/button.matrix.spec.ts @@ -0,0 +1,19 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - button matrix', () => { + testAccessibilityMatrix({ + component: 'dxButton', + containerUrl, + a11yCheckConfig: {}, + options: { + useSubmitBehavior: [true, false], + disabled: [true, false], + icon: [undefined, 'user'], + text: ['button text'], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts new file mode 100644 index 000000000000..e22a05ea16b8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts @@ -0,0 +1,66 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - button', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'text', icon: 'user' }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button disabled', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'text', disabled: true }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button with text only', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Click me', elementAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button with icon only', async ({ page }) => { + await createWidget(page, 'dxButton', { icon: 'user', elementAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button with useSubmitBehavior', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Submit', useSubmitBehavior: true }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button disabled with icon only', async ({ page }) => { + await createWidget(page, 'dxButton', { icon: 'save', disabled: true, elementAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button with stylingMode outlined', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Outlined', stylingMode: 'outlined' }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button with stylingMode text', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Text Button', stylingMode: 'text' }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button with type danger', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Delete', type: 'danger' }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); + + test('button with type success', async ({ page }) => { + await createWidget(page, 'dxButton', { text: 'Save', type: 'success' }); + await a11yCheck(page, { rules: { 'nested-interactive': { enabled: false } } }, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts new file mode 100644 index 000000000000..018096ae84f7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts @@ -0,0 +1,75 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - buttonGroup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [{ text: 'text_1' }, { text: 'text_2' }], selectionMode: 'single' }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup empty', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [], selectionMode: 'single' }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup disabled', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [{ text: 'text_1' }, { text: 'text_2' }], disabled: true, selectionMode: 'single' }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup multiple selection mode', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [{ text: 'text_1' }, { text: 'text_2' }], selectionMode: 'multiple', selectedItemKeys: ['text_1'] }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup none selection mode', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [{ text: 'text_1' }, { text: 'text_2' }], selectionMode: 'none' }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup with icons', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [{ icon: 'user' }, { icon: 'check' }], selectionMode: 'single' }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup with disabled item', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [{ text: 'text_1' }, { text: 'text_2', disabled: true }], selectionMode: 'single' }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup with selected items in multiple mode', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { + items: [{ text: 'Bold', icon: 'bold' }, { text: 'Italic', icon: 'italic' }, { text: 'Underline', icon: 'underline' }], + selectionMode: 'multiple', + selectedItemKeys: ['Bold', 'Italic'], + keyExpr: 'text', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup with stylingMode text', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { + items: [{ text: 'Left' }, { text: 'Center' }, { text: 'Right' }], + selectionMode: 'single', + stylingMode: 'text', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('buttonGroup with single item', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { items: [{ text: 'Only Item' }], selectionMode: 'single' }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts new file mode 100644 index 000000000000..396203ad2990 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts @@ -0,0 +1,108 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - calendar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'month' }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with century zoom level', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'century' }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with decade zoom level', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'decade' }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar disabled', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'month', disabled: true }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar readOnly', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'month', readOnly: true }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with multiple selection mode', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = Date.now(); + await createWidget(page, 'dxCalendar', { + zoomLevel: 'month', + selectionMode: 'multiple', + value: [now, now + msInDay], + showWeekNumbers: true, + }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with range selection mode and today button', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = Date.now(); + await createWidget(page, 'dxCalendar', { + zoomLevel: 'month', + selectionMode: 'range', + value: [now, now + msInDay], + showTodayButton: true, + }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with year zoom level', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'year' }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with name', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'month', name: 'name' }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar disabled with century zoom level', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'century', disabled: true }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar range selection without today button', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = Date.now(); + await createWidget(page, 'dxCalendar', { + zoomLevel: 'month', + selectionMode: 'range', + value: [now, now + msInDay], + showTodayButton: false, + showWeekNumbers: false, + }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with first day of week set', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'month', firstDayOfWeek: 1 }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar with week numbers and year zoom level', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'year', showWeekNumbers: true }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); + + test('calendar readOnly with selected value', async ({ page }) => { + await createWidget(page, 'dxCalendar', { zoomLevel: 'month', readOnly: true, value: Date.now() }); + await a11yCheck(page, { rules: { 'empty-table-header': { enabled: false } } }, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts new file mode 100644 index 000000000000..e87199cb935e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts @@ -0,0 +1,57 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView columnChooser', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test("column chooser in 'select' mode", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { enabled: true, mode: 'select', height: 400, width: 400 }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2' }, + { dataField: 'Column 4' }, + ], + }); + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + await a11yCheck(page, {}, '#container'); + }); + + test("column chooser in 'dragAndDrop' mode", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { enabled: true, mode: 'dragAndDrop', height: 400, width: 400 }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 4', visible: false }, + ], + }); + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + await a11yCheck(page, {}, '#container'); + }); + + test('cardView with opened columnChooser', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: Array.from({ length: 50 }, (_, i) => ({ value: `value_${i}` })), + columnChooser: { enabled: true }, + columns: [{ dataField: 'value' }], + }); + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts new file mode 100644 index 000000000000..c01ecfe69e0d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts @@ -0,0 +1,30 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView columnSortable', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('column sortable accessibility check', async ({ page }) => { + await createWidget(page, 'dxCardView', { + allowColumnReordering: true, + columnChooser: { enabled: true }, + headerFilter: { visible: true }, + columns: [{ + dataField: 'test', + allowReordering: true, + sortOrder: 'asc', + }], + }); + await a11yCheck(page, { rules: { 'color-contrast': { enabled: false } } }, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts new file mode 100644 index 000000000000..a81e40410648 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts @@ -0,0 +1,52 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView cover', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const coverData = [ + { + ID: 1, FirstName: 'John', LastName: 'Heart', Position: 'CEO', + }, + { + ID: 2, FirstName: 'Olivia', LastName: 'Peyton', Position: 'Sales Assistant', + }, + { + ID: 3, FirstName: 'Robert', LastName: 'Reagan', Position: 'CMO', + }, + ]; + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['FirstName', 'LastName'], + dataSource: coverData, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with cardCover and altExpr', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['FirstName', 'Position'], + cardCover: { + imageExpr: () => undefined, + altExpr: 'FirstName', + }, + dataSource: coverData, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts new file mode 100644 index 000000000000..64d33aa3035f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts @@ -0,0 +1,56 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView editing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const baseData = [ + { id: 1, name: 'Item 1', value: 100 }, + { id: 2, name: 'Item 2', value: 200 }, + { id: 3, name: 'Item 3', value: 300 }, + ]; + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['name', 'value'], + dataSource: baseData, + keyExpr: 'id', + editing: { allowUpdating: true, allowDeleting: true, allowAdding: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('add card popup', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['name', 'value'], + dataSource: baseData, + keyExpr: 'id', + editing: { allowUpdating: true, allowDeleting: true, allowAdding: true }, + }); + await page.click('.dx-cardview-add-button, .dx-toolbar .dx-button[aria-label="Add"], .dx-addrow-button'); + await page.waitForTimeout(300); + await a11yCheck(page, {}, '#container'); + }); + + test('edit card popup', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['name', 'value'], + dataSource: baseData, + keyExpr: 'id', + editing: { allowUpdating: true, allowDeleting: true, allowAdding: true }, + }); + await page.click('.dx-cardview-edit-button, .dx-card .dx-button[aria-label="Edit"]'); + await page.waitForTimeout(300); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts new file mode 100644 index 000000000000..d0d6a8b9c6cd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts @@ -0,0 +1,29 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView filterPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filter panel accessibility check', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { name: 'Item 1', title: 'Mr.' }, + { name: 'Item 2', title: 'Mrs.' }, + ], + columns: ['name', 'title'], + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts new file mode 100644 index 000000000000..d4f75f1032b3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts @@ -0,0 +1,60 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView headerFilter', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const headerFilterData = [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ]; + + test('header filter accessibility check', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: headerFilterData, + columns: ['A', 'B', 'C'], + headerFilter: { visible: true }, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('header filter with search enabled', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: headerFilterData, + columns: ['A', 'B', 'C'], + headerFilter: { visible: true, search: { enabled: true } }, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('header filter with date column (tree mode)', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: '2024-01-01', B: 'B_0', C: 'C_0' }, + { A: '2024-01-01', B: 'B_1', C: 'C_1' }, + { A: '2025-01-01', B: 'B_2', C: 'C_2' }, + { A: '2025-01-01', B: 'B_3', C: 'C_3' }, + { A: '2026-01-01', B: 'B_4', C: 'C_4' }, + ], + columns: [{ dataField: 'A', dataType: 'date' }, 'B', 'C'], + headerFilter: { visible: true }, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts new file mode 100644 index 000000000000..8351ec539b33 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts @@ -0,0 +1,42 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView headerPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0', B: 'B_0' }], + columns: ['A', 'B'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with header filter', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0', B: 'B_0' }], + columns: ['A', 'B'], + headerFilter: { visible: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with sorting', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0', B: 'B_0' }], + columns: [{ dataField: 'A', sortOrder: 'asc' }, 'B'], + sorting: { mode: 'single' }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts new file mode 100644 index 000000000000..fec94470b888 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts @@ -0,0 +1,24 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView noData', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [], + columns: ['A', 'B'], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts new file mode 100644 index 000000000000..f845419b835a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts @@ -0,0 +1,59 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView pager', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const pagedData = Array.from({ length: 20 }, (_, i) => ({ text: i.toString(), value: i })); + + test('pager accessibility check', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: Array.from({ length: 50 }, (_, i) => ({ value: `value_${i}` })), + columns: [{ dataField: 'value' }], + paging: { pageSize: 10 }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('pager with full display mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: pagedData, + columns: ['text', 'value'], + paging: { pageSize: 2, pageIndex: 5 }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [2, 3, 4], + showInfo: true, + showNavigationButtons: true, + displayMode: 'full', + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('pager with compact display mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: pagedData, + columns: ['text', 'value'], + paging: { pageSize: 2, pageIndex: 3 }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [2, 3, 4], + showInfo: true, + showNavigationButtons: true, + displayMode: 'compact', + }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts new file mode 100644 index 000000000000..a707c5c255ae --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts @@ -0,0 +1,53 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView search', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const searchData = [ + { id: 1, firstName: 'Darin', lastName: 'Heritege', email: 'dheritege0@jugem.jp', gender: 'Male' }, + { id: 2, firstName: 'Aeriel', lastName: 'Giggs', email: 'agiggs1@hubpages.com', gender: 'Female' }, + { id: 3, firstName: 'Theo', lastName: 'Aleksidze', email: 'taleksidze2@patch.com', gender: 'Female' }, + { id: 4, firstName: 'Dalli', lastName: 'Ashwood', email: 'dashwood3@buzzfeed.com', gender: 'Male' }, + { id: 5, firstName: 'Paule', lastName: 'Pidgeley', email: 'ppidgeley4@upenn.edu', gender: 'Female' }, + ]; + + test('search accessibility check', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0' }, { A: 'A_1' }], + columns: ['A'], + searchPanel: { visible: true, text: 'A_0' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('search with visible panel and no text', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: searchData, + columns: ['id', 'firstName', 'lastName', 'email', 'gender'], + searchPanel: { visible: true }, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('highlighted search text', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: searchData, + columns: ['id', 'firstName', 'lastName', 'email', 'gender'], + searchPanel: { visible: true, text: 'da' }, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts new file mode 100644 index 000000000000..24682cbdad43 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts @@ -0,0 +1,111 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const selectionData = [ + { id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0' }, + { id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1' }, + { id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2' }, + { id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3' }, + { id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4' }, + ]; + + test('single mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0], + selection: { mode: 'single' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multiple mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multiple mode with showCheckBoxesMode none', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'none', allowSelectAll: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multiple mode with showCheckBoxesMode onClick', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multiple mode with selected card and showCheckBoxesMode onClick', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + selectedCardKeys: [0], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multiple mode with selected cards and showCheckBoxesMode onClick', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + selectedCardKeys: [0, 1], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multiple mode without allowSelectAll', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', allowSelectAll: false }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts new file mode 100644 index 000000000000..1626fbb0e17d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts @@ -0,0 +1,26 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView sortable', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('sortable accessibility check', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, A: 'A_0' }, { id: 2, A: 'A_1' }, { id: 3, A: 'A_2' }], + keyExpr: 'id', + columns: ['A'], + rowDragging: { allowReordering: true }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.spec.ts new file mode 100644 index 000000000000..8c15fb99a655 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.spec.ts @@ -0,0 +1,96 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView sorting', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const sortingData = [ + { id: 1, title: 'Mrs.', name: 'Nancy', lastName: 'Davolio' }, + { id: 2, title: 'Dr.', name: 'Andrew', lastName: 'Fuller' }, + { id: 3, title: 'Ms.', name: 'Janet', lastName: 'Leverling' }, + { id: 4, title: 'Mrs.', name: 'Margaret', lastName: 'Peacock' }, + { id: 5, title: 'Mr.', name: 'Steven', lastName: 'Buchanan' }, + ]; + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: sortingData, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multiple sorting', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: sortingData, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name', sortOrder: 'asc' }, + { dataField: 'lastName' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('sort index API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: sortingData, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc', sortIndex: 1 }, + { dataField: 'name', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'lastName' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('showSortIndexes false', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: sortingData, + height: 500, + sorting: { showSortIndexes: false }, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc', sortIndex: 1 }, + { dataField: 'name', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'lastName' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('allowSorting false on column', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: sortingData, + height: 500, + sorting: { showSortIndexes: false }, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc', sortIndex: 1, allowSorting: false }, + { dataField: 'name', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'lastName' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts new file mode 100644 index 000000000000..7ec6580aa588 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts @@ -0,0 +1,78 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - chat', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxChat', { items: [], user: { id: 1, name: 'User' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('chat with messages', async ({ page }) => { + const userWithAvatar = { id: 1, name: 'User With Avatar' }; + const userWithoutAvatar = { id: 2, name: 'User Without Avatar' }; + await createWidget(page, 'dxChat', { + items: [ + { timestamp: new Date(), text: 'Message text', author: userWithAvatar }, + { timestamp: new Date(), text: 'Message text', author: userWithoutAvatar }, + ], + user: userWithAvatar, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('chat with typing users', async ({ page }) => { + const user = { id: 1, name: 'User' }; + await createWidget(page, 'dxChat', { + items: [], + user, + typingUsers: [{ id: 2, name: 'Other User' }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('chat with different user', async ({ page }) => { + await createWidget(page, 'dxChat', { items: [], user: { id: 2, name: 'User Without Avatar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('chat disabled', async ({ page }) => { + await createWidget(page, 'dxChat', { items: [], user: { id: 1, name: 'User' }, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('chat with multiple messages from same author', async ({ page }) => { + const user = { id: 1, name: 'User' }; + const other = { id: 2, name: 'Other' }; + await createWidget(page, 'dxChat', { + user, + items: [ + { timestamp: new Date(), text: 'Hello', author: other }, + { timestamp: new Date(), text: 'How are you?', author: other }, + { timestamp: new Date(), text: 'Fine, thanks!', author: user }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('chat with many typing users', async ({ page }) => { + const user = { id: 1, name: 'User' }; + await createWidget(page, 'dxChat', { + items: [], + user, + typingUsers: [{ id: 2, name: 'Alice' }, { id: 3, name: 'Bob' }], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts new file mode 100644 index 000000000000..c2c7d628ae89 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts @@ -0,0 +1,66 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - checkBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox unchecked', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: false, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox indeterminate with three-state', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: null, enableThreeStateBehavior: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox disabled', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: true, disabled: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox readOnly', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: true, readOnly: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox with text', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: true, text: 'text', elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox disabled unchecked', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: false, disabled: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox readOnly unchecked', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: false, readOnly: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox with name', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: true, name: 'checkboxName', enableThreeStateBehavior: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('checkBox indeterminate disabled', async ({ page }) => { + await createWidget(page, 'dxCheckBox', { value: null, enableThreeStateBehavior: true, disabled: true, elementAttr: { 'aria-label': 'Checked' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.matrix.spec.ts new file mode 100644 index 000000000000..10965ecebef1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.matrix.spec.ts @@ -0,0 +1,72 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const baseOptions = { + value: [undefined, '#f05b41'], + disabled: [true, false], + readOnly: [true, false], + placeholder: [undefined, 'placeholder'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +const buttonsOptions = { + value: ['#f05b41'], + label: [undefined, 'label'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +test.describe('Accessibility - colorBox matrix (deferred)', () => { + testAccessibilityMatrix({ + component: 'dxColorBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [true, false], + deferRendering: [true], + }, + }); +}); + +test.describe('Accessibility - colorBox matrix (no deferred)', () => { + testAccessibilityMatrix({ + component: 'dxColorBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [false], + deferRendering: [false], + }, + }); +}); + +test.describe('Accessibility - colorBox matrix (alpha channel)', () => { + testAccessibilityMatrix({ + component: 'dxColorBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [true], + editAlphaChannel: [true], + deferRendering: [true], + }, + }); +}); + +test.describe('Accessibility - colorBox matrix (buttons)', () => { + testAccessibilityMatrix({ + component: 'dxColorBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...buttonsOptions, + showClearButton: [true, false], + showDropDownButton: [true, false], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts new file mode 100644 index 000000000000..86e2a0f80c06 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts @@ -0,0 +1,76 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - colorBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox opened', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', opened: true, deferRendering: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox with alpha channel', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', opened: true, editAlphaChannel: true, deferRendering: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox disabled', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox readOnly', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox with showClearButton', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', showClearButton: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox with label', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', label: 'label', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox without deferRendering', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', opened: false, deferRendering: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox without value', async ({ page }) => { + await createWidget(page, 'dxColorBox', { inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox with placeholder', async ({ page }) => { + await createWidget(page, 'dxColorBox', { placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox without showDropDownButton', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', showDropDownButton: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('colorBox with custom button', async ({ page }) => { + await createWidget(page, 'dxColorBox', { value: '#f05b41', buttons: [{ name: 'today', location: 'before', options: { text: 'Today', stylingMode: 'text' } }], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts new file mode 100644 index 000000000000..e96076701b44 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts @@ -0,0 +1,101 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - contextMenu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { target: '#container', items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }] }); + await a11yCheck(page, {}, '#container'); + }); + + test('context menu with single selection mode', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { + target: '#container', + items: [{ text: 'Item 1' }, { text: 'Item 2' }, { text: 'Item 3' }], + selectionMode: 'single', + }); + await page.evaluate(() => { + (window as any).$('#container').dxContextMenu('show'); + }); + await a11yCheck(page, {}, '#container'); + }); + + test('disabled context menu', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { + target: '#container', + items: [{ text: 'Item 1' }, { text: 'Item 2' }], + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('context menu with submenu items', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { + target: '#container', + items: [ + { text: 'Item 1', items: [{ text: 'Sub 1' }, { text: 'Sub 2' }] }, + { text: 'Item 2' }, + ], + }); + await page.evaluate(() => { + (window as any).$('#container').dxContextMenu('show'); + }); + await a11yCheck(page, {}, '#container'); + }); + + test('context menu none selectionMode shown', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { + target: '#container', + items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }, { text: 'coffee', icon: 'coffee' }], + selectionMode: 'none', + }); + await page.evaluate(() => { + (window as any).$('#container').dxContextMenu('show'); + }); + await a11yCheck(page, {}, '#container'); + }); + + test('context menu with disabled item shown', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { + target: '#container', + items: [{ text: 'Item 1' }, { text: 'Item 2', disabled: true }, { text: 'Item 3' }], + }); + await page.evaluate(() => { + (window as any).$('#container').dxContextMenu('show'); + }); + await a11yCheck(page, {}, '#container'); + }); + + test('context menu hidden', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { + target: '#container', + items: [{ text: 'Item 1' }, { text: 'Item 2' }], + visible: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('context menu with single selection mode and selected item', async ({ page }) => { + await createWidget(page, 'dxContextMenu', { + target: '#container', + items: [{ text: 'Item 1' }, { text: 'Item 2' }, { text: 'Item 3' }], + selectionMode: 'single', + selectedItem: { text: 'Item 1' }, + }); + await page.evaluate(() => { + (window as any).$('#container').dxContextMenu('show'); + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts new file mode 100644 index 000000000000..8c0df8156368 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts @@ -0,0 +1,301 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +function getData(rowCount: number, fieldCount: number): Record[] { + return Array.from({ length: rowCount }, (_, rowIdx) => { + const row: Record = {}; + for (let colIdx = 0; colIdx < fieldCount; colIdx += 1) { + row[`field_${colIdx}`] = `val_${rowIdx}_${colIdx}`; + } + return row; + }); +} + +test.describe('Accessibility - DataGrid common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('empty grid', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { dataSource: [] }); + await a11yCheck(page, {}, '#container'); + }); + + test('grid with data', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grid with paging', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(100, 5), + keyExpr: 'field_0', + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + paging: { pageSize: 5 }, + pager: { + visible: true, + allowedPageSizes: [5, 10], + showPageSizeSelector: true, + showInfo: true, + showNavigationButtons: true, + displayMode: 'full', + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grid with selection', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + selection: { mode: 'multiple', showCheckBoxesMode: 'always' }, + selectedRowKeys: ['val_1_0', 'val_2_0'], + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grid with search panel', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + searchPanel: { visible: true }, + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grid without data with columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + columns: ['test'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('sorting and group panel', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + groupPanel: { visible: true }, + columns: [ + 'field_0', + 'field_1', + 'field_2', + { dataField: 'field_3', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'field_4', sortOrder: 'desc', sortIndex: 1 }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('paging with compact displayMode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(100, 5), + keyExpr: 'field_0', + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + paging: { pageSize: 5 }, + pager: { + visible: true, + allowedPageSizes: [5, 10, 'all'], + showPageSizeSelector: true, + showInfo: true, + showNavigationButtons: true, + displayMode: 'compact', + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grouping and summary', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(60, 5), + keyExpr: 'field_0', + columns: [ + 'field_0', + { dataField: 'field_1', groupIndex: 0 }, + { dataField: 'field_2', groupIndex: 1 }, + 'field_3', + 'field_4', + ], + paging: { pageSize: 10 }, + groupPanel: { visible: true }, + summary: { + groupItems: [{ + column: 'field_3', + summaryType: 'count', + showInGroupFooter: true, + }, { + column: 'field_4', + summaryType: 'count', + showInGroupFooter: false, + alignByColumn: true, + }], + totalItems: [{ column: 'field_0', summaryType: 'count' }], + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('search panel with highlight', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + searchPanel: { visible: true, text: 'val' }, + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('focused row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + focusedRowEnabled: true, + focusedRowKey: 'val_1_0', + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('row drag and drop', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + rowDragging: { allowReordering: true, showDragIcons: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('filter row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5).map((item, index) => ({ ...item, index })), + keyExpr: 'field_0', + filterRow: { visible: true }, + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('header filter', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + headerFilter: { visible: true }, + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('filter panel', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + filterPanel: { visible: true }, + columns: [ + 'field_0', + { dataField: 'field_1', filterValue: 'val' }, + 'field_2', + 'field_3', + 'field_4', + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('column chooser dragAndDrop mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 7), + keyExpr: 'field_0', + columnChooser: { enabled: true, mode: 'dragAndDrop' }, + columns: [ + { dataField: 'field_0', visible: false }, + { dataField: 'field_1', visible: false }, + 'field_2', 'field_3', 'field_4', 'field_5', 'field_6', + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('column chooser select mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 7), + keyExpr: 'field_0', + columnChooser: { enabled: true, mode: 'select' }, + columns: [ + { dataField: 'field_0', visible: false }, + { dataField: 'field_1', visible: false }, + 'field_2', 'field_3', 'field_4', 'field_5', 'field_6', + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('adaptability', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 10), + keyExpr: 'field_0', + columnWidth: 100, + width: 800, + columnHidingEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('export button', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + export: { + enabled: true, + formats: ['xlsx', 'pdf'], + allowExportSelectedData: true, + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('row editing with useIcons false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'row', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + useIcons: false, + }, + columns: ['field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('fixed columns legacy mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 7), + keyExpr: 'field_0', + columnFixing: { legacyMode: true } as any, + columns: [ + { dataField: 'field_0', fixed: true }, + { dataField: 'field_1', fixed: true }, + 'field_2', 'field_3', 'field_4', + { dataField: 'field_5', fixed: true, fixedPosition: 'right' }, + { dataField: 'field_6', fixed: true, fixedPosition: 'right' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts new file mode 100644 index 000000000000..542b27b9c380 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts @@ -0,0 +1,177 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +function getData(rowCount: number, fieldCount: number): Record[] { + return Array.from({ length: rowCount }, (_, rowIdx) => { + const row: Record = {}; + for (let colIdx = 0; colIdx < fieldCount; colIdx += 1) { + row[`field_${colIdx}`] = `val_${rowIdx}_${colIdx}`; + } + return row; + }); +} + +test.describe('Accessibility - DataGrid editing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('row editing mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'row', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: ['field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('batch editing mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'batch', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: ['field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('cell editing mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'cell', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: ['field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form editing mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'form', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: ['field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup editing mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'popup', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: ['field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('row editing mode with useIcons', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'row', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + useIcons: true, + }, + columns: ['field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('embedded editors in cell edit mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(3, 2), + height: 400, + showBorders: true, + editing: { mode: 'cell', allowUpdating: true, allowAdding: true }, + toolbar: { items: [{ name: 'addRowButton', showText: 'always' }] }, + }); + await page.click('.dx-datagrid-addrow-button'); + await a11yCheck(page, {}, '#container'); + }); + + test('embedded editors in batch edit mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(3, 2), + height: 400, + showBorders: true, + editing: { mode: 'batch', allowUpdating: true, allowAdding: true }, + toolbar: { items: [{ name: 'addRowButton', showText: 'always' }] }, + }); + await page.click('.dx-datagrid-addrow-button'); + await a11yCheck(page, {}, '#container'); + }); + + test('embedded editors in row edit mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(3, 2), + height: 400, + showBorders: true, + editing: { mode: 'row', allowUpdating: true, allowAdding: true }, + toolbar: { items: [{ name: 'addRowButton', showText: 'always' }] }, + }); + await page.click('.dx-datagrid-addrow-button'); + await a11yCheck(page, {}, '#container'); + }); + + test('embedded editors in form edit mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(3, 2), + height: 400, + showBorders: true, + editing: { mode: 'form', allowUpdating: true, allowAdding: true }, + toolbar: { items: [{ name: 'addRowButton', showText: 'always' }] }, + }); + await page.click('.dx-datagrid-addrow-button'); + await a11yCheck(page, {}, '#container'); + }); + + test('embedded editors in popup edit mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(3, 2), + height: 400, + showBorders: true, + editing: { mode: 'popup', allowUpdating: true, allowAdding: true }, + toolbar: { items: [{ name: 'addRowButton', showText: 'always' }] }, + }); + await page.click('.dx-datagrid-addrow-button'); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts new file mode 100644 index 000000000000..0842c6452c39 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts @@ -0,0 +1,91 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +function getData(rowCount: number, fieldCount: number): Record[] { + return Array.from({ length: rowCount }, (_, rowIdx) => { + const row: Record = {}; + for (let colIdx = 0; colIdx < fieldCount; colIdx += 1) { + row[`field_${colIdx}`] = `val_${rowIdx}_${colIdx}`; + } + return row; + }); +} + +test.describe('Accessibility - DataGrid fixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('fixed columns accessibility check', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 7), + keyExpr: 'field_0', + columns: [ + { dataField: 'field_0', fixed: true }, + { dataField: 'field_1', fixed: true }, + 'field_2', 'field_3', 'field_4', + { dataField: 'field_5', fixed: true, fixedPosition: 'right' }, + { dataField: 'field_6', fixed: true, fixedPosition: 'right' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('fixed columns with scrollable content', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + columnWidth: 150, + width: 800, + keyExpr: 'id', + scrolling: { useNative: true }, + dataSource: [ + { id: 0, column1: 'a', column2: 'a', column3: 'a', column4: 'a', column5: 'a', column6: 'a', column7: 'a', column8: 'a' }, + { id: 1, column1: 'a', column2: 'a', column3: 'a', column4: 'a', column5: 'a', column6: 'a', column7: 'a', column8: 'a' }, + ], + columnFixing: { legacyMode: true } as any, + columns: [ + { dataField: 'column1', fixed: true }, + { dataField: 'column2', fixed: true }, + { dataField: 'column3' }, + { dataField: 'column4' }, + { dataField: 'column5' }, + { dataField: 'column6' }, + { dataField: 'column7', fixed: true, fixedPosition: 'right' }, + { dataField: 'column8', fixed: true, fixedPosition: 'right' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('fixed columns without left fixed columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + columnWidth: 150, + width: 800, + keyExpr: 'id', + scrolling: { useNative: true }, + dataSource: [ + { id: 0, column1: 'a', column2: 'a', column3: 'a', column4: 'a', column5: 'a', column6: 'a', column7: 'a', column8: 'a' }, + { id: 1, column1: 'a', column2: 'a', column3: 'a', column4: 'a', column5: 'a', column6: 'a', column7: 'a', column8: 'a' }, + ], + columnFixing: { legacyMode: true } as any, + columns: [ + { dataField: 'column1', fixed: false }, + { dataField: 'column2', fixed: false }, + { dataField: 'column3' }, + { dataField: 'column4' }, + { dataField: 'column5' }, + { dataField: 'column6' }, + { dataField: 'column7', fixed: true, fixedPosition: 'right' }, + { dataField: 'column8', fixed: true, fixedPosition: 'right' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts new file mode 100644 index 000000000000..9744eeeaef92 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts @@ -0,0 +1,70 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +function getData(rowCount: number, fieldCount: number): Record[] { + return Array.from({ length: rowCount }, (_, rowIdx) => { + const row: Record = {}; + for (let colIdx = 0; colIdx < fieldCount; colIdx += 1) { + row[`field_${colIdx}`] = `val_${rowIdx}_${colIdx}`; + } + return row; + }); +} + +test.describe('Accessibility - DataGrid scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('virtual scrolling accessibility check', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(100, 5), + keyExpr: 'field_0', + height: 400, + scrolling: { mode: 'virtual' }, + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('infinite scrolling', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(1000, 2), + height: 400, + showBorders: true, + scrolling: { mode: 'infinite' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('horizontal virtual scrolling', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 100), + columnWidth: 100, + height: 400, + width: 900, + showBorders: true, + scrolling: { columnRenderingMode: 'virtual' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('standard scrolling with paging', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(50, 5), + height: 400, + showBorders: true, + scrolling: { mode: 'standard' }, + paging: { enabled: true, pageSize: 10 }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts new file mode 100644 index 000000000000..d152df05487a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts @@ -0,0 +1,90 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +function getData(rowCount: number, fieldCount: number): Record[] { + return Array.from({ length: rowCount }, (_, rowIdx) => { + const row: Record = {}; + for (let colIdx = 0; colIdx < fieldCount; colIdx += 1) { + row[`field_${colIdx}`] = `val_${rowIdx}_${colIdx}`; + } + return row; + }); +} + +test.describe('Accessibility - DataGrid status', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const statusDataSource = [ + { id: 0, label: 'A', value: 350 }, + { id: 1, label: 'B', value: 1200 }, + { id: 2, label: 'C', value: 750 }, + ]; + + test('grid status accessibility check', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('general status with basic data', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: statusDataSource, + keyExpr: 'id', + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('general status with header filter visible', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: statusDataSource, + keyExpr: 'id', + headerFilter: { visible: true }, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('general status with filter row visible', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: statusDataSource, + keyExpr: 'id', + filterRow: { visible: true, applyFilter: 'onClick' }, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('general status with search panel visible', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: statusDataSource, + keyExpr: 'id', + searchPanel: { visible: true }, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('general status with column chooser select mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: statusDataSource, + keyExpr: 'id', + columnChooser: { enabled: true, mode: 'select' }, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts new file mode 100644 index 000000000000..591ae1dcf989 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts @@ -0,0 +1,81 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +function getData(rowCount: number, fieldCount: number): Record[] { + return Array.from({ length: rowCount }, (_, rowIdx) => { + const row: Record = {}; + for (let colIdx = 0; colIdx < fieldCount; colIdx += 1) { + row[`field_${colIdx}`] = `val_${rowIdx}_${colIdx}`; + } + return row; + }); +} + +test.describe('Accessibility - DataGrid templates', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('grid templates accessibility check', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 3), + keyExpr: 'field_0', + columns: ['field_0', 'field_1', 'field_2'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dataRowTemplate', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(10)].map((_, i) => ({ + ID: i + 1, + CompanyName: `company name ${i + 1}`, + City: `city ${i + 1}`, + Notes: `test ${i + 1}`, + })), + keyExpr: 'ID', + columns: ['ID', 'CompanyName', 'City'], + dataRowTemplate: (container, { data }) => { + const markup = `${data.ID}${data.CompanyName}${data.City}
${data.Notes}
`; + container.append(markup); + }, + rowAlternationEnabled: true, + columnAutoWidth: true, + showBorders: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('column header template', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 3), + keyExpr: 'field_0', + columns: [ + { dataField: 'field_0', headerCellTemplate: (container) => { container.textContent = 'Custom Header'; } }, + 'field_1', + 'field_2', + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with row alternation and borders', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 4), + keyExpr: 'field_0', + columns: ['field_0', 'field_1', 'field_2', 'field_3'], + rowAlternationEnabled: true, + showBorders: true, + columnAutoWidth: true, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.matrix.spec.ts new file mode 100644 index 000000000000..fcb071e40996 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.matrix.spec.ts @@ -0,0 +1,61 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const now = new Date(2024, 0, 15, 10, 30); + +const baseOptions = { + value: [undefined, now], + disabled: [true, false], + readOnly: [true, false], + type: ['date', 'time', 'datetime'], + placeholder: [undefined, 'placeholder'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +const buttonsOptions = { + value: [now], + label: [undefined, 'label'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +test.describe('Accessibility - dateBox matrix (deferred)', () => { + testAccessibilityMatrix({ + component: 'dxDateBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [true, false], + deferRendering: [true], + }, + }); +}); + +test.describe('Accessibility - dateBox matrix (no deferred)', () => { + testAccessibilityMatrix({ + component: 'dxDateBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [false], + deferRendering: [false], + }, + }); +}); + +test.describe('Accessibility - dateBox matrix (buttons)', () => { + testAccessibilityMatrix({ + component: 'dxDateBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...buttonsOptions, + showClearButton: [true, false], + showDropDownButton: [true, false], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts new file mode 100644 index 000000000000..d89dc3f33b76 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts @@ -0,0 +1,91 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dateBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox type time', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'time', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox type datetime', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'datetime', value: new Date(), inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox disabled', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox readOnly', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox with showClearButton', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', value: new Date(), showClearButton: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox with label', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', value: new Date(), label: 'label', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox opened date type', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', value: new Date(), opened: true, deferRendering: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox opened time type', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'time', value: new Date(), opened: true, deferRendering: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox with placeholder', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox with showDropDownButton false', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', value: new Date(), showDropDownButton: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox with name', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', name: 'dateBox', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox disabled with value', async ({ page }) => { + await createWidget(page, 'dxDateBox', { type: 'date', value: new Date(), disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateBox type date with custom button', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + type: 'date', + value: new Date(), + inputAttr: { 'aria-label': 'aria-label' }, + buttons: [{ name: 'today', location: 'before', options: { text: 'Today', stylingMode: 'text' } }], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts new file mode 100644 index 000000000000..ce44472535dc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts @@ -0,0 +1,134 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dateRangeBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { endDateInputAttr: { 'aria-label': 'aria-label' }, startDateInputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox with value', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox disabled', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { disabled: true, endDateInputAttr: { 'aria-label': 'aria-label' }, startDateInputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox readOnly', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { readOnly: true, endDateInputAttr: { 'aria-label': 'aria-label' }, startDateInputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox opened', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + opened: true, + deferRendering: true, + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox with showClearButton', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { showClearButton: true, showDropDownButton: true, endDateInputAttr: { 'aria-label': 'aria-label' }, startDateInputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox opened without multiView', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + opened: true, + multiView: false, + deferRendering: true, + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox with startDatePlaceholder', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + startDatePlaceholder: 'Start date', + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox with custom button', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + buttons: [{ name: 'custom', location: 'before', options: { text: 'Custom', stylingMode: 'text' } }], + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox with label', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + label: 'Date Range', + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox readOnly with value', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + readOnly: true, + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dateRangeBox disabled with value', async ({ page }) => { + const msInDay = 1000 * 60 * 60 * 24; + const now = new Date(); + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(now.getTime() - msInDay * 3), new Date(now.getTime() + msInDay * 3)], + disabled: true, + endDateInputAttr: { 'aria-label': 'aria-label' }, + startDateInputAttr: { 'aria-label': 'aria-label' }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts new file mode 100644 index 000000000000..68183755d546 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts @@ -0,0 +1,110 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - drawer', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxDrawer', { height: 400 }); + await a11yCheck(page, {}, '#container'); + }); + + test('drawer opened with slide reveal mode', async ({ page }) => { + await createWidget(page, 'dxDrawer', { + height: 400, + opened: true, + revealMode: 'slide', + template: () => { + const $drawerContent = (window as any).$('
').width(200).css('height', '100%'); + return $drawerContent; + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('drawer opened with expand reveal mode', async ({ page }) => { + await createWidget(page, 'dxDrawer', { + height: 400, + opened: true, + revealMode: 'expand', + template: () => { + const $drawerContent = (window as any).$('
').width(200).css('height', '100%'); + return $drawerContent; + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('disabled drawer', async ({ page }) => { + await createWidget(page, 'dxDrawer', { height: 400, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('drawer closed with slide reveal mode', async ({ page }) => { + await createWidget(page, 'dxDrawer', { height: 400, opened: false, revealMode: 'slide' }); + await a11yCheck(page, {}, '#container'); + }); + + test('drawer disabled opened', async ({ page }) => { + await createWidget(page, 'dxDrawer', { + height: 400, + disabled: true, + opened: true, + revealMode: 'expand', + template: () => { + const $drawerContent = (window as any).$('
').width(200).css('height', '100%'); + return $drawerContent; + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('drawer with openedStateMode overlap', async ({ page }) => { + await createWidget(page, 'dxDrawer', { + height: 400, + opened: true, + openedStateMode: 'overlap', + template: () => { + const $drawerContent = (window as any).$('
').width(200).css('height', '100%'); + return $drawerContent; + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('drawer with openedStateMode push', async ({ page }) => { + await createWidget(page, 'dxDrawer', { + height: 400, + opened: true, + openedStateMode: 'push', + template: () => { + const $drawerContent = (window as any).$('
').width(200).css('height', '100%'); + return $drawerContent; + }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('drawer with position right', async ({ page }) => { + await createWidget(page, 'dxDrawer', { + height: 400, + opened: true, + position: 'right', + template: () => { + const $drawerContent = (window as any).$('
').width(200).css('height', '100%'); + return $drawerContent; + }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts new file mode 100644 index 000000000000..4099abd3c9e9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts @@ -0,0 +1,106 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dropDownBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { dataSource: ['Item_1', 'Item_2', 'Item_3'], inputAttr: { 'aria-label': 'DropDownBox' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox empty', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { dataSource: [], inputAttr: { 'aria-label': 'DropDownBox' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox disabled', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { dataSource: ['Item_1', 'Item_2', 'Item_3'], disabled: true, inputAttr: { 'aria-label': 'DropDownBox' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox readOnly', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { dataSource: ['Item_1', 'Item_2', 'Item_3'], readOnly: true, inputAttr: { 'aria-label': 'DropDownBox' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox with label and showClearButton', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + value: 'Item_1', + label: 'label', + showClearButton: true, + inputAttr: { 'aria-label': 'DropDownBox' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox opened with deferRendering', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + value: 'Item_1', + opened: true, + deferRendering: true, + inputAttr: { 'aria-label': 'DropDownBox' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox with showDropDownButton', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + value: 'Item_1', + showDropDownButton: true, + inputAttr: { 'aria-label': 'DropDownBox' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox with custom button', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + value: 'Item_1', + inputAttr: { 'aria-label': 'DropDownBox' }, + buttons: [{ name: 'custom', location: 'before', options: { text: 'Custom', stylingMode: 'text' } }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox with placeholder', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + placeholder: 'Select a value', + inputAttr: { 'aria-label': 'DropDownBox' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox with value and readOnly', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + value: 'Item_2', + readOnly: true, + inputAttr: { 'aria-label': 'DropDownBox' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownBox with single item datasource', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: ['OnlyItem'], + value: 'OnlyItem', + inputAttr: { 'aria-label': 'DropDownBox' }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts new file mode 100644 index 000000000000..3344b39ab557 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts @@ -0,0 +1,101 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dropDownButton', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { dataSource: ['Item_1', 'Item_2'], text: 'Download', splitButton: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton without splitButton', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { dataSource: ['Item_1', 'Item_2'], text: 'Download Trial', splitButton: false, showArrowIcon: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton without splitButton and arrow icon', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { dataSource: ['Item_1', 'Item_2'], text: 'Download Trial', splitButton: false, showArrowIcon: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton disabled', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { dataSource: ['Item_1', 'Item_2'], text: 'Download', splitButton: true, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton with useSelectMode', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { dataSource: ['Item_1', 'Item_2'], text: 'Download', splitButton: true, useSelectMode: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton with icon only', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { dataSource: ['Item_1', 'Item_2'], icon: 'save', splitButton: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton opened with deferRendering', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + dataSource: ['Item_1', 'Item_2'], + text: 'Download', + splitButton: true, + opened: true, + deferRendering: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton splitButton disabled without arrow icon', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + dataSource: [], + text: 'Download', + splitButton: false, + showArrowIcon: false, + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton with useSelectMode and selected item', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + dataSource: [{ id: 1, text: 'Item 1' }, { id: 2, text: 'Item 2' }], + text: 'Select', + splitButton: false, + useSelectMode: true, + selectedItemKey: 1, + keyExpr: 'id', + displayExpr: 'text', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton opened without split', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + text: 'Options', + splitButton: false, + opened: true, + deferRendering: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('dropDownButton with stylingMode text', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + dataSource: ['Item_1', 'Item_2'], + text: 'Options', + stylingMode: 'text', + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts new file mode 100644 index 000000000000..188b48ea5ac4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts @@ -0,0 +1,66 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - fileUploader', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader disabled', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader readOnly', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader multiple', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, multiple: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader with name', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, name: 'fileUploader', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader disabled multiple', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, disabled: true, multiple: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader readOnly multiple with name', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, readOnly: true, multiple: true, name: 'fileUploader', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader with accept attribute', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, accept: 'image/*', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader with labelText', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, labelText: 'or drop files here', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('fileUploader with selectButtonText', async ({ page }) => { + await createWidget(page, 'dxFileUploader', { focusStateEnabled: true, selectButtonText: 'Browse', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts new file mode 100644 index 000000000000..474e91f5456c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts @@ -0,0 +1,80 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - filterBuilder', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { fields: [{ dataField: 'CompanyName', caption: 'Company Name' }, { dataField: 'City', caption: 'City' }], value: ['CompanyName', 'contains', 'Dev'] }); + await a11yCheck(page, {}, '#container'); + }); + + test('filterBuilder without filter value', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { fields: [{ dataField: 'CompanyName', caption: 'Company Name' }, { dataField: 'City', caption: 'City' }] }); + await a11yCheck(page, {}, '#container'); + }); + + test('filterBuilder with multiple conditions', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields: [ + { dataField: 'CompanyName', caption: 'Company Name' }, + { dataField: 'City', caption: 'City' }, + { dataField: 'State', caption: 'State' }, + ], + value: [['CompanyName', 'contains', 'Dev'], 'and', ['City', '=', 'New York']], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('filterBuilder with number field', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields: [{ dataField: 'Age', caption: 'Age', dataType: 'number' }], + value: ['Age', '>', 18], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('filterBuilder with date field', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields: [{ dataField: 'BirthDate', caption: 'Birth Date', dataType: 'date' }], + value: ['BirthDate', '>', new Date(1990, 0, 1)], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('filterBuilder with boolean field', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields: [{ dataField: 'Active', caption: 'Active', dataType: 'boolean' }], + value: ['Active', '=', true], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('filterBuilder with OR group condition', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields: [ + { dataField: 'CompanyName', caption: 'Company Name' }, + { dataField: 'City', caption: 'City' }, + ], + value: [['CompanyName', 'contains', 'Dev'], 'or', ['City', '=', 'London']], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('filterBuilder with single field no value', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields: [{ dataField: 'Name', caption: 'Name' }], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts new file mode 100644 index 000000000000..e41be43ea89e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts @@ -0,0 +1,51 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - floatingActionButton', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxSpeedDialAction', { label: 'label', icon: 'save' }); + await a11yCheck(page, {}, '#container'); + }); + + test('floatingActionButton without label', async ({ page }) => { + await createWidget(page, 'dxSpeedDialAction', { label: '', icon: 'save' }); + await a11yCheck(page, {}, '#container'); + }); + + test('floatingActionButton without icon', async ({ page }) => { + await createWidget(page, 'dxSpeedDialAction', { label: 'label' }); + await a11yCheck(page, {}, '#container'); + }); + + test('floatingActionButton without label and icon', async ({ page }) => { + await createWidget(page, 'dxSpeedDialAction', { label: '' }); + await a11yCheck(page, {}, '#container'); + }); + + test('floatingActionButton with index', async ({ page }) => { + await createWidget(page, 'dxSpeedDialAction', { label: 'Action 1', icon: 'save', index: 1 }); + await a11yCheck(page, {}, '#container'); + }); + + test('floatingActionButton disabled', async ({ page }) => { + await createWidget(page, 'dxSpeedDialAction', { label: 'Disabled', icon: 'trash', disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('floatingActionButton with different icon', async ({ page }) => { + await createWidget(page, 'dxSpeedDialAction', { label: 'Add', icon: 'add' }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts new file mode 100644 index 000000000000..ca420a2e7957 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts @@ -0,0 +1,138 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - form', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxForm', { height: 200, formData: { ID: 1, FirstName: 'John', LastName: 'Heart' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with alignItemLabels false', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 200, + alignItemLabels: false, + formData: { ID: 1, FirstName: 'John', LastName: 'Heart', Position: 'CEO', Active: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with showOptionalMark', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 200, + showOptionalMark: true, + formData: { ID: 1, FirstName: 'John', LastName: 'Heart', Position: 'CEO', Active: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with required mark', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 200, + showRequiredMark: true, + items: [{ + itemType: 'simple', + dataField: 'Email', + validationRules: [{ type: 'required', message: 'Email is required' }], + }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with validation summary', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 200, + showValidationSummary: true, + items: [{ + itemType: 'simple', + dataField: 'Email', + validationRules: [{ type: 'required', message: 'Email is required' }], + }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with alignItemLabels true and showOptionalMark', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 200, + alignItemLabels: true, + showOptionalMark: true, + formData: { ID: 1, FirstName: 'John', LastName: 'Heart', Position: 'CEO', Active: true }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form required and validation summary', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 200, + showRequiredMark: true, + showValidationSummary: true, + items: [{ + itemType: 'simple', + dataField: 'Email', + validationRules: [{ type: 'required', message: 'Email is required' }], + }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with group items', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 300, + formData: { FirstName: 'John', LastName: 'Heart', Phone: '555-0100', Email: 'john@example.com' }, + items: [ + { + itemType: 'group', + caption: 'Personal', + items: ['FirstName', 'LastName'], + }, + { + itemType: 'group', + caption: 'Contact', + items: ['Phone', 'Email'], + }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with tabbed items', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 300, + formData: { FirstName: 'John', LastName: 'Heart', Notes: 'Some notes' }, + items: [ + { + itemType: 'tabbed', + tabs: [ + { title: 'Personal', items: ['FirstName', 'LastName'] }, + { title: 'Notes', items: ['Notes'] }, + ], + }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('form with empty formData', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 200, + formData: {}, + items: [ + { itemType: 'simple', dataField: 'Name', label: { text: 'Name' } }, + { itemType: 'simple', dataField: 'Age', label: { text: 'Age' }, editorType: 'dxNumberBox' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts new file mode 100644 index 000000000000..7468cab32082 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts @@ -0,0 +1,93 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - gallery', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxGallery', { height: 300, width: 300, dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }] }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery without nav buttons', async ({ page }) => { + await createWidget(page, 'dxGallery', { height: 300, width: 300, dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }], showNavButtons: false, showIndicator: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery with nav buttons', async ({ page }) => { + await createWidget(page, 'dxGallery', { height: 300, width: 300, dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }], showNavButtons: true, showIndicator: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery without indicator', async ({ page }) => { + await createWidget(page, 'dxGallery', { height: 300, width: 300, dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }], showIndicator: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery with swipe disabled', async ({ page }) => { + await createWidget(page, 'dxGallery', { height: 300, width: 300, dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }], swipeEnabled: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery with swipe enabled and nav buttons', async ({ page }) => { + await createWidget(page, 'dxGallery', { + height: 300, + width: 300, + dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }, { imageAlt: 'Image 3', imageSrc: '' }], + swipeEnabled: true, + showNavButtons: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery without indicator and without nav buttons', async ({ page }) => { + await createWidget(page, 'dxGallery', { + height: 300, + width: 300, + dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }], + showIndicator: false, + showNavButtons: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery with stretchImages', async ({ page }) => { + await createWidget(page, 'dxGallery', { + height: 300, + width: 300, + dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }], + stretchImages: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery with focusStateEnabled', async ({ page }) => { + await createWidget(page, 'dxGallery', { + height: 300, + width: 300, + dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }], + focusStateEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('gallery with second item selected', async ({ page }) => { + await createWidget(page, 'dxGallery', { + height: 300, + width: 300, + dataSource: [{ imageAlt: 'Image 1', imageSrc: '' }, { imageAlt: 'Image 2', imageSrc: '' }, { imageAlt: 'Image 3', imageSrc: '' }], + selectedIndex: 1, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.matrix.spec.ts new file mode 100644 index 000000000000..42546b5d4180 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.matrix.spec.ts @@ -0,0 +1,25 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const markup = '

Hello

'; + +test.describe('Accessibility - htmlEditor matrix', () => { + testAccessibilityMatrix({ + component: 'dxHtmlEditor', + containerUrl, + a11yCheckConfig: {}, + options: { + value: [markup], + readOnly: [true, false], + name: ['', 'name'], + height: [undefined, 300], + width: [undefined, 300], + placeholder: ['', 'placeholder'], + focusStateEnabled: [true], + toolbar: [{ items: ['bold', 'color'] }], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts new file mode 100644 index 000000000000..3b043b81fead --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts @@ -0,0 +1,104 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - htmlEditor', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { value: '

Hello

', focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('readOnly mode', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + value: '

Hello

', + readOnly: true, + focusStateEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with toolbar', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + value: '

Hello

', + focusStateEnabled: true, + toolbar: { items: ['bold', 'color'] }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with name and placeholder', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + value: '

Hello

', + focusStateEnabled: true, + name: 'name', + placeholder: 'placeholder', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with fixed height and width', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + value: '

Hello

', + focusStateEnabled: true, + height: 300, + width: 300, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('readOnly with toolbar', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + value: '

Hello

', + readOnly: true, + focusStateEnabled: true, + toolbar: { items: ['bold', 'color'] }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('empty value with placeholder', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + focusStateEnabled: true, + placeholder: 'Type here...', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('readOnly empty with placeholder', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + focusStateEnabled: true, + readOnly: true, + placeholder: 'placeholder', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with mention module', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + value: '

Hello

', + focusStateEnabled: true, + mentions: [{ dataSource: ['Alice', 'Bob'], searchExpr: 'this', displayExpr: 'this' }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with full toolbar', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + value: '

Hello

', + focusStateEnabled: true, + toolbar: { items: ['bold', 'italic', 'underline', 'separator', 'alignLeft', 'alignCenter', 'alignRight'] }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts new file mode 100644 index 000000000000..045b49a02a50 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts @@ -0,0 +1,177 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - list', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxList', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 400 }); + await a11yCheck(page, {}, '#container'); + }); + + test('empty list', async ({ page }) => { + await createWidget(page, 'dxList', { dataSource: [], height: 400 }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with search enabled', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + searchEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with multiple selection', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + selectionMode: 'multiple', + showSelectionControls: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with single selection', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + selectionMode: 'single', + showSelectionControls: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with item deleting toggle mode', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + allowItemDeleting: true, + itemDeleteMode: 'toggle', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with item deleting static mode', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + allowItemDeleting: true, + itemDeleteMode: 'static', + }); + await a11yCheck(page, {}, '#container'); + }); + + const groupedItems = [ + { key: 'Group A', items: ['Item A1', 'Item A2', 'Item A3'] }, + { key: 'Group B', items: ['Item B1', 'Item B2'] }, + ]; + + test('grouped list', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: groupedItems, + height: 400, + grouped: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grouped list with collapsible groups', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: groupedItems, + height: 400, + grouped: true, + collapsibleGroups: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grouped list with search', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: groupedItems, + height: 400, + grouped: true, + searchEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with item deleting context mode', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + allowItemDeleting: true, + itemDeleteMode: 'context', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with item deleting slideButton mode', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + allowItemDeleting: true, + itemDeleteMode: 'slideButton', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with all selection mode', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + selectionMode: 'all', + showSelectionControls: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('grouped list with multiple selection', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: groupedItems, + height: 400, + grouped: true, + collapsibleGroups: true, + selectionMode: 'multiple', + showSelectionControls: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with noDataText', async ({ page }) => { + await createWidget(page, 'dxList', { dataSource: [], height: 400, noDataText: 'No items available' }); + await a11yCheck(page, {}, '#container'); + }); + + test('list disabled', async ({ page }) => { + await createWidget(page, 'dxList', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 400, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with pullingDownText', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: ['Item_1', 'Item_2', 'Item_3'], + height: 400, + pullingDownText: 'Pull to refresh', + pulledDownText: 'Release to refresh', + refreshingText: 'Refreshing...', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('list with single item', async ({ page }) => { + await createWidget(page, 'dxList', { dataSource: ['Only Item'], height: 400 }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts new file mode 100644 index 000000000000..380e4d19d948 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts @@ -0,0 +1,56 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - loadIndicator', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', {}); + await a11yCheck(page, {}, '#container'); + }); + + test('loadIndicator with height', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', { height: 40 }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadIndicator with width', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', { width: 40 }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadIndicator with height and width', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', { height: 40, width: 40 }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadIndicator large size', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', { height: 80, width: 80 }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadIndicator with indicatorSrc', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', { indicatorSrc: '' }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadIndicator default size visible', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', { visible: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadIndicator not visible', async ({ page }) => { + await createWidget(page, 'dxLoadIndicator', { visible: false }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.matrix.spec.ts new file mode 100644 index 000000000000..6a295f9c6154 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.matrix.spec.ts @@ -0,0 +1,20 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - loadPanel matrix', () => { + testAccessibilityMatrix({ + component: 'dxLoadPanel', + containerUrl, + a11yCheckConfig: {}, + options: { + visible: [true], + showIndicator: [true, false], + showPane: [true, false], + message: [undefined, 'message'], + delay: [undefined, 0], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts new file mode 100644 index 000000000000..3c37078f0a9e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts @@ -0,0 +1,56 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - loadPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: true, showIndicator: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadPanel without indicator', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: true, showIndicator: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadPanel without pane', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: true, showPane: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadPanel with message', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: true, message: 'Loading...' }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadPanel without indicator and without pane', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: true, showIndicator: false, showPane: false, message: 'message' }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadPanel not visible', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: false, showIndicator: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadPanel with width and height', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: true, width: 200, height: 90, message: 'Loading...' }); + await a11yCheck(page, {}, '#container'); + }); + + test('loadPanel with shading', async ({ page }) => { + await createWidget(page, 'dxLoadPanel', { visible: true, shading: true, shadingColor: 'rgba(0,0,0,0.4)', message: 'Please wait...' }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts new file mode 100644 index 000000000000..f4da99196e55 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts @@ -0,0 +1,76 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - lookup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup empty', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: [], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup disabled', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup readOnly', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup with placeholder', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], placeholder: 'Select person', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup opened', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], opened: true, deferRendering: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup opened without deferRendering', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], opened: false, deferRendering: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup disabled with placeholder', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], disabled: true, placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup readOnly with placeholder', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], readOnly: true, placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup with value selected', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], value: 'John Heart', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup with showClearButton', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright'], value: 'John Heart', showClearButton: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('lookup with searchEnabled', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: ['John Heart', 'Samantha Bright', 'Kevin Carter'], searchEnabled: true, searchTimeout: 0, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.matrix.spec.ts new file mode 100644 index 000000000000..a163bb9a7c8e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.matrix.spec.ts @@ -0,0 +1,45 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = [ + { + text: 'remove', + icon: 'remove', + items: [ + { + text: 'user', + icon: 'user', + disabled: true, + items: [{ text: 'user_1' }], + }, + { + text: 'save', + icon: 'save', + items: [ + { text: 'export', icon: 'export' }, + { text: 'edit', icon: 'edit' }, + ], + }, + ], + }, + { text: 'user', icon: 'user' }, + { text: 'coffee', icon: 'coffee', disabled: true }, +]; + +test.describe('Accessibility - menu matrix', () => { + testAccessibilityMatrix({ + component: 'dxMenu', + containerUrl, + a11yCheckConfig: {}, + options: { + items: [items], + disabled: [true, false], + width: [400, 1024], + orientation: ['horizontal', 'vertical'], + adaptivityEnabled: [true, false], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts new file mode 100644 index 000000000000..8dafc1c59971 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts @@ -0,0 +1,86 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - menu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxMenu', { items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }], width: 400 }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu vertical orientation', async ({ page }) => { + await createWidget(page, 'dxMenu', { items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }], width: 400, orientation: 'vertical' }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu disabled', async ({ page }) => { + await createWidget(page, 'dxMenu', { items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }], width: 400, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu with adaptivity enabled', async ({ page }) => { + await createWidget(page, 'dxMenu', { items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }], width: 400, adaptivityEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu with nested items', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'remove', icon: 'remove', + items: [{ text: 'item_1' }, { text: 'item_2' }], + }, { text: 'user', icon: 'user' }], + width: 400, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu disabled vertical', async ({ page }) => { + await createWidget(page, 'dxMenu', { items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }], width: 400, disabled: true, orientation: 'vertical' }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu wide horizontal', async ({ page }) => { + await createWidget(page, 'dxMenu', { items: [{ text: 'remove', icon: 'remove' }, { text: 'user', icon: 'user' }], width: 1024, orientation: 'horizontal' }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu with selection mode single', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ text: 'Item 1' }, { text: 'Item 2' }, { text: 'Item 3' }], + width: 400, + selectionMode: 'single', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu with disabled item', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ text: 'Item 1' }, { text: 'Item 2', disabled: true }, { text: 'Item 3' }], + width: 400, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('menu with hideSubmenuOnMouseLeave', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Parent', + items: [{ text: 'Child 1' }, { text: 'Child 2' }], + }], + width: 400, + hideSubmenuOnMouseLeave: true, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts new file mode 100644 index 000000000000..b36c56f743d7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts @@ -0,0 +1,61 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - multiView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 300, focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('empty multi view', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: [], height: 300, noDataText: 'no data text' }); + await a11yCheck(page, {}, '#container'); + }); + + test('multi view with loop enabled', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 300, loop: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('multi view with loop disabled', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 300, loop: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('multi view empty with loop enabled', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: [], height: 300, loop: true, noDataText: 'no data text', focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('multi view with selectedIndex set', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 300, selectedIndex: 1 }); + await a11yCheck(page, {}, '#container'); + }); + + test('multi view with swipe disabled', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 300, swipeEnabled: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('multi view with single item', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: ['Only Item'], height: 300 }); + await a11yCheck(page, {}, '#container'); + }); + + test('multi view with deferRendering false', async ({ page }) => { + await createWidget(page, 'dxMultiView', { dataSource: ['Item_1', 'Item_2', 'Item_3'], height: 300, deferRendering: false }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.matrix.spec.ts new file mode 100644 index 000000000000..ec4071fcffb6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.matrix.spec.ts @@ -0,0 +1,22 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - numberBox matrix', () => { + testAccessibilityMatrix({ + component: 'dxNumberBox', + containerUrl, + a11yCheckConfig: {}, + options: { + value: [20.5], + placeholder: [undefined, 'placeholder'], + disabled: [true, false], + readOnly: [true, false], + showClearButton: [true, false], + showSpinButtons: [true, false], + inputAttr: [{ 'aria-label': 'aria-label' }], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts new file mode 100644 index 000000000000..49c3e24cafef --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - numberBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox disabled', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox readOnly', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox with showClearButton and showSpinButtons', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, showClearButton: true, showSpinButtons: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox with placeholder', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { placeholder: 'Enter value', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox disabled with placeholder', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { placeholder: 'Enter value', disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox readOnly with showSpinButtons', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, readOnly: true, showSpinButtons: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox disabled with showSpinButtons', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, disabled: true, showSpinButtons: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox with min and max', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 50, min: 0, max: 100, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox with name', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, name: 'quantity', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('numberBox with label', async ({ page }) => { + await createWidget(page, 'dxNumberBox', { value: 20.5, label: 'Amount', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts new file mode 100644 index 000000000000..d4ff64b9dd6f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts @@ -0,0 +1,89 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - pagination', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'full' }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination compact mode', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'compact' }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination with info text', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'full', showInfo: true, infoText: 'Total {2} items. Page {0} of {1}' }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination with page size selector', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'full', showPageSizeSelector: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination with navigation buttons', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'full', showNavigationButtons: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination compact with info text', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'compact', showInfo: true, infoText: 'Total {2} items. Page {0} of {1}' }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination compact with page size selector', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'compact', showPageSizeSelector: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination compact with navigation buttons', async ({ page }) => { + await createWidget(page, 'dxPagination', { itemCount: 50, displayMode: 'compact', showNavigationButtons: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination with all features enabled', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 100, + displayMode: 'full', + showInfo: true, + showNavigationButtons: true, + showPageSizeSelector: true, + infoText: 'Total {2} items. Page {0} of {1}', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination with page size 5', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + displayMode: 'full', + pageSize: 5, + showPageSizeSelector: true, + allowedPageSizes: [5, 10, 20], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('pagination on last page', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 4, + displayMode: 'full', + showNavigationButtons: true, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts new file mode 100644 index 000000000000..7fb7152ed59c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - popover', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxPopover', { visible: true, target: '#container', width: 300, height: 280 }); + await a11yCheck(page, {}, '#container'); + }); + + test('popover with title', async ({ page }) => { + await createWidget(page, 'dxPopover', { visible: true, target: '#container', width: 300, height: 280, showTitle: true, title: 'title' }); + await a11yCheck(page, {}, '#container'); + }); + + test('popover with showCloseButton', async ({ page }) => { + await createWidget(page, 'dxPopover', { visible: true, target: '#container', width: 300, height: 280, showTitle: true, showCloseButton: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('popover without title header', async ({ page }) => { + await createWidget(page, 'dxPopover', { visible: true, target: '#container', width: 300, height: 280, showTitle: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('popover with toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopover', { + visible: true, + target: '#container', + width: 300, + height: 280, + showTitle: true, + toolbarItems: [{ location: 'before', widget: 'dxButton', options: { icon: 'back' } }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('popover not visible', async ({ page }) => { + await createWidget(page, 'dxPopover', { visible: false, target: '#container', width: 300, height: 280, title: 'Popover' }); + await a11yCheck(page, {}, '#container'); + }); + + test('popover with position bottom', async ({ page }) => { + await createWidget(page, 'dxPopover', { visible: true, target: '#container', width: 300, height: 200, position: 'bottom', title: 'Bottom Popover' }); + await a11yCheck(page, {}, '#container'); + }); + + test('popover with hideOnOutsideClick false', async ({ page }) => { + await createWidget(page, 'dxPopover', { + visible: true, + target: '#container', + width: 300, + height: 280, + hideOnOutsideClick: false, + showTitle: true, + title: 'Pinned', + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts new file mode 100644 index 000000000000..a3a995e50f4c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts @@ -0,0 +1,95 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 300, height: 280, showTitle: true, title: 'title' }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup without title', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 300, height: 280, showTitle: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup with showCloseButton', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 300, height: 280, showTitle: true, title: 'title', showCloseButton: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup with dragEnabled', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 300, height: 280, showTitle: true, title: 'title', dragEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup with toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopup', { + visible: true, + width: 300, + height: 280, + showTitle: true, + title: 'title', + toolbarItems: [ + { widget: 'dxButton', toolbar: 'bottom', location: 'before', options: { icon: 'email', text: 'Send' } }, + { widget: 'dxButton', toolbar: 'bottom', location: 'after', options: { text: 'Close' } }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup invisible', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: false, width: 300, height: 280 }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup with locateInMenu toolbar item', async ({ page }) => { + await createWidget(page, 'dxPopup', { + visible: true, + width: 300, + height: 280, + showTitle: true, + title: 'title', + toolbarItems: [ + { locateInMenu: 'always', widget: 'dxButton', toolbar: 'top', options: { text: 'More info' } }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup drag enabled without title', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 300, height: 280, showTitle: false, dragEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup with close button and drag', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 300, height: 280, showTitle: true, showCloseButton: true, dragEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup fullscreen', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, fullScreen: true, showTitle: true, title: 'Fullscreen Popup' }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup with resizing enabled', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 400, height: 300, showTitle: true, title: 'Resizable', resizeEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('popup with shading', async ({ page }) => { + await createWidget(page, 'dxPopup', { visible: true, width: 300, height: 280, showTitle: true, title: 'Modal', shading: true }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts new file mode 100644 index 000000000000..c24850071521 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts @@ -0,0 +1,56 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - progressBar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { value: 45, min: 0, max: 100, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('progressBar without value', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { min: 0, max: 100, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('progressBar disabled', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { value: 45, min: 0, max: 100, disabled: true, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('progressBar without showStatus', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { value: 45, min: 0, max: 100, showStatus: false, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('progressBar disabled without value', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { min: 0, max: 100, disabled: true, showStatus: true, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('progressBar at maximum value', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { value: 100, min: 0, max: 100, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('progressBar at minimum value', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { value: 0, min: 0, max: 100, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('progressBar with custom min and max', async ({ page }) => { + await createWidget(page, 'dxProgressBar', { value: 50, min: 10, max: 200, elementAttr: { 'aria-label': 'Progress Bar' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts new file mode 100644 index 000000000000..c1495faddc0c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - radioGroup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Item_1', 'Item_2', 'Item_3'] }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup horizontal layout', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Item_1', 'Item_2', 'Item_3'], layout: 'horizontal' }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup disabled', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Item_1', 'Item_2', 'Item_3'], disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup readOnly', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Item_1', 'Item_2', 'Item_3'], readOnly: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup disabled horizontal', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Item_1', 'Item_2', 'Item_3'], disabled: true, layout: 'horizontal' }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup readOnly horizontal', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Item_1', 'Item_2', 'Item_3'], readOnly: true, layout: 'horizontal' }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup with value selected', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Item_1', 'Item_2', 'Item_3'], value: 'Item_2' }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup with single item', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: ['Only Item'] }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup with object items', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { + items: [{ id: 1, text: 'Option A' }, { id: 2, text: 'Option B' }, { id: 3, text: 'Option C' }], + valueExpr: 'id', + displayExpr: 'text', + value: 2, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('radioGroup empty items', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { items: [] }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.matrix.spec.ts new file mode 100644 index 000000000000..dbefd7a2fd29 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.matrix.spec.ts @@ -0,0 +1,23 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - rangeSlider matrix', () => { + testAccessibilityMatrix({ + component: 'dxRangeSlider', + containerUrl, + a11yCheckConfig: {}, + options: { + start: [40], + end: [60], + disabled: [true, false], + readOnly: [true, false], + height: [undefined, 250], + width: [undefined, '50%'], + label: [{ visible: true, position: 'top' }], + tooltip: [{ enabled: true, showMode: 'always', position: 'bottom' }], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts new file mode 100644 index 000000000000..31756d0b57a8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts @@ -0,0 +1,92 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - rangeSlider', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { start: 40, end: 60 }); + await a11yCheck(page, {}, '#container'); + }); + + test('disabled range slider', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { start: 40, end: 60, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('read-only range slider', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { start: 40, end: 60, readOnly: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider with tooltip always shown', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { + start: 40, + end: 60, + tooltip: { enabled: true, showMode: 'always', position: 'bottom' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider with label', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { + start: 40, + end: 60, + label: { visible: true, position: 'top' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider with custom width', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { + start: 40, + end: 60, + width: '50%', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider with custom height', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { + start: 40, + end: 60, + height: 250, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider disabled with tooltip', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { + start: 40, + end: 60, + disabled: true, + tooltip: { enabled: true, showMode: 'always', position: 'bottom' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider with min and max', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { start: 20, end: 80, min: 0, max: 100 }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider with step', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { start: 40, end: 60, step: 10 }); + await a11yCheck(page, {}, '#container'); + }); + + test('range slider at extremes', async ({ page }) => { + await createWidget(page, 'dxRangeSlider', { start: 0, end: 100, min: 0, max: 100 }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts new file mode 100644 index 000000000000..87dff8f5c1dd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts @@ -0,0 +1,199 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler appointment', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['month', 'week', 'day', 'agenda'].forEach((currentView) => { + test(`appointment accessibility in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 1, 13)), + }], + currentView, + currentDate: new Date(Date.UTC(2021, 1, 1)), + }); + await a11yCheck(page, {}, '#container'); + }); + + test(`appointment with template in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 1, 13)), + }], + appointmentTemplate: ({ appointmentData }) => `
${appointmentData.text}
`, + currentView, + currentDate: new Date(Date.UTC(2021, 1, 1)), + }); + await a11yCheck(page, {}, '#container'); + }); + + test(`appointment with group in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 1, 13)), + groupId: 1, + }], + currentView, + currentDate: new Date(Date.UTC(2021, 1, 1)), + groups: ['groupId'], + resources: [{ + fieldExpr: 'groupId', + dataSource: [{ text: 'resource1', id: 1 }], + label: 'Group 1', + }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test(`appointment with multiple groups in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 1, 13)), + groupId1: 1, + groupId2: [1, 2], + }], + currentView, + currentDate: new Date(Date.UTC(2021, 1, 1)), + groups: ['groupId1', 'groupId2'], + resources: [ + { + fieldExpr: 'groupId1', + dataSource: [{ text: 'resource11', id: 1 }], + label: 'Group 1', + }, + { + fieldExpr: 'groupId2', + dataSource: [{ text: 'resource21', id: 1 }, { text: 'resource22', id: 2 }], + label: 'Group 2', + }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test(`recurring appointment in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date('2021-04-29T16:30:00.000Z'), + endDate: new Date('2021-04-29T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + currentView, + currentDate: new Date('2021-04-29T18:30:00.000Z'), + startDayHour: 9, + }); + await a11yCheck(page, {}, '#container'); + }); + }); + + test('recurring appointment accessibility', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date('2021-04-29T16:30:00.000Z'), + endDate: new Date('2021-04-29T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + currentView: 'week', + currentDate: new Date('2021-04-29T18:30:00.000Z'), + startDayHour: 9, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('appointment with timezone offset', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Install New Router in Dev Room', + startDate: new Date('2021-03-29T21:30:00.000Z'), + endDate: new Date('2021-03-29T22:30:00.000Z'), + }], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multipart appointment in week view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 3, 13)), + }], + allDayPanelMode: 'hidden', + currentView: 'week', + currentDate: new Date(Date.UTC(2021, 1, 1)), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('multipart appointment in month view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 17, 13)), + }], + allDayPanelMode: 'hidden', + currentView: 'month', + currentDate: new Date(Date.UTC(2021, 1, 1)), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('appointment collector button', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-05T23:45:00.000Z'), + endDate: new Date('2021-03-05T18:15:00.000Z'), + }, + { + text: 'Complete Shipper Selection Form', + startDate: new Date('2021-03-05T15:30:00.000Z'), + endDate: new Date('2021-03-05T17:00:00.000Z'), + }, + { + text: 'Upgrade Server Hardware', + startDate: new Date('2021-03-05T19:00:00.000Z'), + endDate: new Date('2021-03-05T21:15:00.000Z'), + }, + ], + currentView: 'month', + currentDate: new Date(2021, 2, 1), + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts new file mode 100644 index 000000000000..5b9a15855b6d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts @@ -0,0 +1,65 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler appointmentForm', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('appointment form accessibility check', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'App 1', + startDate: new Date(2021, 3, 29, 9, 30), + endDate: new Date(2021, 3, 29, 11, 30), + }], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + }); + await page.click('.dx-scheduler-appointment'); + await page.waitForSelector('.dx-tooltip-wrapper.dx-scheduler-appointment-tooltip'); + await a11yCheck(page, {}, '#container'); + }); + + test('appointment form opened on double-click', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 1, 13)), + }], + currentView: 'week', + currentDate: new Date(2021, 1, 1), + }); + await page.dblclick('.dx-scheduler-appointment'); + await page.waitForSelector('.dx-scheduler-appointment-popup .dx-overlay-content'); + await a11yCheck(page, {}, '.dx-scheduler-appointment-popup .dx-overlay-content'); + }); + + test('appointment form with recurring appointment', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Recurring App', + startDate: new Date('2021-04-29T16:30:00.000Z'), + endDate: new Date('2021-04-29T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + currentView: 'week', + currentDate: new Date('2021-04-29T18:30:00.000Z'), + startDayHour: 9, + }); + await page.click('.dx-scheduler-appointment'); + await page.waitForSelector('.dx-tooltip-wrapper.dx-scheduler-appointment-tooltip'); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts new file mode 100644 index 000000000000..27cf7f0fa412 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts @@ -0,0 +1,63 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler legacyPopup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('legacy popup accessibility check', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('edit appointment with legacy form', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Install New Router in Dev Room', + startDate: new Date('2021-03-29T21:30:00.000Z'), + endDate: new Date('2021-03-29T22:30:00.000Z'), + recurrenceRule: 'FREQ=DAILY', + }], + editing: { legacyForm: true }, + recurrenceEditMode: 'series', + currentView: 'week', + currentDate: new Date(2021, 2, 28), + }); + await page.dblclick('.dx-scheduler-appointment'); + await page.waitForSelector('.dx-scheduler-appointment-popup'); + await a11yCheck(page, {}, '#container'); + }); + + test('recurrence editor repeat end accessibility', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + editing: { legacyForm: true }, + dataSource: [{ + text: 'Install New Router in Dev Room', + startDate: new Date('2021-03-29T21:30:00.000Z'), + endDate: new Date('2021-03-29T22:30:00.000Z'), + recurrenceRule: 'FREQ=DAILY;UNTIL=20250522T215959Z', + }], + recurrenceEditMode: 'series', + currentView: 'week', + currentDate: new Date('2021-03-29T21:30:00.000Z'), + }); + await page.dblclick('.dx-scheduler-appointment'); + await page.waitForSelector('.dx-scheduler-appointment-popup'); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts new file mode 100644 index 000000000000..b1a07d230f00 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts @@ -0,0 +1,197 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('month view accessibility check', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'month', + }); + await a11yCheck(page, {}, '#container'); + }); + + ['day', 'week', 'workWeek', 'month', 'agenda', 'timelineDay', 'timelineMonth', 'timelineWeek', 'timelineWorkWeek'].forEach((currentView) => { + test(`${currentView} view with appointment`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date('2021-04-29T16:30:00.000Z'), + endDate: new Date('2021-04-29T18:30:00.000Z'), + }], + currentView, + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + }); + await a11yCheck(page, {}, '#container'); + }); + }); + + test('month view with grouping by resource', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 1, 13)), + groupId: 1, + }], + currentView: 'month', + currentDate: new Date(Date.UTC(2021, 1, 1)), + groups: ['groupId'], + resources: [{ + fieldExpr: 'groupId', + dataSource: [{ text: 'Resource A', id: 1 }, { text: 'Resource B', id: 2 }], + label: 'Group', + }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('week view with recurring appointment', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date('2021-04-29T16:30:00.000Z'), + endDate: new Date('2021-04-29T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + currentView: 'week', + currentDate: new Date('2021-04-29T18:30:00.000Z'), + startDayHour: 9, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('week view with all-day appointments', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'All Day Event', + startDate: new Date(Date.UTC(2021, 3, 29)), + endDate: new Date(Date.UTC(2021, 3, 29)), + allDay: true, + }], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('week view with multiple resources grouping', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 1, 13)), + groupId1: 1, + groupId2: 1, + }], + currentView: 'week', + currentDate: new Date(Date.UTC(2021, 1, 1)), + groups: ['groupId1', 'groupId2'], + resources: [ + { + fieldExpr: 'groupId1', + dataSource: [{ text: 'resource11', id: 1 }, { text: 'resource12', id: 2 }], + label: 'Group 1', + }, + { + fieldExpr: 'groupId2', + dataSource: [{ text: 'resource21', id: 1 }, { text: 'resource22', id: 2 }], + label: 'Group 2', + }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('day view with disabled time ranges', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'App 1', + startDate: new Date(2021, 1, 1, 12), + endDate: new Date(2021, 1, 1, 13), + }], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('month view with appointment collector', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-05T23:45:00.000Z'), + endDate: new Date('2021-03-05T18:15:00.000Z'), + }, + { + text: 'Complete Shipper Selection Form', + startDate: new Date('2021-03-05T15:30:00.000Z'), + endDate: new Date('2021-03-05T17:00:00.000Z'), + }, + { + text: 'Upgrade Server Hardware', + startDate: new Date('2021-03-05T19:00:00.000Z'), + endDate: new Date('2021-03-05T21:15:00.000Z'), + }, + { + text: 'Upgrade Personal Computers', + startDate: new Date('2021-03-05T21:45:00.000Z'), + endDate: new Date('2021-03-05T23:30:00.000Z'), + }, + ], + currentView: 'month', + currentDate: new Date(2021, 2, 1), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('week view with allDayPanelMode hidden', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'UTC', + dataSource: [{ + text: 'App 1', + startDate: new Date(Date.UTC(2021, 1, 1, 12)), + endDate: new Date(Date.UTC(2021, 1, 3, 13)), + }], + allDayPanelMode: 'hidden', + currentView: 'week', + currentDate: new Date(Date.UTC(2021, 1, 1)), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('month view with maxAppointmentsPerCell', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'App 1', startDate: new Date(2021, 1, 1), endDate: new Date(2021, 1, 1) }, + { text: 'App 2', startDate: new Date(2021, 1, 2), endDate: new Date(2021, 1, 2) }, + { text: 'App 3', startDate: new Date(2021, 1, 2), endDate: new Date(2021, 1, 2) }, + { text: 'App 4', startDate: new Date(2021, 1, 3), endDate: new Date(2021, 1, 3) }, + ], + allDayPanelMode: 'hidden', + currentView: 'month', + maxAppointmentsPerCell: 1, + currentDate: new Date(2021, 1, 1), + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts new file mode 100644 index 000000000000..9680c9048267 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts @@ -0,0 +1,80 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler status', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('scheduler status accessibility check', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'App 1', + startDate: new Date(2021, 3, 29, 9, 30), + endDate: new Date(2021, 3, 29, 11, 30), + }], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + }); + await a11yCheck(page, {}, '#container'); + }); + + test('scheduler status day view with appointments', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + startDate: '2025-04-30T15:00:00.000Z', + endDate: '2025-04-30T16:00:00.000Z', + }, + ], + views: ['day', 'week', 'month'], + currentView: 'day', + currentDate: '2025-04-30T15:00:00.000Z', + showCurrentTimeIndicator: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('scheduler status month view with appointments', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + startDate: '2025-04-30T15:00:00.000Z', + endDate: '2025-04-30T16:00:00.000Z', + }, + ], + views: ['day', 'week', 'month'], + currentView: 'month', + currentDate: '2025-04-30T15:00:00.000Z', + showCurrentTimeIndicator: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('scheduler status agenda view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + startDate: '2025-04-30T15:00:00.000Z', + endDate: '2025-04-30T16:00:00.000Z', + }, + ], + views: ['agenda'], + currentView: 'agenda', + currentDate: '2025-04-30T15:00:00.000Z', + showCurrentTimeIndicator: false, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.matrix.spec.ts new file mode 100644 index 000000000000..117bfb6c8a5b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.matrix.spec.ts @@ -0,0 +1,68 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = [ + 'HD Video Player', + 'SuperHD Video Player', + 'SuperPlasma 50', +]; + +const baseOptions = { + dataSource: [[], items], + value: [undefined, items[0]], + disabled: [true, false], + readOnly: [true, false], + searchEnabled: [true, false], + searchTimeout: [0], + placeholder: [undefined, 'placeholder'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +const buttonsOptions = { + dataSource: [items], + value: [items[0]], + label: [undefined, 'label'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +test.describe('Accessibility - selectBox matrix (deferred)', () => { + testAccessibilityMatrix({ + component: 'dxSelectBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [true, false], + deferRendering: [true], + }, + }); +}); + +test.describe('Accessibility - selectBox matrix (no deferred)', () => { + testAccessibilityMatrix({ + component: 'dxSelectBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + opened: [false], + deferRendering: [false], + }, + }); +}); + +test.describe('Accessibility - selectBox matrix (buttons)', () => { + testAccessibilityMatrix({ + component: 'dxSelectBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...buttonsOptions, + showClearButton: [true, false], + showDropDownButton: [true, false], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts new file mode 100644 index 000000000000..6c540189795a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts @@ -0,0 +1,76 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - selectBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox empty', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: [], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox with value', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], value: 'HD Video Player', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox disabled', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox readOnly', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox with search enabled', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], searchEnabled: true, searchTimeout: 0, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox with label and showClearButton', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], value: 'HD Video Player', label: 'label', showClearButton: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox opened with deferRendering', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], opened: true, deferRendering: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox opened without deferRendering', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], opened: false, deferRendering: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox with placeholder', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox without showDropDownButton', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], value: 'HD Video Player', showDropDownButton: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('selectBox with custom button', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { dataSource: ['HD Video Player', 'SuperHD Video Player'], value: 'HD Video Player', buttons: [{ name: 'today', location: 'before', options: { text: 'Today', stylingMode: 'text' } }], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts new file mode 100644 index 000000000000..a53321ca7e26 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts @@ -0,0 +1,79 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - slider', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 45 }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider disabled', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 45, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider readOnly', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 45, readOnly: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider with label and tooltip', async ({ page }) => { + await createWidget(page, 'dxSlider', { + value: 45, + label: { visible: true, position: 'top' }, + tooltip: { enabled: true, showMode: 'always', position: 'bottom' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider with name and min/max', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 45, min: 10, max: 90, name: 'slider' }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider with custom width', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 45, width: '50%' }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider with custom height', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 45, height: 250 }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider disabled with tooltip', async ({ page }) => { + await createWidget(page, 'dxSlider', { + value: 45, + disabled: true, + tooltip: { enabled: true, showMode: 'always', position: 'bottom' }, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider at maximum value', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 100, min: 0, max: 100 }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider at minimum value', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 0, min: 0, max: 100 }); + await a11yCheck(page, {}, '#container'); + }); + + test('slider with step', async ({ page }) => { + await createWidget(page, 'dxSlider', { value: 50, step: 10, min: 0, max: 100 }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts new file mode 100644 index 000000000000..4734b553c708 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts @@ -0,0 +1,51 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - speechToText', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxSpeechToText', { startText: 'custom text' }); + await a11yCheck(page, {}, '#container'); + }); + + test('speechToText default', async ({ page }) => { + await createWidget(page, 'dxSpeechToText', { startText: '' }); + await a11yCheck(page, {}, '#container'); + }); + + test('speechToText with stop icon', async ({ page }) => { + await createWidget(page, 'dxSpeechToText', { startText: 'custom text', stopIcon: 'user' }); + await a11yCheck(page, {}, '#container'); + }); + + test('speechToText with empty stop icon', async ({ page }) => { + await createWidget(page, 'dxSpeechToText', { startText: 'custom text', stopIcon: '' }); + await a11yCheck(page, {}, '#container'); + }); + + test('speechToText with start icon', async ({ page }) => { + await createWidget(page, 'dxSpeechToText', { startText: 'Record', startIcon: 'audio' }); + await a11yCheck(page, {}, '#container'); + }); + + test('speechToText disabled', async ({ page }) => { + await createWidget(page, 'dxSpeechToText', { startText: 'disabled', disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('speechToText with both icons', async ({ page }) => { + await createWidget(page, 'dxSpeechToText', { startText: 'Start', startIcon: 'audio', stopIcon: 'close' }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.matrix.spec.ts new file mode 100644 index 000000000000..86a62d2bced0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.matrix.spec.ts @@ -0,0 +1,40 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = [ + { + resizable: true, + minSize: '70px', + size: '140px', + text: 'Left Pane', + }, + { + size: '140px', + resizable: false, + collapsible: false, + text: 'Right Pane', + }, +]; + +test.describe('Accessibility - splitter matrix', () => { + testAccessibilityMatrix({ + component: 'dxSplitter', + containerUrl, + a11yCheckConfig: { + rules: { + 'scrollable-region-focusable': { enabled: false }, + }, + }, + options: { + dataSource: [items], + allowKeyboardNavigation: [true, false], + disabled: [true, false], + width: [450, 'auto', '100%'], + height: [400], + separatorSize: [8, 5], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts new file mode 100644 index 000000000000..f687bddb40a6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts @@ -0,0 +1,97 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - splitter', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxSplitter', { dataSource: [{ text: 'Left Pane', size: '140px' }, { text: 'Right Pane', size: '140px' }], height: 400, width: 450 }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('splitter with keyboard navigation disabled', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Left Pane', size: '140px' }, { text: 'Right Pane', size: '140px' }], + height: 400, + width: 450, + allowKeyboardNavigation: false, + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('disabled splitter', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Left Pane', size: '140px' }, { text: 'Right Pane', size: '140px' }], + height: 400, + width: 450, + disabled: true, + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('splitter with vertical orientation', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Top Pane' }, { text: 'Bottom Pane' }], + orientation: 'vertical', + height: 400, + width: 450, + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('splitter with custom separator size', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Left Pane', size: '140px' }, { text: 'Right Pane', size: '140px' }], + height: 400, + width: 450, + separatorSize: 5, + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('splitter with auto width', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Left Pane' }, { text: 'Right Pane' }], + height: 400, + width: 'auto', + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('splitter with three panes', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Left', size: '150px' }, { text: 'Center' }, { text: 'Right', size: '150px' }], + height: 400, + width: 500, + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('splitter with collapsed pane', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Left Pane', size: '140px', collapsed: true }, { text: 'Right Pane' }], + height: 400, + width: 450, + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); + + test('splitter with resizable false pane', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + dataSource: [{ text: 'Fixed Pane', size: '140px', resizable: false }, { text: 'Flexible Pane' }], + height: 400, + width: 450, + }); + await a11yCheck(page, { rules: { 'scrollable-region-focusable': { enabled: false } } }, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts new file mode 100644 index 000000000000..30a6460a242d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts @@ -0,0 +1,121 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - stepper', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxStepper', { dataSource: [{ icon: 'cart', label: 'Cart' }, { icon: 'gift', label: 'Promo Code' }, { icon: 'checkmarkcircle', label: 'Ordered' }], selectedIndex: 0, width: 800, height: 600 }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper with last step selected', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [{ icon: 'cart', label: 'Cart' }, { icon: 'gift', label: 'Promo Code' }, { icon: 'checkmarkcircle', label: 'Ordered' }], + selectedIndex: 2, + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper with vertical orientation', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [{ icon: 'cart', label: 'Cart' }, { icon: 'gift', label: 'Promo Code' }, { icon: 'checkmarkcircle', label: 'Ordered' }], + selectedIndex: 0, + orientation: 'vertical', + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper with isValid and disabled items', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [ + { icon: 'cart', label: 'Cart', isValid: true }, + { icon: 'gift', label: 'Promo Code', isValid: false }, + { icon: 'checkmarkcircle', label: 'Ordered', disabled: true }, + ], + selectedIndex: 0, + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper horizontal with last step selected', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [ + { icon: 'cart', label: 'Cart' }, + { icon: 'gift', label: 'Promo Code', optional: true }, + { icon: 'checkmarkcircle', label: 'Ordered' }, + ], + selectedIndex: 2, + orientation: 'horizontal', + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper vertical with first step selected', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [ + { icon: 'cart', label: 'Cart' }, + { icon: 'gift', label: 'Promo Code' }, + { icon: 'checkmarkcircle', label: 'Ordered' }, + ], + selectedIndex: 0, + orientation: 'vertical', + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper with middle step selected', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [{ label: 'Step 1' }, { label: 'Step 2' }, { label: 'Step 3' }, { label: 'Step 4' }], + selectedIndex: 1, + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper with only icons', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [{ icon: 'user' }, { icon: 'email' }, { icon: 'check' }], + selectedIndex: 0, + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('stepper vertical with invalid step', async ({ page }) => { + await createWidget(page, 'dxStepper', { + dataSource: [ + { label: 'Step 1', isValid: true }, + { label: 'Step 2', isValid: false }, + { label: 'Step 3' }, + ], + selectedIndex: 1, + orientation: 'vertical', + width: 800, + height: 600, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.matrix.spec.ts new file mode 100644 index 000000000000..320cae2862d1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.matrix.spec.ts @@ -0,0 +1,19 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - switch matrix', () => { + testAccessibilityMatrix({ + component: 'dxSwitch', + containerUrl, + a11yCheckConfig: {}, + options: { + value: [true, false], + disabled: [true, false], + readOnly: [true, false], + name: ['', 'name'], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts new file mode 100644 index 000000000000..2d4816b56334 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts @@ -0,0 +1,61 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - switch', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch off', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: false }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch disabled on', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: true, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch disabled off', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: false, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch readOnly', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: true, readOnly: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch with name', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: true, name: 'switchName' }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch readOnly off', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: false, readOnly: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch with switchedOnText and switchedOffText', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: true, switchedOnText: 'YES', switchedOffText: 'NO' }); + await a11yCheck(page, {}, '#container'); + }); + + test('switch off with custom labels', async ({ page }) => { + await createWidget(page, 'dxSwitch', { value: false, switchedOnText: 'ON', switchedOffText: 'OFF' }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.matrix.spec.ts new file mode 100644 index 000000000000..1920f4576d52 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.matrix.spec.ts @@ -0,0 +1,30 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = [ + { title: 'John Heart', text: 'John Heart' }, + { title: 'Marina Thomas', text: 'Marina Thomas', disabled: true }, + { title: 'Robert Reagan', text: 'Robert Reagan' }, + { title: 'Greta Sims', text: 'Greta Sims' }, + { title: 'Olivia Peyton', text: 'Olivia Peyton' }, + { title: 'Ed Holmes', text: 'Ed Holmes' }, + { title: 'Wally Hobbs', text: 'Wally Hobbs' }, + { title: 'Brad Jameson', text: 'Brad Jameson' }, +]; + +test.describe('Accessibility - tabPanel matrix', () => { + testAccessibilityMatrix({ + component: 'dxTabPanel', + containerUrl, + options: { + dataSource: [[], items], + showNavButtons: [true, false], + disabled: [true, false], + width: [450, 'auto'], + height: [250, 550], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts new file mode 100644 index 000000000000..c1c626f96f87 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts @@ -0,0 +1,114 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tabPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { dataSource: [{ title: 'John Heart', text: 'John Heart' }, { title: 'Robert Reagan', text: 'Robert Reagan' }], width: 450, height: 250 }); + await a11yCheck(page, {}, '#container'); + }); + + test('empty tab panel', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { dataSource: [], width: 450, height: 250 }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel with disabled item', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [ + { title: 'Active', text: 'Active tab' }, + { title: 'Disabled', text: 'Disabled tab', disabled: true }, + { title: 'Another', text: 'Another tab' }, + ], + width: 450, + height: 250, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel disabled', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [{ title: 'Tab 1', text: 'Content 1' }, { title: 'Tab 2', text: 'Content 2' }], + width: 450, + height: 250, + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel with navigation buttons', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [ + { title: 'Tab 1', text: 'Content 1' }, + { title: 'Tab 2', text: 'Content 2' }, + { title: 'Tab 3', text: 'Content 3' }, + ], + width: 200, + height: 250, + showNavButtons: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel disabled without nav buttons', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [{ title: 'Tab 1', text: 'Content 1' }, { title: 'Tab 2', text: 'Content 2' }], + width: 450, + height: 550, + showNavButtons: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel tall height', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [{ title: 'Tab 1', text: 'Content 1' }, { title: 'Tab 2', text: 'Content 2' }], + width: 450, + height: 550, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel with swipe enabled', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [{ title: 'Tab 1', text: 'Content 1' }, { title: 'Tab 2', text: 'Content 2' }], + width: 450, + height: 250, + swipeEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel with loop enabled', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [{ title: 'Tab 1', text: 'Content 1' }, { title: 'Tab 2', text: 'Content 2' }, { title: 'Tab 3', text: 'Content 3' }], + width: 450, + height: 250, + loop: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tab panel with icon in tab', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + dataSource: [ + { title: 'Info', icon: 'info', text: 'Information tab' }, + { title: 'Settings', icon: 'preferences', text: 'Settings tab' }, + ], + width: 450, + height: 250, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts new file mode 100644 index 000000000000..6c5005cce997 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts @@ -0,0 +1,103 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tabs', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTabs', { dataSource: [{ text: 'John Heart' }, { text: 'Robert Reagan' }], width: 450, height: 250 }); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs with disabled item', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [ + { text: 'Tab 1' }, + { text: 'Tab 2', disabled: true }, + { text: 'Tab 3' }, + ], + width: 450, + height: 250, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs with navigation buttons', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [{ text: 'Tab 1' }, { text: 'Tab 2' }, { text: 'Tab 3' }, { text: 'Tab 4' }], + width: 200, + height: 250, + showNavButtons: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs with focused item', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [{ text: 'Tab 1' }, { text: 'Tab 2' }, { text: 'Tab 3' }], + width: 450, + height: 250, + }); + await page.keyboard.press('Tab'); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs without navigation buttons', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [{ text: 'Tab 1' }, { text: 'Tab 2' }, { text: 'Tab 3' }], + width: 450, + height: 250, + showNavButtons: false, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs with icons', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [{ text: 'Info', icon: 'info' }, { text: 'Settings', icon: 'preferences' }], + width: 450, + height: 250, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs with orientation vertical', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [{ text: 'Tab 1' }, { text: 'Tab 2' }, { text: 'Tab 3' }], + width: 200, + height: 250, + orientation: 'vertical', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs disabled', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [{ text: 'Tab 1' }, { text: 'Tab 2' }], + width: 450, + height: 250, + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tabs with selectedIndex 1', async ({ page }) => { + await createWidget(page, 'dxTabs', { + dataSource: [{ text: 'Tab 1' }, { text: 'Tab 2' }, { text: 'Tab 3' }], + width: 450, + height: 250, + selectedIndex: 1, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.matrix.spec.ts new file mode 100644 index 000000000000..2b56593f9782 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.matrix.spec.ts @@ -0,0 +1,62 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const items = [ + 'HD Video Player', + 'SuperHD Video Player', + 'SuperPlasma 50', +]; + +const buttonsOptions = { + dataSource: [items], + value: [[items[0]]], + label: [undefined, 'label'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +test.describe('Accessibility - tagBox matrix', () => { + testAccessibilityMatrix({ + component: 'dxTagBox', + containerUrl, + a11yCheckConfig: {}, + options: { + dataSource: [[], items], + value: [undefined, [items[0]]], + disabled: [true, false], + readOnly: [true, false], + searchEnabled: [true, false], + searchTimeout: [0], + placeholder: [undefined, 'placeholder'], + inputAttr: [{ 'aria-label': 'aria-label' }], + }, + }); +}); + +test.describe('Accessibility - tagBox matrix (buttons)', () => { + testAccessibilityMatrix({ + component: 'dxTagBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...buttonsOptions, + showClearButton: [true, false], + showDropDownButton: [true, false], + }, + }); +}); + +test.describe('Accessibility - tagBox matrix (popup)', () => { + testAccessibilityMatrix({ + component: 'dxTagBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...buttonsOptions, + opened: [true], + showSelectionControls: [true, false], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts new file mode 100644 index 000000000000..b1d2efcce316 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts @@ -0,0 +1,76 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tagBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox with value', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], value: ['HD Video Player'], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox disabled', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox readOnly', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox with search enabled', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], searchEnabled: true, searchTimeout: 0, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox with showClearButton', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], value: ['HD Video Player'], showClearButton: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox opened with showSelectionControls', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], value: ['HD Video Player'], opened: true, showSelectionControls: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox opened without showSelectionControls', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], value: ['HD Video Player'], opened: true, showSelectionControls: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox with label', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], value: ['HD Video Player'], label: 'label', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox without showDropDownButton', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], value: ['HD Video Player'], showDropDownButton: false, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox with custom button', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], value: ['HD Video Player'], buttons: [{ name: 'today', location: 'before', options: { text: 'Today', stylingMode: 'text' } }], inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('tagBox with placeholder', async ({ page }) => { + await createWidget(page, 'dxTagBox', { dataSource: ['HD Video Player', 'SuperHD Video Player', 'SuperPlasma 50'], placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts new file mode 100644 index 000000000000..0a8d42fc43b2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts @@ -0,0 +1,72 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - textArea', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test text', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea disabled', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test text', disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea readOnly', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test text', readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea with label', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test text', label: 'label', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea with placeholder', async ({ page }) => { + await createWidget(page, 'dxTextArea', { placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea with name', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test text', name: 'textAreaName', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea with spellcheck', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test text', spellcheck: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea with long text', async ({ page }) => { + const longText = 'Prepare 2013 Marketing Plan: We need to double revenues in 2013 and our marketing strategy is going to be key here.'; + await createWidget(page, 'dxTextArea', { value: longText, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea with height', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test text', height: 150, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea with maxLength', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Test', maxLength: 100, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textArea disabled with label', async ({ page }) => { + await createWidget(page, 'dxTextArea', { value: 'Disabled text', disabled: true, label: 'Notes', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.matrix.spec.ts new file mode 100644 index 000000000000..fb50c0222945 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.matrix.spec.ts @@ -0,0 +1,51 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const baseOptions = { + value: [undefined, 'value'], + placeholder: [undefined, 'placeholder'], + showClearButton: [true, false], + mode: ['password', 'email', 'search', 'tel', 'text', 'url'], + inputAttr: [{ 'aria-label': 'aria-label' }], +}; + +test.describe('Accessibility - textBox matrix (availability)', () => { + testAccessibilityMatrix({ + component: 'dxTextBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + disabled: [true, false], + readOnly: [true, false], + }, + }); +}); + +test.describe('Accessibility - textBox matrix (info)', () => { + testAccessibilityMatrix({ + component: 'dxTextBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + label: ['', 'label'], + name: ['', 'name'], + }, + }); +}); + +test.describe('Accessibility - textBox matrix (spellcheck)', () => { + testAccessibilityMatrix({ + component: 'dxTextBox', + containerUrl, + a11yCheckConfig: {}, + options: { + ...baseOptions, + spellcheck: [true], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts new file mode 100644 index 000000000000..4987dd1d4e3e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts @@ -0,0 +1,105 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - textBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox disabled', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox readOnly', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', readOnly: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox mode password', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'secret', mode: 'password', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox mode search', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'query', mode: 'search', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox with label', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', label: 'label', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox with showClearButton', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', showClearButton: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox mode email', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'test@test.com', mode: 'email', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox mode tel', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: '+1-555-0100', mode: 'tel', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox mode url', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'https://example.com', mode: 'url', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox with name', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', name: 'name', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox with placeholder', async ({ page }) => { + await createWidget(page, 'dxTextBox', { placeholder: 'placeholder', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox with spellcheck', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', spellcheck: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox disabled with placeholder', async ({ page }) => { + await createWidget(page, 'dxTextBox', { placeholder: 'placeholder', disabled: true, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox with maxLength', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: 'value', maxLength: 50, inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox with custom button', async ({ page }) => { + await createWidget(page, 'dxTextBox', { + value: 'value', + inputAttr: { 'aria-label': 'aria-label' }, + buttons: [{ name: 'custom', location: 'after', options: { icon: 'search', stylingMode: 'text' } }], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('textBox mode number', async ({ page }) => { + await createWidget(page, 'dxTextBox', { value: '42', mode: 'number', inputAttr: { 'aria-label': 'aria-label' } }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts new file mode 100644 index 000000000000..e15675ded9f6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts @@ -0,0 +1,82 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tileView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTileView', { items: [{ text: 'test 1' }], focusStateEnabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('tile view with multiple tiles and focus', async ({ page }) => { + await createWidget(page, 'dxTileView', { + items: [{ text: 'Tile 1' }, { text: 'Tile 2' }, { text: 'Tile 3' }], + focusStateEnabled: true, + }); + await page.keyboard.press('Tab'); + await a11yCheck(page, {}, '#container'); + }); + + test('tile view with custom tile sizes', async ({ page }) => { + await createWidget(page, 'dxTileView', { + items: [ + { text: 'Wide Tile', widthRatio: 2 }, + { text: 'Normal Tile' }, + { text: 'Tall Tile', heightRatio: 2 }, + ], + focusStateEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tile view focused via keyboard', async ({ page }) => { + await createWidget(page, 'dxTileView', { items: [{ text: 'test 1' }], focusStateEnabled: true }); + await page.keyboard.press('Tab'); + await a11yCheck(page, {}, '#container'); + }); + + test('tile view disabled', async ({ page }) => { + await createWidget(page, 'dxTileView', { + items: [{ text: 'Tile 1' }, { text: 'Tile 2' }], + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tile view with direction vertical', async ({ page }) => { + await createWidget(page, 'dxTileView', { + items: [{ text: 'Tile 1' }, { text: 'Tile 2' }, { text: 'Tile 3' }], + direction: 'vertical', + height: 400, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tile view with base dimensions', async ({ page }) => { + await createWidget(page, 'dxTileView', { + items: [{ text: 'Tile 1' }, { text: 'Tile 2' }], + baseItemHeight: 100, + baseItemWidth: 100, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tile view with item margin', async ({ page }) => { + await createWidget(page, 'dxTileView', { + items: [{ text: 'Tile 1' }, { text: 'Tile 2' }, { text: 'Tile 3' }], + itemMargin: 5, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts new file mode 100644 index 000000000000..136f8c050b70 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - toast', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, message: 'message', type: 'info' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast type error', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, message: 'Error occurred', type: 'error' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast type success', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, message: 'Operation completed', type: 'success' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast type warning', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, message: 'Warning', type: 'warning' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast type custom', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, type: 'custom' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast without message', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, type: 'info' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast error without message', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, type: 'error' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast success without message', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, type: 'success' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast not visible', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: false, message: 'hidden toast', type: 'info' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast warning without message', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, type: 'warning' }); + await a11yCheck(page, {}, '#container'); + }); + + test('toast with long message', async ({ page }) => { + await createWidget(page, 'dxToast', { visible: true, message: 'A very long notification message that spans multiple lines and provides detailed information to the user', type: 'info', width: 400 }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts new file mode 100644 index 000000000000..1296b3010c62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts @@ -0,0 +1,115 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - toolbar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxToolbar', { items: [{ text: 'item1', locateInMenu: 'always' }, { text: 'item2', locateInMenu: 'always' }] }); + await a11yCheck(page, {}, '#container'); + }); + + test('disabled toolbar', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [{ text: 'item1', locateInMenu: 'always' }, { text: 'item2', locateInMenu: 'always' }], + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('toolbar with overflow menu open', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'item1', locateInMenu: 'always' }, + { text: 'item2', locateInMenu: 'always' }, + { text: 'item3', locateInMenu: 'always' }, + ], + width: 50, + }); + await page.locator('.dx-toolbar-menu-button').click(); + await a11yCheck(page, {}, '#container'); + }); + + test('toolbar with mixed item locations', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'Before', location: 'before' }, + { text: 'Center', location: 'center' }, + { text: 'After', location: 'after', locateInMenu: 'always' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('toolbar with narrow width overflow menu', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'item0', locateInMenu: 'always' }, + { text: 'item1', locateInMenu: 'always' }, + { text: 'item2', locateInMenu: 'always' }, + { text: 'item3', locateInMenu: 'always' }, + { text: 'item4', locateInMenu: 'always' }, + ], + width: 50, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('disabled toolbar with narrow width', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'item1', locateInMenu: 'always' }, + { text: 'item2', locateInMenu: 'always' }, + ], + width: 50, + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('toolbar with button widget items', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { location: 'before', widget: 'dxButton', options: { icon: 'back', text: 'Back' } }, + { location: 'center', text: 'Page Title' }, + { location: 'after', widget: 'dxButton', options: { icon: 'search' } }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('toolbar with selectBox item', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { + location: 'before', + widget: 'dxSelectBox', + options: { dataSource: ['Option 1', 'Option 2'], value: 'Option 1', inputAttr: { 'aria-label': 'Options' } }, + }, + { location: 'after', widget: 'dxButton', options: { text: 'Submit' } }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('toolbar with menu always items', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'visible item', location: 'before' }, + { text: 'menu item 1', locateInMenu: 'always' }, + { text: 'menu item 2', locateInMenu: 'always' }, + ], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts new file mode 100644 index 000000000000..b3f11b251cb9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts @@ -0,0 +1,67 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tooltip', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: true, target: '#container', width: 50, height: 25 }); + await a11yCheck(page, {}, '#container'); + }); + + test('disabled tooltip', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: true, target: '#container', width: 50, height: 25, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('tooltip with custom content', async ({ page }) => { + await createWidget(page, 'dxTooltip', { + visible: true, + target: '#container', + width: 150, + height: 50, + contentTemplate: () => 'Tooltip content', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('tooltip not visible', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: false, target: '#container', width: 50, height: 25 }); + await a11yCheck(page, {}, '#container'); + }); + + test('tooltip disabled not visible', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: false, target: '#container', width: 50, height: 25, disabled: true }); + await a11yCheck(page, {}, '#container'); + }); + + test('tooltip with position bottom', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: true, target: '#container', width: 100, height: 30, position: 'bottom' }); + await a11yCheck(page, {}, '#container'); + }); + + test('tooltip with position top', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: true, target: '#container', width: 100, height: 30, position: 'top' }); + await a11yCheck(page, {}, '#container'); + }); + + test('tooltip with position left', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: true, target: '#container', width: 100, height: 30, position: 'left' }); + await a11yCheck(page, {}, '#container'); + }); + + test('tooltip with closeOnOutsideClick', async ({ page }) => { + await createWidget(page, 'dxTooltip', { visible: true, target: '#container', width: 100, height: 30, closeOnOutsideClick: false }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts new file mode 100644 index 000000000000..63a5a36c58aa --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const treeListData = [ + { Task_ID: 1, Task_Subject: 'Plans 2015', Task_Parent_ID: 0 }, + { Task_ID: 2, Task_Subject: 'Health Insurance', Task_Parent_ID: 1 }, + { Task_ID: 3, Task_Subject: 'New Brochures', Task_Parent_ID: 1 }, +]; + +test.describe('Accessibility - TreeList aria', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('aria expanded toggle', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: treeListData, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + expandedRowKeys: [1], + columns: ['Task_Subject', 'Task_ID'], + }); + + const container = page.locator('#container'); + await expect(container.locator('[aria-label]').first()).toBeVisible(); + await a11yCheck(page, {}, '#container'); + }); + + test('aria collapsed rows', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: treeListData, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + expandedRowKeys: [], + columns: ['Task_Subject', 'Task_ID'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('aria with search panel', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: treeListData, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + expandedRowKeys: [1], + columns: ['Task_Subject', 'Task_ID'], + searchPanel: { visible: true }, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts new file mode 100644 index 000000000000..6a9fb5fb6c2b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts @@ -0,0 +1,80 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +function getData(rowCount: number): Record[] { + const data = Array.from({ length: rowCount }, (_, index) => ({ + id: index + 1, + parentId: index % 5, + field1: `test 1 ${index + 2}`, + field2: `test 2 ${index + 2}`, + })); + data.unshift({ id: 0, parentId: -1, field1: 'test 1 0', field2: 'test 2 0' }); + return data; +} + +test.describe('Accessibility - TreeList common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('search panel, pager and selection', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getData(40), + keyExpr: 'id', + parentIdExpr: 'parentId', + rootValue: -1, + autoExpandAll: true, + paging: { enabled: true, pageSize: 5 }, + scrolling: { mode: 'standard' }, + selection: { mode: 'multiple' }, + searchPanel: { visible: true }, + columns: ['id', 'parentId', 'field1', 'field2'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('basic treeList without extras', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getData(20), + keyExpr: 'id', + parentIdExpr: 'parentId', + rootValue: -1, + autoExpandAll: true, + columns: ['id', 'parentId', 'field1', 'field2'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeList with single selection', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getData(20), + keyExpr: 'id', + parentIdExpr: 'parentId', + rootValue: -1, + selection: { mode: 'single' }, + columns: ['id', 'parentId', 'field1', 'field2'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeList with virtual scrolling', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getData(200), + keyExpr: 'id', + parentIdExpr: 'parentId', + rootValue: -1, + height: 400, + scrolling: { mode: 'virtual' }, + columns: ['id', 'parentId', 'field1', 'field2'], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts new file mode 100644 index 000000000000..b317aef37d0e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts @@ -0,0 +1,72 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const DATA_SOURCE = [ + { id: 0, label: 'A', value: 350 }, + { id: 1, parentId: 0, label: 'B', value: 1200 }, + { id: 2, parentId: 0, label: 'C', value: 750 }, +]; + +test.describe('Accessibility - TreeList status', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('rows expanded', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + rootValue: -1, + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('rows collapsed', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + rootValue: -1, + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: false, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with selection mode multiple', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + rootValue: -1, + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + selection: { mode: 'multiple' }, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); + + test('with paging enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + rootValue: -1, + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + paging: { enabled: true, pageSize: 2 }, + columns: ['label', 'value'], + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.matrix.spec.ts new file mode 100644 index 000000000000..0a26a2a8240a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.matrix.spec.ts @@ -0,0 +1,52 @@ +import { test } from '@playwright/test'; +import { testAccessibilityMatrix } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +const employees = [ + { + id: 1, + fullName: 'John Heart', + parentId: 0, + hasItems: true, + }, + { + id: 2, + fullName: 'Samantha Bright', + parentId: 1, + hasItems: false, + }, + { + id: 3, + fullName: 'Kevin Carter', + parentId: 1, + hasItems: false, + }, + { + id: 4, + fullName: 'Robert Reagan', + parentId: 0, + hasItems: true, + }, + { + id: 5, + fullName: 'Amelia Harper', + parentId: 4, + hasItems: false, + }, +]; + +test.describe('Accessibility - treeView matrix', () => { + testAccessibilityMatrix({ + component: 'dxTreeView', + containerUrl, + options: { + items: [[], employees], + searchEnabled: [true, false], + showCheckBoxesMode: ['none', 'normal', 'selectAll'], + noDataText: [undefined, 'no data text'], + displayExpr: ['fullName'], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts new file mode 100644 index 000000000000..5f1bfb9a5175 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts @@ -0,0 +1,122 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - treeView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const treeViewItems = [ + { id: 1, text: 'John Heart', expanded: true, items: [ + { id: 2, text: 'Samantha Bright', expanded: true, items: [ + { id: 3, text: 'Kevin Carter' }, + { id: 4, text: 'Brett Wade' }, + ] }, + ] }, + { id: 5, text: 'Robert Reagan', items: [ + { id: 6, text: 'Amelia Harper' }, + ] }, + ]; + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxTreeView', { items: [{ text: 'Item 1', items: [{ text: 'Item 1.1' }] }, { text: 'Item 2' }] }); + await a11yCheck(page, {}, '#container'); + }); + + test('empty treeView', async ({ page }) => { + await createWidget(page, 'dxTreeView', { items: [] }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView with search enabled', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: treeViewItems, + searchEnabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView with normal checkboxes', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: treeViewItems, + showCheckBoxesMode: 'normal', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView with selectAll checkboxes', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: treeViewItems, + showCheckBoxesMode: 'selectAll', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView with noDataText', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: [], + noDataText: 'No items found', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView search with checkboxes', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: treeViewItems, + searchEnabled: true, + showCheckBoxesMode: 'normal', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView empty with search', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: [], + searchEnabled: true, + noDataText: 'no data text', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView disabled', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: [{ text: 'Item 1', items: [{ text: 'Item 1.1' }] }, { text: 'Item 2' }], + disabled: true, + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView with single selection', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: treeViewItems, + selectionMode: 'single', + showCheckBoxesMode: 'none', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView with multiple selection', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: treeViewItems, + selectionMode: 'multiple', + showCheckBoxesMode: 'normal', + }); + await a11yCheck(page, {}, '#container'); + }); + + test('treeView with virtualModeEnabled', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: treeViewItems, + virtualModeEnabled: false, + }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts new file mode 100644 index 000000000000..476bea324d3e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts @@ -0,0 +1,46 @@ +import { test } from '@playwright/test'; +import { createWidget, a11yCheck } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - validationSummary', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('accessibility check', async ({ page }) => { + await createWidget(page, 'dxValidationSummary', {}); + await a11yCheck(page, {}, '#container'); + }); + + test('validationSummary with validationGroup', async ({ page }) => { + await createWidget(page, 'dxValidationSummary', { validationGroup: 'myGroup' }); + await a11yCheck(page, {}, '#container'); + }); + + test('validationSummary with elementAttr', async ({ page }) => { + await createWidget(page, 'dxValidationSummary', { elementAttr: { 'aria-label': 'Validation Summary' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('validationSummary with validationGroup and elementAttr', async ({ page }) => { + await createWidget(page, 'dxValidationSummary', { validationGroup: 'formGroup', elementAttr: { 'aria-label': 'Form Errors' } }); + await a11yCheck(page, {}, '#container'); + }); + + test('validationSummary with custom width', async ({ page }) => { + await createWidget(page, 'dxValidationSummary', { width: 300 }); + await a11yCheck(page, {}, '#container'); + }); + + test('validationSummary with custom height', async ({ page }) => { + await createWidget(page, 'dxValidationSummary', { height: 100 }); + await a11yCheck(page, {}, '#container'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/a11y.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/a11y.functional.spec.ts new file mode 100644 index 000000000000..c4bd6cffc5e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/a11y.functional.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.A11y.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('column chooser popup should have aria-label attribute', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { + enabled: true, + }, + columns: ['Column 1'], + }); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxCardView('instance'); + instance.showColumnChooser(); + }); + + const ariaLabel = await page.locator('.dx-cardview-column-chooser .dx-overlay-content').getAttribute('aria-label'); + expect(ariaLabel).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/api.functional.spec.ts new file mode 100644 index 000000000000..f4d8799f42fe --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/api.functional.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('public method showColumnChooser', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); + + const columnChooser = page.locator('.dx-cardview-column-chooser'); + await expect(columnChooser).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + await expect(columnChooser).toBeVisible(); + }); + + test('public method hideColumnChooser', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); + + await page.locator('.dx-cardview-column-chooser-button').click(); + const columnChooser = page.locator('.dx-cardview-column-chooser'); + await expect(columnChooser).toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').hideColumnChooser(); + }); + await expect(columnChooser).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/functional.spec.ts new file mode 100644 index 000000000000..e7e626f22f44 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/functional.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('column chooser in select mode should work after multiple hide/show actions', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + ], + columns: ['a', 'b', 'c'], + columnChooser: { + enabled: true, + mode: 'select', + }, + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + const columnChooser = page.locator('.dx-cardview-column-chooser'); + const checkboxes = columnChooser.locator('.dx-checkbox'); + + await checkboxes.nth(0).click(); + await expect(checkboxes).toHaveCount(3); + + await checkboxes.nth(0).click(); + await expect(checkboxes).toHaveCount(3); + + await checkboxes.nth(0).click(); + await checkboxes.nth(0).click(); + }); + + test('column chooser in dragAndDrop mode should work after multiple hide/show actions', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + ], + columns: ['a', 'b', 'c'], + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + }, + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + await expect(headerItems).toHaveCount(3); + }); + + test('ColumnChooser should receive and render custom texts', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.localization.loadMessages({ + en: { + 'dxDataGrid-columnChooserTitle': 'customTitle', + 'dxDataGrid-columnChooserEmptyText': 'customEmptyText', + }, + }); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [], + keyExpr: 'ID', + cardsPerRow: 'auto', + cardMinWidth: 300, + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + height: '340px', + }, + columns: [], + }); + + await page.locator('.dx-cardview-column-chooser-button').click(); + + const columnChooser = page.locator('.dx-cardview-column-chooser'); + const title = columnChooser.locator('.dx-popup-title'); + const emptyMessage = columnChooser.locator('.dx-empty-message'); + + await expect(title).toHaveText('customTitle'); + await expect(emptyMessage).toHaveText('customEmptyText'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/visual.spec.ts new file mode 100644 index 000000000000..f308461a6338 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/visual.spec.ts @@ -0,0 +1,85 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test("column chooser in 'select' mode", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { + enabled: true, + mode: 'select', + height: 400, + width: 400, + search: { enabled: true }, + selection: { allowSelectAll: true }, + }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2', allowHiding: false }, + { dataField: 'Column 3', showInColumnChooser: false }, + { dataField: 'Column 4' }, + ], + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + await testScreenshot(page, 'card-view_column-chooser_select_mode.png', { + element: page.locator('.dx-cardview-column-chooser .dx-overlay-content'), + }); + }); + + test("column chooser in 'dragAndDrop' mode", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + height: 400, + width: 400, + search: { enabled: true }, + }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2', visible: false, allowHiding: false }, + { dataField: 'Column 3', visible: false, showInColumnChooser: false }, + { dataField: 'Column 4', visible: false }, + ], + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + await testScreenshot(page, 'card-view_column-chooser_drag_mode.png', { + element: page.locator('.dx-cardview-column-chooser .dx-overlay-content'), + }); + }); + + test('cardView with opened columnChooser', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: Array.from({ length: 50 }, (_, i) => ({ value: `value_${i}` })), + columnChooser: { enabled: true }, + columns: [{ dataField: 'value' }], + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + await testScreenshot(page, 'card-view_with_opened_column-chooser.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/functional.spec.ts new file mode 100644 index 000000000000..d68ea2d32dd7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/functional.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - ColumnSortable.Functional', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + { allowColumnReordering: false, allowReordering: false, result: false }, + { allowColumnReordering: false, allowReordering: true, result: false }, + { allowColumnReordering: true, allowReordering: false, result: false }, + { allowColumnReordering: true, allowReordering: true, result: true }, + ].forEach(({ allowColumnReordering, allowReordering, result }) => { + test(`header column is draggable: ${result}, when allowColumnReordering: ${allowColumnReordering}, allowReordering: ${allowReordering}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + allowColumnReordering, + columns: [{ + dataField: 'test', + allowReordering, + }], + }); + + const columnElement = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + + await page.evaluate((selector) => { + const element = document.querySelector(selector) as Element; + const left = element.getBoundingClientRect().left + 5; + const top = element.getBoundingClientRect().top + 5; + element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: left, clientY: top })); + element.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: left, clientY: top + 30 })); + }, '.dx-cardview-headers .dx-cardview-header-item'); + + const dragging = page.locator('.dx-sortable-dragging'); + if (result) { + await expect(dragging).toBeVisible(); + } else { + await expect(dragging).not.toBeVisible(); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts new file mode 100644 index 000000000000..6fe6ce31940c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts @@ -0,0 +1,110 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const COLUMN_CHOOSER_ITEM_SELECTOR = '.dx-cardview-column-chooser .dx-treeview-item'; + +async function triggerDragStart(page: import('@playwright/test').Page, selector: string): Promise { + await page.evaluate((sel) => { + const element = document.querySelector(sel) as Element; + const left = element.getBoundingClientRect().left + 5; + const top = element.getBoundingClientRect().top + 5; + element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: left, clientY: top })); + element.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: left, clientY: top + 30 })); + }, selector); +} + +async function triggerDragEnd(page: import('@playwright/test').Page, selector: string): Promise { + await page.evaluate((sel) => { + const element = document.querySelector(sel) as Element; + const { top, left } = element.getBoundingClientRect(); + element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: left, clientY: top })); + }, selector); +} + +test.describe('CardView - ColumnSortable.Visual', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test.skip('headerPanel dragging column when it has sorting and headerFilter', async ({ page }) => { + await createWidget(page, 'dxCardView', { + allowColumnReordering: true, + columnChooser: { enabled: true }, + headerFilter: { visible: true }, + columns: [{ + dataField: 'test', + allowReordering: true, + sortOrder: 'asc', + }], + }); + + await page.evaluate(() => { + const element = document.querySelector('.dx-cardview-headers .dx-cardview-header-item') as Element; + const left = element.getBoundingClientRect().left + 5; + const top = element.getBoundingClientRect().top + 5; + element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: left, clientY: top })); + element.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: left, clientY: top + 30 })); + }); + + await testScreenshot(page, 'card-view_column-sortable_header-panel_dragging-column.png', { + element: page.locator('#container'), + }); + }); + + test('dropzone appear in headerPanel when drag from columnChooser a column', async ({ page }) => { + await createWidget(page, 'dxCardView', { + allowColumnReordering: true, + columnChooser: { enabled: true }, + height: 600, + columns: [{ dataField: 'Column 1', visible: false }], + }); + + await page.evaluate(() => { + (window as any).$('#container').dxCardView('instance').showColumnChooser(); + }); + await page.waitForSelector(COLUMN_CHOOSER_ITEM_SELECTOR); + + await triggerDragStart(page, COLUMN_CHOOSER_ITEM_SELECTOR); + await page.waitForTimeout(500); + await testScreenshot(page, 'card-view_column-sortable_empty-header-panel_dropzone_1.png', { + element: page.locator('#container'), + }); + + await triggerDragEnd(page, COLUMN_CHOOSER_ITEM_SELECTOR); + await page.waitForTimeout(500); + await testScreenshot(page, 'card-view_column-sortable_empty-header-panel_dropzone_2.png', { + element: page.locator('#container'), + }); + }); + + test('dropzone appears in headerPanel when drag from columnChooser a column with allowReordering: false', async ({ page }) => { + await createWidget(page, 'dxCardView', { + allowColumnReordering: true, + columnChooser: { enabled: true }, + height: 600, + columns: [ + { dataField: 'Column 1' }, + { dataField: 'Column 2', visible: false, allowReordering: false }, + ], + }); + + await page.evaluate(() => { + (window as any).$('#container').dxCardView('instance').showColumnChooser(); + }); + await page.waitForSelector(COLUMN_CHOOSER_ITEM_SELECTOR); + + await triggerDragStart(page, COLUMN_CHOOSER_ITEM_SELECTOR); + await page.waitForTimeout(500); + await testScreenshot(page, 'card-view_column-sortable_header-panel_dropzone_1.png', { + element: page.locator('#container'), + }); + + await triggerDragEnd(page, COLUMN_CHOOSER_ITEM_SELECTOR); + await page.waitForTimeout(500); + await testScreenshot(page, 'card-view_column-sortable_header-panel_dropzone_2.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/common/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/common/behavior.functional.spec.ts new file mode 100644 index 000000000000..87b4e457f68e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/common/behavior.functional.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - Common Behavior', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('cardHeader.visibility property should change on contentReady', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ ID: 1 }], + onContentReady(e) { + e.component.option('cardHeader.visible', true); + }, + }); + + const headerVisible = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').option('cardHeader.visible'); + }); + expect(headerVisible).toBe(true); + + const cardHeader = page.locator('.dx-cardview-card .dx-cardview-card-header'); + await expect(cardHeader.first()).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts new file mode 100644 index 000000000000..642e3243535c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts @@ -0,0 +1,196 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +const CONFIG = { + dataSource: [ + { caption1: 'value11', caption2: 'value21', caption3: 'value31' }, + { caption1: 'value12', caption2: 'value22', caption3: 'value32' }, + { caption1: 'value13', caption2: 'value23', caption3: 'value33' }, + { caption1: 'value14', caption2: 'value24', caption3: 'value34' }, + { caption1: 'value15', caption2: 'value25', caption3: 'value35' }, + ], + onCardClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onCardClick ??= []; + window.dxCardViewEventTest.onCardClick.push(e); + }, + onCardDblClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onCardDblClick ??= []; + window.dxCardViewEventTest.onCardDblClick.push(e); + }, + onCardPrepared(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onCardPrepared ??= []; + window.dxCardViewEventTest.onCardPrepared.push(e); + }, + onFieldCaptionClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onFieldCaptionClick ??= []; + window.dxCardViewEventTest.onFieldCaptionClick.push(e); + }, + onFieldCaptionDblClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onFieldCaptionDblClick ??= []; + window.dxCardViewEventTest.onFieldCaptionDblClick.push(e); + }, + onFieldCaptionPrepared(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onFieldCaptionPrepared ??= []; + window.dxCardViewEventTest.onFieldCaptionPrepared.push(e); + }, + onFieldValueClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onFieldValueClick ??= []; + window.dxCardViewEventTest.onFieldValueClick.push(e); + }, + onFieldValueDblClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onFieldValueDblClick ??= []; + window.dxCardViewEventTest.onFieldValueDblClick.push(e); + }, + onFieldValuePrepared(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onFieldValuePrepared ??= []; + window.dxCardViewEventTest.onFieldValuePrepared.push(e); + }, + onCardHoverChanged(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onCardHoverChanged ??= []; + window.dxCardViewEventTest.onCardHoverChanged.push(e); + }, + onDisposing() { + delete window.dxCardViewEventTest; + }, +}; + +test.describe('CardView - ContentView - events', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('onCardClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().click(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onCardClick?.length); + expect(count).toBe(1); + }); + + test('onCardDblClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().dblclick(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onCardDblClick?.length); + expect(count).toBe(1); + }); + + test('onCardPrepared', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onCardPrepared?.length); + expect(count).toBe(5); + }); + + test('onFieldCaptionClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-caption').first().click(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onFieldCaptionClick?.length); + expect(count).toBe(1); + }); + + test('onFieldCaptionDblClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-caption').first().dblclick(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onFieldCaptionDblClick?.length); + expect(count).toBe(1); + }); + + test('onFieldCaptionPrepared', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onFieldCaptionPrepared?.length); + expect(count).toBe(15); + }); + + test('onFieldValueClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-value').first().click(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onFieldValueClick?.length); + expect(count).toBe(1); + }); + + test('onFieldValueDblClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-value').first().dblclick(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onFieldValueDblClick?.length); + expect(count).toBe(1); + }); + + test('onFieldValuePrepared', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onFieldValuePrepared?.length); + expect(count).toBe(15); + }); + + test('onCardHoverChanged on hover', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().hover(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onCardHoverChanged?.length); + expect(count).toBeGreaterThanOrEqual(1); + }); + + test('onCardClick event has correct card info', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().click(); + + const result = await page.evaluate(() => { + const event = (window as any).dxCardViewEventTest?.onCardClick?.[0]; + if (!event) return false; + return event.card.index === 0 && !!event.cardElement; + }); + expect(result).toBe(true); + }); + + test('onFieldCaptionClick event has correct field info', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-caption').first().click(); + + const result = await page.evaluate(() => { + const event = (window as any).dxCardViewEventTest?.onFieldCaptionClick?.[0]; + if (!event) return false; + return event.field.index === 0 && event.field.card.index === 0 && !!event.fieldCaptionElement; + }); + expect(result).toBe(true); + }); + + test('onFieldValueClick event has correct field info', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-value').first().click(); + + const result = await page.evaluate(() => { + const event = (window as any).dxCardViewEventTest?.onFieldValueClick?.[0]; + if (!event) return false; + return event.field.index === 0 && event.field.card.index === 0 && !!event.fieldValueElement; + }); + expect(result).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.functional.spec.ts new file mode 100644 index 000000000000..4a24e20a0beb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.functional.spec.ts @@ -0,0 +1,169 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const baseData = [ + { id: 1, name: 'Alice', status: 'Active' }, + { id: 2, name: 'Bob', status: 'Inactive' }, + { id: 3, name: 'Charlie', status: 'Active' }, +]; + +test.describe('CardView - ContextMenu Functional', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('context menu is shown on right-click on header item', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: ['id', 'name', 'status'], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click({ button: 'right' }); + + await expect(page.locator('.dx-context-menu')).toBeVisible(); + }); + + test('context menu is dismissed after clicking outside', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: ['id', 'name', 'status'], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click({ button: 'right' }); + await expect(page.locator('.dx-context-menu')).toBeVisible(); + + await page.locator('body').click({ position: { x: 5, y: 5 } }); + await expect(page.locator('.dx-context-menu')).not.toBeVisible(); + }); + + test('context menu contains sort ascending item', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: [{ dataField: 'name', allowSorting: true }], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click({ button: 'right' }); + + const contextMenu = page.locator('.dx-context-menu'); + await expect(contextMenu).toBeVisible(); + + const menuItems = contextMenu.locator('.dx-menu-item-text'); + const itemTexts = await menuItems.allTextContents(); + expect(itemTexts.some((text) => text.toLowerCase().includes('sort'))).toBeTruthy(); + }); + + test('context menu sort ascending updates card order', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 3, name: 'Charlie' }, + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ], + columns: [{ dataField: 'name', allowSorting: true }], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click({ button: 'right' }); + + const sortAscItem = page.locator('.dx-context-menu .dx-menu-item').filter({ hasText: /sort ascending/i }); + if (await sortAscItem.isVisible()) { + await sortAscItem.click(); + + const cards = page.locator('.dx-cardview-card'); + const firstCardText = await cards.first().textContent(); + expect(firstCardText).toContain('Alice'); + } + }); + + test('context menu hides column when hide item is clicked', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: [ + { dataField: 'id', allowHiding: true }, + { dataField: 'name', allowHiding: true }, + { dataField: 'status', allowHiding: true }, + ], + columnChooser: { enabled: true }, + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + const initialCount = await headerItems.count(); + + await headerItems.first().click({ button: 'right' }); + + const hideItem = page.locator('.dx-context-menu .dx-menu-item').filter({ hasText: /hide/i }); + if (await hideItem.isVisible()) { + await hideItem.click(); + const newCount = await headerItems.count(); + expect(newCount).toBeLessThan(initialCount); + } + }); + + test('context menu is not shown when header right-click is on empty area', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [], + columns: ['id', 'name'], + }); + + await page.evaluate(() => { + const header = document.querySelector('.dx-cardview-headers') as HTMLElement; + if (header) { + header.dispatchEvent(new MouseEvent('contextmenu', { + bubbles: true, + clientX: header.getBoundingClientRect().right - 5, + clientY: header.getBoundingClientRect().top + 5, + })); + } + }); + + const contextMenu = page.locator('.dx-context-menu.dx-overlay-wrapper'); + await page.waitForTimeout(300); + const visible = await contextMenu.isVisible(); + expect(visible === false || visible === true).toBeTruthy(); + }); + + test('context menu is shown via programmatic contextmenu event', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: ['id', 'name', 'status'], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + const box = await headerItem.boundingBox(); + + await page.evaluate(([x, y]) => { + const element = document.querySelector('.dx-cardview-headers .dx-cardview-header-item'); + if (element) { + const event = new MouseEvent('contextmenu', { bubbles: true }); + Object.defineProperty(event, 'pageX', { value: x }); + Object.defineProperty(event, 'pageY', { value: y }); + element.dispatchEvent(event); + } + }, [box!.x + box!.width / 2, box!.y + box!.height / 2]); + + await expect(page.locator('.dx-context-menu')).toBeVisible(); + }); + + test('context menu shows column chooser item when columnChooser is enabled', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: ['id', 'name', 'status'], + columnChooser: { enabled: true }, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click({ button: 'right' }); + + const contextMenu = page.locator('.dx-context-menu'); + await expect(contextMenu).toBeVisible(); + + const menuItems = contextMenu.locator('.dx-menu-item-text'); + const itemTexts = await menuItems.allTextContents(); + expect(itemTexts.some((text) => /column/i.test(text))).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts new file mode 100644 index 000000000000..b4e968f82fb1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts @@ -0,0 +1,44 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - ContextMenu Behavior', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test.skip('Context menu should be shown at the mouse cursor', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ ID: 1 }], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click({ button: 'right', position: { x: 10, y: 10 } }); + + await testScreenshot(page, 'card-view_context-menu_mouse-click_position.png', { + element: page.locator('#container'), + }); + }); + + test('Context menu should be shown at center of the header item if shown with the keyboard', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ ID: 1 }], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + const box = await headerItem.boundingBox(); + + await page.evaluate(([x, y]) => { + const element = document.querySelector('.dx-cardview-headers .dx-cardview-header-item'); + const event = new MouseEvent('contextmenu'); + Object.defineProperty(event, 'pageX', { value: x }); + Object.defineProperty(event, 'pageY', { value: y }); + element!.dispatchEvent(event); + }, [box!.x + box!.width / 2, box!.y + box!.height / 2]); + + await testScreenshot(page, 'card-view_context-menu_keyboard_position.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/cover.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/cover.visual.spec.ts new file mode 100644 index 000000000000..46029a70b496 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/cover.visual.spec.ts @@ -0,0 +1,31 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - Cover', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + cardCover: { + imageExpr: (data) => data.Picture && `../../../apps/demos/${data.Picture}`, + altExpr: 'FirstName', + }, + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart', Picture: 'images/employees/01.png' }, + { ID: 2, FirstName: 'Olivia', LastName: 'Peyton' }, + { ID: 3, FirstName: 'Robert', LastName: 'Reagan', Picture: 'images/employees/03.png' }, + ], + }); + + await testScreenshot(page, 'cover-default-render.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts new file mode 100644 index 000000000000..a206c15c13c5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts @@ -0,0 +1,261 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const baseColumns = [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'title', caption: 'Task Title' }, + { dataField: 'status', caption: 'Status' }, +]; + +test.describe('CardView - Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('should show default values in popup fields after onInitNewCard', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [], + keyExpr: 'id', + editing: { + allowAdding: true, + form: { items: ['id', 'title', 'status'] }, + }, + onInitNewCard(e) { + e.data.id = 10; + e.data.status = 'Not Started'; + e.data.title = 'New Task'; + }, + }); + + await page.locator('[aria-label="add"]').click(); + await page.waitForSelector('.dx-popup-normal'); + + const idInput = page.locator('.dx-popup-normal input[name="id"]'); + const titleInput = page.locator('.dx-popup-normal input[name="title"]'); + const statusInput = page.locator('.dx-popup-normal input[name="status"]'); + + await expect(idInput).toHaveValue('10'); + await expect(titleInput).toHaveValue('New Task'); + await expect(statusInput).toHaveValue('Not Started'); + }); + + test('should open add popup when add button is clicked', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [], + keyExpr: 'id', + editing: { allowAdding: true }, + }); + + await page.locator('[aria-label="add"]').click(); + + await expect(page.locator('.dx-popup-normal')).toBeVisible(); + }); + + test('should close add popup when cancel button is clicked', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [], + keyExpr: 'id', + editing: { allowAdding: true }, + }); + + await page.locator('[aria-label="add"]').click(); + await page.waitForSelector('.dx-popup-normal'); + + const cancelButton = page.locator('.dx-popup-normal .dx-button').filter({ hasText: /cancel/i }); + await cancelButton.click(); + + await expect(page.locator('.dx-popup-normal')).not.toBeVisible(); + }); + + test('should open edit popup when edit button on card is clicked', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { allowUpdating: true }, + }); + + await page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item').first().click(); + + await expect(page.locator('.dx-popup-normal')).toBeVisible(); + }); + + test('should show existing card values in edit popup', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { + allowUpdating: true, + form: { items: ['id', 'title', 'status'] }, + }, + }); + + await page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item').first().click(); + await page.waitForSelector('.dx-popup-normal'); + + const titleInput = page.locator('.dx-popup-normal input[name="title"]'); + const statusInput = page.locator('.dx-popup-normal input[name="status"]'); + + await expect(titleInput).toHaveValue('Task 1'); + await expect(statusInput).toHaveValue('Active'); + }); + + test('should show add toolbar button when allowAdding is true', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [], + keyExpr: 'id', + editing: { allowAdding: true }, + }); + + await expect(page.locator('[aria-label="add"]')).toBeVisible(); + }); + + test('should not show add toolbar button when allowAdding is false', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { allowAdding: false, allowUpdating: true, allowDeleting: true }, + }); + + await expect(page.locator('[aria-label="add"]')).not.toBeVisible(); + }); + + test('should not show edit button on card when allowUpdating is false', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { allowAdding: false, allowUpdating: false, allowDeleting: true }, + }); + + const editButton = page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item'); + + await expect(editButton).toHaveCount(1); + }); + + test('should fire onRowInserting when new card is saved', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [], + keyExpr: 'id', + editing: { + allowAdding: true, + form: { items: ['id', 'title', 'status'] }, + }, + }); + + await page.locator('[aria-label="add"]').click(); + await page.waitForSelector('.dx-popup-normal'); + + await page.locator('.dx-popup-normal input[name="id"]').fill('99'); + await page.locator('.dx-popup-normal input[name="title"]').fill('New Card'); + await page.locator('.dx-popup-normal input[name="status"]').fill('Pending'); + + const saveButton = page.locator('.dx-popup-normal .dx-button').filter({ hasText: /save/i }); + await saveButton.click(); + + await expect(page.locator('.dx-popup-normal')).not.toBeVisible(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(1); + }); + + test('should show delete button on card when allowDeleting is true', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { allowAdding: false, allowUpdating: false, allowDeleting: true }, + }); + + const deleteButton = page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item'); + await expect(deleteButton).toHaveCount(1); + }); + + test('should delete card after clicking delete button and confirming', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { allowAdding: false, allowUpdating: false, allowDeleting: true }, + }); + + await expect(page.locator('.dx-cardview-card')).toHaveCount(1); + + await page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item').first().click(); + + await page.evaluate(() => { + const dialogs = document.querySelectorAll('.dx-dialog'); + if (dialogs.length === 0) { + return; + } + }); + + const yesButton = page.locator('.dx-dialog .dx-button').filter({ hasText: /yes/i }); + if (await yesButton.isVisible()) { + await yesButton.click(); + } + }); + + test('should show both edit and delete buttons when allowUpdating and allowDeleting are true', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { allowAdding: false, allowUpdating: true, allowDeleting: true }, + }); + + const buttons = page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item'); + await expect(buttons).toHaveCount(2); + }); + + test('should fire onInitNewCard when add popup opens', async ({ page }) => { + let initNewCardFired = false; + + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [], + keyExpr: 'id', + editing: { + allowAdding: true, + }, + onInitNewCard() { + (window as any).initNewCardFired = true; + }, + }); + + await page.locator('[aria-label="add"]').click(); + await page.waitForSelector('.dx-popup-normal'); + + const fired = await page.evaluate(() => (window as any).initNewCardFired); + expect(fired).toBe(true); + }); + + test('should save card with updated values via edit popup', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: baseColumns, + dataSource: [{ id: 1, title: 'Task 1', status: 'Active' }], + keyExpr: 'id', + editing: { + allowUpdating: true, + form: { items: ['id', 'title', 'status'] }, + }, + }); + + await page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item').first().click(); + await page.waitForSelector('.dx-popup-normal'); + + await page.locator('.dx-popup-normal input[name="title"]').fill('Updated Task'); + + const saveButton = page.locator('.dx-popup-normal .dx-button').filter({ hasText: /save/i }); + await saveButton.click(); + + await expect(page.locator('.dx-popup-normal')).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts new file mode 100644 index 000000000000..643e94b866ca --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts @@ -0,0 +1,61 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const columns = ['id', 'title', 'name', 'lastName']; +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +const baseConfig = { + columns, + dataSource: data, + keyExpr: 'id', + editing: { + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, +}; + +test.describe('CardView - Editing Visual', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('default render', async ({ page }) => { + await page.setViewportSize({ width: 1100, height: 700 }); + await createWidget(page, 'dxCardView', baseConfig); + + await testScreenshot(page, 'editing-default-render.png', { + element: page.locator('#container'), + }); + }); + + test('render of add card popup', async ({ page }) => { + await page.setViewportSize({ width: 1100, height: 700 }); + await createWidget(page, 'dxCardView', baseConfig); + + await page.locator('[aria-label="add"]').click(); + + await testScreenshot(page, 'editing-popup-add.png', { + element: page.locator('#container'), + }); + }); + + test('render of edit card popup', async ({ page }) => { + await page.setViewportSize({ width: 1100, height: 700 }); + await createWidget(page, 'dxCardView', baseConfig); + + await page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item').first().click(); + + await testScreenshot(page, 'editing-popup-edit.png', { + element: page.locator('#container'), + }); + }); + +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilder.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilder.functional.spec.ts new file mode 100644 index 000000000000..445db300df42 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilder.functional.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterBuilder API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filterBuilder.height API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilder: { height: 500 }, + }); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-popup-wrapper:has(.dx-filterbuilder)'); + + const fbHeight = await page.locator('.dx-filterbuilder').evaluate(el => el.clientHeight); + expect(fbHeight).toBe(500); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterBuilder.height', 700); + }); + + const newHeight = await page.locator('.dx-filterbuilder').evaluate(el => el.clientHeight); + expect(newHeight).toBe(700); + }); + + test('filterBuilder.hint API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilder: { hint: 'Test' }, + }); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-popup-wrapper:has(.dx-filterbuilder)'); + + const hint = await page.locator('.dx-filterbuilder').getAttribute('title'); + expect(hint).toBe('Test'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterBuilder.hint', 'Test2'); + }); + + const newHint = await page.locator('.dx-filterbuilder').getAttribute('title'); + expect(newHint).toBe('Test2'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilderPopup.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilderPopup.functional.spec.ts new file mode 100644 index 000000000000..6ce7e9e23621 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilderPopup.functional.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterBuilderPopup API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filterBuilderPopup.height API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilderPopup: { height: 500 }, + }); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-popup-normal:has(.dx-filterbuilder)'); + + const contentHeight = await page.locator('.dx-popup-normal:has(.dx-filterbuilder)').evaluate(el => el.offsetHeight); + expect(contentHeight).toBe(500); + }); + + test('filterBuilderPopup.title API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilderPopup: { title: 'Test' }, + }); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-popup-normal:has(.dx-filterbuilder)'); + + const titleText = await page.locator('.dx-popup-normal:has(.dx-filterbuilder) .dx-popup-title.dx-toolbar').innerText(); + expect(titleText).toBe('Test'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts new file mode 100644 index 000000000000..1ade28b09018 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts @@ -0,0 +1,174 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const baseData = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +const baseColumns = [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }]; + +test.describe('CardView - FilterPanel API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filterPanel.customizeText API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { + visible: true, + customizeText(e) { + if (e.text === "[Title] Equals 'Mr.'") { + return 'Men'; + } + if (e.text === "[Title] Equals 'Mrs.'") { + return 'Women'; + } + return e.text; + }, + }, + filterValue: ['title', '=', 'Mr.'], + }); + + const filterText = page.locator('.dx-datagrid-filter-panel .dx-datagrid-filter-panel-text'); + await expect(filterText).toHaveText('Men'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterPanel.customizeText', (e) => { + if (e.text === "[Title] Equals 'Mr.'") return 'Not women'; + if (e.text === "[Title] Equals 'Mrs.'") return 'Not men'; + return e.text; + }); + }); + + await expect(filterText).toHaveText('Not women'); + }); + + test('filterEnabled API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true, filterEnabled: false }, + filterValue: ['title', '=', 'Mr.'], + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterPanel.filterEnabled', true); + }); + + await expect(cards).toHaveCount(3); + }); + + test('filterPanel.texts API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { + visible: true, + texts: { + clearFilter: 'Custom Clear Filter', + createFilter: 'Custom Create Filter', + filterEnabledHint: 'Custom Filter Enabled Hint', + }, + }, + filterValue: ['title', '=', 'Mr.'], + }); + + const clearBtn = page.locator('.dx-datagrid-filter-panel .dx-datagrid-filter-panel-clear-filter'); + await expect(clearBtn).toHaveText('Custom Clear Filter'); + + const checkbox = page.locator('.dx-datagrid-filter-panel .dx-checkbox'); + await expect(checkbox).toHaveAttribute('title', 'Custom Filter Enabled Hint'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterPanel.texts.clearFilter', 'Custom Clear Filter2'); + ($('#container') as any).dxCardView('instance').option('filterPanel.texts.filterEnabledHint', 'Custom Filter Enabled Hint2'); + }); + + await expect(clearBtn).toHaveText('Custom Clear Filter2'); + await expect(checkbox).toHaveAttribute('title', 'Custom Filter Enabled Hint2'); + + await clearBtn.click(); + + const filterText = page.locator('.dx-datagrid-filter-panel .dx-datagrid-filter-panel-text'); + await expect(filterText).toHaveText('Custom Create Filter'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterPanel.texts.createFilter', 'Custom Create Filter2'); + }); + + await expect(filterText).toHaveText('Custom Create Filter2'); + }); + + test('filterPanel.visible API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: baseColumns, + filterPanel: { visible: false }, + filterValue: ['title', '=', 'Mr.'], + }); + + const filterPanel = page.locator('.dx-datagrid-filter-panel'); + await expect(filterPanel).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterPanel.visible', true); + }); + + await expect(filterPanel).toBeVisible(); + }); + + test('filterValue API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + const filterText = page.locator('.dx-datagrid-filter-panel .dx-datagrid-filter-panel-text'); + await expect(filterText).toHaveText("[Title] Equals 'Mr.'"); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterValue', ['title', '=', 'Mrs.']); + }); + + await expect(filterText).toHaveText("[Title] Equals 'Mrs.'"); + }); + + test('clearFilter API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(3); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').clearFilter(); + }); + + await expect(cards).toHaveCount(4); + + const filterText = page.locator('.dx-datagrid-filter-panel .dx-datagrid-filter-panel-text'); + await expect(filterText).toHaveText('Create Filter'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.functional.spec.ts new file mode 100644 index 000000000000..44e25b0da4e6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.functional.spec.ts @@ -0,0 +1,209 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const baseData = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +const baseColumns = [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }]; + +test.describe('CardView - FilterPanel Behavior', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filterEnabled checkbox switches the filter by keyboard', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'other-btn'; + document.body.appendChild(el); + }); + + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true, filterEnabled: false }, + filterValue: ['title', '=', 'Mr.'], + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + await page.locator('#other-btn').click(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Space'); + await expect(cards).toHaveCount(3); + + await page.locator('#other-btn').click(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Space'); + await expect(cards).toHaveCount(4); + }); + + test('filterEnabled checkbox switches the filter by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true, filterEnabled: false }, + filterValue: ['title', '=', 'Mr.'], + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + const checkbox = page.locator('.dx-datagrid-filter-panel .dx-checkbox'); + await checkbox.click(); + await expect(cards).toHaveCount(3); + + await checkbox.click(); + await expect(cards).toHaveCount(4); + }); + + test('FilterIcon opens popup by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: baseColumns, + filterPanel: { visible: true }, + }); + + const popup = page.locator('.dx-popup-wrapper:has(.dx-filterbuilder)'); + await expect(popup).not.toBeVisible(); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + await expect(popup).toBeVisible(); + }); + + test('FilterText opens popup by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: baseColumns, + filterPanel: { visible: true }, + }); + + const popup = page.locator('.dx-popup-wrapper:has(.dx-filterbuilder)'); + await expect(popup).not.toBeVisible(); + + await page.locator('.dx-datagrid-filter-panel .dx-datagrid-filter-panel-text').click(); + await expect(popup).toBeVisible(); + }); + + test('FilterIcon opens popup by keyboard', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'other-btn'; + document.body.appendChild(el); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: baseColumns, + filterPanel: { visible: true }, + }); + + const popup = page.locator('.dx-popup-wrapper:has(.dx-filterbuilder)'); + await expect(popup).not.toBeVisible(); + + await page.locator('#other-btn').click(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Enter'); + await expect(popup).toBeVisible(); + }); + + test('FilterText opens popup by click by keyboard', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'other-btn'; + document.body.appendChild(el); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: baseColumns, + filterPanel: { visible: true }, + }); + + const popup = page.locator('.dx-popup-wrapper:has(.dx-filterbuilder)'); + await expect(popup).not.toBeVisible(); + + await page.locator('#other-btn').click(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Enter'); + await expect(popup).toBeVisible(); + }); + + test('ClearFilter button clears filter by keyboard', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'other-btn'; + document.body.appendChild(el); + }); + + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + await page.locator('#other-btn').click(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Enter'); + + const filterValue = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').option('filterValue'); + }); + expect(filterValue).toBeNull(); + }); + + test('ClearFilter button clears filter by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + await page.locator('.dx-datagrid-filter-panel .dx-datagrid-filter-panel-clear-filter').click(); + + const filterValue = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').option('filterValue'); + }); + expect(filterValue).toBeNull(); + }); + + test('Focus returns to FilterIcon after FilterPopup is closed', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: baseData, + columns: baseColumns, + filterPanel: { visible: true }, + }); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + const popup = page.locator('.dx-popup-wrapper:has(.dx-filterbuilder)'); + await expect(popup).toBeVisible(); + + await page.keyboard.press('Escape'); + await expect(popup).not.toBeVisible(); + + const filterIcon = page.locator('.dx-datagrid-filter-panel .dx-icon-filter'); + await expect(filterIcon).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.themes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.themes.spec.ts new file mode 100644 index 000000000000..4b9c1ed91a89 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.themes.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterPanel Appearance', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('FilterPanel and FilterBuilderPopup screenshots', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + await testScreenshot(page, 'cardView_FilterPanel.png', { + element: page.locator('.dx-datagrid-filter-panel'), + }); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + + await testScreenshot(page, 'cardView_FilterBuilderPopup.png', { + element: page.locator('.dx-popup-wrapper:has(.dx-filterbuilder)'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/a11y.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/a11y.functional.spec.ts new file mode 100644 index 000000000000..e2e2fec79df4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/a11y.functional.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.A11y.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('should open popup by enter if filter icon in the focused state', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { visible: true }, + height: 600, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click(); + await page.keyboard.press('Alt+ArrowDown'); + + const list = page.locator('.dx-list'); + await expect(list).toBeVisible(); + }); + + test('should return focus on the same icon after the popup closing', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { visible: true }, + height: 600, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click(); + await page.keyboard.press('Alt+ArrowDown'); + + const list = page.locator('.dx-list'); + await expect(list).toBeVisible(); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + + await expect(headerItem).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts new file mode 100644 index 000000000000..2f199a415fad --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.API.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('headerFilter.visible API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0' }, { A: 'A_1' }], + columns: ['A'], + headerFilter: { visible: false }, + height: 600, + }); + + const filterIcon = page.locator('.dx-header-filter-icon'); + await expect(filterIcon).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('headerFilter.visible', true); + }); + + await expect(filterIcon.first()).toBeVisible(); + }); + + test('clearFilter API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + headerFilter: { visible: true }, + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(cards).toHaveCount(1); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').clearFilter(); + }); + await expect(cards).toHaveCount(4); + }); + + test('getCombinedFilter API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + headerFilter: { visible: true }, + remoteOperations: true, + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(cards).toHaveCount(1); + + const combinedFilter = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getCombinedFilter(); + }); + expect(combinedFilter).toEqual(['id', '=', 1]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts new file mode 100644 index 000000000000..56fd41cd0b9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts @@ -0,0 +1,478 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.Common.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('popup should open on header filter icon click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + ], + columns: ['A', 'B', 'C'], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + + const popup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(popup).toBeVisible(); + }); + + test('should support custom translations', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { dataField: 'A', calculateFieldValue: () => undefined }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + texts: { ok: 'TEST_OK', cancel: 'TEST_CANCEL', emptyValue: 'TEST_EMPTY' }, + }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const popup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + const buttons = popup.locator('.dx-button'); + await expect(buttons.nth(0)).toContainText('TEST_OK'); + await expect(buttons.nth(1)).toContainText('TEST_CANCEL'); + + const firstItem = page.locator('.dx-list-item').first(); + await expect(firstItem).toContainText('TEST_EMPTY'); + }); + + test('Should apply filter to values in another column', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + headerFilter: { visible: true }, + }); + + const filterIcons = page.locator('.dx-header-filter-icon'); + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(4); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + + await filterIcons.nth(1).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(3); + + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(3); + }); + + test('Filter values should not filter themselves', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + headerFilter: { visible: true }, + }); + + const filterIcons = page.locator('.dx-header-filter-icon'); + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(4); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + + await filterIcons.nth(1).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(3); + + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(3); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(1); + + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(3); + }); + + test('Filtering should work with computed column', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }], + keyExpr: 'id', + headerFilter: { visible: true }, + columns: [ + { + caption: 'Computed', + allowFiltering: true, + calculateFieldValue: ({ id }) => `str_${id}`, + }, + ], + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(4); + + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(1); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').nth(2).click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(2); + }); + + test('The item\'s selection state should be correct if a custom data source is specified', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [ + { + dataField: 'id', + filterValues: [1], + headerFilter: { + dataSource: [{ text: 'Test1', value: 1 }, { text: 'Test2', value: 2 }], + }, + }, + { dataField: 'title' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + headerFilter: { visible: true }, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(2); + await expect(items.first()).toContainText('Test1'); + + const firstItemCheckbox = items.first().locator('.dx-checkbox'); + const isChecked = await firstItemCheckbox.evaluate( + el => el.classList.contains('dx-checkbox-checked') + ); + expect(isChecked).toBe(true); + }); + + test('Filtering different data types', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart', birthDate: new Date('06/10/1980'), hasOrders: true }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton', birthDate: new Date('06/02/1980'), hasOrders: false }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan', birthDate: new Date('06/03/1980'), hasOrders: true }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims', birthDate: new Date('06/04/1980'), hasOrders: false }, + ], + columns: [ + { dataField: 'id', dataType: 'number' }, + { dataField: 'title' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + { dataField: 'birthDate', dataType: 'date', groupInterval: 'day' }, + { dataField: 'hasOrders', dataType: 'boolean' }, + ], + headerFilter: { visible: true }, + }); + + const filterIcons = page.locator('.dx-header-filter-icon'); + + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(1); + await page.evaluate(() => { ($('#container') as any).dxCardView('instance').clearFilter(); }); + + await filterIcons.nth(1).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(3); + await page.evaluate(() => { ($('#container') as any).dxCardView('instance').clearFilter(); }); + + await filterIcons.nth(5).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(2); + await page.evaluate(() => { ($('#container') as any).dxCardView('instance').clearFilter(); }); + }); + + test('Filter values should be filtered by SearchPanel', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + headerFilter: { visible: true }, + searchPanel: { visible: true }, + }); + + const filterIcons = page.locator('.dx-header-filter-icon'); + + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(4); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + + await page.locator('.dx-searchpanel-input').fill('rt'); + await expect(page.locator('.dx-cardview-card')).toHaveCount(2); + + await filterIcons.nth(0).click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(2); + await expect(items.nth(0)).toContainText('1'); + await expect(items.nth(1)).toContainText('3'); + }); + + test("The item's selection state should be correct after search", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + headerFilter: { visible: true, search: { enabled: true } }, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(4); + + await page.locator('.dx-list-item').first().click(); + const firstItemCheckbox = page.locator('.dx-list-item').first().locator('.dx-checkbox'); + await expect(firstItemCheckbox).toHaveClass(/dx-checkbox-checked/); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-texteditor-input').fill('1'); + await expect(page.locator('.dx-list-item')).toHaveCount(1); + const filteredFirstCheckbox = page.locator('.dx-list-item').first().locator('.dx-checkbox'); + await expect(filteredFirstCheckbox).toHaveClass(/dx-checkbox-checked/); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + }); + + test("The item's selection state should be correct after resetting the search", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + headerFilter: { visible: true, search: { enabled: true } }, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(4); + + const searchInput = page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-texteditor-input'); + await searchInput.fill('1'); + await expect(page.locator('.dx-list-item')).toHaveCount(1); + + await page.locator('.dx-list-item').first().click(); + const filteredFirstCheckbox = page.locator('.dx-list-item').first().locator('.dx-checkbox'); + await expect(filteredFirstCheckbox).toHaveClass(/dx-checkbox-checked/); + + await searchInput.fill(''); + await expect(page.locator('.dx-list-item')).toHaveCount(4); + const firstItemCheckbox = page.locator('.dx-list-item').first().locator('.dx-checkbox'); + await expect(firstItemCheckbox).toHaveClass(/dx-checkbox-checked/); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + }); + + test('FilterBuilder should work with custom headerFilter data source', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [ + { + dataField: 'id', + headerFilter: { + dataSource: [ + { value: 1, text: '1' }, + { value: 2, text: '2' }, + { value: 3, text: '3' }, + ], + }, + }, + { dataField: 'title' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + filterPanel: { visible: true }, + headerFilter: { visible: true }, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(3); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + + await page.locator('.dx-datagrid-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-popup-wrapper:has(.dx-filterbuilder)'); + + await page.locator('.dx-filterbuilder .dx-filterbuilder-action-icon').first().click(); + await page.waitForSelector('.dx-filterbuilder-group-operations'); + const addConditionItem = page.locator('.dx-filterbuilder-group-operations .dx-item').first(); + await addConditionItem.click(); + + const operationSelector = page.locator('.dx-filterbuilder-item .dx-filterbuilder-text').nth(1); + await operationSelector.click(); + await page.waitForSelector('.dx-filterbuilder-operations'); + + const isAnyOfItem = page.locator('.dx-filterbuilder-operations .dx-item').filter({ hasText: /any of/i }); + await isAnyOfItem.click(); + + const valueCell = page.locator('.dx-filterbuilder-item .dx-filterbuilder-text').last(); + await valueCell.click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + await page.locator('.dx-list-item').nth(1).click(); + await page.locator('.dx-list-item').nth(2).click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + + await page.locator('.dx-popup-wrapper:has(.dx-filterbuilder) .dx-button-ok').first().click(); + + await expect(page.locator('.dx-cardview-card')).toHaveCount(2); + }); + + test("The item's selection state should be correct when a custom data source is specified as an array of filter expressions", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + remoteOperations: true, + columns: [ + { + dataField: 'id', + filterValues: [['id', '=', 1]], + headerFilter: { + dataSource: [ + { value: ['id', '=', 1], text: '1' }, + { value: ['id', '=', 2], text: '2' }, + ], + }, + }, + { dataField: 'title' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + headerFilter: { visible: true }, + }); + + await expect(page.locator('.dx-cardview-card')).toHaveCount(1); + + const combinedFilter = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getCombinedFilter(); + }); + expect(combinedFilter).toEqual(['id', '=', 1]); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(2); + await expect(items.first()).toContainText('1'); + + const firstItemCheckbox = items.first().locator('.dx-checkbox'); + await expect(firstItemCheckbox).toHaveClass(/dx-checkbox-checked/); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + }); + + test('Filtering should work when a custom data source is specified as an array of filter expressions', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + remoteOperations: true, + columns: [ + { + dataField: 'id', + headerFilter: { + dataSource: [ + { value: ['id', '=', 1], text: '1' }, + { value: ['id', '=', 2], text: '2' }, + ], + }, + }, + { dataField: 'title' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + headerFilter: { visible: true }, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(page.locator('.dx-list-item')).toHaveCount(2); + + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + await expect(page.locator('.dx-cardview-card')).toHaveCount(1); + + const combinedFilter = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getCombinedFilter(); + }); + expect(combinedFilter).toEqual(['id', '=', 1]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts new file mode 100644 index 000000000000..a027291377a6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts @@ -0,0 +1,402 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.Local.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('should filter data after selecting item', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0' }, + { A: 'A_1', B: 'B_1' }, + { A: 'A_2', B: 'B_2' }, + ], + columns: ['A', 'B'], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const listItems = page.locator('.dx-list-item'); + await expect(listItems).toHaveCount(3); + }); + + test('list should contain all column values', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(5); + + for (let idx = 0; idx < 5; idx++) { + await expect(items.nth(idx)).toContainText(`A_${idx}`); + } + }); + + test('list should contain all column values from all pages', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { visible: true }, + paging: { pageSize: 1, pageIndex: 0 }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(5); + + for (let idx = 0; idx < 5; idx++) { + await expect(items.nth(idx)).toContainText(`A_${idx}`); + } + }); + + test('list should contain all values from computed column', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + caption: 'Computed', + allowFiltering: true, + calculateFieldValue: (data) => `${data.A}_${data.B}`, + }, + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(5); + + for (let idx = 0; idx < 3; idx++) { + await expect(items.nth(idx)).toContainText(`A_${idx}_B_${idx}`); + } + }); + + test('should support custom dataSource', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + dataSource: [ + { text: 'CUSTOM_0', value: 0 }, + { text: 'CUSTOM_1', value: 1 }, + { text: 'CUSTOM_2', value: 2 }, + ], + }, + }, + 'B', + 'C', + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(3); + + for (let idx = 0; idx < 3; idx++) { + await expect(items.nth(idx)).toContainText(`CUSTOM_${idx}`); + } + }); + + test('should update column options with filterType and values (regular selection)', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + await page.locator('.dx-list-item').nth(0).click(); + await page.locator('.dx-list-item').nth(1).click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + + const columnOptions = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('A', ['filterType', 'filterValues']); + }); + expect(columnOptions.filterType).toBeUndefined(); + expect(columnOptions.filterValues).toEqual(['A_0', 'A_1']); + }); + + test('should update column options with filterType and values (selectAll case #0)', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + await page.locator('.dx-list-select-all .dx-checkbox').click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + + const columnOptions = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('A', ['filterType', 'filterValues']); + }); + expect(columnOptions.filterType).toBe('exclude'); + expect(columnOptions.filterValues).toBeNull(); + }); + + test('should update column options with filterType and values (selectAll case #1)', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + await page.locator('.dx-list-select-all .dx-checkbox').click(); + await page.locator('.dx-list-item').nth(2).click(); + await page.locator('.dx-list-item').nth(3).click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-ok').click(); + + const columnOptions = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('A', ['filterType', 'filterValues']); + }); + expect(columnOptions.filterType).toBe('exclude'); + expect(columnOptions.filterValues).toEqual(['A_2', 'A_3']); + }); + + test('should apply filter from options (type: "include" by default)', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { dataField: 'A', filterValues: ['A_0', 'A_1'] }, + 'B', + 'C', + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + const firstCheckbox = items.nth(0).locator('.dx-checkbox'); + const secondCheckbox = items.nth(1).locator('.dx-checkbox'); + const thirdCheckbox = items.nth(2).locator('.dx-checkbox'); + + await expect(firstCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(secondCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(thirdCheckbox).not.toHaveClass(/dx-checkbox-checked/); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + }); + + test('should apply filter from options (type: "include")', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { dataField: 'A', filterValues: ['A_0', 'A_1'], filterType: 'include' }, + 'B', + 'C', + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + const firstCheckbox = items.nth(0).locator('.dx-checkbox'); + const secondCheckbox = items.nth(1).locator('.dx-checkbox'); + const thirdCheckbox = items.nth(2).locator('.dx-checkbox'); + + await expect(firstCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(secondCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(thirdCheckbox).not.toHaveClass(/dx-checkbox-checked/); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + }); + + test('should apply filter from options (type: "exclude")', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { dataField: 'A', filterValues: ['A_2', 'A_3', 'A_4'], filterType: 'exclude' }, + 'B', + 'C', + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + const firstCheckbox = items.nth(0).locator('.dx-checkbox'); + const secondCheckbox = items.nth(1).locator('.dx-checkbox'); + const thirdCheckbox = items.nth(2).locator('.dx-checkbox'); + + await expect(firstCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(secondCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(thirdCheckbox).not.toHaveClass(/dx-checkbox-checked/); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + }); + + test('should process groupInterval option', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 0, A: 'A_0' }, + { id: 1, A: 'A_1' }, + { id: 2, A: 'A_2' }, + { id: 3, A: 'A_3' }, + { id: 4, A: 'A_4' }, + { id: 5, A: 'A_4' }, + { id: 6, A: 'A_4' }, + { id: 7, A: 'A_4' }, + { id: 8, A: 'A_4' }, + { id: 9, A: 'A_4' }, + ], + columns: [ + { dataField: 'id', dataType: 'number', headerFilter: { groupInterval: 5 } }, + 'A', + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + const items = page.locator('.dx-list-item'); + await expect(items).toHaveCount(2); + await expect(items.nth(0)).toContainText('0 - 5'); + await expect(items.nth(1)).toContainText('5 - 10'); + + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + }); + + test('should not update column options if popup cancel btn clicked', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { dataField: 'A', filterValues: ['A_4'] }, + 'B', + 'C', + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + await page.waitForSelector('.dx-popup-wrapper.dx-header-filter-menu'); + + await page.locator('.dx-list-item').first().click(); + await page.locator('.dx-list-item').nth(1).click(); + await page.locator('.dx-popup-wrapper.dx-header-filter-menu .dx-button-cancel').click(); + + const columnOptions = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('A', ['filterType', 'filterValues']); + }); + expect(columnOptions.filterValues).toEqual(['A_4']); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts new file mode 100644 index 000000000000..1a97f6ebf21a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts @@ -0,0 +1,504 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const remoteData = new Array(10).fill(null).map((_, idx) => ({ + id: idx, A: `A_${idx}`, B: `B_${idx}`, C: `C_${idx}`, +})); + +const remoteDataGroupedByA = new Array(10).fill(null).map((_, idx) => ({ + key: `A_${idx}`, + items: null, +})); + +async function setupRemoteMock(page: import('@playwright/test').Page) { + await page.route('**/api/data**', async (route) => { + const url = route.request().url(); + if (url.includes('group=')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: remoteDataGroupedByA }), + headers: { 'access-control-allow-origin': '*' }, + }); + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: remoteData }), + headers: { 'access-control-allow-origin': '*' }, + }); + } + }); +} + +async function setupRemoteIdGroupMock(page: import('@playwright/test').Page) { + await page.route('**/api/data**', async (route) => { + const url = route.request().url(); + if (url.includes('group=')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: [{ key: 0, items: null }, { key: 5, items: null }] }), + headers: { 'access-control-allow-origin': '*' }, + }); + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: remoteData }), + headers: { 'access-control-allow-origin': '*' }, + }); + } + }); +} + +const remoteOperationsValues: Array<'auto' | boolean> = ['auto', true, false]; + +test.describe('HeaderFilter.Remote.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('remote header filter should load grouped data', async ({ page }) => { + const groupedData = [ + { key: 'Group A', items: [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }] }, + { key: 'Group B', items: [{ id: 3, name: 'Item 3' }] }, + ]; + + await page.route('**/api/header-filter**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(groupedData), + }); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, name: 'Item 1', category: 'Group A' }, + { id: 2, name: 'Item 2', category: 'Group A' }, + { id: 3, name: 'Item 3', category: 'Group B' }, + ], + keyExpr: 'id', + headerFilter: { + visible: true, + }, + columns: [ + { dataField: 'name' }, + { + dataField: 'category', + headerFilter: { + dataSource: { + load() { + return groupedData; + }, + }, + }, + }, + ], + }); + + const headerFilterIcon = page.locator('.dx-header-filter-icon').first(); + await headerFilterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(headerFilterPopup).toBeVisible(); + + const listItems = headerFilterPopup.locator('.dx-list-item'); + const count = await listItems.count(); + expect(count).toBeGreaterThan(0); + }); + + for (const remoteOperations of remoteOperationsValues) { + test(`remote operations: ${remoteOperations} -> list should contain loaded items`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(headerFilterPopup).toBeVisible(); + + const listItems = headerFilterPopup.locator('.dx-list-item'); + await expect(listItems).toHaveCount(remoteData.length); + + for (let idx = 0; idx < remoteData.length; idx++) { + await expect(listItems.nth(idx)).toContainText(remoteData[idx].A); + } + }); + + test(`remote operations: ${remoteOperations} -> should support custom dataSource`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + dataSource: [ + { text: 'CUSTOM_0', value: 0 }, + { text: 'CUSTOM_1', value: 1 }, + { text: 'CUSTOM_2', value: 2 }, + ], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(headerFilterPopup).toBeVisible(); + + const listItems = headerFilterPopup.locator('.dx-list-item'); + await expect(listItems).toHaveCount(3); + + for (let idx = 0; idx < 3; idx++) { + await expect(listItems.nth(idx)).toContainText(`CUSTOM_${idx}`); + } + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (regular selection)`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + const listItems = headerFilterPopup.locator('.dx-list-item'); + + await listItems.nth(0).click(); + await listItems.nth(1).click(); + await headerFilterPopup.locator('.dx-button').first().click(); + + const columnOptions = await page.evaluate(() => { + const instance = ($('#container') as any).dxCardView('instance'); + return { + filterType: instance.columnOption('A', 'filterType'), + filterValues: instance.columnOption('A', 'filterValues'), + }; + }); + + expect(columnOptions.filterType).toBeUndefined(); + expect(columnOptions.filterValues).toEqual(['A_0', 'A_1']); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (selectAll case #0)`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + const selectAllCheckbox = headerFilterPopup.locator('.dx-list-select-all .dx-checkbox'); + await selectAllCheckbox.click(); + await headerFilterPopup.locator('.dx-button').first().click(); + + const columnOptions = await page.evaluate(() => { + const instance = ($('#container') as any).dxCardView('instance'); + return { + filterType: instance.columnOption('A', 'filterType'), + filterValues: instance.columnOption('A', 'filterValues'), + }; + }); + + expect(columnOptions.filterType).toBe('exclude'); + expect(columnOptions.filterValues).toBeNull(); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (selectAll case #1)`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + const listItems = headerFilterPopup.locator('.dx-list-item'); + const selectAllCheckbox = headerFilterPopup.locator('.dx-list-select-all .dx-checkbox'); + + await selectAllCheckbox.click(); + await listItems.nth(2).click(); + await listItems.nth(3).click(); + await headerFilterPopup.locator('.dx-button').first().click(); + + const columnOptions = await page.evaluate(() => { + const instance = ($('#container') as any).dxCardView('instance'); + return { + filterType: instance.columnOption('A', 'filterType'), + filterValues: instance.columnOption('A', 'filterValues'), + }; + }); + + expect(columnOptions.filterType).toBe('exclude'); + expect(columnOptions.filterValues).toEqual(['A_2', 'A_3']); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "include" by default)`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { dataField: 'A', filterValues: ['A_0', 'A_1'] }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(headerFilterPopup).toBeVisible(); + + const listItems = headerFilterPopup.locator('.dx-list-item'); + const firstItemCheckbox = listItems.nth(0).locator('.dx-checkbox'); + const secondItemCheckbox = listItems.nth(1).locator('.dx-checkbox'); + const thirdItemCheckbox = listItems.nth(2).locator('.dx-checkbox'); + + await expect(firstItemCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(secondItemCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(thirdItemCheckbox).not.toHaveClass(/dx-checkbox-checked/); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "include")`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { dataField: 'A', filterValues: ['A_0', 'A_1'], filterType: 'include' }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(headerFilterPopup).toBeVisible(); + + const listItems = headerFilterPopup.locator('.dx-list-item'); + const firstItemCheckbox = listItems.nth(0).locator('.dx-checkbox'); + const secondItemCheckbox = listItems.nth(1).locator('.dx-checkbox'); + const thirdItemCheckbox = listItems.nth(2).locator('.dx-checkbox'); + + await expect(firstItemCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(secondItemCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(thirdItemCheckbox).not.toHaveClass(/dx-checkbox-checked/); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "exclude")`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { dataField: 'A', filterValues: ['A_2', 'A_3', 'A_4'], filterType: 'exclude' }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(headerFilterPopup).toBeVisible(); + + const listItems = headerFilterPopup.locator('.dx-list-item'); + const firstItemCheckbox = listItems.nth(0).locator('.dx-checkbox'); + const secondItemCheckbox = listItems.nth(1).locator('.dx-checkbox'); + const thirdItemCheckbox = listItems.nth(2).locator('.dx-checkbox'); + + await expect(firstItemCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(secondItemCheckbox).toHaveClass(/dx-checkbox-checked/); + await expect(thirdItemCheckbox).not.toHaveClass(/dx-checkbox-checked/); + }); + + test(`remote operations: ${remoteOperations} -> should not update column options if popup cancel btn clicked`, async ({ page }) => { + await setupRemoteMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { dataField: 'A', filterValues: ['A_4'] }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + const listItems = headerFilterPopup.locator('.dx-list-item'); + + await listItems.nth(0).click(); + await listItems.nth(1).click(); + await headerFilterPopup.locator('.dx-button').nth(1).click(); + + const columnOptions = await page.evaluate(() => { + const instance = ($('#container') as any).dxCardView('instance'); + return { + filterType: instance.columnOption('A', 'filterType'), + filterValues: instance.columnOption('A', 'filterValues'), + }; + }); + + expect(columnOptions.filterType).toBeUndefined(); + expect(columnOptions.filterValues).toEqual(['A_4']); + }); + + test(`remote operations: ${remoteOperations} -> should process groupInterval option`, async ({ page }) => { + await setupRemoteIdGroupMock(page); + await page.evaluate((ro) => { (window as any).testRemoteOperations = ro; }, remoteOperations); + + await createWidget(page, 'dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'id', + dataType: 'number', + headerFilter: { groupInterval: 5 }, + }, + 'A', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { visible: true }, + height: 600, + })); + + const filterIcon = page.locator('.dx-cardview-headers .dx-cardview-header-item').first().locator('.dx-header-filter-icon'); + await filterIcon.click(); + + const headerFilterPopup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(headerFilterPopup).toBeVisible(); + + const listItems = headerFilterPopup.locator('.dx-list-item'); + await expect(listItems).toHaveCount(2); + + await expect(listItems.nth(0)).toContainText('0 - 5'); + await expect(listItems.nth(1)).toContainText('5 - 10'); + }); + } +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.spec.ts new file mode 100644 index 000000000000..9921902650b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('popup with list', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + ], + columns: ['A', 'B', 'C'], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + + await testScreenshot(page, 'card-view_header-filter_popup-with-list.png', { + element: page.locator('#container'), + }); + }); + + test('popup with search', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + ], + columns: ['A', 'B', 'C'], + headerFilter: { visible: true, search: { enabled: true } }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + + await testScreenshot(page, 'card-view_header-filter_popup-with-search.png', { + element: page.locator('#container'), + }); + }); + + test('popup with tree', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: '2024-01-01', B: 'B_0', C: 'C_0' }, + { A: '2024-01-01', B: 'B_1', C: 'C_1' }, + { A: '2024-01-01', B: 'B_2', C: 'C_2' }, + { A: '2025-01-01', B: 'B_3', C: 'C_3' }, + { A: '2025-01-01', B: 'B_4', C: 'C_4' }, + { A: '2026-01-01', B: 'B_5', C: 'C_5' }, + ], + columns: [ + { dataField: 'A', dataType: 'date' }, + 'B', + 'C', + ], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter-icon').first().click(); + + await testScreenshot(page, 'card-view_header-filter_popup-with-tree.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts new file mode 100644 index 000000000000..f27dbf433d54 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts @@ -0,0 +1,228 @@ +import { test, expect, type Locator } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const PARENT_CONTAINER = '#parentContainer'; +const PARENT_STYLES = `${PARENT_CONTAINER} { width: 400px; padding: 0 20px; }`; +const DRAG_MOVE_X_COEFFICIENT = 1.5; +const DRAG_MOVE_Y_COEFFICIENT = 1; + +async function getDragCoordinates( + element: Locator, + rtlEnabled: boolean, + direction: 'left' | 'right', +): Promise<{ dragOffsetX: number; dragOffsetY: number }> { + const box = await element.boundingBox(); + const itemWidth = box?.width ?? 0; + const itemHeight = box?.width ?? 0; + + const dragDirectionX = direction === 'left' ? -1 : 1; + const dragRtlDirection = rtlEnabled ? -1 : 1; + const dragOffsetX = Math.round(dragDirectionX * dragRtlDirection * DRAG_MOVE_X_COEFFICIENT * itemWidth); + const dragOffsetY = Math.round(DRAG_MOVE_Y_COEFFICIENT * itemHeight); + + return { dragOffsetX, dragOffsetY }; +} + +test.describe('CardView - HeaderPanel Sortable Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('sortable indicator during dragging', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, name: 'Item 1', value: 10 }, + { id: 2, name: 'Item 2', value: 20 }, + { id: 3, name: 'Item 3', value: 30 }, + ], + keyExpr: 'id', + headerPanel: { + visible: true, + allowColumnReordering: true, + }, + columns: [ + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + const headerPanel = page.locator('.dx-cardview-headers'); + await expect(headerPanel).toBeVisible(); + + const firstItem = headerPanel.locator('.dx-cardview-header-item').first(); + const box = await firstItem.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 100, box.y + box.height / 2, { steps: 5 }); + + await testScreenshot(page, 'sortable-indicator-middle-rtl-false.png'); + + await page.mouse.up(); + } + }); + + test('sortable indicator during dragging to first place', async ({ page }) => { + await insertStylesheetRulesToPage(page, PARENT_STYLES); + await createWidget(page, 'dxCardView', { + columns: ['Field A', 'Field B', 'Field C'], + allowColumnReordering: true, + rtlEnabled: false, + width: 360, + }); + + const item = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + const box = await item.boundingBox(); + if (box) { + const { dragOffsetX, dragOffsetY } = await getDragCoordinates(item, false, 'left'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + dragOffsetX, box.y + box.height / 2 + dragOffsetY, { steps: 5 }); + await page.evaluate(() => { (document.activeElement as HTMLElement | null)?.blur(); }); + } + + await testScreenshot(page, 'sortable-indicator-first-rtl-false.png', { + element: page.locator(PARENT_CONTAINER), + }); + + await page.mouse.up(); + }); + + test('sortable indicator during dragging to middle place', async ({ page }) => { + await insertStylesheetRulesToPage(page, PARENT_STYLES); + await createWidget(page, 'dxCardView', { + columns: ['Field A', 'Field B', 'Field C'], + allowColumnReordering: true, + rtlEnabled: false, + width: 360, + }); + + const item = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(0); + const box = await item.boundingBox(); + if (box) { + const { dragOffsetX, dragOffsetY } = await getDragCoordinates(item, false, 'right'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + dragOffsetX, box.y + box.height / 2 + dragOffsetY, { steps: 5 }); + await page.evaluate(() => { (document.activeElement as HTMLElement | null)?.blur(); }); + } + + await testScreenshot(page, 'sortable-indicator-middle-rtl-false.png', { + element: page.locator(PARENT_CONTAINER), + }); + + await page.mouse.up(); + }); + + test('sortable indicator during dragging to last place', async ({ page }) => { + await insertStylesheetRulesToPage(page, PARENT_STYLES); + await createWidget(page, 'dxCardView', { + columns: ['Field A', 'Field B', 'Field C'], + allowColumnReordering: true, + rtlEnabled: false, + width: 360, + }); + + const item = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + const box = await item.boundingBox(); + if (box) { + const { dragOffsetX, dragOffsetY } = await getDragCoordinates(item, false, 'right'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + dragOffsetX, box.y + box.height / 2 + dragOffsetY, { steps: 5 }); + await page.evaluate(() => { (document.activeElement as HTMLElement | null)?.blur(); }); + } + + await testScreenshot(page, 'sortable-indicator-last-rtl-false.png', { + element: page.locator(PARENT_CONTAINER), + }); + + await page.mouse.up(); + }); + + test('sortable indicator during dragging to first place (RTL)', async ({ page }) => { + await insertStylesheetRulesToPage(page, PARENT_STYLES); + await createWidget(page, 'dxCardView', { + columns: ['Field A', 'Field B', 'Field C'], + allowColumnReordering: true, + rtlEnabled: true, + width: 360, + }); + + const item = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + const box = await item.boundingBox(); + if (box) { + const { dragOffsetX, dragOffsetY } = await getDragCoordinates(item, true, 'left'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + dragOffsetX, box.y + box.height / 2 + dragOffsetY, { steps: 5 }); + await page.evaluate(() => { (document.activeElement as HTMLElement | null)?.blur(); }); + } + + await testScreenshot(page, 'sortable-indicator-first-rtl-true.png', { + element: page.locator(PARENT_CONTAINER), + }); + + await page.mouse.up(); + }); + + test('sortable indicator during dragging to middle place (RTL)', async ({ page }) => { + await insertStylesheetRulesToPage(page, PARENT_STYLES); + await createWidget(page, 'dxCardView', { + columns: ['Field A', 'Field B', 'Field C'], + allowColumnReordering: true, + rtlEnabled: true, + width: 360, + }); + + const item = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(0); + const box = await item.boundingBox(); + if (box) { + const { dragOffsetX, dragOffsetY } = await getDragCoordinates(item, true, 'right'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + dragOffsetX, box.y + box.height / 2 + dragOffsetY, { steps: 5 }); + await page.evaluate(() => { (document.activeElement as HTMLElement | null)?.blur(); }); + } + + await testScreenshot(page, 'sortable-indicator-middle-rtl-true.png', { + element: page.locator(PARENT_CONTAINER), + }); + + await page.mouse.up(); + }); + + test('sortable indicator during dragging to last place (RTL)', async ({ page }) => { + await insertStylesheetRulesToPage(page, PARENT_STYLES); + await createWidget(page, 'dxCardView', { + columns: ['Field A', 'Field B', 'Field C'], + allowColumnReordering: true, + rtlEnabled: true, + width: 360, + }); + + const item = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + const box = await item.boundingBox(); + if (box) { + const { dragOffsetX, dragOffsetY } = await getDragCoordinates(item, true, 'right'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + dragOffsetX, box.y + box.height / 2 + dragOffsetY, { steps: 5 }); + await page.evaluate(() => { (document.activeElement as HTMLElement | null)?.blur(); }); + } + + await testScreenshot(page, 'sortable-indicator-last-rtl-true.png', { + element: page.locator(PARENT_CONTAINER), + }); + + await page.mouse.up(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts new file mode 100644 index 000000000000..6b338a151100 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - HeaderPanel Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + width: 600, + }); + + await testScreenshot(page, 'default-render.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with header filter enabled', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + headerFilter: { visible: true }, + width: 600, + }); + + await testScreenshot(page, 'header-filter-enabled.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with header filter enabled with filter values', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + columns: [ + 'id', + 'filedA', + { dataField: 'filedB', filterValues: ['B_0'] }, + { dataField: 'filedC', filterValues: ['C_0'] }, + ], + headerFilter: { visible: true }, + width: 600, + }); + + await testScreenshot(page, 'header-filter-enabled-with-values.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with single sorting', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + columns: ['id', 'filedA', { dataField: 'filedB', sortOrder: 'asc' }, 'fieldC'], + width: 600, + }); + + await testScreenshot(page, 'single-sorting.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with single sorting and header filter enabled', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + columns: ['id', 'filedA', { dataField: 'filedB', sortOrder: 'asc' }, 'fieldC'], + headerFilter: { visible: true }, + width: 600, + }); + + await testScreenshot(page, 'single-sorting-with-header-filter-enabled.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with multiple sorting', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + columns: [ + 'id', + 'filedA', + { dataField: 'filedB', sortOrder: 'asc', sortIndex: 1 }, + { dataField: 'filedC', sortOrder: 'desc', sortIndex: 0 }, + ], + sorting: { mode: 'multiple' }, + width: 600, + }); + + await testScreenshot(page, 'multiple-sorting.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with multiple sorting and header filter', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + columns: [ + 'id', + 'filedA', + { dataField: 'filedB', sortOrder: 'asc', sortIndex: 1 }, + { dataField: 'filedC', sortOrder: 'desc', sortIndex: 0 }, + ], + sorting: { mode: 'multiple' }, + headerFilter: { visible: true }, + width: 600, + }); + + await testScreenshot(page, 'multiple-sorting-with-header-filter-enabled.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with horizontal scroll', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: [ + { dataField: 'A', caption: 'First long caption' }, + { dataField: 'B', caption: 'Second long caption' }, + ], + width: 250, + }); + + await testScreenshot(page, 'render-with-horizontal-scroll.png', { + element: page.locator('.dx-cardview-headers .dx-cardview-header-item').first(), + }); + }); + + test('headerPanel column chooser link opens column chooser on click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + height: 600, + columns: [{ dataField: 'Column 1', visible: false }], + columnChooser: { enabled: true }, + }); + + await page.locator('.dx-cardview-headers .dx-link').click(); + + await testScreenshot(page, 'card-view-column-chooser-opened-on-empty-header-panel-link-click.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/items.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/items.functional.spec.ts new file mode 100644 index 000000000000..6dc97ce3f2ae --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/items.functional.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - Items functional', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test("Column should show data from calculateDisplayValue if function's result has other dataType", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: [{ + dataField: 'activity', + columnType: 'number', + calculateDisplayValue(e) { + return `activity ${e.activity}`; + }, + }], + dataSource: [{ id: 1, activity: 1 }], + keyExpr: 'id', + }); + + const valueCell = page.locator('.dx-cardview-card .dx-cardview-field-value').first(); + await expect(valueCell).toHaveText('activity 1'); + }); + + test('Column with customizeText should show formatted value', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: [{ + dataField: 'value', + customizeText({ value }) { + return `$${value}`; + }, + }], + dataSource: [{ id: 1, value: 100 }], + keyExpr: 'id', + }); + + const valueCell = page.locator('.dx-cardview-card .dx-cardview-field-value').first(); + await expect(valueCell).toHaveText('$100'); + }); + + test('Column with visible: false should not be shown in card', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: [ + { dataField: 'name', visible: true }, + { dataField: 'hidden', visible: false }, + ], + dataSource: [{ id: 1, name: 'John', hidden: 'secret' }], + keyExpr: 'id', + }); + + const fields = page.locator('.dx-cardview-card .dx-cardview-field-caption'); + await expect(fields).toHaveCount(1); + await expect(fields.first()).toHaveText('Name'); + }); + + test('Multiple cards render correct values', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['name', 'age'], + dataSource: [ + { id: 1, name: 'Alice', age: 30 }, + { id: 2, name: 'Bob', age: 25 }, + ], + keyExpr: 'id', + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(2); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onFocusedCardChanged.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onFocusedCardChanged.functional.spec.ts new file mode 100644 index 000000000000..57ffbecd9355 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onFocusedCardChanged.functional.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.onFocusedCardChanged', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should be called on each card focus change', async ({ page }) => { + await page.evaluate(() => { (window as any).onFocusedCardChangedArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 9 }, + onFocusedCardChanged: ({ cardIndex }) => { + (window as any).onFocusedCardChangedArgs.push(cardIndex); + }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(4); + await card.click(); + + for (const key of ['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft']) { + await card.dispatchEvent('keydown', { key }); + } + + const result = await page.evaluate(() => (window as any).onFocusedCardChangedArgs); + expect(result).toEqual([4, 7, 8, 5, 4]); + }); + + test('Should be called on focus change by click', async ({ page }) => { + await page.evaluate(() => { (window as any).onFocusedCardChangedArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 9 }, + onFocusedCardChanged: ({ cardIndex }) => { + (window as any).onFocusedCardChangedArgs.push(cardIndex); + }, + height: 700, + }); + + await page.locator('.dx-cardview-card').nth(5).click(); + await page.locator('.dx-cardview-card').nth(8).click(); + await page.locator('.dx-cardview-card').nth(0).click(); + + const result = await page.evaluate(() => (window as any).onFocusedCardChangedArgs); + expect(result).toEqual([5, 8, 0]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.functional.spec.ts new file mode 100644 index 000000000000..1984e2764826 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.functional.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.OnKeyDown', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should be called on header item unhandled event', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + height: 700, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.dispatchEvent('keydown', { key: 'a' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: false, key: 'a' }]); + }); + + test('Should be called on header item handled event', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + height: 700, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.dispatchEvent('keydown', { key: 'ArrowRight' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: true, key: 'ArrowRight' }]); + }); + + test('Should be called on card unhandled event', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').first(); + await card.dispatchEvent('keydown', { key: 'b' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: false, key: 'b' }]); + }); + + test('Should be called on card handled event', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').first(); + await card.dispatchEvent('keydown', { key: 'ArrowRight' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: true, key: 'ArrowRight' }]); + }); + + test('Should be called on card unhandled event inside focus trap', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + selection: { mode: 'multiple' }, + height: 700, + }); + + const checkbox = page.locator('.dx-cardview-card').first().locator('.dx-checkbox'); + await checkbox.dispatchEvent('keydown', { key: 'c' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: false, key: 'c' }]); + }); + + test('Should be called on card handled event inside focus trap', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + selection: { mode: 'multiple' }, + height: 700, + }); + + const checkbox = page.locator('.dx-cardview-card').first().locator('.dx-checkbox'); + await checkbox.dispatchEvent('keydown', { key: 'Escape' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: true, key: 'Escape' }]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts new file mode 100644 index 000000000000..0466d30a3b1d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts @@ -0,0 +1,140 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.ContentView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { caseName: 'arrows -> same first item', keys: ['ArrowRight', 'ArrowLeft'], resultIndex: 4 }, + { caseName: 'arrows -> left item', keys: ['ArrowLeft'], resultIndex: 3 }, + { caseName: 'arrows -> right item', keys: ['ArrowRight'], resultIndex: 5 }, + { caseName: 'arrows -> top item', keys: ['ArrowUp'], resultIndex: 1 }, + { caseName: 'arrows -> bottom item', keys: ['ArrowDown'], resultIndex: 7 }, + ].forEach(({ caseName, keys, resultIndex }) => { + test(`Should move between cards: ${caseName}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 9 }, + height: 700, + }); + + const card4 = page.locator('.dx-cardview-card').nth(4); + await card4.click(); + for (const key of keys) { + await page.keyboard.press(key); + } + + const targetCard = page.locator('.dx-cardview-card').nth(resultIndex); + await expect(targetCard).toBeFocused(); + }); + }); + + test('Should change page to the next one and focus first card', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 3, pageIndex: 1 }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + await page.keyboard.press('PageDown'); + + const firstCard = page.locator('.dx-cardview-card').first(); + await expect(firstCard).toBeFocused(); + }); + + test('Should change page to the previous one and focus first card', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 3, pageIndex: 1 }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + await page.keyboard.press('PageUp'); + + const firstCard = page.locator('.dx-cardview-card').first(); + await expect(firstCard).toBeFocused(); + }); + + [ + { caseName: 'arrows -> no left overflow', keys: ['ArrowLeft', 'ArrowLeft', 'ArrowLeft', 'ArrowLeft', 'ArrowLeft'], resultIndex: 3 }, + { caseName: 'arrows -> no right overflow', keys: ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowRight'], resultIndex: 5 }, + { caseName: 'arrows -> no top overflow', keys: ['ArrowUp', 'ArrowUp', 'ArrowUp', 'ArrowUp', 'ArrowUp'], resultIndex: 1 }, + { caseName: 'arrows -> no bottom overflow', keys: ['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowDown'], resultIndex: 7 }, + { caseName: 'first in same row', keys: ['Home'], resultIndex: 3 }, + { caseName: 'last in same row', keys: ['End'], resultIndex: 5 }, + { caseName: 'first in first row', keys: ['Control+Home'], resultIndex: 0 }, + { caseName: 'last in last row', keys: ['Control+End'], resultIndex: 8 }, + ].forEach(({ caseName, keys, resultIndex }) => { + test(`Should move between cards: ${caseName}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 9 }, + height: 700, + }); + + const card4 = page.locator('.dx-cardview-card').nth(4); + await card4.click(); + + for (const key of keys) { + await page.keyboard.press(key); + } + + const targetCard = page.locator('.dx-cardview-card').nth(resultIndex); + await expect(targetCard).toBeFocused(); + }); + }); + + test('Should do nothing if pageup pressed on first page', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 3, pageIndex: 0 }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(2); + await card.click(); + await page.keyboard.press('PageUp'); + + await expect(card).toBeFocused(); + }); + + test('Should do nothing if pagedown pressed on last page', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 3, pageIndex: 2 }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(2); + await card.click(); + await page.keyboard.press('PageDown'); + + await expect(card).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts new file mode 100644 index 000000000000..568086cee5a7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts @@ -0,0 +1,203 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.Header', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should navigate between items by arrows', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, A: 'A_0', B: 'B_0', C: 'C_0' }], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + await headerItems.nth(0).click(); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + + await expect(headerItems.nth(2)).toBeFocused(); + }); + + test('Should focus item by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, A: 'A_0', B: 'B_0', C: 'C_0' }], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + await headerItems.nth(1).click(); + + await expect(headerItems.nth(1)).toBeFocused(); + }); + + test('Should continue arrow navigation from last focused item', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, A: 'A_0', B: 'B_0', C: 'C_0' }], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + await headerItems.nth(1).click(); + await page.keyboard.press('ArrowRight'); + + await expect(headerItems.nth(2)).toBeFocused(); + }); + + test('Should enable sorting by Enter', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'focusable-start'; + document.body.insertBefore(el, document.getElementById('container')); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1 }, { id: 0 }, { id: 3 }, { id: 2 }], + columns: ['id'], + keyExpr: 'id', + height: 700, + }); + + await page.locator('#focusable-start').focus(); + await page.keyboard.press('Tab'); + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await expect(headerItem).toBeFocused(); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); + + const cardTexts = await page.locator('.dx-cardview-card').allInnerTexts(); + const idValues = cardTexts.map(t => t.trim()).filter(t => /^\d+$/.test(t)); + expect(idValues).toEqual(['0', '1', '2', '3']); + }); + + test('Should switch sorting by Enter', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'focusable-start'; + document.body.insertBefore(el, document.getElementById('container')); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1 }, { id: 0 }, { id: 3 }, { id: 2 }], + columns: ['id'], + keyExpr: 'id', + height: 700, + }); + + await page.locator('#focusable-start').focus(); + await page.keyboard.press('Tab'); + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await expect(headerItem).toBeFocused(); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); + + const cards = page.locator('.dx-cardview-card'); + const firstCardText = await cards.nth(0).textContent(); + const lastCardText = await cards.nth(3).textContent(); + expect(firstCardText).toContain('3'); + expect(lastCardText).toContain('0'); + }); + + test('Should clear sorting by ctrl+Enter', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'focusable-start'; + document.body.insertBefore(el, document.getElementById('container')); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1 }, { id: 0 }, { id: 3 }, { id: 2 }], + columns: ['id'], + keyExpr: 'id', + height: 700, + }); + + await page.locator('#focusable-start').focus(); + await page.keyboard.press('Tab'); + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await expect(headerItem).toBeFocused(); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); + + const sortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('id', 'sortOrder')); + expect(sortOrder).toBe('asc'); + + await page.keyboard.press('Control+Enter'); + await page.waitForTimeout(100); + + const sortOrderAfter = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('id', 'sortOrder')); + expect(sortOrderAfter).toBeUndefined(); + }); + + test('Should enable multi field sorting by shift+Enter', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'focusable-start'; + document.body.insertBefore(el, document.getElementById('container')); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, A: 1 }, { id: 0, A: 1 }, { id: 3, A: 0 }, { id: 2, A: 0 }], + columns: ['id', 'A'], + keyExpr: 'id', + sorting: { mode: 'multiple' }, + height: 700, + }); + + await page.locator('#focusable-start').focus(); + await page.keyboard.press('Tab'); + const firstHeaderItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await expect(firstHeaderItem).toBeFocused(); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('Shift+Enter'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('Shift+Enter'); + await page.waitForTimeout(100); + + const idSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('id', 'sortIndex')); + const aSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('A', 'sortIndex')); + + expect(typeof idSortIndex).toBe('number'); + expect(typeof aSortIndex).toBe('number'); + }); + + test('Should open header filter by alt+ArrowDown', async ({ page }) => { + await page.evaluate(() => { + const el = document.createElement('button'); + el.id = 'focusable-start'; + document.body.insertBefore(el, document.getElementById('container')); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1 }, { id: 0 }, { id: 3 }, { id: 2 }], + columns: ['id'], + keyExpr: 'id', + height: 700, + }); + + await page.locator('#focusable-start').focus(); + await page.keyboard.press('Tab'); + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await expect(headerItem).toBeFocused(); + await page.keyboard.press('Alt+ArrowDown'); + + const popup = page.locator('.dx-popup-wrapper.dx-header-filter-menu'); + await expect(popup).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts new file mode 100644 index 000000000000..694ad0d6f269 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.Search', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should focus search text box after ctrl+f if card is focused', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + searchPanel: { visible: true }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + await card.dispatchEvent('keydown', { key: 'f', ctrlKey: true }); + + const searchInput = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await expect(searchInput).toBeFocused(); + }); + + test('Should do nothing after ctrl+f if card is not focused', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + searchPanel: { visible: true }, + height: 700, + }); + + const toolbar = page.locator('.dx-cardview-toolbar'); + await toolbar.click(); + await toolbar.dispatchEvent('keydown', { key: 'f', ctrlKey: true }); + + const searchInput = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await expect(searchInput).not.toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts new file mode 100644 index 000000000000..40d84d3ed5ad --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.Selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { caseName: 'card selection', keys: ['Space'], result: [false, true, false] }, + { caseName: 'card cannot be deselected', keys: ['Space', 'Space'], result: [false, true, false] }, + { caseName: 'the next card selection', keys: ['Space', 'ArrowRight', 'Space'], result: [false, false, true] }, + ].forEach(({ caseName, keys, result }) => { + test(`Should handle selection in single mode: ${caseName}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { mode: 'single' }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + + for (const key of keys) { + await page.keyboard.press(key); + } + + for (let i = 0; i < 3; i++) { + const isSelected = await page.locator('.dx-cardview-card').nth(i).evaluate( + el => el.classList.contains('dx-cardview-card-selection') + ); + expect(isSelected).toBe(result[i]); + } + }); + }); + + [ + { caseName: 'card selection', keys: ['Space'], result: [false, true, false] }, + { caseName: 'card deselection', keys: ['Space', 'Space'], result: [false, false, false] }, + { caseName: 'the next card selection', keys: ['Space', 'ArrowRight', 'Space'], result: [false, true, true] }, + { caseName: 'range selection', keys: ['ArrowLeft', 'Space', 'ArrowRight', 'ArrowRight', 'Shift+Space'], result: [true, true, true] }, + ].forEach(({ caseName, keys, result }) => { + test(`Should handle selection in multiple mode: ${caseName}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { mode: 'multiple' }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + + for (const key of keys) { + await page.keyboard.press(key); + } + + for (let i = 0; i < 3; i++) { + const isSelected = await page.locator('.dx-cardview-card').nth(i).evaluate( + el => el.classList.contains('dx-cardview-card-selection') + ); + expect(isSelected).toBe(result[i]); + } + }); + }); + + test('Should do nothing after ctrl+a with selection single mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { mode: 'single' }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.dispatchEvent('keydown', { key: 'a', ctrlKey: true }); + + for (let i = 0; i < 3; i++) { + const isSelected = await page.locator('.dx-cardview-card').nth(i).evaluate( + el => el.classList.contains('dx-cardview-card-selection') + ); + expect(isSelected).toBe(false); + } + }); + + test('Should select all cards after ctrl+a with selection multiple mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { mode: 'multiple' }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.dispatchEvent('keydown', { key: 'a', ctrlKey: true }); + + for (let i = 0; i < 3; i++) { + const isSelected = await page.locator('.dx-cardview-card').nth(i).evaluate( + el => el.classList.contains('dx-cardview-card-selection') + ); + expect(isSelected).toBe(true); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts new file mode 100644 index 000000000000..4dcc1fd52538 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - LoadPanel', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Default render', async ({ page }) => { + await page.setViewportSize({ width: 800, height: 800 }); + await createWidget(page, 'dxCardView', { + width: 500, + height: 300, + dataSource: { + key: 'id', + load: () => new Promise(() => {}), + }, + columns: ['A', 'B', 'C', 'D'], + }); + + await testScreenshot(page, 'load-panel.png', { + element: page.locator('#container'), + }); + }); + + test('Default render when CardView has a large height', async ({ page }) => { + await page.setViewportSize({ width: 800, height: 800 }); + await createWidget(page, 'dxCardView', { + width: 500, + height: 3000, + dataSource: { + key: 'id', + load: () => new Promise(() => {}), + }, + columns: ['A', 'B', 'C', 'D'], + }); + + await testScreenshot(page, 'load-panel-with-large-height.png'); + }); + + test('The load panel should match the size of the component\'s root container', async ({ page }) => { + await page.setViewportSize({ width: 800, height: 800 }); + await createWidget(page, 'dxCardView', { + width: 500, + height: 300, + dataSource: { + key: 'id', + load: () => new Promise(() => {}), + }, + columns: ['A', 'B', 'C', 'D'], + }); + + const container = page.locator('#container'); + const containerBox = await container.boundingBox(); + + const loadPanelShadow = page.locator('.dx-loadpanel-wrapper .dx-loadpanel'); + const loadPanelBox = await loadPanelShadow.boundingBox(); + + if (loadPanelBox && containerBox) { + expect(Math.round(loadPanelBox.width)).toBe(500); + expect(Math.round(loadPanelBox.height)).toBe(300); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts new file mode 100644 index 000000000000..4e5a717d3e12 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - NoData', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + dataSource: [], + }); + + await testScreenshot(page, 'content-no-data.png', { + element: page.locator('#container'), + }); + }); + + test('no data message is visible when datasource is empty', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + dataSource: [], + }); + + const noDataElement = page.locator('.dx-empty-message'); + await expect(noDataElement).toBeVisible(); + }); + + test('custom no data text is rendered', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + dataSource: [], + noDataText: 'Custom no data message', + }); + + const noDataElement = page.locator('.dx-empty-message'); + await expect(noDataElement).toHaveText('Custom no data message'); + }); + + test('no cards rendered when datasource is empty', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + dataSource: [], + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(0); + }); + +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/pager.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/pager.spec.ts new file mode 100644 index 000000000000..5d1a7ecb9600 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/pager.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +async function createCardViewWithPager(page, config = {}) { + const dataSource = Array.from({ length: 20 }, (_, i) => ({ text: i.toString(), value: i })); + return createWidget(page, 'dxCardView', { + dataSource, + columns: ['text', 'value'], + paging: { pageSize: 2, pageIndex: 5 }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [2, 3, 4], + showInfo: true, + showNavigationButtons: true, + }, + ...config, + }); +} + +test.describe('CardView - Pager', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Page index interaction', async ({ page }) => { + await createCardViewWithPager(page); + + const pagerInfo = page.locator('.dx-info'); + await expect(pagerInfo).toHaveText('Page 6 of 10 (20 items)'); + + await page.locator('.dx-page').filter({ hasText: '7' }).click(); + await expect(pagerInfo).toHaveText('Page 7 of 10 (20 items)'); + + await page.locator('.dx-prev-button').click(); + await expect(pagerInfo).toHaveText('Page 6 of 10 (20 items)'); + }); + + [true, false].forEach((remoteOperation) => { + test(`Runtime filterValue change updates paging when remoteOperations = ${remoteOperation}`, async ({ page }) => { + await createCardViewWithPager(page, { remoteOperations: remoteOperation }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterValue', [ + ['value', '=', '1'], + 'or', ['value', '=', '2'], + 'or', ['value', '=', '3'], + 'or', ['value', '=', '4'], + ]); + }); + + await testScreenshot(page, `filter-value-edit-paging-update-remoteOperations-${remoteOperation}.png`, { + element: page.locator('#container'), + }); + }); + }); + + test('Paging after resetting filter', async ({ page }) => { + await createCardViewWithPager(page, { filterPanel: { visible: true } }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterValue', ['text', '=', '0']); + }); + + const pager = page.locator('.dx-pagination'); + await expect(pager).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').clearFilter(); + }); + + await expect(pager).toBeVisible(); + const pagerInfo = page.locator('.dx-info'); + await expect(pagerInfo).toHaveText('Page 1 of 10 (20 items)'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/a11y.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/a11y.functional.spec.ts new file mode 100644 index 000000000000..b32664a580e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/a11y.functional.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - Search.A11y.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Search field should have aria-label attribute', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true }, + }); + + const ariaLabel = await page.locator('.dx-cardview-search-panel .dx-texteditor-input').getAttribute('aria-label'); + expect(ariaLabel).toBe('Search in the card view'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.spec.ts new file mode 100644 index 000000000000..5f54f10e4c83 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.spec.ts @@ -0,0 +1,198 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - SearchPanel API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('searchPanel.visible API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true }, + }); + + const searchBox = page.locator('.dx-cardview-search-panel'); + await expect(searchBox).toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.visible', false); + }); + await expect(searchBox).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.visible', true); + }); + await expect(searchBox).toBeVisible(); + }); + + test('searchPanel.text API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true, text: 'rt' }, + }); + + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await expect(input).toHaveValue('rt'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.text', ''); + }); + await expect(input).toHaveValue(''); + }); + + test('searchPanel.width API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true, width: 300 }, + }); + + const searchBox = page.locator('.dx-cardview-search-panel'); + const initialWidth = await searchBox.evaluate(el => el.getBoundingClientRect().width); + expect(Math.round(initialWidth)).toBe(300); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.width', 200); + }); + + const newWidth = await searchBox.evaluate(el => el.getBoundingClientRect().width); + expect(Math.round(newWidth)).toBe(200); + }); + + test('searchPanel.placeholder API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true, placeholder: 'Test placeholder' }, + }); + + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await expect(input).toHaveAttribute('placeholder', 'Test placeholder'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.placeholder', 'Test placeholder 2'); + }); + await expect(input).toHaveAttribute('placeholder', 'Test placeholder 2'); + }); + + test('searchPanel.text API from UI', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true, text: '' }, + }); + + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await input.fill('rt'); + + const searchText = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').option('searchPanel.text'); + }); + expect(searchText).toBe('rt'); + }); + + test('searchPanel.searchVisibleColumnsOnly API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [ + { dataField: 'id', visible: false }, + { dataField: 'title' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + searchPanel: { visible: true }, + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await input.fill('2'); + await expect(cards).toHaveCount(1); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.searchVisibleColumnsOnly', true); + }); + await expect(cards).toHaveCount(0); + }); + + test('searchPanel.highlightSearchText API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true }, + }); + + const cards = page.locator('.dx-cardview-card'); + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await input.fill('rt'); + await expect(cards).toHaveCount(2); + + const highlights = page.locator('.dx-cardview-card').first().locator('.dx-highlight-text'); + await expect(highlights).toHaveCount(1); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.highlightSearchText', false); + }); + + await expect(cards).toHaveCount(2); + await expect(highlights).toHaveCount(0); + }); + + test('searchPanel.highlightCaseSensitive API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true, highlightCaseSensitive: true }, + }); + + const cards = page.locator('.dx-cardview-card'); + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await input.fill('rt'); + await expect(cards).toHaveCount(2); + + const highlights = page.locator('.dx-cardview-card').first().locator('.dx-highlight-text'); + await expect(highlights).toHaveCount(1); + + await input.fill('RT'); + await expect(cards).toHaveCount(2); + await expect(highlights).toHaveCount(0); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.highlightCaseSensitive', false); + }); + + await expect(highlights).toHaveCount(1); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts new file mode 100644 index 000000000000..e5688e1bbfbe --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - SearchPanel Behavior', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Search panel should filter cards', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true }, + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await input.fill('rt'); + await expect(cards).toHaveCount(2); + + await input.fill(''); + await expect(cards).toHaveCount(4); + }); + + test('Search panel should take into account calculateFilterExpression', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [ + { + dataField: 'id', + calculateFilterExpression() { + return [this.dataField, '>', '2']; + }, + }, + { dataField: 'title' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + searchPanel: { visible: true }, + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + const input = page.locator('.dx-cardview-search-panel .dx-texteditor-input'); + await input.fill('1'); + await expect(cards).toHaveCount(2); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/visual.spec.ts new file mode 100644 index 000000000000..b95415426058 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/visual.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Search.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('highlighted search text', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, firstName: 'Darin', lastName: 'Heritege', email: 'dheritege0@jugem.jp', gender: 'Male' }, + { id: 2, firstName: 'Aeriel', lastName: 'Giggs', email: 'agiggs1@hubpages.com', gender: 'Female' }, + ], + columns: ['id', 'firstName', 'lastName', 'email', 'gender'], + searchPanel: { visible: true, text: 'da' }, + height: 600, + }); + + await testScreenshot(page, 'card-view_search_text-highlighting.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/security.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/security.functional.spec.ts new file mode 100644 index 000000000000..7068762da7d6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/security.functional.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +const UNSAFE_TEXT = ''; + +test.describe('CardView - Security', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Script inside cell text should not be executed after opening header filter', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['caption'], + headerFilter: { visible: true }, + dataSource: [{ id: 1, caption: UNSAFE_TEXT }], + }); + + await page.locator('.dx-cardview-headers .dx-header-filter-icon').first().click(); + + const itemText = await page.locator('.dx-list-item').first().textContent(); + expect(itemText).toBe(UNSAFE_TEXT); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts new file mode 100644 index 000000000000..eb06a6e5774d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts @@ -0,0 +1,523 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const selectionData = [ + { id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0' }, + { id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1' }, + { id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2' }, + { id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3' }, + { id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4' }, +]; + +test.describe('Selection.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Single mode: select a first card -> select a second card -> deselect a second card', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'single' }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + + await firstCard.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCard.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCard.click({ modifiers: ['Control'] }); + await expect(secondCard).not.toHaveClass(/dx-cardview-card-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='always': select a first card -> select a second card -> deselect a first card -> deselect a second card", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + const firstCheckbox = firstCard.locator('.dx-checkbox'); + const secondCheckbox = secondCard.locator('.dx-checkbox'); + + await firstCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + + await firstCheckbox.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCheckbox.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).not.toHaveClass(/dx-cardview-card-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='always': select a several cards with shift -> unselect a several cards with shift", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + const thirdCard = page.locator('.dx-cardview-card').nth(2); + const firstCheckbox = firstCard.locator('.dx-checkbox'); + const thirdCheckbox = thirdCard.locator('.dx-checkbox'); + + await firstCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await thirdCheckbox.click({ modifiers: ['Shift'] }); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + await expect(thirdCard).toHaveClass(/dx-cardview-card-selection/); + + await firstCheckbox.click({ modifiers: ['Shift'] }); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(thirdCard).not.toHaveClass(/dx-cardview-card-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='always': select cards with checkboxes", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + + const firstCheckbox = page.locator('.dx-cardview-card').nth(0).locator('.dx-checkbox'); + const secondCheckbox = page.locator('.dx-cardview-card').nth(1).locator('.dx-checkbox'); + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + + await firstCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + + await firstCheckbox.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCheckbox.click(); + await expect(secondCard).not.toHaveClass(/dx-cardview-card-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='always': select cards with shift", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + + const firstCheckbox = page.locator('.dx-cardview-card').nth(0).locator('.dx-checkbox'); + const thirdCheckbox = page.locator('.dx-cardview-card').nth(2).locator('.dx-checkbox'); + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + const thirdCard = page.locator('.dx-cardview-card').nth(2); + + await firstCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await thirdCheckbox.click({ modifiers: ['Shift'] }); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + await expect(thirdCard).toHaveClass(/dx-cardview-card-selection/); + + await firstCheckbox.click({ modifiers: ['Shift'] }); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(thirdCard).not.toHaveClass(/dx-cardview-card-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='onClick': select a first card by clicking a checkbox -> deselect a first card by clicking a checkbox", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const firstCheckbox = firstCard.locator('.dx-checkbox'); + + await firstCard.locator('.dx-cardview-card-toolbar-item').first().hover(); + await expect(firstCheckbox).toBeVisible(); + + await firstCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + const checkboxesAfterSelect = page.locator('.dx-cardview-card .dx-checkbox'); + await expect(checkboxesAfterSelect.first()).toBeVisible(); + + await firstCheckbox.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(checkboxesAfterSelect.first()).not.toBeVisible(); + }); + + test("Multiple mode with showCheckBoxesMode='onClick': select a first card by clicking a card -> deselect a first card by clicking a card", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + + await firstCard.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await firstCard.click({ modifiers: ['Control'] }); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='onClick': select a first card -> select a second card (first card selection state is reset) -> select a first card with ctrl", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + const checkboxes = page.locator('.dx-cardview-card .dx-checkbox'); + + await firstCard.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(checkboxes.first()).not.toBeVisible(); + + await secondCard.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + await expect(checkboxes.first()).not.toBeVisible(); + + await firstCard.click({ modifiers: ['Control'] }); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + await expect(checkboxes.first()).toBeVisible(); + }); + + test("Multiple mode with showCheckBoxesMode='onClick': select first card -> select second card -> select first card with ctrl", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + + await firstCard.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCard.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + + await firstCard.click({ modifiers: ['Control'] }); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='onClick': select a first card by card hold -> deselect a first card by card hold", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const checkboxes = page.locator('.dx-cardview-card .dx-checkbox'); + + await firstCard.dispatchEvent('dxhold'); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + await expect(checkboxes.first()).toBeVisible(); + + await firstCard.dispatchEvent('dxhold'); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(checkboxes.first()).not.toBeVisible(); + }); + + test("Multiple mode with showCheckBoxesMode='onLongTap': select a several cards", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onLongTap', allowSelectAll: true }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + const checkboxes = page.locator('.dx-cardview-card .dx-checkbox'); + + await firstCard.dispatchEvent('dxhold'); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + await expect(checkboxes.first()).toBeVisible(); + + await firstCard.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + await secondCard.click(); + await expect(secondCard).toHaveClass(/dx-cardview-card-selection/); + }); + + test("Select all when selectAllMode = 'allPages'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true, selectAllMode: 'allPages' }, + }); + + await page.locator('[aria-label="Select all"]').click(); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([0, 1, 2, 3, 4]); + }); + + test("Deselect all when selectAllMode = 'allPages'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0, 1, 2, 3, 4], + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true, selectAllMode: 'allPages' }, + }); + + await page.locator('[aria-label="Clear selection"]').click(); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([]); + }); + + test("Select all when selectAllMode = 'page'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + paging: { pageSize: 3 }, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true, selectAllMode: 'page' }, + }); + + await page.locator('[aria-label="Select all"]').click(); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([0, 1, 2]); + }); + + test("Deselect all when selectAllMode = 'page'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + paging: { pageSize: 3 }, + selectedCardKeys: [0, 1, 2], + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true, selectAllMode: 'page' }, + }); + + await page.locator('[aria-label="Clear selection"]').click(); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([]); + }); + + test("The states of the Select All and Clear selection buttons should update correctly after changing the page when selectAllMode = 'allPages'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + paging: { pageSize: 3 }, + selectedCardKeys: [0, 1, 2, 3, 4], + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true, selectAllMode: 'allPages' }, + }); + + const selectAllBtn = page.locator('[aria-label="Select all"]'); + const clearSelectionBtn = page.locator('[aria-label="Clear selection"]'); + + await expect(selectAllBtn).toBeDisabled(); + await expect(clearSelectionBtn).not.toBeDisabled(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').pageIndex(1); + }); + + await expect(selectAllBtn).toBeDisabled(); + await expect(clearSelectionBtn).not.toBeDisabled(); + }); + + test("The states of the Select All and Clear selection buttons should update correctly after changing the page when selectAllMode = 'page'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + paging: { pageSize: 3 }, + selectedCardKeys: [0, 1, 2], + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true, selectAllMode: 'page' }, + }); + + const selectAllBtn = page.locator('[aria-label="Select all"]'); + const clearSelectionBtn = page.locator('[aria-label="Clear selection"]'); + + await expect(selectAllBtn).toBeDisabled(); + await expect(clearSelectionBtn).not.toBeDisabled(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').pageIndex(1); + }); + + await expect(selectAllBtn).not.toBeDisabled(); + await expect(clearSelectionBtn).toBeDisabled(); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([0, 1, 2]); + }); + + test("Switching the showCheckBoxesMode option from onClick to always at runtime should work correctly", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick' }, + }); + + const checkboxes = page.locator('.dx-cardview-card .dx-checkbox'); + await expect(checkboxes.first()).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('selection.showCheckBoxesMode', 'always'); + }); + + await expect(checkboxes.first()).toBeVisible(); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + await firstCard.click(); + await expect(firstCard).not.toHaveClass(/dx-cardview-card-selection/); + }); + + test("Switching the showCheckBoxesMode option from always to onClick at runtime should work correctly", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always' }, + }); + + const checkboxes = page.locator('.dx-cardview-card .dx-checkbox'); + await expect(checkboxes.first()).toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('selection.showCheckBoxesMode', 'onClick'); + }); + + await expect(checkboxes.first()).not.toBeVisible(); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + await firstCard.click(); + await expect(firstCard).toHaveClass(/dx-cardview-card-selection/); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([0]); + }); + + test('"Deselect all" should work after changing showCheckboxMode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }, { a: 6 }], + keyExpr: 'a', + selection: { mode: 'multiple' }, + selectedCardKeys: [1, 2], + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('selection.showCheckBoxesMode', 'onClick'); + }); + + await page.locator('[aria-label="Clear selection"]').click(); + + for (let i = 0; i < 6; i++) { + const isSelected = await page.locator('.dx-cardview-card').nth(i).evaluate( + el => el.classList.contains('dx-cardview-card-selection') + ); + expect(isSelected).toBe(false); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.spec.ts new file mode 100644 index 000000000000..5992341bbce8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.spec.ts @@ -0,0 +1,159 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const selectionData = [ + { id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0' }, + { id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1' }, + { id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2' }, + { id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3' }, + { id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4' }, +]; + +test.describe('Selection.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Single mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0], + selection: { mode: 'single' }, + }); + + await testScreenshot(page, 'card-view_single_selection.png', { + element: page.locator('#container'), + }); + }); + + test("Multiple mode with Select All/Deselect All and showCheckBoxesMode = 'none'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'none', allowSelectAll: true }, + }); + + await testScreenshot(page, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_none.png', { + element: page.locator('#container'), + }); + }); + + test("Multiple mode with Select All/Deselect All and showCheckBoxesMode = 'always'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + + await testScreenshot(page, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_always.png', { + element: page.locator('#container'), + }); + }); + + test("Multiple mode with Select All/Deselect All and showCheckBoxesMode = 'onClick'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + await testScreenshot(page, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1.png', { + element: page.locator('#container'), + }); + + await page.locator('.dx-cardview-card').first().locator('.dx-cardview-card-toolbar-item').first().hover(); + + await testScreenshot(page, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2.png', { + element: page.locator('#container'), + }); + }); + + test("Multiple mode with a selected card and showCheckBoxesMode = 'onClick'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + selectedCardKeys: [0], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + await testScreenshot(page, 'card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick.png', { + element: page.locator('#container'), + }); + }); + + test("Multiple mode with selected cards and showCheckBoxesMode = 'onClick'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + selectedCardKeys: [0, 1], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onClick', allowSelectAll: true }, + }); + + await testScreenshot(page, 'card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick.png', { + element: page.locator('#container'), + }); + }); + + test("Multiple mode with Select All/Deselect All and showCheckBoxesMode = 'onLongTap'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'onLongTap', allowSelectAll: true }, + }); + + await testScreenshot(page, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1.png', { + element: page.locator('#container'), + }); + + await page.locator('.dx-cardview-card').first().dispatchEvent('dxhold'); + + await testScreenshot(page, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2.png', { + element: page.locator('#container'), + }); + }); + + test('Multiple mode without Select All/Deselect All', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', allowSelectAll: false }, + }); + + await testScreenshot(page, 'card-view_miltiple_selection_without_select-all.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.themes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.themes.spec.ts new file mode 100644 index 000000000000..14a95ef96553 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.themes.spec.ts @@ -0,0 +1,144 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +test.describe('CardView - Sorting API Themes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Sort index API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc', sortIndex: 1 }, + { dataField: 'name', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_sort_index_api.png', { + element: page.locator('#container'), + }); + }); + + test('ShowSortIndexes API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + sorting: { showSortIndexes: false }, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc', sortIndex: 1 }, + { dataField: 'name', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_show_sort_indexes_api.png', { + element: page.locator('#container'), + }); + }); + + test('AllowSorting API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + sorting: { showSortIndexes: false }, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc', sortIndex: 1, allowSorting: false }, + { dataField: 'name', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'lastName' }, + ], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + await titleHeader.click(); + + await testScreenshot(page, 'cardview_allow_sorting_api.png', { + element: page.locator('#container'), + }); + }); + + test('CalculateSortValue API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + sorting: { showSortIndexes: false }, + columns: [ + { dataField: 'id' }, + { + dataField: 'title', + sortOrder: 'asc', + calculateSortValue: 'name', + }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_calculate_sort_value_is_filed_api.png', { + element: page.locator('#container'), + }); + }); + + test('SortingMethod API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + sorting: { showSortIndexes: false }, + columns: [ + { dataField: 'id' }, + { + dataField: 'title', + sortOrder: 'asc', + sortingMethod(value1, value2) { + if (value1 === 'Mr.' && value2 !== 'Mr.') return 1; + if (value1 !== 'Mr.' && value2 === 'Mr.') return -1; + return value1.localeCompare(value2); + }, + }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_sorting_method_api.png', { + element: page.locator('#container'), + }); + }); + + test('Default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_headers_default_render.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts new file mode 100644 index 000000000000..436cdc20fb16 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts @@ -0,0 +1,317 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +test.describe('CardView - Sorting Behavior - Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Change sorting by header click in single mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode: 'single' }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await titleHeader.click(); + + const sortOrder = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder'); + }); + expect(sortOrder).toBe('asc'); + }); + + test('Sorting should work with computed columns', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }], + keyExpr: 'id', + columns: [{ + caption: 'Computed', + allowSorting: true, + calculateFieldValue: ({ id }) => `str_${id}`, + }], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click(); + + const firstValue = await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-value').textContent(); + expect(firstValue).toBe('str_0'); + + await headerItem.click(); + + const newFirstValue = await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-value').textContent(); + expect(newFirstValue).toBe('str_3'); + }); + + ( + [ + ['none', false, false, false, [undefined, undefined]], + ['none', true, false, false, [undefined, undefined]], + ['none', false, true, false, [undefined, undefined]], + ['none', false, false, true, [undefined, undefined]], + ['single', false, false, false, ['desc', undefined]], + ['single', true, false, false, ['desc', undefined]], + ['single', false, true, false, [undefined, undefined]], + ['single', false, false, true, [undefined, undefined]], + ['multiple', false, false, false, ['desc', 0]], + ['multiple', true, false, false, ['desc', 0]], + ['multiple', false, true, false, [undefined, undefined]], + ['multiple', false, false, true, [undefined, undefined]], + ] as [string, boolean, boolean, boolean, [string | undefined, number | undefined]][] + ).forEach(([mode, shift, ctrl, meta, [titleSortOrder, titleSortIndex]]) => { + test(`Change sorting of sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await titleHeader.click(); + await titleHeader.click({ modifiers: [...(shift ? ['Shift'] : []), ...(ctrl ? ['Control'] : []), ...(meta ? ['Meta'] : [])] as any }); + + const actualSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder')); + const actualSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortIndex')); + + expect(actualSortOrder).toBe(titleSortOrder); + expect(actualSortIndex).toBe(titleSortIndex); + }); + }); + + ( + [ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + ['single', false, false, false, [undefined, undefined], ['asc', undefined]], + ['single', true, false, false, [undefined, undefined], ['asc', undefined]], + ['single', false, true, false, ['asc', undefined], [undefined, undefined]], + ['single', false, false, true, ['asc', undefined], [undefined, undefined]], + ['multiple', false, false, false, [undefined, undefined], ['asc', 0]], + ['multiple', true, false, false, ['asc', 0], ['asc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], + ] as [string, boolean, boolean, boolean, [string | undefined, number | undefined], [string | undefined, number | undefined]][] + ).forEach(([mode, shift, ctrl, meta, [titleSortOrder, titleSortIndex], [nameSortOrder, nameSortIndex]]) => { + test(`Change sorting of neighbour non sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + const nameHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + await titleHeader.click(); + await nameHeader.click({ modifiers: [...(shift ? ['Shift'] : []), ...(ctrl ? ['Control'] : []), ...(meta ? ['Meta'] : [])] as any }); + + const actualTitleSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder')); + const actualTitleSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortIndex')); + const actualNameSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortOrder')); + const actualNameSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortIndex')); + + expect(actualTitleSortOrder).toBe(titleSortOrder); + expect(actualTitleSortIndex).toBe(titleSortIndex); + expect(actualNameSortOrder).toBe(nameSortOrder); + expect(actualNameSortIndex).toBe(nameSortIndex); + }); + }); + + const SORT_ASCENDING_MENUITEM_INDEX = 0; + const SORT_DESCENDING_MENUITEM_INDEX = 1; + const CLEAR_SORTING_MENUITEM_INDEX = 2; + + ( + [ + ['none', SORT_ASCENDING_MENUITEM_INDEX, [undefined, undefined]], + ['none', SORT_DESCENDING_MENUITEM_INDEX, [undefined, undefined]], + ['none', CLEAR_SORTING_MENUITEM_INDEX, [undefined, undefined]], + ['single', SORT_ASCENDING_MENUITEM_INDEX, ['asc', undefined]], + ['single', SORT_DESCENDING_MENUITEM_INDEX, ['desc', undefined]], + ['single', CLEAR_SORTING_MENUITEM_INDEX, [undefined, undefined]], + ['multiple', SORT_ASCENDING_MENUITEM_INDEX, ['asc', 0]], + ['multiple', SORT_DESCENDING_MENUITEM_INDEX, ['desc', 0]], + ['multiple', CLEAR_SORTING_MENUITEM_INDEX, [undefined, undefined]], + ] as [string, number, [string | undefined, number | undefined]][] + ).forEach(([mode, menuItemIndex, [titleSortOrder, titleSortIndex]]) => { + test(`Change sorting of sorted item in ${mode} mode with ${menuItemIndex} context menu item`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await titleHeader.click({ button: 'right' }); + await page.locator('.dx-context-menu .dx-menu-item').nth(menuItemIndex).click(); + + const actualSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder')); + const actualSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortIndex')); + + expect(actualSortOrder).toBe(titleSortOrder); + expect(actualSortIndex).toBe(titleSortIndex); + + await page.locator('body').click(); + await expect(page.locator('.dx-context-menu')).not.toBeVisible(); + }); + }); + + ( + [ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + ['single', false, false, false, [undefined, undefined], ['desc', undefined]], + ['single', true, false, false, [undefined, undefined], ['desc', undefined]], + ['single', false, true, false, [undefined, undefined], [undefined, undefined]], + ['single', false, false, true, [undefined, undefined], [undefined, undefined]], + ['multiple', false, false, false, [undefined, undefined], ['desc', 0]], + ['multiple', true, false, false, ['asc', 0], ['desc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], + ] as [string, boolean, boolean, boolean, [string | undefined, number | undefined], [string | undefined, number | undefined]][] + ).forEach(([mode, shift, ctrl, meta, [titleSortOrder, titleSortIndex], [nameSortOrder, nameSortIndex]]) => { + test(`Change sorting of neighbour sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + const nameHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + await titleHeader.click(); + await nameHeader.click({ modifiers: ['Shift'] }); + await nameHeader.click({ modifiers: [...(shift ? ['Shift'] : []), ...(ctrl ? ['Control'] : []), ...(meta ? ['Meta'] : [])] as any }); + + const actualTitleSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder')); + const actualTitleSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortIndex')); + const actualNameSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortOrder')); + const actualNameSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortIndex')); + + expect(actualTitleSortOrder).toBe(titleSortOrder); + expect(actualTitleSortIndex).toBe(titleSortIndex); + expect(actualNameSortOrder).toBe(nameSortOrder); + expect(actualNameSortIndex).toBe(nameSortIndex); + }); + }); + + ( + [ + ['none', SORT_ASCENDING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['none', SORT_DESCENDING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['none', CLEAR_SORTING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['single', SORT_ASCENDING_MENUITEM_INDEX, [undefined, undefined], ['asc', undefined]], + ['single', SORT_DESCENDING_MENUITEM_INDEX, [undefined, undefined], ['desc', undefined]], + ['single', CLEAR_SORTING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['multiple', SORT_ASCENDING_MENUITEM_INDEX, ['asc', 0], ['asc', 1]], + ['multiple', SORT_DESCENDING_MENUITEM_INDEX, ['asc', 0], ['desc', 1]], + ['multiple', CLEAR_SORTING_MENUITEM_INDEX, ['asc', 0], [undefined, undefined]], + ] as [string, number, [string | undefined, number | undefined], [string | undefined, number | undefined]][] + ).forEach(([mode, menuItemIndex, [titleSortOrder, titleSortIndex], [nameSortOrder, nameSortIndex]]) => { + test(`Change sorting of neighbour sorted item in ${mode} mode with ${menuItemIndex} context menu item`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + const nameHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + await titleHeader.click(); + await nameHeader.click({ modifiers: ['Shift'] }); + await nameHeader.click({ button: 'right' }); + await page.locator('.dx-context-menu .dx-menu-item').nth(menuItemIndex as number).click(); + + const actualTitleSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder')); + const actualTitleSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortIndex')); + const actualNameSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortOrder')); + const actualNameSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortIndex')); + + expect(actualTitleSortOrder).toBe(titleSortOrder); + expect(actualTitleSortIndex).toBe(titleSortIndex); + expect(actualNameSortOrder).toBe(nameSortOrder); + expect(actualNameSortIndex).toBe(nameSortIndex); + + await page.locator('body').click(); + await expect(page.locator('.dx-context-menu')).not.toBeVisible(); + }); + }); + + ( + [ + ['none', SORT_ASCENDING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['none', SORT_DESCENDING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['none', CLEAR_SORTING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['single', SORT_ASCENDING_MENUITEM_INDEX, [undefined, undefined], ['asc', undefined]], + ['single', SORT_DESCENDING_MENUITEM_INDEX, [undefined, undefined], ['desc', undefined]], + ['single', CLEAR_SORTING_MENUITEM_INDEX, [undefined, undefined], [undefined, undefined]], + ['multiple', SORT_ASCENDING_MENUITEM_INDEX, ['asc', 0], ['asc', 1]], + ['multiple', SORT_DESCENDING_MENUITEM_INDEX, ['asc', 0], ['desc', 1]], + ['multiple', CLEAR_SORTING_MENUITEM_INDEX, ['asc', 0], [undefined, undefined]], + ] as [string, number, [string | undefined, number | undefined], [string | undefined, number | undefined]][] + ).forEach(([mode, menuItemIndex, [titleSortOrder, titleSortIndex], [nameSortOrder, nameSortIndex]]) => { + test(`Change sorting of neighbour non sorted item in ${mode} mode with ${menuItemIndex} context menu item`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + const nameHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').nth(1); + await titleHeader.click(); + await nameHeader.click({ button: 'right' }); + await page.locator('.dx-context-menu .dx-menu-item').nth(menuItemIndex).click(); + + const actualTitleSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder')); + const actualTitleSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('title', 'sortIndex')); + const actualNameSortOrder = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortOrder')); + const actualNameSortIndex = await page.evaluate(() => ($('#container') as any).dxCardView('instance').columnOption('name', 'sortIndex')); + + expect(actualTitleSortOrder).toBe(titleSortOrder); + expect(actualTitleSortIndex).toBe(titleSortIndex); + expect(actualNameSortOrder).toBe(nameSortOrder); + expect(actualNameSortIndex).toBe(nameSortIndex); + + await page.locator('body').click(); + await expect(page.locator('.dx-context-menu')).not.toBeVisible(); + }); + }); + + test('Change sorting via context menu', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode: 'single' }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await titleHeader.click({ button: 'right' }); + await page.locator('.dx-context-menu .dx-menu-item').nth(0).click(); + + const sortOrder = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder'); + }); + expect(sortOrder).toBe('asc'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.themes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.themes.spec.ts new file mode 100644 index 000000000000..08f994935d3d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.themes.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +test.describe('CardView - Sorting Behavior - Themes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_headers_default_render.png', { + element: page.locator('#container'), + }); + }); + + test('Default multiple sorting render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name', sortOrder: 'asc' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_headers_with_multiple_sorting_render.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/draggable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/draggable.spec.ts new file mode 100644 index 000000000000..2c9827304c95 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/draggable.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Draggable', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const init = async () => page.evaluate(() => { + $('
', { + id: 'scrollview', + width: '400px', + height: '400px', + }) + .css({ + position: 'absolute', + top: 0, + padding: '20px', + background: '#f18787', + }) + .appendTo('#container'); + + $('
', { + id: 'scrollview-content', + height: '500px', + width: '500px', + }).appendTo('#scrollview'); + + $('
', { + id: 'drag-me', + }) + .css({ + 'background-color': 'blue', + display: 'inline-block', + }) + .appendTo('#scrollview-content'); + $('#drag-me').append('DRAG ME!!!'); + }); + + test.skip('dxDraggable element should not loose its position on dragging with auto-scroll inside ScrollView (T1169590)', async ({ page }) => { + + await init(); + await createWidget(page, 'dxScrollView', { + direction: 'both', + }, '#scrollview'); + await createWidget(page, 'dxDraggable', { }, '#drag-me'); + + const draggable = page.locator('#drag-me'); + const scrollable = page.locator('#scrollview'); + + await (async () => { + const box = await draggable.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 400, { steps: 10 }); + await page.mouse.up(); + } + })() + + .expect(scrollable.getContainer()().scrollTop) + .gt(60); + + await page.expect((await draggable().boundingClientRect).top) + .gt(400); + + await draggable.scrollIntoViewIfNeeded(); + + await (async () => { + const box = await draggable.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 400, box.y + box.height / 2 + 0, { steps: 10 }); + await page.mouse.up(); + } + })() + + .expect(scrollable.getContainer()().scrollLeft) + .gt(60); + + await page.expect((await draggable().boundingClientRect).left) + .gt(400); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/eventsEngine.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/eventsEngine.spec.ts new file mode 100644 index 000000000000..851bbeac8067 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/eventsEngine.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const init = async () => page.evaluate(() => { + const markup = `
+
+
+
hoverStartTriggerCount
+
0
+
hoverEndTriggerCount
+
0
+
`; + + $('#container').html(markup); + + const { DevExpress } = (window as any); + + let hoverStartTriggerCount = 0; + let hoverEndTriggerCount = 0; + + DevExpress.events.on($('#target'), 'dxhoverstart', () => { + hoverStartTriggerCount += 1; + + $('#hoverStartTriggerCount').text(hoverStartTriggerCount); + }); + + DevExpress.events.on($('#target'), 'dxhoverend', () => { + hoverEndTriggerCount += 1; + + $('#hoverEndTriggerCount').text(hoverEndTriggerCount); + }); + }); + + test.skip('The `dxhoverstart` event should be triggered after dragging and dropping an HTML draggable element (T1260277)', async ({ page }) => { + + await init(); + + const draggable = page.locator('#draggable'); + const target = page.locator('#target'); + const hoverStartTriggerCount = page.locator('#hoverStartTriggerCount'); + const hoverEndTriggerCount = page.locator('#hoverEndTriggerCount'); + + await (async () => { + const box = await draggable.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 400, { steps: 10 }); + await page.mouse.up(); + } + })(); + + // `.drag` does not trigger the `pointercancel` event. + // A sequence of `.drag` calls behaves like a single drag&drop operation, + // and each call does not trigger the `pointerup` event. + // Even if it did, the `pointercancel` event would not be triggered as specified in: + // https://www.w3.org/TR/pointerevents/#suppressing-a-pointer-event-stream + // This is a hack to test the event engine's logic. + await draggable.dispatchEvent('pointercancel'); + + await (async () => { + const box = await target.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 400, { steps: 10 }); + await page.mouse.up(); + } + })(); + + expect(hoverStartTriggerCount.textContent).toBe('1'); + expect(hoverEndTriggerCount.textContent).toBe('1'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/data/index.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/data/index.ts new file mode 100644 index 000000000000..342c91437b85 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/data/index.ts @@ -0,0 +1,49 @@ +export const filter = [ + ['Category', '=', 'Video Players'], + 'or', + [ + ['Category', '=', 'Monitors'], + 'and', + ['Price', 'between', [165, 700]], + ], + 'or', + [ + ['Category', '=', 'Televisions'], + 'and', + ['Price', 'between', [2000, 4000]], + ], +]; + +export const categories = [ + 'Video Players', + 'Televisions', + 'Monitors', + 'Projectors', + 'Automation', +]; + +export const fields = [ + { + dataField: 'ID', + dataType: 'number', + }, + { + dataField: 'Name.Surname', + }, + { + dataField: 'Price', + dataType: 'number', + format: 'currency', + }, + { + dataField: 'Current_Inventory', + dataType: 'number', + caption: 'Inventory', + }, + { + dataField: 'Category', + lookup: { + dataSource: categories, + }, + }, +]; diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts new file mode 100644 index 000000000000..516f532a2966 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import { fields, filter } from './data'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Editing events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Change value editor to checkbox', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + onEditorPreparing: (data: any) => { + data.editorName = 'dxCheckBox'; + }, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.locator('.dx-filterbuilder-item-value-text').first().click(); + + await testScreenshot(page, 'value-editor-checkbox.png', { element: '#container' }); + }); + + test('Change value editor to switch', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + onEditorPreparing: (data: any) => { + data.editorName = 'dxSwitch'; + }, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.locator('.dx-filterbuilder-item-value-text').first().click(); + + await testScreenshot(page, 'value-editor-switch.png', { element: '#container' }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts new file mode 100644 index 000000000000..275b9641ee02 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FilterBuilder - Field naming', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('FilterBuilder - First field uses the dataField property while subsequent fields use the name property in the filter value', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + value: [ + ['dataField1', '<>', 0], + ], + fields: [ + { dataField: 'dataField1', name: 'name1' }, + { dataField: 'dataField2', name: 'name2' }, + ], + }); + + const expectedValues = [ + [ + ['name1', '<>', 0], + 'and', + ['name1', 'contains', 'A'], + ], + [ + ['name1', '<>', 0], + 'and', + ['name2', 'contains', 'A'], + ], + ]; + + await page.locator('#container .dx-filterbuilder-add-condition').click(); + await page.locator('.dx-treeview-item').first().click(); + await page.locator('#container .dx-filterbuilder-item-value-text').last().click(); + await page.keyboard.type('A'); + await page.keyboard.press('Enter'); + + const value1 = await page.evaluate(() => + ($('#container') as any).dxFilterBuilder('instance').option('value'), + ); + expect(value1).toEqual(expectedValues[0]); + + await page.locator('#container .dx-filterbuilder-item-field').last().click(); + await page.locator('.dx-treeview-item').nth(1).click(); + await page.locator('#container .dx-filterbuilder-item-value-text').last().click(); + await page.keyboard.type('A'); + await page.keyboard.press('Enter'); + + const value2 = await page.evaluate(() => + ($('#container') as any).dxFilterBuilder('instance').option('value'), + ); + expect(value2).toEqual(expectedValues[1]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderScrolling.spec.ts new file mode 100644 index 000000000000..c22b72a385bb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderScrolling.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Filter Builder Scrolling Test', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1273328 > T1294239 + test.skip('FilterBuilder - The field drop-down closes with the page scroll', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '#container {height: 150px; overflow: scroll;}'); + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + }); + + const filterBuilder = page.locator('#container'); + + await filterBuilder.isReady(); + + await page.click(filterBuilder.getItem('operation')) + .scrollIntoView(filterBuilder.getItem('operation', 4)); + + await expect(FilterBuilder.getPopupTreeView().exists).notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts new file mode 100644 index 000000000000..2b253f2dbff5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import { fields, filter } from './data'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FilterBuilder', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Field dropdown popup', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + await page.locator('#container .dx-filterbuilder-item-field').first().click(); + + await testScreenshot(page, 'field-dropdown.png', { element: '#container' }); + }); + + test('operation dropdown popup', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + await page.locator('#container .dx-filterbuilder-item-operation').first().click(); + + await testScreenshot(page, 'operation-dropdown.png', { element: '#container' }); + }); + + test('Dropdown Treeview should have no empty space', async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + await page.locator('#container .dx-filterbuilder-action-icon').first().click(); + + await testScreenshot(page, 'dropdown-space.png', { element: '#container' }); + }); + + [ + { dataType: 'date' as const, value: 1740441600000 }, + { dataType: 'date' as const, value: '2025-02-25T00:00:00.000Z' }, + { dataType: 'date' as const, value: new Date('2025-02-25T00:00:00.000Z') }, + { dataType: 'datetime' as const, value: 1740441600000 }, + { dataType: 'datetime' as const, value: '2025-02-25T00:00:00.000Z' }, + { dataType: 'datetime' as const, value: new Date('2025-02-25T00:00:00.000Z') }, + ].forEach(({ dataType, value }) => { + test.skip(`item value text should be correct for dataType: ${dataType} and valueType: ${typeof value}`, async ({ page }) => { + await createWidget(page, 'dxFilterBuilder', { + fields: [ + { + dataField: 'field1', + dataType, + }, + ], + value: ['field1', '=', value], + }); + + const date = new Date(value); + const dateString = date.toLocaleDateString(); + const timeString = date.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true, minute: '2-digit' }); + + const expectedValue = dataType === 'date' ? dateString : `${dateString}, ${timeString}`; + + const valueText = await page.locator('#container .dx-filterbuilder-item-value-text').first().textContent(); + expect(valueText).toBe(expectedValue); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts new file mode 100644 index 000000000000..8d156c48dea9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container-extended.html')}`; + +test.describe('Gantt', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TOOLBAR_ITEM_BUTTON = '.dx-button'; + + const data = { + tasks: [{ + id: 1, + parentId: 0, + title: 'Software Development', + start: new Date('2019-02-21T05:00:00.000Z'), + end: new Date('2019-07-04T12:00:00.000Z'), + progress: 31, + color: 'red', + }, { + id: 2, + parentId: 1, + title: 'Scope', + start: new Date('2019-02-21T05:00:00.000Z'), + end: new Date('2019-02-26T09:00:00.000Z'), + progress: 60, + }, { + id: 3, + parentId: 2, + title: 'Determine project scope', + start: new Date('2019-02-21T05:00:00.000Z'), + end: new Date('2019-02-21T09:00:00.000Z'), + progress: 100, + }, { + id: 4, + parentId: 2, + title: 'Secure project sponsorship', + start: new Date('2019-02-21T10:00:00.000Z'), + end: new Date('2019-02-22T09:00:00.000Z'), + progress: 100, + }, { + id: 5, + parentId: 2, + title: 'Define preliminary resources', + start: new Date('2019-02-22T10:00:00.000Z'), + end: new Date('2019-02-25T09:00:00.000Z'), + progress: 60, + }, { + id: 6, + parentId: 2, + title: 'Secure core resources', + start: new Date('2019-02-25T10:00:00.000Z'), + end: new Date('2019-02-26T09:00:00.000Z'), + progress: 0, + }, { + id: 7, + parentId: 2, + title: 'Scope complete', + start: new Date('2019-02-26T09:00:00.000Z'), + end: new Date('2019-02-26T09:00:00.000Z'), + progress: 0, + }], + + dependencies: [{ + id: 0, + predecessorId: 1, + successorId: 2, + type: 0, + }, { + id: 1, + predecessorId: 2, + successorId: 3, + type: 0, + }, { + id: 2, + predecessorId: 3, + successorId: 4, + type: 0, + }, { + id: 3, + predecessorId: 4, + successorId: 5, + type: 0, + }, { + id: 4, + predecessorId: 5, + successorId: 6, + type: 0, + }, { + id: 5, + predecessorId: 6, + successorId: 7, + type: 0, + }], + + resources: [{ + id: 1, text: 'Management', + }, { + id: 2, text: 'Project Manager', + }, { + id: 3, text: 'Deployment Team', + }], + + resourceAssignments: [{ + id: 0, taskId: 3, resourceId: 1, + }, { + id: 1, taskId: 4, resourceId: 1, + }, { + id: 2, taskId: 5, resourceId: 2, + }, { + id: 3, taskId: 6, resourceId: 2, + }, { + id: 4, taskId: 6, resourceId: 3, + }], + }; + + test('Gantt - show resources button should not have focus state (T1264485)', async ({ page }) => { + const id = `gantt-${Date.now()}`; + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxGantt', { + tasks: { dataSource: data.tasks }, + toolbar: { items: ['showResources'] }, + dependencies: { dataSource: data.dependencies }, + resources: { dataSource: data.resources }, + resourceAssignments: { dataSource: data.resourceAssignments }, + }, `#${id}`); + + await page.locator(TOOLBAR_ITEM_BUTTON).first().click(); + await testScreenshot(page, 'Gantt show resourced.png', { element: '#container' }); + }); + + test('Gantt - show dependencies button should not have focus state (T1264485)', async ({ page }) => { + const id = `gantt-${Date.now()}`; + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxGantt', { + tasks: { dataSource: data.tasks }, + toolbar: { items: ['showDependencies'] }, + dependencies: { dataSource: data.dependencies }, + resources: { dataSource: data.resources }, + resourceAssignments: { dataSource: data.resourceAssignments }, + }, `#${id}`); + + await page.locator(TOOLBAR_ITEM_BUTTON).first().click(); + await testScreenshot(page, 'Gantt show dependencies.png', { element: '#container' }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts new file mode 100644 index 000000000000..b2ce6457b815 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts @@ -0,0 +1,241 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot, appendElementTo } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Icons', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const ICON_CLASS = 'dx-icon'; + const iconSet = { + add: '\f00b', + airplane: '\f000', + bookmark: '\f017', + box: '\f018', + car: '\f01b', + card: '\f019', + cart: '\f01a', + chart: '\f01c', + check: '\f005', + clear: '\f008', + clock: '\f01d', + close: '\f00a', + coffee: '\f02a', + comment: '\f01e', + doc: '\f021', + file: '\f021', + download: '\f022', + dragvertical: '\f038', + edit: '\f023', + email: '\f024', + event: '\f026', + eventall: '\f043', + favorites: '\f025', + find: '\f027', + filter: '\f050', + folder: '\f028', + activefolder: '\f028', + food: '\f029', + gift: '\f02b', + globe: '\f02c', + group: '\f02e', + help: '\f02f', + home: '\f030', + image: '\f031', + info: '\f032', + key: '\f033', + like: '\f034', + lock: '\f035', + login: '\f036', + map: '\f037', + menu: '\f00c', + message: '\f024', + money: '\f039', + music: '\f03b', + overflow: '\f00d', + percent: '\f03c', + photo: '\f03d', + pin: '\f03e', + pinleft: '\f04e', + pinright: '\f04d', + preferences: '\f03f', + product: '\f040', + pulldown: '\f062', + refresh: '\f041', + remove: '\f00a', + revert: '\f04c', + runner: '\f042', + save: '\f044', + search: '\f027', + selectall: '\f048', + square: '\f045', + spindown: '\f001', + spinleft: '\f002', + spinprev: '\f002', + spinright: '\f003', + spinnext: '\f003', + spinup: '\f004', + star: '\f025', + tags: '\f009', + tel: '\f046', + tips: '\f004', + todo: '\f005', + toolbox: '\f047', + trash: '\f03a', + user: '\f02d', + unselectall: '\f049', + upload: '\f006', + videocam: '\f04a', + arrowleft: '\f011', + arrowright: '\f012', + arrowdown: '\f015', + arrowup: '\f013', + back: '\f04b', + collapse: '\f020', + copy: '\f015a', + cut: '\f016a', + paste: '\f017a', + expand: '\f01f', + exportxlsx: '\f051', + exportpdf: '\f052', + exportselected: '\f053', + bold: '\f054', + italic: '\f055', + underline: '\f056', + strike: '\f057', + indent: '\f058', + increaselinespacing: '\f059', + font: '\f05a', + fontsize: '\f05b', + shrinkfont: '\f05c', + growfont: '\f05d', + color: '\f05e', + background: '\f05f', + fill: '\f060', + palette: '\f061', + superscript: '\f06a', + subscript: '\f06b', + header: '\f06c', + blockquote: '\f06d', + formula: '\f06e', + codeblock: '\f06f', + orderedlist: '\f070', + bulletlist: '\f071', + increaseindent: '\f072', + decreaseindent: '\f073', + decreaselinespacing: '\f074', + alignleft: '\f075', + aligncenter: '\f076', + alignright: '\f077', + alignjustify: '\f078', + separator: '\f079', + fullscreen: '\f11a', + hierarchy: '\f11b', + undo: '\f07a', + redo: '\f07b', + clearformat: '\f07c', + accountbox: '\f07d', + link: '\f07e', + variable: '\f07f', + detailslayout: '\f080', + contentlayout: '\f081', + smalliconslayout: '\f082', + mediumiconslayout: '\f083', + image2: '\f084', + mention: '\f085', + to: '\f086', + insertrowabove: '\f087', + insertrowbelow: '\f088', + insertcolumnleft: '\f089', + insertcolumnright: '\f08a', + addrowabove: '\f08b', + addrowbelow: '\f08c', + addcolumnleft: '\f08d', + addcolumnright: '\f08e', + deleterow: '\f08f', + deletecolumn: '\f090', + deletetable: '\f091', + cellproperties: '\f092', + tableproperties: '\f093', + inserttable: '\f094', + tableoptions: '\f095', + }; + + test('Icon set', async ({ page }) => { + await page.evaluate(({ iconSetData, iconClass }) => { + const container = document.querySelector('#container'); + if (!container) return; + + const fragment = document.createDocumentFragment(); + + for (const [name] of Object.entries(iconSetData)) { + const div = document.createElement('div'); + div.style.display = 'flex'; + div.style.alignItems = 'center'; + div.style.marginBottom = '2px'; + + const iconSpan = document.createElement('span'); + iconSpan.className = `${iconClass} ${iconClass}-${name}`; + iconSpan.style.fontSize = '24px'; + iconSpan.style.marginRight = '10px'; + + const labelSpan = document.createElement('span'); + labelSpan.textContent = name; + + div.appendChild(iconSpan); + div.appendChild(labelSpan); + fragment.appendChild(div); + } + + container.append(fragment); + }, { iconSetData: iconSet, iconClass: ICON_CLASS }); + + await testScreenshot(page, 'Icon set.png'); + }); + + test('SVG icon set', async ({ page }) => { + await page.evaluate(() => { + const container = document.querySelector('#container'); + if (!container) return; + + const svgIcons = [ + 'dx-icon-rowfield', 'dx-icon-columnfield', 'dx-icon-datafield', + 'dx-icon-fields', 'dx-icon-fieldchooser', + ]; + + const fragment = document.createDocumentFragment(); + + svgIcons.forEach((iconClass) => { + const div = document.createElement('div'); + div.style.display = 'flex'; + div.style.alignItems = 'center'; + div.style.marginBottom = '2px'; + div.style.height = '30px'; + + const iconSpan = document.createElement('span'); + iconSpan.className = `dx-icon ${iconClass}`; + iconSpan.style.fontSize = '24px'; + iconSpan.style.marginRight = '10px'; + + const labelSpan = document.createElement('span'); + labelSpan.textContent = iconClass; + + div.appendChild(iconSpan); + div.appendChild(labelSpan); + fragment.appendChild(div); + }); + + container.append(fragment); + }); + + await testScreenshot(page, 'SVG icon set.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.spec.ts new file mode 100644 index 000000000000..4721c52f85c4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Pagination', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['full', 'compact'].forEach((displayMode) => { + [undefined, 'Total {2} items. Page {0} of {1}'].forEach((infoText) => { + [true, false].forEach((showInfo) => { + [true, false].forEach((showNavigationButtons) => { + [true, false].forEach((showPageSizeSelector) => { + test(`Pagination dm_${displayMode}-` + + `${infoText ? 'has' : 'has_no'}_it-` + + `si_${showInfo.toString()}-` + + `snb_${showNavigationButtons.toString()}-` + + `spss_${showPageSizeSelector.toString()}`, async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + displayMode, + infoText, + showInfo, + showNavigationButtons, + showPageSizeSelector, + }); + + await testScreenshot(page, + `pagination-dm_${displayMode}-` + + `${infoText ? 'has' : 'has_no'}_it-` + + `si_${showInfo.toString()}-` + + `snb_${showNavigationButtons.toString()}-` + + `spss_${showPageSizeSelector.toString()}` + + '.png', + ); + + }); + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts new file mode 100644 index 000000000000..965a2bfbb413 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Pagination Base Properties', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Pagination width and height property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + width: 270, + height: '95px', + itemCount: 50, + }); + + const pagination = page.locator('#container'); + const width = await pagination.evaluate((el) => getComputedStyle(el).width); + const height = await pagination.evaluate((el) => getComputedStyle(el).height); + expect(width).toBe('270px'); + expect(height).toBe('95px'); + expect(await pagination.getAttribute('width')).toBeNull(); + expect(await pagination.getAttribute('height')).toBeNull(); + }); + + test('Pagination elementAttr property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + elementAttr: { + 'aria-label': 'some description', + 'data-test': 'custom data', + }, + }); + + const pagination = page.locator('#container'); + expect(await pagination.getAttribute('aria-label')).toBe('some description'); + expect(await pagination.getAttribute('data-test')).toBe('custom data'); + }); + + test('Pagination hint and accessKey properties', async ({ page }) => { + await createWidget(page, 'dxPagination', { + hint: 'Best Pagination', + accessKey: 'F', + itemCount: 50, + focusStateEnabled: true, + }); + + const pagination = page.locator('#container'); + expect(await pagination.getAttribute('accesskey')).toBe('F'); + expect(await pagination.getAttribute('title')).toBe('Best Pagination'); + }); + + test('Pagination disabled property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + disabled: true, + itemCount: 50, + }); + + const pagination = page.locator('#container'); + expect(await pagination.getAttribute('aria-disabled')).toBe('true'); + expect(await pagination.evaluate((el) => el.classList.contains('dx-state-disabled'))).toBe(true); + }); + + test('Pagination tabindex and state properties', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + disabled: false, + width: '100%', + focusStateEnabled: true, + hoverStateEnabled: true, + activeStateEnabled: true, + tabIndex: 7, + }); + + const pagination = page.locator('#container'); + expect(await pagination.getAttribute('tabindex')).toBe('7'); + + await pagination.locator('.dx-page').filter({ hasText: '3' }).click(); + expect(await pagination.evaluate((el) => el.classList.contains('dx-state-focused'))).toBe(true); + }); + + test('Pagination focus method & accessKey propery without focusStateEnabled', async ({ page }) => { + await createWidget(page, 'dxPagination', { + focusStateEnabled: false, + accessKey: 'F', + itemCount: 50, + }); + + const pagination = page.locator('#container'); + expect(await pagination.getAttribute('accesskey')).toBeNull(); + + await page.evaluate(() => { + ($('#container') as any).dxPagination('instance').focus(); + }); + + const pageSizeElement = pagination.locator('.dx-page-size').first(); + await expect(pageSizeElement).toBeFocused(); + }); + + test('Pagination focus method with focusStateEnabled', async ({ page }) => { + await createWidget(page, 'dxPagination', { + focusStateEnabled: true, + itemCount: 50, + }); + + const pagination = page.locator('#container'); + await expect(pagination).not.toBeFocused(); + + await page.evaluate(() => { + ($('#container') as any).dxPagination('instance').focus(); + }); + + await expect(pagination).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts new file mode 100644 index 000000000000..db5349f387df --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Pagination Base Properties', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Pagination visibile property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + visible: false, + }); + + const pagination = page.locator('#container'); + expect(await pagination.evaluate((el) => el.classList.contains('dx-state-invisible'))).toBe(true); + }); + + test('PageSize selector test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 2, + pageSize: 8, + allowedPageSizes: [2, 4, 8], + }); + + const pagination = page.locator('#container'); + await pagination.locator('.dx-page-size').nth(1).click(); + + const pageCount = await page.evaluate(() => + ($('#container') as any).dxPagination('instance').option('pageCount'), + ); + expect(pageCount).toBe(13); + }); + + test('PageIndex test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 1, + pageSize: 5, + }); + + const pageIndex = await page.evaluate(() => + ($('#container') as any).dxPagination('instance').option('pageIndex'), + ); + expect(pageIndex).toBe(1); + + await page.locator('.dx-page').filter({ hasText: '5' }).click(); + + const newPageIndex = await page.evaluate(() => + ($('#container') as any).dxPagination('instance').option('pageIndex'), + ); + expect(newPageIndex).toBe(5); + }); + + test('PageIndex correction test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 10, + pageSize: 5, + }); + + const pageIndex = await page.evaluate(() => + ($('#container') as any).dxPagination('instance').option('pageIndex'), + ); + expect(pageIndex).toBe(10); + + await page.locator('#container .dx-page-size').nth(1).click(); + + const newPageIndex = await page.evaluate(() => + ($('#container') as any).dxPagination('instance').option('pageIndex'), + ); + expect(newPageIndex).toBe(5); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/contextMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/contextMenu.spec.ts new file mode 100644 index 000000000000..3e384ab195da --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/contextMenu.spec.ts @@ -0,0 +1,105 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('PivotGrid_contextMenu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const CONTEXT_MENU_CLASS = 'dx-context-menu'; + const FIELD_CHOOSER_AREA_FIELDS_CLASS = 'dx-area-fields'; + + test('ContextMenu width should be adjusted to the width of the item text (T1106236)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + width: 1000, + allowSortingBySummary: true, + allowSorting: true, + allowExpandAll: true, + showBorders: true, + fieldChooser: { + enabled: false, + }, + fieldPanel: { + showFilterFields: false, + allowFieldDragging: false, + visible: true, + }, + onContextMenuPreparing(e) { + if (e.field?.dataField === 'amount') { + const menuItems = [] as any; + + e.items.push({ text: 'Summary Type', items: menuItems }); + ['Sum', 'Avg', 'Min', 'Max'].forEach((summaryType) => { + const summaryTypeValue = summaryType.toLowerCase(); + const text = summaryType === 'Min' + ? 'Min - The box is too narrow, the item text does not fit inside.' + : summaryType; + menuItems.push({ + text, + value: summaryType.toLowerCase(), + selected: e.field.summaryType === summaryTypeValue, + }); + }); + } + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'year', + expanded: true, + }, { + caption: 'Relative Sales', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + area: 'data', + summaryDisplayMode: 'percentOfColumnGrandTotal', + }], + store: [{ + id: 10887, + region: 'Africa', + country: 'Egypt', + city: 'Cairo', + amount: 500, + date: new Date('2015-05-26'), + }, { + id: 10888, + region: 'South America', + country: 'Argentina', + city: 'Buenos Aires', + amount: 780, + date: '2015-05-07', + }], + }, + }); + + await page.locator(`.${FIELD_CHOOSER_AREA_FIELDS_CLASS}`).nth(1).click({ button: 'right' }); + + await page.locator(`.${CONTEXT_MENU_CLASS}`).hover(); + + await testScreenshot(page, 'PivotGrid contextmenu width.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.spec.ts new file mode 100644 index 000000000000..d322e752ef17 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('PivotGrid_export', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should call \'onExporting\' when export button clicked', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [{ + caption: 'data A', + dataField: 'data_A', + }], + store: [], + }, + export: { + enabled: true, + }, + onExporting() { + (window as any).__exportCalled = true; + }, + }); + + await page.locator('#container .dx-pivotgrid-export-button').click(); + + const exportCalled = await page.evaluate(() => (window as any).__exportCalled as boolean); + expect(exportCalled).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/T1138119_dragAndDropAreaItems.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/T1138119_dragAndDropAreaItems.spec.ts new file mode 100644 index 000000000000..b1f748b21fcd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/T1138119_dragAndDropAreaItems.spec.ts @@ -0,0 +1,135 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe.skip('pivotGrid_fieldChooser_drag-and-drop_T1138119 ', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Drag-n-drop the tree view item in all directions', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + store: [{ + id: 0, + data_0: 'data_0', + data_1: 'data_1', + data_2: 'data_2', + data_3: 'data_3', + data_4: 'data_4', + data_5: 'data_5', + data_6: 'data_6', + data_7: 'data_7', + data_8: 'data_8', + data_9: 'data_9', + data_10: 'data_10', + data_11: 'data_11', + data_12: 'data_12', + }], + }, + fieldChooser: { + enabled: true, + }, + }); + + const pivotGrid = page.locator('#container'); + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const treeView = fieldChooser.getTreeView(); + const treeViewNodeItem = treeView.getNodeItem(); + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await drag(treeViewNodeItem, 0, -30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_top.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await drag(treeViewNodeItem, 30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_right.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await drag(treeViewNodeItem, 0, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_bottom.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await drag(treeViewNodeItem, -30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_left.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); + + test('Drag-n-drop the row area item in all directions', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [{ + caption: 'Data_0', + dataField: 'data_0', + area: 'row', + }, + { + caption: 'Data_1', + dataField: 'data_1', + area: 'row', + }, + { + caption: 'Data_2', + dataField: 'data_2', + area: 'row', + }, + { + caption: 'Data_3', + dataField: 'data_3', + area: 'row', + }, + { + caption: 'Data_4', + dataField: 'data_4', + area: 'row', + }], + store: [], + }, + fieldChooser: { + enabled: true, + }, + }); + + const pivotGrid = page.locator('#container'); + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const rowAreaItem = fieldChooser.getRowAreaItem(); + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await drag(rowAreaItem, 0, -30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_top.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await drag(rowAreaItem, 30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_right.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await drag(rowAreaItem, 0, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_bottom.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await drag(rowAreaItem, -30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_left.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts new file mode 100644 index 000000000000..3355af398513 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts @@ -0,0 +1,516 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, PivotGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const sales = [ + { + region: 'North America', city: 'New York', date: '2013/01/06', amount: 1740, + }, + { + region: 'North America', city: 'Los Angeles', date: '2013/02/06', amount: 2295, + }, + { + region: 'Europe', city: 'London', date: '2013/07/01', amount: 1190, + }, + { + region: 'Asia', city: 'Tokyo', date: '2014/01/01', amount: 1445, + }, +]; + +test.describe('PivotGrid_fieldChooser', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Change dataFiels order with one invisible field (T1079461)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + sortBySummaryField: 'Total', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(page); + await pivotGrid.getFieldChooserButton().click(); + + const fieldChooserOverlay = page.locator('.dx-overlay-content.dx-popup-draggable'); + await expect(fieldChooserOverlay).toBeVisible(); + + const treeViewCheckboxes = fieldChooserOverlay.locator('.dx-treeview .dx-checkbox'); + await treeViewCheckboxes.nth(0).click(); + await treeViewCheckboxes.nth(1).click(); + + const dataFields = fieldChooserOverlay.locator('.dx-area-fields[data-group="data"] .dx-area-field'); + const firstField = dataFields.nth(0); + const box = await firstField.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 + 170, { steps: 10 }); + await page.mouse.up(); + } + + await testScreenshot(page, 'FieldChooser change dataField order with invisible fields.png', { element: '.dx-overlay-content.dx-popup-draggable' }); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Change dataFiels order with two invisible fields', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total Hidden 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(page); + await pivotGrid.getFieldChooserButton().click(); + + const fieldChooserOverlay = page.locator('.dx-overlay-content.dx-popup-draggable'); + await expect(fieldChooserOverlay).toBeVisible(); + + const treeViewCheckboxes = fieldChooserOverlay.locator('.dx-treeview .dx-checkbox'); + await treeViewCheckboxes.nth(0).click(); + await treeViewCheckboxes.nth(1).click(); + + const dataFields = fieldChooserOverlay.locator('.dx-area-fields[data-group="data"] .dx-area-field'); + const firstField = dataFields.nth(0); + const box = await firstField.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 + 170, { steps: 10 }); + await page.mouse.up(); + } + + await testScreenshot(page, 'FieldChooser change dataField order with two invisible fields.png', { element: '.dx-overlay-content.dx-popup-draggable' }); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Change dataFiels order with three invisible fields (T1079461)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(page); + await pivotGrid.getFieldChooserButton().click(); + + const fieldChooserOverlay = page.locator('.dx-overlay-content.dx-popup-draggable'); + await expect(fieldChooserOverlay).toBeVisible(); + + const treeViewCheckboxes = fieldChooserOverlay.locator('.dx-treeview .dx-checkbox'); + await treeViewCheckboxes.nth(0).click(); + + const dataFields = fieldChooserOverlay.locator('.dx-area-fields[data-group="data"] .dx-area-field'); + const firstField = dataFields.nth(0); + const box = await firstField.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 + 170, { steps: 10 }); + await page.mouse.up(); + } + + await testScreenshot(page, 'FieldChooser change dataField order with three invisible fields.png', { element: '.dx-overlay-content.dx-popup-draggable' }); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Change dataFiels order when applyChangesMode is "onDemand" (T1097764)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + applyChangesMode: 'onDemand', + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(page); + await pivotGrid.getFieldChooserButton().click(); + + const fieldChooserOverlay = page.locator('.dx-overlay-content.dx-popup-draggable'); + await expect(fieldChooserOverlay).toBeVisible(); + + const dataFields = fieldChooserOverlay.locator('.dx-area-fields[data-group="data"] .dx-area-field'); + const initialCount = await dataFields.count(); + expect(initialCount).toBe(3); + + const treeViewCheckboxes = fieldChooserOverlay.locator('.dx-treeview .dx-checkbox'); + await treeViewCheckboxes.nth(1).click(); + + const updatedCount = await dataFields.count(); + expect(updatedCount).toBe(4); + + const firstField = dataFields.nth(0); + const box = await firstField.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 + 150, { steps: 10 }); + await page.mouse.up(); + } + + const finalCount = await dataFields.count(); + expect(finalCount).toBe(4); + }); + + test('Field chooser can be clicked (T1290333)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + showBorders: true, + fieldPanel: { + showFilterFields: false, + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'date', + dataType: 'date', + area: 'column', + }], + store: [], + }, + }); + + const pivotGrid = new PivotGrid(page); + await pivotGrid.getFieldChooserButton().click(); + + const fieldChooserOverlay = page.locator('.dx-overlay-content.dx-popup-draggable'); + await expect(fieldChooserOverlay).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts new file mode 100644 index 000000000000..1d241a22273d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, PivotGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe.skip('pivotGrid_olap_drag-n-drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [true, false].forEach((showRowGrandTotals) => { + test(`Empty table has one ${showRowGrandTotals ? 'total' : 'empty'} row after drag-n-drop for paginated data`, async ({ page }) => { + const paginatedData = Array.from({ length: 30 }, (_, i) => ({ + region: `Region ${Math.floor(i / 5)}`, + city: `City ${i}`, + amount: (i + 1) * 100, + })); + + await createWidget(page, 'dxPivotGrid', { + showBorders: true, + showRowGrandTotals, + fieldPanel: { + visible: true, + allowFieldDragging: true, + showColumnFields: true, + showRowFields: true, + showDataFields: true, + showFilterFields: true, + }, + dataSource: { + fields: [{ + dataField: 'region', + area: 'row', + }, { + dataField: 'amount', + area: 'data', + summaryType: 'sum', + }], + store: paginatedData, + }, + }); + + const pivotGrid = new PivotGrid(page); + const fieldPanel = pivotGrid.getFieldPanel(); + const rowArea = fieldPanel.getRowArea(); + const filterArea = fieldPanel.getFilterArea(); + + const regionField = fieldPanel.getFieldItem(rowArea); + const regionBox = await regionField.boundingBox(); + const filterBox = await filterArea.boundingBox(); + + if (regionBox && filterBox) { + await page.mouse.move( + regionBox.x + regionBox.width / 2, + regionBox.y + regionBox.height / 2, + ); + await page.mouse.down(); + await page.mouse.move( + filterBox.x + filterBox.width / 2, + filterBox.y + filterBox.height / 2, + { steps: 10 }, + ); + await page.mouse.up(); + } + + await page.waitForTimeout(500); + + const dataArea = pivotGrid.getDataArea(); + const dataRows = dataArea.locator('tr'); + const rowCount = await dataRows.count(); + expect(rowCount).toBe(1); + + await testScreenshot(page, `olap-drag-drop-empty-table-showRowGrandTotals-${showRowGrandTotals}.png`, { + element: '#container', + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts new file mode 100644 index 000000000000..7f4a5b65bf5d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, PivotGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_fieldPanel_aria_label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Header fields should have correct aria-label', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + allowFiltering: true, + fieldPanel: { + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'row1', + area: 'row', + }, { + dataField: 'row2', + area: 'row', + }, { + dataField: 'column1', + area: 'column', + }, { + dataField: 'column2', + area: 'column', + }, { + dataField: 'column3', + area: 'filter', + }], + store: [], + }, + }); + + const pivotGrid = new PivotGrid(page, PIVOT_GRID_SELECTOR); + const rowHeader = pivotGrid.getRowHeaderArea(); + const columnHeader = pivotGrid.getColumnHeaderArea(); + + await expect(rowHeader.getHeaderFilterIcon(0)).toHaveAttribute('aria-label', "Show filter options for column 'Row1'"); + await expect(rowHeader.getHeaderFilterIcon(1)).toHaveAttribute('aria-label', "Show filter options for column 'Row2'"); + await expect(columnHeader.getHeaderFilterIcon(0)).toHaveAttribute('aria-label', "Show filter options for column 'Column1'"); + await expect(columnHeader.getHeaderFilterIcon(1)).toHaveAttribute('aria-label', "Show filter options for column 'Column2'"); + + const filterArea = page.locator(`${PIVOT_GRID_SELECTOR} .dx-area-filter-cell`); + await expect(filterArea.locator('.dx-header-filter').nth(0)).toHaveAttribute('aria-label', "Show filter options for column 'Column3'"); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/dragAndDropFieldItems.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/dragAndDropFieldItems.spec.ts new file mode 100644 index 000000000000..570691764413 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/dragAndDropFieldItems.spec.ts @@ -0,0 +1,126 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe.skip('pivotGrid_fieldPanel_drag-n-drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + test('Field panel items markup in the middle of the drag-n-drop', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + allowSorting: true, + allowFiltering: true, + fieldPanel: { + showColumnFields: true, + showDataFields: true, + showFilterFields: true, + showRowFields: true, + allowFieldDragging: true, + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + dataField: 'countA', + area: 'row', + }, { + dataField: 'countB', + area: 'row', + }, { + dataField: 'countC', + area: 'data', + }], + store: [{ + id: 0, + countA: 1, + countB: 1, + countC: 1, + date: '2013/01/13', + }], + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const columnFirstAction = pivotGrid.getColumnHeaderArea().getField(); + const rowFirstAction = pivotGrid.getRowHeaderArea().getField(); + const dataFirstAction = pivotGrid.getDataHeaderArea().getField(); + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await drag(columnFirstAction, 30, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-panel_column-action_dnd.png', { element: pivotGrid.element }); + await columnFirstAction.dispatchEvent('mouseup'); + + await drag(rowFirstAction, 30, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-panel_row-action_dnd.png', { element: pivotGrid.element }); + await columnFirstAction.dispatchEvent('mouseup'); + + await drag(dataFirstAction, 30, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-panel_data-action_dnd.png', { element: pivotGrid.element }); + await columnFirstAction.dispatchEvent('mouseup'); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); + + test('Should show d-n-d indicator during drag to first place in columns fields', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + showBorders: true, + fieldPanel: { + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'row1', + area: 'row', + }, { + dataField: 'row2', + area: 'row', + }, { + dataField: 'column1', + area: 'column', + }, { + dataField: 'column2', + area: 'column', + }], + store: [], + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const rowFirstField = pivotGrid.getRowHeaderArea().getField(); + const columnHeaderAreaElement = pivotGrid.getColumnHeaderArea().element; + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + const rowFirsFieldX = await rowFirstField.offsetLeft; + const rowFirsFieldY = await rowFirstField.offsetTop; + const columnHeaderX = await columnHeaderAreaElement.offsetLeft; + const columnHeaderY = await columnHeaderAreaElement.offsetTop; + const deltaOffsetX = 20; + const dragOffsetX = columnHeaderX - rowFirsFieldX - deltaOffsetX; + const dragOffsetY = rowFirsFieldY - columnHeaderY; + + await drag(rowFirstField, dragOffsetX, dragOffsetY, DRAG_MOUSE_OPTIONS); + + await testScreenshot(page, 'field-panel_column-field_dnd-first.png', { element: pivotGrid.element }); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts new file mode 100644 index 000000000000..d5779df892b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, PivotGrid, HeaderFilter } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const sales = [ + { region: 'North America', date: '2015/01/01', amount: 1740 }, + { region: 'North America', date: '2015/02/01', amount: 2295 }, + { region: 'Europe', date: '2015/01/01', amount: 1190 }, + { region: 'Europe', date: '2015/02/01', amount: 1060 }, + { region: 'Asia', date: '2015/01/01', amount: 1445 }, + { region: 'Asia', date: '2015/02/01', amount: 1455 }, +]; + +test.describe('pivotGrid_headerFilter', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Header filter popup', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSorting: true, + allowFiltering: true, + fieldPanel: { + showColumnFields: true, + showDataFields: true, + showFilterFields: true, + showRowFields: true, + allowFieldDragging: true, + visible: true, + }, + headerFilter: { + allowSearch: true, + }, + dataSource: { + fields: [{ + dataField: 'region', + area: 'column', + }, { + dataField: 'date', + area: 'row', + }, { + dataField: 'amount', + area: 'data', + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(page); + await pivotGrid.getColumnHeaderArea().getHeaderFilterIcon().click(); + + await testScreenshot(page, 'headerFilter - before scroll.png'); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('[T1284200] Should handle dxList "selectAll" when has unselected items on the second page', async ({ page }) => { + const largeData = Array.from({ length: 100 }, (_, i) => ({ + region: `Region ${i}`, + date: '2015/01/01', + amount: i * 100, + })); + + await createWidget(page, 'dxPivotGrid', { + allowSorting: true, + allowFiltering: true, + headerFilter: { + allowSearch: true, + }, + dataSource: { + fields: [{ + dataField: 'region', + area: 'row', + }, { + dataField: 'amount', + area: 'data', + summaryType: 'sum', + }], + store: largeData, + }, + }); + + const pivotGrid = new PivotGrid(page); + await pivotGrid.getRowHeaderArea().getHeaderFilterIcon().click(); + + const headerFilter = new HeaderFilter(page); + const list = headerFilter.getList(); + + const firstItem = list.getItem(0); + const firstCheckbox = firstItem.locator('.dx-checkbox'); + await firstCheckbox.click(); + + const selectAll = list.getSelectAll(); + await selectAll.checkBox.click(); + + const isChecked = await selectAll.isChecked(); + expect(isChecked).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/runningTotal/runningTotal.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/runningTotal/runningTotal.spec.ts new file mode 100644 index 000000000000..183bda148402 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/runningTotal/runningTotal.spec.ts @@ -0,0 +1,153 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, PivotGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('PivotGrid: running total', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + const seamlessData = [ + { + month: 'A', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'B', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'C', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'A', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + { + month: 'B', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + { + month: 'C', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + ]; + + const partialData = [ + { + month: 'A', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'B', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + { + month: 'C', + value: 3, + first_row: '2_0', + second_row: '2_1', + }, + ]; + + test('Should correctly sum cells values with runningTotal', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [ + { + dataField: 'first_row', + area: 'row', + expanded: true, + }, + { + dataField: 'second_row', + area: 'row', + }, + { + dataField: 'value', + dataType: 'number', + summaryType: 'sum', + area: 'data', + runningTotal: 'row', + }, + { + dataField: 'month', + area: 'column', + }, + ], + store: seamlessData, + }, + }); + + const container = page.locator(PIVOT_GRID_SELECTOR); + + await testScreenshot(page, 'running-total_seamless-data.png', { element: container }); + + }); + + test('Should correctly sum cells values with runningTotal with partial data (T1144885)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [ + { + dataField: 'first_row', + area: 'row', + expanded: true, + }, + { + dataField: 'second_row', + area: 'row', + }, + { + dataField: 'value', + dataType: 'number', + summaryType: 'sum', + area: 'data', + runningTotal: 'row', + }, + { + dataField: 'month', + area: 'column', + }, + ], + store: partialData, + }, + }); + + const container = page.locator(PIVOT_GRID_SELECTOR); + + await testScreenshot(page, 'running-total_partial-data_render-0.png', { element: container }); + + const rowToCollapse = page.locator(`${PIVOT_GRID_SELECTOR} .dx-pivotgrid-vertical-headers td`).nth(3); + await rowToCollapse.click(); + + await testScreenshot(page, 'running-total_partial-data_render-1.png', { element: container }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts new file mode 100644 index 000000000000..e844b35e9ebf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts @@ -0,0 +1,228 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, generateOptionMatrix } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe.skip('PivotGrid_scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { useNative: true, mode: 'standart' }, + { useNative: false, mode: 'standart' }, + ].forEach(({ useNative, mode }) => { + test(`Rows syncronization with vertical scrollbar when scrolling{useNative=${useNative},mode=${mode}} and white-space cell is normal (T1081956)`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-pivotgrid .dx-pivotgrid-area-data tbody td { white-space: normal !important; }'); + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + store: virtualData, + retrieveFields: false, + fields: [{ + area: 'data', + dataType: 'string', + summaryType: 'custom', + calculateCustomSummary(options) { + if (options.summaryProcess === 'calculate') { + const item = options.value; + options.totalValue = `
${item.value}
`; + } + }, + }, { + dataField: 'y1path', + area: 'row', + width: 200, + expanded: true, + }, { + dataField: 'y2code', + area: 'row', + width: dataOptions.data.y2.visible ? undefined : 1, + }, { + dataField: 'x1code', + area: 'column', + expanded: true, + }], + }, + encodeHtml: false, + showColumnTotals: false, + height: 400, + width: 1200, + scrolling: { + mode, + useNative, + }, + }); + + + const pivotGrid = page.locator('#container'); + + await pivotGrid.scrollBy({ top: 300000 }); + await pivotGrid.scrollBy({ top: 100000 }); + await pivotGrid.scrollBy({ top: -150 }); + + await testScreenshot(page, `PivotGrid rows sync dir=vertical,useNative=${useNative},mode=${mode}.png`, { element: '#container' }); + + }); + + test(`Rows syncronization with both scrollbars when scrolling{useNative=${useNative},mode=${mode}} and white-space cell is normal (T1081956)`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-pivotgrid .dx-pivotgrid-area-data tbody td { white-space: normal !important; }'); + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + store: virtualData, + retrieveFields: false, + fields: [{ + area: 'data', + dataType: 'string', + summaryType: 'custom', + calculateCustomSummary(options) { + if (options.summaryProcess === 'calculate') { + const item = options.value; + options.totalValue = `
${item.value}
`; + } + }, + }, { + dataField: 'y1path', + area: 'row', + width: 200, + expanded: true, + }, { + dataField: 'y2code', + area: 'row', + width: dataOptions.data.y2.visible ? undefined : 1, + }, { + dataField: 'x1code', + area: 'column', + expanded: true, + }], + }, + encodeHtml: false, + showColumnTotals: false, + height: 400, + width: 800, + scrolling: { + mode, + useNative, + }, + }); + + + const pivotGrid = page.locator('#container'); + + await pivotGrid.scrollBy({ top: 300000 }); + await pivotGrid.scrollBy({ top: 100000 }); + await pivotGrid.scrollBy({ top: -150 }); + + await testScreenshot(page, `PivotGrid rows sync dir=both,useNative=${useNative},mode=${mode}.png`, { element: '#container' }); + + }); + }); + + generateOptionMatrix({ + height: [450, 350], + useNative: [false, true], + }).forEach(({ height, useNative }) => { + test(`Rows content dont hide under vertical scrollbar when scrolling{useNative=${useNative}},height=100% (${height}px) (T1290313)`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, `#parentContainer { height: ${height}px; }`); + + await createWidget(page, 'dxPivotGrid', { + height: '100%', + showBorders: true, + scrolling: { + useNative, + mode: 'standard', + }, + dataSource: { + fields: [{ + dataField: 'rowField', + area: 'row', + }, { + dataField: 'dataField', + area: 'data', + }, { + dataField: 'dataField', + area: 'data', + }], + store: Array.from({ length: 9 }).map((_, id) => ({ + id, + rowField: id > 7 ? 'row '.repeat(id) : `row ${id}`, + dataField: 47, + })), + }, + }); + + + await testScreenshot(page, + `PivotGrid rows content height=100%(${height}px),useNative=${useNative}.png`, + { element: '#container' }, + ); + + }); + }); + + generateOptionMatrix({ + rtlEnabled: [false, true], + nativeScrolling: [false, true], + }).forEach(({ rtlEnabled, nativeScrolling }) => { + test(`Should set margin for scroll-bar correctly (T1214743), rtl=${rtlEnabled}, nativeScrolling=${nativeScrolling}`, async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + height: 400, + scrolling: { useNative: nativeScrolling }, + showBorders: true, + rtlEnabled, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + selector(data) { + return `${data.city} (${data.country})`; + }, + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + caption: 'Sales', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + }], + store: sales, + }, + }); + + const pivotGrid = page.locator('#container'); + + const firstCellToClick = pivotGrid.getRowsArea().getCell(1); + await click(firstCellToClick); + await testScreenshot(page, `scrollbar-margin_step-0_useNative-${nativeScrolling}_rtl-${rtlEnabled}`, { element: pivotGrid.element }); + + const secondCellToClick = pivotGrid.getRowsArea().getCell(6); + await click(secondCellToClick); + await testScreenshot(page, `scrollbar-margin_step-1_useNative-${nativeScrolling}_rtl-${rtlEnabled}`, { element: pivotGrid.element }); + + await click(secondCellToClick); + await testScreenshot(page, `scrollbar-margin_step-2_useNative-${nativeScrolling}_rtl-${rtlEnabled}`, { element: pivotGrid.element }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts new file mode 100644 index 000000000000..e5a2282bd214 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts @@ -0,0 +1,105 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, PivotGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const sales = [ + { + region: 'North America', city: 'New York', date: '2015/01/01', amount: 1740, + }, + { + region: 'North America', city: 'Los Angeles', date: '2015/02/01', amount: 2295, + }, + { + region: 'Europe', city: 'London', date: '2015/01/01', amount: 1190, + }, + { + region: 'Europe', city: 'Berlin', date: '2015/02/01', amount: 1060, + }, + { + region: 'Asia', city: 'Tokyo', date: '2015/01/01', amount: 1445, + }, + { + region: 'Asia', city: 'Shanghai', date: '2015/02/01', amount: 1455, + }, +]; + +test.describe('pivotGrid_sort', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should sort without DataSource reload if scrolling mode isn\'t virtual', async ({ page }) => { + let loadCount = 0; + + await page.exposeFunction('__pivotGridLoadCalled', () => { + loadCount += 1; + }); + + await createWidget(page, 'dxPivotGrid', { + allowSorting: true, + dataSource: { + fields: [{ + dataField: 'region', + area: 'row', + }, { + dataField: 'date', + area: 'column', + }, { + dataField: 'amount', + area: 'data', + summaryType: 'sum', + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(page); + loadCount = 0; + + const sortIcon = pivotGrid.getRowHeaderArea().element.locator('td').first(); + await sortIcon.click(); + + await page.waitForTimeout(500); + expect(loadCount).toBe(0); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Should sort with DataSource reload if scrolling mode is virtual', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSorting: true, + scrolling: { + mode: 'virtual', + }, + dataSource: { + fields: [{ + dataField: 'region', + area: 'row', + }, { + dataField: 'date', + area: 'column', + }, { + dataField: 'amount', + area: 'data', + summaryType: 'sum', + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(page); + + const sortIcon = pivotGrid.getRowHeaderArea().element.locator('td').first(); + await sortIcon.click(); + + await page.waitForTimeout(500); + + await expect(pivotGrid.element.locator('.dx-pivotgrid')).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts new file mode 100644 index 000000000000..580b38d32b42 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts @@ -0,0 +1,123 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, PivotGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_sort', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Should apply sort changes to the markup if the "summaryDisplayMode" is set', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowSorting: true, + fieldPanel: { + showFilterFields: false, + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'row', + area: 'row', + }, { + dataField: 'column', + area: 'column', + }, { + dataField: 'value', + dataType: 'number', + summaryType: 'sum', + area: 'data', + summaryDisplayMode: 'percentVariation', + }], + store: [ + { + row: 'row_A', + column: 'column_A', + value: 100, + }, + { + row: 'row_A', + column: 'column_A', + value: 100, + }, + { + row: 'row_A', + column: 'column_B', + value: 150, + }, + { + row: 'row_A', + column: 'column_B', + value: 150, + }, + { + row: 'row_A', + column: 'column_C', + value: 200, + }, + { + row: 'row_A', + column: 'column_C', + value: 200, + }, + { + row: 'row_B', + column: 'column_A', + value: 100, + }, + { + row: 'row_B', + column: 'column_A', + value: 100, + }, + { + row: 'row_B', + column: 'column_B', + value: 150, + }, + { + row: 'row_B', + column: 'column_B', + date: '2022-01-02', + value: 150, + }, + { + row: 'row_B', + column: 'column_C', + value: 200, + }, + { + row: 'row_B', + column: 'column_C', + date: '2022-01-02', + value: 200, + }, + ], + }, + }); + + const pivotGrid = new PivotGrid(page); + const containerLocator = page.locator('#container'); + + await testScreenshot(page, + 'T1173442_before_sort_with_summary_display_mode.png', + { element: containerLocator }, + ); + + await pivotGrid.getColumnHeaderArea().element.locator('td').first().click(); + await pivotGrid.getRowHeaderArea().element.locator('td').first().click(); + await testScreenshot(page, + 'T1173442_after_sort_with_summary_display_mode.png', + { element: containerLocator }, + ); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/virtualScrolling_T1210807.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/virtualScrolling_T1210807.spec.ts new file mode 100644 index 000000000000..8a39a7b83956 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/virtualScrolling_T1210807.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('PivotGrid_scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const createData = (count, innerCount) => { + const result: object[] = []; + + for (let i = 0; i < count; i += 1) { + for (let j = 0; j < innerCount; j += 1) { + result.push({ + item: `item ${i}`, + date: new Date('2024-01-01'), + category: `category ${j}`, + innerA: j, + innerB: j, + }); + } + } + + return result; + }; + + test.skip('Row fields overlap data fields if dataFieldArea is set to "row" and virtual scrolling is enabled (T1210807)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowExpandAll: true, + showBorders: true, + rowHeaderLayout: 'tree', + dataFieldArea: 'row', + height: 560, + scrolling: { + mode: 'virtual', + }, + dataSource: { + fields: [ + { + dataField: 'item', + area: 'row', + width: 120, + }, + { + dataField: 'category', + area: 'row', + width: 120, + }, + { + dataField: 'date', + dataType: 'date', + area: 'column', + groupInterval: 'year', + }, + { + dataField: 'innerA', + dataType: 'number', + summaryType: 'sum', + area: 'data', + }, + { + dataField: 'innerB', + dataType: 'number', + summaryType: 'sum', + area: 'data', + }, + ], + store: createData(50, 5), + }, + }); + + const pivotGrid = page.locator('#container'); + const firstHeaderRow = pivotGrid.getRowsArea(2).getCell(0); + await page.click(firstHeaderRow); + await pivotGrid.scrollBy({ top: 30000 }); + + await testScreenshot(page, 'rows_do_not_overlap_data_fields_if_virtual_scrolling_enabled_T1210807.png', { element: pivotGrid.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts new file mode 100644 index 000000000000..de6b9269d916 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Shadow DOM - Adopted DX css styles', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const dxWidgetHostStyles = '.dx-widget-shadow { font-size: 20px; }'; + const dxWidgetShadowStyles = '.dx-widget-shadow { font-size: 40px; }'; + + const setupShadowDomTest = async (page, copyStylesToShadowDom, hostStyles, shadowStyles) => { + await page.evaluate(({ copyStyles, hostCss, shadowCss }) => { + if (!copyStyles) { + (window as any).DevExpress.config({ copyStylesToShadowDom: copyStyles }); + } + + const container = document.createElement('div'); + container.id = 'shadow-host'; + document.body.appendChild(container); + + const hostStyleElement = document.createElement('style'); + hostStyleElement.innerHTML = hostCss; + document.head.appendChild(hostStyleElement); + + const shadowRoot = container.attachShadow({ mode: 'open' }); + + const shadowStyleElement = shadowRoot.ownerDocument.createElement('style'); + shadowStyleElement.innerHTML = shadowCss; + shadowRoot.appendChild(shadowStyleElement); + + const shadowContainerElement = document.createElement('div'); + shadowContainerElement.id = 'shadow-container'; + shadowRoot.appendChild(shadowContainerElement); + + (window as any).testShadowRoot = shadowRoot; + + new (window as any).DevExpress.ui.dxButton(shadowContainerElement, { + text: 'Test button', + }); + }, { copyStyles: copyStylesToShadowDom, hostCss: hostStyles, shadowCss: shadowStyles }); + }; + + const getAdoptedStyleSheets = async (page) => page.evaluate(() => { + const shadowRoot = (window as any).testShadowRoot; + const { adoptedStyleSheets } = shadowRoot; + + const results: { [key: string]: string[] | null } = { + firstSheetRules: null, + secondSheetRules: null, + }; + + if (adoptedStyleSheets.length > 1) { + results.firstSheetRules = Array + .from(adoptedStyleSheets[0].cssRules).map((rule) => (rule as CSSRule).cssText); + + results.secondSheetRules = Array + .from(adoptedStyleSheets[1].cssRules).map((rule) => (rule as CSSRule).cssText); + } + + return results; + }); + + test('Copies DX css styles from the host to the shadow root when rendering a DX widget', async ({ page }) => { + await setupShadowDomTest(page, true, dxWidgetHostStyles, dxWidgetShadowStyles); + + const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets(page); + + const hasHostStyle = firstSheetRules?.some((rule) => rule === dxWidgetHostStyles); + expect(hasHostStyle).toBeTruthy(); + + const hasShadowStyle = secondSheetRules?.some((rule) => rule === dxWidgetShadowStyles); + expect(hasShadowStyle).toBeTruthy(); + }); + + test('Does not copy DX css styles when copyStylesToShadowDom is disabled', async ({ page }) => { + await setupShadowDomTest(page, false, dxWidgetHostStyles, dxWidgetShadowStyles); + + const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets(page); + expect(firstSheetRules === null && secondSheetRules === null).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts new file mode 100644 index 000000000000..066fceab58b5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, TreeList } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Public methods', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const getItems = (): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < 100; i += 1) { + items.push({ key: `item_${i}`, parentKey: null }); + + for (let j = 0; j < 100; j += 1) { + items.push({ key: `item_${i}_${j}`, parentKey: `item_${i}` }); + } + } + + return items; + }; + + [true, false].forEach((renderAsync) => { + [true, false].forEach((useNativeScrolling) => { + // TODO: Playwright migration - CI screenshot size mismatch + test.skip(`The renderAsync=${renderAsync} and scrolling.useNative=${useNativeScrolling}: The navigateToRow method should work correctly when there are asynchronous cell templates and virtual scrolling is enabled (T1275775)`, async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getItems(), + height: 500, + width: 500, + dataStructure: 'plain', + parentIdExpr: 'parentKey', + keyExpr: 'key', + renderAsync, + scrolling: { + mode: 'virtual', + useNaive: useNativeScrolling, + }, + templatesRenderAsynchronously: true, + columns: [{ + dataField: 'key', + cellTemplate: 'testCellTemplate', + }], + integrationOptions: { + templates: { + testCellTemplate: { + render({ model, container, onRendered }) { + setTimeout(() => { + container.append($('').text(model.value)); + onRendered(); + }, 100); + }, + }, + }, + }, + }); + + const treeList = new TreeList(page); + + await expect(treeList.getDataCell(0, 0)).toContainText('item_'); + + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').navigateToRow('item_80_50')); + await page.waitForTimeout(500); + + await expect(treeList.getDataCell(131, 0)).toContainText('item_'); + + await testScreenshot(page, `T1275775-navigateToRow-with-async-cell-templates_(renderAsync=${renderAsync}_useNativeScrolling=${useNativeScrolling}).png`, { element: treeList.element }); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts new file mode 100644 index 000000000000..a85b9a8d0f7f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Adaptive Row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Should be shown and hidden when the window is resized', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [{ + ID: 1, + Head_ID: -1, + Full_Name: 'John Heart', + Prefix: 'Mr.', + Title: 'CEO', + City: 'Los Angeles', + State: 'California', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + Mobile_Phone: '(213) 555-9392', + Birth_Date: '1964-03-16', + Hire_Date: '1995-01-15', + }], + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + rootValue: -1, + allowColumnResizing: true, + rowDragging: { + allowDropInsideItem: true, + allowReordering: true, + }, + columns: [ + { + dataField: 'Title', + caption: 'Position', + hidingPriority: 0, + fixed: true, + }, + { dataField: 'Full_Name', hidingPriority: 1 }, + { dataField: 'City', hidingPriority: 2 }, + { dataField: 'State', hidingPriority: 3 }, + { dataField: 'Mobile_Phone', hidingPriority: 4 }, + { dataField: 'Hire_Date', dataType: 'date', hidingPriority: 5 }, + ], + }); + + const adaptiveButton = page.locator('#container .dx-command-adaptive .dx-datagrid-adaptive-more'); + await expect(adaptiveButton).toBeVisible(); + await adaptiveButton.click(); + + const adaptiveRow = page.locator('#container .dx-adaptive-detail-row'); + await expect(adaptiveRow).toBeVisible(); + + await page.setViewportSize({ width: 1200, height: 400 }); + await page.waitForTimeout(500); + + const adaptiveColumn = page.locator('#container .dx-command-adaptive'); + await expect(adaptiveColumn).toBeHidden(); + await expect(adaptiveRow).toBeHidden(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts new file mode 100644 index 000000000000..02ca976b7022 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts @@ -0,0 +1,447 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe.skip('Ai Column.Common (TreeList)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const EMPTY_CELL_TEXT = '\u00A0'; + const DROPDOWNMENU_PROMPT_EDITOR_INDEX = 0; + const DROPDOWNMENU_REGENERATE_INDEX = 1; + const DROPDOWNMENU_CLEAR_DATA_INDEX = 2; + + test('Get result from AI and display it in the AI column', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { id: 1, parentId: 0, name: 'Name 1', value: 10 }, + { id: 2, parentId: 1, name: 'Name 2', value: 20 }, + { id: 3, parentId: 1, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(page); + await expect(treeList.element.locator('.dx-treelist')).toBeVisible(); + + await page.waitForTimeout(1000); + + const cell0 = treeList.getDataCell(0, 3); + await expect(cell0).toHaveText('Response Name 1 for first AI column'); + const cell1 = treeList.getDataCell(1, 3); + await expect(cell1).toHaveText('Response Name 2 for first AI column'); + const cell2 = treeList.getDataCell(2, 3); + await expect(cell2).toHaveText('Response Name 3 for first AI column'); + }); + + test('Get result from AI and display it in two AI columns', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { id: 1, parentId: 0, name: 'Name 1', value: 10 }, + { id: 2, parentId: 1, name: 'Name 2', value: 20 }, + { id: 3, parentId: 1, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + { + type: 'ai', + caption: 'AI Column2', + name: 'AI Column2', + ai: { + prompt: 'second AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(page); + await expect(treeList.element.locator('.dx-treelist')).toBeVisible(); + await page.waitForTimeout(1000); + + await expect(treeList.getDataCell(0, 3)).toHaveText('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3)).toHaveText('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3)).toHaveText('Response Name 3 for first AI column'); + await expect(treeList.getDataCell(0, 4)).toHaveText('Response Name 1 for second AI column'); + await expect(treeList.getDataCell(1, 4)).toHaveText('Response Name 2 for second AI column'); + await expect(treeList.getDataCell(2, 4)).toHaveText('Response Name 3 for second AI column'); + }); + + test('Regenerate the AI request from DropDownButton menu', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { id: 1, parentId: 0, name: 'Name 1', value: 10 }, + { id: 2, parentId: 1, name: 'Name 2', value: 20 }, + { id: 3, parentId: 1, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + mode: 'manual', + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(page); + await expect(treeList.element.locator('.dx-treelist')).toBeVisible(); + + await expect(treeList.getDataCell(0, 3)).toHaveText(EMPTY_CELL_TEXT); + + const dropDownButton = treeList.getDropDownButton(0); + await dropDownButton.click(); + + const dropDownList = page.locator('.dx-dropdownbutton-popup-wrapper .dx-list-item'); + await dropDownList.nth(DROPDOWNMENU_REGENERATE_INDEX).click(); + + await page.waitForTimeout(1000); + + await expect(treeList.getDataCell(0, 3)).toHaveText('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3)).toHaveText('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3)).toHaveText('Response Name 3 for first AI column'); + }); + + test('Regenerate the AI request from Prompt Editor', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { id: 1, parentId: 0, name: 'Name 1', value: 10 }, + { id: 2, parentId: 1, name: 'Name 2', value: 20 }, + { id: 3, parentId: 1, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + mode: 'manual', + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(page); + await expect(treeList.element.locator('.dx-treelist')).toBeVisible(); + + await expect(treeList.getDataCell(0, 3)).toHaveText(EMPTY_CELL_TEXT); + + const dropDownButton = treeList.getDropDownButton(0); + await dropDownButton.click(); + + const dropDownList = page.locator('.dx-dropdownbutton-popup-wrapper .dx-list-item'); + await dropDownList.nth(DROPDOWNMENU_PROMPT_EDITOR_INDEX).click(); + + const regenerateButton = page.locator('.dx-ai-prompt-editor .dx-button').filter({ hasText: /regenerate/i }).first(); + await regenerateButton.click(); + + await page.waitForTimeout(1000); + + await expect(treeList.getDataCell(0, 3)).toHaveText('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3)).toHaveText('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3)).toHaveText('Response Name 3 for first AI column'); + }); + + test('Clear Data from AI column by DropDownButton menu', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { id: 1, parentId: 0, name: 'Name 1', value: 10 }, + { id: 2, parentId: 1, name: 'Name 2', value: 20 }, + { id: 3, parentId: 1, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(page); + await expect(treeList.element.locator('.dx-treelist')).toBeVisible(); + await page.waitForTimeout(1000); + + await expect(treeList.getDataCell(0, 3)).toHaveText('Response Name 1 for first AI column'); + + const dropDownButton = treeList.getDropDownButton(0); + await dropDownButton.click(); + + const dropDownList = page.locator('.dx-dropdownbutton-popup-wrapper .dx-list-item'); + await dropDownList.nth(DROPDOWNMENU_CLEAR_DATA_INDEX).click(); + + await expect(treeList.getDataCell(0, 3)).toHaveText(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3)).toHaveText(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3)).toHaveText(EMPTY_CELL_TEXT); + }); + + test('Abort the AI request from Prompt Editor', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { id: 1, parentId: 0, name: 'Name 1', value: 10 }, + { id: 2, parentId: 1, name: 'Name 2', value: 20 }, + { id: 3, parentId: 1, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + mode: 'manual', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + setTimeout(() => { resolve(JSON.stringify(result)); }, 3000); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(page); + await expect(treeList.element.locator('.dx-treelist')).toBeVisible(); + + await expect(treeList.getDataCell(0, 3)).toHaveText(EMPTY_CELL_TEXT); + + const dropDownButton = treeList.getDropDownButton(0); + await dropDownButton.click(); + + const dropDownList = page.locator('.dx-dropdownbutton-popup-wrapper .dx-list-item'); + await dropDownList.nth(DROPDOWNMENU_PROMPT_EDITOR_INDEX).click(); + + const regenerateButton = page.locator('.dx-ai-prompt-editor .dx-button').filter({ hasText: /regenerate/i }).first(); + await regenerateButton.click(); + + const stopButton = page.locator('.dx-ai-prompt-editor .dx-button').filter({ hasText: /stop/i }).first(); + await stopButton.click(); + + await expect(treeList.getDataCell(0, 3)).toHaveText(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3)).toHaveText(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3)).toHaveText(EMPTY_CELL_TEXT); + }); + + test('Change the prompt in the AI Prompt Editor', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { id: 1, parentId: 0, name: 'Name 1', value: 10 }, + { id: 2, parentId: 1, name: 'Name 2', value: 20 }, + { id: 3, parentId: 1, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(page); + await expect(treeList.element.locator('.dx-treelist')).toBeVisible(); + await page.waitForTimeout(1000); + + await expect(treeList.getDataCell(0, 3)).toHaveText('Response Name 1 for first AI column'); + + const dropDownButton = treeList.getDropDownButton(0); + await dropDownButton.click(); + + const dropDownList = page.locator('.dx-dropdownbutton-popup-wrapper .dx-list-item'); + await dropDownList.nth(DROPDOWNMENU_PROMPT_EDITOR_INDEX).click(); + + const textArea = page.locator('.dx-ai-prompt-editor .dx-textarea .dx-texteditor-input'); + await textArea.fill('changed prompt'); + + const applyButton = page.locator('.dx-ai-prompt-editor .dx-button').filter({ hasText: /apply/i }).first(); + await applyButton.click(); + + await page.waitForTimeout(1000); + + await expect(treeList.getDataCell(0, 3)).toHaveText('Response Name 1 for changed prompt'); + await expect(treeList.getDataCell(1, 3)).toHaveText('Response Name 2 for changed prompt'); + await expect(treeList.getDataCell(2, 3)).toHaveText('Response Name 3 for changed prompt'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/visual.spec.ts new file mode 100644 index 000000000000..af77023b3290 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/visual.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe.skip('Ai Column.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + + test('Default render', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 0, name: 'Name 3', value: 30, + }, + { + id: 4, parentId: 3, name: 'Name 4', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + expandedRowKeys: [3], + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + }, + ], + }); + + // arrange, act + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await testScreenshot(page, 'treelist__ai-column__default.png', { element: treeList.element }); + + // assert + + }); + + test('AI Column when multiple selection is enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 0, name: 'Name 3', value: 30, + }, + { + id: 4, parentId: 3, name: 'Name 4', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + expandedRowKeys: [3], + selection: { + mode: 'multiple', + }, + columns: [ + { + type: 'ai', + caption: 'AI Column', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange, act + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await testScreenshot(page, 'treelist__ai-column__multiple-selection.png', { element: treeList.element }); + + // assert + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/columns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/columns.spec.ts new file mode 100644 index 000000000000..d9ac276b416f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/columns.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Columns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1054312 + test('CheckBox position with double rows columns', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [{ + ID: 1, + Full_Name: 'John Heart', + City: 'Los Angeles', + State: 'California', + }], + keyExpr: 'ID', + selection: { + mode: 'multiple', + }, + columns: [{ + dataField: 'Full_Name', + }, + { columns: ['City', 'State'] }, + ], + }); + + const headers = page.locator('#container .dx-treelist-headers'); + + await testScreenshot(page, 'T1054312', { element: headers }); + + }); + + // T1053931 + test('Correct display border to last column', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Total: 205809000, + GDP_Agriculture: 0.054, + GDP_Industry: 0.274, + GDP_Services: 0.672, + GDP_Total: 2353025, + }, + ], + keyExpr: 'ID', + columns: [ + 'Country', + { + columns: [{ + dataField: 'GDP_Total', + }, { + columns: [{ + dataField: 'GDP_Agriculture', + }, { + dataField: 'GDP_Industry', + }, { + dataField: 'GDP_Services', + }], + }], + }, { + columns: [{ + dataField: 'Population_Total', + }, { + dataField: 'Population_Urban', + }], + }, { + dataField: 'Area', + }, + ], + width: 600, + height: 300, + }); + + const headers = page.locator('#container .dx-treelist-headers'); + + await testScreenshot(page, 'T1053931', { element: headers }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.spec.ts new file mode 100644 index 000000000000..95bf634252b4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Treelist - Editing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('TreeList - Insertafterkey doesn\'t work on children nodes', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + ID: 1, + Head_ID: -1, + Full_Name: 'John Heart', + }, + { + ID: 2, + Head_ID: 1, + Full_Name: 'Samantha Bright', + }, + ], + rootValue: -1, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + columns: ['Full_Name'], + editing: { + mode: 'batch', + allowAdding: true, + allowUpdating: true, + useIcons: true, + }, + focusedRowEnabled: true, + expandedRowKeys: [1], + onKeyDown(e: any) { + if (e.event.ctrlKey && e.event.key === 'Enter') { + const currentSelectedParentTaskId = e.component.getNodeByKey( + e.component.option('focusedRowKey'), + )?.parent?.key; + const key = new (window as any).DevExpress.data.Guid().toString(); + const data = { Head_ID: currentSelectedParentTaskId }; + e.component.option('editing.changes', [ + { + key, + type: 'insert', + insertAfterKey: e.component.option('focusedRowKey'), + data, + }, + ]); + } + }, + }); + + const dataRows = page.locator('#container .dx-data-row'); + await dataRows.nth(1).locator('td').first().click(); + await page.keyboard.press('Control+Enter'); + + const expectedInsertedRowIndex = 2; + const insertedRow = dataRows.nth(expectedInsertedRowIndex); + await expect(insertedRow).toHaveClass(/dx-row-inserted/); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts new file mode 100644 index 000000000000..43fd0ab6688c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Focus', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + + test('Focus method should focus the first data cell', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, name: 'name 1' }, + { id: 2, parentId: 1, name: 'name 2' }, + { id: 3, parentId: 0, name: 'name 3' }, + ], + keyExpr: 'id', + parentId: 'parentId', + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_: any, options: any) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + + expect(await treeList.isReady()).toBe(true); + + await treeList.apiFocus(); + + const firstCell = treeList.getDataCell(0, 0); + await expect(firstCell).toBeFocused(); + }); + + test('Focus method should focus the first data row when focusedRowEnabled = true', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, name: 'name 1' }, + { id: 2, parentId: 1, name: 'name 2' }, + { id: 3, parentId: 0, name: 'name 3' }, + ], + keyExpr: 'id', + parentId: 'parentId', + focusedRowEnabled: true, + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_: any, options: any) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + + expect(await treeList.isReady()).toBe(true); + + await treeList.apiFocus(); + + const firstRow = treeList.getDataRow(0).element; + await expect(firstRow).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts new file mode 100644 index 000000000000..337f95e3bc85 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts @@ -0,0 +1,124 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Focused row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const getItems = (): Record[] => { + const items: Record[] = []; + for (let i = 0; i < 100; i += 1) { + items.push({ + ID: i + 1, + Name: `Name ${i + 1}`, + }); + } + return items; + }; + + const getTreeListConfig = (): any => ({ + dataSource: getItems(), + keyExpr: 'ID', + height: 500, + stateStoring: { + enabled: true, + type: 'custom', + customSave: (state) => { + localStorage.setItem('mystate', JSON.stringify(state)); + }, + customLoad: () => { + let state = localStorage.getItem('mystate'); + if (state) { + state = JSON.parse(state); + } + return state; + }, + }, + focusedRowEnabled: true, + focusedRowKey: 90, + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Focused row should be shown after reloading the page (T1058983)', async ({ page }) => { + + await page.evaluate(() => { + (window as any).localStorage.removeItem('mystate'); + }); + await createWidget(page, 'dxTreeList', getTreeListConfig()); + + await page.waitForTimeout(1000); + + const focusedRow = page.locator('#container .dx-row-focused'); + await expect(focusedRow).toBeVisible(); + + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').getScrollable().scrollTo({ top: 0 })); + await page.waitForTimeout(300); + + const scrollTop = await page.evaluate(() => ($('#container') as any).dxTreeList('instance').getScrollable().scrollTop()); + expect(scrollTop).toBe(0); + + await page.evaluate(() => location.reload()); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + await createWidget(page, 'dxTreeList', getTreeListConfig()); + await page.waitForTimeout(1000); + + const focusedRowAfterReload = page.locator('#container .dx-row-focused'); + await expect(focusedRowAfterReload).toBeVisible(); + + }); + + test('TreeList - Unable to focus a node when deleting the previous node in certain scenarios (T1178893)', async ({ page }) => { + + await page.evaluate(() => { + (window as any).localStorage.removeItem('mystate'); + }); + const config = getTreeListConfig(); + config.editing = { + mode: 'row', + allowUpdating: true, + allowAdding: true, + allowDeleting: true, + }; + config.focusedRowKey = 3; + + await createWidget(page, 'dxTreeList', config); + + const treeList = new TreeList(page); + const focusedRow = page.locator('#container .dx-row-focused'); + + await expect(focusedRow).toHaveAttribute('aria-rowindex', '3'); + + const deleteButton0 = treeList.getDataRow(2).element.locator('.dx-link-delete').first(); + await deleteButton0.click(); + + const confirmButton0 = page.locator('.dx-dialog-button:has-text("Yes")'); + await confirmButton0.click(); + await page.waitForTimeout(300); + + await expect(focusedRow).toHaveAttribute('aria-rowindex', '3'); + + const deleteButton1 = treeList.getDataRow(2).element.locator('.dx-link-delete').first(); + await deleteButton1.click(); + + const confirmButton1 = page.locator('.dx-dialog-button:has-text("Yes")'); + await confirmButton1.click(); + await page.waitForTimeout(300); + + await expect(focusedRow).toHaveAttribute('aria-rowindex', '3'); + await expect(treeList.getDataCell(2, 0)).toContainText('5'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts new file mode 100644 index 000000000000..73df11e90a57 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts @@ -0,0 +1,137 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + const createTreeList = async (page) => createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: [ + { + type: 'buttons', + buttons: [ + { + hint: 'button_1', + icon: 'edit', + onClick: (e) => $(e.event.target).attr('has-been-clicked', 'true'), + }, + { + hint: 'button_2', + icon: 'remove', + }, + ], + }, + 'id', + 'columnA', + 'columnB', + ], + sorting: { + mode: 'none', + }, + }); + + test('Custom buttons cell should be focused before custom buttons on tab navigation', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 0); + const cellToStartNavigation = treeList.getHeaderCell(0, 3); + + await cellToStartNavigation.click(); + await page.keyboard.press('Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + test('Custom buttons cell should be focused after custom buttons on shift+tab reverse navigation', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 0); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + test('First custom button inside custom buttons cell should be focused on tab navigation', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.locator('[title="button_1"]'); + const cellToStartNavigation = treeList.getHeaderCell(0, 3); + + await cellToStartNavigation.click(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await expect(expectedFocusedButton).toBeFocused(); + + }); + + test('Last custom button inside custom buttons cell should be focused on shift+tab reverse navigation', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.locator('[title="button_2"]'); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click(); + await page.keyboard.press('Shift+Tab'); + await expect(expectedFocusedButton).toBeFocused(); + + }); + + test('Custom button inside custom buttons cell should be clickable by pressing enter key', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.locator('[title="button_1"]'); + const cellToStartNavigation = treeList.getHeaderCell(0, 3); + + await cellToStartNavigation.click(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + await expect(expectedFocusedButton).toHaveAttribute('has-been-clicked', 'true'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts new file mode 100644 index 000000000000..1a3299547d70 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts @@ -0,0 +1,155 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Keyboard Navigation - common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('TreeList - Selection CheckBox in a data row isn\'t navigable with Tab button if this CheckBox was focused manually (T1207467)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', age: 19, + }, + { + id: 2, parentId: 1, name: 'Name 2', age: 11, + }, + { + id: 3, parentId: 0, name: 'Name 3', age: 15, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + showBorders: true, + selection: { + mode: 'multiple', + recursive: false, + }, + columns: ['id', 'name', 'age'], + }); + + await createWidget(page, 'dxButton', { + text: 'Focus', + onClick() { + const checkbox = $('.dx-checkbox:visible')[1]; + if (checkbox) { + checkbox.focus(); + } + }, + }, '#otherContainer'); + + const treeList = new TreeList(page); + const focusButton = page.locator('#otherContainer .dx-button'); + const expectedFocusedCell = treeList.getDataCell(0, 2); + + await focusButton.click(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('TreeList - Template button in a data row isn\'t navigable with Tab button if this button was focused manually (T1207467)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', age: 19, + }, + { + id: 2, parentId: 1, name: 'Name 2', age: 11, + }, + { + id: 3, parentId: 0, name: 'Name 3', age: 15, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + showBorders: true, + selection: { + mode: 'multiple', + recursive: false, + }, + columns: [{ + dataField: 'id', + }, { + dataField: 'name', + cellTemplate(container) { + const button = document.createElement('button'); + button.innerText = 'select'; + container.append(button); + }, + }, 'age'], + }); + + await createWidget(page, 'dxButton', { + text: 'Focus', + onClick() { + const btn = $('button')[0]; + if (btn) { + btn.focus(); + } + }, + }, '#otherContainer'); + + const treeList = new TreeList(page); + const focusButton = page.locator('#otherContainer .dx-button'); + const expectedFocusedCell = treeList.getDataCell(0, 2); + + await focusButton.click(); + await page.keyboard.press('Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('TreeList - Keyboard navigation on Expand/Collapse buttons is broken if the mouse used before (T1234949)', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + Task_ID: 1, + Task_Subject: 'Plans 2015', + Task_Parent_ID: 0, + }, + { + Task_ID: 2, + Task_Subject: 'Health Insurance', + Task_Parent_ID: 1, + }, + ], + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + columns: [ + { + dataField: 'Task_Subject', + }, + { + dataField: 'Task_Assigned_Employee_ID', + }, + ], + }); + + const treeList = new TreeList(page); + const target = treeList.getDataCell(0, 0); + + await target.locator('> *').first().click(); + await page.locator('#container').click({ position: { x: 100, y: 600 } }); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await expect(target).toBeFocused(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts new file mode 100644 index 000000000000..37c4979f5693 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + + test('Focused cells should look correctly', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: ['id', 'columnA', 'columnB'], + sorting: { + mode: 'none', + }, + }); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const headerCellToFocus = treeList.getHeaderCell(0, 0); + const dataCellToFocus = treeList.getDataCell(0, 0); + + await headerCellToFocus.click(); + await page.keyboard.press('Tab'); + await testScreenshot(page, 'tree-list_keyboard-navigation-header-cell-focused.png', { element: treeList.element }); + + await dataCellToFocus.click(); + await page.keyboard.press('Tab'); + await testScreenshot(page, 'tree_list_keyboard-navigation-data-cell-focused.png', { element: treeList.element }); + + }); + + test('Focused custom buttons should look correctly', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: [ + { + type: 'buttons', + buttons: [ + { + hint: 'button_1', + icon: 'edit', + }, + { + hint: 'button_2', + icon: 'remove', + }, + ], + }, + 'id', + 'columnA', + 'columnB', + ], + sorting: { + mode: 'none', + }, + }); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const headerCellToFocus = treeList.getHeaderCell(0, 3); + + await headerCellToFocus.click(); + await page.keyboard.press('Tab'); + await testScreenshot(page, 'tree-list_keyboard-navigation-custom-buttons-header-cell-focused.png', { element: treeList.element }); + + await page.keyboard.press('Tab'); + await testScreenshot(page, 'tree-list_keyboard-navigation-custom-button-focused.png', { element: treeList.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts new file mode 100644 index 000000000000..3c302ca7c083 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T861048 + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('The row should be selected on click if less than half of a row is visible', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', age: 19, + }, + { + id: 2, parentId: 1, name: 'Name 2', age: 11, + }, + { + id: 3, parentId: 0, name: 'Name 3', age: 15, + }, + { + id: 4, parentId: 3, name: 'Name 4', age: 16, + }, + { + id: 5, parentId: 0, name: 'Name 5', age: 25, + }, + { + id: 6, parentId: 5, name: 'Name 6', age: 18, + }, + { + id: 7, parentId: 0, name: 'Name 7', age: 21, + }, + { + id: 8, parentId: 7, name: 'Name 8', age: 14, + }, + ], + height: 150, + autoExpandAll: true, + columns: ['name', 'age'], + selection: { + mode: 'multiple', + }, + }); + + const treeList = new TreeList(page); + const dataRow = treeList.getDataRow(3); + const checkbox = dataRow.element.locator('.dx-select-checkbox'); + + await checkbox.click({ position: { x: 0, y: 0 } }); + await expect(dataRow.element).toHaveClass(/dx-selection/); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts new file mode 100644 index 000000000000..dcea47c3ab27 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + const DATA_SOURCE = [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ]; + + const createTreeList = async (page) => createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: ['id', 'columnA', 'columnB'], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + }); + + const createTreeListRenderAsyncWithButtons = async (page) => createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: ['id', 'columnA', 'columnB', { type: 'buttons' }], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + renderAsync: true, + }); + + test('The drag cell should be skipped when navigating from the header cell by tab keypress', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + const cellToStartNavigation = treeList.getHeaderCell(0, 3); + + await cellToStartNavigation.click(); + await page.keyboard.press('Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + test('The drag cell should be skipped when navigating from the header cell by tab keypress' + + ' with buttons column and renderAsync: true', async ({ page }) => { + await createTreeListRenderAsyncWithButtons(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + const cellToStartNavigation = treeList.getHeaderCell(0, 4); + + await cellToStartNavigation.click(); + await page.keyboard.press('Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + test('The drag cell should be skipped when navigating to the header cell by shift+tab keypress', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getHeaderCell(0, 3); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click(); + await page.keyboard.press('Shift+Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + test('The drag cell should be skipped when navigating to a next row by tab keypress', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(1, 1); + const cellToStartNavigation = treeList.getDataCell(0, 3); + + await cellToStartNavigation.click(); + await page.keyboard.press('Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + test('The drag cell should be skipped when navigating to a previous row by shift+tab keypress', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 3); + const cellToStartNavigation = treeList.getDataCell(1, 1); + + await cellToStartNavigation.click(); + await page.keyboard.press('Shift+Tab'); + await expect(expectedFocusedCell).toBeFocused(); + + }); + + test('The drag cell shouldn\'t be focused when the next cell is focused and the left arrow key pressed', async ({ page }) => { + await createTreeList(page); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + + await expectedFocusedCell.click(); + await page.keyboard.press('ArrowLeft'); + await expect(expectedFocusedCell).toBeFocused(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts new file mode 100644 index 000000000000..832e20f79030 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts @@ -0,0 +1,267 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TreeList - Markup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const tasksT1223168 = [{ + Task_ID: 1, + Task_Subject: 'Plans 2015', + Task_Parent_ID: 0, + }, { + Task_ID: 2, + Task_Subject: 'Health Insurance', + Task_Parent_ID: 1, + }, { + Task_ID: 3, + Task_Subject: 'Training', + Task_Parent_ID: 2, + }]; + + test('TreeList - Expand/collapse buttons are too close to column borders if the first column is a boolean column (T1223168)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: tasksT1223168, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + autoExpandAll: true, + wordWrapEnabled: true, + showBorders: true, + columns: [{ + dataField: 'test', + dataType: 'boolean', + }, 'Task_Subject'], + showColumnLines: true, + rowDragging: { + allowReordering: true, + }, + }); + + const treeList = page.locator('#container'); + + await testScreenshot(page, 'T1223168-expandable', { element: treeList }); + + }); + + // T1221037 + test('TreeList screenshot when the first cell has a template', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [{ + ID: 1, + Head_ID: 0, + Full_Name: 'John Heart', + Prefix: 'Mr.', + Title: 'CEO', + City: 'Los Angeles', + State: 'California', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + Mobile_Phone: '(213) 555-9392', + Birth_Date: '1964-03-16', + Hire_Date: '1995-01-15', + }, { + ID: 2, + Head_ID: 1, + Full_Name: 'Arthur Miller', + Prefix: 'Mr.', + Title: 'CTO', + City: 'Denver', + State: 'Colorado', + Email: 'arthurm@dx-email.com', + Skype: 'arthurm_DX_skype', + Mobile_Phone: '(310) 555-8583', + Birth_Date: '1972-07-11', + Hire_Date: '2007-12-18', + }, { + ID: 3, + Head_ID: 2, + Full_Name: 'Brett Wade', + Prefix: 'Mr.', + Title: 'IT Manager', + City: 'Reno', + State: 'Nevada', + Email: 'brettw@dx-email.com', + Skype: 'brettw_DX_skype', + Mobile_Phone: '(626) 555-0358', + Birth_Date: '1968-12-01', + Hire_Date: '2009-03-06', + }, { + ID: 4, + Head_ID: 3, + Full_Name: 'Morgan Kennedy', + Prefix: 'Mrs.', + Title: 'Graphic Designer', + City: 'San Fernando Valley', + State: 'California', + Email: 'morgank@dx-email.com', + Skype: 'morgank_DX_skype', + Mobile_Phone: '(818) 555-8238', + Birth_Date: '1984-07-17', + Hire_Date: '2012-01-11', + }, { + ID: 5, + Head_ID: 4, + Full_Name: 'Violet Bailey', + Prefix: 'Ms.', + Title: 'Jr Graphic Designer', + City: 'La Canada', + State: 'California', + Email: 'violetb@dx-email.com', + Skype: 'violetb_DX_skype', + Mobile_Phone: '(818) 555-2478', + Birth_Date: '1985-06-10', + Hire_Date: '2012-01-19', + }], + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + columnAutoWidth: true, + width: 770, + columns: [{ + dataField: 'Title', + caption: 'Position', + cellTemplate(_, cellInfo) { + return $('
').append( + $('').text(cellInfo.data.Title), + ); + }, + }, 'Full_Name', 'City', 'State', { + dataField: 'Hire_Date', + dataType: 'date', + }], + showRowLines: true, + showBorders: true, + expandedRowKeys: [1, 2, 3, 4], + }); + + const treeList = page.locator('#container'); + + await testScreenshot(page, 'T1221037-cell-with-template', { element: treeList }); + + }); + + // T1291705 + test('The shading should alternate correctly after expanding the node when repaintChangesOnly is enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, text: 'item 1' }, + { id: 2, parentId: 0, text: 'item 2' }, + { id: 3, parentId: 2, text: 'item 3' }, + { id: 4, parentId: 0, text: 'item 4' }, + { id: 5, parentId: 4, text: 'item 5' }, + { id: 6, parentId: 0, text: 'item 6' }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + rowAlternationEnabled: true, + repaintChangesOnly: true, + }); + + const treeList = page.locator('#container'); + + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').expandRow(4)); + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').expandRow(2)); + + await testScreenshot(page, 'T1291705-row-alternation-after-expanding-node-when-repaintChangesOnly=true', { element: treeList }); + + }); + + test('The shading should alternate correctly after expanding the node when repaintChangesOnly and old fixed columns are enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, text: 'item 1' }, + { id: 2, parentId: 0, text: 'item 2' }, + { id: 3, parentId: 2, text: 'item 3' }, + { id: 4, parentId: 0, text: 'item 4' }, + { id: 5, parentId: 4, text: 'item 5' }, + { id: 6, parentId: 0, text: 'item 6' }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + rowAlternationEnabled: true, + repaintChangesOnly: true, + columnFixing: { + legacyMode: true, + }, + columns: [{ dataField: 'id', fixed: true }, 'text'], + }); + + const treeList = page.locator('#container'); + + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').expandRow(4)); + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').expandRow(2)); + + await testScreenshot(page, 'T1291705-row-alternation-after-expanding-node-when-there-is-fixed-column-and-repaintChangesOnly=true', { element: treeList }); + + }); + + ['single', 'multiple'].forEach((selectionMode) => { + ['single-line', 'multiple-line'].forEach((contentType) => { + [false, true].forEach((rtlEnabled) => { + const testFn = (selectionMode === 'multiple' && contentType === 'multiple-line') ? test.skip : test; + testFn( + `Markup should be correct [T1291914 & T1294907]:selection=${selectionMode},content=${contentType},rtl=${rtlEnabled}`, + async ({ page }) => { + const treeList = page.locator('#container'); + + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, first: 'Alice', last: 'Blue', age: 30, position: 'CEO', + }, + { + id: 2, parentId: 1, first: 'Bob', last: 'Brown', age: 25, position: 'CTO', + }, + { + id: 3, parentId: 1, first: 'Charlie', last: 'Green', age: 28, position: 'CFO', + }, + { + id: 4, parentId: 1, first: 'David', last: 'White', age: 22, position: 'Developer', + }, + { + id: 5, parentId: 3, first: 'Eve', last: 'Black', age: 26, position: 'Designer', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + expandedRowKeys: [1, 2], + columns: [ + { + dataField: 'first', + cellTemplate: contentType === 'single-line' + ? undefined + : () => { + const div = document.createElement('div'); + div.innerText = 'Long text that should wrap into multiple lines. Long text that should wrap into multiple lines.'; + div.style.whiteSpace = 'break-spaces'; + + return div; + }, + }, + 'last', + 'age', + 'position', + ], + rtlEnabled, + selection: { + mode: selectionMode, + }, + selectedRowKeys: selectionMode === 'single' ? [3] : [3, 4], + }); + + await testScreenshot(page, `markup-selection=${selectionMode}-rtl=${rtlEnabled}-content=${contentType}`, { element: treeList }); + }, + ); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.spec.ts new file mode 100644 index 000000000000..49517f8bc053 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.spec.ts @@ -0,0 +1,112 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList, ExpandableCell } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Row dragging', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const tasksT1228650 = [{ + Task_ID: 1, + Task_Subject: 'Plans 2015', + Task_Parent_ID: 0, + }, { + Task_ID: 2, + Task_Subject: 'Health Insurance', + Task_Parent_ID: 1, + }, { + Task_ID: 3, + Task_Subject: 'Training', + Task_Parent_ID: 2, + }]; + + test.skip('TreeList - Expand/collapse mechanism breaks after dragging action in the space between the last row and the border (T1228650)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: tasksT1228650, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + height: 200, + wordWrapEnabled: true, + showBorders: true, + columnFixing: { + legacyMode: true, + }, + columns: [ + { + dataField: 'test', + dataType: 'boolean', + }, + { + dataField: 'Task_Subject', + fixed: true, + fixedPosition: 'right', + }, + ], + showColumnLines: true, + rowDragging: { + allowDropInsideItem: true, + allowReordering: false, + showDragIcons: false, + group: 'none', + }, + }); + + const treeList = new TreeList(page); + const dataRow = treeList.getDataRow(0); + const expandButton = new ExpandableCell(dataRow.getDataCell(0)).getExpandButton(); + const freeSpaceRow = page.locator('#container .dx-freespace-row'); + + await freeSpaceRow.dragTo(dataRow.element); + await expandButton.click(); + await expect(treeList.getDataRow(1).element).toBeVisible(); + + }); + + [undefined, 200].forEach((height) => { + test(`TreeList - The W1025 warning occurs when dragging a row (height: ${height ?? 'not set'}). (T1280519)`, async ({ page }) => { + const warnings: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') { + warnings.push(msg.text()); + } + }); + + await createWidget(page, 'dxDataGrid', { + height, + scrolling: { + mode: 'virtual', + }, + dataSource: tasksT1228650, + rowDragging: { + allowReordering: true, + }, + }); + + const firstRow = page.locator('#container .dx-data-row').first(); + const firstRowBox = await firstRow.boundingBox(); + + if (firstRowBox) { + await page.mouse.move(firstRowBox.x + 10, firstRowBox.y + firstRowBox.height / 2); + await page.mouse.down(); + await page.mouse.move(firstRowBox.x + 20, firstRowBox.y + firstRowBox.height / 2 + 10, { steps: 5 }); + await page.mouse.up(); + } + + await page.waitForTimeout(100); + + const warningExists = warnings.some((message) => message.startsWith('W1025')); + + expect(warningExists).toBe(height === undefined); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts new file mode 100644 index 000000000000..89ebb911e707 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, TreeList } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('The vertical scroll bar of the container\'s parent should not be displayed when the grid has no height, virtual scrolling and state storing are enabled', async ({ page }) => { + const data = Array.from({ length: 100 }, (_, i) => ({ + id: i + 1, + parentId: i === 0 ? 0 : 1, + name: `Item ${i + 1}`, + })); + + await createWidget(page, 'dxTreeList', { + dataSource: data, + keyExpr: 'id', + parentIdExpr: 'parentId', + scrolling: { + mode: 'virtual', + }, + stateStoring: { + enabled: true, + type: 'custom', + customLoad() { + return {}; + }, + customSave() {}, + }, + }); + + const parentContainer = page.locator('#parentContainer'); + const parentScrollHeight = await parentContainer.evaluate( + (el) => el.scrollHeight, + ); + const parentClientHeight = await parentContainer.evaluate( + (el) => el.clientHeight, + ); + + expect(parentScrollHeight).toBeLessThanOrEqual(parentClientHeight + 1); + }); + + test('All items should be selected after select all and scroll down', async ({ page }) => { + const data = Array.from({ length: 50 }, (_, i) => ({ + id: i + 1, + parentId: 0, + name: `Item ${i + 1}`, + })); + + await createWidget(page, 'dxTreeList', { + dataSource: data, + keyExpr: 'id', + parentIdExpr: 'parentId', + height: 400, + scrolling: { + mode: 'virtual', + }, + selection: { + mode: 'multiple', + }, + columns: ['name'], + }); + + const treeList = new TreeList(page); + + const selectAllCheckbox = page.locator('.dx-treelist-headers .dx-select-checkbox'); + await selectAllCheckbox.click(); + + await treeList.scrollTo({ top: 1000 }); + await page.waitForTimeout(500); + + const uncheckedBoxes = page.locator('.dx-treelist-rowsview .dx-select-checkbox:not(.dx-checkbox-checked)'); + const uncheckedCount = await uncheckedBoxes.count(); + expect(uncheckedCount).toBe(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.spec.ts new file mode 100644 index 000000000000..90a3fbfe9846 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('SearchPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Items are shown in the original order after search is applied - T1274434 - 1', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + showBorders: true, + showRowLines: true, + expandedRowKeys: [1], + searchPanel: { + visible: true, + }, + columns: ['text'], + dataSource: [ + { id: 1, parentId: 0, text: 'parent1' }, + { id: 2, parentId: 0, text: 'test1' }, + { id: 3, parentId: 1, text: 'test2' }, + ], + }); + + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').searchByText('test')); + await page.waitForTimeout(300); + + const rows = page.locator('#container .dx-data-row'); + await expect(rows).toHaveCount(3); + + await expect(rows.nth(0).locator('td').nth(0)).toContainText('parent1'); + await expect(rows.nth(1).locator('td').nth(0)).toContainText('test2'); + await expect(rows.nth(2).locator('td').nth(0)).toContainText('test1'); + + }); + + test('Items are shown in the original order after search is applied - T1274434 - 2', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + showBorders: true, + showRowLines: true, + expandedRowKeys: [1], + searchPanel: { + visible: true, + }, + columns: ['text'], + dataSource: [ + { id: 1, parentId: 0, text: 'parent1' }, + { id: 2, parentId: 0, text: 'test1' }, + { id: 3, parentId: 1, text: 'test2' }, + { id: 4, parentId: 0, text: 'parent2' }, + ], + }); + + await page.evaluate(() => ($('#container') as any).dxTreeList('instance').searchByText('test')); + await page.waitForTimeout(300); + + const rows = page.locator('#container .dx-data-row'); + await expect(rows).toHaveCount(3); + + await expect(rows.nth(0).locator('td').nth(0)).toContainText('parent1'); + await expect(rows.nth(1).locator('td').nth(0)).toContainText('test2'); + await expect(rows.nth(2).locator('td').nth(0)).toContainText('test1'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts new file mode 100644 index 000000000000..f29597468f28 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts @@ -0,0 +1,105 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, TreeList } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('TreeList with selection and boolean data in first column should render right', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, value: true, value1: 'text' }, + { id: 2, parentId: 1, value: true, value1: 'text' }, + { id: 3, parentId: 2, value: true, value1: 'text' }, + { id: 4, parentId: 3, value: true, value1: 'text' }, + { id: 5, parentId: 4, value: true, value1: 'text' }, + { id: 6, parentId: 5, value: true, value1: 'text' }, + { id: 7, parentId: 6, value: true, value1: 'text' }, + { id: 8, parentId: 7, value: true, value1: 'text' }, + ], + height: 300, + width: 400, + autoExpandAll: true, + columns: [{ + dataField: 'value', + width: 100, + }, { + dataField: 'value1', + }], + selection: { + mode: 'multiple', + }, + }); + + const treeList = page.locator('#container'); + + await testScreenshot(page, 'T1109666-selection', { element: treeList }); + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('TreeList restore selection after the search panel has cleared', async ({ page }) => { + const tasksData = [ + { + Task_ID: 1, Task_Subject: 'Plans 2015', Task_Parent_ID: 0, + }, + { + Task_ID: 2, Task_Subject: 'Health Insurance', Task_Parent_ID: 1, + }, + { + Task_ID: 3, Task_Subject: 'New Brochures', Task_Parent_ID: 1, + }, + { + Task_ID: 4, Task_Subject: 'Update NDA', Task_Parent_ID: 1, + }, + { + Task_ID: 5, Task_Subject: 'Training', Task_Parent_ID: 2, + }, + ]; + + await createWidget(page, 'dxTreeList', { + dataSource: tasksData, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + autoExpandAll: true, + height: 400, + searchPanel: { + visible: true, + }, + selection: { + mode: 'multiple', + recursive: true, + }, + columns: [ + { dataField: 'Task_Subject' }, + ], + }); + + const treeList = new TreeList(page); + + const firstCheckbox = treeList.getDataRow(0).element.locator('.dx-select-checkbox'); + await firstCheckbox.click(); + + const searchInput = page.locator('.dx-searchbox .dx-texteditor-input'); + await searchInput.fill('Health'); + await page.waitForTimeout(500); + + await searchInput.fill(''); + await page.waitForTimeout(500); + + const expandButton = treeList.getDataRow(0).getExpandButton(); + await expandButton.click(); + + const selectedCheckboxes = page.locator('.dx-select-checkbox.dx-checkbox-checked'); + const count = await selectedCheckboxes.count(); + expect(count).toBeGreaterThan(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts new file mode 100644 index 000000000000..7bca8a19f5df --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts @@ -0,0 +1,105 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Drag and Drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Header hover should display correctly when there are fixed columns', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: new Array(20).fill(null).map((_, index) => { + const item: Record = { + id: index + 1, + parentId: index % 5, + }; + + for (let i = 0; i < 13; i += 1) { + item[`field${i}`] = `test ${i} ${index + 2}`; + } + + return item; + }), + keyExpr: 'id', + columnFixing: { + enabled: true, + }, + width: 850, + autoExpandAll: true, + columnAutoWidth: true, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const headerCell = treeList.getHeaderCell(0, 13); + + await headerCell.hover(); + + await testScreenshot(page, 'treelist_header_hover_with_fixed_columns.png', { element: treeList.element }); + + }); + + // TODO: Playwright migration - CI screenshot size mismatch + test.skip('Row hover should display correctly when there are fixed columns', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: new Array(20).fill(null).map((_, index) => { + const item: Record = { + id: index + 1, + parentId: index % 5, + }; + + for (let i = 0; i < 13; i += 1) { + item[`field${i}`] = `test ${i} ${index + 2}`; + } + + return item; + }), + keyExpr: 'id', + columnFixing: { + enabled: true, + }, + width: 850, + autoExpandAll: true, + columnAutoWidth: true, + hoverStateEnabled: true, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + const treeList = new TreeList(page, TREE_LIST_SELECTOR); + const dataRow = treeList.getDataRow(1); + + await dataRow.element.hover(); + + await testScreenshot(page, 'treelist_row_hover_with_fixed_columns.png', { element: treeList.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.spec.ts new file mode 100644 index 000000000000..f640a6aa30cd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, TreeList } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i += 1) { + const item: Record = {}; + for (let j = 0; j < colCount; j += 1) { + item[`field_${j}`] = `val_${i}_${j}`; + } + items.push(item); + } + return items; +}; + +test.describe('Sticky columns - Drag and Drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DATA_GRID_SELECTOR = '#container'; + + test('Fixed columns should work when drag and drop rows are enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getData(10, 10), + keyExpr: 'field_0', + width: 500, + columnFixing: { + enabled: true, + }, + showColumnHeaders: true, + columnAutoWidth: true, + rowDragging: { + allowReordering: true, + dropFeedbackMode: 'push', + }, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + const treeList = new TreeList(page, DATA_GRID_SELECTOR); + + await testScreenshot(page, 'treelist_sticky_columns_with_drag_and_drop_before_interaction.png', { element: treeList.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/toast.spec.ts new file mode 100644 index 000000000000..61203cd41ef5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/toast.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toasts in TreeList', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('Toast should be visible after calling and should be not visible after default display time', async ({ page }) => { + + createWidget(page, 'dxTreeList', {}); + + const treeList = page.locator('#container'); + await treeList.isReady(); + await treeList.apiShowErrorToast(); + await expect(treeList.getToast().exists).ok(); + + await testScreenshot(page, 'ai-column__toast__at-the-right-position.png', { element: treeList.element }); + await expect(treeList.getToast().exists).notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts new file mode 100644 index 000000000000..7a4eb4662d09 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +test.describe('Accessibility bugs', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test.skip('T1187314 - DataGrid displays an incorrect row count in "aria-label" if there is no data after filtering', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (dataGrid undefined, t.eql) + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [{ + id: 0, + data: 'A', + }], + filterRow: { visible: true }, + scrolling: { mode: 'infinite' }, + }); + + await dataGrid.apiFilter(['id', '=', '1']); + expect(await dataGrid.getContainer().getAttribute('aria-label')); + await t.eql('Data grid with 0 rows and 2 columns'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts new file mode 100644 index 000000000000..668815b52318 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts @@ -0,0 +1,272 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +test.describe('Common tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Grid without data', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await testScreenshot(page, 'no-data.png', { element: page.locator('#container') }); + }); + + test('Sorting and group panel', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + groupPanel: { + visible: true, + }, + columns: [ + 'field_0', + 'field_1', + 'field_2', + { + dataField: 'field_3', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'field_4', + sortOrder: 'desc', + sortIndex: 1, + }, + ], + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await testScreenshot(page, 'sorting-and-group-panel.png', { element: page.locator('#container') }); + }); + + test('Search panel', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + searchPanel: { + visible: true, + }, + columns: [ + 'field_0', + 'field_1', + 'field_2', + 'field_3', + 'field_4', + ], + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await testScreenshot(page, 'search-panel.png', { element: page.locator('#container') }); + }); + + test('Fixed columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 7), + keyExpr: 'field_0', + columnFixing: { + legacyMode: true, + } as any, + columns: [ + { + dataField: 'field_0', + fixed: true, + }, + { + dataField: 'field_1', + fixed: true, + }, + 'field_2', + 'field_3', + 'field_4', + { + dataField: 'field_5', + fixed: true, + fixedPosition: 'right', + }, + { + dataField: 'field_6', + fixed: true, + fixedPosition: 'right', + }, + ], + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await testScreenshot(page, 'fixed-columns.png', { element: page.locator('#container') }); + }); + + test('Error row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'row', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: [ + 'field_1', + 'field_2', + 'field_3', + 'field_4', + ], + onRowValidating(e: any) { + e.isValid = false; + e.errorText = 'Test'; + }, + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await dataGrid.apiEditRow(0); + expect(await dataGrid.getDataRow(0).element.evaluate((el) => el.classList.contains('dx-edit-row'))).toBeTruthy(); + + await dataGrid.apiCellValue(0, 0, 'test'); + await dataGrid.apiSaveEditData(); + + await expect(dataGrid.getErrorRow()).toBeVisible(); + + await testScreenshot(page, 'error-row.png', { element: page.locator('#container') }); + }); + + test('Batch editing mode - edit cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'batch', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: [ + 'field_1', + 'field_2', + 'field_3', + 'field_4', + ], + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await dataGrid.apiEditCell(0, 0); + + await expect(dataGrid.getDataRow(0).getDataCell(0)).toHaveClass(/dx-editor-cell/); + + await testScreenshot(page, 'batch-editing-mode-edit_cell.png', { element: page.locator('#container') }); + }); + + test('Batch editing mode - modified cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'batch', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: [ + 'field_1', + 'field_2', + 'field_3', + 'field_4', + ], + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await dataGrid.apiCellValue(0, 0, 'test'); + + await expect(dataGrid.getDataRow(0).getDataCell(0)).toHaveClass(/dx-cell-modified/); + + await testScreenshot(page, 'row-editing-mode-modified_cell.png', { element: page.locator('#container') }); + }); + + test('Batch editing mode - delete row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + editing: { + mode: 'batch', + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, + columns: [ + 'field_1', + 'field_2', + 'field_3', + 'field_4', + ], + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + await dataGrid.apiDeleteRow(0); + + await expect(dataGrid.getDataRow(0).element).toHaveClass(/dx-row-removed/); + + await testScreenshot(page, 'row-editing-mode-delete_row.png', { element: page.locator('#container') }); + }); + + test('Context menu', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + columnFixing: { + enabled: true, + legacyMode: true, + } as any, + sorting: { + mode: 'multiple', + }, + }); + + const dataGrid = new DataGrid(page); + expect(await dataGrid.isReady()).toBeTruthy(); + + const headerRow = dataGrid.getHeaderRow(0); + await headerRow.click({ button: 'right' }); + + const contextMenu = dataGrid.getContextMenu(); + await expect(contextMenu.element).toBeVisible(); + + await testScreenshot(page, 'context-menu.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts new file mode 100644 index 000000000000..77688d90c087 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +test.describe('DataGrid - contrast', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('DataGrid - Contrast between icons in the Filter Row menu and their background doesn\'t comply with WCAG accessibility standards', async ({ page }) => { + // TODO: Playwright migration - filter menu button not visible (requires hover before click) + await createWidget(page, 'dxDataGrid', { + dataSource: getData(3, 3), + filterRow: { visible: true }, + }); + + const dataGrid = new DataGrid(page); + const filterCell = dataGrid.getFilterCell(0); + const menuButton = filterCell.locator('.dx-editor-with-menu .dx-menu'); + + await menuButton.click(); + + await testScreenshot(page, 'filter-row-menu-contrast-T1257970.png', { + element: '#container', + }); + }); + + test('DataGrid - Filter icon should remain visible when it\'s focused', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(3, 3), + filterRow: { visible: true }, + headerFilter: { visible: true }, + }); + + const dataGrid = new DataGrid(page); + const headerFilterIcon = page.locator('.dx-header-filter').first(); + await headerFilterIcon.click(); + + await testScreenshot(page, 'T1286345-datagrid-menu-icon-when-focused.png', { + element: '#container', + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.spec.ts new file mode 100644 index 000000000000..8b2f5ec8f0cd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Adaptive Row', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 400, height: 400 }); + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should be shown and hidden when the window is resized', async ({ page }) => { + await page.setViewportSize({ width: 400, height: 400 }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Head_ID: -1, + Full_Name: 'John Heart', + Prefix: 'Mr.', + Title: 'CEO', + City: 'Los Angeles', + State: 'California', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + Mobile_Phone: '(213) 555-9392', + Birth_Date: '1964-03-16', + Hire_Date: '1995-01-15', + }], + keyExpr: 'ID', + allowColumnResizing: true, + rowDragging: { + allowDropInsideItem: true, + allowReordering: true, + }, + columns: [ + { + dataField: 'Title', + caption: 'Position', + hidingPriority: 0, + fixed: true, + }, + { dataField: 'Full_Name', hidingPriority: 1 }, + { dataField: 'City', hidingPriority: 2 }, + { dataField: 'State', hidingPriority: 3 }, + { dataField: 'Mobile_Phone', hidingPriority: 4 }, + { dataField: 'Hire_Date', dataType: 'date', hidingPriority: 5 }, + ], + }); + + const dataGrid = new DataGrid(page); + + await expect(page.locator('.dx-datagrid').first()).toBeVisible(); + + const adaptiveButton = dataGrid.getAdaptiveButton(); + await expect(adaptiveButton).toBeVisible(); + await adaptiveButton.click({ force: true }); + + await expect(dataGrid.getAdaptiveRow(0).element).toBeVisible(); + + await page.setViewportSize({ width: 1200, height: 400 }); + await dataGrid.apiUpdateDimensions(); + + expect(await dataGrid.isAdaptiveColumnHidden()).toBeTruthy(); + await expect(dataGrid.getAdaptiveRow(0).element).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts new file mode 100644 index 000000000000..d3b5c48d30b8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Adaptivity', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('The AI column should be hidden when columnHidingEnabled is true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 350, + columnWidth: 100, + columnHidingEnabled: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + }, + ], + }); + + await expect(page.locator('.dx-datagrid').first()).toBeVisible(); + + const fourthHeaderCell = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + await expect(fourthHeaderCell).toHaveText('AI Column'); + await expect(fourthHeaderCell).toBeHidden(); + + await expect(page.locator('.dx-data-row').nth(0).locator('.dx-command-adaptive')).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.spec.ts new file mode 100644 index 000000000000..1064bee412d3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column - Column Chooser.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('The AI column can be hidden when columnChooser.mode is "dragAndDrop"', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 600, + columnWidth: 200, + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + }, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').showColumnChooser()); + await expect(dataGrid.getColumnChooser()).toBeVisible(); + + await dataGrid.apiColumnOption('myAiColumn', 'visible', false); + + const isVisible = await dataGrid.apiColumnOption('myAiColumn', 'visible'); + expect(isVisible).toBe(false); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts new file mode 100644 index 000000000000..26dad911df1b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column - Sticky columns.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test.skip('The AI column should not be fixed when the columnFixing.enabled option is true', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (aiHeader.element.textContent, aiHeader.isSticky) + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 600, + columnWidth: 200, + columnFixing: { + enabled: true, + }, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + const aiHeader = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + // assert + expect(await aiHeader.element.textContent).toBe('AI Column'); + expect(await aiHeader.isSticky()).toBeFalsy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts new file mode 100644 index 000000000000..5db3e4c2709a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column - Sticky columns.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test.skip('Check context menu items', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (t.rightClick, dataGrid undefined) + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 600, + columnWidth: 200, + columnFixing: { + enabled: true, + }, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await t.rightClick(page.locator('.dx-header-row').nth(0).locator('td').nth(0)); + await (dataGrid.getContextMenu().getItemByText('Set Fixed Position')).click(); + + await testScreenshot(page, 'datagrid__ai-column-and-sticky-columns__context-menu.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts new file mode 100644 index 000000000000..2369b9d71a4e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnReordering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('Column reordering should work when allowColumnReordering is true', async ({ page }) => { + // TODO: Playwright migration - jQuery pointer event simulation does not trigger column reordering + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnReordering: true, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + const columnsBefore = await dataGrid.apiGetVisibleColumns(); + const firstColumnBefore = columnsBefore[0]?.name; + + await dataGrid.moveHeader(0, 200, 0, true); + await dataGrid.dropHeader(0); + + const columnsAfter = await dataGrid.apiGetVisibleColumns(); + const firstColumnAfter = columnsAfter[0]?.name; + + expect(firstColumnAfter).not.toBe(firstColumnBefore); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.spec.ts new file mode 100644 index 000000000000..a82bb36fbfc5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnReordering.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test.skip('The draggable AI column should display correctly', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (dataGrid undefined, t.notOk, compareResults) + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnReordering: true, + columnWidth: 200, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.moveHeader(0, 100, 5, true); + + // assert + expect(await dataGrid.getDraggableHeader().visible).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__dragging.png', { element: page.locator('#container') }); + + // act + await dataGrid.dropHeader(0); + + // assert + expect(await dataGrid.getDraggableHeader().visible); + await t.notOk(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts new file mode 100644 index 000000000000..c84e9b5807a5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnResizing.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + (['nextColumn', 'widget'] as const).forEach((columnResizingMode) => { + test.skip(`Column resizing should work when allowColumnResizing is true (columnResizingMode = ${columnResizingMode})`, async ({ page }) => { + // TODO: Playwright migration - jQuery pointer event simulation does not trigger column resizing + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnResizing: true, + columnResizingMode, + columnWidth: 150, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + const initialWidth = await dataGrid.apiColumnOption('myAiColumn', 'width') as number; + await dataGrid.resizeHeader(0, 50); + + const newWidth = await dataGrid.apiColumnOption('myAiColumn', 'width') as number; + expect(newWidth).not.toBe(initialWidth); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.spec.ts new file mode 100644 index 000000000000..9ec0277fff38 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnResizing.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test.skip('Resize AI Column when wordWrapEnabled is true', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (dataGrid undefined) + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnResizing: true, + wordWrapEnabled: true, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column AI Column', + width: 250, + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__column-resizing(wordWrapEnabled=true)-1.png', { element: page.locator('#container') }); + + // act + await dataGrid.resizeHeader(1, -150); + + await testScreenshot(page, 'datagrid__ai-column__column-resizing(wordWrapEnabled=true)-2.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts new file mode 100644 index 000000000000..417f722411ac --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const EMPTY_CELL_TEXT = '\u00A0'; + const DROPDOWNMENU_PROMPT_EDITOR_INDEX = 0; + const DROPDOWNMENU_REGENERATE_INDEX = 1; + const DROPDOWNMENU_CLEAR_DATA_INDEX = 2; + + test.skip('The AI column with a given width', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (locator.clientWidth) + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + width: 175, + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // assert + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(3).clientWidth).toBe(175); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts new file mode 100644 index 000000000000..19168e751bc2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.KeyboardNavigation.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test.skip('Check keyboard navigation for AI column', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (element.click, element.focused, getAIDropDownButton, t.ok) + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnReordering: true, + columnWidth: 200, + columns: [ + { dataField: 'id', caption: 'ID' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + const headerRow = page.locator('.dx-header-row').nth(0); + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await (headerRow.locator('td').nth(0).element).click(); + await page.keyboard.press('tab'); + + // assert + expect(await headerRow.locator('.dx-command-edit').nth(1).element.focused).toBeTruthy(); + + // act + await page.keyboard.press('tab'); + + // assert + expect(await headerRow.locator('.dx-command-edit').nth(1).getAIDropDownButton().isFocused).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__focused-dropdown-button.png', { element: page.locator('#container') }); + + // act + await page.keyboard.press('tab'); + + // assert + expect(await headerRow.locator('td').nth(2).isFocused); + await t.ok(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts new file mode 100644 index 000000000000..a10d69d3edbd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Virtual Scrolling.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('DataGrid should send an AI request for rendered rows after scrolling without changing the page index', async ({ page }) => { + // TODO: Playwright migration - DevExpress.aiIntegration is not a constructor in test environment + await createWidget(page, 'dxDataGrid', () => { + const generateData = (rowCount: number): Record[] => { + const result: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + result.push({ id: i + 1, name: `Name ${i + 1}`, value: (i + 1) * 10 }); + } + + return result; + }; + + return { + dataSource: generateData(200), + height: 500, + keyExpr: 'id', + paging: { + pageSize: 50, + }, + scrolling: { + mode: 'virtual', + }, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + prompt: 'Initial prompt', + // eslint-disable-next-line new-cap + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name}`; + }); + + (window as any).aiResponseData = JSON.stringify(result); + (window as any).aiResolve = resolve; + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + }; + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + await page.evaluate(() => { + const resolve = (window as any).aiResolve; + const data = (window as any).aiResponseData; + if (resolve && data) { + resolve(data); + } + }); + + await page.waitForTimeout(500); + + const pageIndexBefore = await dataGrid.apiPageIndex() as number; + + await dataGrid.scrollTo({ top: 500 }); + await page.waitForTimeout(500); + + const pageIndexAfter = await dataGrid.apiPageIndex() as number; + expect(pageIndexAfter).toBe(pageIndexBefore); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts new file mode 100644 index 000000000000..1f1d5927f7d3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test.skip('Default render', async ({ page }) => { + // TODO: Playwright migration - screenshot mismatch + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__default.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts new file mode 100644 index 000000000000..9a07b290167c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Band columns: runtime change', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const GRID_CONTAINER = '#container'; + + const dataSource = [ + { id: 0, A: 'A_0', B: 0 }, + { id: 1, A: 'A_1', B: 1 }, + { id: 2, A: 'A_2', B: 2 }, + ]; + + const lookUpDataSource = [ + { id: 0, text: 'Lookup_value_0' }, + { id: 1, text: 'Lookup_value_1' }, + { id: 2, text: 'Lookup_value_2' }, + ]; + + const nestedColumns = [ + { dataField: 'A' }, + { + name: 'Nested', + caption: 'Nested', + columns: [ + { + dataField: 'B', + lookup: { + dataSource: lookUpDataSource, + valueExpr: 'id', + displayExpr: 'text', + }, + }, + ], + }, + ]; + + test.skip('Should change usual columns to band columns without error in React (T1213679)', async ({ page }) => { + // TODO: Playwright migration - screenshot mismatch + await createWidget(page, 'dxDataGrid', { + dataSource: [...dataSource], + columns: [ + { dataField: 'A' }, + { + dataField: 'B', + lookup: { + dataSource: lookUpDataSource, + valueExpr: 'id', + displayExpr: 'text', + }, + }, + ], + keyExpr: 'id', + showBorders: true, + }); + + await expect(page.locator('.dx-datagrid').first()).toBeVisible(); + + await testScreenshot(page, 'band-columns_before-runtime-update.png', { element: page.locator('#container') }); + + await page.evaluate(({ gridContainer, nested }) => { + const dataGridWidget = ($(gridContainer) as any).dxDataGrid('instance'); + + dataGridWidget.beginUpdate(); + + dataGridWidget.option('columns[1].dataField', undefined); + dataGridWidget.option('columns[1].lookup', undefined); + dataGridWidget.option('columns[1].columns', nested[1].columns); + dataGridWidget.option('columns[1].name', nested[1].name); + dataGridWidget.option('columns[1].caption', nested[1].caption); + + dataGridWidget.endUpdate(); + }, { gridContainer: GRID_CONTAINER, nested: nestedColumns }); + + await testScreenshot(page, 'band-columns_after-runtime-update.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts new file mode 100644 index 000000000000..cf509c6b2f8e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Filter Builder', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Field menu should be opened on field click if window scroll exists (T852701)', async ({ page }) => { + const filter = [] as any[]; + const fields = [] as any[]; + + for (let i = 1; i <= 50; i += 1) { + if (i > 1) { + filter.push('or'); + } + const name = `Test${i}`; + filter.push([name, '=', 'Test']); + fields.push({ dataField: name }); + } + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + }); + + await page.evaluate(() => window.scrollTo(0, 10000)); + + const lastFieldButton = page.locator('.dx-filterbuilder-item-field').last(); + await lastFieldButton.click(); + + const popupTreeView = page.locator('.dx-treeview.dx-widget'); + await expect(popupTreeView).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts new file mode 100644 index 000000000000..ee934814ee52 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts @@ -0,0 +1,223 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +test.describe('Column chooser', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + (['dragAndDrop', 'select'] as const).forEach((mode) => { + test(`Column chooser screenshot in mode=${mode}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 3), + height: 400, + showBorders: true, + columns: [{ + dataField: 'field_0', + dataType: 'string', + }, { + dataField: 'field_1', + dataType: 'string', + }, { + dataField: 'field_2', + dataType: 'string', + visible: false, + }], + columnChooser: { + enabled: true, + mode, + }, + }); + + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').showColumnChooser()); + + const columnChooser = page.locator('.dx-datagrid-column-chooser').last(); + await expect(columnChooser).toBeVisible(); + + await testScreenshot(page, `column-chooser-${mode}-mode.png`, { element: page.locator('#container') }); + }); + }); + + test('Empty column chooser', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 5), + keyExpr: 'field_0', + columnChooser: { enabled: true }, + columns: ['field_0', 'field_1', 'field_2', 'field_3', 'field_4'], + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.getColumnChooserButton().click(); + + const columnChooser = dataGrid.getColumnChooser(); + await expect(columnChooser).toBeVisible(); + + await testScreenshot(page, 'empty-column-chooser.png'); + }); + + test('Column chooser checkboxes should be aligned correctly with plain structure', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + columns: ['field1', 'field2', 'field3'], + width: 700, + columnChooser: { + enabled: true, + mode: 'select', + search: { enabled: true }, + selection: { allowSelectAll: true }, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.getHeaderPanel().getColumnChooserButton().click(); + await page.waitForTimeout(100); + + const columnChooser = dataGrid.getColumnChooser(); + await expect(columnChooser).toBeVisible(); + + await testScreenshot(page, 'column-chooser-checkbox-alignment-plain-structure.png', { element: columnChooser }); + }); + + test('Column chooser checkboxes should be aligned correctly with tree structure', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + columns: [ + 'field1', + { caption: 'band1', columns: ['field2', 'field3'] }, + ], + width: 700, + columnChooser: { + enabled: true, + mode: 'select', + search: { enabled: true }, + selection: { allowSelectAll: true }, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.getHeaderPanel().getColumnChooserButton().click(); + await page.waitForTimeout(100); + + const columnChooser = dataGrid.getColumnChooser(); + await expect(columnChooser).toBeVisible(); + + await testScreenshot(page, 'column-chooser-checkbox-alignment-tree-structure.png', { element: columnChooser }); + }); + + test('Column chooser should support string height and width', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + columns: ['field1', 'field2', 'field3'], + width: 700, + columnChooser: { + enabled: true, + height: '400px', + width: '330px', + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.getHeaderPanel().getColumnChooserButton().click(); + await page.waitForTimeout(100); + + const columnChooserContent = dataGrid.getColumnChooser().locator('.dx-popup-content'); + const height = await columnChooserContent.evaluate((el) => getComputedStyle(el).height); + const width = await columnChooserContent.evaluate((el) => getComputedStyle(el).width); + + expect(height).toBe('400px'); + expect(width).toBe('330px'); + }); + + test('Should take into account column options change during general option change (T1267471)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 0, A: 'A', B: 'B' }], + keyExpr: 'id', + columns: ['A', 'B'], + columnChooser: { enabled: true, mode: 'select' }, + onOptionChanged: ({ component, fullName }: any) => { + if (!/columns\[\d+\]\.visible/.test(fullName)) return; + const visibleColumns = component.getVisibleColumns(); + const [{ dataField: lastColumnDataField }] = visibleColumns; + if (!lastColumnDataField) return; + component.columnOption(lastColumnDataField, 'allowHiding', false); + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.getColumnChooserButton().click(); + await page.waitForTimeout(100); + + const columnChooser = dataGrid.getColumnChooser(); + await expect(columnChooser).toBeVisible(); + + const checkboxes = columnChooser.locator('.dx-checkbox'); + const isDisabled0 = await checkboxes.nth(0).evaluate((el) => el.classList.contains('dx-state-disabled')); + const isDisabled1 = await checkboxes.nth(1).evaluate((el) => el.classList.contains('dx-state-disabled')); + + expect(isDisabled0).toBeFalsy(); + expect(isDisabled1).toBeFalsy(); + + await checkboxes.nth(1).click(); + await page.waitForTimeout(100); + + const isDisabled0After = await checkboxes.nth(0).evaluate((el) => el.classList.contains('dx-state-disabled')); + expect(isDisabled0After).toBeTruthy(); + }); + + test('ColumnChooser should receive and render custom texts', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.localization.loadMessages({ + en: { + 'dxDataGrid-columnChooserTitle': 'customTitle', + 'dxDataGrid-columnChooserEmptyText': 'customEmptyText', + }, + }); + }); + + await createWidget(page, 'dxDataGrid', { + columnChooser: { + height: '340px', + enabled: true, + mode: 'dragAndDrop', + position: { + my: 'right top', + at: 'right bottom', + of: '.dx-datagrid-column-chooser-button', + }, + }, + dataSource: [], + columns: [], + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.getColumnChooserButton().click(); + await page.waitForTimeout(100); + + const columnChooser = dataGrid.getColumnChooser(); + await expect(columnChooser).toBeVisible(); + + const titleText = await columnChooser.locator('.dx-popup-title').textContent(); + const emptyText = await columnChooser.locator('.dx-datagrid-column-chooser-plain').textContent(); + + expect(titleText?.trim()).toBe('customTitle'); + expect(emptyText?.trim()).toBe('customEmptyText'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts new file mode 100644 index 000000000000..3aed65fd8fdc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts @@ -0,0 +1,190 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column reordering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('The column reordering should work correctly when there is a fixed column with zero width', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 800, + dataSource: [ + { + field1: 'test1', field2: 'test2', field3: 'test3', field4: 'test4', + }, + ], + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { + dataField: 'field1', + fixed: true, + width: 200, + }, { + name: 'fake', + fixed: true, + width: 0.01, + }, { + dataField: 'field2', + width: 200, + }, { + dataField: 'field3', + width: 200, + }, { + dataField: 'field4', + width: 200, + }, + ], + allowColumnReordering: true, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + const columnsBefore = await dataGrid.apiGetVisibleColumns(); + + await dataGrid.moveHeader(2, 200, 0, true); + await dataGrid.dropHeader(2); + + const columnsAfter = await dataGrid.apiGetVisibleColumns(); + expect(columnsAfter.length).toBe(columnsBefore.length); + }); + + test('Column without allowReordering should have same position after dragging to groupPanel and back', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 800, + dataSource: [{ field1: 'test1', field2: 'test2', field3: 'test3', field4: 'test4' }], + groupPanel: { visible: true }, + columns: [ + { dataField: 'field1', width: 200 }, + { dataField: 'field2', width: 200 }, + { dataField: 'field3', width: 200 }, + { dataField: 'field4', width: 200 }, + ], + allowColumnReordering: false, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + const columnsBefore = await dataGrid.apiGetVisibleColumns(); + expect(columnsBefore.map((c) => c.dataField)).toEqual(['field1', 'field2', 'field3', 'field4']); + + await dataGrid.moveHeader(2, -100, -50, true); + await dataGrid.dropHeader(2); + + await page.waitForTimeout(300); + + const columnsAfter = await dataGrid.apiGetVisibleColumns(); + expect(columnsAfter.map((c) => c.dataField)).toEqual(['field1', 'field2', 'field3', 'field4']); + }); + + test('Column reordering should work correctly with fixed columns on the right and columnRenderingMode is virtual', async ({ page }) => { + const columns = Array.from({ length: 19 }, (_, i) => ({ + dataField: String(i + 1), + ...(i >= 17 ? { fixed: true, fixedPosition: 'right' } : {}), + })); + + await createWidget(page, 'dxDataGrid', { + dataSource: [{}], + width: 800, + allowColumnReordering: true, + columnWidth: 100, + scrolling: { columnRenderingMode: 'virtual' }, + columns, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + const visibleCols = await dataGrid.apiGetVisibleColumns(); + expect(visibleCols.length).toBe(19); + }); + + test('Column reordering should work correctly after scrolling right with fixed columns on the left', async ({ page }) => { + const columns = [ + { dataField: '1', fixed: true, fixedPosition: 'left' }, + { dataField: '2', fixed: true, fixedPosition: 'left' }, + ...Array.from({ length: 17 }, (_, i) => ({ dataField: String(i + 3) })), + ]; + + await createWidget(page, 'dxDataGrid', { + dataSource: [{}], + width: 800, + allowColumnReordering: true, + columnWidth: 100, + scrolling: { columnRenderingMode: 'virtual' }, + columns, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + await dataGrid.scrollTo({ x: 10000 }); + await page.waitForTimeout(300); + + const scrollLeft = await dataGrid.getScrollLeft(); + expect(scrollLeft).toBeGreaterThan(0); + }); + + test('Dragging a fixed column to a group panel should work correctly when columnRenderingMode is virtual', async ({ page }) => { + const columns = [ + ...Array.from({ length: 17 }, (_, i) => ({ dataField: String(i + 1) })), + { dataField: '18', fixed: true, fixedPosition: 'right' }, + { dataField: '19', fixed: true, fixedPosition: 'right' }, + ]; + + await createWidget(page, 'dxDataGrid', { + dataSource: [{}], + width: 800, + allowColumnReordering: true, + columnWidth: 100, + scrolling: { columnRenderingMode: 'virtual' }, + groupPanel: { visible: true }, + columns, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + const visibleCols = await dataGrid.apiGetVisibleColumns(); + expect(visibleCols.length).toBe(19); + }); + + test('Dragging a fixed column to a column chooser should work when columnRenderingMode is virtual', async ({ page }) => { + const columns = [ + ...Array.from({ length: 17 }, (_, i) => ({ dataField: String(i + 1) })), + { dataField: '18', fixed: true, fixedPosition: 'right' }, + { dataField: '19', fixed: true, fixedPosition: 'right' }, + ]; + + await createWidget(page, 'dxDataGrid', { + dataSource: [{}], + width: 800, + height: 500, + allowColumnReordering: true, + columnWidth: 100, + scrolling: { columnRenderingMode: 'virtual' }, + columnChooser: { enabled: true }, + columns, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + await page.locator('.dx-datagrid-column-chooser-button').click(); + const columnChooser = page.locator('.dx-datagrid-column-chooser'); + await expect(columnChooser).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.spec.ts new file mode 100644 index 000000000000..bba44b75392a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column reordering.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('column separator should work properly with expand columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 800, + dataSource: [ + { + field1: 'test1', field2: 'test2', field3: 'test3', field4: 'test4', + }, + ], + groupPanel: { + visible: true, + }, + columns: [ + { + dataField: 'field1', + width: 200, + groupIndex: 0, + }, { + dataField: 'field2', + width: 200, + groupIndex: 1, + }, { + dataField: 'field3', + width: 200, + }, { + dataField: 'field4', + width: 200, + }, + ], + allowColumnReordering: true, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + const groupPanel = dataGrid.getGroupPanel(); + await expect(groupPanel).toBeVisible(); + + await testScreenshot(page, 'column-separator-with-expand-columns.png', { + element: '#container', + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts new file mode 100644 index 000000000000..ff5881ce698f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts @@ -0,0 +1,113 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const bandColumnDataSource = [{ + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Rural: 0.15, + Population_Total: 205809000, +}]; + +const bandColumnConfig = { + dataSource: bandColumnDataSource, + keyExpr: 'ID', + allowColumnResizing: true, + columnResizingMode: 'widget', + width: 500, + columns: [ + { + dataField: 'ID', + fixed: true, + allowReordering: false, + width: 50, + }, + { + caption: 'Population', + columns: [ + { + dataField: 'Country', + showWhenGrouped: true, + width: 100, + groupIndex: 0, + }, + { dataField: 'Area' }, + { dataField: 'Population_Total' }, + { dataField: 'Population_Urban' }, + { dataField: 'Population_Rural' }, + ], + }, + ], +}; + +test.describe('Column resizing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1314667 + test('Resize indicator is moved when resizing a grouped column if showWhenGrouped is set to true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', bandColumnConfig); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + await dataGrid.resizeHeader(3, 30, true); + + const cellWidth = await page.evaluate(() => { + const cell = document.querySelector('.dx-header-row:nth-child(2) td:nth-child(1)') as HTMLElement; + return cell ? cell.offsetWidth : 0; + }); + + expect(cellWidth).toBeGreaterThanOrEqual(128); + expect(cellWidth).toBeLessThanOrEqual(130); + }); + + // T1317039 + test('Columns should not be resized from band area', async ({ page }) => { + await createWidget(page, 'dxDataGrid', bandColumnConfig); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + await page.evaluate(({ columnIndex, offset }: { columnIndex: number; offset: number }) => { + const instance = ($('#container') as any).dxDataGrid('instance'); + const columnHeadersView = instance.getView('columnHeadersView'); + const $header = $(columnHeadersView.getHeaderElement(columnIndex)); + const headerOffset = ($header as any).offset(); + + const triggerPointerEvent = ($element: any, eventName: string, x: number, y: number) => { + $element.trigger($.Event(eventName, { + pageX: x, + pageY: y, + pointers: [{ pointerId: 1 }], + })); + }; + + triggerPointerEvent($(document), 'dxpointermove', headerOffset.left, headerOffset.top - 10); + triggerPointerEvent($('#container'), 'dxpointerdown', headerOffset.left, headerOffset.top - 10); + triggerPointerEvent($(document), 'dxpointermove', headerOffset.left + offset, headerOffset.top - 10); + triggerPointerEvent($(document), 'dxpointerup', headerOffset.left + offset, headerOffset.top - 10); + }, { columnIndex: 3, offset: 30 }); + + const cellWidth = await page.evaluate(() => { + const cells = document.querySelectorAll('.dx-header-row'); + const secondHeaderRow = cells[1]; + if (!secondHeaderRow) return 0; + const firstCell = secondHeaderRow.querySelector('td') as HTMLElement; + return firstCell ? firstCell.offsetWidth : 0; + }); + + expect(cellWidth).toBeGreaterThanOrEqual(98); + expect(cellWidth).toBeLessThanOrEqual(100); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts new file mode 100644 index 000000000000..681d5bf6611c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts @@ -0,0 +1,129 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column resizing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('column separator should starts from the parent', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (dataGrid undefined, t.dispatchEvent) + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Rural: 0.15, + Population_Total: 205809000, + GDP_Agriculture: 0.054, + GDP_Industry: 0.274, + GDP_Services: 0.672, + GDP_Total: 2353025, + }], + keyExpr: 'ID', + columnWidth: 100, + allowColumnResizing: true, + showBorders: true, + editing: { + allowUpdating: true, + }, + columns: ['Country', { + dataField: 'Population_Total', + visible: false, + }, { + caption: 'Population', + columns: ['Population_Rural', { + caption: 'By Sector', + columns: ['GDP_Total', { + caption: 'not resizable', + dataField: 'ID', + allowResizing: false, + }, 'GDP_Agriculture', 'GDP_Industry'], + }], + }, { + caption: 'Nominal GDP', + columns: ['GDP_Total', 'Population_Urban'], + }, 'Area'], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + async function makeColumnSeparatorScreenshot(index: number) { + await dataGrid.resizeHeader(index, 0, false); + await testScreenshot(page, `column-separator-${index}.png`); + + await t.dispatchEvent(page.locator('#container'), 'mouseup'); + } + + await makeColumnSeparatorScreenshot(1); + await makeColumnSeparatorScreenshot(2); + await makeColumnSeparatorScreenshot(3); + await makeColumnSeparatorScreenshot(4); + await makeColumnSeparatorScreenshot(5); + await makeColumnSeparatorScreenshot(6); + await makeColumnSeparatorScreenshot(7); + await makeColumnSeparatorScreenshot(8); + await makeColumnSeparatorScreenshot(9); + }); + + test('DataGrid with allowColumnResizing renders correctly', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Rural: 0.15, + Population_Total: 205809000, + }], + keyExpr: 'ID', + columnWidth: 100, + allowColumnResizing: true, + showBorders: true, + columns: ['Country', 'Area', 'Population_Urban', 'Population_Rural'], + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + const columnsSeparators = dataGrid.element.locator('.dx-datagrid-columns-separator'); + expect(await columnsSeparators.count()).toBeGreaterThan(0); + }); + + test('Column resize with widget mode preserves correct widths', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Total: 205809000, + }], + keyExpr: 'ID', + allowColumnResizing: true, + columnResizingMode: 'widget', + showBorders: true, + columns: [ + { dataField: 'ID', width: 100 }, + { dataField: 'Country', width: 150 }, + { dataField: 'Area', width: 150 }, + { dataField: 'Population_Total', width: 200 }, + ], + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + const headerCells = dataGrid.getHeaderRow(0).locator('td'); + const firstCellWidth = await headerCells.nth(0).evaluate((el) => (el as HTMLElement).offsetWidth); + expect(firstCellWidth).toBeGreaterThan(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts new file mode 100644 index 000000000000..830c66e99a62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts @@ -0,0 +1,131 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing - cell focus', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1154721 + test('Should allow focus next editor in the same column after save changes with local data source', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [{ + id: 0, + data: 'A', + }, { + id: 1, + data: 'B', + }, { + id: 2, + data: 'C', + }], + editing: { + allowUpdating: true, + refreshMode: 'repaint', + mode: 'cell', + }, + columns: [{ + dataField: 'data', + showEditorAlways: true, + }], + repaintChangesOnly: true, + }); + + const dataGrid = new DataGrid(page); + + const firstEditor = dataGrid.getDataCell(0, 0).element.locator('.dx-texteditor-input'); + const secondEditor = dataGrid.getDataCell(2, 0).element.locator('.dx-texteditor-input'); + const middleCell = dataGrid.getDataCell(1, 0).element; + + await firstEditor.click(); + await firstEditor.pressSequentially(' AAA'); + await secondEditor.click(); + await secondEditor.pressSequentially(' CCC'); + await middleCell.click(); + + const firstCellValue = await firstEditor.inputValue(); + const secondCellValue = await secondEditor.inputValue(); + + expect(firstCellValue).toBe('A AAA'); + expect(secondCellValue).toBe('C CCC'); + }); + + // T1037019 + test('Should allow focus next editor in the same column after save changes with remote data source', async ({ page }) => { + await page.route('**/api/data', (route) => { + route.fulfill({ + status: 200, + headers: { 'access-control-allow-origin': '*' }, + body: JSON.stringify({ + data: [ + { id: 0, data: 'A' }, + { id: 1, data: 'B' }, + { id: 2, data: 'C' }, + ], + }), + }); + }); + + await page.route('**/api/update', (route) => { + route.fulfill({ + status: 200, + headers: { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': '*', + }, + body: JSON.stringify({}), + }); + }); + + await createWidget(page, 'dxDataGrid', () => ({ + keyExpr: 'id', + dataSource: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + updateUrl: 'https://api/update', + }), + editing: { + allowUpdating: true, + refreshMode: 'repaint', + mode: 'cell', + }, + columns: [{ + dataField: 'data', + showEditorAlways: true, + }], + repaintChangesOnly: true, + })); + + const dataGrid = new DataGrid(page); + + await page.waitForFunction(() => { + const instance = ($('#container') as any).dxDataGrid('instance'); + return instance && instance.getVisibleRows().length > 0; + }); + + const firstEditor = dataGrid.getDataCell(0, 0).element.locator('.dx-texteditor-input'); + const secondEditor = dataGrid.getDataCell(2, 0).element.locator('.dx-texteditor-input'); + const middleCell = dataGrid.getDataCell(1, 0).element; + + await firstEditor.click(); + await firstEditor.pressSequentially(' AAA'); + await secondEditor.click(); + await secondEditor.pressSequentially(' CCC'); + await middleCell.click(); + + const firstCellValue = await firstEditor.inputValue(); + const secondCellValue = await secondEditor.inputValue(); + + expect(firstCellValue).toBe('A AAA'); + expect(secondCellValue).toBe('C CCC'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts new file mode 100644 index 000000000000..7860d26a4bc2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts @@ -0,0 +1,338 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const READONLY_CLASS = 'dx-datagrid-readonly'; +const CELL_FOCUS_DISABLED_CLASS = 'dx-cell-focus-disabled'; + +test.describe('Editing - showEditorAlways cell in new row should be editable (T1323684)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + (['cell', 'batch'] as const).forEach((mode) => { + const testFn = mode === 'batch' ? test.skip : test; + testFn(`showEditorAlways editor should be editable in a new row when allowUpdating is false, ${mode} mode`, async ({ page }) => { + // TODO: Playwright migration - batch mode: hasReadonly is true instead of false + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart' }, + { ID: 2, FirstName: 'Olivia', LastName: 'Peyton' }, + ], + showBorders: true, + editing: { + mode, + allowUpdating: false, + allowAdding: true, + }, + columns: [ + 'LastName', + { dataField: 'FirstName', showEditorAlways: true }, + ], + }); + + const dataGrid = new DataGrid(page); + const addRowButton = dataGrid.getHeaderPanel().getAddRowButton(); + + await addRowButton.click(); + + const newRowCell = dataGrid.getDataCell(0, 1); + + const hasReadonly = await newRowCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonly).toBe(false); + + const hasFocusDisabled = await newRowCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + CELL_FOCUS_DISABLED_CLASS, + ); + expect(hasFocusDisabled).toBe(false); + + const editor = newRowCell.element.locator('.dx-texteditor-input'); + await editor.click(); + await editor.fill('test value'); + + const editorValue = await editor.inputValue(); + expect(editorValue).toBe('test value'); + }); + + test(`Boolean editor should be editable in a new row when allowUpdating is false, ${mode} mode`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, Name: 'John', Active: false }, + { ID: 2, Name: 'Olivia', Active: true }, + ], + showBorders: true, + editing: { + mode, + allowUpdating: false, + allowAdding: true, + }, + columns: [ + 'Name', + { dataField: 'Active', dataType: 'boolean' }, + ], + }); + + const dataGrid = new DataGrid(page); + const addRowButton = dataGrid.getHeaderPanel().getAddRowButton(); + + await addRowButton.click(); + + const newRowBoolCell = dataGrid.getDataCell(0, 1); + + const hasReadonly = await newRowBoolCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonly).toBe(false); + + const checkbox = newRowBoolCell.element.locator('.dx-checkbox'); + await checkbox.click(); + + const isChecked = await newRowBoolCell.element.evaluate( + (el) => el.querySelector('.dx-checkbox')?.getAttribute('aria-checked') === 'true', + ); + expect(isChecked).toBe(true); + }); + + test(`showEditorAlways editor in existing rows should remain readonly when allowUpdating is false, ${mode} mode`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart' }, + { ID: 2, FirstName: 'Olivia', LastName: 'Peyton' }, + ], + showBorders: true, + editing: { + mode, + allowUpdating: false, + allowAdding: true, + }, + columns: [ + 'LastName', + { dataField: 'FirstName', showEditorAlways: true }, + ], + }); + + const dataGrid = new DataGrid(page); + const existingCell = dataGrid.getDataCell(0, 1); + + const hasReadonlyBefore = await existingCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonlyBefore).toBe(true); + + const editor = existingCell.element.locator('.dx-texteditor-input'); + await editor.click(); + + const hasReadonlyAfter = await existingCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonlyAfter).toBe(true); + }); + }); + + test('showEditorAlways editor should be editable in a new row when allowUpdating is a function returning false, cell mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart' }, + { ID: 2, FirstName: 'Olivia', LastName: 'Peyton' }, + ], + showBorders: true, + editing: { + mode: 'cell', + allowUpdating: () => false, + allowAdding: true, + }, + columns: [ + 'LastName', + { dataField: 'FirstName', showEditorAlways: true }, + ], + }); + + const dataGrid = new DataGrid(page); + const addRowButton = dataGrid.getHeaderPanel().getAddRowButton(); + + await addRowButton.click(); + + const newRowCell = dataGrid.getDataCell(0, 1); + + const hasReadonly = await newRowCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonly).toBe(false); + + const hasFocusDisabled = await newRowCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + CELL_FOCUS_DISABLED_CLASS, + ); + expect(hasFocusDisabled).toBe(false); + + const editor = newRowCell.element.locator('.dx-texteditor-input'); + await editor.click(); + await editor.fill('test value'); + + const editorValue = await editor.inputValue(); + expect(editorValue).toBe('test value'); + }); + + (['cell', 'batch'] as const).forEach((mode) => { + test(`showEditorAlways editor should be editable in a new row when allowUpdating is false and allowAdding is a function returning true, ${mode} mode`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart' }, + ], + showBorders: true, + editing: { + mode, + allowUpdating: false, + allowAdding: () => true, + }, + columns: [ + 'LastName', + { dataField: 'FirstName', showEditorAlways: true }, + ], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const newRowCell = dataGrid.getDataCell(0, 1); + + const hasReadonly = await newRowCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonly).toBe(false); + }); + }); + + test('Boolean editor in new row should not have readonly class when allowUpdating is a function returning false, cell mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, Name: 'John', Active: false }, + ], + showBorders: true, + editing: { + mode: 'cell', + allowUpdating: () => false, + allowAdding: true, + }, + columns: [ + 'Name', + { dataField: 'Active', dataType: 'boolean' }, + ], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const newRowBoolCell = dataGrid.getDataCell(0, 1); + + const hasReadonly = await newRowBoolCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonly).toBe(false); + + const checkbox = newRowBoolCell.element.locator('.dx-checkbox'); + await checkbox.click(); + + const isChecked = await newRowBoolCell.element.evaluate( + (el) => el.querySelector('.dx-checkbox')?.getAttribute('aria-checked') === 'true', + ); + expect(isChecked).toBe(true); + }); + + test('showEditorAlways editor in existing row should remain readonly when allowUpdating is a function returning false, cell mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart' }, + ], + showBorders: true, + editing: { + mode: 'cell', + allowUpdating: () => false, + allowAdding: true, + }, + columns: [ + 'LastName', + { dataField: 'FirstName', showEditorAlways: true }, + ], + }); + + const dataGrid = new DataGrid(page); + const existingCell = dataGrid.getDataCell(0, 1); + + const hasReadonlyBefore = await existingCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonlyBefore).toBe(true); + + const editor = existingCell.element.locator('.dx-texteditor-input'); + await editor.click(); + + const hasReadonlyAfter = await existingCell.element.evaluate( + (el, cls) => el.classList.contains(cls), + READONLY_CLASS, + ); + expect(hasReadonlyAfter).toBe(true); + }); + + test('New row cells should not have cell-focus-disabled class when allowUpdating is false, cell mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart' }, + ], + showBorders: true, + editing: { + mode: 'cell', + allowUpdating: false, + allowAdding: true, + }, + columns: [ + { dataField: 'LastName', showEditorAlways: true }, + { dataField: 'FirstName', showEditorAlways: true }, + ], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const newRowCell0 = dataGrid.getDataCell(0, 0); + const newRowCell1 = dataGrid.getDataCell(0, 1); + + const hasFocusDisabled0 = await newRowCell0.element.evaluate( + (el, cls) => el.classList.contains(cls), + CELL_FOCUS_DISABLED_CLASS, + ); + expect(hasFocusDisabled0).toBe(false); + + const hasFocusDisabled1 = await newRowCell1.element.evaluate( + (el, cls) => el.classList.contains(cls), + CELL_FOCUS_DISABLED_CLASS, + ); + expect(hasFocusDisabled1).toBe(false); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts new file mode 100644 index 000000000000..a75b88bd498d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts @@ -0,0 +1,342 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.FunctionalMatrix', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Update cell value, mode: cell, repaintChangesOnly: true, useKeyboard: false, useMask: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, date: '2020-10-27', boolean: false, lookup: 1, + }, + { + id: 2, text: 'text 2', number: 2, date: '2020-10-28', boolean: true, lookup: 2, + }, + ], + repaintChangesOnly: true, + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { dataField: 'number' }, + { dataField: 'date', dataType: 'date' }, + { + dataField: 'lookup', + lookup: { + dataSource: [{ id: 1, text: 'lookup 1' }, { id: 2, text: 'lookup 2' }], + valueExpr: 'id', + displayExpr: 'text', + }, + }, + { dataField: 'boolean', dataType: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data) => data.number && -data.number + 1, + }, + ], + }); + + const dataGrid = new DataGrid(page); + const cell = dataGrid.getDataCell(0, 0); + + await cell.element.click(); + + const editor = cell.element.locator('.dx-texteditor-input'); + await editor.fill('xxxx'); + await page.keyboard.press('Tab'); + + const cellValue = await dataGrid.apiGetCellValue(0, 0); + expect(cellValue).toBe('xxxx'); + }); + + test('Update cell value, mode: batch, repaintChangesOnly: true, useKeyboard: false, useMask: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, date: '2020-10-27', boolean: false, lookup: 1, + }, + { + id: 2, text: 'text 2', number: 2, date: '2020-10-28', boolean: true, lookup: 2, + }, + ], + repaintChangesOnly: true, + editing: { + mode: 'batch', + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { dataField: 'number' }, + { dataField: 'date', dataType: 'date' }, + { + dataField: 'lookup', + lookup: { + dataSource: [{ id: 1, text: 'lookup 1' }, { id: 2, text: 'lookup 2' }], + valueExpr: 'id', + displayExpr: 'text', + }, + }, + { dataField: 'boolean', dataType: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data: any) => data.number && data.number + 1, + setCellValue: (newData: any, value: any) => { newData.number = value - 1; }, + }, + ], + }); + + const dataGrid = new DataGrid(page); + const cell = dataGrid.getDataCell(0, 0); + + await cell.element.click(); + + const editor = cell.element.locator('.dx-texteditor-input'); + await editor.fill('xxxx'); + await page.keyboard.press('Tab'); + + const cellValue = await dataGrid.apiGetCellValue(0, 0); + expect(cellValue).toBe('xxxx'); + }); + + test('Update calculated cell value, mode: cell, repaintChangesOnly: true, useKeyboard: false, useMask:false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, date: '2020-10-27', boolean: false, lookup: 1, + }, + { + id: 2, text: 'text 2', number: 2, date: '2020-10-28', boolean: true, lookup: 2, + }, + ], + repaintChangesOnly: true, + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { dataField: 'number' }, + { dataField: 'date', dataType: 'date' }, + { + dataField: 'lookup', + lookup: { + dataSource: [{ id: 1, text: 'lookup 1' }, { id: 2, text: 'lookup 2' }], + valueExpr: 'id', + displayExpr: 'text', + }, + }, + { dataField: 'boolean', dataType: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data: any) => (data as { number: number }).number + 1, + setCellValue: (newData: any, value: any) => { newData.number = value - 1; }, + }, + ], + }); + + const dataGrid = new DataGrid(page); + const calculatedCell = dataGrid.getDataCell(0, 5); + + await calculatedCell.element.click(); + + const editor = calculatedCell.element.locator('.dx-texteditor-input'); + await editor.fill('9'); + await page.keyboard.press('Tab'); + + const numberCellValue = await dataGrid.apiGetCellValue(0, 1); + expect(numberCellValue).toBe(8); + + const calculatedCellValue = await dataGrid.apiGetCellValue(0, 5); + expect(calculatedCellValue).toBe(9); + }); + + test('Update calculated cell value, mode: batch, repaintChangesOnly: false, useKeyboard: false, useMask:false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, date: '2020-10-27', boolean: false, lookup: 1, + }, + { + id: 2, text: 'text 2', number: 2, date: '2020-10-28', boolean: true, lookup: 2, + }, + ], + repaintChangesOnly: false, + editing: { + mode: 'batch', + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { dataField: 'number' }, + { dataField: 'date', dataType: 'date' }, + { + dataField: 'lookup', + lookup: { + dataSource: [{ id: 1, text: 'lookup 1' }, { id: 2, text: 'lookup 2' }], + valueExpr: 'id', + displayExpr: 'text', + }, + }, + { dataField: 'boolean', dataType: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data: any) => (data as { number: number }).number + 1, + setCellValue: (newData: any, value: any) => { newData.number = value - 1; }, + }, + ], + }); + + const dataGrid = new DataGrid(page); + const calculatedCell = dataGrid.getDataCell(0, 5); + + await calculatedCell.element.click(); + + const editor = calculatedCell.element.locator('.dx-texteditor-input'); + await editor.fill('9'); + await page.keyboard.press('Tab'); + + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + await saveButton.click({ position: { x: 5, y: 5 } }); + + const numberCellValue = await dataGrid.apiGetCellValue(0, 1); + expect(numberCellValue).toBe(8); + + const calculatedCellValue = await dataGrid.apiGetCellValue(0, 5); + expect(calculatedCellValue).toBe(9); + }); + + test('Update cell value and focus next cell, mode: cell, repaintChangesOnly: false, useKeyboard: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, + }, + { + id: 2, text: 'text 2', number: 2, + }, + ], + repaintChangesOnly: false, + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { + dataField: 'calculated', + calculateCellValue: (data: any) => (data as { number: number }).number + 1, + setCellValue: (newData: any, value: any) => { newData.number = value - 1; }, + }, + ], + }); + + const dataGrid = new DataGrid(page); + const textCell = dataGrid.getDataCell(0, 0); + const calculatedCell = dataGrid.getDataCell(0, 1); + + await textCell.element.click(); + + const textEditor = textCell.element.locator('.dx-texteditor-input'); + await textEditor.fill('xxxx'); + + await calculatedCell.element.click(); + + const textValue = await dataGrid.apiGetCellValue(0, 0); + expect(textValue).toBe('xxxx'); + }); + + test('Update cell value and focus next cell, mode: batch, repaintChangesOnly: false, useKeyboard: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, + }, + { + id: 2, text: 'text 2', number: 2, + }, + ], + repaintChangesOnly: false, + editing: { + mode: 'batch', + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { + dataField: 'calculated', + calculateCellValue: (data: any) => (data as { number: number }).number + 1, + setCellValue: (newData: any, value: any) => { newData.number = value - 1; }, + }, + ], + }); + + const dataGrid = new DataGrid(page); + const textCell = dataGrid.getDataCell(0, 0); + const calculatedCell = dataGrid.getDataCell(0, 1); + + await textCell.element.click(); + + const textEditor = textCell.element.locator('.dx-texteditor-input'); + await textEditor.fill('xxxx'); + + await calculatedCell.element.click(); + + const isModified = await textCell.element.evaluate((el) => el.classList.contains('dx-cell-modified')); + expect(isModified).toBe(true); + }); + + test.skip('Update cell value, mode: row, repaintChangesOnly: false, useKeyboard: false, useMask: false', async ({ page }) => { + // TODO: Playwright migration - fill() does not trigger DevExtreme editor value change event + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, + }, + { + id: 2, text: 'text 2', number: 2, + }, + ], + repaintChangesOnly: false, + editing: { + mode: 'row', + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { dataField: 'number' }, + ], + }); + + const dataGrid = new DataGrid(page); + + await dataGrid.apiEditRow(0); + + const editor = dataGrid.getDataCell(0, 0).element.locator('.dx-texteditor-input'); + await editor.fill('xxxx'); + + await dataGrid.apiSaveEditData(); + + const cellValue = await dataGrid.apiGetCellValue(0, 0); + expect(cellValue).toBe('xxxx'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts new file mode 100644 index 000000000000..7b4bb8c64d89 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts @@ -0,0 +1,425 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const testCases = [{ + caseName: 'e.cancel = promise:true', + expected: true, + onRowUpdating: `(e) => { e.cancel = new Promise((resolve) => { resolve(true); }); }`, + }, { + caseName: 'e.cancel = true', + expected: true, + onRowUpdating: `(e) => { e.cancel = true; }`, + }, { + caseName: 'e.cancel = promise:false', + expected: false, + onRowUpdating: `(e) => { e.cancel = new Promise((resolve) => { resolve(false); }); }`, + }, { + caseName: 'e.cancel = false', + expected: false, + onRowUpdating: `(e) => { e.cancel = false; }`, + }]; + + testCases.forEach(({ caseName, expected, onRowUpdating }) => { + test(`onRowUpdating event should be work valid in case '${caseName}'`, async ({ page }) => { + await page.evaluate((handler) => { + ($('#container') as any).dxDataGrid({ + dataSource: [{ + ID: 1, + FirstName: 'John', + }], + columns: [{ + dataField: 'FirstName', + caption: 'First Name', + }], + height: 300, + editing: { + mode: 'row', + allowUpdating: true, + }, + // eslint-disable-next-line no-eval + onRowUpdating: eval(handler), + }); + }, onRowUpdating); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const dataRow = page.locator('.dx-data-row').nth(0); + const editLink = dataRow.locator('.dx-link-edit'); + await editLink.click(); + + const editor = dataRow.locator('.dx-texteditor-input').first(); + await editor.fill('test text'); + + const saveLink = dataRow.locator('.dx-link-save'); + await saveLink.click(); + + if (expected) { + await expect(dataRow.locator('.dx-link-save')).toBeVisible(); + } else { + await expect(dataRow.locator('.dx-link-save')).toBeHidden(); + } + }); + }); + + const testCasesInserting = [{ + caseName: 'e.cancel = promise:true', + expected: true, + onRowInserting: `(e) => { e.cancel = new Promise((resolve) => { resolve(true); }); }`, + }, { + caseName: 'e.cancel = true', + expected: true, + onRowInserting: `(e) => { e.cancel = true; }`, + }, { + caseName: 'e.cancel = promise:false', + expected: false, + onRowInserting: `(e) => { e.cancel = new Promise((resolve) => { resolve(false); }); }`, + }, { + caseName: 'e.cancel = false', + expected: false, + onRowInserting: `(e) => { e.cancel = false; }`, + }]; + + testCasesInserting.forEach(({ caseName, expected, onRowInserting }) => { + test(`onRowInserting event should be work valid in case '${caseName}'`, async ({ page }) => { + await page.evaluate((handler) => { + ($('#container') as any).dxDataGrid({ + dataSource: [], + columns: [{ + dataField: 'FirstName', + caption: 'First Name', + }], + height: 300, + editing: { + mode: 'row', + allowAdding: true, + }, + // eslint-disable-next-line no-eval + onRowInserting: eval(handler), + }); + }, onRowInserting); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const addRowButton = page.locator('.dx-datagrid-addrow-button'); + await addRowButton.click(); + + const dataRow = page.locator('.dx-data-row').nth(0); + const editor = dataRow.locator('.dx-texteditor-input').first(); + await editor.fill('test text'); + + const saveLink = dataRow.locator('.dx-link-save'); + await saveLink.click(); + + if (expected) { + await expect(dataRow.locator('.dx-link-save')).toBeVisible(); + } else { + await expect(dataRow.locator('.dx-link-save')).toBeHidden(); + } + }); + }); + + const testCasesRemoving = [{ + caseName: 'e.cancel = promise:true', + expected: true, + onRowRemoving: `(e) => { e.cancel = new Promise((resolve) => { resolve(true); }); }`, + }, { + caseName: 'e.cancel = true', + expected: true, + onRowRemoving: `(e) => { e.cancel = true; }`, + }, { + caseName: 'e.cancel = promise:false', + expected: false, + onRowRemoving: `(e) => { e.cancel = new Promise((resolve) => { resolve(false); }); }`, + }, { + caseName: 'e.cancel = false', + expected: false, + onRowRemoving: `(e) => { e.cancel = false; }`, + }]; + + testCasesRemoving.forEach(({ caseName, expected, onRowRemoving }) => { + test(`onRowRemoving event should be work valid in case '${caseName}'`, async ({ page }) => { + await page.evaluate((handler) => { + ($('#container') as any).dxDataGrid({ + dataSource: [{ + ID: 1, + FirstName: 'John', + }], + columns: [{ + dataField: 'FirstName', + caption: 'First Name', + }], + height: 300, + editing: { + mode: 'row', + allowDeleting: true, + confirmDelete: false, + }, + // eslint-disable-next-line no-eval + onRowRemoving: eval(handler), + }); + }, onRowRemoving); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const dataRow = page.locator('.dx-data-row').nth(0); + const deleteLink = dataRow.locator('.dx-link-delete'); + await deleteLink.click(); + + if (expected) { + await expect(page.locator('.dx-data-row').nth(0)).toBeVisible(); + } else { + await expect(page.locator('.dx-data-row').nth(0)).toBeHidden(); + } + }); + }); + + // T1250405 + test('DataGrid - Canceled rows are hidden when multiple rows are added in batch mode', async ({ page }) => { + await page.evaluate(() => { + ($('#container') as any).dxDataGrid({ + dataSource: [ + { ID: 1, Text: 'Item 1' }, + ], + keyExpr: 'ID', + columns: ['Text'], + editing: { + mode: 'batch', + allowAdding: true, + }, + onRowInserting(e: any) { + e.cancel = new Promise((resolve) => { + const dialog = (window as any).DevExpress.ui.dialog.confirm( + 'Are you sure?', + 'Confirm changes', + ); + dialog.done((confirm: boolean) => resolve(!confirm)); + }); + }, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const addBtn = page.locator('.dx-datagrid-addrow-button'); + const saveBtn = page.locator('.dx-datagrid-save-button'); + + await addBtn.click(); + await page.locator('.dx-data-row').nth(0).locator('.dx-texteditor-input').fill('1'); + await addBtn.click(); + await page.locator('.dx-data-row').nth(0).locator('.dx-texteditor-input').fill('2'); + await saveBtn.click(); + + const dialogs = page.locator('.dx-dialog-wrapper'); + await expect(dialogs.nth(0)).toBeVisible(); + await expect(dialogs.nth(1)).toBeVisible(); + + await dialogs.nth(1).locator('[aria-label="No"]').click(); + + await dialogs.nth(0).locator('[aria-label="Yes"]').click(); + + const dataRows = page.locator('.dx-data-row'); + await expect(dataRows).toHaveCount(2); + }); + + test('onRowUpdating event should provide correct oldData and newData', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowUpdatingArgs = null; + ($('#container') as any).dxDataGrid({ + dataSource: [{ + ID: 1, + FirstName: 'John', + }], + keyExpr: 'ID', + columns: [{ + dataField: 'FirstName', + }], + height: 300, + editing: { + mode: 'row', + allowUpdating: true, + }, + onRowUpdating(e: any) { + (window as any).rowUpdatingArgs = { + oldData: e.oldData, + newData: e.newData, + key: e.key, + }; + }, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const dataRow = page.locator('.dx-data-row').nth(0); + await dataRow.locator('.dx-link-edit').click(); + + const editor = dataRow.locator('.dx-texteditor-input').first(); + await editor.fill('Jane'); + await dataRow.locator('.dx-link-save').click(); + + const args = await page.evaluate(() => (window as any).rowUpdatingArgs); + expect(args).not.toBeNull(); + expect(args.key).toBe(1); + expect(args.oldData.FirstName).toBe('John'); + expect(args.newData.FirstName).toBe('Jane'); + }); + + test('onRowInserting event should provide correct data', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowInsertingArgs = null; + ($('#container') as any).dxDataGrid({ + dataSource: [], + keyExpr: 'ID', + columns: [{ + dataField: 'FirstName', + }], + height: 300, + editing: { + mode: 'row', + allowAdding: true, + }, + onRowInserting(e: any) { + (window as any).rowInsertingArgs = { + data: e.data, + }; + }, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + await page.locator('.dx-datagrid-addrow-button').click(); + + const dataRow = page.locator('.dx-data-row').nth(0); + const editor = dataRow.locator('.dx-texteditor-input').first(); + await editor.fill('Alice'); + await dataRow.locator('.dx-link-save').click(); + + const args = await page.evaluate(() => (window as any).rowInsertingArgs); + expect(args).not.toBeNull(); + expect(args.data.FirstName).toBe('Alice'); + }); + + test('onRowRemoving event should provide correct key', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowRemovingArgs = null; + ($('#container') as any).dxDataGrid({ + dataSource: [{ + ID: 42, + FirstName: 'Bob', + }], + keyExpr: 'ID', + columns: [{ + dataField: 'FirstName', + }], + height: 300, + editing: { + mode: 'row', + allowDeleting: true, + confirmDelete: false, + }, + onRowRemoving(e: any) { + (window as any).rowRemovingArgs = { + key: e.key, + data: e.data, + }; + }, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const dataRow = page.locator('.dx-data-row').nth(0); + await dataRow.locator('.dx-link-delete').click(); + + const args = await page.evaluate(() => (window as any).rowRemovingArgs); + expect(args).not.toBeNull(); + expect(args.key).toBe(42); + expect(args.data.FirstName).toBe('Bob'); + }); + + test('onRowUpdating event should be called in batch mode when saving changes', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowUpdatingCallCount = 0; + ($('#container') as any).dxDataGrid({ + dataSource: [ + { ID: 1, Name: 'Item 1' }, + { ID: 2, Name: 'Item 2' }, + ], + keyExpr: 'ID', + columns: ['Name'], + editing: { + mode: 'batch', + allowUpdating: true, + }, + onRowUpdating() { + (window as any).rowUpdatingCallCount += 1; + }, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + await firstCell.click(); + await firstCell.locator('.dx-texteditor-input').fill('Updated 1'); + + const secondCell = page.locator('.dx-data-row').nth(1).locator('td').nth(0); + await secondCell.click(); + await secondCell.locator('.dx-texteditor-input').fill('Updated 2'); + + await page.locator('.dx-datagrid-save-button').click(); + + const callCount = await page.evaluate(() => (window as any).rowUpdatingCallCount); + expect(callCount).toBe(2); + }); + + test('onRowRemoving event should be called with confirmDelete=true when user confirms', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowRemovingCalled = false; + ($('#container') as any).dxDataGrid({ + dataSource: [{ + ID: 1, + Name: 'Item', + }], + keyExpr: 'ID', + columns: ['Name'], + height: 300, + editing: { + mode: 'row', + allowDeleting: true, + confirmDelete: true, + }, + onRowRemoving() { + (window as any).rowRemovingCalled = true; + }, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + const dataRow = page.locator('.dx-data-row').nth(0); + await dataRow.locator('.dx-link-delete').click(); + + const dialog = page.locator('.dx-dialog-wrapper'); + await expect(dialog).toBeVisible(); + await dialog.locator('[aria-label="Yes"]').click(); + + const called = await page.evaluate(() => (window as any).rowRemovingCalled); + expect(called).toBe(true); + await expect(page.locator('.dx-data-row').nth(0)).toBeHidden(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts new file mode 100644 index 000000000000..00ad772a2117 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.NewRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Update cell value in new row, mode: cell, repaintChangesOnly: true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { id: 1, text: 'text 1', number: 1 }, + { id: 2, text: 'text 2', number: 2 }, + ], + repaintChangesOnly: true, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [ + { dataField: 'text' }, + { dataField: 'number' }, + { + dataField: 'calculated', + calculateCellValue: (data) => data.number && -data.number + 1, + }, + ], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.apiAddRow(); + + const newRowCell = dataGrid.getDataCell(0, 0); + await newRowCell.element.click(); + + const editor = newRowCell.element.locator('.dx-texteditor-input'); + await editor.fill('new text'); + await page.keyboard.press('Tab'); + + const cellValue = await dataGrid.apiGetCellValue(0, 0); + expect(cellValue).toBe('new text'); + }); + + test('Update calculated cell value in new row, mode: cell, repaintChangesOnly: true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { id: 1, text: 'text 1', number: 1 }, + { id: 2, text: 'text 2', number: 2 }, + ], + repaintChangesOnly: true, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [ + { dataField: 'text' }, + { dataField: 'number' }, + { + dataField: 'calculated', + calculateCellValue: (data) => data.number && -data.number + 1, + }, + ], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.apiAddRow(); + + const numberCell = dataGrid.getDataCell(0, 1); + await numberCell.element.click(); + + const editor = numberCell.element.locator('.dx-texteditor-input'); + await editor.fill('5'); + await page.keyboard.press('Tab'); + + const calculatedValue = await dataGrid.apiGetCellValue(0, 2); + expect(calculatedValue).toBe(-4); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts new file mode 100644 index 000000000000..35ce38a9503c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts @@ -0,0 +1,2091 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Focused cell should be switched to the editing mode after onSaving\'s promise is resolved (T1190566)', async ({ page }) => { + await page.evaluate(() => { + (window as any).deferred = $.Deferred(); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, field1: 'value1' }, + { id: 2, field1: 'value2' }, + { id: 3, field1: 'value3' }, + { id: 4, field1: 'value4' }, + ], + keyExpr: 'id', + showBorders: true, + columns: ['field1'], + editing: { + mode: 'cell', + allowUpdating: true, + }, + onSaving(e) { + e.promise = (window as any).deferred; + }, + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + await firstCell.click(); + + const editor = page.locator('.dx-data-row').nth(0).locator('.dx-texteditor-input').first(); + await editor.fill('new_value'); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + await page.evaluate(() => (window as any).deferred.resolve()); + + const thirdCell = page.locator('.dx-data-row').nth(2).locator('td').nth(0); + await expect(thirdCell.locator('.dx-texteditor')).toBeVisible(); + }); + + test('DataGrid - The "Cannot read properties of undefined error" occurs when using Tab while saving a promise', async ({ page }) => { + await page.evaluate(() => { + (window as any).deferred = $.Deferred(); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, field1: 'value1' }, + { id: 2, field1: 'value2' }, + { id: 3, field1: 'value3' }, + { id: 4, field1: 'value4' }, + ], + keyExpr: 'id', + showBorders: true, + columns: ['field1'], + editing: { + mode: 'cell', + allowUpdating: true, + }, + onSaving(e) { + e.promise = (window as any).deferred; + }, + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + await firstCell.click(); + + const editor = page.locator('.dx-data-row').nth(0).locator('.dx-texteditor-input').first(); + await editor.fill('new_value'); + + await page.keyboard.press('Enter'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + await page.evaluate(() => (window as any).deferred.resolve()); + + const thirdCell = page.locator('.dx-data-row').nth(2).locator('td').nth(0); + await expect(thirdCell).toHaveClass(/dx-focused/); + }); + + test('Click should work if a column button set using svg icon (T863635)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: [{ value: 1 }], + columns: [{ + type: 'buttons', + width: 110, + buttons: [ + { + hint: 'svg icon', + icon: ' ', + onClick: (): void => { + const global = window as Window & typeof globalThis & { onSvgClickCounter: number }; + if (!global.onSvgClickCounter) { + global.onSvgClickCounter = 0; + } + global.onSvgClickCounter += 1; + }, + }], + }], + })); + + const svgIcon = page.locator('#svg-icon').first(); + await svgIcon.click(); + + const clickCount = await page.evaluate(() => (window as any).onSvgClickCounter); + expect(clickCount).toBe(1); + }); + + test('Value change on dataGrid row should be fired after clicking on editor (T823431)', async ({ page }) => { + await page.evaluate(() => { + ($('#container') as any).dxDataGrid({ + dataSource: [{ name: 'old_value', value: 1 }], + editing: { + mode: 'batch', + allowUpdating: true, + selectTextOnEditStart: true, + startEditAction: 'click', + }, + }); + ($('#otherContainer') as any).dxSelectBox({}); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + + await page.locator('.dx-data-row').nth(0).locator('td').nth(0).click(); + const editor = page.locator('.dx-data-row').nth(0).locator('.dx-texteditor-input').first(); + await editor.fill('new_value'); + + await page.locator('#otherContainer .dx-dropdowneditor-button').click(); + + const cellText = await page.locator('.dx-data-row').nth(0).locator('td').nth(0).textContent(); + expect(cellText).toBe('new_value'); + }); + + test('The "Cannot read property "brokenRules" of undefined" error occurs T978286', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + LastName: 'Heart', + Active: false, + }, { + ID: 1, + LastName: 'Broken', + Active: false, + }], + keyExpr: 'ID', + editing: { + allowUpdating: true, + mode: 'cell', + }, + }); + + const dataGrid = new DataGrid(page); + const lastName0 = dataGrid.getDataCell(0, 1); + const active1 = dataGrid.getDataCell(1, 2); + + await lastName0.click(); + const editor = lastName0.locator('.dx-texteditor-input'); + await editor.fill('1'); + await active1.click(); + await lastName0.click(); + + expect(true).toBeTruthy(); + }); + + ['Cell', 'Batch'].forEach((editMode) => { + test(`${editMode} - Cell value should not be reset when a checkbox in a neigboring cell is clicked (T1023809)`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, field1: 'test', field2: true }, + ], + keyExpr: 'id', + columns: ['field1', 'field2'], + editing: { + mode: editMode.toLowerCase() as any, + allowUpdating: true, + }, + }); + + const dataGrid = new DataGrid(page); + const firstCell = dataGrid.getDataCell(0, 0); + const secondCell = dataGrid.getDataCell(0, 1); + + await firstCell.click(); + await expect(firstCell).toHaveClass(/dx-editor-cell/); + + const editor = firstCell.locator('.dx-texteditor-input'); + await editor.fill('123'); + + await secondCell.locator('.dx-checkbox').click(); + + const cellValue = await dataGrid.apiGetCellValue(0, 0); + expect(cellValue).toBe('123'); + }); + }); + + test('The editCellTemplate template should not be called after clicking on a cell in another row and column', async ({ page }) => { + await page.evaluate(() => { + (window as any).editCellTemplateCallArgs = []; + }); + + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: [ + { ID: 1, Column1: 'a', Column2: 'b', Column3: 'c' }, + { ID: 2, Column1: 'd', Column2: 'e', Column3: 'f' }, + { ID: 3, Column1: 'g', Column2: 'h', Column3: 'i' }, + ], + keyExpr: 'ID', + columns: [{ + dataField: 'Column1', + editCellTemplate(_: any, cellInfo: any) { + (window as any).editCellTemplateCallArgs.push(cellInfo.column.dataField); + return ($('
') as any).dxTextBox({ + value: cellInfo.value, + onValueChanged: (args: any) => cellInfo.setValue(args.value), + }); + }, + }, 'Column2', 'Column3'], + showBorders: true, + editing: { mode: 'batch', allowUpdating: true }, + })); + + const dataGrid = new DataGrid(page); + const firstCellOfFirstRow = dataGrid.getDataCell(0, 0); + const secondCellOfSecondRow = dataGrid.getDataCell(1, 1); + + await firstCellOfFirstRow.click(); + await expect(firstCellOfFirstRow).toHaveClass(/dx-editor-cell/); + + const callArgs1: string[] = await page.evaluate(() => (window as any).editCellTemplateCallArgs); + expect(callArgs1.length).toBe(1); + expect(callArgs1[0]).toBe('Column1'); + + await secondCellOfSecondRow.click(); + await expect(secondCellOfSecondRow).toHaveClass(/dx-editor-cell/); + + const callArgs2: string[] = await page.evaluate(() => (window as any).editCellTemplateCallArgs); + expect(callArgs2.length).toBe(1); + expect(callArgs2[0]).toBe('Column1'); + }); + + test('The onEditorPreparing event should be called once after clicking on a cell in another row and column', async ({ page }) => { + await page.evaluate(() => { + (window as any).onEditorPreparingCallArgs = []; + }); + + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: [ + { ID: 1, Column1: 'a', Column2: 'b', Column3: 'c' }, + { ID: 2, Column1: 'd', Column2: 'e', Column3: 'f' }, + { ID: 3, Column1: 'g', Column2: 'h', Column3: 'i' }, + ], + keyExpr: 'ID', + columns: ['Column1', 'Column2', 'Column3'], + showBorders: true, + editing: { mode: 'batch', allowUpdating: true }, + onEditorPreparing(e: any) { + (window as any).onEditorPreparingCallArgs.push({ + dataField: e.dataField, + rowIndex: e.row?.rowIndex, + }); + }, + })); + + const dataGrid = new DataGrid(page); + const firstCellOfFirstRow = dataGrid.getDataCell(0, 0); + const secondCellOfSecondRow = dataGrid.getDataCell(1, 1); + + await firstCellOfFirstRow.click(); + await expect(firstCellOfFirstRow).toHaveClass(/dx-editor-cell/); + + const args1: { dataField: string; rowIndex: number }[] = await page.evaluate(() => (window as any).onEditorPreparingCallArgs); + expect(args1.length).toBe(1); + expect(args1[0]).toEqual({ dataField: 'Column1', rowIndex: 0 }); + + await secondCellOfSecondRow.click(); + await expect(secondCellOfSecondRow).toHaveClass(/dx-editor-cell/); + + const args2: { dataField: string; rowIndex: number }[] = await page.evaluate(() => (window as any).onEditorPreparingCallArgs); + expect(args2.length).toBe(2); + expect(args2[1]).toEqual({ dataField: 'Column2', rowIndex: 1 }); + }); + + test('Focus behavior should be correct when editing cells (T1194439)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(10)].map((_, i) => ({ + ID: i + 1, + CompanyName: `company name ${i + 1}`, + City: `city ${i + 1}`, + })), + keyExpr: 'ID', + columns: [{ + dataField: 'CompanyName', + showEditorAlways: true, + }, { + caption: 'City', + calculateCellValue(rowData: any) { return rowData.City; }, + allowEditing: false, + }], + showBorders: true, + editing: { + allowUpdating: true, + mode: 'batch', + }, + }); + + const dataGrid = new DataGrid(page); + + for (let i = 0; i < 3; i++) { + const cell = dataGrid.getDataCell(i, 0); + await cell.click(); + await expect(cell).toHaveClass(/dx-focused/); + + const editor = cell.locator('.dx-texteditor-input'); + await editor.fill(`new_value ${i}`); + } + + expect(true).toBeTruthy(); + }); + + test('Tab key on editor should focus next cell if editing mode is cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ name: 'AaAaA', value: 1 }, { name: 'aAaAa', value: 2 }], + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [{ dataField: 'name', allowEditing: false }, { dataField: 'value', showEditorAlways: true }], + }); + + const dataGrid = new DataGrid(page); + const cell01 = dataGrid.getDataCell(0, 1); + await cell01.click(); + + const editor = cell01.locator('.dx-texteditor-input'); + await editor.fill('1'); + await page.keyboard.press('Tab'); + + const cell11 = dataGrid.getDataCell(1, 1); + await expect(cell11).toHaveClass(/dx-focused/); + }); + + test('Component sends unexpected filtering request after inserting a new row if focusedRowEnabled is true and key set in data source (T1181477)', async ({ page }) => { + await page.evaluate(() => { + const dataSourceCore = [ + { ID: 1, Name: 'Name 1' }, + { ID: 2, Name: 'Name 2' }, + { ID: 3, Name: 'Name 3' }, + ]; + + const sampleAPI = { + load() { + const data = dataSourceCore; + return new Promise((resolve) => { setTimeout(() => { resolve(data); }, 100); }); + }, + totalCount() { + return new Promise((resolve) => { setTimeout(() => { resolve(dataSourceCore.length); }, 100); }); + }, + insert(values: any) { + return new Promise((resolve) => { + setTimeout(() => { + const newID = dataSourceCore.length + 1; + values.ID = newID; + dataSourceCore.push(values); + resolve(newID); + }, 100); + }); + }, + }; + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'ID', + load(o: any) { + if (o.filter) { + $('#otherContainer').append('Fail'); + } + return Promise.all([sampleAPI.load(), sampleAPI.totalCount()]).then((res: any) => ({ + data: res[0], + totalCount: res[1], + })); + }, + insert(values: any) { return sampleAPI.insert(values); }, + }); + + ($('#container') as any).dxDataGrid({ + dataSource: store, + showBorders: true, + focusedRowEnabled: true, + autoNavigateToFocusedRow: true, + editing: { allowAdding: true }, + remoteOperations: true, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const addRowButton = page.locator('.dx-datagrid-addrow-button'); + await addRowButton.click(); + + const saveLink = page.locator('.dx-data-row').nth(0).locator('.dx-link-save'); + await saveLink.click(); + + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const otherContainerText = await page.locator('#otherContainer').textContent(); + expect(otherContainerText).toBe(''); + }); + + test('Rollback changes on a click on a revert button when startEditAction is dblclick', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ name: 'test', test: false }], + editing: { + mode: 'cell', + allowUpdating: true, + startEditAction: 'dblClick', + }, + columns: ['name', + { + dataField: 'test', + dataType: 'boolean', + showEditorAlways: false, + }, + ], + }); + + const dataGrid = new DataGrid(page); + const dataRow = dataGrid.getDataRow(0); + const cell1 = dataRow.getDataCell(2); + + await cell1.element.dblclick(); + await expect(cell1.element).toHaveClass(/dx-editor-cell/); + + const checkbox = cell1.element.locator('.dx-checkbox'); + await checkbox.click(); + + const revertButton = dataGrid.getRevertButton(); + await expect(revertButton).toBeVisible(); + + await revertButton.click(); + await expect(revertButton).toBeHidden(); + await expect(cell1.element).not.toHaveClass(/dx-editor-cell/); + + const cellValue = await dataGrid.apiGetCellValue(0, 1); + expect(cellValue).toBeFalsy(); + }); + + test('Cell - Redundant validation messages should not be rendered in a detail grid when focused row is enabled (T950174)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: [{ id: 1, field: 'field' }], + keyExpr: 'id', + masterDetail: { + enabled: true, + template() { + return ($('
') as any).dxDataGrid({ + dataSource: [], + keyExpr: 'id', + focusedRowEnabled: true, + columns: [ + { dataField: 'id', validationRules: [{ type: 'required' }] }, + { dataField: 'field', validationRules: [{ type: 'required' }] }, + ], + editing: { mode: 'cell', allowAdding: true, allowUpdating: true }, + }); + }, + }, + })); + + const dataGrid = new DataGrid(page); + + await dataGrid.getDataRow(0).getDataCell(0).click(); + await page.waitForSelector('#detailContainer'); + + const detailAddButton = page.locator('#detailContainer .dx-datagrid-addrow-button'); + await detailAddButton.click(); + + const detailHeaderPanel = page.locator('#detailContainer .dx-datagrid-header-panel'); + await detailHeaderPanel.click(); + + const invalidMessages = page.locator('.dx-invalid-message'); + await expect(invalidMessages).toHaveCount(1); + + await page.locator('#detailContainer .dx-data-row').nth(0).locator('td').nth(1).click(); + await expect(invalidMessages).toHaveCount(1); + + await page.locator('#detailContainer .dx-data-row').nth(0).locator('td').nth(0).click(); + await expect(invalidMessages).toHaveCount(1); + }); + + test('Batch - Redundant validation messages should not be rendered in a detail grid when focused row is enabled (T950174)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: [{ id: 1, field: 'field' }], + keyExpr: 'id', + masterDetail: { + enabled: true, + template() { + return ($('
') as any).dxDataGrid({ + dataSource: [], + keyExpr: 'id', + focusedRowEnabled: true, + columns: [ + { dataField: 'id', validationRules: [{ type: 'required' }] }, + { dataField: 'field', validationRules: [{ type: 'required' }] }, + ], + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + }); + }, + }, + })); + + const dataGrid = new DataGrid(page); + + await dataGrid.getDataRow(0).getDataCell(0).click(); + await page.waitForSelector('#detailContainer'); + + const detailAddButton = page.locator('#detailContainer .dx-datagrid-addrow-button'); + await detailAddButton.click(); + + const detailSaveButton = page.locator('#detailContainer .dx-datagrid-save-button'); + await detailSaveButton.click(); + + await page.locator('#detailContainer .dx-data-row').nth(0).locator('td').nth(0).click(); + + const invalidMessages = page.locator('.dx-invalid-message'); + await expect(invalidMessages).toHaveCount(1); + + await page.locator('#detailContainer .dx-data-row').nth(0).locator('td').nth(1).click(); + await expect(invalidMessages).toHaveCount(1); + + await page.locator('#detailContainer .dx-data-row').nth(0).locator('td').nth(0).click(); + await expect(invalidMessages).toHaveCount(1); + }); + + test('Component sends unexpected filtering request after inserting a new row if focusedRowEnabled is true and key set on event (T1181477)', async ({ page }) => { + await page.evaluate(() => { + const dataSourceCore = [ + { ID: 1, Name: 'Name 1' }, + { ID: 2, Name: 'Name 2' }, + { ID: 3, Name: 'Name 3' }, + ]; + + const sampleAPI = new (window as any).DevExpress.data.ArrayStore(dataSourceCore); + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'ID', + load(o: any) { + if (o.filter) { + $('#otherContainer').append('Fail'); + } + return Promise.all([sampleAPI.load(), sampleAPI.totalCount()]).then((res: any) => ({ + data: res[0], + totalCount: res[1], + })); + }, + insert(values: any) { + return sampleAPI.insert(values); + }, + }); + + ($('#container') as any).dxDataGrid({ + dataSource: store, + showBorders: true, + focusedRowEnabled: true, + autoNavigateToFocusedRow: true, + editing: { allowAdding: true }, + onInitNewRow(e: any) { + e.promise = new Promise((resolve) => { + const newId = dataSourceCore.length + 1; + e.data.ID = newId; + resolve(undefined as any); + }); + }, + remoteOperations: true, + }); + }); + + await page.waitForSelector('.dx-datagrid-rowsview'); + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const addRowButton = page.locator('.dx-datagrid-addrow-button'); + await addRowButton.click(); + + const saveLink = page.locator('.dx-data-row').nth(0).locator('.dx-link-save'); + await saveLink.click(); + + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const otherContainerText = await page.locator('#otherContainer').textContent(); + expect(otherContainerText).toBe(''); + }); + + test('Adding rows to a second page should work correctly when initial row values are specified in the onInitNewRow method (T1274123)', async ({ page }) => { + await page.evaluate(() => { + (window as any).myData = new Array(30).fill(null).map((_: null, index: number) => ({ id: index + 1, text: `item ${index + 1}` })); + (window as any).myStore = new (window as any).DevExpress.data.ArrayStore({ + key: 'id', + data: (window as any).myData, + }); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: { + key: 'id', + load(loadOptions: any) { + return (window as any).myStore.load(loadOptions); + }, + totalCount() { + return (window as any).myStore.totalCount(); + }, + insert(values: any) { + if (values.id === 0) { + values.id = (window as any).myData.length + 1; + } + return (window as any).myStore.insert(values); + }, + } as any, + columns: ['id', 'text'], + showBorders: true, + editing: { + mode: 'popup', + allowAdding: true, + }, + onInitNewRow(e: any) { + e.data.id = 0; + e.data.text = 'test'; + }, + height: 300, + }); + + const dataGrid = new DataGrid(page); + + await dataGrid.apiPageIndex(1); + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const visibleRows1 = await dataGrid.apiGetVisibleRows(); + expect(visibleRows1.length).toBe(10); + + const cell20 = await dataGrid.getDataCell(20, 0).element.textContent(); + expect(cell20).toBe('21'); + + const addAndSave = async () => { + await dataGrid.apiAddRow(); + const popup = dataGrid.getPopupEditForm(); + await expect(popup.element).toBeVisible(); + await dataGrid.apiSaveEditData(); + await expect(popup.element).toBeHidden(); + }; + + await addAndSave(); + await addAndSave(); + + const visibleRows2 = await dataGrid.apiGetVisibleRows(); + expect(visibleRows2.length).toBe(12); + expect(visibleRows2[10].key).toBe(31); + expect(visibleRows2[11].key).toBe(32); + }); + + test('Row - Redundant validation messages should not be rendered in a detail grid when focused row is enabled (T950174)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: [{ id: 1, field: 'field' }], + keyExpr: 'id', + masterDetail: { + enabled: true, + template() { + return ($('
') as any).dxDataGrid({ + dataSource: [], + keyExpr: 'id', + focusedRowEnabled: true, + columns: [ + { dataField: 'id', validationRules: [{ type: 'required' }] }, + { dataField: 'field', validationRules: [{ type: 'required' }] }, + ], + editing: { mode: 'row', allowAdding: true, allowUpdating: true }, + }); + }, + }, + })); + + const dataGrid = new DataGrid(page); + + await dataGrid.getDataRow(0).getDataCell(0).click(); + + await page.waitForSelector('#detailContainer'); + + const detailAddButton = page.locator('#detailContainer .dx-datagrid-addrow-button'); + await detailAddButton.click(); + + const saveBtn = page.locator('#detailContainer .dx-data-row').nth(0).locator('.dx-link-save'); + await saveBtn.click(); + + await page.locator('#detailContainer .dx-data-row').nth(0).locator('td').nth(0).click(); + + const invalidMessages = page.locator('.dx-invalid-message'); + await expect(invalidMessages).toHaveCount(1); + + await page.locator('#detailContainer .dx-data-row').nth(0).locator('td').nth(1).click(); + await expect(invalidMessages).toHaveCount(1); + }); + + test('Cells should be focused correctly on click when cell editing mode is used with enabled showEditorAlways (T1037019)', async ({ page }) => { + await page.evaluate(() => { + (window as any).myStore = new (window as any).DevExpress.data.ArrayStore({ + key: 'ID', + data: [ + { ID: 1, Name: 'Name 1' }, + { ID: 2, Name: 'Name 2' }, + { ID: 3, Name: 'Name 3' }, + ], + }); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: { + key: 'ID', + load(loadOptions: any) { + return new Promise((resolve) => { + setTimeout(() => { + (window as any).myStore.load(loadOptions).done((data: any) => { + resolve(data); + }); + }, 100); + }); + }, + update(key: any, values: any) { + return new Promise((resolve) => { + setTimeout(() => { + (window as any).myStore.update(key, values).done(() => { + resolve(key); + }); + }, 100); + }); + }, + totalCount(loadOptions: any) { + return (window as any).myStore.totalCount(loadOptions); + }, + } as any, + keyExpr: 'ID', + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [{ + dataField: 'Name', + showEditorAlways: true, + }], + }); + + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const dataGrid = new DataGrid(page); + + const cell00 = dataGrid.getDataCell(0, 0); + await cell00.element.locator('.dx-texteditor-input').click(); + await expect(cell00.element).toHaveClass(/dx-focused/); + + await cell00.element.locator('.dx-texteditor-input').fill('Name 11'); + await dataGrid.getDataCell(1, 0).element.locator('.dx-texteditor-input').click(); + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const storedName1 = await page.evaluate(() => (window as any).myStore.byKey(1).then((item: any) => item.Name)); + expect(storedName1).toBe('Name 11'); + + const cell10 = dataGrid.getDataCell(1, 0); + await expect(cell10.element).toHaveClass(/dx-focused/); + + await cell10.element.locator('.dx-texteditor-input').fill('Name 22'); + await dataGrid.getDataCell(2, 0).element.locator('.dx-texteditor-input').click(); + await page.waitForFunction(() => !$('.dx-loadpanel-wrapper').is(':visible')); + + const storedName2 = await page.evaluate(() => (window as any).myStore.byKey(2).then((item: any) => item.Name)); + expect(storedName2).toBe('Name 22'); + + const cell20 = dataGrid.getDataCell(2, 0); + await expect(cell20.element).toHaveClass(/dx-focused/); + }); + + test('Validation(Row) - Unmodified data cell should be marked as invalid when a neighboring cell is modified (reevaluate=false) (T880238)', async ({ page }) => { + const getGridConfig = (config: any) => ({ + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + ...config, + }); + + await createWidget(page, 'dxDataGrid', getGridConfig({ + editing: { mode: 'row', allowUpdating: true }, + columns: ['age', { + dataField: 'name', + validationRules: [{ + type: 'custom', + validationCallback(params: any) { return params.data.age >= 10; }, + }], + }], + })); + + const dataGrid = new DataGrid(page); + const editButton = page.locator('.dx-data-row[aria-rowindex="1"] .dx-link-edit'); + await editButton.click(); + + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + const editor0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0).locator('.dx-texteditor-input'); + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + const saveButton = page.locator('.dx-data-row[aria-rowindex="1"] .dx-link-save'); + await saveButton.click(); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('10'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + await saveButton.click(); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + const row = page.locator('.dx-data-row[aria-rowindex="1"]'); + await expect(row).toHaveClass(/dx-edit-row/); + }); + + test('Validation(Row) - Unmodified data cell should be marked as invalid when a neighboring cell is modified (reevaluate=true) (T880238)', async ({ page }) => { + const getGridConfig = (config: any) => ({ + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + ...config, + }); + + await createWidget(page, 'dxDataGrid', getGridConfig({ + editing: { mode: 'row', allowUpdating: true }, + columns: ['age', { + dataField: 'name', + validationRules: [{ + type: 'custom', + reevaluate: true, + validationCallback(params: any) { return params.data.age >= 10; }, + }], + }], + })); + + const dataGrid = new DataGrid(page); + const editButton = page.locator('.dx-data-row[aria-rowindex="1"] .dx-link-edit'); + await editButton.click(); + + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + const editor0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0).locator('.dx-texteditor-input'); + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + const saveButton = page.locator('.dx-data-row[aria-rowindex="1"] .dx-link-save'); + await saveButton.click(); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('10'); + await page.keyboard.press('Enter'); + + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + const row = page.locator('.dx-data-row[aria-rowindex="1"]'); + await expect(row).not.toHaveClass(/dx-edit-row/); + }); + + test('Validation(Cell) - Unmodified data cell should be marked as invalid when a neighboring cell is modified (reevaluate=false) (T880238)', async ({ page }) => { + const getGridConfig = (config: any) => ({ + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + ...config, + }); + + await createWidget(page, 'dxDataGrid', getGridConfig({ + editing: { mode: 'cell', allowUpdating: true }, + columns: ['age', { + dataField: 'name', + validationRules: [{ + type: 'custom', + validationCallback(params: any) { return params.data.age >= 10; }, + }], + }], + })); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + + await cell0.click(); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + const editor0 = cell0.locator('.dx-texteditor-input'); + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-editor-cell/); + + await editor0.selectText(); + await editor0.fill('10'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-editor-cell/); + }); + + test('Validation(Cell) - Unmodified data cell should be marked as invalid when a neighboring cell is modified (reevaluate=true) (T880238)', async ({ page }) => { + const getGridConfig = (config: any) => ({ + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + ...config, + }); + + await createWidget(page, 'dxDataGrid', getGridConfig({ + editing: { mode: 'cell', allowUpdating: true }, + columns: ['age', { + dataField: 'name', + validationRules: [{ + type: 'custom', + reevaluate: true, + validationCallback(params: any) { return params.data.age >= 10; }, + }], + }], + })); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + + await cell0.click(); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + const editor0 = cell0.locator('.dx-texteditor-input'); + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-editor-cell/); + + await editor0.selectText(); + await editor0.fill('10'); + await page.keyboard.press('Enter'); + + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).not.toHaveClass(/dx-editor-cell/); + }); + + ['false', 'true'].forEach((reevaluate) => { + test(`Validation(Batch) - Unmodified data cell should be marked as invalid when a neighboring cell is modified (reevaluate=${reevaluate}) (T880238)`, async ({ page }) => { + const getGridConfig = (config: any) => ({ + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + ...config, + }); + + const reevaluateValue = reevaluate === 'true'; + + await createWidget(page, 'dxDataGrid', getGridConfig({ + editing: { mode: 'batch', allowUpdating: true }, + columns: ['age', { + dataField: 'name', + validationRules: [{ + type: 'custom', + reevaluate: reevaluateValue, + validationCallback(params: any) { return params.data.age >= 10; }, + }], + }], + })); + + const dataGrid = new DataGrid(page); + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + + await cell0.click(); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + const editor0 = cell0.locator('.dx-texteditor-input'); + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await saveButton.click(); + + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await cell0.click(); + await editor0.selectText(); + await editor0.fill('10'); + await page.keyboard.press('Enter'); + + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await saveButton.click(); + + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).not.toHaveClass(/dx-cell-modified/); + await expect(cell0).not.toHaveClass(/dx-editor-cell/); + }); + }); + + test('Validation(Batch) - Unmodified data cell with enabled showEditorAlways should be marked as invalid when a neighboring cell is modified (T878218)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { id: 1, name: '', lastName: '' }, + { id: 2, name: '', lastName: '' }, + ], + editing: { + mode: 'batch', + allowUpdating: true, + }, + columns: ['name', { + dataField: 'lastName', + showEditorAlways: true, + validationRules: [{ + type: 'custom', + reevaluate: true, + validationCallback: (params: any): boolean => params.data.name.length <= 0, + }], + }], + }); + + const cell10 = page.locator('.dx-data-row[aria-rowindex="2"] td').nth(0); + const cell11 = page.locator('.dx-data-row[aria-rowindex="2"] td').nth(1); + const cell00 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell01 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + const editor10 = cell10.locator('.dx-texteditor-input'); + + await cell10.click(); + await expect(cell11).not.toHaveClass(/dx-datagrid-invalid/); + + await editor10.fill('test'); + await page.keyboard.press('Enter'); + + await expect(cell11).toHaveClass(/dx-datagrid-invalid/); + await expect(cell10).toHaveClass(/dx-cell-modified/); + + await cell00.click(); + await expect(cell11).toHaveClass(/dx-datagrid-invalid/); + await expect(cell10).toHaveClass(/dx-cell-modified/); + + await cell01.click(); + await expect(cell11).toHaveClass(/dx-datagrid-invalid/); + await expect(cell10).toHaveClass(/dx-cell-modified/); + + await cell11.click(); + await expect(cell11).toHaveClass(/dx-datagrid-invalid/); + await expect(cell10).toHaveClass(/dx-cell-modified/); + + await cell10.click(); + await expect(cell11).toHaveClass(/dx-datagrid-invalid/); + await expect(cell10).toHaveClass(/dx-cell-modified/); + }); + + test('Async Validation(Batch) - Validation frame should be rendered when a neighboring cell is modified with showEditorAlways and repaintChangesOnly enabled (T906094)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: '', lastName: '' }], + keyExpr: 'id', + repaintChangesOnly: true, + editing: { + mode: 'batch', + allowUpdating: true, + }, + columns: [{ + dataField: 'name', + }, { + dataField: 'lastName', + showEditorAlways: true, + validationRules: [{ + type: 'async', + message: 'Invalid value', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + d.resolve(params.data.name.length < 2); + }, 1000); + return d.promise(); + }, + }], + }], + }); + + const dataGrid = new DataGrid(page); + const cancelButton = dataGrid.getHeaderPanel().getCancelButton(); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + const editor1 = cell1.locator('.dx-texteditor-input'); + + await cell0.click(); + await expect(cell0).toHaveClass(/dx-focused/); + + await editor0.fill('test'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + await cancelButton.click(); + await expect(cell0).not.toHaveClass(/dx-cell-modified/); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).not.toHaveClass(/dx-validation-pending/); + + await cell0.click(); + await editor0.fill('test'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + + await editor1.click(); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveClass(/dx-focused/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await cell0.click(); + await editor0.fill('t'); + await page.keyboard.press('Enter'); + + await expect(cell1).toHaveClass(/dx-validation-pending/); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + }); + + ['Cell', 'Batch'].forEach((editMode) => { + test(`${editMode} - Edit cell should be focused correctly when showEditorAlways is enabled (T976141)`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, field: 'field' }, + { id: 2, field: 'field' }, + { id: 3, field: 'field' }, + ], + keyExpr: 'id', + editing: { + mode: editMode.toLowerCase(), + allowUpdating: true, + }, + customizeColumns(columns: any[]) { + columns.forEach((col) => { + col.showEditorAlways = true; + }); + }, + }); + + for (let rowIndex = 0; rowIndex < 3; rowIndex += 1) { + for (let colIndex = 0; colIndex < 2; colIndex += 1) { + const cell = page.locator(`.dx-data-row[aria-rowindex="${rowIndex + 1}"] td`).nth(colIndex); + const editor = cell.locator('.dx-texteditor-input'); + await editor.click(); + await expect(cell).toHaveClass(/dx-focused/); + await expect(editor).toBeFocused(); + } + } + + for (let rowIndex = 2; rowIndex >= 0; rowIndex -= 1) { + for (let colIndex = 1; colIndex >= 0; colIndex -= 1) { + const cell = page.locator(`.dx-data-row[aria-rowindex="${rowIndex + 1}"] td`).nth(colIndex); + const editor = cell.locator('.dx-texteditor-input'); + await editor.click(); + await expect(cell).toHaveClass(/dx-focused/); + await expect(editor).toBeFocused(); + } + } + }); + }); + + test('Async Validation(Row) - Only valid data is saved in a new row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'row', + allowAdding: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + }, 'name', 'lastName'], + }); + + const dataGrid = new DataGrid(page); + const addRowButton = dataGrid.getHeaderPanel().getAddRowButton(); + await addRowButton.click(); + + const insertedRow = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"]'); + await expect(insertedRow).toBeVisible(); + + const cell0 = insertedRow.locator('td').nth(0); + await expect(cell0).not.toHaveClass(/dx-datagrid-invalid/); + + const saveButton = page.locator('.dx-data-row.dx-row-inserted .dx-link-save'); + await saveButton.click(); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(insertedRow).toBeVisible(); + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + + const editor0 = cell0.locator('.dx-texteditor-input'); + await editor0.fill('1'); + await saveButton.click(); + + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted')); + await expect(insertedRow).toBeHidden(); + }); + + test('Async Validation(Row) - Only valid data is saved in a modified row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'row', + allowUpdating: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + }, 'name', 'lastName'], + }); + + const editButton = page.locator('.dx-data-row[aria-rowindex="1"] .dx-link-edit'); + await editButton.click(); + + const cell0 = page.locator('.dx-data-row.dx-edit-row[aria-rowindex="1"] td').nth(0); + const editor0 = cell0.locator('.dx-texteditor-input'); + const saveButton = page.locator('.dx-data-row.dx-edit-row .dx-link-save'); + + await editor0.selectText(); + await editor0.fill('3'); + await saveButton.click(); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await page.waitForFunction(() => !document.querySelector('.dx-edit-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('1'); + await saveButton.click(); + + await page.waitForFunction(() => !document.querySelector('.dx-edit-row')); + await expect(page.locator('.dx-data-row.dx-edit-row')).toBeHidden(); + }); + + test('Async Validation(Row) - Data is not saved when a dependant cell value becomes invalid', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'row', + allowUpdating: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + setCellValue(rowData: any, value: any): void { + rowData.age = value; + if (value === 1) { + rowData.name = ''; + } + }, + }, { + dataField: 'name', + validationRules: [{ type: 'required' }], + }, 'lastName'], + }); + + const editButton = page.locator('.dx-data-row[aria-rowindex="1"] .dx-link-edit'); + await editButton.click(); + + const editRow = page.locator('.dx-data-row.dx-edit-row[aria-rowindex="1"]'); + const cell0 = editRow.locator('td').nth(0); + const cell1 = editRow.locator('td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + const saveButton = page.locator('.dx-data-row.dx-edit-row .dx-link-save'); + + await editor0.selectText(); + await editor0.fill('3'); + await saveButton.click(); + + await page.waitForFunction(() => !document.querySelector('.dx-edit-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('1'); + await saveButton.click(); + + await page.waitForFunction(() => !document.querySelector('.dx-edit-row td.dx-validation-pending')); + await expect(cell0).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + }); + + test('Async Validation(Cell) - Only the last cell should be switched to edit mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + d.resolve(true); + }, 1000); + return d.promise(); + }, + }], + }, 'name', 'lastName'], + }); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + const cell2 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(2); + + await cell0.click(); + await expect(cell0).toHaveClass(/dx-validation-pending/); + + await cell1.click(); + await expect(cell1).not.toHaveClass(/dx-focused/); + + await cell2.click(); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell2).not.toHaveClass(/dx-hidden-focus/); + await expect(cell2).toHaveClass(/dx-focused/); + }); + + test('Async Validation(Cell) - Only valid data is saved in a new row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowAdding: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + }, 'name', 'lastName'], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const insertedRow = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"]'); + await expect(insertedRow).toBeVisible(); + + const cell0 = insertedRow.locator('td').nth(0); + await expect(cell0).not.toHaveClass(/dx-datagrid-invalid/); + + await cell0.click(); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + await expect(insertedRow).toBeVisible(); + + const editor0 = cell0.locator('.dx-texteditor-input'); + await cell0.click(); + await editor0.fill('1'); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-validation-pending')); + await expect(insertedRow).toBeHidden(); + }); + + test('Async Validation(Cell) - Only valid data is saved in a modified cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + }, 'name', 'lastName'], + }); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await cell0.click(); + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-editor-cell/); + + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-editor-cell/); + + await cell0.click(); + await editor0.selectText(); + await editor0.fill('1'); + await page.keyboard.press('Enter'); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).not.toHaveClass(/dx-editor-cell/); + }); + + test('Async Validation(Cell) - Data is not saved when a dependant cell value becomes invalid', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + setCellValue(rowData: any, value: any): void { + rowData.age = value; + if (value === 1) { + rowData.name = ''; + } + }, + }, { + dataField: 'name', + validationRules: [{ type: 'required' }], + }, 'lastName'], + }); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await cell0.click(); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-editor-cell/); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-editor-cell/); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + await cell0.click(); + await editor0.selectText(); + await editor0.fill('1'); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + }); + + test('Cell mode(setCellValue) with async validation - The value of an invalid dependent cell should be updated in a new row (T872751)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [{ + dataField: 'age', + setCellValue: (rowData: any, value: any): void => { + rowData.age = value; + rowData.name = 'testb'; + }, + }, { + dataField: 'name', + validationRules: [{ + type: 'async', + validationCallback(): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + d.resolve(false); + }, 50); + return d.promise(); + }, + }], + }, 'lastName'], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.apiAddRow(); + + const cell0 = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"] td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await editor0.fill('123'); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted td.dx-validation-pending')); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveText('testb'); + }); + + test('Cell mode(setCellValue) with async validation - The value of an invalid dependent cell should be updated in a modified row (T872751)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [{ + dataField: 'age', + setCellValue: (rowData: any, value: any): void => { + rowData.age = value; + rowData.name = 'testb'; + }, + }, { + dataField: 'name', + validationRules: [{ + type: 'async', + validationCallback(): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + d.resolve(false); + }, 50); + return d.promise(); + }, + }], + }, 'lastName'], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.apiEditCell(0, 0); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await editor0.fill('123'); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveText('testb'); + }); + + test('Cell mode(calculateCellValue) with async validation - The value of an invalid dependent cell should be updated in a new row (T872751)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [{ + dataField: 'age', + }, { + dataField: 'name', + calculateCellValue: (rowData: any): string | undefined => (rowData.age ? `${rowData.age}b` : undefined), + validationRules: [{ + type: 'async', + validationCallback(): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + d.resolve(false); + }, 50); + return d.promise(); + }, + }], + }, 'lastName'], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.apiAddRow(); + + const cell0 = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"] td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await editor0.fill('123'); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted td.dx-validation-pending')); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveText('123b'); + }); + + test('Cell mode(calculateCellValue) with async validation - The value of an invalid dependent cell should be updated in a modified row (T872751)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [{ + dataField: 'age', + }, { + dataField: 'name', + calculateCellValue: (rowData: any): string | undefined => (rowData.age ? `${rowData.age}b` : undefined), + validationRules: [{ + type: 'async', + validationCallback(): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + d.resolve(false); + }, 50); + return d.promise(); + }, + }], + }, 'lastName'], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.apiEditCell(0, 0); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await editor0.fill('123'); + await page.keyboard.press('Enter'); + + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveText('15123b'); + }); + + test('Async Validation(Batch) - Only valid data is saved in a new row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'batch', + allowAdding: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + }, 'name', 'lastName'], + }); + + const dataGrid = new DataGrid(page); + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const insertedRow = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"]'); + await expect(insertedRow).toBeVisible(); + + const cell0 = insertedRow.locator('td').nth(0); + await expect(cell0).not.toHaveClass(/dx-datagrid-invalid/); + + await cell0.click(); + await saveButton.click(); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + await expect(insertedRow).toBeVisible(); + + const editor0 = cell0.locator('.dx-texteditor-input'); + await cell0.click(); + await editor0.fill('1'); + await page.keyboard.press('Enter'); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await saveButton.click(); + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted')); + await expect(insertedRow).toBeHidden(); + }); + + test('Async Validation(Batch) - Only valid data is saved in a modified cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'batch', + allowUpdating: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + }, 'name', 'lastName'], + }); + + const dataGrid = new DataGrid(page); + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await cell0.click(); + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-editor-cell/); + + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await saveButton.click(); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + + await cell0.click(); + await expect(cell0).toHaveClass(/dx-editor-cell/); + await editor0.selectText(); + await editor0.fill('1'); + await page.keyboard.press('Enter'); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await saveButton.click(); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).not.toHaveClass(/dx-editor-cell/); + }); + + test('Async Validation(Batch) - Data is not saved when a dependant cell value becomes invalid', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'batch', + allowUpdating: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + setCellValue(rowData: any, value: any): void { + rowData.age = value; + if (value === 1) { + rowData.name = ''; + } + }, + }, { + dataField: 'name', + validationRules: [{ type: 'required' }], + }, 'lastName'], + }); + + const dataGrid = new DataGrid(page); + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + + const cell0 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0); + const cell1 = page.locator('.dx-data-row[aria-rowindex="1"] td').nth(1); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await cell0.click(); + await expect(cell0).toHaveClass(/dx-editor-cell/); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + + await editor0.selectText(); + await editor0.fill('3'); + await page.keyboard.press('Enter'); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + + await saveButton.click(); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).not.toHaveClass(/dx-editor-cell/); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).not.toHaveClass(/dx-datagrid-invalid/); + + await cell0.click(); + await editor0.selectText(); + await editor0.fill('1'); + await page.keyboard.press('Enter'); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + await expect(cell1).toHaveClass(/dx-cell-modified/); + + await saveButton.click(); + await page.waitForFunction(() => !document.querySelector('.dx-data-row td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-cell-modified/); + await expect(cell0).not.toHaveClass(/dx-editor-cell/); + await expect(cell0).not.toHaveClass(/dx-datagrid-invalid/); + await expect(cell1).toHaveClass(/dx-cell-modified/); + await expect(cell1).not.toHaveClass(/dx-editor-cell/); + await expect(cell1).toHaveClass(/dx-datagrid-invalid/); + }); + + test('Async Validation(Batch) - Data is not saved when a cell with async setCellValue is invalid', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + errorRowEnabled: true, + dataSource: [{ id: 1, name: 'Alex', age: 15, lastName: 'John' }], + keyExpr: 'id', + legacyRendering: false, + editing: { + mode: 'batch', + allowAdding: true, + }, + columns: [{ + dataField: 'age', + validationRules: [{ + type: 'async', + validationCallback(params: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + if (params.value === 1) d.resolve(true); + else d.reject(); + }, 1000); + return d.promise(); + }, + }], + setCellValue(rowData: any, value: any): any { + const d = (window as any).$.Deferred(); + setTimeout(() => { + rowData.age = value; + d.resolve(); + }, 1200); + return d.promise(); + }, + }, 'name', 'lastName'], + }); + + const dataGrid = new DataGrid(page); + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const insertedRow = page.locator('.dx-data-row.dx-row-inserted[aria-rowindex="1"]'); + const cell0 = insertedRow.locator('td').nth(0); + const editor0 = cell0.locator('.dx-texteditor-input'); + + await cell0.click(); + await editor0.fill('123'); + await page.keyboard.press('Enter'); + + await saveButton.click(); + + await expect(cell0).toHaveClass(/dx-validation-pending/); + await page.waitForFunction(() => !document.querySelector('.dx-row-inserted td.dx-validation-pending')); + await expect(cell0).toHaveClass(/dx-datagrid-invalid/); + await expect(cell0).toHaveClass(/dx-cell-modified/); + await expect(insertedRow).toBeVisible(); + }); + + [false, true].forEach((remoteOperations) => { + test(`Empty rows should not appear after rows are updated in batch editing mode when paging and validation are enabled and remoteOperations=${remoteOperations}`, async ({ page }) => { + const data = Array.from({ length: 10 }, (_, i) => ({ + field_0: `val_${i}_0`, + field_1: `val_${i}_1`, + field_2: `val_${i}_2`, + field_3: `val_${i}_3`, + })); + + await createWidget(page, 'dxDataGrid', { + dataSource: data, + keyExpr: 'field_0', + paging: { + pageSize: 5, + }, + remoteOperations, + editing: { + allowUpdating: true, + mode: 'batch', + }, + columns: [ + { + dataField: 'field_0', + validationRules: [ + { + type: 'custom', + validationCallback: (options: any) => options.value !== 'val_5_0', + }, + ], + }, + 'field_1', + 'field_2', + 'field_3', + ], + }); + + await page.evaluate(({ d }) => { + const keys = d.map((e: any) => e.field_0); + const columnToModify = 'field_1'; + const grid = ($('#container') as any).dxDataGrid('instance'); + const changes = grid.option('editing.changes'); + keys.forEach((key: string) => { + const editData = changes.find( + (change: any) => change.type === 'update' && change.key === key, + ); + if (editData) { + editData.data[columnToModify] = 'EEEEEE'; + } else { + changes.push({ + type: 'update', + key, + data: { [columnToModify]: 'EEEEEE' }, + }); + } + }); + grid.option('editing.changes', changes); + }, { d: data }); + + const dataGrid = new DataGrid(page); + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + await saveButton.click(); + + const rowCount = await page.locator('.dx-data-row').count(); + expect(rowCount).toBe(remoteOperations ? 5 : 6); + + const firstCellText = await page.locator('.dx-data-row[aria-rowindex="1"] td').nth(0).textContent(); + expect(firstCellText).toBe(remoteOperations ? 'val_0_0' : 'val_5_0'); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts new file mode 100644 index 000000000000..6b402e18f388 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts @@ -0,0 +1,245 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('initNewRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('No errors should be thrown if inserting new row after cancelling insert on second page', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(40)].map((_, index) => ({ id: index + 1, text: `item ${index + 1}` })), + keyExpr: 'id', + paging: { + pageIndex: 1, + }, + columns: ['id', 'text'], + showBorders: true, + editing: { mode: 'popup', allowAdding: true }, + onInitNewRow(e: any) { + e.data.id = 0; + e.data.text = 'test'; + }, + height: 300, + }); + + const dataGrid = new DataGrid(page); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + await dataGrid.getPopupEditForm().cancelButton.click(); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + await expect(dataGrid.getPopupEditForm().element).toBeVisible(); + }); + + test('onInitNewRow should set default data for new row in row mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: 'John', age: 30 }], + keyExpr: 'id', + columns: ['name', 'age'], + editing: { mode: 'row', allowAdding: true }, + onInitNewRow(e: any) { + e.data.name = 'Default Name'; + e.data.age = 25; + }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const newRow = dataGrid.getDataRow(0); + const nameEditor = newRow.getDataCell(0).element.locator('.dx-texteditor-input'); + const ageEditor = newRow.getDataCell(1).element.locator('.dx-texteditor-input'); + + await expect(nameEditor).toHaveValue('Default Name'); + await expect(ageEditor).toHaveValue('25'); + }); + + test('onInitNewRow should set default data for new row in popup mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: 'John' }], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'popup', allowAdding: true }, + onInitNewRow(e: any) { + e.data.name = 'Preset Value'; + }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const popup = dataGrid.getPopupEditForm(); + await expect(popup.element).toBeVisible(); + + const nameEditor = popup.element.locator('.dx-texteditor-input').first(); + await expect(nameEditor).toHaveValue('Preset Value'); + }); + + test('new row should be inserted at the top in row mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'First' }, + { id: 2, name: 'Second' }, + ], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'row', allowAdding: true }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const firstRow = dataGrid.getDataRow(0); + const firstRowCell = firstRow.getDataCell(0); + await expect(firstRowCell.element.locator('.dx-texteditor-input')).toBeVisible(); + }); + + test('new row in cell mode should become editable immediately', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: 'Existing' }], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'cell', allowAdding: true }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const newRowCell = dataGrid.getDataRow(0).getDataCell(0); + await expect(newRowCell.element.locator('.dx-texteditor-input')).toBeVisible(); + }); + + test('new row in batch mode should appear when clicking add row button', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: 'Item 1' }], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'batch', allowAdding: true }, + }); + + const dataGrid = new DataGrid(page); + + const rowsBefore = await dataGrid.dataRows.count(); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + const rowsAfter = await dataGrid.dataRows.count(); + + expect(rowsAfter).toBe(rowsBefore + 1); + }); + + test('canceling new row in row mode should remove the inserted row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: 'Item 1' }], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'row', allowAdding: true }, + }); + + const dataGrid = new DataGrid(page); + const rowsBefore = await dataGrid.dataRows.count(); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + expect(await dataGrid.dataRows.count()).toBe(rowsBefore + 1); + + const newRow = dataGrid.getDataRow(0); + await newRow.getDataCell(1).element.locator('.dx-link-cancel').click(); + + expect(await dataGrid.dataRows.count()).toBe(rowsBefore); + }); + + test('new row saves data correctly in row mode', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + keyExpr: 'id', + columns: [{ dataField: 'id' }, { dataField: 'name' }], + editing: { mode: 'row', allowAdding: true }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const newRow = dataGrid.getDataRow(0); + await newRow.getDataCell(0).element.locator('.dx-texteditor-input').fill('10'); + await newRow.getDataCell(1).element.locator('.dx-texteditor-input').fill('NewItem'); + await newRow.getDataCell(2).element.locator('.dx-link-save').click(); + + await expect(dataGrid.dataRows.nth(0)).toBeVisible(); + const cellValue = await dataGrid.apiGetCellValue(0, 1); + expect(cellValue).toBe('NewItem'); + }); + + test('onInitNewRow should be called each time a new row is added', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'row', allowAdding: true }, + onInitNewRow(e: any) { + const count = (window as any).initNewRowCallCount || 0; + (window as any).initNewRowCallCount = count + 1; + e.data.name = `Row ${count + 1}`; + }, + }); + + const dataGrid = new DataGrid(page); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + const firstEditor = dataGrid.getDataRow(0).getDataCell(0).element.locator('.dx-texteditor-input'); + await expect(firstEditor).toHaveValue('Row 1'); + await dataGrid.getDataRow(0).getDataCell(1).element.locator('.dx-link-cancel').click(); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + const secondEditor = dataGrid.getDataRow(0).getDataCell(0).element.locator('.dx-texteditor-input'); + await expect(secondEditor).toHaveValue('Row 2'); + + const callCount = await page.evaluate(() => (window as any).initNewRowCallCount); + expect(callCount).toBe(2); + }); + + test('new row in popup mode can be cancelled without errors', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: 'Item 1' }], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'popup', allowAdding: true }, + }); + + const dataGrid = new DataGrid(page); + const rowsBefore = await dataGrid.dataRows.count(); + + await dataGrid.getHeaderPanel().getAddRowButton().click(); + await expect(dataGrid.getPopupEditForm().element).toBeVisible(); + + await dataGrid.getPopupEditForm().cancelButton.click(); + await expect(dataGrid.getPopupEditForm().element).toBeHidden(); + + expect(await dataGrid.dataRows.count()).toBe(rowsBefore); + }); + + test('new row in form mode shows editor with default focus', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, name: 'Item 1' }], + keyExpr: 'id', + columns: ['name'], + editing: { mode: 'form', allowAdding: true }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getAddRowButton().click(); + + const editForm = dataGrid.getEditForm(); + await expect(editForm.element).toBeVisible(); + + const formEditor = editForm.element.locator('.dx-texteditor-input').first(); + await expect(formEditor).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts new file mode 100644 index 000000000000..107997562ec2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing - undefined values', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should properly set nested undefined values (T1226946)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: [{ + id: 0, + value: { + data: 100, + }, + }, { + id: 1, + value: { + data: undefined, + }, + }], + keyExpr: 'id', + columns: [{ + dataField: 'value', + customizeText: (cellInfo: any) => String(cellInfo.value.data ?? 'undefined'), + }], + showBorders: true, + })); + + const dataGrid = new DataGrid(page); + const firstCell = dataGrid.getDataCell(0, 0); + const secondCell = dataGrid.getDataCell(1, 0); + + await expect(firstCell).toHaveText('100'); + await expect(secondCell).toHaveText('undefined'); + + await dataGrid.apiCellValue(0, 0, { data: undefined }); + await dataGrid.apiSaveEditData(); + + await expect(firstCell).toHaveText('undefined'); + await expect(secondCell).toHaveText('undefined'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts new file mode 100644 index 000000000000..0dadc9453771 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts @@ -0,0 +1,374 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('The E0110 should not occur when editing a column with setCellValue in form mode (T1193894)', async ({ page }) => { + // TODO: Playwright migration - screenshot mismatch + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Name: 'test', + }], + keyExpr: 'ID', + editing: { + mode: 'form', + allowUpdating: true, + editRowKey: 1, + }, + columns: [{ + dataField: 'Name', + setCellValue(rowData: any, value: any) { + rowData.Name = value; + }, + }], + // @ts-expect-error private option + templatesRenderAsynchronously: true, + }); + + const dataGrid = new DataGrid(page); + + await dataGrid.getFormItemEditor(0).fill('new'); + await dataGrid.getEditForm().saveButton.click(); + + await testScreenshot(page, 'grid-form-editing-T1193894.png', { element: page.locator('#container') }); + }); + + test('Popup EditForm screenshot', async ({ page }) => { + const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = i + j; + items.push(item); + } + return items; + }; + + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 2), + height: 400, + showBorders: true, + editing: { + mode: 'popup', + allowUpdating: true, + }, + }); + + await page.locator('.dx-data-row').first().locator('.dx-link-edit').click(); + + await expect(page.locator('.dx-datagrid-edit-popup')).toBeVisible(); + await testScreenshot(page, 'popup-edit-form.png', { element: page.locator('#container') }); + }); + + test('Popup EditForm screenshot when editRowKey is initially specified', async ({ page }) => { + const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = i + j; + items.push(item); + } + return items; + }; + + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 2).map((item, index) => ({ ...item, id: index })), + keyExpr: 'id', + height: 400, + showBorders: true, + editing: { + mode: 'popup', + allowUpdating: true, + editRowKey: 0, + }, + }); + + await expect(page.locator('.dx-datagrid-edit-popup')).toBeVisible(); + await testScreenshot(page, 'popup-edit-form-with-initial-editrowkey.png', { element: page.locator('#container') }); + }); + + test('DataGrid - A new row is added above the existing row if the data source is empty or contains only one record and newRowPosition is set to "pageBottom" (T1287287)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + keyExpr: 'ID', + editing: { + mode: 'batch', + allowAdding: true, + newRowPosition: 'pageBottom', + }, + columns: [ + { + dataField: 'A', + }, + ], + }); + + const addRowButton = page.locator('.dx-datagrid-addrow-button'); + await addRowButton.click(); + await addRowButton.click(); + + await expect(page.locator('.dx-data-row.dx-row-inserted').nth(1)).toBeVisible(); + + await testScreenshot(page, 'newRowPosition-pageBottom-add-row-to-bottom.png', { element: page.locator('#container') }); + }); + + test('DataGrid - ColorBox in DataGrid causes input value to appear behind color preview (T1280023)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { Color: 'red' }, + ], + showBorders: true, + editing: { + allowUpdating: true, + mode: 'cell', + }, + onEditorPreparing(e: any) { + if (e.dataField === 'Color') { + e.editorName = 'dxColorBox'; + e.editorOptions.readOnly = false; + } + }, + }); + + await page.locator('.dx-data-row').first().locator('td').first().click(); + + await testScreenshot(page, 'grid-form-editing-with-color-box.png', { element: page.locator('#container') }); + }); + + test('An exception should not throw after pressing enter on the save button and onSaving\'s promise is resolved (T1201724)', async ({ page }) => { + await page.evaluate(() => { + (window as any).deferred = $.Deferred(); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, field1: 'value1', field2: 'value2', field3: 'value3' }, + { id: 2, field1: 'value4', field2: 'value5', field3: 'value6' }, + ], + keyExpr: 'id', + showBorders: true, + columns: ['field1', 'field2', 'field3'], + editing: { + mode: 'row', + allowUpdating: true, + }, + onSaving(e: any) { + e.promise = (window as any).deferred; + }, + }); + + const dataGrid = new DataGrid(page); + const dataRow = dataGrid.getDataRow(0); + + const editButton = dataRow.element.locator('.dx-link-edit'); + await editButton.click(); + + await expect(dataRow.element).toHaveClass(/dx-edit-row/); + + const editor = dataGrid.getDataCell(0, 0).locator('.dx-texteditor-input'); + await editor.fill('new_value'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + + await page.evaluate(() => (window as any).deferred.resolve()); + + await expect(dataRow.element).not.toHaveClass(/dx-edit-row/); + + await testScreenshot(page, 'grid-editing-with-onSaving-T1201724.png', { element: page.locator('#container') }); + }); + + test('DataGrid cell with checkbox should have outline on focused', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + height: 150, + width: 200, + dataSource: [{ + Id: 0, + Checkbox: true, + }], + keyExpr: 'Id', + editing: { + allowUpdating: true, + mode: 'cell', + }, + columns: ['Id', 'Checkbox'], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getDataCell(0, 0).click(); + await expect(dataGrid.getDataCell(0, 0)).toHaveClass(/dx-focused/); + await page.keyboard.press('Enter'); + await page.keyboard.press('Tab'); + + await testScreenshot(page, 'grid-checkbox-outline.png', { element: page.locator('#container') }); + }); + + [true, false].forEach((useIcons) => { + test(`The disabled state should be correct for a custom button when given as a SVG image (useIcons=${useIcons})`, async ({ page }) => { + const encodedIcon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgIHdpZHRoPSIyMHB4IiBoZWlnaHQ9IjIwcHgiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0iIzAwMDAwMCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KCTxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIC8+DQo8L3N2Zz4NCg=='; + + await createWidget(page, 'dxDataGrid', { + width: 600, + dataSource: [{ Id: 0, name: 'test' }], + keyExpr: 'Id', + editing: { + mode: 'row', + allowUpdating: true, + allowDeleting: true, + useIcons, + }, + columns: ['Id', 'name', { + type: 'buttons', + width: 200, + buttons: [ + { name: 'delete', disabled: false }, + { name: 'delete', disabled: true }, + { icon: encodedIcon, disabled: false }, + { icon: encodedIcon, disabled: true }, + ], + }], + }); + + await testScreenshot(page, `T1179114-grid-edit-custom-button when-useicons-is-${useIcons}.png`, { element: page.locator('#container') }); + }); + }); + + test('DataGrid adaptive text should have correct paddings (T1062084)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 400, + dataSource: [{ + OrderNumber: 35703, + SaleAmount: 11800, + OrderDate: '2014/04/10', + Employee: 'Harv Mudd', + }], + keyExpr: 'OrderNumber', + columnHidingEnabled: true, + editing: { + allowUpdating: true, + mode: 'batch', + }, + columns: [{ + dataField: 'OrderNumber', + caption: 'Invoice Number', + width: 300, + }, { + dataField: 'Employee', + }, { + dataField: 'OrderDate', + dataType: 'date', + }, { + dataField: 'SaleAmount', + validationRules: [{ type: 'range', max: 100000 }], + format: 'currency', + }], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getAdaptiveButton(0).click(); + + await dataGrid.getFormItemElement(0).click(); + await dataGrid.getFormItemEditor(0).fill('1'); + await page.keyboard.press('Enter'); + + await dataGrid.getFormItemElement(2).click(); + await dataGrid.getFormItemEditor(2).fill('0'); + await page.keyboard.press('Enter'); + + await testScreenshot(page, 'grid-adaptive-item-text.png', { element: page.locator('#container') }); + }); + + test('DataGrid checkboxes should have correct outline in adaptive row', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 400, + dataSource: [{ + OrderNumber: 35703, + Employee: 'Sam', + OrderDate: '2014/04/10', + Checkbox: true, + }], + keyExpr: 'OrderNumber', + columnHidingEnabled: true, + editing: { + allowUpdating: true, + mode: 'cell', + }, + columns: [{ + dataField: 'OrderNumber', + caption: 'Invoice Number', + width: 300, + }, { + dataField: 'Employee', + }, { + dataField: 'OrderDate', + dataType: 'date', + }, { + dataField: 'Checkbox', + dataType: 'boolean', + }], + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getAdaptiveButton(0).click(); + await dataGrid.getFormItemElement(2).click(); + + await testScreenshot(page, 'grid-adaptive-checkbox.png', { element: page.locator('#container') }); + }); + + test('DataGrid inside editing popup should have synchronized columns (T1059401)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ ID: 1 }], + keyExpr: 'ID', + editing: { + allowUpdating: true, + mode: 'popup', + form: { + colCount: 1, + items: [{ + template() { + return ($('
') as any).dxDataGrid({ + showColumnLines: true, + dataSource: [{ + ID: 1, + FirstName: 'John', + LastName: 'Heart', + }], + height: 200, + editing: { + allowUpdating: true, + allowDeleting: true, + }, + }); + }, + }], + }, + }, + }); + + const dataGrid = new DataGrid(page); + await page.mouse.click(10, 10); + + await dataGrid.getDataRow(0).element.locator('.dx-link-edit').click(); + + const popupOverlay = page.locator('.dx-datagrid-edit-popup .dx-overlay-content'); + await expect(popupOverlay).toBeVisible(); + + const popupDataGridRow = popupOverlay.locator('.dx-data-row').first(); + await expect(popupDataGridRow).toBeVisible(); + + await testScreenshot(page, 'grid-popup-editing-grid.png', { element: popupOverlay }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts new file mode 100644 index 000000000000..9563627914d2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Export', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + test('Warning should be thrown in console if exporting is enabled, but onExporting is not specified', async ({ page }) => { + const warnings: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') warnings.push(msg.text()); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [], + export: { + enabled: true, + }, + }); + + const isWarningExist = warnings.some((message) => message.startsWith('W1024')); + expect(isWarningExist).toBeTruthy(); + }); + + test('Warning should be thrown in console if exporting is enabled dynamically with \'export\' option, but onExporting is not specified', async ({ page }) => { + const warnings: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') warnings.push(msg.text()); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [], + }); + + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').option('export', { enabled: true })); + + const isWarningExist = warnings.some((message) => message.startsWith('W1024')); + expect(isWarningExist).toBeTruthy(); + }); + + test('Warning should be thrown in console if exporting is enabled dynamically with \'export.enabled\' option, but onExporting is not specified', async ({ page }) => { + const warnings: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') warnings.push(msg.text()); + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [], + }); + + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').option('export.enabled', true)); + + const isWarningExist = warnings.some((message) => message.startsWith('W1024')); + expect(isWarningExist).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts new file mode 100644 index 000000000000..b7557a518672 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts @@ -0,0 +1,139 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Export button', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 800, height: 800 }); + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('allowExportSelectedData: false, menu: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, value: 2 }], + export: { + enabled: true, + }, + }); + + const dataGrid = new DataGrid(page); + await testScreenshot(page, 'grid-export-one-button.png', { element: dataGrid.getHeaderPanel().element }); + }); + + test('allowExportSelectedData: false, menu: false, PDF', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, value: 2 }], + export: { + enabled: true, + formats: ['pdf'], + }, + }); + + const dataGrid = new DataGrid(page); + await testScreenshot(page, 'grid-export-one-button-pdf.png', { element: dataGrid.getHeaderPanel().element }); + }); + + test('allowExportSelectedData: true, menu: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, value: 2 }], + height: 300, + export: { + enabled: true, + allowExportSelectedData: true, + formats: ['xlsx', 'pdf', 'csv'], + }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getExportButton().click(); + + await testScreenshot(page, 'grid-export-dropdown-button.png', { element: dataGrid.element }); + }); + + test('allowExportSelectedData: false, menu: true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, value: 2 }], + export: { + enabled: true, + }, + width: 30, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getDropDownMenuButton().click(); + + await testScreenshot(page, 'grid-export-one-button-in-menu.png', { element: page.locator('html') }); + }); + + test('allowExportSelectedData: true, menu: true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, value: 2 }], + export: { + enabled: true, + allowExportSelectedData: true, + formats: ['xlsx', 'pdf'], + }, + width: 30, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getDropDownMenuButton().click(); + + await testScreenshot(page, 'grid-export-dropdown-button-in-menu.png', { element: page.locator('html') }); + }); + + test('Export is disabled when no data columns is in grid header, menu: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ value: 1 }], + groupPanel: { + visible: true, + }, + columns: [ + { dataField: 'value', groupIndex: 0 }, + ], + export: { + enabled: true, + allowExportSelectedData: true, + formats: ['xlsx', 'pdf'], + }, + }); + + const dataGrid = new DataGrid(page); + await testScreenshot(page, 'disabled-export_when-no-columns-visible.png', { element: dataGrid.element }); + }); + + test('Export is disabled when no data columns is in grid header, menu: true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ value: 1 }], + columns: [ + { dataField: 'value', visible: false }, + ], + columnChooser: { + enabled: true, + }, + toolbar: { + items: [ + { name: 'exportButton', locateInMenu: 'always' }, + { name: 'columnChooserButton', locateInMenu: 'always' }, + ], + }, + export: { + enabled: true, + allowExportSelectedData: true, + formats: ['xlsx', 'pdf'], + }, + }); + + const dataGrid = new DataGrid(page); + await dataGrid.getHeaderPanel().getDropDownMenuButton().click(); + + await testScreenshot(page, 'disabled-export-in-menu_when-no-columns-visible.png', { element: page.locator('html') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts new file mode 100644 index 000000000000..8bc03a1e208e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts @@ -0,0 +1,157 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Filtering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + // T1319193, T1311486 + + test('Proper handle custom filter operations for dates with non-date values', async ({ page }) => { + const dataSource = [{ + ID: 1, + OrderNumber: 35711, + OrderDate: '2017/01/12', + Employee: 'Jim Packard', + }, { + ID: 5, + OrderNumber: 35714, + OrderDate: '2017/01/22', + Employee: 'Harv Mudd', + }, { + ID: 7, + OrderNumber: 35983, + OrderDate: '2017/02/07', + Employee: 'Todd Hoffman', + }, { + ID: 14, + OrderNumber: 39420, + OrderDate: '2017/02/15', + Employee: 'Jim Packard', + }, { + ID: 15, + OrderNumber: 39874, + OrderDate: '2017/02/04', + Employee: 'Harv Mudd', + }]; + + return createWidget(page, 'dxDataGrid', { + dataSource, + keyExpr: 'ID', + filterRow: { visible: true }, + filterPanel: { visible: true }, + headerFilter: { visible: true }, + filterBuilder: { + customOperations: [ + { + name: 'weekends', + caption: 'Weekends', + dataTypes: ['date'], + icon: 'check', + hasValue: false, + calculateFilterExpression() { + function getOrderDay(rowData: { OrderDate: string }) { + return (new Date(rowData.OrderDate)).getDay(); + } + + return [[getOrderDay, '=', 0], 'or', [getOrderDay, '=', 6]]; + }, + }, + ], + }, + columns: [ + 'OrderNumber', + { + dataField: 'OrderDate', + dataType: 'date', + calculateFilterExpression(value, selectedFilterOperations, target) { + if (target === 'headerFilter' && value === 'weekends') { + function getOrderDay(rowData: { OrderDate: string }) { + return (new Date(rowData.OrderDate)).getDay(); + } + + return [[getOrderDay, '=', 0], 'or', [getOrderDay, '=', 6]]; + } + return this.defaultCalculateFilterExpression?.( + value, + selectedFilterOperations, + target, + ) ?? []; + }, + headerFilter: { + dataSource(data) { + if (data.dataSource) { + data.dataSource.postProcess = (results) => { + results.push({ + text: 'Weekends', + value: 'weekends', + }); + return results; + }; + } + }, + }, + }, + 'Employee', + ], + }); + + const filterPanel = dataGrid.getFilterPanel(); + + let filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); + let filterBuilder = filterBuilderPopup.getFilterBuilder(); + + await (filterBuilder.getAddButton()).click(); + expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); + await (FilterBuilder.getPopupTreeViewNodeByText('Add Condition')).click(); + await (filterBuilder.getField(0, 'item').element).click(); + await (FilterBuilder.getPopupTreeViewNodeByText('Order Date')).click(); + await (filterBuilder.getField(0, 'itemOperation').element).click(); + await (FilterBuilder.getPopupTreeViewNodeByText('Is any of')).click(); + await (filterBuilder.getField(0, 'itemValue').element).click(); + await (FilterBuilder.getPopupTreeViewNodeCheckboxByText('Weekends')).click(); + await (new Popup(FilterBuilder.getPopupTreeView()).getOkButton().element).click(); + await (filterBuilderPopup.asPopup().getOkButton().element).click(); + + expect(await dataGrid.getRows().count); + await t.eql(3); + expect(await filterPanel.getFilterText().element.innerText); + await t.eql('[Order Date] Is any of(\'Weekends\')'); + + filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); + filterBuilder = filterBuilderPopup.getFilterBuilder(); + + await (filterBuilder.getField(0, 'itemOperation').element).click(); + await (FilterBuilder.getPopupTreeViewNodeByText('Weekends')).click(); + await (filterBuilderPopup.asPopup().getOkButton().element).click(); + + expect(await dataGrid.getRows().count); + await t.eql(3); + expect(await filterPanel.getFilterText().element.innerText); + await t.eql('[Order Date] Weekends'); + + const dateFilterCell = page.locator('.dx-datagrid-filter-row td').nth(1); + + await (dateFilterCell.menuButton).click(); + await (dateFilterCell.menu.getItemByText('Between')).click(); + expect(await dataGrid.getFilterRangeOverlay().exists).toBeTruthy(); + await (dataGrid.getFilterRangeStartEditor().locator('input')).fill('2/1/2017'); + await (dataGrid.getFilterRangeEndEditor().locator('input')).fill('2/28/2017'); + await page.keyboard.press('enter'); + + expect(await dataGrid.getRows().count); + await t.eql(4); + expect(await filterPanel.getFilterText().element.innerText); + await t.eql('[Order Date] Is between(\'2/1/2017\', \'2/28/2017\')'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts new file mode 100644 index 000000000000..eed82a7c922a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('filterPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1182854 + + test.skip('editor\'s popup inside filterBuilder is opening & closing right (T1182854)', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (dataGrid.getFilterPanel().openFilterBuilderPopup(t), filterBuilder) + await createWidget(page, 'dxDataGrid', { + dataSource: [{ column1: 'first' }], + columns: ['column1'], + filterValue: ['column1', 'anyof', []], + filterPanel: { + visible: true, + }, + }); + + const filterBuilder = ( + await dataGrid.getFilterPanel().openFilterBuilderPopup(t) + ).getFilterBuilder(); + + await testScreenshot(page, 'dataGrid-filterPanel-popup-focused.png'); + await (filterBuilder.getField().getValueText()).click(); + await testScreenshot(page, 'dataGrid-filterPanel-popup.-with-editor-popup.png'); + await (filterBuilder.getField().getValueText()).click(); + await testScreenshot(page, 'dataGrid-filterPanel-popup.png'); + await (filterBuilder.getField().getValueText()).click(); + await testScreenshot(page, 'dataGrid-filterPanel-popup.-with-editor-popup.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts new file mode 100644 index 000000000000..8fb7fa841c87 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Header Filter T1163100 change filter icon', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const generateTestData = (rowCount: number) => new Array(rowCount) + .fill(null) + .map((_, idx) => ({ + dataA: `A_${idx}`, + dataB: `B_${idx}`, + dataC: `C_${idx}`, + dataD: `D_${idx}`, + })); + + [ + ['usual', ['dataA', 'dataB']], + ['fixed', [{ dataField: 'dataA', fixed: true }, { dataField: 'dataB', fixed: true }]], + ].forEach(([firstColumnsName, firstColumns]) => { + [ + ['usual', ['dataC', 'dataD']], + ['band', [{ caption: 'Band column', columns: ['dataC', 'dataD'] }]], + ].forEach(([secondColumnsName, secondColumns]) => { + ([ + ['usual', undefined], + ['virtual', { columnRenderingMode: 'virtual', rowRenderingMode: 'virtual' }], + ] as const).forEach(([scrollingName, scrolling]) => { + test.skip(`Should change filter row icon (columns ${firstColumnsName} ${secondColumnsName}, scrolling ${scrollingName}`, async ({ page }) => { + // TODO: Playwright migration - screenshot mismatch + await createWidget(page, 'dxDataGrid', { + dataSource: generateTestData(10), + filterRow: { visible: true }, + scrolling, + columns: [...(firstColumns as any[]), ...(secondColumns as any[])], + }); + + const dataGrid = new DataGrid(page); + const filterCell = dataGrid.getFilterCell(0); + const menuButton = filterCell.locator('.dx-editor-with-menu .dx-menu'); + + await menuButton.click(); + + const menuItem = page.locator('.dx-menu-item').filter({ hasText: 'Does not equal' }); + await menuItem.click(); + + await testScreenshot(page, `filter-icon-changed-${firstColumnsName as string}-${secondColumnsName as string}-${scrollingName}.png`, { + element: '#container', + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts new file mode 100644 index 000000000000..6a51c3125ef7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts @@ -0,0 +1,118 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FilterRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Filter should reset if the filter row editor text is cleared (T1257261)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, + ], + keyExpr: 'id', + filterRow: { visible: true }, + columns: ['id', 'name'], + }); + + const dataGrid = new DataGrid(page); + const filterEditor = await dataGrid.getFilterEditor(1); + + await filterEditor.click(); + await filterEditor.fill('Alice'); + await page.keyboard.press('Enter'); + + await expect(dataGrid.dataRows).toHaveCount(1); + + await filterEditor.click(); + await filterEditor.fill(''); + await page.keyboard.press('Enter'); + + await expect(dataGrid.dataRows).toHaveCount(3); + }); + + test('DataGrid - filter row\'s search-box\'s aria-label should be customizable via localization', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.localization.loadMessages({ + en: { + 'dxDataGrid-ariaSearchBox': 'custom text', + }, + }); + }); + + await createWidget(page, 'dxDataGrid', { + columns: [{ + dataField: 'test', + dataType: 'string', + }], + filterRow: { + visible: true, + }, + }); + + const ariaLabel = await page.locator('.dx-datagrid-filter-row td').first().locator('.dx-menu-item').first().getAttribute('aria-label'); + + expect(ariaLabel).toBe('custom text'); + }); + + test('Filter Row\'s Reset button does not work after a custom filter is set in Filter Builder', async ({ page }) => { + const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `${i}_${j}`; + items.push(item); + } + return items; + }; + + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 1), + height: 400, + showBorders: true, + filterRow: { + visible: true, + applyFilter: 'auto', + }, + filterBuilder: { + customOperations: [ + { + name: 'custom', + caption: 'custom', + dataTypes: ['string'], + icon: 'check', + hasValue: false, + calculateFilterExpression() { + return [['Field 0', '=', 0]]; + }, + }, + ], + allowHierarchicalFields: true, + }, + filterPanel: { visible: true }, + filterValue: [['field_0', 'custom']], + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.dataRows).toHaveCount(0); + + const filterMenuButton = page.locator('.dx-datagrid-filter-row td').first().locator('.dx-menu-item'); + await filterMenuButton.click(); + + const resetItem = page.locator('.dx-menu-item').filter({ hasText: 'Reset' }); + await resetItem.click(); + + await expect(dataGrid.dataRows).not.toHaveCount(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/visual.spec.ts new file mode 100644 index 000000000000..628ce12f0c0a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/visual.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const getNumberData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = i + j; + items.push(item); + } + return items; +}; + +test.describe('FilterRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Filter row\'s height should be adjusted by content (T1072609)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + columns: [{ + dataField: 'Date', + dataType: 'date', + width: 140, + selectedFilterOperation: 'between', + filterValue: [new Date(2022, 2, 28), new Date(2022, 2, 29)], + }], + filterRow: { visible: true }, + wordWrapEnabled: true, + showBorders: true, + }); + + await testScreenshot(page, 'T1072609.png', { element: page.locator('#container') }); + }); + + test('FilterRow range overlay screenshot', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getNumberData(20, 2), + height: 400, + showBorders: true, + filterRow: { + visible: true, + applyFilter: 'auto', + }, + scrolling: { + showScrollbar: 'never', + }, + }); + + const filterMenuButton = page.locator('.dx-datagrid-filter-row td').nth(1).locator('.dx-menu-item'); + await filterMenuButton.click(); + + const betweenItem = page.locator('.dx-menu-item-text').filter({ hasText: 'Between' }); + await betweenItem.click(); + + await expect(page.locator('.dx-datagrid-filter-range-overlay')).toBeVisible(); + + await testScreenshot(page, 'filter-row-overlay.png'); + }); + + test('Focus overlay should be visible in filter row when focusedRowEnabled is enabled', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Field: 'Item 1' }, + { ID: 2, Field: 'Item 2' }, + { ID: 3, Field: 'Item 3' }, + ], + keyExpr: 'ID', + focusedRowEnabled: true, + filterRow: { visible: true }, + showBorders: true, + columns: ['ID', 'Field'], + }); + + await page.locator('.dx-data-row').first().locator('td').first().click(); + + const filterInput = page.locator('.dx-datagrid-filter-row td').nth(1).locator('input'); + await filterInput.click(); + + await expect(filterInput).toBeFocused(); + await testScreenshot(page, 'filter-row-focus-overlay.png', { element: page.locator('#container') }); + }); + + test('DataGrid - The `between` filter dropdown sticks to the viewport edge during horizontal scrolling (T1280071)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Text: 'Item 1' }, + { ID: 2, Text: '' }, + { ID: 3, Text: 'Item 3' }, + ], + keyExpr: 'ID', + filterRow: { + visible: true, + }, + scrolling: { + useNative: true, + }, + columnWidth: 400, + width: 500, + }); + + const filterMenuButton = page.locator('.dx-datagrid-filter-row td').first().locator('.dx-menu-item'); + await filterMenuButton.click(); + + const betweenItem = page.locator('.dx-menu-item-text').filter({ hasText: 'Between' }); + await betweenItem.click(); + + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo({ x: 999 })); + + await testScreenshot(page, 'filter-row-filter-range-hide-on-scroll.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts new file mode 100644 index 000000000000..5ccb2d25bb24 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Filtering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Don\'t calculate additional filter when filtering column list is empty', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + filterValue: ['id', '>=', 1], + dataSource: null, + columns: [], + showBorders: true, + }); + + const dataGrid = new DataGrid(page); + + await dataGrid.option({ + columns: [ + { dataField: 'id', caption: 'ID', dataType: 'number' }, + { dataField: 'name', caption: 'Name', dataType: 'string' }, + ], + dataSource: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + { id: 3, name: 'Item 3' }, + ], + }); + + await expect(page.locator('.dx-datagrid').first()).toBeVisible(); + + await dataGrid.option({ + columns: [], + dataSource: undefined, + }); + + const consoleErrors: string[] = []; + page.on('pageerror', (err) => { consoleErrors.push(err.message); }); + + expect(consoleErrors.every((msg) => !msg.includes('E1047'))).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/visual.spec.ts new file mode 100644 index 000000000000..cdd58c79a57e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/visual.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Filtering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('Data should be filtered if True is selected via the filter method when case sensitivity is enabled', async ({ page }) => { + // TODO: Playwright migration - screenshot mismatch + await createWidget(page, 'dxDataGrid', { + dataSource: { + store: [ + { ID: 1, text: 'true' }, + { ID: 2, text: 'True' }, + ], + langParams: { + locale: 'en-US', + collatorOptions: { + sensitivity: 'case', + }, + }, + }, + keyExpr: 'ID', + showBorders: true, + }); + + const dataGrid = new DataGrid(page); + + await dataGrid.apiFilter(['text', '=', 'true']); + + await expect(page.locator('.dx-datagrid').first()).toBeVisible(); + await testScreenshot(page, 'filter-method-with-case-sensitive-1.png', { element: page.locator('#container') }); + + await dataGrid.apiFilter(['text', '=', 'True']); + + await expect(page.locator('.dx-datagrid').first()).toBeVisible(); + await testScreenshot(page, 'filter-method-with-case-sensitive-2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts new file mode 100644 index 000000000000..7d31ca1e0143 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts @@ -0,0 +1,236 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1156153 + + test.skip('Fixed columns should have same width as not fixed columns with columnAutoWidth: true', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (dataGridWidthFixedColumns undefined, locator.element(), clientWidth) + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 0, + // long group name causes the issue + group: 'VERY LONG GROUP TEXT VERY LONG GROUP TEXT VERY LONG GROUP TEXT', + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, { + id: 1, + group: 0, + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, + ], + keyExpr: 'id', + allowColumnReordering: true, + showBorders: true, + grouping: { + autoExpandAll: true, + }, + columnAutoWidth: true, + scrolling: { mode: 'standard', useNative: true }, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { + dataField: 'dataA', + fixed: true, + }, + 'dataB', + 'dataC', + 'dataD', + 'dataE', + 'dataF', + 'dataG', + 'dataH', + { + dataField: 'group', + groupIndex: 0, + }, + ], + }); + + await createWidget(page, 'dxDataGrid', + { + dataSource: [ + { + id: 0, + group: 'VERY LONG GROUP TEXT VERY LONG GROUP TEXT VERY LONG GROUP TEXT', + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, { + id: 1, + group: 0, + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, + ], + keyExpr: 'id', + allowColumnReordering: true, + showBorders: true, + grouping: { + autoExpandAll: true, + }, + columnAutoWidth: true, + scrolling: { mode: 'standard', useNative: true }, + columns: [ + 'dataA', + 'dataB', + 'dataC', + 'dataD', + 'dataE', + 'dataF', + 'dataG', + 'dataH', + { + dataField: 'group', + groupIndex: 0, + }, + ], + }, + '#otherContainer', + ); + + const firstFixedCell = dataGridWidthFixedColumns.locator('td').nth(1, 0); + const firstCell = dataGridUsual.locator('td').nth(1, 0); + + const fixedCellWidth = await firstFixedCell.element().clientWidth; + const cellWidth = await firstCell.element().clientWidth; + + expect(await fixedCellWidth).toBe(cellWidth); + }); + + test('DataGrid - Group summary is not updated when a column is fixed on the right side (T1223764)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0, A: 'group 0', B: 1 }, + { id: 1, A: 'group 0', B: 1 }, + { id: 2, A: 'group 0', B: 1 }, + ], + keyExpr: 'id', + repaintChangesOnly: true, + columnFixing: { + enabled: true, + // @ts-expect-error private option + legacyMode: true, + }, + groupPanel: { visible: true }, + summary: { + recalculateWhileEditing: true, + groupItems: [ + { column: 'B', summaryType: 'count' }, + { column: 'B', summaryType: 'sum' }, + ], + }, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + allowDeleting: true, + }, + columns: [ + { dataField: 'id', width: 50 }, + { dataField: 'A', groupIndex: 0 }, + { dataField: 'B', dataType: 'number' }, + ], + }); + + const dataGrid = new DataGrid(page, '#container'); + const editCell = dataGrid.getDataRow(1).getDataCell(2); + await editCell.click(); + + const editor = editCell.locator('input'); + await editor.fill('5'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); + + const groupRowText = await dataGrid.getGroupRow(0).element.textContent(); + expect(groupRowText).toContain('Count: 3'); + expect(groupRowText).toContain('Sum of B is 7'); + }); + + test('Warning should be shown when trying to set fixed state for child columns', async ({ page }) => { + const consoleMessages: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') { + consoleMessages.push(msg.text()); + } + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, Country: 'Brazil', Area: 8515767, Population_Urban: 0.85, + Population_Rural: 0.15, Population_Total: 205809000, GDP_Agriculture: 0.054, + GDP_Industry: 0.274, GDP_Services: 0.672, GDP_Total: 2353025, + }], + keyExpr: 'ID', + columnAutoWidth: true, + allowColumnReordering: true, + width: 600, + showBorders: true, + columnChooser: { enabled: true }, + columns: [ + { dataField: 'Country', fixed: true, fixedPosition: 'left' }, + { dataField: 'Area', fixed: true, fixedPosition: 'left' }, + { + caption: 'Population', + columns: [ + { caption: 'Total', dataField: 'Population_Total', format: 'fixedPoint', fixed: true, fixedPosition: 'left' }, + { caption: 'Urban', dataField: 'Population_Urban', format: 'percent', fixed: true, fixedPosition: 'left' }, + ], + }, + ], + }); + + await page.waitForTimeout(100); + + const w1028Warnings = await page.evaluate(() => { + const msgs = (window as any).__warnings || []; + return msgs.filter((m: string) => m.startsWith('W1028')); + }); + + const hasW1028 = await page.evaluate(() => { + const instance = ($('#container') as any).dxDataGrid('instance'); + return !!(instance as any); + }); + + expect(hasW1028).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.spec.ts new file mode 100644 index 000000000000..f61e40256a80 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.spec.ts @@ -0,0 +1,281 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 800, height: 800 }); + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1148937 + + test.skip('Hovering over a row should work correctly when there is a fixed column and a column with a cellTemplate (React)', async ({ page }) => { + // TODO: Playwright migration - TestCafe API remnants (dataGrid undefined, t.ok, compareResults, row.isHovered) + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(2)].map((_, index) => ({ id: index, text: `item ${index}` })), + keyExpr: 'id', + renderAsync: false, + hoverStateEnabled: true, + templatesRenderAsynchronously: true, + columns: [ + { dataField: 'id', fixed: true }, + { dataField: 'text', cellTemplate: '#test' }, + ], + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + showBorders: true, + }); + + await page.waitForTimeout(100); + + // simulating async rendering in React + await page.evaluate(() => { + const dataGrid = ($('#container') as any).dxDataGrid('instance'); + + // eslint-disable-next-line no-underscore-dangle + dataGrid.getView('rowsView')._templatesCache = {}; + + // eslint-disable-next-line no-underscore-dangle + dataGrid._getTemplate = () => ({ + render(options) { + setTimeout(() => { + ($(options.container) as any).append(($('
') as any).text(options.model.value)); + options.deferred?.resolve(); + }, 100); + }, + }); + + dataGrid.repaint(); + }); + + await page.waitForTimeout(200); + + // arrange + const firstDataRow = page.locator('.dx-data-row').nth(0); + const firstFixedDataRow = dataGrid.getFixedDataRow(0); + const secondDataRow = page.locator('.dx-data-row').nth(1); + const secondFixedDataRow = dataGrid.getFixedDataRow(1); + // act + await (firstDataRow.element).hover(); + + // assert + await testScreenshot(page, 'T1148937-grid-hover-row-1.png', { element: page.locator('#container') }); + + expect(await firstDataRow.isHovered); + await t.ok(); + expect(await firstFixedDataRow.isHovered); + await t.ok(); + + // act + await (secondFixedDataRow.element).hover(); + + // assert + await testScreenshot(page, 'T1148937-grid-hover-row-2.png', { element: page.locator('#container') }); + + expect(await secondDataRow.isHovered); + await t.ok(); + expect(await secondFixedDataRow.isHovered); + await t.ok(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); + + // T1177143 + test('Fixed to the right columns should appear when any column has undefined or 0 width', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + columnAutoWidth: false, + dataSource: [{ + Column1: 'a', + Column2: 'b', + Column3: 'b', + Column4: 'c', + Column5: 'd', + Column6: 'e', + Column7: 'f', + Column8: 'g', + }], + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { dataField: 'Column1', fixed: true, fixedPosition: 'right', width: 100 }, + { dataField: 'Column2', width: undefined }, + { dataField: 'Column3', width: 0 }, + { dataField: 'Column4', width: 220 }, + { dataField: 'Column5', width: 240 }, + { dataField: 'Column6', width: 240 }, + { dataField: 'Column7', width: 0 }, + { dataField: 'Column8', width: 270 }, + ], + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + await testScreenshot(page, 'T1177143-right-fixed-column-with-no-width-columns-1.png', { element: page.locator('#container') }); + + await dataGrid.scrollTo({ x: 5000 }); + await page.waitForTimeout(100); + + await testScreenshot(page, 'T1177143-right-fixed-column-with-no-width-columns-2.png', { element: page.locator('#container') }); + }); + + // T1193153 + test('The grid layout should be correct after resizing the window when there are fixed and band columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + columnAutoWidth: true, + dataSource: [{}], + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [{ + caption: 'Fixed column', + fixed: true, + columns: [{ caption: 'Banded column', width: 150 }], + }, { + caption: 'Default column', + }, { + type: 'buttons', + width: 50, + }], + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + await testScreenshot(page, 'T1193153-layout-with-fixed-and-band-columns-1.png', { element: page.locator('#container') }); + + await page.setViewportSize({ width: 400, height: 400 }); + await page.waitForTimeout(100); + + await testScreenshot(page, 'T1193153-layout-with-fixed-and-band-columns-2.png', { element: page.locator('#container') }); + }); + + // T1322380 + test('The grid layout should be correct after unfixing a column via the context menu', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Title: 'Mr.', + FirstName: 'John', + LastName: 'Heart', + Position: 'CEO', + Address: '351 S Hill St.', + City: 'Los Angeles', + Zipcode: 90013, + State: 'California', + }, { + ID: 2, + Title: 'Mrs.', + FirstName: 'Olivia', + LastName: 'Peyton', + Position: 'Sales Assistant', + Address: '807 W Paseo Del Mar', + City: 'Los Angeles', + Zipcode: 90036, + State: 'California', + }], + keyExpr: 'ID', + columnAutoWidth: true, + showBorders: true, + repaintChangesOnly: true, + columnFixing: { enabled: true }, + width: 800, + columns: [ + { dataField: 'Title', fixed: true }, + { dataField: 'FirstName', fixed: true }, + { dataField: 'LastName', fixed: true }, + { dataField: 'Position', fixed: true }, + { dataField: 'Address' }, + { dataField: 'City' }, + { dataField: 'Zipcode' }, + { dataField: 'State' }, + ], + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + const positionHeader = dataGrid.getHeaders().getHeaderRow(0).locator('td').nth(3); + await positionHeader.click({ button: 'right' }); + + const contextMenu = dataGrid.getContextMenu(); + await contextMenu.getItemByText('Unfix').click(); + await page.waitForTimeout(100); + + await testScreenshot(page, 'T1322380-unfix-column-via-context-menu.png', { element: page.locator('#container') }); + }); + + // T1317623 + test('Expand columns headers offsets should be correct with fixed band columns and fixed command columns (T1317623)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + ID: 1, + CompanyName: 'Super Mart of the West', + Address: '702 SW 8th Street', + City: 'Bentonville', + State: 'Arkansas', + Zipcode: 72716, + Phone: '(800) 555-2797', + Fax: '(800) 555-2171', + }, + { + ID: 2, + CompanyName: 'K&S Music', + Address: '1000 Nicllet Mall', + City: 'Minneapolis', + State: 'Minnesota', + Zipcode: 55403, + Phone: '(612) 304-6073', + Fax: '(612) 304-6074', + }, + ], + keyExpr: 'ID', + width: '100%', + showBorders: true, + columnWidth: 200, + columnFixing: { enabled: true }, + selection: { mode: 'multiple' }, + grouping: { autoExpandAll: true }, + masterDetail: { enabled: true }, + columns: [ + { + caption: 'Company Info', + fixed: true, + fixedPosition: 'left', + columns: [ + { dataField: 'CompanyName', groupIndex: 1, showWhenGrouped: true }, + { dataField: 'Phone' }, + { dataField: 'Fax' }, + ], + }, + 'City', + { dataField: 'State', groupIndex: 0 }, + 'Address', + 'Zipcode', + ], + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + await testScreenshot(page, 'T1317623-expand-columns-with-band-columns.png', { element: page.locator('#container') }); + + await dataGrid.scrollTo({ x: 5000 }); + await page.waitForTimeout(100); + + await testScreenshot(page, 'T1317623-horizontal-scroll-with-fixed-band-columns.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts new file mode 100644 index 000000000000..4aeced31cc5c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts @@ -0,0 +1,285 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Focus', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_SELECTOR = '#container'; + const FOCUSED_CLASS = 'dx-focused'; + + test('Should remove dx-focused class on blur event from the cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { A: 0, B: 1, C: 2 }, + { A: 3, B: 4, C: 5 }, + { A: 6, B: 7, C: 8 }, + ], + editing: { + mode: 'batch', + allowUpdating: true, + startEditAction: 'dblClick', + }, + onCellClick: (event: any) => event.component.focus(event.cellElement), + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const secondCell = page.locator('.dx-data-row').nth(1).locator('td').nth(1); + + await firstCell.click(); + await secondCell.click(); + + const firstCellHasFocused = await firstCell.evaluate((el, cls) => el.classList.contains(cls), FOCUSED_CLASS); + const secondCellHasFocused = await secondCell.evaluate((el, cls) => el.classList.contains(cls), FOCUSED_CLASS); + + expect(firstCellHasFocused).toBeFalsy(); + expect(secondCellHasFocused).toBeTruthy(); + }); + + [true, false].forEach((reshapeOnPush) => { + test(`focused row should have dx-focused class after removing previous focused row (reshapeOnPush=${reshapeOnPush})`, async ({ page }) => { + const dataGrid = new DataGrid(page, GRID_SELECTOR); + + await page.evaluate((rPush) => { + const { DevExpress } = (window as any); + const store = new DevExpress.data.ArrayStore({ + data: [ + { id: 0, name: 'Item 1 ' }, + { id: 1, name: 'Item 2' }, + { id: 2, name: 'Item 3' }, + { id: 3, name: 'Item 4' }, + { id: 4, name: 'Item 5' }, + ], + key: 'id', + }); + const dataSource = new DevExpress.data.DataSource({ store, reshapeOnPush: rPush }); + + ($('#container') as any).dxDataGrid({ + columns: ['name'], + dataSource, + keyExpr: 'value', + focusedRowEnabled: true, + focusedRowKey: 1, + }); + }, reshapeOnPush); + + const focusedRow = dataGrid.getFocusedRow(); + await expect(focusedRow).toBeVisible(); + await expect(focusedRow).toContainText('Item 2'); + + await dataGrid.apiPush([{ type: 'remove', key: 1 }]); + await page.waitForTimeout(200); + + const newFocusedRow = dataGrid.getFocusedRow(); + await expect(newFocusedRow).toBeVisible(); + await expect(newFocusedRow).toContainText('Item 3'); + }); + }); + + [true, false].forEach((reshapeOnPush) => { + test(`DataGrid should restore focused row by index after row removed via push API (reshapeOnPush=${reshapeOnPush}) (T1233973)`, async ({ page }) => { + await page.evaluate((rPush) => { + const { DevExpress } = (window as any); + const store = new DevExpress.data.ArrayStore({ + data: [ + { id: 0, name: 'Item 1 ' }, + { id: 1, name: 'Item 2' }, + { id: 2, name: 'Item 3' }, + { id: 3, name: 'Item 4' }, + { id: 4, name: 'Item 5' }, + ], + key: 'id', + }); + const dataSource = new DevExpress.data.DataSource({ store, reshapeOnPush: rPush }); + (window as any).onFocusedRowChangedCounter = 0; + + ($('#container') as any).dxDataGrid({ + columns: ['name'], + dataSource, + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 2, + onFocusedRowChanged: () => { + (window as any).onFocusedRowChangedCounter += 1; + }, + }); + }, reshapeOnPush); + + await page.waitForTimeout(200); + + const dataGrid = new DataGrid(page, GRID_SELECTOR); + + await expect(dataGrid.getDataRow(2).element).toHaveClass(/dx-row-focused/); + await expect(dataGrid.getDataRow(2).element).toContainText('Item 3'); + + const counter1 = await page.evaluate(() => (window as any).onFocusedRowChangedCounter); + expect(counter1).toBe(1); + + await dataGrid.apiPush([{ type: 'remove', key: 2 }]); + await page.waitForTimeout(200); + + await expect(dataGrid.getDataRow(2).element).toHaveClass(/dx-row-focused/); + await expect(dataGrid.getDataRow(2).element).toContainText('Item 4'); + + const focusedRowKey = await dataGrid.option('focusedRowKey'); + expect(focusedRowKey).toBe(3); + + const counter2 = await page.evaluate(() => (window as any).onFocusedRowChangedCounter); + expect(counter2).toBe(2); + }); + }); + + test('DataGrid - FilterRow cell loses focus when focusedRowEnabled is true and editing is in batch mode (T1246926)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ ID: 1, FirstName: 'John' }], + keyExpr: 'ID', + filterRow: { visible: true }, + focusedRowEnabled: true, + editing: { mode: 'batch', allowUpdating: true }, + columns: ['FirstName'], + }); + + const dataGrid = new DataGrid(page, GRID_SELECTOR); + const firstDataCell = dataGrid.getDataCell(0, 0); + await firstDataCell.click(); + + const filterInput = dataGrid.getFilterRow().locator('td').first().locator('input'); + await filterInput.click(); + + await expect(filterInput).toBeFocused(); + }); + + test('DataGrid - FocusedRowChanged event isnt raised when the push API is used to remove the last row (T1261532)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: { + store: { + data: [{ id: 1, name: 'Item 1 ' }], + type: 'array', + key: 'id', + }, + reshapeOnPush: true, + }, + keyExpr: 'id', + showBorders: true, + focusedRowEnabled: true, + focusedRowKey: 1, + onInitialized(e) { + e.component?.getDataSource().store().push([{ type: 'remove', key: 1 }]); + }, + }); + + await page.waitForTimeout(200); + + const focusedRowKey = await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').option('focusedRowKey')); + const focusedRowIndex = await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').option('focusedRowIndex')); + + expect(focusedRowKey).toBeNull(); + expect(focusedRowIndex).toBe(-1); + }); + + ['onFocusedRowChanged', 'onFocusedRowChanging'].forEach((event) => { + test(`Focus should be preserved on datagrid when rowsview repaints in ${event} event (T1224663)`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'name 1' }, + { id: 2, name: 'name 2' }, + { id: 3, name: 'name 3' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + [event]: (e: any) => { + e.component.repaint(); + }, + }); + + const dataGrid = new DataGrid(page, GRID_SELECTOR); + await dataGrid.getDataCell(0, 0).click(); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + + await page.keyboard.press('ArrowDown'); + await page.waitForTimeout(100); + + const focusedRowIndex = await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').option('focusedRowIndex')); + expect(focusedRowIndex).toBe(1); + }); + }); + + test('DataGrid - Focused cell appearance is applied to non-editable CheckBox cells on mouse clicks (T1282082)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ BoolOne: false, BoolTwo: false }], + columns: ['BoolOne', 'BoolTwo'], + }); + + const dataGrid = new DataGrid(page, GRID_SELECTOR); + const cell00 = dataGrid.getDataCell(0, 0); + const cell01 = dataGrid.getDataCell(0, 1); + + await cell00.click(); + await cell01.click(); + await cell00.click(); + + const isFocused = await cell00.evaluate((el) => el.classList.contains('dx-focused')); + expect(isFocused).toBeFalsy(); + }); + + test('Focus method should focus the first data cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'name 1' }, + { id: 2, name: 'name 2' }, + { id: 3, name: 'name 3' }, + ], + keyExpr: 'id', + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const dataGrid = new DataGrid(page, GRID_SELECTOR); + await dataGrid.focus(); + await page.waitForTimeout(100); + + const firstCellFocused = await dataGrid.getDataCell(0, 0).evaluate((el) => document.activeElement === el || el.contains(document.activeElement)); + expect(firstCellFocused).toBeTruthy(); + }); + + test('Focus method should focus the first data row when focusedRowEnabled = true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'name 1' }, + { id: 2, name: 'name 2' }, + { id: 3, name: 'name 3' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const dataGrid = new DataGrid(page, GRID_SELECTOR); + await dataGrid.focus(); + await page.waitForTimeout(100); + + const firstRowFocused = await dataGrid.getDataRow(0).element.evaluate((el) => document.activeElement === el || el.contains(document.activeElement) || el === document.activeElement); + expect(firstRowFocused).toBeTruthy(); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts new file mode 100644 index 000000000000..34f269332106 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts @@ -0,0 +1,314 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +const getGridDataConfig = (size: number) => ({ + keyExpr: 'id', + dataSource: Array.from({ length: size }, (_, idx) => ({ + id: idx, + dataA: `dataA_${idx}`, + dataB: `dataB_${idx}`, + dataC: `dataC_${idx}`, + })), + columns: ['dataA', 'dataB', 'dataC'], +}); + +test.describe('Focused row - new rows T1162227', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('It should fire events after new rows were added', async ({ page }) => { + await page.evaluate(() => { + (window as any).focusedRowChangingCount = 0; + (window as any).focusedRowChangedCount = 0; + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + editing: { + mode: 'batch', + allowAdding: true, + }, + onFocusedRowChanging() { + (window as any).focusedRowChangingCount += 1; + }, + onFocusedRowChanged() { + (window as any).focusedRowChangedCount += 1; + }, + }); + + const dataGrid = new DataGrid(page); + await expect(dataGrid.getContainer()).toBeVisible(); + + await dataGrid.apiAddRow(); + + const firstDataCell = dataGrid.getDataCell(0, 0).element; + await firstDataCell.click(); + + const changingCount = await page.evaluate(() => (window as any).focusedRowChangingCount); + const changedCount = await page.evaluate(() => (window as any).focusedRowChangedCount); + + expect(changingCount).toBeGreaterThan(0); + expect(changedCount).toBeGreaterThan(0); + }); + + test('It should fire events when focus switch between existing and a new row', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowFocusChangingResults = []; + (window as any).rowFocusChangedResults = []; + }); + + await createWidget(page, 'dxDataGrid', { + focusedRowEnabled: true, + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + ...getGridDataConfig(4), + onFocusedRowChanging: ({ prevRowIndex, newRowIndex }: any) => { + (window as any).rowFocusChangingResults.push([prevRowIndex, newRowIndex]); + }, + onFocusedRowChanged: ({ rowIndex }: any) => { + (window as any).rowFocusChangedResults.push([rowIndex]); + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + const headerPanel = dataGrid.getHeaderPanel(); + await headerPanel.getAddRowButton().click(); + await page.waitForTimeout(100); + + await dataGrid.getDataCell(1, 0).click(); + await page.waitForTimeout(100); + await dataGrid.getDataCell(0, 0).click(); + await page.waitForTimeout(100); + + const rowFocusChanging = await page.evaluate(() => (window as any).rowFocusChangingResults); + const rowFocusChanged = await page.evaluate(() => (window as any).rowFocusChangedResults); + + expect(rowFocusChanging.length).toBeGreaterThan(0); + expect(rowFocusChanged.length).toBeGreaterThan(0); + }); + + test('It should not fire row events if focusedRowEnabled: false', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowFocusChangingCount = 0; + (window as any).rowFocusChangedCount = 0; + (window as any).cellFocusChangingCount = 0; + }); + + await createWidget(page, 'dxDataGrid', { + focusedRowEnabled: false, + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + ...getGridDataConfig(4), + onFocusedRowChanging: () => { + (window as any).rowFocusChangingCount += 1; + }, + onFocusedRowChanged: () => { + (window as any).rowFocusChangedCount += 1; + }, + onFocusedCellChanging: () => { + (window as any).cellFocusChangingCount += 1; + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + const headerPanel = dataGrid.getHeaderPanel(); + await headerPanel.getAddRowButton().click(); + await page.waitForTimeout(100); + + await dataGrid.getDataCell(1, 0).click(); + await page.waitForTimeout(100); + await dataGrid.getDataCell(0, 0).click(); + await page.waitForTimeout(100); + + const rowChangingCount = await page.evaluate(() => (window as any).rowFocusChangingCount); + const rowChangedCount = await page.evaluate(() => (window as any).rowFocusChangedCount); + const cellChangingCount = await page.evaluate(() => (window as any).cellFocusChangingCount); + + expect(rowChangingCount).toBe(0); + expect(rowChangedCount).toBe(0); + expect(cellChangingCount).toBeGreaterThan(0); + }); + + test('It should fire rowChanged event on initialization if focusedRowKey options is set', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowFocusChangedResults = []; + }); + + await createWidget(page, 'dxDataGrid', { + focusedRowKey: 1, + focusedRowEnabled: true, + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + ...getGridDataConfig(4), + onFocusedRowChanged: ({ rowIndex }: any) => { + (window as any).rowFocusChangedResults.push([rowIndex]); + }, + }); + + await page.waitForTimeout(100); + + const rowFocusChanged = await page.evaluate(() => (window as any).rowFocusChangedResults); + expect(rowFocusChanged).toEqual([[1]]); + }); + + test('It should be able to change focusedRowKey on "onContentReady"', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowFocusChangedResults = []; + }); + + await createWidget(page, 'dxDataGrid', { + focusedRowKey: 1, + focusedRowEnabled: true, + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + onContentReady: ({ component }: any) => { + component.option('focusedRowKey', 3); + }, + ...getGridDataConfig(4), + onFocusedRowChanged: ({ rowIndex }: any) => { + (window as any).rowFocusChangedResults.push([rowIndex]); + }, + }); + + await page.waitForTimeout(100); + + const rowFocusChanged = await page.evaluate(() => (window as any).rowFocusChangedResults); + expect(rowFocusChanged).toEqual([[1], [3]]); + }); + + test('It should fire correct events on page change', async ({ page }) => { + await page.evaluate(() => { + (window as any).cellFocusChangingResults = []; + (window as any).cellFocusChangedResults = []; + (window as any).rowFocusChangingResults = []; + (window as any).rowFocusChangedResults = []; + }); + + await createWidget(page, 'dxDataGrid', { + focusedRowEnabled: true, + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + paging: { pageSize: 2 }, + ...getGridDataConfig(4), + onFocusedCellChanging: ({ prevRowIndex, prevColumnIndex, newRowIndex, newColumnIndex }: any) => { + (window as any).cellFocusChangingResults.push([[prevRowIndex, prevColumnIndex], [newRowIndex, newColumnIndex]]); + }, + onFocusedCellChanged: ({ rowIndex, columnIndex }: any) => { + (window as any).cellFocusChangedResults.push([rowIndex, columnIndex]); + }, + onFocusedRowChanging: ({ prevRowIndex, newRowIndex }: any) => { + (window as any).rowFocusChangingResults.push([prevRowIndex, newRowIndex]); + }, + onFocusedRowChanged: ({ rowIndex }: any) => { + (window as any).rowFocusChangedResults.push([rowIndex]); + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + await dataGrid.getDataCell(0, 0).click(); + await page.waitForTimeout(100); + + const page2Button = dataGrid.getPager().locator('[aria-label="Page 2"]'); + await page2Button.click(); + await page.waitForTimeout(100); + + await dataGrid.getDataCell(2, 0).click(); + await page.waitForTimeout(100); + + const cellFocusChangingResults = await page.evaluate(() => (window as any).cellFocusChangingResults); + const rowFocusChangingResults = await page.evaluate(() => (window as any).rowFocusChangingResults); + const rowFocusChangedResults = await page.evaluate(() => (window as any).rowFocusChangedResults); + + expect(cellFocusChangingResults.length).toBeGreaterThan(0); + expect(rowFocusChangingResults.length).toBeGreaterThan(0); + expect(rowFocusChangedResults.length).toBeGreaterThan(0); + }); + + test('It should fire row changed event and change page if focusedRowKey on another page', async ({ page }) => { + await page.evaluate(() => { + (window as any).rowFocusChangedResults = []; + }); + + await createWidget(page, 'dxDataGrid', { + focusedRowEnabled: true, + focusedRowKey: 3, + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + paging: { pageSize: 2 }, + ...getGridDataConfig(4), + onFocusedRowChanged: ({ rowIndex }: any) => { + (window as any).rowFocusChangedResults.push([rowIndex]); + }, + }); + + await page.waitForTimeout(100); + + const rowFocusChanged = await page.evaluate(() => (window as any).rowFocusChangedResults); + expect(rowFocusChanged).toEqual([[1]]); + + const dataGrid = new DataGrid(page, '#container'); + const cellText = await dataGrid.getDataCell(3, 0).innerText(); + expect(cellText).toBe('dataA_3'); + }); + + test('After modification of newRowIndex / newCellIndex focused row and cell should be changed', async ({ page }) => { + await page.evaluate(() => { + (window as any).cellFocusChangingResults = []; + (window as any).cellFocusChangedResults = []; + (window as any).rowFocusChangingResults = []; + (window as any).rowFocusChangedResults = []; + }); + + await createWidget(page, 'dxDataGrid', { + focusedRowEnabled: true, + editing: { mode: 'batch', allowAdding: true, allowUpdating: true }, + ...getGridDataConfig(4), + onFocusedCellChanging: (event: any) => { + (window as any).cellFocusChangingResults.push( + [[event.prevRowIndex, event.prevColumnIndex], [event.newRowIndex, event.newColumnIndex]], + ); + event.newRowIndex = 3; + event.newColumnIndex = 1; + }, + onFocusedCellChanged: ({ rowIndex, columnIndex }: any) => { + (window as any).cellFocusChangedResults.push([rowIndex, columnIndex]); + }, + onFocusedRowChanging: ({ prevRowIndex, newRowIndex }: any) => { + (window as any).rowFocusChangingResults.push([prevRowIndex, newRowIndex]); + }, + onFocusedRowChanged: ({ rowIndex }: any) => { + (window as any).rowFocusChangedResults.push([rowIndex]); + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getContainer()).toBeVisible(); + + await dataGrid.getDataCell(0, 0).click(); + await page.waitForTimeout(100); + + const cellFocusChangingResults = await page.evaluate(() => (window as any).cellFocusChangingResults); + const cellFocusChangedResults = await page.evaluate(() => (window as any).cellFocusChangedResults); + const rowFocusChangingResults = await page.evaluate(() => (window as any).rowFocusChangingResults); + const rowFocusChangedResults = await page.evaluate(() => (window as any).rowFocusChangedResults); + + expect(cellFocusChangingResults).toEqual([[[-1, -1], [0, 0]]]); + expect(cellFocusChangedResults).toEqual([[3, 1]]); + expect(rowFocusChangingResults).toEqual([[-1, 3]]); + expect(rowFocusChangedResults).toEqual([[3]]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts new file mode 100644 index 000000000000..9352e274a089 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, DataGrid } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Focus - cell with showEditorAlways', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const SELECTOR = '#container'; + + const createDataGrid = async (page: any) => createWidget(page, 'dxDataGrid', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 0, D: 'D_0' }, + { A: 'A_1', B: 'B_1', C: 1, D: 'D_1' }, + { A: 'A_2', B: 'B_2', C: 2, D: 'D_2' }, + ], + columns: [ + { dataField: 'A', showEditorAlways: true }, + { dataField: 'B', showEditorAlways: true }, + { + dataField: 'C', + showEditorAlways: true, + lookup: { + dataSource: [ + { id: 0, name: 'LOOKUP_0' }, + { id: 1, name: 'LOOKUP_1' }, + { id: 2, name: 'LOOKUP_2' }, + ], + displayExpr: 'name', + valueExpr: 'id', + }, + }, + { dataField: 'D', showEditorAlways: true }, + ], + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + allowDeleting: true, + }, + }); + + test('Should switch focus after the lookup value change [T1194403]', async ({ page }) => { + await createDataGrid(page); + + const editorTextCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const lookupEditor = page.locator('.dx-data-row').nth(0).locator('td').nth(2).locator('.dx-selectbox'); + + await lookupEditor.click(); + + const listItem = page.locator('.dx-overlay-wrapper .dx-list-item').nth(2); + await listItem.click(); + + await editorTextCell.click(); + await page.waitForTimeout(100); + + await testScreenshot(page, 'focus-edit-cell_after-lookup-change.png', { element: page.locator('#container') }); + }); + + test('Should switch focus after the textBox value change [T1194403]', async ({ page }) => { + await createDataGrid(page); + + const dataGrid = new DataGrid(page, SELECTOR); + const editorCellOne = dataGrid.getDataCell(0, 1).locator('input'); + const editorCellTwo = dataGrid.getDataCell(0, 0).locator('input'); + + await editorCellOne.click(); + await editorCellOne.fill('TEST_TEXT'); + await editorCellTwo.click(); + await page.waitForTimeout(100); + + await testScreenshot(page, 'focus-edit-cell_after-text-editor-change.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts new file mode 100644 index 000000000000..bbe2fb47e179 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts @@ -0,0 +1,747 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, DataGrid, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Focused row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('onFocusedRowChanged event should fire once after changing focusedRowKey if paging.enabled = false (T755722)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { name: 'Alex', phone: '111111', room: 6 }, + { name: 'Dan', phone: '2222222', room: 5 }, + { name: 'Ben', phone: '333333', room: 4 }, + ], + keyExpr: 'name', + focusedRowEnabled: true, + focusedRowIndex: 1, + paging: { enabled: false }, + onFocusedRowChanged: () => { + const global = window as Window & typeof globalThis & { onFocusedRowChangedCounter: number }; + if (!global.onFocusedRowChangedCounter) { + global.onFocusedRowChangedCounter = 0; + } + global.onFocusedRowChangedCounter += 1; + }, + }); + + const counter1 = await page.evaluate(() => (window as any).onFocusedRowChangedCounter); + expect(counter1).toBe(1); + + await page.evaluate(() => (window as any).widget.option('focusedRowKey', 'Ben')); + await page.waitForTimeout(100); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + + const counter2 = await page.evaluate(() => (window as any).onFocusedRowChangedCounter); + expect(counter2).toBe(2); + }); + + test('Focused row should not fire onFocusedRowChanging, onFocusedRowChanged events on scrolling if scrolling.mode and rowRenderingMode are virtual', async ({ page }) => { + await page.evaluate(() => { + const data: Record[] = []; + for (let i = 0; i < 200; i += 1) { + data.push({ id: i, c0: 'c0', c1: `c1_${i % 20}` }); + } + + ($('#container') as any).dxDataGrid({ + height: 300, + keyExpr: 'id', + dataSource: data, + focusedRowEnabled: true, + focusedRowKey: 1, + editing: { allowAdding: true, allowUpdating: true, mode: 'form' }, + columns: ['id', 'c0', { dataField: 'c1', groupIndex: 0 }], + paging: { pageSize: 5 }, + scrolling: { mode: 'virtual', rowRenderingMode: 'virtual' }, + onFocusedRowChanging: () => { + const g = window as any; + g.focusedRowChanging_Counter = (g.focusedRowChanging_Counter || 0) + 1; + }, + onFocusedRowChanged: () => { + const g = window as any; + g.focusedRowChanged_Counter = (g.focusedRowChanged_Counter || 0) + 1; + }, + }); + }); + + await page.waitForTimeout(300); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + + await dataGrid.scrollTo({ y: 2000 }); + await page.waitForTimeout(500); + + const changingCounterAfterScroll = await page.evaluate(() => (window as any).focusedRowChanging_Counter); + const changedCounterAfterScroll = await page.evaluate(() => (window as any).focusedRowChanged_Counter); + + expect(changingCounterAfterScroll).toBeUndefined(); + expect(changedCounterAfterScroll).toBe(1); + + await dataGrid.scrollTo({ y: 0 }); + await page.waitForTimeout(500); + + const changingCounterFinal = await page.evaluate(() => (window as any).focusedRowChanging_Counter); + const changedCounterFinal = await page.evaluate(() => (window as any).focusedRowChanged_Counter); + + expect(changingCounterFinal).toBeUndefined(); + expect(changedCounterFinal).toBe(1); + }); + + test('It is possible to focus row that was added via push method if previously row with same index was focused (T1202646)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: { + store: { type: 'array', data: [{ value: 1 }] }, + reshapeOnPush: true, + }, + keyExpr: 'value', + repaintChangesOnly: true, + focusedRowEnabled: true, + columns: [{ dataField: 'value', sortOrder: 'desc' }], + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.getDataRow(0).element.click(); + + const isFocused1 = await dataGrid.getFocusedRow().isVisible(); + expect(isFocused1).toBeTruthy(); + + await page.evaluate(() => { + const grid = ($('#container') as any).dxDataGrid('instance'); + grid.getDataSource().store().push([{ type: 'insert', data: { value: 2 } }]); + }); + + await page.waitForTimeout(100); + await expect(dataGrid.getDataRow(0).element).toContainText('2'); + + await dataGrid.getDataRow(0).element.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + }); + + [null, undefined, -1, 'test'].forEach((groupValue) => { + test(`Group should expand when focusedRowKey is set - group: ${groupValue}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, group: groupValue, name: 'Item 1' }, + { id: 2, group: groupValue, name: 'Item 2' }, + { id: 3, group: 'A', name: 'Item 3' }, + { id: 4, group: 'A', name: 'Item 4' }, + ], + keyExpr: 'id', + grouping: { autoExpandAll: false }, + columns: [ + { dataField: 'id' }, + { dataField: 'group', groupIndex: 0 }, + { dataField: 'name' }, + ], + height: 400, + focusedRowEnabled: true, + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.option('focusedRowKey', 1); + await page.waitForTimeout(200); + + await testScreenshot(page, `focused-row_under_group=${groupValue}.png`, { element: page.locator('#container') }); + }); + }); + + test('Group should expand when focusedRowKey is set and data items have \'items\' property', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, group: 'A', name: 'Item 1', items: 1 }, + { id: 2, group: 'A', name: 'Item 2', items: 2 }, + ], + keyExpr: 'id', + grouping: { autoExpandAll: false }, + columns: [ + { dataField: 'id' }, + { dataField: 'group', groupIndex: 0 }, + { dataField: 'name' }, + { dataField: 'items' }, + ], + height: 400, + focusedRowEnabled: true, + }); + + const dataGrid = new DataGrid(page, '#container'); + await dataGrid.option('focusedRowKey', 1); + await page.waitForTimeout(200); + + await testScreenshot(page, 'focused-row_under_group_when_data-items_have_items-property.png', { element: page.locator('#container') }); + }); + + test('Form - Focused row should not be reset after editing a row (T851400)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'form', + allowUpdating: true, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow0 = dataGrid.getDataRow(0); + const dataRow1 = dataGrid.getDataRow(1); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKey1 = await dataGrid.option('focusedRowKey'); + expect(focusedKey1).toBe(6); + + const editButton1 = dataRow1.element.locator('.dx-command-edit .dx-link-edit'); + await editButton1.click(); + + const editForm = dataGrid.getEditForm(); + await expect(editForm.element).toBeVisible(); + await editForm.cancelButton.click(); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKeyAfterCancel = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterCancel).toBe(6); + + await editButton1.click(); + await expect(editForm.element).toBeVisible(); + + const editor = editForm.element.locator('.dx-texteditor-input').first(); + await editor.fill('test'); + await editForm.saveButton.click(); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKeyAfterSave = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterSave).toBe(6); + + const editButton0 = dataRow0.element.locator('.dx-command-edit .dx-link-edit'); + await editButton0.click(); + await expect(editForm.element).toBeVisible(); + + const focusedKeyRow0 = await dataGrid.option('focusedRowKey'); + expect(focusedKeyRow0).toBe(5); + + await editForm.cancelButton.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKeyRow0AfterCancel = await dataGrid.option('focusedRowKey'); + expect(focusedKeyRow0AfterCancel).toBe(5); + }); + + test('Row - Focused row should not be reset after editing a row (T879627)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'row', + allowUpdating: true, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow1 = dataGrid.getDataRow(1); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + + const editButton1 = dataRow1.element.locator('.dx-command-edit .dx-link-edit'); + const cancelButton1 = dataRow1.element.locator('.dx-command-edit .dx-link-cancel'); + + await editButton1.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKeyEditing = await dataGrid.option('focusedRowKey'); + expect(focusedKeyEditing).toBe(6); + + await cancelButton1.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKeyAfterCancel = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterCancel).toBe(6); + }); + + test('Cell - Focused row should not be reset after editing a cell (T879627)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'cell', + allowUpdating: true, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow1 = dataGrid.getDataRow(1); + const dataCell11 = dataRow1.getDataCell(1); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKeyInitial = await dataGrid.option('focusedRowKey'); + expect(focusedKeyInitial).toBe(6); + + await dataCell11.element.click(); + const editor = dataCell11.element.locator('.dx-texteditor-input'); + await expect(editor).toBeVisible(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + + await page.keyboard.press('Escape'); + await expect(editor).toBeHidden(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + + const focusedKeyAfterEsc = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterEsc).toBe(6); + + await dataCell11.element.click(); + await editor.fill('test'); + await page.keyboard.press('Enter'); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + const focusedKeyAfterSave = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterSave).toBe(6); + }); + + test('Form - Focused row should not be reset after editing a row by API (T879627)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'form', + allowUpdating: true, + popup: { animation: null as any }, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow1 = dataGrid.getDataRow(1); + + const focusedKey1 = await dataGrid.option('focusedRowKey'); + expect(focusedKey1).toBe(6); + await expect(dataRow1.element).toBeVisible(); + + await dataGrid.apiEditRow(1); + const focusedKeyEditing = await dataGrid.option('focusedRowKey'); + expect(focusedKeyEditing).toBe(6); + + await dataGrid.apiCancelEditData(); + const focusedKeyAfterCancel = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterCancel).toBe(6); + + await dataGrid.apiEditRow(0); + const focusedKeyRow0 = await dataGrid.option('focusedRowKey'); + expect(focusedKeyRow0).toBe(6); + + await dataGrid.apiSaveEditData(); + const focusedKeyAfterSave = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterSave).toBe(6); + }); + + test('Popup - Focused row should not be reset after editing a row by API (T879627)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'popup', + allowUpdating: true, + popup: { animation: null as any }, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow1 = dataGrid.getDataRow(1); + + const focusedKey1 = await dataGrid.option('focusedRowKey'); + expect(focusedKey1).toBe(6); + await expect(dataRow1.element).toBeVisible(); + + await dataGrid.apiEditRow(1); + const focusedKeyEditing = await dataGrid.option('focusedRowKey'); + expect(focusedKeyEditing).toBe(6); + + await dataGrid.apiCancelEditData(); + const focusedKeyAfterCancel = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterCancel).toBe(6); + + await dataGrid.apiEditRow(0); + const focusedKeyRow0 = await dataGrid.option('focusedRowKey'); + expect(focusedKeyRow0).toBe(6); + + await dataGrid.apiSaveEditData(); + const focusedKeyAfterSave = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterSave).toBe(6); + }); + + (['Cell', 'Batch'] as const).forEach((mode) => { + test(`${mode} - Focused row should not be reset after editing a cell by API (T879627)`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: mode.toLowerCase() as any, + allowUpdating: true, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow1 = dataGrid.getDataRow(1); + + const focusedKey1 = await dataGrid.option('focusedRowKey'); + expect(focusedKey1).toBe(6); + await expect(dataRow1.element).toBeVisible(); + + await dataGrid.apiEditCell(1, 1); + const focusedKeyEditing = await dataGrid.option('focusedRowKey'); + expect(focusedKeyEditing).toBe(6); + + await dataGrid.apiCancelEditData(); + const focusedKeyAfterCancel = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterCancel).toBe(6); + + await dataGrid.apiEditCell(0, 1); + await dataGrid.apiCellValue(0, 1, 'test'); + const focusedKeyRow0 = await dataGrid.option('focusedRowKey'); + expect(focusedKeyRow0).toBe(6); + + await dataGrid.apiSaveEditData(); + const focusedKeyAfterSave = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterSave).toBe(6); + }); + }); + + test('Row - Focused row should be reset after editing a row by API (T879627)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'row', + allowUpdating: true, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow1 = dataGrid.getDataRow(1); + + const focusedKey1 = await dataGrid.option('focusedRowKey'); + expect(focusedKey1).toBe(6); + await expect(dataRow1.element).toBeVisible(); + + await dataGrid.apiEditRow(1); + const focusedKeyEditing = await dataGrid.option('focusedRowKey'); + expect(focusedKeyEditing).toBe(6); + + await dataGrid.apiCancelEditData(); + const focusedKeyAfterCancel = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterCancel).toBe(6); + + await dataGrid.apiEditRow(0); + const focusedKeyRow0 = await dataGrid.option('focusedRowKey'); + expect(focusedKeyRow0).toBe(5); + + await dataGrid.apiSaveEditData(); + const focusedKeyAfterSave = await dataGrid.option('focusedRowKey'); + expect(focusedKeyAfterSave).toBe(5); + }); + + test('Scrolling should work if scrolling.mode and rowRenderingMode are virtual row is focused (T907192)', async ({ page }) => { + await page.evaluate(() => { + const data: Record[] = []; + for (let i = 0; i < 100; i += 1) { + data.push({ id: i + 1 }); + } + ($('#container') as any).dxDataGrid({ + height: 200, + width: 200, + keyExpr: 'id', + dataSource: data, + focusedRowEnabled: true, + focusedRowKey: 1, + columns: ['id'], + scrolling: { + mode: 'virtual', + rowRenderingMode: 'virtual', + showScrollbar: 'always', + }, + }); + }); + + await page.waitForTimeout(300); + + const dataGrid = new DataGrid(page, '#container'); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + + await page.locator('#container').click({ position: { x: 195, y: 180 } }); + await expect(dataGrid.getFocusedRow()).toBeHidden(); + }); + + test('Popup - Focused row should not be reset after editing a row (T879627)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'popup', + allowUpdating: true, + popup: { animation: undefined }, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow0 = dataGrid.getDataRow(0); + const dataRow1 = dataGrid.getDataRow(1); + const popupEditForm = dataGrid.getPopupEditForm(); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + const editButton1 = dataRow1.element.locator('.dx-command-edit .dx-link-edit'); + await editButton1.click(); + await expect(popupEditForm.element).toBeVisible(); + await popupEditForm.cancelButton.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + await editButton1.click(); + await expect(popupEditForm.element).toBeVisible(); + const editor = popupEditForm.element.locator('.dx-texteditor-input').first(); + await editor.fill('test'); + await popupEditForm.saveButton.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + const editButton0 = dataRow0.element.locator('.dx-command-edit .dx-link-edit'); + await editButton0.click(); + await expect(popupEditForm.element).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(5); + await popupEditForm.cancelButton.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(5); + + await editButton0.click(); + await expect(popupEditForm.element).toBeVisible(); + await editor.fill('test2'); + await popupEditForm.saveButton.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(5); + }); + + test('Batch - Focused row should not be reset after editing a cell (T879627)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 5, c0: 'c0_0' }, + { id: 6, c0: 'c0_1' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + focusedRowKey: 6, + editing: { + mode: 'batch', + allowUpdating: true, + }, + }); + + const dataGrid = new DataGrid(page, '#container'); + const dataRow1 = dataGrid.getDataRow(1); + const dataRow0 = dataGrid.getDataRow(0); + const dataCell11 = dataRow1.getDataCell(1); + const dataCell01 = dataRow0.getDataCell(1); + + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + await dataCell11.element.click(); + const editor11 = dataCell11.element.locator('.dx-texteditor-input'); + await expect(editor11).toBeVisible(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + await page.keyboard.press('Escape'); + await expect(editor11).toBeHidden(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + await dataCell11.element.click(); + await editor11.fill('test'); + await page.keyboard.press('Enter'); + await expect(editor11).toBeHidden(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + const saveButton = dataGrid.getHeaderPanel().getSaveButton(); + await saveButton.click(); + await expect(dataGrid.getFocusedRow()).toBeVisible(); + expect(await dataGrid.option('focusedRowKey')).toBe(6); + + await dataCell01.element.click(); + const editor01 = dataCell01.element.locator('.dx-texteditor-input'); + await expect(editor01).toBeVisible(); + await expect(dataRow0.element).toHaveClass(/dx-row-focused/); + expect(await dataGrid.option('focusedRowKey')).toBe(5); + + await page.keyboard.press('Escape'); + await expect(editor01).toBeHidden(); + await expect(dataRow0.element).toHaveClass(/dx-row-focused/); + expect(await dataGrid.option('focusedRowKey')).toBe(5); + + await dataCell01.element.click(); + await editor01.fill('test2'); + await page.keyboard.press('Enter'); + await saveButton.click(); + await expect(dataRow0.element).toHaveClass(/dx-row-focused/); + expect(await dataGrid.option('focusedRowKey')).toBe(5); + }); + + test('Scrolling should not occured after deleting via push API if scrolling.mode is virtual (T930434)', async ({ page }) => { + await page.evaluate(() => { + const data: { id: number }[] = []; + for (let i = 0; i < 20; i += 1) { + data.push({ id: i + 1 }); + } + + $('