diff --git a/.github/workflows/playwright_tests.yml b/.github/workflows/playwright_tests.yml new file mode 100644 index 000000000000..ba1d54b15c3a --- /dev/null +++ b/.github/workflows/playwright_tests.yml @@ -0,0 +1,170 @@ +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" }, + ] + runs-on: devextreme-shr2 + timeout-minutes: 30 + + 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: pnpx 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 + + for i in $(seq 1 $REPEAT_COUNT); do + echo "=== Run $i / $REPEAT_COUNT ===" + pnpx playwright test \ + --config playwright.config.ts \ + playwright-tests/${{ matrix.ARGS.componentFolder }}/ \ + $SHARD_ARG \ + --reporter=list \ + 2>&1 | tee -a playwright-output-run-$i.log + echo "" + done + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-results-${{ matrix.ARGS.name }} + path: | + e2e/testcafe-devextreme/playwright-results/ + e2e/testcafe-devextreme/playwright-output-*.log + e2e/testcafe-devextreme/test-results/ + + merge-results: + name: Merge Playwright results + if: always() + needs: playwright + runs-on: devextreme-shr2 + steps: + - name: Merge artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: playwright-all-results + pattern: playwright-results-* + delete-merged: true 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/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/createWidget.ts b/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts new file mode 100644 index 000000000000..e4079a662b09 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts @@ -0,0 +1,15 @@ +import type { Page } from '@playwright/test'; + +export async function createWidget( + page: Page, + widgetName: string, + widgetOptions: Record | (() => Record), + selector = '#container', + disableFxAnimation = true, +): Promise { + await page.evaluate(({ name, opts, sel, disableFx }) => { + (window as any).DevExpress.fx.off = disableFx; + const options = typeof opts === 'function' ? opts() : opts; + ($(sel) as any)[name](options); + }, { name: widgetName, opts: widgetOptions, sel: selector, disableFx: disableFxAnimation }); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts new file mode 100644 index 000000000000..392ec294d9fa --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts @@ -0,0 +1,74 @@ +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 }) => { + const element = document.querySelector(sel); + const styles = element?.getAttribute('style') ?? ''; + element?.setAttribute('style', `${styles} ${style}`); + }, { 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, + attrs?: Record, +): Promise { + await page.evaluate(({ parent, tag, attributes }) => { + const el = document.createElement(tag); + if (attributes) { + Object.entries(attributes).forEach(([key, val]) => el.setAttribute(key, val)); + } + document.querySelector(parent)?.appendChild(el); + }, { parent: parentSelector, tag: childSelector, attributes: attrs }); +} + +export async function setClassAttribute( + page: Page, + selector: string, + className: string, +): Promise { + await page.evaluate(({ sel, cls }) => { + document.querySelector(sel)?.setAttribute('class', cls); + }, { sel: selector, cls: className }); +} 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/index.ts b/e2e/testcafe-devextreme/playwright-helpers/index.ts new file mode 100644 index 000000000000..00d96fdf73ae --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/index.ts @@ -0,0 +1,26 @@ +export { createWidget } from './createWidget'; +export { + changeTheme, + getCurrentTheme, + getFullThemeName, + getThemePostfix, + isFluent, + isMaterial, + isMaterialBased, + testScreenshot, +} from './themeUtils'; +export { + appendElementTo, + getStyleAttribute, + insertStylesheetRulesToPage, + removeStylesheetRulesFromPage, + setAttribute, + setClassAttribute, + setStyleAttribute, +} from './domUtils'; +export { + clearTestPage, + getContainerUrl, + setupTestPage, +} from './testPageUtils'; +export { generateOptionMatrix } from './generateOptionMatrix'; diff --git a/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts new file mode 100644 index 000000000000..245fa04e2611 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts @@ -0,0 +1,49 @@ +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); +} + +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); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts new file mode 100644 index 000000000000..963ba0045cda --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts @@ -0,0 +1,74 @@ +import type { Page, Locator } from '@playwright/test'; +import { expect } from '@playwright/test'; + +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`; +}; + +export async function testScreenshot( + page: Page, + screenshotName: string, + options?: { + element?: Locator | string | null; + theme?: string; + shouldTestInCompact?: boolean; + }, +): Promise { + const { + element, + theme, + shouldTestInCompact = false, + } = options ?? {}; + + if (theme) { + await changeTheme(page, theme); + } + + const locator = typeof element === 'string' + ? page.locator(element) + : element ?? page.locator('#container'); + + await expect(locator).toHaveScreenshot(getScreenshotName(screenshotName, theme)); + + if (shouldTestInCompact) { + const themeName = (theme ?? process.env.theme) ?? defaultThemeName; + await changeTheme(page, `${themeName}.compact`); + + await expect(locator).toHaveScreenshot( + getScreenshotName(screenshotName, `${themeName}.compact`), + ); + } + + if (theme || shouldTestInCompact) { + await changeTheme(page, process.env.theme ?? defaultThemeName); + } +} 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..de6e8c2f2c04 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..02376adc6f7f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..b4aa5f19c35c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..1b0d1490dfe9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..cf68c7d6422c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..c567fec1c1bc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..6ae90f686182 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts @@ -0,0 +1,28 @@ +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('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.skip('select mode', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('dragAndDrop mode', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('cardView with opened columnChooser', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..8ee1c4a9910f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts @@ -0,0 +1,28 @@ +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('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.skip('headerPanel dragging column', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('dropzone in headerPanel from columnChooser', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('dropzone with allowReordering false', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..d633a4dc3a82 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts @@ -0,0 +1,20 @@ +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('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'); + }); + + test.skip('default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..48521929788f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts @@ -0,0 +1,28 @@ +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('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'); + }); + + test.skip('default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render of add card popup', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render of edit card popup', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..2a42828d8ee8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('FilterPanel and FilterBuilderPopup', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..95f417db3562 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts @@ -0,0 +1,28 @@ +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('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'); + }); + + test.skip('popup with list', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('popup with search', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('popup with tree', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..19385f462cf8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts @@ -0,0 +1,36 @@ +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('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.skip('Default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render with header filter enabled', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render with single sorting', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render with multiple sorting', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('headerPanel column chooser link', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..c87d7d2d75ee --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..0700b655330a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts @@ -0,0 +1,20 @@ +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('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'); + }); + + test.skip('Runtime filterValue change updates paging', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..7cbf0d15c382 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts @@ -0,0 +1,20 @@ +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('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'); + }); + + test.skip('highlighted search text', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..178fe6c54755 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts @@ -0,0 +1,36 @@ +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('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'); + }); + + test.skip('Single mode', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode always', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode onClick', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode onLongTap', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode without Select All', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..31b00e9b9155 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts @@ -0,0 +1,28 @@ +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('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.skip('sortable indicator first', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('sortable indicator middle', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('sortable indicator last', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..60c635c6e696 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.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('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'); + }); + + test.skip('Default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Default multiple sorting render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Sort index API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('ShowSortIndexes API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('AllowSorting API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('CalculateSortValue API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('SortingMethod API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); 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..99cb4cfd9eab --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..1274b58f5bea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..93a16518604b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..e2d736acbae1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..e7dd6f58f98c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..065b42175d84 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..eb5cff15f318 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..5b7a67f6e93b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..934a65176245 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts @@ -0,0 +1,20 @@ +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('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'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..098f66e57b0a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..8d9fb6ea6722 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..c4725bf1a4a4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..85a5d30fa8f8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..ed9cc90d7ef5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..d438c4c28fb3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..403336a4b120 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..7837693cc5ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..c22e13c91181 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..c348ec5dbfa2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..dd51555de4e7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..fc3833e74b6b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..2a2fb9a552b2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..75d3690a4689 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..6fc1c1503233 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..a9edb2205c79 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..af7100e38df7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..4299da538529 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..be43d167b61f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..c0d352a35f1f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..7f5e75dc597c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..e3e6c7a66a95 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..520328620b17 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..103d83fc4371 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..a8f1f493ce1e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..4164bb2ad46b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts @@ -0,0 +1,20 @@ +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('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'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..1214eeb2cc15 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..8dc4ba95b3a2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..bfe0bc554581 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts @@ -0,0 +1,20 @@ +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('Accessibility - Scheduler 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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..9b2dc2fa47ff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..5f8eb824991f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..e07f5dff0043 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..4377fcf5f9d3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..e84af758267d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..59391fd204b9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..765d6dba96fb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..c5b3e17bc194 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..189701a13525 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..8f2b385b3176 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..c04d41d235bf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..fb3ce0d08873 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..8a83b0665d82 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..a85bf0f35fbc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..a213b715fed3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..aecf068a3737 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..6070f2494634 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..69554a80dd33 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..94c9c178efb4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); 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..d840b65ebf48 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts @@ -0,0 +1,20 @@ +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('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'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..a1c7c972abbb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts @@ -0,0 +1,20 @@ +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('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.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); 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..0b00cb6d0e60 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts @@ -0,0 +1,35 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - ColumnSortable.Visual', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('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'), + }); + }); +}); 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..6fe54a8f41c9 --- /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 ( 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..34a904a52413 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts @@ -0,0 +1,63 @@ +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); + }, + 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); + }); +}); 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..0d5052b2bb79 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts @@ -0,0 +1,23 @@ +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('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'), + }); + }); +}); 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..e52d3b79aa11 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +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: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'title', caption: 'Task Title' }, + { dataField: 'status', caption: 'Status' }, + ], + 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('.dx-cardview-addcard-button').click(); + await page.waitForSelector('.dx-cardview-edit-popup'); + + const idInput = page.locator('.dx-cardview-edit-popup input[name="id"]'); + const titleInput = page.locator('.dx-cardview-edit-popup input[name="title"]'); + const statusInput = page.locator('.dx-cardview-edit-popup input[name="status"]'); + + await expect(idInput).toHaveValue('10'); + await expect(titleInput).toHaveValue('New Task'); + await expect(statusInput).toHaveValue('Not Started'); + }); +}); 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..02a06eb424df --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts @@ -0,0 +1,60 @@ +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('.dx-cardview-addcard-button').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..1a6e29eb15ea --- /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-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + 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-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + 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..347db4d11cac --- /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-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + const contentHeight = await page.locator('.dx-filterbuilder-popup .dx-overlay-content').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-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + const titleText = await page.locator('.dx-filterbuilder-popup .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..74cad2b46188 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts @@ -0,0 +1,59 @@ +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 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.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' }], + filterPanel: { visible: false }, + filterValue: ['title', '=', 'Mr.'], + }); + + const filterPanel = page.locator('.dx-cardview-filter-panel'); + await expect(filterPanel).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterPanel.visible', true); + }); + + await expect(filterPanel).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' }], + 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); + }); +}); 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..cbab595f2aac --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/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 - 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('FilterIcon opens popup by click', 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 }, + }); + + const popup = page.locator('.dx-filterbuilder-popup'); + await expect(popup).not.toBeVisible(); + + await page.locator('.dx-cardview-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: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + }); + + const popup = page.locator('.dx-filterbuilder-popup'); + await expect(popup).not.toBeVisible(); + + await page.locator('.dx-cardview-filter-panel .dx-cardview-filter-panel-text').click(); + await expect(popup).toBeVisible(); + }); + + test('ClearFilter button clears filter by click', 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 page.locator('.dx-cardview-filter-panel .dx-cardview-filter-panel-clear-filter').click(); + + const filterValue = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').option('filterValue'); + }); + expect(filterValue).toBeNull(); + }); +}); 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..d099f9858bd0 --- /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-cardview-filter-panel'), + }); + + await page.locator('.dx-cardview-filter-panel .dx-icon-filter').click(); + + await testScreenshot(page, 'cardView_FilterBuilderPopup.png', { + element: page.locator('.dx-filterbuilder-popup'), + }); + }); +}); 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..a5d545ccd815 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts @@ -0,0 +1,34 @@ +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'); + await expect(filterIcon).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('headerFilter.visible', true); + }); + + await expect(filterIcon.first()).toBeVisible(); + }); +}); 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..4285dd9a4c54 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts @@ -0,0 +1,33 @@ +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').first().click(); + + const popup = page.locator('.dx-header-filter-menu'); + await expect(popup).toBeVisible(); + }); +}); 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..c954e844e574 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts @@ -0,0 +1,35 @@ +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').first().click(); + await page.waitForSelector('.dx-header-filter-menu'); + + const listItems = page.locator('.dx-list-item'); + await expect(listItems).toHaveCount(3); + }); +}); 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..fb45be35e10d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts @@ -0,0 +1,20 @@ +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.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.skip('remote header filter should load grouped data', async ({ page }) => { + // TODO: Convert remote API mock (RequestMock) to Playwright route handling + }); +}); 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..30fd27808156 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.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.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').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').first().click(); + + await testScreenshot(page, 'card-view_header-filter_popup-with-search.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..2a882e760229 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts @@ -0,0 +1,20 @@ +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 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.skip('sortable indicator during dragging', async ({ page }) => { + // TODO: Convert drag-and-drop visual tests with MouseUpEvents helpers to Playwright + }); +}); 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..824ace83d479 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts @@ -0,0 +1,65 @@ +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 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('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-cardview-column-chooser-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..fb155fd4968a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/items.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 - 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'); + }); +}); 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..3d50cdfcfe0a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.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('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 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' }]); + }); +}); 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..f87c95239f6a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts @@ -0,0 +1,74 @@ +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 -> 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(); + await page.keyboard.press(keys); + + 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(); + }); +}); 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..a61eccfb11a8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts @@ -0,0 +1,46 @@ +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(); + }); +}); 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..cdaa859d433b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts @@ -0,0 +1,33 @@ +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 .dx-texteditor-input'); + await expect(searchInput).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..5274c7380496 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts @@ -0,0 +1,66 @@ +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-selection') + ); + expect(isSelected).toBe(result[i]); + } + }); + }); + + 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-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..0532a95405e4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts @@ -0,0 +1,42 @@ +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'); + }); +}); 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..04dcb269fc0e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts @@ -0,0 +1,23 @@ +import { test } 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'), + }); + }); +}); 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..8506b78ac03e --- /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(() => { + ( 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(() => { + ( as any).dxCardView('instance').option('filterValue', ['text', '=', '0']); + }); + + const pager = page.locator('.dx-pager'); + await expect(pager).not.toBeVisible(); + + await page.evaluate(() => { + ( 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..aa924de22ec1 --- /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 .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..35667de164af --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.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('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'); + 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 .dx-texteditor-input'); + await expect(input).toHaveValue('rt'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.text', ''); + }); + await expect(input).toHaveValue(''); + }); +}); 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..a9ff25093657 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts @@ -0,0 +1,39 @@ +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 .dx-texteditor-input'); + await input.fill('rt'); + await expect(cards).toHaveCount(2); + + await input.fill(''); + await expect(cards).toHaveCount(4); + }); +}); 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..513ca5aadd80 --- /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').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..e752532ccda3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts @@ -0,0 +1,96 @@ +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-selection/); + + await secondCard.click(); + await expect(firstCard).not.toHaveClass(/dx-selection/); + await expect(secondCard).toHaveClass(/dx-selection/); + + await secondCard.click({ modifiers: ['Control'] }); + await expect(secondCard).not.toHaveClass(/dx-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-selection/); + + await secondCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-selection/); + await expect(secondCard).toHaveClass(/dx-selection/); + + await firstCheckbox.click(); + await expect(firstCard).not.toHaveClass(/dx-selection/); + await expect(secondCard).toHaveClass(/dx-selection/); + + await secondCheckbox.click(); + await expect(secondCard).not.toHaveClass(/dx-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('.dx-cardview-select-all-button').click(); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([0, 1, 2, 3, 4]); + }); +}); 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..5d172d2b3c62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.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')}`; + +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 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 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..926fe55a0e20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.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 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('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..5cd401adf89d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts @@ -0,0 +1,79 @@ +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'); + }); + + 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..3ec1bcdbec42 --- /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('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..a156cca6dde4 --- /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('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/filterBuilderEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts new file mode 100644 index 000000000000..439e4a821be8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.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('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'); + }); + + // T1310528 + test('Change value editor to checkbox', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + onEditorPreparing: (data) => { + data.editorName = 'dxCheckBox'; + }, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemValue').element.click(); + + await testScreenshot(page, 'value-editor-checkbox.png', { element: filterBuilder.element }); + + }); + + // T1310528 + test('Change value editor to switch', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + onEditorPreparing: (data) => { + data.editorName = 'dxSwitch'; + }, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemValue').element.click(); + + await testScreenshot(page, 'value-editor-switch.png', { element: filterBuilder.element }); + + }); +}); 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..450369a0d49e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts @@ -0,0 +1,64 @@ +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'); + }); + + // T1253754 + test('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 filterBuilder = page.locator('#container'); + + const expectedValues = [ + [ + ['name1', '<>', 0], + 'and', + ['name1', 'contains', 'A'], + ], + [ + ['name1', '<>', 0], + 'and', + ['name2', 'contains', 'A'], + ], + ]; + await page.click(filterBuilder.getAddButton()) + .expect(FilterBuilder.getPopupTreeView().visible).ok() + .click(FilterBuilder.getPopupTreeViewNode(0)) + .click(filterBuilder.getField(1, 'itemValue').element) + .pressKey('A enter'); + + await page.expect(await filterBuilder.option('value')) + .eql(expectedValues[0]); + + await filterBuilder.getField(1, 'item').element.click() + .expect(FilterBuilder.getPopupTreeView().visible).ok() + .click(FilterBuilder.getPopupTreeViewNode(1)) + .click(filterBuilder.getField(1, 'itemValue').element) + .pressKey('A enter'); + + await page.expect(await filterBuilder.option('value')) + .eql(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..24841b085f0f --- /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('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..8cf1a2fa4c62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts @@ -0,0 +1,96 @@ +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('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('Field dropdown popup', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'item').element.click(); + + await testScreenshot(page, 'field-dropdown.png', { element: filterBuilder.element }); + + }); + + test('operation dropdown popup', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemOperation').element.click(); + + await testScreenshot(page, 'operation-dropdown.png', { element: filterBuilder.element }); + + }); + + // T1222027 + test('Dropdown Treeview should have no empty space', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemAction').element.click(); + + await testScreenshot(page, 'dropdown-space.png', { element: filterBuilder.element }); + + }); + + [ + { dataType: 'date', value: 1740441600000 }, + { dataType: 'date', value: '2025-02-25T00:00:00.000Z' }, + { dataType: 'date', value: new Date('2025-02-25T00:00:00.000Z') }, + { dataType: 'datetime', value: 1740441600000 }, + { dataType: 'datetime', value: '2025-02-25T00:00:00.000Z' }, + { dataType: 'datetime', value: new Date('2025-02-25T00:00:00.000Z') }, + ].forEach(({ dataType, value }) => { + test(`item value text should be correct for dataType: ${dataType} and valueType: ${typeof value}`, async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields: [ + { + dataField: 'field1', + dataType: dataType as DataType, + }, + ], + value: ['field1', '=', value], + }); + + + const filterBuilder = page.locator('#container'); + + 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}`; + + await expect(filterBuilder.getField(0).getValueText().textContent).eql(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..e682a90300e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts @@ -0,0 +1,158 @@ +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 = `${new Guid()}`; + 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.click(Selector(TOOLBAR_ITEM_BUTTON)); + await testScreenshot(page, 'Gantt show resourced.png', { element: '#container' }); + + }); + + test('Gantt - show dependencies button should not have focus state (T1264485)', async ({ page }) => { + + const id = `${new Guid()}`; + 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.click(Selector(TOOLBAR_ITEM_BUTTON)); + 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..e73fb573f188 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts @@ -0,0 +1,428 @@ +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', + map: '\f035', + menu: '\f00c', + message: '\f024', + money: '\f036', + music: '\f037', + overflow: '\f00d', + percent: '\f039', + photo: '\f03a', + plus: '\f00b', + minus: '\f074', + preferences: '\f03b', + product: '\f03c', + pulldown: '\f062', + refresh: '\f03d', + remove: '\f00a', + revert: '\f04c', + runner: '\f040', + save: '\f041', + search: '\f027', + tags: '\f009', + tel: '\f003', + tips: '\f004', + todo: '\f005', + toolbox: '\f007', + trash: '\f03e', + user: '\f02d', + upload: '\f006', + floppy: '\f073', + arrowleft: '\f011', + arrowdown: '\f015', + arrowright: '\f00e', + arrowup: '\f013', + spinleft: '\f04f', + spinprev: '\f04f', + spinright: '\f04e', + spinnext: '\f04e', + spindown: '\f001', + spinup: '\f002', + chevronleft: '\f012', + chevronprev: '\f012', + back: '\f012', + chevronright: '\f010', + chevronnext: '\f010', + chevrondown: '\f016', + chevronup: '\f014', + chevrondoubleleft: '\f042', + chevrondoubleright: '\f03f', + equal: '\f044', + notequal: '\f045', + less: '\f046', + greater: '\f047', + lessorequal: '\f048', + greaterorequal: '\f049', + isblank: '\f075', + isnotblank: '\f076', + sortup: '\f051', + sortdown: '\f052', + sortuptext: '\f053', + sortdowntext: '\f054', + sorted: '\f055', + expand: '\f04a', + collapse: '\f04b', + columnfield: '\f057', + rowfield: '\f058', + datafield: '\f101', + fields: '\f059', + fieldchooser: '\f05a', + columnchooser: '\f04d', + pin: '\f05b', + unpin: '\f05c', + pinleft: '\f05d', + pinright: '\f05e', + contains: '\f063', + startswith: '\f064', + endswith: '\f065', + doesnotcontain: '\f066', + range: '\f06a', + export: '\f05f', + exportxlsx: '\f060', + exportpdf: '\f061', + exportselected: '\f06d', + ordersbox: '\f06e', + warning: '\f06b', + taskhelpneeded: '\f06f', + more: '\f06c', + square: '\f067', + clearsquare: '\f068', + repeat: '\f069', + selectall: '\f070', + unselectall: '\f071', + print: '\f072', + bold: '\f077', + italic: '\f078', + underline: '\f079', + strike: '\f07a', + indent: '\f07b', + increaselinespacing: '\f07b', + font: '\f11b', + fontsize: '\f07c', + shrinkfont: '\f07d', + growfont: '\f07e', + color: '\f07f', + background: '\f080', + fill: '\f10d', + palette: '\f120', + superscript: '\f081', + subscript: '\f082', + header: '\f083', + blockquote: '\f084', + formula: '\f056', + codeblock: '\f085', + orderedlist: '\f086', + bulletlist: '\f087', + increaseindent: '\f088', + decreaseindent: '\f089', + decreaselinespacing: '\f106', + alignleft: '\f08a', + alignright: '\f08b', + aligncenter: '\f08c', + alignjustify: '\f08d', + link: '\f08e', + video: '\f08f', + mention: '\f090', + variable: '\f091', + clearformat: '\f092', + accountbox: '\f094', + fullscreen: '\f11a', + hierarchy: '\f124', + docfile: '\f111', + docxfile: '\f110', + pdffile: '\f118', + pptfile: '\f114', + pptxfile: '\f115', + rtffile: '\f112', + txtfile: '\f113', + xlsfile: '\f116', + xlsxfile: '\f117', + copy: '\f107', + cut: '\f10a', + paste: '\f108', + share: '\f11f', + inactivefolder: '\f105', + newfolder: '\f123', + movetofolder: '\f121', + parentfolder: '\f122', + rename: '\f109', + detailslayout: '\f10b', + contentlayout: '\f11e', + smalliconslayout: '\f119', + mediumiconslayout: '\f10c', + undo: '\f04c', + redo: '\f093', + hidepanel: '\f11c', + showpanel: '\f11d', + checklist: '\f141', + verticalaligntop: '\f14f', + verticalaligncenter: '\f14e', + verticalalignbottom: '\f14d', + rowproperties: '\f14c', + columnproperties: '\f14b', + cellproperties: '\f14a', + tableproperties: '\f140', + splitcells: '\f139', + mergecells: '\f138', + deleterow: '\f137', + deletecolumn: '\f136', + insertrowabove: '\f135', + insertrowbelow: '\f134', + insertcolumnleft: '\f133', + insertcolumnright: '\f132', + inserttable: '\f130', + deletetable: '\f131', + edittableheader: '\f142', + addtableheader: '\f143', + pasteplaintext: '\f144', + importselected: '\f145', + import: '\f146', + textdocument: '\f147', + jpgfile: '\f148', + bmpfile: '\f149', + svgfile: '\f150', + attach: '\f151', + return: '\f152', + indeterminatestate: '\f153', + lock: '\f154', + unlock: '\f155', + imgarlock: '\f156', + imgarunlock: '\f157', + bell: '\f158', + sun: '\f159', + arrowback: '\f15a', + taskcomplete: '\f15b', + taskrejected: '\f15c', + taskinprogress: '\f15d', + taskstop: '\f15e', + clearcircle: '\f15f', + send: '\f160', + pinmap: '\f179', + photooutline: '\f162', + panelright: '\f163', + panelleft: '\f164', + optionsgear: '\f165', + moon: '\f166', + login: '\f167', + eyeopen: '\f168', + eyeclose: '\f169', + expandform: '\f170', + description: '\f171', + belloutline: '\f172', + to: '\f173', + errorcircle: '\f174', + datatrending: '\f175', + dataarea: '\f176', + datausage: '\f177', + datapie: '\f178', + handlevertical: '\f161', + handlehorizontal: '\f16a', + triangleup: '\f16b', + triangledown: '\f16c', + triangleright: '\f16d', + triangleleft: '\f16e', + sendfilled: '\f09a', + chat: '\f17e', + fixcolumn: '\f16f', + unfixcolumn: '\f17a', + fixcolumnleft: '\f17b', + stickcolumn: '\f17c', + fixcolumnright: '\f17d', + ratingoutline: '\f17f', + ratingfilled: '\f180', + csv: '\f181', + packagebox: '\f182', + checkmarkcircle: '\f183', + clipboardtasklist: '\f184', + today: '\f185', + daterangepicker: '\f186', + cardcontent: '\f187', + cursormove: '\f188', + cursorprohibition: '\f189', + arrowsortup: '\f190', + arrowsortdown: '\f191', + imagethumbnail: '\f192', + sparkle: '\f193', + servicebell: '\f194', + dropzone: '\f195', + restore: '\f196', + groupbycolumn: '\f197', + ungroupcolumn: '\f198', + ungroupallcolumns: '\f199', + chatadd: '\f200', + colordismiss: '\f201', + clipboardpastesparkle: '\f202', + micoutline: '\f203', + micfilled: '\f204', + stopoutline: '\f205', + stopfilled: '\f206', + optionsoutline: '\f207', + optionsfilled: '\f208', + conferenceroomoutline: '\f209', + conferenceroomfilled: '\f210', + chatsparkleoutline: '\f211', + chatsparklefilled: '\f212', + addcircleoutline: '\f213', + addcirclefilled: '\f214', + zoomoutoutline: '\f215', + zoomoutfilled: '\f216', + zoominoutline: '\f217', + zoominfilled: '\f218', + calendardatestartoutline: '\f219', + calendardatestartfilled: '\f220', + calendardateendoutline: '\f221', + calendardateendfilled: '\f222', + datalineoutline: '\f223', + datalinefilled: '\f224', + dataareaoutline: '\f225', + dataareafilled: '\f226', + databaroutline: '\f227', + databarfilled: '\f228', + datastackedbaroutline: '\f229', + datastackedbarfilled: '\f230', + datapieoutline: '\f231', + datapiefilled: '\f232', + datadoughnutoutline: '\f233', + datadoughnutfilled: '\f234', + checkmarkcirclefilled: '\f235', + botoutline: '\f236', + botfilled: '\f237', + copyfilled: '\f238', + }; + + test('Icon set', async ({ page }) => { + + const icons = Object.entries(iconSet).map(([iconName, glyph]) => ({ + id: `dx-${new Guid()}`, + iconName, + glyph, + })); + + await ClientFunction((icons, ICON_CLASS) => { + const container = document.querySelector('#container'); + if (!container) return; + + const fragment = document.createDocumentFragment(); + + for (const { id, iconName, glyph } of icons) { + const element = document.createElement('div'); + element.id = id; + Object.assign(element.style, { + display: 'inline-flex', + padding: '3px', + border: '1px solid black', + alignItems: 'center', + flexDirection: 'column', + fontSize: '10px', + }); + + const iconDiv = document.createElement('div'); + iconDiv.classList.add(ICON_CLASS, `${ICON_CLASS}-${iconName}`); + + const nameDiv = document.createElement('div'); + nameDiv.textContent = iconName; + + const glyphDiv = document.createElement('div'); + glyphDiv.textContent = glyph.replace('\f', '\\f'); + + element.append(iconDiv, nameDiv, glyphDiv); + fragment.append(element); + } + + container.append(fragment); + }, { dependencies: { iconSet, ICON_CLASS } })(icons, ICON_CLASS); + + await testScreenshot(page, 'Icon set.png'); + + }); + + test('SVG icon set', async ({ page }) => { + + const themeName = (process.env.theme ?? 'fluent.blue.light'); + const icons = Object.keys(iconSet).map((iconName) => ({ + id: `dx-${new Guid()}`, + iconName, + src: `../../../packages/devextreme-scss/images/icons/${themeName}/${iconName}.svg`, + })); + + await ClientFunction((icons, appendElementTo) => { + for (const { id, iconName, src } of icons) { + appendElementTo(page, '#container', 'div', id, { + display: 'inline-flex', + padding: '3px', + border: '1px solid black', + alignItems: 'center', + flexDirection: 'column', + fontSize: '10px', + }); + + const el = document.getElementById(id); + if (el) { + const img = document.createElement('img'); + img.src = src; + + const nameDiv = document.createElement('div'); + nameDiv.textContent = iconName; + + el.append(img, nameDiv); + } + } + }, { dependencies: { appendElementTo } })(icons, appendElementTo); + + 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..154fdc7afb9d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.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('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..20ddaefbd828 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts @@ -0,0 +1,156 @@ +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'); + await page.expect(pagination.element.getStyleProperty('width')) + .eql('270px') + .expect(pagination.element.getStyleProperty('height')) + .eql('95px') + .expect(pagination.element.getAttribute('width')) + .eql(null) + .expect(pagination.element.getAttribute('height')) + .eql(null); + + }); + + test('Pagination elementAttr property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + elementAttr: { + 'aria-label': 'some description', + 'data-test': 'custom data', + }, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getAttribute('aria-label')) + .eql('some description') + .expect(pagination.element.getAttribute('data-test')) + .eql('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'); + await page.expect(pagination.element.getAttribute('accesskey')) + .eql('F') + .expect(pagination.element.getAttribute('title')) + .eql('Best Pagination'); + + }); + + test('Pagination disabled property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + disabled: true, + itemCount: 50, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getAttribute('aria-disabled')) + .eql('true') + .expect(pagination.element.hasClass('dx-state-disabled')) + .ok(); + + }); + + 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'); + await page.expect(pagination.element.getAttribute('tabindex')) + .eql('7') + + .click(pagination.getNavPage('3').element) + .expect(pagination.element.hasClass('dx-state-focused')) + .ok() + .expect(pagination.element.hasClass('dx-state-hover')) + .ok() + .expect(pagination.element.hasClass('dx-state-active')) + .notOk(); + + await ClientFunction((_pagination) => { + const $root = $(_pagination.element()); + + $root.trigger($.Event('dxpointerdown', { + pointers: [{ pointerId: 1 }], + })); + })(pagination); + + await page.expect(pagination.element.hasClass('dx-state-active')) + .ok(); + + }); + + 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'); + await page.expect(pagination.element.getAttribute('accesskey')) + .eql(null) + .expect(pagination.getPageSize(0).element.focused) + .notOk(); + + await page.evaluate(() => { + _pagination.getInstance().focus(); + }); + + await page.expect(pagination.getPageSize(0).element.focused) + .ok(); + + }); + + test('Pagination focus method with focusStateEnabled', async ({ page }) => { + await createWidget(page, 'dxPagination', { + focusStateEnabled: true, + itemCount: 50, + }); + + const pagination = page.locator('#container'); + expect(pagination.element.focused).toBeFalsy(); + + await page.evaluate(() => { + _pagination.getInstance().focus(); + }); + + expect(pagination.element.focused).toBeTruthy(); + + }); +}); 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..3a2d53cf0747 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts @@ -0,0 +1,78 @@ +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'); + await page.expect(pagination.element.hasClass('dx-state-invisible')) + .ok(); + + }); + + test('PageSize selector test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 2, + pageSize: 8, // pageCount: 7 + allowedPageSizes: [2, 4, 8], + }); + + const pagination = page.locator('#container'); + + await pagination.getPageSize(1).element.click() + .expect(pagination.option('pageCount')) + .eql(13); + + }); + + test('PageIndex test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 1, + pageSize: 5, // pageCount: 10 + }); + + const pagination = page.locator('#container'); + + await page.expect(pagination.option('pageIndex')) + .eql(1) + .click(pagination.getNavPage('5').element) + .expect(pagination.option('pageIndex')) + .eql(5); + + }); + + test('PageIndex correction test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 10, + pageSize: 5, // pageCount: 10 + }); + + const pagination = page.locator('#container'); + + await page.expect(pagination.option('pageIndex')) + .eql(10) + .click(pagination.getPageSize(1).element) + .expect(pagination.option('pageIndex')) + .eql(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..56c5feb06528 --- /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 rightClick(page.locator(`.${FIELD_CHOOSER_AREA_FIELDS_CLASS}`).nth(1)); + + 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..ae67e4301276 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.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('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'); + }); + + 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; + }, + }); + + const pivotGrid = page.locator('#container'); + const exportBtn = pivotGrid.getExportButton(); + + await click(exportBtn); + const exportCalled = await ClientFunction(() => (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..11319feff91a --- /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('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..8c955ddcb73b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts @@ -0,0 +1,600 @@ +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_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'); + }); + + test('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 = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(0).element.click(); + await fieldChooserTreeView.getCheckBoxByNodeIndex(1).element.click(); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).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 + 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' }); + + }); + + test('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', + 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 Hidden', + 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 = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(0).element.click(); + await fieldChooserTreeView.getCheckBoxByNodeIndex(1).element.click(); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).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 + 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' }); + + }); + + test('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, + }, + onInitialized(e) { + function expand(dataSource) { + setTimeout(() => { + dataSource.expandHeaderItem('row', ['North America']); + dataSource.expandHeaderItem('column', [2013]); + }, 0); + } + + expand(e.component.getDataSource()); + }, + 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 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 = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(0).element.click(); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).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 + 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' }); + + }); + + test('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', + }, + onInitialized(e) { + function expand(dataSource) { + setTimeout(() => { + dataSource.expandHeaderItem('row', ['North America']); + dataSource.expandHeaderItem('column', [2013]); + }, 0); + } + + expand(e.component.getDataSource()); + }, + 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 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, + }, { + caption: 'Total Hidden 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 6', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + const dataFields = fieldChooser.getDataFields(); + + expect(dataFields.count).toBe(3) + .expect(dataFields.nth(0).textContent) + .eql('Total') + .expect(dataFields.nth(1).textContent) + .eql('Total 2') + .expect(dataFields.nth(2).textContent) + .eql('Total 3'); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(1).element.click(); + + expect(dataFields.count).toBe(4) + .expect(dataFields.nth(0).textContent) + .eql('Total') + .expect(dataFields.nth(1).textContent) + .eql('Total 2') + .expect(dataFields.nth(2).textContent) + .eql('Total 3') + .expect(dataFields.nth(3).textContent) + .eql('Total 5'); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).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 + 150, { steps: 10 }); + await page.mouse.up(); + } + })(); + + expect(dataFields.count).toBe(4) + .expect(dataFields.nth(0).textContent) + .eql('Total 2') + .expect(dataFields.nth(1).textContent) + .eql('Total 3') + .expect(dataFields.nth(2).textContent) + .eql('Total 5') + .expect(dataFields.nth(3).textContent) + .eql('Total'); + + }); + + 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 = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + await page.expect(pivotGrid.getFieldChooser().element.exists) + .ok(); + + }); +}); 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..811400df2201 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts @@ -0,0 +1,63 @@ +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_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'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + [true, false].forEach((showRowGrandTotals) => { + test(`Empty table has one ${showRowGrandTotals ? 'total' : 'empty'} row after drag-n-drop for paginated data`, async ({ page }) => { + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const loadPanel = pivotGrid.getLoadPanel(); + + await expect(loadPanel.isInvisible()).ok(); + await pivotGrid.scrollTo({ top: 5000 }); + await page.waitForTimeout(1000) + .expect(loadPanel.isInvisible()).ok(); + await dragToElement( + pivotGrid.getRowHeaderArea().getField(), + pivotGrid.getColumnHeaderArea().element, + + await expect(loadPanel.isInvisible()).ok(); + + await testScreenshot(page, + `empty_table_after_dnd (showRowGrandTotals=${showRowGrandTotals}).png`, + { element: pivotGrid.element }, + + });.before(async ({ page }) => { + await addRequestHooks(OLAPApiMock); + await createWidget(page, 'dxPivotGrid', { + height: 500, + fieldPanel: { visible: true }, + showRowGrandTotals, + scrolling: { mode: 'virtual', useNative: false }, + dataSource: { + paginate: true, + fields: [ + { dataField: '[Customer].[Customer]', area: 'row' }, + { dataField: '[Ship Date].[Calendar Year]', area: 'column' }, + { dataField: '[Measures].[Internet Sales Amount]', area: 'data' }, + ], + store: { + type: 'xmla', + url: 'https://api/data', + catalog: 'Catalog', + cube: 'Cube', + }, + }, + }); + }); + }); +}); 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..d6104595fac0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts @@ -0,0 +1,64 @@ +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_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'; + + test('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(PIVOT_GRID_SELECTOR); + const rowHeader = pivotGrid.getRowHeaderArea(); + const columnHeader = pivotGrid.getColumnHeaderArea(); + const filterHeader = pivotGrid.getFilterHeaderArea(); + + await page.expect(rowHeader.getHeaderFilterIcon(0).ariaLabel) + .eql('Show filter options for column \'Row1\'') + .expect(rowHeader.getHeaderFilterIcon(1).ariaLabel) + .eql('Show filter options for column \'Row2\'') + .expect(columnHeader.getHeaderFilterIcon(0).ariaLabel) + .eql('Show filter options for column \'Column1\'') + .expect(columnHeader.getHeaderFilterIcon(1).ariaLabel) + .eql('Show filter options for column \'Column2\'') + .expect(filterHeader.getHeaderFilterIcon(0).ariaLabel) + .eql('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..0d40c0a8ff98 --- /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('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..9ff804111d04 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts @@ -0,0 +1,100 @@ +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_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 PIVOT_GRID_SELECTOR = '#container'; + + test('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(PIVOT_GRID_SELECTOR); + + await pivotGrid.getColumnHeaderArea().getHeaderFilterIcon().element.click(); + + await testScreenshot(page, 'headerFilter - before scroll.png'); + + await scroll(pivotGrid.getColumnHeaderArea().getHeaderFilterScrollable(), 0, 10); + + await testScreenshot(page, 'headerFilter - after scroll.png'); + + }); + + test('[T1284200] Should handle dxList "selectAll" when has unselected items on the second page', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [ + { + dataField: 'id', + area: 'column', + filterType: 'exclude', + filterValues: [70], + }, + ], + store: new Array(100).fill(null).map((_, idx) => ({ + id: idx, + })), + }, + allowSorting: true, + allowFiltering: true, + fieldPanel: { + visible: true, + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + const filterIconElement = pivotGrid.getColumnHeaderArea().getHeaderFilterIcon().element; + const headerFilter = new HeaderFilter(); + const list = headerFilter.getList(); + + await page.click(filterIconElement) + .click(list.selectAll.checkBox.element); + + expect(list.selectAll.checkBox.isChecked).toBeTruthy(); + + await list.selectAll.checkBox.element.click(); + + expect(list.selectAll.checkBox.isChecked).toBeFalsy(); + + }); +}); 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..acfac5d556c4 --- /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 } 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 pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + await testScreenshot(page, 'running-total_seamless-data.png', { element: pivotGrid.element }); + + }); + + 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 pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + await testScreenshot(page, 'running-total_partial-data_render-0.png', { element: pivotGrid.element }); + + const rowToCollapse = pivotGrid.getRowsArea().getCell(3); + await click(rowToCollapse); + + await testScreenshot(page, 'running-total_partial-data_render-1.png', { element: pivotGrid.element }); + + }); +}); 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..5cbf66ac6391 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts @@ -0,0 +1,227 @@ +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('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)', 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..186d2a0ab976 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts @@ -0,0 +1,193 @@ +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_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 }) => { + const requestLogger = RequestLogger(/\/api\/data/); + const pivotGrid = page.locator('#container'); + + await addRequestHooks(requestLogger); + + await page.waitForTimeout(500); + + requestLogger.clear(); + const initialRequestCount = await requestLogger.count(() => true); + + await click(pivotGrid.getColumnHeaderArea().getField()); + + await page.waitForTimeout(500); + + const afterSortRequestCount = await requestLogger.count(() => true); + const requestCount = afterSortRequestCount - initialRequestCount; + + expect(requestCount).toBe(0); + + await removeRequestHooks(requestLogger); + + });.before(async ({ page }) => { + const apiRequestMock = RequestMock() + .onRequestTo(/\/api\/data\?skip/) + .respond( + { + data: [ + { id: 0, label: 'A', value: 10 }, + { id: 1, label: 'B', value: 20 }, + { id: 2, label: 'C', value: 30 }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?group/) + .respond( + { + data: [ + { + key: 'A', + items: null, + summary: [10], + }, + { + key: 'B', + items: null, + summary: [20], + }, + { + key: 'C', + items: null, + summary: [30], + }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + + (t.ctx as any).apiRequestMock = apiRequestMock; + await addRequestHooks(apiRequestMock); + + await createWidget(page, 'dxPivotGrid', () => ({ + allowSorting: true, + fieldPanel: { visible: true }, + dataSource: { + remoteOperations: true, + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + fields: [ + { + dataField: 'label', + area: 'column', + }, + { + dataField: 'value', + dataType: 'number', + area: 'data', + }, + ], + }, + })); + }).after(async ({ page }) => { + await removeRequestHooks((t.ctx as any).apiRequestMock); + }); + + test('Should sort with DataSource reload if scrolling mode is virtual', async ({ page }) => { + const requestLogger = RequestLogger(/\/api\/data/); + const pivotGrid = page.locator('#container'); + + await addRequestHooks(requestLogger); + + await page.waitForTimeout(500); + + requestLogger.clear(); + const initialRequestCount = await requestLogger.count(() => true); + + await click(pivotGrid.getColumnHeaderArea().getField()); + + const afterSortRequestCount = await requestLogger.count(() => true); + const requestCount = afterSortRequestCount - initialRequestCount; + + expect(requestCount).toBe(1); + + await removeRequestHooks(requestLogger); + + });.before(async ({ page }) => { + const apiRequestMock = RequestMock() + .onRequestTo(/\/api\/data\?skip/) + .respond( + { + data: [ + { id: 0, label: 'A', value: 10 }, + { id: 1, label: 'B', value: 20 }, + { id: 2, label: 'C', value: 30 }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?group/) + .respond( + { + data: [ + { + key: 'A', + items: null, + summary: [10], + }, + { + key: 'B', + items: null, + summary: [20], + }, + { + key: 'C', + items: null, + summary: [30], + }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + + (t.ctx as any).apiRequestMock = apiRequestMock; + await addRequestHooks(apiRequestMock); + + await createWidget(page, 'dxPivotGrid', () => ({ + allowSorting: true, + fieldPanel: { visible: true }, + scrolling: { mode: 'virtual' }, + dataSource: { + remoteOperations: true, + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + fields: [ + { + dataField: 'label', + area: 'column', + }, + { + dataField: 'value', + dataType: 'number', + area: 'data', + }, + ], + }, + })); + }).after(async ({ page }) => { + await removeRequestHooks((t.ctx as any).apiRequestMock); + }); +}); 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..4e44035330bd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts @@ -0,0 +1,119 @@ +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_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 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 = page.locator('#container'); + + await testScreenshot(page, + 'T1173442_before_sort_with_summary_display_mode.png', + { element: pivotGrid.element }, + + await click(pivotGrid.getColumnHeaderArea().getField()); + await click(pivotGrid.getRowHeaderArea().getField()); + await testScreenshot(page, + 'T1173442_after_sort_with_summary_display_mode.png', + { element: pivotGrid.element }, + + }); +}); 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..2f74abcf3118 --- /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('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..7e3ef41d030f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts @@ -0,0 +1,95 @@ +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 = (copyStylesToShadowDom) => ClientFunction((copyStyles) => { + 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 = dxWidgetHostStyles; + document.head.appendChild(hostStyleElement); + + const shadowRoot = container.attachShadow({ mode: 'open' }); + + const shadowStyleElement = shadowRoot.ownerDocument.createElement('style'); + shadowStyleElement.innerHTML = dxWidgetShadowStyles; + 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', + }); + }, { + dependencies: { + dxWidgetHostStyles, + dxWidgetShadowStyles, + }, + })(copyStylesToShadowDom); + + const getAdoptedStyleSheets = async () => 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(true); + + const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets(); + + const hasHostStyle = firstSheetRules?.some((rule) => rule === dxWidgetHostStyles); + await expect(hasHostStyle).ok('First adopted stylesheet contains host styles'); + + const hasShadowStyle = secondSheetRules?.some((rule) => rule === dxWidgetShadowStyles); + await expect(hasShadowStyle).ok('Second adopted stylesheet contains shadow styles'); + + }); + + test('Does not copy DX css styles when copyStylesToShadowDom is disabled', async ({ page }) => { + await setupShadowDomTest(false); + + const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets(); + await expect(firstSheetRules === null && secondSheetRules === null) + .ok('No adopted stylesheets should be created when copyStylesToShadowDom is disabled'); + + }); +}); 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..005acdd00708 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts @@ -0,0 +1,83 @@ +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('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) => { + test(`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); + }, + }, + }, + }, + }); + + // arrange + const treeList = page.locator('#container'); + + await page.expect(treeList.getDataCell(0, 0).element.textContent) + .contains('item_'); + + // act + await treeList.apiNavigateToRow('item_80_50'); + + // assert + await page.expect(treeList.getDataCell(131, 0).element.textContent) + .contains('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..c3611b3bdedd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts @@ -0,0 +1,71 @@ +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'); + }); + + test('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 treeList = page.locator('#container'); + await treeList.isReady(); + + const adaptiveButton = treeList.getAdaptiveButton(); + expect(adaptiveButton.exists).toBeTruthy(); + await click(adaptiveButton); + + await expect(treeList.getAdaptiveRow(0).element.exists).ok(); + + await resizeWindow(1200, 400); + + await expect(treeList.isAdaptiveColumnHidden()).ok(); + await expect(treeList.getAdaptiveRow(0).element.exists).notOk(); + + }); +}); 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..2b4818e89fa5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts @@ -0,0 +1,515 @@ +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 (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 TREE_LIST_SELECTOR = '#container'; + 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(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('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(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + await expect(treeList.getDataCell(0, 4).element.innerText).eql('Response Name 1 for second AI column'); + await expect(treeList.getDataCell(1, 4).element.innerText).eql('Response Name 2 for second AI column'); + await expect(treeList.getDataCell(2, 4).element.innerText).eql('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(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_REGENERATE_INDEX).element.click(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('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(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_PROMPT_EDITOR_INDEX).element.click(); + + const promptEditor = treeList.getAIPromptEditor(); + + await promptEditor.getRegenerateButton().element.click(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('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 => {}, + }; + }, + }), + }, + }, + ], + })); + + // arrange, act + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + // assert + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_CLEAR_DATA_INDEX).element.click(); + + // assert + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(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(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_PROMPT_EDITOR_INDEX).element.click(); + + const promptEditor = treeList.getAIPromptEditor(); + + await promptEditor.getRegenerateButton().element.click() + .click(promptEditor.getStopButton().element); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(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(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_PROMPT_EDITOR_INDEX).element.click(); + + const promptEditor = treeList.getAIPromptEditor(); + + await promptEditor.getTextArea().element.fill('changed prompt') + .click(promptEditor.getApplyButton().element); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for changed prompt'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for changed prompt'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('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..d466c5a4114e --- /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('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..a8bf3fe2a0eb --- /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 treeList = page.locator('#container'); + + await testScreenshot(page, 'T1054312', { element: treeList.getHeaders().element }); + + }); + + // 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 treeList = page.locator('#container'); + + await testScreenshot(page, 'T1053931', { element: treeList.getHeaders().element }); + + }); +}); 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..940a3348dfe0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.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('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'); + }); + + // T1247158 + 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) { + 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 treeList = page.locator('#container'); + const expectedInsertedRowIndex = 2; + + await treeList.getDataCell(1, 0).element.click() + .pressKey('ctrl+enter') + .expect(treeList.getDataRow(expectedInsertedRowIndex).isInserted) + .ok(); + + }); +}); 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..a69613ae909c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts @@ -0,0 +1,79 @@ +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('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'; + + // T1294363 + 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: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await treeList.apiFocus(); + + await page.expect(treeList.getDataCell(0, 0).element.focused) + .ok(); + + }); + + // T1294363 + 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: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await treeList.apiFocus(); + + await page.expect(treeList.getDataRow(0).element.focused) + .ok(); + + }); +}); 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..fe15b1b9a5bd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts @@ -0,0 +1,119 @@ +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('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 clearLocalStorage = async () => page.evaluate(() => { + (window as any).localStorage.removeItem('mystate'); + }); + + 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, + }); + + test('Focused row should be shown after reloading the page (T1058983)', async ({ page }) => { + + await clearLocalStorage(); + await createWidget(page, 'dxTreeList', getTreeListConfig()); + + const treeList = page.locator('#container'); + + await page.waitForTimeout(1000); + let scrollTopPosition = await treeList.getScrollTop(); + + // assert + await page.expect(treeList.isFocusedRowInViewport()) + .ok(); + + // act + await treeList.scrollTo(t, { top: 0 }); + scrollTopPosition = await treeList.getScrollTop(); + + // assert + expect(scrollTopPosition).toBe(0); + + await page.eval(() => location.reload()); + await createWidget(page, 'dxTreeList', getTreeListConfig()); + await page.waitForTimeout(1000); + + scrollTopPosition = await treeList.getScrollTop(); + + // assert + await page.expect(treeList.isFocusedRowInViewport()) + .ok(); + + }); + + test('TreeList - Unable to focus a node when deleting the previous node in certain scenarios (T1178893)', async ({ page }) => { + + await clearLocalStorage(); + const config = getTreeListConfig(); + config.editing = { + mode: 'row', + allowUpdating: true, + allowAdding: true, + allowDeleting: true, + }; + config.focusedRowKey = 3; + + await createWidget(page, 'dxTreeList', config); + + const treeList = page.locator('#container'); + + await page.expect(treeList.getFocusedRow().getAttribute('aria-rowindex')) + .eql('3') + + .click(treeList.getDataRow(2).getCommandCell(2).getButton(2)) + .click(treeList.getConfirmDeletionButton()) + .expect(treeList.getFocusedRow().getAttribute('aria-rowindex')) + .eql('3') + + .click(treeList.getDataRow(2).getCommandCell(2).getButton(2)) + .click(treeList.getConfirmDeletionButton()) + .expect(treeList.getFocusedRow().getAttribute('aria-rowindex')) + .eql('3') + .expect(treeList.getDataRow(2).getDataCell(0).element.textContent) + .eql('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..a8a9f3c48e70 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts @@ -0,0 +1,146 @@ +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('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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - custom buttons` + .page(url(__dirname, '../../../container.html')); + + const TREE_LIST_SELECTOR = '#container'; + const createTreeList = async () => 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(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 0); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('Custom buttons cell should be focused after custom buttons on shift+tab reverse navigation', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 0); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .pressKey('shift+tab') + .pressKey('shift+tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('First custom button inside custom buttons cell should be focused on tab navigation', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.getIconByTitle('button_1'); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .pressKey('tab') + .expect(expectedFocusedButton.focused) + .ok(); + + }); + + test('Last custom button inside custom buttons cell should be focused on shift+tab reverse navigation', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.getIconByTitle('button_2'); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .expect(expectedFocusedButton.focused) + .ok(); + + }); + + test('Custom button inside custom buttons cell should be clickable by pressing enter key', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.getIconByTitle('button_1'); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .pressKey('tab') + .pressKey('enter') + .expect(expectedFocusedButton.withAttribute('has-been-clicked').exists) + .ok(); + + }); +}); 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..9ad26fad6606 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts @@ -0,0 +1,152 @@ +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('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'); + }); + + test('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 = page.locator('#container'); + const focusButton = page.locator('#otherContainer'); + const expectedFocusedCell = treeList.getDataCell(0, 2); + + await focusButton.click() + .pressKey('tab tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('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 = page.locator('#container'); + const focusButton = page.locator('#otherContainer'); + const expectedFocusedCell = treeList.getDataCell(0, 2); + + await focusButton.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('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 = page.locator('#container'); + const target = treeList.getDataRow(0).getDataCell(0); + + await page.click(treeList.getDataRow(0).getDataCell(0).element.child(0)) + .click(treeList.getContainer(), { offsetX: 100, offsetY: 600 }) + .pressKey('tab tab tab') + .expect(target.element.focused) + .ok(); + + }); +}); 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..8172dd1d2cda --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts @@ -0,0 +1,125 @@ +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('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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - screenshots` + .page(url(__dirname, '../../../container.html')); + + 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(TREE_LIST_SELECTOR); + const headerCellToFocus = treeList.getHeaders().getHeaderRow(0).getHeaderCell(0); + const dataCellToFocus = treeList.getDataCell(0, 0); + + await headerCellToFocus.click() + .pressKey('tab'); + await testScreenshot(page, 'tree-list_keyboard-navigation-header-cell-focused.png', { element: treeList.element }); + + await dataCellToFocus.click() + .pressKey('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(TREE_LIST_SELECTOR); + const headerCellToFocus = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await headerCellToFocus.click() + .pressKey('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..f6b9549bcc55 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts @@ -0,0 +1,65 @@ +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('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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - focus on click` + .page(url(__dirname, '../../../container.html')); + + // T861048 + test('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 = page.locator('#container'); + const dataRow = treeList.getDataRow(3); + + await page.click(dataRow.getSelectCheckBox(), { offsetX: 0, offsetY: 0 }) + .expect(dataRow.isSelected).ok(); + + }); +}); 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..ee71e66879e9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts @@ -0,0 +1,150 @@ +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('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'); + }); + + // T1147695 + fixture + .disablePageReloads`Keyboard Navigation - skip drag cell` + .page(url(__dirname, '../../../container.html')); + + 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 () => createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: ['id', 'columnA', 'columnB'], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + }); + + const createTreeListRenderAsyncWithButtons = async () => 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(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + 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(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(4); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('The drag cell should be skipped when navigating to the header cell by shift+tab keypress', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .expect(expectedFocusedCell.isFocused).ok(); + + }); + + test('The drag cell should be skipped when navigating to a next row by tab keypress', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(1, 1); + const cellToStartNavigation = treeList.getDataCell(0, 3); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused).ok(); + + }); + + test('The drag cell should be skipped when navigating to a previous row by shift+tab keypress', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 3); + const cellToStartNavigation = treeList.getDataCell(1, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .expect(expectedFocusedCell.isFocused).ok(); + + }); + + test('The drag cell shouldn\'t be focused when the next cell is focused and the left arrow key pressed', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + + await expectedFocusedCell.click() + .pressKey('left') + .expect(expectedFocusedCell.isFocused).ok(); + + }); +}); 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..a18c870f19d4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts @@ -0,0 +1,272 @@ +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'); + }); + + fixture.disablePageReloads`TreeList - Markup` + .disablePageReloads + .page(url(__dirname, '../../container.html')); + + 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.element }); + + }); + + // 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 expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1221037-cell-with-template', { element: treeList.element }); + + }); + + // 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 treeList.apiExpandRow(4); + await treeList.apiExpandRow(2); + + await testScreenshot(page, 'T1291705-row-alternation-after-expanding-node-when-repaintChangesOnly=true', { element: treeList.element }); + + }); + + 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 treeList.apiExpandRow(4); + await treeList.apiExpandRow(2); + + await testScreenshot(page, 'T1291705-row-alternation-after-expanding-node-when-there-is-fixed-column-and-repaintChangesOnly=true', { element: treeList.element }); + + }); + + ['single', 'multiple'].forEach((selectionMode) => { + ['single-line', 'multiple-line'].forEach((contentType) => { + [false, true].forEach((rtlEnabled) => { + test( + `Markup should be correct [T1291914 & T1294907]:selection=${selectionMode},content=${contentType},rtl=${rtlEnabled}`, + async ({ page }) => { + const treeList = page.locator('#container'); + + await testScreenshot(page, `markup-selection=${selectionMode}-rtl=${rtlEnabled}-content=${contentType}`, { element: treeList.element }); + + });, + ).before(async () => { + 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], + }); + }); + }); + }); + }); +}); 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..b830c44f784f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.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('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('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 = page.locator('#container'); + const dataRow = treeList.getDataRow(0); + const expandButton = new ExpandableCell(dataRow.getDataCell(0)).getExpandButton(); + const freeSpaceRow = treeList.getFreeSpaceRow(); + await page.dragToElement(freeSpaceRow, dataRow.element) + .click(expandButton) + .expect(treeList.getDataRow(1).element.exists) + .ok(); + + }); + + [undefined, 200].forEach((height) => { + test(`TreeList - The W1025 warning occurs when dragging a row (height: ${height ?? 'not set'}). (T1280519)`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + height, + scrolling: { + mode: 'virtual', + }, + dataSource: tasksT1228650, + rowDragging: { + allowReordering: true, + }, + }); + + const treeList = page.locator('#container'); + + await treeList.isReady(); + + await treeList.moveRow(0, 10, 10, true); + + await page.waitForTimeout(100); + + const consoleMessages = await getBrowserConsoleMessages(); + const warningExists = !!consoleMessages?.warn.find((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..3f90e8d4dd65 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts @@ -0,0 +1,154 @@ +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('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'); + }); + + const scrollWindowTo = async (position: object) => { + await ClientFunction( + () => { + (window as any).scroll(position); + }, + { + dependencies: { + position, + }, + }, + )(); + }; + + function generateData(rowCount): Record[] { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + items.push({ + ID: i, + Head_ID: -1, + Full_Name: 'Ken Samuelson Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Prefix: 'Dr. Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Title: 'Ombudsman Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + City: 'St. Louis Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + State: 'Missouri Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Email: 'kents@dx-email.com Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Skype: 'kents_DX_skype', + Mobile_Phone: '(562) 555-9282', + Birth_Date: '1972-09-11', + Hire_Date: '2009-04-22', + }); + } + + return items; + } + + // T1129106 + test('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 }) => { + // arrange, act + const treeList = page.locator('#container'); + + await expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1129106-treelist-virtual-scrolling-1'); + + // act + await scrollWindowTo({ top: 10000000 }); + + await expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1129106-treelist-virtual-scrolling-2'); + + // act + await scrollWindowTo({ top: 0 }); + + await expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1129106-treelist-virtual-scrolling-3'); + + // assert + + });.before(async ({ page }) => { + await page.evaluate(() => { + $('#container').wrap('
'); + }); + + await resizeWindow(550, 700); + + await createWidget(page, 'dxTreeList', { + dataSource: generateData(1000), + rootValue: -1, + columnMinWidth: 80, + wordWrapEnabled: true, + columnAutoWidth: true, + allowColumnResizing: true, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + showRowLines: true, + showBorders: true, + autoExpandAll: true, + scrolling: { + mode: 'virtual', + }, + stateStoring: { + enabled: true, + type: 'custom', + customSave: () => {}, + customLoad: () => ({ + pageIndex: 50, + }), + }, + columns: ['Title', 'Full_Name', 'City', 'State', 'Mobile_Phone', 'Hire_Date'], + }); + }); + + // T1189118 + test('All items should be selected after select all and scroll down', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: generateData(100), + height: 400, + rootValue: -1, + columnMinWidth: 80, + columnAutoWidth: true, + allowColumnResizing: true, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + showRowLines: true, + showBorders: true, + autoExpandAll: true, + scrolling: { + mode: 'virtual', + }, + selection: { + allowSelectAll: true, + mode: 'multiple', + }, + columns: ['Title', 'Full_Name', 'City'], + }); + + // arrange + const treeList = page.locator('#container'); + + // assert + await page.expect(treeList.isReady()) + .ok(); + + // act + const selectAllCheckBox = new CheckBox( + treeList.getHeaders().getHeaderRow(0).getHeaderCell(0).getEditor().element, + + await selectAllCheckBox.click(); + + await testScreenshot(page, 'T1189118-treelist-select-all-with-virtual-scrolling-1', { element: treeList.element }); + + // act + await treeList.scrollTo(t, { y: 300 }); + + await testScreenshot(page, 'T1189118-treelist-select-all-with-virtual-scrolling-2', { element: treeList.element }); + + }); +}); 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..e558fde1e6f2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.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('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' }, + ], + }); + + const treeList = page.locator('#container'); + await treeList.apiSearchByText('test'); + + await page.expect((await treeList.apiGetVisibleRows()).length) + .eql(3); + + await page.expect(treeList.apiGetCellValue(0, 0)) + .eql('parent1'); + + await page.expect(treeList.apiGetCellValue(1, 0)) + .eql('test2'); + + await page.expect(treeList.apiGetCellValue(2, 0)) + .eql('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' }, + ], + }); + + const treeList = page.locator('#container'); + await treeList.apiSearchByText('test'); + + await page.expect((await treeList.apiGetVisibleRows()).length) + .eql(3); + + await page.expect(treeList.apiGetCellValue(0, 0)) + .eql('parent1'); + + await page.expect(treeList.apiGetCellValue(1, 0)) + .eql('test2'); + + await page.expect(treeList.apiGetCellValue(2, 0)) + .eql('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..bafccce69ea9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts @@ -0,0 +1,134 @@ +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('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'); + }); + + // T1109666 + 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.element }); + + }); + + // T1264312 + test('TreeList restore selection after the search panel has cleared', async ({ page }) => { + const treeList = page.locator('#container'); + const dataRow = treeList.getDataRow(0); + const expandableCell = new ExpandableCell(dataRow.getDataCell(0)); + const searchBox = treeList.getSearchBox(); + + await page.click(dataRow.getSelectCheckBox()) + .expect(dataRow.isSelected).ok(); + await page.click(expandableCell.getExpandButton()) + .expect(expandableCell.isExpanded()).ok(); + await testScreenshot(page, 'T1264312-selection-checked-all', { element: treeList.element }); + + await page.click(expandableCell.getCollapseButton()) + .typeText(searchBox.input, 'google') + .expect(expandableCell.isExpanded()).ok(); + await testScreenshot(page, 'T1264312-selection-checked-searched', { element: treeList.element }); + + await page.click(dataRow.getSelectCheckBox()) + .expect(dataRow.isSelected).notOk(); + await testScreenshot(page, 'T1264312-selection-unchecked-searched', { element: treeList.element }); + + await page.click(searchBox.getClearButton()) + .click(expandableCell.getExpandButton()) + .expect(expandableCell.isExpanded()).ok(); + await testScreenshot(page, 'T1264312-selection-unchecked-all.png', { element: treeList.element }); + + });.before(async ({ page }) => { + await addRequestHooks(tasksApiMock); + await createWidget(page, 'dxTreeList', () => ({ + dataSource: (window as any).DevExpress.data.AspNet.createStore({ + key: 'Task_ID', + loadUrl: 'https://api/data', + }), + selection: { mode: 'multiple', recursive: true, allowSelectAll: false }, + remoteOperations: { filtering: true, sorting: true, grouping: true }, + parentIdExpr: 'Task_Parent_ID', + hasItemsExpr: 'Has_Items', + searchPanel: { + visible: true, + }, + headerFilter: { + visible: true, + }, + showRowLines: true, + showBorders: true, + columnWidth: 180, + columns: [{ + dataField: 'Task_Subject', + width: 300, + }, { + dataField: 'Task_Assigned_Employee_ID', + caption: 'Assigned', + }, { + dataField: 'Task_Status', + caption: 'Status', + }, { + dataField: 'Task_Start_Date', + caption: 'Start Date', + dataType: 'date', + }, { + dataField: 'Task_Due_Date', + caption: 'Due Date', + dataType: 'date', + }, + ], + })); + }); +}); 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..7c1c0ce24bc5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts @@ -0,0 +1,111 @@ +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('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'; + + test('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 = { + 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(TREE_LIST_SELECTOR); + const headerCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(13); + + await expect(treeList.isReady()).ok(); + + await hover(headerCell.element); + + await expect(headerCell.isHovered()).ok(); + + await testScreenshot(page, 'treelist_header_hover_with_fixed_columns.png', { element: treeList.element }); + + }); + + test('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 = { + 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(TREE_LIST_SELECTOR); + const dataRow = treeList.getDataRow(1); + + await expect(treeList.isReady()).ok(); + + await hover(dataRow.element); + + expect(dataRow.isHovered).toBeTruthy(); + + 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..c693f152752d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.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('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'; + }, + }); + + // arrange, act + const treeList = new TreeList(DATA_GRID_SELECTOR); + + await testScreenshot(page, 'treelist_sticky_columns_with_drag_and_drop_before_interaction.png', { element: treeList.element }); + + // assert + + }); +}); 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..0c98a62f2e21 --- /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('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..aab3dbaeb59f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts @@ -0,0 +1,41 @@ +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('T1187314 - DataGrid displays an incorrect row count in "aria-label" if there is no data after filtering', async ({ page }) => { + 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..e210adc9e808 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.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')}`; + +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'); + }); + // visual: fluent.blue.light + // visual: fluent.blue.dark + const screenshotCheck = async ( + t: TestController, + screenshotName: string, + ) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, `${screenshotName}.png`); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }; + + test('Grid without data', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + await screenshotCheck(t, 'no-data'); + }); +}); 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..539c5cce76bd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts @@ -0,0 +1,84 @@ +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 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'); + }); + // T1257970 + // visual: generic.light + // visual: fluent.blue.light + // visual: material.blue.light + + test('DataGrid - Contrast between icons in the Filter Row menu and their background doesn\'t comply with WCAG accessibility standards', async ({ page }) => { + const filterCell = page.locator('.dx-datagrid-filter-row td').nth(0); + const searchButton = filterCell.menuButton; + const filterMenu = filterCell.menu; + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + await (searchButton).click(); + expect(await filterMenu.element.exists); + await t.ok(); + + await testScreenshot(page, 'T1257970-datagrid-menu-icon-contrast.png', { element: page.locator('#container') }); + }).before( + async () => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 5), + filterRow: { + visible: true, + }, + }); + }, + ); + + // T1286345 + // visual: generic.light + // visual: fluent.blue.light + // visual: material.blue.light + test('DataGrid - Filter icon should remain visible when it\'s focused', async ({ page }) => { + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + const searchIconContainer = dataGrid + .getHeaders() + .getFilterRow() + .getFilterCell(1) + .getSearchIcon() + .element; + + await (page.locator('.dx-datagrid-filter-row td').nth(0).element).click(); + await page.keyboard.press('tab'); + expect(await searchIconContainer.focused); + await t.ok(); + + await testScreenshot(page, 'T1286345-datagrid-menu-icon-when-focused.png', { element: page.locator('#container') }); + ); + }).before( + async () => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(2, 2), + filterRow: { + visible: true, + }, + }); + }, +}); 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..c82f2ea83a9b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.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('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'); + }); + test('Should be shown and hidden when the window is resized', async ({ page }) => { + 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 }, + ], + }); + + await page.locator('.dx-datagrid').first().isVisible(); + + const adaptiveButton = dataGrid.getAdaptiveButton(); + expect(await adaptiveButton.exists).toBeTruthy(); + await (adaptiveButton).click(); + + expect(await dataGrid.getAdaptiveRow(0).element.exists).toBeTruthy(); + + await page.setViewportSize({ width: 1200, height: 400 }); + + expect(await dataGrid.isAdaptiveColumnHidden()).toBeTruthy(); + expect(await dataGrid.getAdaptiveRow(0).element.exists).toBeFalsy(); + }); +}); 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..923de819340b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts @@ -0,0 +1,68 @@ +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'); + }); + const resolveAIRequest = ClientFunction((): void => { + const { aiResponseData } = (window as any); + const { aiResolve } = (window as any); + + if (aiResponseData && aiResolve) { + aiResolve(aiResponseData); + + (window as any).aiResponseData = null; + (window as any).aiResolve = null; + } + }); + + const deleteGlobalVariables = ClientFunction((): void => { + delete (window as any).aiResponseData; + delete (window as any).aiResolve; + }); + + 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', + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + const fourthHeaderCell = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + // assert: the AI column is hidden + expect(await fourthHeaderCell.element.textContent).toBe('AI Column'); + expect(await fourthHeaderCell.isHidden).toBeTruthy(); + + // assert: the adaptive button is visible + expect(await page.locator('.dx-data-row').nth(0).locator('.dx-command-edit').nth(4).getAdaptiveButton().visible).toBeTruthy(); + }); + // TODO: .after() block removed +}); 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..1a8835bcad12 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.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('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' }, + ], + }); + + // arrange + const headerRow = page.locator('.dx-header-row').nth(0); + const columnChooser = page.locator('.dx-datagrid-column-chooser'); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // assert + expect(await dataGrid.apiColumnOption('myAiColumn', 'visible')).toBeTruthy(); + expect(await headerRow.getHeaderTexts()).toBe(['AI Column', 'ID', 'Name', 'Value']); + + // act + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').showColumnChooser()); + + // assert + expect(await columnChooser.isVisible()).toBeTruthy(); + expect(await columnChooser.getColumnTexts()).toBe([]); + + // act + await t.dragToElement( + page.locator('.dx-header-row').nth(0).locator('td').nth(0), + page.locator('.dx-datagrid-column-chooser').content, + ); + + // assert + expect(await dataGrid.apiColumnOption('myAiColumn', 'visible')).toBeFalsy(); + expect(await headerRow.getHeaderTexts()).toBe(['ID', 'Name', 'Value']); + expect(await columnChooser.getColumnTexts()).toBe(['AI Column']); + }); +}); 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..173e0b7a7365 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts @@ -0,0 +1,50 @@ +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('The AI column should not be fixed when the columnFixing.enabled option 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: 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..f59c3f6e8a67 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.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 - 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('Check context menu items', 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, + 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..4ecf256bf198 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts @@ -0,0 +1,52 @@ +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.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('Column reordering should work when allowColumnReordering 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', + allowColumnReordering: true, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { 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(); + + // assert + expect(await headerRow.getHeaderTexts()).toBe(['AI Column', 'ID', 'Name', 'Value']); + + // act + await t.drag(headerRow.locator('td').nth(0).element, 150, 0); + + // assert + expect(await headerRow.getHeaderTexts()).toBe(['ID', 'AI Column', 'Name', 'Value']); + }); +}); 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..574af2cce039 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.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')}`; + +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('The draggable AI column should display correctly', 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', + 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..d8c41b3b41a9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts @@ -0,0 +1,58 @@ +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.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(`Column resizing should work when allowColumnResizing is true (columnResizingMode = ${columnResizingMode})`, 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', + allowColumnResizing: true, + columnResizingMode, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAIColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + const dataCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // assert + expect(await page.locator('.dx-header-row').nth(0).locator('td').nth(0).textContent); + await t.eql('AI Column'); + expect(await dataCell.element.clientWidth); + await t.eql(120); + + // act + await dataGrid.resizeHeader(1, 50); + + // assert + expect(await dataCell.element.clientWidth).toBe(170); + }); +}); 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..84d434b49c3f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.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('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('Resize AI Column when wordWrapEnabled 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', + 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..c1c8c7b922dc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts @@ -0,0 +1,47 @@ +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('The AI column with a given width', 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', + 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..9a850f8bb781 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts @@ -0,0 +1,66 @@ +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('Check keyboard navigation for AI column', 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', + 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..de4c125dc1ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts @@ -0,0 +1,144 @@ +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.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'); + }); + const checkAIColumnTexts = async ( + t: TestController, + component: DataGrid, + expectedRowCount: number, + ): Promise => { + const visibleRows: Record[] = await component.apiGetVisibleRows(); + + await t.expect(visibleRows.length).eql(expectedRowCount); + + // eslint-disable-next-line no-restricted-syntax + for (const row of visibleRows) { + await t + .expect(component.locator('td').nth(row.dataIndex, 3).textContent) + .eql(`Response ${row.data.name}`); + } + }; + + const resolveAIRequest = ClientFunction((): void => { + const { aiResponseData } = (window as any); + const { aiResolve } = (window as any); + + if (aiResponseData && aiResolve) { + aiResolve(aiResponseData); + + (window as any).aiResponseData = null; + (window as any).aiResolve = null; + } + }); + + const deleteGlobalVariables = ClientFunction((): void => { + delete (window as any).aiResponseData; + delete (window as any).aiResolve; + }); + + test('DataGrid should send an AI request for rendered rows after scrolling without changing the page index', async ({ page }) => { + 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 => {}, + }; + }, + }), + }, + }, + ], + }; + }); + + // arrange + // assert + expect(await dataGrid.getLoadPanel().isVisible()); + await t.ok(); + + // act + await resolveAIRequest(); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + expect(await dataGrid.getLoadPanel().isVisible()); + await t.notOk(); + await checkAIColumnTexts(t, dataGrid, 11); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 1000 }); + + // assert + expect(await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTop())); + await t.eql(1000); + expect(await dataGrid.apiPageIndex()); + await t.eql(0); + expect(await page.locator('.dx-data-row').nth(20).locator('td').nth(0).textContent()); + await t.eql('21'); + expect(await dataGrid.getLoadPanel().isVisible()); + await t.ok(); + + // act + await resolveAIRequest(); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + expect(await dataGrid.getLoadPanel().isVisible()); + await t.notOk(); + await checkAIColumnTexts(t, dataGrid, 12); + }); +}); 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..afc6a920d524 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts @@ -0,0 +1,43 @@ +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('Default render', 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', + 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..548844f43674 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts @@ -0,0 +1,115 @@ +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 columns = [ + { + dataField: 'A', + }, + { + dataField: 'B', + lookup: { + dataSource: lookUpDataSource, + valueExpr: 'id', + displayExpr: 'text', + }, + }, + ]; + + const nestedColumns = [ + { + dataField: 'A', + }, + { + name: 'Nested', + caption: 'Nested', + columns: [ + { + dataField: 'B', + lookup: { + dataSource: lookUpDataSource, + valueExpr: 'id', + displayExpr: 'text', + }, + }, + ], + }, + ]; + + const changeDataGridColumnsReactWay = ClientFunction(() => { + const dataGridWidget = ($(`${GRID_CONTAINER}`) as any).dxDataGrid('instance'); + + dataGridWidget.beginUpdate(); + + dataGridWidget.option('columns[1].dataField', undefined); + dataGridWidget.option('columns[1].lookup', undefined); + dataGridWidget.option('columns[1].columns', nestedColumns[1].columns); + dataGridWidget.option('columns[1].name', nestedColumns[1].name); + dataGridWidget.option('columns[1].caption', nestedColumns[1].caption); + + dataGridWidget.endUpdate(); + }, { dependencies: { GRID_CONTAINER, nestedColumns } }); + + test('Should change usual columns to band columns without error in React (T1213679)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...dataSource], + columns: [...columns], + keyExpr: 'id', + showBorders: true, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'band-columns_before-runtime-update.png', { element: page.locator('#container') }); + + await changeDataGridColumnsReactWay(); + + 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..f90bd2db0153 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts @@ -0,0 +1,47 @@ +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'); + }); + + const scrollTo = ClientFunction((x, y) => { + window.scrollTo(x, y); + }); + 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 }); + } + + return createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + }); + + const filterBuilder = new FilterBuilder('#container'); + const lastField = filterBuilder.getField(49); + + await scrollTo(0, 10000); + await (lastField.element).click(); + + expect(await lastField.text).toBe('Test 50'); + expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); + }); +}); 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..bfb015642459 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts @@ -0,0 +1,62 @@ +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 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'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + // visual: fluent.blue.dark + ['dragAndDrop', 'select'].forEach((mode: any) => { + + 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()); + + expect(await page.locator('.dx-datagrid-column-chooser').isVisible()); + await t.ok(); + + await testScreenshot(page, `column-chooser-${mode}-mode.png`, { element: page.locator('#container') }); + }); + // TODO: .after() block removed +}); 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..243169d5c65a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts @@ -0,0 +1,80 @@ +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('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'); + }); + + const CLASS = ClassNames; + + const getVisibleColumns = (dataGrid: DataGrid): Promise => { + const { getInstance } = dataGrid; + + return ClientFunction( + () => (getInstance() as any) + .getVisibleColumns() + .map((column: any) => column.dataField ?? column.name), + { dependencies: { getInstance } }, + )(); + }; + const getColumnsSeparatorOffset = ClientFunction(() => $(`.${CLASS.columnsSeparator}`).offset(), { dependencies: { CLASS } }); + // T975549 + + 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, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + const headers = page.locator('.dx-header-row'); + const headerRow = headers.getHeaderRow(0); + + expect(await headerRow.locator('td').nth(2).element.textContent); + await t.eql('Field 2'); + await t.drag(headerRow.locator('td').nth(3).element, -400, 0); + expect(await headerRow.locator('td').nth(2).element.textContent); + await t.eql('Field 3'); + expect(await getVisibleColumns(dataGrid)); + await t.eql(['field1', 'fake', 'field3', 'field2', 'field4']); + }); +}); 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..c69cdd86511e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.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')}`; + +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, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await t.drag(dataGrid.getGroupPanel().getHeader(0).element, 0, 30); + await testScreenshot(page, 'column-separator-with-expand-columns.png'); + ); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + }); + // TODO: .after() block removed +}); 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..9e13e4f93e43 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts @@ -0,0 +1,65 @@ +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('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('DataGrid – Resize indicator is moved when resizing a grouped column if showWhenGrouped is set to true', 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', + 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' }, + ], + }, + ], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.resizeHeader(3, 30, false); + + expect(await page.locator('.dx-header-row').nth(1).locator('td').nth(0).clientWidth); + await t.within(128, 130); + }); +}); 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..93f449deb349 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts @@ -0,0 +1,75 @@ +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('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('column separator should starts from the parent', async ({ page }) => { + 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); + }); +}); 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..0e3e43ff5f2e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts @@ -0,0 +1,88 @@ +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 - 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'); + }); + const apiRequestMock = RequestMock() + .onRequestTo(/\/api\/data/) + .respond( + { + data: [ + { + id: 0, + data: 'A', + }, { + id: 1, + data: 'B', + }, { + id: 2, + data: 'C', + }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/update/) + .respond( + {}, + 200, + { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': '*', + }, + ); + + // 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 firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const middleCell = page.locator('.dx-data-row').nth(1).locator('td').nth(0); + const secondCell = page.locator('.dx-data-row').nth(2).locator('td').nth(0); + + await (firstCell.locator('.dx-editor-cell')).fill(' AAA'); + await (secondCell.locator('.dx-editor-cell')).fill(' CCC'); + await (middleCell.element).click(); + + const firstCellValue = await firstCell.locator('.dx-editor-cell')().value; + const secondCellValue = await secondCell.locator('.dx-editor-cell')().value; + + expect(await firstCellValue).toBe('A AAA'); + expect(await secondCellValue).toBe('C CCC'); + }); + // TODO: .after() block removed +}); 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..70ae7d56a703 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts @@ -0,0 +1,64 @@ +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 - 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'); + }); + const READONLY_CLASS = 'dx-datagrid-readonly'; + const CELL_FOCUS_DISABLED_CLASS = 'dx-cell-focus-disabled'; + + (['cell', 'batch'] as GridsEditMode[]).forEach((mode) => { + + test(`showEditorAlways 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, 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 addRowButton = page.locator('.dx-datagrid-header-panel').getAddRowButton(); + + await (addRowButton).click(); + + const newRow = page.locator('.dx-data-row').nth(0); + expect(await newRow.isInserted).toBeTruthy(); + + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const editor = cell.locator('.dx-editor-cell'); + + expect(await cell.element.hasClass(READONLY_CLASS)); + await t.notOk('showEditorAlways cell in new row should not have readonly class'); + expect(await cell.element.hasClass(CELL_FOCUS_DISABLED_CLASS)); + await t.notOk('showEditorAlways cell in new row should not have cell-focus-disabled class'); + + await (editor.element).click(); + expect(await cell.isFocused); + await t.ok('showEditorAlways cell should be focused after click'); + expect(await editor.element.focused); + await t.ok('editor should be focused after click'); + await (editor.element).fill('test value'); + expect(await editor.element.value); + await t.eql('test value'); + }); +}); 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..fae24f20828e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts @@ -0,0 +1,615 @@ +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.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'); + }); + + /* eslint-disable @typescript-eslint/init-declarations */ + + ); + + interface ColumnInfo { + columnIndex: number; + dataField: string; + newValue: string; + newMaskValue?: string; + } + + const editingModes: GridsEditMode[] = ['cell', 'batch', 'row', 'form', 'popup']; + + const textColumnInfos: ColumnInfo[] = [ + { columnIndex: 0, dataField: 'text', newValue: 'xxxx' }, + { columnIndex: 5, dataField: 'calculated', newValue: '9' }, + ]; + + const expectedTextColumnResult: ColumnInfo[] = [ + ...textColumnInfos, + { columnIndex: 1, dataField: 'number', newValue: '8' }, + ]; + + const maskedColumnInfos: ColumnInfo[] = [ + { + columnIndex: 0, dataField: 'text', newValue: 'xxxx', newMaskValue: 'xxxxx', + }, + { + columnIndex: 1, dataField: 'number', newValue: '-9', newMaskValue: '9-', + }, + { + columnIndex: 2, dataField: 'date', newValue: '10/1/2020', newMaskValue: '101', + }, + ]; + + const expectedMaskedColumnResult: ColumnInfo[] = [ + ...maskedColumnInfos, + { columnIndex: 5, dataField: 'calculated', newValue: '-8' }, + ]; + + const basicColumnInfos: ColumnInfo[] = [ + { columnIndex: 0, dataField: 'text', newValue: 'xxxx' }, + { columnIndex: 1, dataField: 'number', newValue: '-9' }, + { columnIndex: 2, dataField: 'date', newValue: '10/1/2020' }, + { columnIndex: 3, dataField: 'lookup', newValue: 'lookup 2' }, + { columnIndex: 4, dataField: 'boolean', newValue: 'true' }, + ]; + + const expectedBasicColumnResult: ColumnInfo[] = [ + ...basicColumnInfos, + { columnIndex: 5, dataField: 'calculated', newValue: '-8' }, + ]; + + const dataGrid = new DataGrid('#container'); + + const createDataGrid = ({ + mode, repaintChangesOnly = false, useMask = false, + }) => async (): Promise => 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, + editing: { + mode, + allowUpdating: true, + }, + columns: [ + { + dataField: 'text', + editorOptions: { + mask: useMask ? 'cccc' : undefined, + }, + }, + { + dataField: 'number', + editorOptions: { + format: '#0', + useMaskBehavior: useMask, + }, + }, + { + dataField: 'date', + dataType: 'date', + editorOptions: { + useMaskBehavior: useMask, + pickerType: 'calendar', + }, + }, + { + dataField: 'lookup', + lookup: { + valueExpr: 'id', + displayExpr: 'text', + dataSource: [ + { id: 1, text: 'lookup 1' }, + { id: 2, text: 'lookup 2' }, + ], + }, + }, + { dataField: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data): number => (data as { number: number }).number + 1, + setCellValue: (newData, value): void => { + newData.number = value - 1; + }, + }, + ], + }); + + const getEditForm = (mode: GridsEditMode): EditForm | null => { + if (mode === 'form') { + return dataGrid.getEditForm(); + } + if (mode === 'popup') { + return dataGrid.getPopupEditForm(); + } + return null; + }; + + const clickEditButtonIfExists = async (t: TestController, form: EditForm | null): Promise => { + const formAlreadyOpened = await form?.element.exists && await form?.element.visible; + + if (formAlreadyOpened) { + return; + } + + const editButton = dataGrid.getDataRow(0).locator('.dx-command-edit').nth(6).getEditButton(); + + if (await editButton.exists) { + await t.click(editButton); + } + }; + + const checkCellFocused = async ( + t: TestController, + mode: GridsEditMode, + { dataField }: ColumnInfo, + cell: DataCell | undefined, + editor: CellEditor | undefined, + ): Promise => { + const isRowBasedMode = mode === 'row'; + const isFormBasedMode = mode === 'form' || mode === 'popup'; + + if (!isFormBasedMode) { + await t.expect(cell?.isFocused).ok(); + } + + if ((isRowBasedMode || isFormBasedMode) && dataField === 'boolean') { + return; + } + + await t + .expect(editor?.element.focused) + .eql(true); + }; + + const clickCellEditor = async ( + t: TestController, + mode: GridsEditMode, + columnInfo: ColumnInfo, + form: EditForm | null, + cell: DataCell, + editor: CellEditor, + ): Promise => { + await clickEditButtonIfExists(t, form); + + const item = !form ? cell.element : editor.getItemLabel(); + await t.click(item, { offsetX: 25 }); + + await checkCellFocused(t, mode, columnInfo, cell, editor); + }; + + const moveToFirstCellEditor = async (t: TestController, mode: GridsEditMode): Promise => { + await t + .pressKey('tab') + .pressKey('ctrl+down') + .pressKey('enter'); + + if (mode === 'popup') { + const columns = await dataGrid.apiOption('columns'); + await t + .pressKey(columns.map(() => 'tab').join(' ')) + .pressKey('enter') + .wait(500); + } + }; + + const setEditorValue = async ( + t: TestController, + mode: GridsEditMode, + { dataField, newMaskValue, newValue }: ColumnInfo, + editor: CellEditor, + useKeyboard = false, + useMask = false, + ): Promise => { + if (dataField === 'boolean') { + if (useKeyboard) { + await t.pressKey('space'); + } else { + await t.click(editor.element); + } + + return; + } + + const value: string = useMask ? newMaskValue ?? '' : newValue; + + if (dataField === 'date' && !useKeyboard && !useMask) { + await t.click(editor.getDropDownButton()); + await t.click(Selector(`.${CLASS.calendarCell}`).withText(value.split('/')[1])); + + return; + } + + if (dataField === 'lookup' && !useKeyboard) { + if (mode === 'cell' || mode === 'batch') { + await t.click(editor.getDropDownButton()); + } + await t.click(Selector(`.${CLASS.listItemContent}`).withText(value)); + + return; + } + + await t.typeText(editor.element, value, { replace: true }); + + if (dataField === 'lookup' && useKeyboard) { + await Selector(`.${CLASS.listItemContent}`).withText(value)(); + await t.pressKey('enter'); + } + }; + + const focusNextCellEditor = async ( + t: TestController, + mode: GridsEditMode, + { columnIndex }: ColumnInfo, + useKeyboard = false, + ): Promise<{ nextColumnIndex: number }> => { + const form = getEditForm(mode); + const nextColumnIndex = columnIndex === 0 ? 1 : columnIndex - 1; + const nextColumnInfo = basicColumnInfos[nextColumnIndex]; + + let nextEditor: CellEditor | undefined; + let nextCell: DataCell | undefined; + + if (form) { + if (nextColumnInfo) { + nextEditor = new CellEditor(form.getItem(nextColumnInfo.dataField)); + if (useKeyboard) { + await t.pressKey(columnIndex === 0 ? 'tab' : 'shift+tab'); + } else { + await t.click(nextEditor.element); + } + } + } else { + nextCell = dataGrid.locator('td').nth(0, nextColumnIndex); + nextEditor = nextCell.locator('.dx-editor-cell'); + + if (useKeyboard) { + await t.pressKey(columnIndex === 0 ? 'tab' : 'shift+tab'); + } else { + const isCellRevertBug = mode === 'cell' && columnIndex < nextColumnIndex; + await t.click(nextCell.element, { offsetX: isCellRevertBug ? 50 : 5 }); + } + } + + await checkCellFocused(t, mode, nextColumnInfo, nextCell, nextEditor); + + return { nextColumnIndex }; + }; + + const getSaveButton = ( + mode: string, + form: EditForm | null, + ): Selector | undefined => { + switch (mode) { + case 'batch': + return dataGrid.getHeaderPanel().getSaveButton(); + case 'row': + return dataGrid.getDataRow(0).locator('.dx-command-edit').nth(6).locator('.dx-link').nth(0); + case 'cell': + return Selector('body'); + default: + return form?.saveButton; + } + }; + + const getCellText = async ( + dataField: string, + cell: DataCell, + ): Promise => { + if (dataField === 'boolean') { + return await cell.locator('.dx-editor-cell').isChecked() ? 'true' : 'false'; + } + + return cell.element.textContent; + }; + + const checkSavedCell = async ( + t: TestController, + { dataField, newValue }: ColumnInfo, + cell: DataCell, + ): Promise => { + await t + .expect(await getCellText(dataField, cell)) + .eql(newValue) + .expect(cell.isEditCell) + .eql(dataField === 'boolean') + .expect(cell.isModified) + .notOk() + .expect(DataCell.getModifiedCells().count) + .eql(0); + }; + + const getEditorValue = async ( + dataField: string, + editor: CellEditor, + ): Promise => { + if (dataField === 'boolean') { + return await editor.isChecked() ? 'true' : 'false'; + } + + return editor.element.value; + }; + + const checkModifiedCell = async ( + t: TestController, + mode: string, + { dataField, newValue }: ColumnInfo, + cell: DataCell, + editor: CellEditor, + modifiedCellsCount: number, + ): Promise => { + const editorText = mode === 'batch' || mode === 'cell' + ? await getCellText(dataField, cell) + : await getEditorValue(dataField, editor); + + await t + .expect(editorText) + .eql(newValue); + + if (mode !== 'form' && mode !== 'popup') { + await t + .expect(cell.isEditCell) + .eql(mode === 'row' || dataField === 'boolean') + .expect(cell.isModified) + .eql(mode === 'batch') + .expect(DataCell.getModifiedCells().count) + .eql(modifiedCellsCount); + } + }; + + editingModes.forEach((mode) => { + const configurations = [ + { + repaintChangesOnly: true, + useMask: false, + columnInfos: textColumnInfos, + expectedResult: expectedTextColumnResult, + }, + { + repaintChangesOnly: false, + useMask: true, + columnInfos: maskedColumnInfos, + expectedResult: expectedMaskedColumnResult, + }, + { + repaintChangesOnly: false, + useMask: false, + columnInfos: basicColumnInfos, + expectedResult: expectedBasicColumnResult, + }, + ]; + + configurations.forEach(({ + repaintChangesOnly, useMask, columnInfos, expectedResult, + }) => { + + test( + `Update cell value, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}, useKeyboard: false, useMask: ${useMask}`, + async ({ page }) => { + const form = getEditForm(mode); + + if (mode === 'batch') { + await dataGrid.apiCellValue(0, 0, 'modified'); + } + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of columnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form + ? new CellEditor(form.getItem(columnInfo.dataField)) + : cell.locator('.dx-editor-cell'); + + await clickCellEditor(t, mode, columnInfo, form, cell, editor); + await setEditorValue(t, mode, columnInfo, editor, false, useMask); + } + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); + } + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of expectedResult) { + await checkSavedCell( + t, + columnInfo, + page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid({ + mode, + useMask, + repaintChangesOnly, + })); + }); + + [true, false].forEach((repaintChangesOnly) => { + test( + `Update calculated cell value, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}, useKeyboard: false, useMask:false`, + async ({ page }) => { + const columnInfo = { columnIndex: 5, dataField: 'calculated', newValue: '9' }; + const form = getEditForm(mode); + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + await clickCellEditor(t, mode, columnInfo, form, cell, editor); + await setEditorValue(t, mode, columnInfo, editor); + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + if (!repaintChangesOnly && (mode === 'row' || mode === 'form')) { + await ('body').click(); + } + + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); + } + + const expectedColumnResult: ColumnInfo[] = [ + { columnIndex: 1, dataField: 'number', newValue: '8' }, + { columnIndex: 5, dataField: 'calculated', newValue: '9' }, + ]; + + // eslint-disable-next-line no-restricted-syntax + for (const resultColumnInfo of expectedColumnResult) { + await checkSavedCell( + t, + resultColumnInfo, + page.locator('.dx-data-row').nth(0).locator('td').nth(resultColumnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid({ + mode, + repaintChangesOnly, + })); + }); + + const keyboardConfigurations = [ + { + useMask: true, + columnInfos: maskedColumnInfos, + expectedResult: expectedMaskedColumnResult, + }, + { + useMask: false, + columnInfos: basicColumnInfos, + expectedResult: expectedBasicColumnResult, + }, + ]; + + keyboardConfigurations.forEach(({ useMask, columnInfos, expectedResult }) => { + test( + `Update cell value, mode: ${mode}, repaintChangesOnly: false, useKeyboard: true, useMask: ${useMask}`, + async ({ page }) => { + const form = getEditForm(mode); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + await moveToFirstCellEditor(t, mode); + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of columnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form + ? new CellEditor(form.getItem(columnInfo.dataField)) + : cell.locator('.dx-editor-cell'); + + if (columnInfo.columnIndex > 0) { + await page.keyboard.press('tab'); + } + + await checkCellFocused(t, mode, columnInfo, cell, editor); + await setEditorValue(t, mode, columnInfo, editor, true, useMask); + } + + await page.keyboard.press('enter'); + + if (mode === 'batch' || mode === 'popup') { + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); + } + } + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of expectedResult) { + await checkSavedCell( + t, + columnInfo, + page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid({ + mode, + useMask, + })); + }); + + test( + `Update cell value and focus next cell, mode: ${mode}, repaintChangesOnly: false, useKeyboard: true`, + async ({ page }) => { + const form = getEditForm(mode); + let activeColumnIndex = 0; + let modifiedCellCount = mode === 'batch' ? 1 : 0; + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + await moveToFirstCellEditor(t, mode); + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of textColumnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + for (let i = activeColumnIndex; i < columnInfo.columnIndex; i += 1) { + await page.keyboard.press('tab'); + } + + await checkCellFocused(t, mode, columnInfo, cell, editor); + await setEditorValue(t, mode, columnInfo, editor, true); + + const { nextColumnIndex } = await focusNextCellEditor(t, mode, columnInfo, true); + activeColumnIndex = nextColumnIndex; + + if (mode === 'batch') { + modifiedCellCount += 1; + } + + await checkModifiedCell(t, mode, columnInfo, cell, editor, modifiedCellCount); + } + }, + ).before(createDataGrid({ + mode, + })); + + [true, false].forEach((repaintChangesOnly) => { + test( + `Update cell value and focus next cell, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}, useKeyboard: false`, + async ({ page }) => { + const form = getEditForm(mode); + let modifiedCellCount = mode === 'batch' ? 1 : 0; + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of textColumnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form + ? new CellEditor(form.getItem(columnInfo.dataField)) + : cell.locator('.dx-editor-cell'); + + await clickCellEditor(t, mode, columnInfo, form, cell, editor); + await setEditorValue(t, mode, columnInfo, editor); + + await focusNextCellEditor(t, mode, columnInfo); + + if (mode === 'batch') { + modifiedCellCount += 1; + } + + await checkModifiedCell(t, mode, columnInfo, cell, editor, modifiedCellCount); + } + }, + ).before(createDataGrid({ + mode, + repaintChangesOnly, + })); + }); + }); +}); 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..c0bfc53ed07f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts @@ -0,0 +1,118 @@ +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('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'); + }); + + /* eslint-disable @typescript-eslint/no-misused-promises */ + + ); + + // T1186997 + const testCases = [{ + caseName: 'e.cancel = promise:true', + expected: true, + + onRowUpdating: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(true); + }); + }), + onRowInserting: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(true); + }); + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(true); + }); + }), + }, { + caseName: 'e.cancel = true', + expected: true, + + onRowUpdating: ClientFunction((e) => { + e.cancel = true; + }), + onRowInserting: ClientFunction((e) => { + e.cancel = true; + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = true; + }), + }, { + caseName: 'e.cancel = promise:false', + expected: false, + + onRowUpdating: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(false); + }); + }), + onRowInserting: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(false); + }); + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(false); + }); + }), + }, { + caseName: 'e.cancel = false', + expected: false, + + onRowUpdating: ClientFunction((e) => { + e.cancel = false; + }), + onRowInserting: ClientFunction((e) => { + e.cancel = false; + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = false; + }), + }]; + + // onRowUpdating + testCases.forEach(({ caseName, expected, onRowUpdating }) => { + + test(`onRowUpdating event should be work valid in case '${caseName}'`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + FirstName: 'John', + }], + columns: [{ + dataField: 'FirstName', + caption: 'Firs tName', + }], + height: 300, + editing: { + mode: 'row', + allowUpdating: true, + }, + onRowUpdating, + }); + + const dataRow = page.locator('.dx-data-row').nth(0); + + await (dataRow.locator('td').nth(1).getLinkEdit()).click(); + + await (dataRow.locator('td').nth(0).locator('.dx-editor-cell')).fill('test text'); + await (dataRow.locator('td').nth(1).getLinkSave()).click(); + + expect(await dataRow.locator('td').nth(1).getLinkSave().exists).toBe(expected); + }); +}); 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..7d8f56e4215e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts @@ -0,0 +1,239 @@ +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.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'); + }); + interface ColumnInfo { + columnIndex: number; + dataField: string; + newValue: string; + } + + const createDataGrid = (mode: GridsEditMode, repaintChangesOnly = false) => async (): Promise => 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, + editing: { + mode, + allowAdding: true, + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { + dataField: 'number', + editorOptions: { + format: '#0', + }, + }, + { + dataField: 'date', + dataType: 'date', + editorOptions: { + pickerType: 'calendar', + }, + }, + { + dataField: 'lookup', + lookup: { + valueExpr: 'id', + displayExpr: 'text', + dataSource: [ + { id: 1, text: 'lookup 1' }, + { id: 2, text: 'lookup 2' }, + ], + }, + }, + { dataField: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data): number => (data as { number: number }).number + 1, + setCellValue: (newData, value): void => { + newData.number = value - 1; + }, + }, + ], + }); + + const expectedCalculatedColumnResult: ColumnInfo[] = [ + { columnIndex: 1, dataField: 'number', newValue: '8' }, + { columnIndex: 5, dataField: 'calculated', newValue: '9' }, + ]; + + const dataGrid = new DataGrid('#container'); + + const getEditForm = (mode: GridsEditMode): EditForm | null => { + if (mode === 'form') { + return dataGrid.getEditForm(); + } + if (mode === 'popup') { + return dataGrid.getPopupEditForm(); + } + return null; + }; + + const addRow = async ( + t: TestController, + { columnIndex }: { columnIndex: number }, + form: EditForm | null, + cell: DataCell, + editor: CellEditor, + ): Promise => { + const addRowButton = dataGrid.getHeaderPanel().getAddRowButton(); + await t.click(addRowButton); + + if (columnIndex > 0) { + const item = !form ? cell.element : editor.getItemLabel(); + await t.click(item, { offsetX: 5 }); + } + }; + + const checkCellFocused = async ( + t: TestController, + mode: GridsEditMode, + { dataField }: ColumnInfo, + cell: DataCell | undefined, + editor: CellEditor | undefined, + ): Promise => { + if (mode !== 'form' && mode !== 'popup') { + await t.expect(cell?.isFocused).ok(); + } + + const isRowBasedMode = mode === 'row'; + const isFormBasedMode = mode === 'form' || mode === 'popup'; + + if ((isRowBasedMode || isFormBasedMode) && dataField === 'boolean') { + return; + } + + await t + .expect(editor?.element.focused) + .eql(true); + }; + + const getSaveButton = ( + mode: string, + form: EditForm | null, + ): Selector | undefined => { + switch (mode) { + case 'batch': + return dataGrid.getHeaderPanel().getSaveButton(); + case 'row': + return dataGrid.getDataRow(0).locator('.dx-command-edit').nth(6).locator('.dx-link').nth(0); + case 'cell': + return Selector('body'); + default: + return form?.saveButton; + } + }; + + const getCellText = async ( + dataField: string, + cell: DataCell, + ): Promise => { + if (dataField === 'boolean') { + return await cell.locator('.dx-editor-cell').isChecked() ? 'true' : 'false'; + } + + return cell.element.textContent; + }; + + const checkSavedCell = async ( + t: TestController, + { dataField, newValue }: ColumnInfo, + cell: DataCell, + ): Promise => { + await t + .expect(await getCellText(dataField, cell)) + .eql(newValue) + .expect(cell.isEditCell) + .eql(dataField === 'boolean') + .expect(cell.isModified) + .notOk() + .expect(DataCell.getModifiedCells().count) + .eql(0); + }; + + const modes: GridsEditMode[] = ['cell', 'batch', 'row', 'form', 'popup']; + + modes.forEach((mode) => { + [true, false].forEach((repaintChangesOnly) => { + + test( + `Update cell value in new row, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}`, + async ({ page }) => { + const columnInfo = { columnIndex: 0, dataField: 'text', newValue: 'xxxx' }; + const form = getEditForm(mode); + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + await addRow(t, columnInfo, form, cell, editor); + + await checkCellFocused(t, mode, columnInfo, cell, editor); + + await t.typeText(editor.element, columnInfo.newValue, { replace: true }); + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); + } + + await checkSavedCell(t, columnInfo, page.locator('.dx-data-row').nth(2).locator('td').nth(columnInfo.columnIndex)); + }, + ).before(createDataGrid(mode, repaintChangesOnly)); + + test( + `Update calculated cell value in new row, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}`, + async ({ page }) => { + const columnInfo = { columnIndex: 5, dataField: 'calculated', newValue: '9' }; + const form = getEditForm(mode); + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + await addRow(t, columnInfo, form, cell, editor); + + await checkCellFocused(t, mode, columnInfo, cell, editor); + + await t.typeText(editor.element, columnInfo.newValue, { replace: true }); + + if (!repaintChangesOnly && (mode === 'row' || mode === 'form')) { + await ('body').click(); + } + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); + } + + // eslint-disable-next-line no-restricted-syntax + for (const resultColumnInfo of expectedCalculatedColumnResult) { + await checkSavedCell( + t, + resultColumnInfo, + page.locator('.dx-data-row').nth(2).locator('td').nth(resultColumnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid(mode, repaintChangesOnly)); + }); + }); +}); 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..075ac35c61e2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts @@ -0,0 +1,74 @@ +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; +}; + +// TODO: import defaultConfig from sticky helpers or inline the data + +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'); + }); + const getGridConfig = (config): Record => { + const defaultConfig = { + errorRowEnabled: true, + dataSource: [{ + id: 1, name: 'Alex', age: 15, lastName: 'John', + }], + keyExpr: 'id', + legacyRendering: false, + }; + + return config ? { ...defaultConfig, ...config } : defaultConfig; + }; + + 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(); + }); + + return 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 resolveOnSavingDeferred = ClientFunction(() => (window as any).deferred.resolve()); + + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).fill('new_value'); + await page.keyboard.press('tab tab'); + await resolveOnSavingDeferred(); + expect(await page.locator('.dx-data-row').nth(2).locator('td').nth(0).isEditCell).toBeTruthy(); + }); + // TODO: .after() block removed +}); 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..d522ef7cbdad --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts @@ -0,0 +1,47 @@ +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('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'); + }); + // T1274123 + + 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) { + e.data.id = 0; + e.data.text = 'test'; + }, + height: 300, + }); + + await (; + page.locator('.dx-datagrid-header-panel').getAddRowButton(),).click() + .click( + dataGrid.getPopupEditForm().cancelButton, + ); + + await (page.locator('.dx-datagrid-header-panel').getAddRowButton(),).click(); + + expect(await + dataGrid.getPopupEditForm().element.exists, + ).toBeTruthy(); + }); +}); 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..d3dcd2ac331f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts @@ -0,0 +1,54 @@ +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 - 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'); + }); + + fixture.disablePageReloads`Editing - undefined values` + .disablePageReloads + .page(url(__dirname, '../../../container.html')); + + 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) => String(cellInfo.value.data ?? 'undefined'), + }], + showBorders: true, + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const secondCell = page.locator('.dx-data-row').nth(1).locator('td').nth(0); + + expect(await firstCell.element().textContent).toBe('100'); + expect(await secondCell.element().textContent).toBe('undefined'); + + await dataGrid.apiCellValue(0, 0, { data: undefined }); + await dataGrid.apiSaveEditData(); + + expect(await firstCell.element().textContent).toBe('undefined'); + expect(await secondCell.element().textContent).toBe('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..006844a3667f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.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 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('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'); + }); + const encodedIcon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgIHdpZHRoPSIyMHB4IiBoZWlnaHQ9IjIwcHgiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0iIzAwMDAwMCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KCTxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIC8+DQo8L3N2Zz4NCg=='; + + test('The E0110 should not occur when editing a column with setCellValue in form mode (T1193894)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Name: 'test', + }], + keyExpr: 'ID', + editing: { + mode: 'form', + allowUpdating: true, + editRowKey: 1, + }, + columns: [{ + dataField: 'Name', + setCellValue(rowData, value) { + rowData.Name = value; + }, + }], + // @ts-expect-error private option + templatesRenderAsynchronously: true, + }); + + // act + await (dataGrid.getFormItemEditor(0)).fill('new'); + await (dataGrid.getEditForm().saveButton).click(); + + // assert + await testScreenshot(page, 'grid-form-editing-T1193894.png', { element: page.locator('#container') }); + }); +}); 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..cce71359c670 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts @@ -0,0 +1,31 @@ +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 }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + export: { + enabled: true, + }, + }); + + const consoleMessages = await t.getBrowserConsoleMessages(); + const isWarningExist = !!consoleMessages?.warn.find((message) => message.startsWith('W1024')); + + expect(await 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..2eb6a3bf0894 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts @@ -0,0 +1,26 @@ +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('Export 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('allowExportSelectedData: false, menu: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, value: 2 }], + export: { + enabled: true, + }, + }); + + await testScreenshot(page, 'grid-export-one-button.png', { element: page.locator('.dx-datagrid-header-panel') }); + }); +}); 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..b29f81a32031 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.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('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('editor\'s popup inside filterBuilder is opening & closing right (T1182854)', async ({ page }) => { + 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..363bd5eef9a1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts @@ -0,0 +1,68 @@ +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('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 GRID_SELECTOR = '#container'; + + 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(`Should change filter row icon (columns ${firstColumnsName} ${secondColumnsName}, scrolling ${scrollingName}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: generateTestData(25), + filterRow: { + visible: true, + }, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + ...firstColumns, + ...secondColumns, + ], + scrolling, + }); + + for (let columnIdx = 0; columnIdx < 4; columnIdx += 1) { + const filterCell = page.locator('.dx-datagrid-filter-row td').nth(columnIdx); + await (filterCell.menuButton).click(); + await (filterCell.menu.getItemByText('Starts with')).click(); + } + + await testScreenshot(page, + `T1163100_change-icon_columns-${firstColumnsName}-${secondColumnsName}_scrolling-${scrollingName}.png`, + { element: page.locator('#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..836a8188980e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts @@ -0,0 +1,68 @@ +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('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, Text: 'Item 1' }, + { ID: 2, Text: '' }, + { ID: 3, Text: 'Item 3' }, + ], + keyExpr: 'ID', + showBorders: true, + remoteOperations: true, + headerFilter: { visible: true }, + filterRow: { visible: true }, + filterPanel: { visible: true }, + filterValue: ['Text', '=', 'i'], + columns: ['ID', { + dataField: 'Text', + selectedFilterOperation: '=', + }], + onEditorPreparing(e: any) { + e.updateValueTimeout = 100; + }, + }); + + const filterEditor = dataGrid.getFilterEditor(1, TextBox); + const filterPanelText = dataGrid.getFilterPanel().getFilterText(); + + // assert + .expect(filterPanelText.element.textContent) + .eql('[Text] Equals \'i\'') + // act + .click(filterEditor.locator('input')) + .pressKey('backspace') + .wait(100) // updateValueTimeout + // assert + .expect(filterPanelText.element.textContent) + .eql('Create Filter') + // act + .click(page.locator('#container')) + // assert + .expect(filterPanelText.element.textContent) + .eql('Create Filter'); + }); +}); 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..a93e33a802f6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/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('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') }); + }); +}); 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..9a44c950ec9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts @@ -0,0 +1,58 @@ +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'; + + // T1311818 + + 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, + }); + + // arrange + const consoleMessages = await t.getBrowserConsoleMessages(); + + // act + 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' }, + ], + }); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await dataGrid.option({ + columns: [], + dataSource: undefined, + }); + + // assert + expect(await consoleMessages.error.every((msg) => !msg.includes('E1047'))); + await t.ok(); + }); +}); 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..2bb64c10ffd0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/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('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'; + + test('Data should be filtered if True is selected via the filter method when case sensitivity is enabled', async ({ page }) => { + 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, + }); + + // arrange + // act + await dataGrid.apiFilter(['text', '=', 'true']); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'filter-method-with-case-sensitive-1.png', { element: page.locator('#container') }); + + // act + await dataGrid.apiFilter(['text', '=', 'True']); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + 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..97fd40a9abd9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts @@ -0,0 +1,138 @@ +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('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('Fixed columns should have same width as not fixed columns with columnAutoWidth: true', async ({ page }) => { + 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); + }); +}); 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..bb6b28dcdfb5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.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('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'); + }); + // T1148937 + + test('Hovering over a row should work correctly when there is a fixed column and a column with a cellTemplate (React)', async ({ page }) => { + 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()); + }); +}); 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..12416f501493 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts @@ -0,0 +1,44 @@ +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('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) => 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.element).click(); + await (secondCell.element).click(); + + expect(await firstCell.element().hasClass(FOCUSED_CLASS)).toBeFalsy(); + expect(await secondCell.element().hasClass(FOCUSED_CLASS)).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..cb8470a0ad29 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts @@ -0,0 +1,147 @@ +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('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'); + }); + + // TODO: Something wrong with test cleanup with 'disablePageReloads' + // old events from previous test still alive on the next test case run + // So, we should disable it for these tests until this problem exists. + ); + + type FocusCellChangingData = [ + [prevRowIdx: number, prevColumnIdx: number], + [rowIdx: number, columnIdx: number], + ]; + type FocusCellChangedData = [rowIdx: number, columnIdx: number]; + type FocusRowChangingData = [prevRowIdx: number, rowIdx: number]; + type FocusRowChangedData = [rowIdx: number]; + + const GRID_SELECTOR = '#container'; + + const initCallbackTesting = async () => { + await CallbackTestHelper.initClientTesting([ + 'cellFocusChanging', + 'cellFocusChanged', + 'rowFocusChanging', + 'rowFocusChanged', + ]); + }; + + const clearCallbackTesting = async () => { + await CallbackTestHelper.clearClientData([ + 'cellFocusChanging', + 'cellFocusChanged', + 'rowFocusChanging', + 'rowFocusChanged', + ]); + }; + + const collectEventsCallbackResults = async () => [ + await CallbackTestHelper.getClientResults('cellFocusChanging'), + await CallbackTestHelper.getClientResults('cellFocusChanged'), + await CallbackTestHelper.getClientResults('rowFocusChanging'), + await CallbackTestHelper.getClientResults('rowFocusChanged'), + ]; + + const getGridDataConfig = (size: number) => ({ + keyExpr: 'id', + dataSource: new Array(size).fill(null).map((_, idx) => ({ + id: idx, + dataA: `dataA_${idx}`, + dataB: `dataB_${idx}`, + dataC: `dataC_${idx}`, + })), + columns: ['dataA', 'dataB', 'dataC'], + }); + + const getGridEventsConfig = () => ({ + onFocusedCellChanging: ({ + prevRowIndex, + prevColumnIndex, + newRowIndex, + newColumnIndex, + }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('cellFocusChanging', [ + [prevRowIndex, prevColumnIndex], [newRowIndex, newColumnIndex], + ]); + }, + onFocusedCellChanged: ({ rowIndex, columnIndex }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('cellFocusChanged', [rowIndex, columnIndex]); + }, + onFocusedRowChanging: ({ prevRowIndex, newRowIndex }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('rowFocusChanging', [prevRowIndex, newRowIndex]); + }, + onFocusedRowChanged: ({ rowIndex }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('rowFocusChanged', [rowIndex]); + }, + }); + + test('It should fire events after new rows were added', async ({ page }) => { + await initCallbackTesting(); + await createWidget(page, 'dxDataGrid', { + focusedRowEnabled: true, + editing: { + mode: 'batch', + allowAdding: true, + allowUpdating: true, + }, + ...getGridDataConfig(4), + ...getGridEventsConfig(), + }); + + const expectedCellFocusChanging: FocusCellChangingData[] = [ + [[-1, -1], [0, 0]], [[1, 0], [0, 0]], [[1, 0], [0, 0]], + ]; + const expectedCellFocusChanged: FocusCellChangedData[] = [ + [0, 0], [0, 0], [0, 0], + ]; + const expectedRowFocusChanging: FocusRowChangingData[] = [ + [-1, 0], [1, 0], [1, 0], + ]; + const expectedRowFocusChanged: FocusRowChangedData[] = [ + [0], [1], [0], [1], [0], + ]; + + const addRowBtn = page.locator('.dx-toolbar').getItem(); + + await (addRowBtn).click() + .click(addRowBtn) + .click(addRowBtn); + + const [ + cellFocusChanging, + cellFocusChanged, + rowFocusChanging, + rowFocusChanged, + ] = await collectEventsCallbackResults(); + + expect(await cellFocusChanging); + await t.eql(expectedCellFocusChanging); + expect(await cellFocusChanged); + await t.eql(expectedCellFocusChanged); + expect(await rowFocusChanging); + await t.eql(expectedRowFocusChanging); + expect(await rowFocusChanged); + await t.eql(expectedRowFocusChanged); + }); + // TODO: .after() block removed +}); 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..d89f73cd6641 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts @@ -0,0 +1,100 @@ +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('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 OVERLAY_SELECTOR = '.dx-overlay-wrapper'; + + const createDataGrid = () => 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(); + + const editorTextCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const lookupCell = page.locator('.dx-data-row').nth(0).locator('td').nth(2).locator('.dx-editor-cell'); + + await (lookupCell.element).click(); + + const list = new List(OVERLAY_SELECTOR); + const item = list.getItem(2); + + await (item.element).click(); + await (editorTextCell.element).click(); + + await testScreenshot(page, 'focus-edit-cell_after-lookup-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..1a7e47669256 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts @@ -0,0 +1,46 @@ +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('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; + }, + }); + + expect(await ClientFunction(() => (window as any).onFocusedRowChangedCounter)()).toBe(1); + + await ClientFunction(() => (window as any).widget.option('focusedRowKey', 'Ben'))(); + + expect(await dataGrid.getFocusedRow().exists).toBeTruthy(); + expect(await ClientFunction(() => (window as any).onFocusedRowChangedCounter)()).toBe(2); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/markup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/markup.spec.ts new file mode 100644 index 000000000000..be69b22ffb1e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/markup.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Focused row - 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'); + }); + // TODO: Enable multi-theming testcafe run in the future. + // visual: generic.light + // visual: material.blue.light + + test('markup - generic.light', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + focusedRowEnabled: true, + editing: { + mode: 'batch', + allowUpdating: true, + }, + dataSource: [{ + id: 0, + dataA: 'dataA_1', + dataB: 'dataB_1', + dataC: 'dataC_1', + }, { + id: 1, + dataA: 'dataA_2', + dataB: 'dataB_2', + dataC: 'dataC_2', + }], + columns: [{ + dataField: 'dataA', + validationRules: [{ type: 'required' }], + }, { + dataField: 'dataB', + validationRules: [{ type: 'required' }], + }, { + dataField: 'dataC', + validationRules: [{ type: 'required' }], + }], + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const secondCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const thirdCell = page.locator('.dx-data-row').nth(0).locator('td').nth(2); + + await (firstCell.element).click(); + await (firstCell.locator('.dx-editor-cell')).fill('TEST'); + + await (secondCell.element).click(); + await (secondCell.locator('.dx-editor-cell')).fill(' '); + + await (thirdCell.element).click(); + + await testScreenshot(page, 'focused-row_markup.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts new file mode 100644 index 000000000000..c4e78379f67c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts @@ -0,0 +1,130 @@ +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('Grouping Panel - One group on different pages', () => { + 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 endsOnNextPageApiMock = RequestMock() + .onRequestTo(/\/api\/data\?skip=0&take=5/) + .respond( + { + data: [ + { key: 'KeyA', items: null, count: 6 }, + { key: 'KeyB', items: null, count: 3 }, + ], + groupCount: 2, + totalCount: 11, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?skip=0&take=1/) + .respond( + { + data: [ + { key: 'KeyA', items: null, count: 6 }, + ], + groupCount: 2, + totalCount: 11, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?skip=0&take=3/) + .respond( + { + data: [ + { key: 'KeyA', items: null, count: 6 }, + { key: 'KeyB', items: null, count: 3 }, + ], + groupCount: 2, + totalCount: 11, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?take=4.*&filter=.*KeyA/) + .respond( + { + data: [ + { id: 0, data: 'A' }, + { id: 1, data: 'B' }, + { id: 2, data: 'C' }, + { id: 3, data: 'D' }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?skip=4.*&filter=.*KeyA/) + .respond( + { + data: [ + { id: 4, data: 'E' }, + { id: 5, data: 'F' }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ); + + test('Group panel restored from cache and ends at the next page', async ({ page }) => { + await t.addRequestHooks(endsOnNextPageApiMock); + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + columns: [ + 'data', + { + dataField: 'key', + groupIndex: 0, + }, + ], + groupPanel: { + visible: true, + }, + grouping: { + autoExpandAll: false, + }, + remoteOperations: { + groupPaging: true, + }, + pager: { + visible: true, + showInfo: true, + showPageSizeSelector: true, + allowedPageSizes: [5], + displayMode: 'full', + }, + paging: { + pageSize: 5, + }, + })); + + await (page.locator('.dx-group-row').nth(0).getCell(0).element).click(); + await testScreenshot(page, 'group-panel_loaded_first-page.png'); + + await (page.locator('.dx-pager').locator('.dx-page').filter({hasText: '2'}).element).click(); + await testScreenshot(page, 'group-panel_loaded_second-page.png'); + + await (page.locator('.dx-pager').locator('.dx-page').filter({hasText: '1'}).element).click(); + await testScreenshot(page, 'group-panel_restored_first-page.png'); + + await (page.locator('.dx-pager').locator('.dx-page').filter({hasText: '2'}).element).click(); + await testScreenshot(page, 'group-panel_restored_second-page.png'); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts new file mode 100644 index 000000000000..1cb0ca9307c2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts @@ -0,0 +1,224 @@ +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('Grouping API - calculateGroupValue runtime changes', () => { + 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( + 'One group: should expand grouped section after calculateGroupValue update', + async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0, A: 0, B: 'B_0' }, + { id: 1, A: 1, B: 'B_1' }, + { id: 2, A: 2, B: 'B_2' }, + { id: 3, A: 3, B: 'B_3' }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'A', sortOrder: 'desc' }, + 'B', + ], + sorting: { mode: 'single' }, + }); + + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0) + .getExpandCell()).click(); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(4); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0, A: 'A_0', group: 'A' }, + { id: 1, A: 'A_1', group: 'A' }, + { id: 2, A: 'A_2', group: 'B' }, + { id: 3, A: 'A_3', group: 'B' }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + 'A', + ], + grouping: { autoExpandAll: false }, + })); + + // NOTE: Intersection with "column configuration from first data source item" feature + // Because one of first item's fields is null and different logic is applied + test( + 'One group: should expand grouped section after calculateGroupValue update if first record contains null value', + async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0) + .getExpandCell()).click(); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(4); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0, A: 'A_0', group: 'A' }, + { id: 1, A: 'A_1', group: 'A' }, + { id: 2, A: 'A_2', group: 'B' }, + { id: 3, A: 'A_3', group: 'B' }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + 'A', + ], + grouping: { autoExpandAll: false }, + })); + + test( + 'Multiple groups: should expand grouped section after calculateGroupValue update', + async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0) + .getExpandCell()).click(); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(5); + expect(await dataGrid.dataRows.count); + await t.eql(0); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 0, A: 'A_0', B: 'B_0', group: 'A', + }, + { + id: 1, A: 'A_1', B: 'B_1', group: 'A', + }, + { + id: 2, A: 'A_2', B: 'B_2', group: 'B', + }, + { + id: 3, A: 'A_3', B: 'B_3', group: 'B', + }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + { dataField: 'A', groupIndex: 1 }, + 'B', + ], + grouping: { autoExpandAll: false }, + })); + + // NOTE: Intersection with "column configuration from first data source item" feature + // Because one of first item's fields is null and different logic is applied + test( + 'Multiple groups: should expand grouped section after calculateGroupValue update if first record contains null value [T1281192]', + async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0) + .getExpandCell()).click(); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(5); + expect(await dataGrid.dataRows.count); + await t.eql(0); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 0, A: 'A_0', B: null, group: 'A', + }, + { + id: 1, A: 'A_1', B: 'B_1', group: 'A', + }, + { + id: 2, A: 'A_2', B: 'B_2', group: 'B', + }, + { + id: 3, A: 'A_3', B: 'B_3', group: 'B', + }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + { dataField: 'A', groupIndex: 1 }, + 'B', + ], + grouping: { autoExpandAll: false }, + })); + + test('Should not reset sorting parameters after calculateGroupValue update [T1298901]', async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + + expect(await dataGrid.apiColumnOption('A', 'sortOrder')); + await t.eql('desc'); + expect(await dataGrid.apiColumnOption('A', 'sortIndex')); + await t.eql(0); + + await dataGrid.apiColumnOption('A', 'calculateGroupValue', () => 'ALL'); + + expect(await dataGrid.apiColumnOption('A', 'sortOrder')); + await t.eql('desc'); + expect(await dataGrid.apiColumnOption('A', 'sortIndex')); + await t.eql(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/grouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/grouping.spec.ts new file mode 100644 index 000000000000..b168265e6626 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/grouping.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('Grouping Panel', () => { + 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('Grouping Panel label should not overflow in a narrow grid (T1103925)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: { + store: [ + { + field1: '1', field2: '2', field3: '3', field4: '4', field5: '5', + }, + { + field1: '11', field2: '22', field3: '33', field4: '44', field5: '55', + }], + }, + width: 200, + groupPanel: { + emptyPanelText: 'Long long long long long long long long long long long text', + visible: true, + }, + editing: { allowAdding: true, mode: 'batch' }, + columnChooser: { + enabled: true, + }, + }); + + await testScreenshot(page, 'groupingPanel.png', { element: page.locator('.dx-toolbar') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts new file mode 100644 index 000000000000..3efcd29149d6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.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')}`; + +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('Header Filter', () => { + 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('Data should be filtered if (Blank) is selected in the header filter (T1257261)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Text: 'Item 1' }, + { ID: 2, Text: '' }, + { ID: 3, Text: 'Item 3' }, + ], + keyExpr: 'ID', + showBorders: true, + remoteOperations: true, + headerFilter: { visible: true }, + filterRow: { visible: true }, + filterPanel: { visible: true }, + }); + + const result: string[] = []; + const headerCell = page.locator('.dx-header-row').nth(0).locator('td').nth(1); + const dataCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const filterIconElement = headerCell.getFilterIcon(); + const headerFilter = new HeaderFilter(); + const buttons = headerFilter.getButtons(); + const list = headerFilter.getList(); + + await (filterIconElement).click(); + await (list.getItem(1).element).click() // Select second item with value 'Item 1'; + await (buttons.nth(0)).click(); // Click OK; + + result[0] = await dataCell.element().innerText; + + await (filterIconElement).click(); + await (list.getItem(1).element).click() // Deselect second item with value 'Item 1'; + await (list.getItem(0).element).click() // Select second item with value '(Blanks)'; + await (buttons.nth(0)).click(); // Click OK; + + result[1] = await dataCell.element().innerText; + + expect(await result[0]).toBe('1') + .expect(result[1]).toBe('2'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilterList.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilterList.spec.ts new file mode 100644 index 000000000000..a387ee33d47e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilterList.spec.ts @@ -0,0 +1,56 @@ +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('Header Filter - dxList integration', () => { + 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 openHeaderFilterAndGetList = async (t: TestController, dataGrid: DataGrid) => { + const headerCell = dataGrid.getHeaders() + .getHeaderRow(0) + .locator('td').nth(0); + const filterIconElement = headerCell.getFilterIcon(); + const headerFilter = new HeaderFilter(); + const list = headerFilter.getList(); + const firstListItem = list.getItem(0); + const secondListItem = list.getItem(1); + + await t + .click(filterIconElement); + + return { list, firstListItem, secondListItem }; + }; + + test('Should has unchecked "Select all" checkbox state if no values is selected', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0 }, + { id: 1 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', filterValues: [] }, + ], + headerFilter: { visible: true }, + }); + + const { list, firstListItem, secondListItem } = await openHeaderFilterAndGetList(t, dataGrid); + + expect(await list.selectAll.checkBox.getCheckBoxState()); + await t.eql('unchecked'); + expect(await firstListItem.isSelected); + await t.notOk(); + expect(await secondListItem.isSelected); + await t.notOk(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts new file mode 100644 index 000000000000..ee48bcf939ce --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts @@ -0,0 +1,91 @@ +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 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('Header Panel', () => { + 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('Drop-down window should be positioned correctly after resizing the toolbar (T1037975)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Name: 'Name 1', Category: 'Category 1' }, + { ID: 2, Name: 'Name 2', Category: 'Category 1' }, + ], + keyExpr: 'ID', + columns: ['ID', { dataField: 'Name', groupIndex: 0 }, 'Category'], + toolbar: { + items: [ + { + location: 'before', + locateInMenu: 'always', + widget: 'dxSelectBox', + options: { + width: 200, + items: ['Name', 'Category'], + value: 'Name', + onValueChanged(e) { + const gridInstance = ($('#container') as any).dxDataGrid('instance'); + gridInstance.clearGrouping(); + gridInstance.columnOption(e.value, 'groupIndex', 0); + }, + }, + }, + ], + }, + }); + + const headerPanel = page.locator('.dx-datagrid-header-panel'); + + // act + await (headerPanel.locator('.dx-dropdownmenu-button')).click(); + + // assert + const selectPopup = headerPanel.getDropDownSelectPopup(); + const popupContent = selectPopup.menuContent(); + + expect(await popupContent.exists); + await t.ok(); + expect(await popupContent.visible); + await t.ok(); + + // act + await (selectPopup.editButton()).click(); + + // assert + const menuItem = selectPopup.getSelectItem(1); + + expect(await menuItem.exists); + await t.ok(); + expect(await menuItem.visible); + await t.ok(); + + // act + await (menuItem).click(); + + const visibleRows = await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').getVisibleRows()); + + // assert + expect(await visibleRows.length); + await t.eql(3); + + await testScreenshot(page, 'grid-toolbar-dropdown-menu.png', { element: 'body' }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts new file mode 100644 index 000000000000..acf25b17021d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.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')}`; + +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 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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - Column Reordering` + .page(url(__dirname, '../../../container.html')); + + // Regular columns + [true, false].forEach((rtlEnabled) => { + + test(`reorder column when ${rtlEnabled ? 'left' : 'right'} arrow is pressed when rtlEnabled = ${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + rtlEnabled, + allowColumnReordering: true, + dataSource: [{ + field1: 'test1', + field2: 'test2', + field3: 'test3', + field4: 'test4', + }], + }); + + const firstHeaderCell = page.locator('.dx-header-row').nth(0).locator('td').nth(0); + const shortcut = rtlEnabled ? 'ctrl+left' : 'ctrl+right'; + + await (firstHeaderCell.element).click(); + await t.pressKey(shortcut); + + await testScreenshot(page, `reorder_column_to_${rtlEnabled ? 'left' : 'right'}_when_rtlEnabled_=_${rtlEnabled}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/customButtons.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/customButtons.functional.spec.ts new file mode 100644 index 000000000000..e22b4425866b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/customButtons.functional.spec.ts @@ -0,0 +1,75 @@ +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('DataGrid 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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - custom buttons` + .page(url(__dirname, '../../../container.html')); + + const createDataGrid = async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 1, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + 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 createDataGrid(); + + const expectedFocusedCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const cellToStartNavigation = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + await (cellToStartNavigation.element).click(); + await page.keyboard.press('tab'); + expect(await expectedFocusedCell.isFocused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/editOnKeyPress.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/editOnKeyPress.spec.ts new file mode 100644 index 000000000000..336580cb15ab --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/editOnKeyPress.spec.ts @@ -0,0 +1,64 @@ +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('DataGrid 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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - editOnKeyPress` + .page(url(__dirname, '../../../container.html')); + + [ + { name: 'input', template: () => $('') }, + { name: 'div', template: () => $('
').text('Hi, I\'m the template!') }, + ].forEach(({ name, template }) => { + + test(`should render edit cell template without errors, template: ${name}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + data_A: 'data_A', + data_B: 'data_B', + }, + ], + columns: [ + { + dataField: 'data_A', + editCellTemplate: template, + }, + 'data_B', + ], + keyboardNavigation: { + enabled: true, + editOnKeyPress: true, + enterKeyDirection: 'column', + }, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + startEditAction: 'dblClick', + }, + // @ts-expect-error private option + templatesRenderAsynchronously: true, + }); + await makeRowsViewTemplatesAsync(DATA_GRID_SELECTOR); + + const dataCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + + await (dataCell.element).click() + .pressKey('f'); + + await testScreenshot(page, `edit-cell-keypress-with-custom-cell-template_template-${name}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.functional.spec.ts new file mode 100644 index 000000000000..02bb4fdc44ee --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.functional.spec.ts @@ -0,0 +1,53 @@ +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('DataGrid 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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - Column Reordering` + .page(url(__dirname, '../../../container.html')); + + const triggerVisibilityChange = ClientFunction(() => { + document.dispatchEvent(new Event('visibilitychange')); + }); + + test('The column should be grouped when pressing Ctrl + G if grouping.contextMenuEnabled is false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 550, + columnWidth: 100, + grouping: { + contextMenuEnabled: false, + }, + groupPanel: { + visible: true, + }, + dataSource: [{ + field1: 'test1', + field2: 'test2', + field3: 'test3', + field4: 'test4', + }], + }); + + const firstVisibleHeader = page.locator('.dx-header-row').nth(0).locator('td').nth(0); + + await (firstVisibleHeader.element).click(); + await page.keyboard.press('ctrl+g'); + + expect(await dataGrid.getGroupPanel().getHeadersCount()); + await t.eql(1); + expect(await page.locator('.dx-header-row').nth(0).locator('td').nth(1).isFocused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.visual.spec.ts new file mode 100644 index 000000000000..b345bf201a4f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.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('DataGrid 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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - Group Column Reordering` + .page(url(__dirname, '../../../container.html')); + + // Move grouped columns + [true, false].forEach((rtlEnabled) => { + + test(`reorder group column when ${rtlEnabled ? 'left' : 'right'} arrow is pressed when rtlEnabled = ${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + rtlEnabled, + dataSource: [{ + field1: 'test1', + field2: 'test2', + field3: 'test3', + field4: 'test4', + }], + groupPanel: { + visible: true, + }, + columns: [ + { + dataField: 'field1', + groupIndex: 1, + }, + 'field2', + 'field3', + { + dataField: 'field4', + groupIndex: 0, + }, + ], + }); + + const firstGroupHeader = dataGrid.getGroupPanel().getHeader(0); + const shortcut = rtlEnabled ? 'ctrl+left' : 'ctrl+right'; + + await (firstGroupHeader.element).click(); + await t.pressKey(shortcut); + + await testScreenshot(page, `reorder_group_column_to_${rtlEnabled ? 'left' : 'right'}_when_rtlEnabled_=_${rtlEnabled}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts new file mode 100644 index 000000000000..9fdae0d9cf9a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts @@ -0,0 +1,137 @@ +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 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('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'); + }); + + const CLASS = ClassNames; + + const getOnKeyDownCallCount = ClientFunction(() => (window as any).onKeyDownCallCount); + + ); + + test('Changing keyboardNavigation options should not invalidate the entire content (T1197829)', async ({ page }) => { + await page.evaluate(() => { + (window as any).invalidateCounter = 0; + (window as any).renderTableCounter = 0; + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(5)].map((_, index) => ({ id: index, text: `item ${index}` })), + keyExpr: 'id', + columns: [ + { dataField: 'id' }, + { dataField: 'text' }, + ], + focusedRowEnabled: true, + keyboardNavigation: { + editOnKeyPress: true, + enterKeyAction: 'startEdit', + enterKeyDirection: 'column', + }, + editing: { + mode: 'cell', + allowUpdating: true, + }, + onFocusedRowChanging(e) { + if ((e.newRowIndex + 1) % 2 === 0) { + e.component.option('keyboardNavigation.enterKeyAction', 'moveFocus'); + } else { + e.component.option('keyboardNavigation.enterKeyAction', 'startEdit'); + } + }, + onInitialized(e) { + const dataGrid: any = e.component; + const rowsView = dataGrid.getView('rowsView'); + // eslint-disable-next-line no-underscore-dangle + const defaultInvalidate = rowsView._invalidate; + // eslint-disable-next-line no-underscore-dangle + dataGrid.getView('rowsView')._invalidate = (...args) => { + ((window as any).invalidateCounter as number) += 1; + return defaultInvalidate.apply(rowsView, args); + }; + + // eslint-disable-next-line no-underscore-dangle + const defaultRenderTable = rowsView._renderTable; + // eslint-disable-next-line no-underscore-dangle + dataGrid.getView('rowsView')._renderTable = (...args) => { + ((window as any).renderTableCounter as number) += 1; + return defaultRenderTable.apply(rowsView, args); + }; + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + expect(await ClientFunction(() => (window as any).invalidateCounter)()); + await t.eql(0); + expect(await ClientFunction(() => (window as any).renderTableCounter)()); + await t.eql(2); + // test enter key behavior + .click(page.locator('.dx-data-row').nth(1).locator('td').nth(1)) // set initial focus + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isEditCell) + .ok() + .pressKey('enter') // move focus to next cell + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(1).isEditCell) + .notOk() + .pressKey('enter') // switch cell to the editing state + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(1).isEditCell) + .ok() + .pressKey('enter') // move focus to next cell + .expect(page.locator('.dx-data-row').nth(3).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(3).locator('td').nth(1).isEditCell) + .notOk() + .pressKey('enter') // move focus to next cell again + .expect(page.locator('.dx-data-row').nth(4).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(4).locator('td').nth(1).isEditCell) + .notOk() + .pressKey('enter') // switch cell to the editing state + .expect(page.locator('.dx-data-row').nth(4).locator('td').nth(1).isEditCell) + .ok() + + // test tab key behavior + .click(page.locator('.dx-data-row').nth(1).locator('td').nth(1)) // set initial focus + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isEditCell) + .ok() + .pressKey('tab') + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(0).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(0).isEditCell) + .ok() + + .expect(ClientFunction(() => (window as any).invalidateCounter)()) + .eql(0) + .expect(ClientFunction(() => (window as any).renderTableCounter)()) + .eql(9); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts new file mode 100644 index 000000000000..c6a8d9cd4ead --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.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')}`; + +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('Keyboard Navigation.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'); + }); + // Quick navigation through grid cells via Home and End keys + + test('Focus the last cell in the row that contains focus when pressing the End key', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 7), + columnWidth: 100, + height: 500, + width: 800, + showBorders: true, + scrolling: { + showScrollbar: 'never', + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // act + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); + await page.keyboard.press('end'); + + await testScreenshot(page, 'focus_last_cell_in_row_that_contains_focus_when_pressing_End_key.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/markup.screenshots.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/markup.screenshots.spec.ts new file mode 100644 index 000000000000..2f24277814cd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/markup.screenshots.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('DataGrid 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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - screenshots` + .page(url(__dirname, '../../../container.html')); + + test('Focused cells should look correctly', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 1, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + columns: ['id', 'columnA', 'columnB'], + sorting: { + mode: 'none', + }, + }); + + const headerCellToFocus = page.locator('.dx-header-row').nth(0).locator('td').nth(0); + const dataCellToFocus = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + + await (headerCellToFocus.element).click() + .pressKey('tab'); + await testScreenshot(page, 'data-grid_keyboard-navigation-header-cell-focused.png'); + + await (dataCellToFocus.element).click() + .pressKey('tab'); + await testScreenshot(page, 'data-grid_keyboard-navigation-data-cell-focused.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts new file mode 100644 index 000000000000..2d5f3dffac02 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts @@ -0,0 +1,35 @@ +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('DataGrid 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'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - screenshots` + .page(url(__dirname, '../../../../container.html')); + + test('Focus goes inside master detail on tab', async ({ page }) => { + await createWidget(page, 'dxDataGrid', gridOptions); + + await (page.locator('.dx-data-row').nth(0).locator('.dx-command-edit').nth(0),).click(); + + const innerDataGrid = new DataGrid(page.locator('.dx-master-detail-row').nth(0).element.find('.dx-datagrid').parent()); + + await (page.locator('.dx-data-row').nth(0).locator('td').nth(4),).click() + .pressKey('tab') + .pressKey('tab'); + + expect(await innerDataGrid.getHeaders().getHeaderRow(0).locator('td').nth(0).isFocused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/skipDragCell.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/skipDragCell.functional.spec.ts new file mode 100644 index 000000000000..7a4c153713c4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/skipDragCell.functional.spec.ts @@ -0,0 +1,75 @@ +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('DataGrid 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'); + }); + + // T1147695 + fixture + .disablePageReloads`Keyboard Navigation - skip drag cell` + .page(url(__dirname, '../../../container.html')); + + const DATA_SOURCE = [ + { + id: 1, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + columnA: 'A_2', + columnB: 'B_2', + }, + ]; + const createDataGrid = async () => createWidget(page, 'dxDataGrid', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + columns: ['id', 'columnA', 'columnB'], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + }); + + const createDataGridRenderAsyncWithButtons = async () => createWidget(page, 'dxDataGrid', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + 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 createDataGrid(); + + const expectedFocusedCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const cellToStartNavigation = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + await (cellToStartNavigation.element).click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts new file mode 100644 index 000000000000..bd2ab3407dfa --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts @@ -0,0 +1,44 @@ +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('Keyboard Navigation - editOnKeyPress', () => { + 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('Editing should start by pressing enter after scrolling content with scrolling.mode=virtual', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(50)].map((_, i) => ({ + data1: i * 2, + data2: i * 2 + 1, + })), + columns: [ + 'data1', + 'data2', + ], + editing: { + allowUpdating: true, + }, + scrolling: { + mode: 'virtual', + }, + height: 300, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollBy(opts), { y: 10000 }); + + await (page.locator('.dx-data-row').nth(49).locator('td').nth(1)).click(); + await page.keyboard.press('enter'); + + expect(await page.locator('.dx-data-row').nth(49).locator('td').nth(1).locator('.dx-editor-cell').focused).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts new file mode 100644 index 000000000000..5c855777c87e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.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('Virtual 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'); + }); + const generateData = (rowCount: number, columnCount: number): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + test('DataGrid should scroll to the first cell of the next row and focus it when navigating with Tab key', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 500, + dataSource: generateData(10, 20), + columnWidth: 100, + scrolling: { + columnRenderingMode: 'virtual', + }, + }); + + // arrange + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + // assert + expect(await dataGrid.getScrollLeft()); + await t.eql(1500); + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(19).exists); + await t.ok(); + + // act + await (page.locator('.dx-data-row').nth(0).locator('td').nth(19)).click(); + + // assert + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(19).focused); + await t.ok(); + + // act + await page.keyboard.press('tab'); + + // assert + expect(await dataGrid.getScrollLeft()); + await t.eql(0); + expect(await page.locator('.dx-data-row').nth(1).locator('td').nth(0).focused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1163515_alternateRowGroupBorders.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1163515_alternateRowGroupBorders.spec.ts new file mode 100644 index 000000000000..76006aeab826 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1163515_alternateRowGroupBorders.spec.ts @@ -0,0 +1,372 @@ +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('Grouping Panel - check borders and backgrounds with various options', () => { + 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'); + }); + interface MatrixOptions { + rowAlternationEnabled: boolean; + showColumnLines: boolean; + showRowLines: boolean; + showBorders: boolean; + hasFixedColumn: boolean; + hasMasterDetail: boolean; + } + + const SELECTORS = { + gridContainer: 'container', + masterDetailRowClass: 'dx-master-detail-row', + groupRowClass: 'dx-group-row', + rowLinesClass: 'dx-row-lines', + groupSpaceClass: 'dx-datagrid-group-space', + pointerEventsNoneClass: 'dx-pointer-events-none', + rowAlternativeClass: 'dx-row-alt', + }; + + const BORDER_WIDTH = { + big: 2, + normal: 1, + none: 0, + }; + + const dataSource = [ + { + group: 'A', + label: 'LABEL_A_0', + value: 'VALUE_A_0', + count: 1, + }, + { + group: 'A', + label: 'LABEL_A_1', + value: 'VALUE_A_1', + count: 2, + }, + { + group: 'B', + label: 'LABEL_B_0', + value: 'VALUE_B_0', + count: 3, + }, + { + group: 'B', + label: 'LABEL_B_1', + value: 'VALUE_B_1', + count: 4, + }, + { + group: 'B', + label: 'LABEL_B_2', + value: 'VALUE_B_2', + count: 5, + }, + { + group: 'C', + label: 'LABEL_C_0', + value: 'VALUE_C_0', + count: 6, + }, + { + group: 'C', + label: 'LABEL_C_1', + value: 'VALUE_C_1', + count: 7, + }, + ]; + + const getTestParams = ({ + rowAlternationEnabled, + showColumnLines, + showRowLines, + showBorders, + hasFixedColumn, + hasMasterDetail, + }: MatrixOptions) => [ + `rowAlternationEnabled: ${rowAlternationEnabled}`, + `showColumnLines: ${showColumnLines}`, + `showRowLines: ${showRowLines}`, + `showBorders: ${showBorders}`, + `hasFixedColumn: ${hasFixedColumn}`, + `hasMasterDetail: ${hasMasterDetail}`, + ].join(', '); + + const createDataGrid = async ({ + rowAlternationEnabled, + showColumnLines, + showRowLines, + showBorders, + hasFixedColumn, + hasMasterDetail, + }: MatrixOptions) => { + await createWidget(page, 'dxDataGrid', { + dataSource, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { + dataField: 'group', + groupIndex: 0, + }, + { + dataField: 'label', + fixed: hasFixedColumn, + }, + 'value', + 'count', + ], + summary: { + groupItems: [{ + column: 'count', + summaryType: 'sum', + }], + }, + masterDetail: hasMasterDetail + ? { + enabled: true, + autoExpandAll: true, + template: ($container) => { + $('
') + .text('MASTER DETAIL') + .appendTo($container); + }, + } + : undefined, + editing: { + mode: 'row', + allowDeleting: true, + confirmDelete: false, + }, + showBorders, + rowAlternationEnabled, + showRowLines, + showColumnLines, + }); + }; + + const checkShowBordersState = async ( + t: TestController, + dataGrid: DataGrid, + showBorders: boolean, + ) => { + const expectedBorderWidth = showBorders ? BORDER_WIDTH.normal : BORDER_WIDTH.none; + + const gridContainer = dataGrid.getContainer(); + const containerClasses = await gridContainer.getAttribute('class'); + + if (showBorders) { + await t.expect(containerClasses).contains('dx-datagrid-borders'); + } else { + await t.expect(containerClasses).notContains('dx-datagrid-borders'); + } + + const headersContainer = dataGrid.getHeadersContainer(); + + const borderTop = await headersContainer.getStyleProperty('border-top-width'); + const borderLeft = await headersContainer.getStyleProperty('border-left-width'); + const borderRight = await headersContainer.getStyleProperty('border-right-width'); + + await t.expect(parseInt(borderTop, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(borderLeft, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(borderRight, 10)).eql(expectedBorderWidth); + + const rowsView = dataGrid.getRowsView(); + + const rowsViewBorderLeft = await rowsView.getStyleProperty('border-left-width'); + const rowsViewBorderRight = await rowsView.getStyleProperty('border-right-width'); + const rowsViewBorderBottom = await rowsView.getStyleProperty('border-bottom-width'); + + await t.expect(parseInt(rowsViewBorderLeft, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(rowsViewBorderRight, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(rowsViewBorderBottom, 10)).eql(expectedBorderWidth); + }; + + const checkShowRowLinesState = async ( + t: TestController, + dataGrid: DataGrid, + showRowLines: boolean, + showBorders: boolean, + ) => { + const expectedBorderWidth = showRowLines ? BORDER_WIDTH.normal : BORDER_WIDTH.none; + /* + getRows() returns double collection of rows (two tables) when + columnFixing.legacyMode = true AND DataGrid has fixed columns + */ + const filteredRows = dataGrid.getRows().filter(`.${SELECTORS.rowLinesClass}`); + const cells = filteredRows.find('td'); + const cellsCount = await cells.count; + + for (let i = 0; i < cellsCount; i += 1) { + const dataCell = cells.nth(i); + + // Skip checking for last lines if showBorders is enabled + if (showBorders) { + const parentRow = dataCell.parent('tr'); + const nextRow = parentRow.nextSibling('tr.dx-row-lines'); + const isLastRow = await nextRow.count === 0; + + if (isLastRow) { + // eslint-disable-next-line no-continue + continue; + } + } + + const borderBottom = await dataCell.getStyleProperty('border-bottom-width'); + await t.expect(parseInt(borderBottom, 10)).eql(expectedBorderWidth); + } + }; + + const checkShowColumnLinesState = async ( + t: TestController, + dataGrid: DataGrid, + showColumnLines: boolean, + ) => { + const getExpBorderWith = ( + isColumnLinesEnabled: boolean, + hasPointerEventsNoneClass: boolean, + ) => { + if (hasPointerEventsNoneClass) { + return BORDER_WIDTH.big; + } + + if (isColumnLinesEnabled) { + return BORDER_WIDTH.normal; + } + + return BORDER_WIDTH.none; + }; + + /* + getRows() returns double collection of rows (two tables) when + columnFixing.legacyMode = true AND DataGrid has fixed columns + */ + const filteredRows = dataGrid.getRows().filter(`:not(.${SELECTORS.masterDetailRowClass})`); + const cells = filteredRows.find(`td:not(.${SELECTORS.groupSpaceClass})`); + + const cellsCount = await cells.count; + + for (let i = 0; i < cellsCount; i += 1) { + const cell = cells.nth(i); + + const parentRow = cell.parent('tr'); + const rowCells = parentRow.find(`td:not(.${SELECTORS.groupSpaceClass})`); + const rowCellsCount = await rowCells.count; + const indexInRow = await cell.prevSibling(`td:not(.${SELECTORS.groupSpaceClass})`).count; + + const isFirstCellInRow = indexInRow === 0; + const isLastCellInRow = indexInRow === rowCellsCount - 1; + + const cellClasses = await cell.getAttribute('class'); + const hasPointerEventsNoneClass = cellClasses?.includes(SELECTORS.pointerEventsNoneClass); + const expectedBorderWidth = getExpBorderWith(showColumnLines, !!hasPointerEventsNoneClass); + + if (!isFirstCellInRow) { + const borderLeftWidth = await cell.getStyleProperty('border-left-width'); + + await t.expect(parseInt(borderLeftWidth, 10)).eql(expectedBorderWidth); + } + + if (!isLastCellInRow) { + const borderRightWidth = await cell.getStyleProperty('border-right-width'); + + await t.expect(parseInt(borderRightWidth, 10)).eql(expectedBorderWidth); + } + } + }; + + const checkRowAlternationEnabledState = async ( + t: TestController, + dataGrid: DataGrid, + rowAlternationEnabled: boolean, + ) => { + /* + getRows() returns double collection of rows (two tables) when + columnFixing.legacyMode = true AND DataGrid has fixed columns + */ + const filteredRows = dataGrid.getRows().filter(`:not(.${SELECTORS.masterDetailRowClass})`); + const filteredRowsLength = await filteredRows.count; + + let i = 1; + while (i < filteredRowsLength) { + const currentRow = filteredRows.nth(i); + const previousRow = filteredRows.nth(i - 1); + + const currentClasses = await currentRow.getAttribute('class'); + const previousClasses = await previousRow.getAttribute('class'); + + if (currentClasses?.includes(SELECTORS.groupRowClass)) { + i += 2; + // eslint-disable-next-line no-continue + continue; + } + + if (previousClasses?.includes(SELECTORS.groupRowClass)) { + i += 1; + // eslint-disable-next-line no-continue + continue; + } + + const currentHasAltClass = currentClasses?.includes(SELECTORS.rowAlternativeClass); + const previousHasAltClass = previousClasses?.includes(SELECTORS.rowAlternativeClass); + + if (rowAlternationEnabled) { + await t.expect(currentHasAltClass).notEql(previousHasAltClass); + } else { + await t.expect(currentHasAltClass).eql(previousHasAltClass); + } + + const currentFirstCell = currentRow.find('td').nth(0); + const previousFirstCell = previousRow.find('td').nth(0); + + const currentFirstCellBg = await currentFirstCell.getStyleProperty('background-color'); + const previousFirstCellBg = await previousFirstCell.getStyleProperty('background-color'); + + if (rowAlternationEnabled) { + await t.expect(currentFirstCellBg).notEql(previousFirstCellBg); + } else { + await t.expect(currentFirstCellBg).eql(previousFirstCellBg); + } + + i += 1; + } + }; + + const verifyGridStyles = async (t: TestController, dataGrid: DataGrid, { + showBorders, showRowLines, rowAlternationEnabled, showColumnLines, + }: MatrixOptions) => { + await checkShowBordersState(t, dataGrid, showBorders); + await checkShowRowLinesState(t, dataGrid, showRowLines, showBorders); + await checkShowColumnLinesState(t, dataGrid, showColumnLines); + await checkRowAlternationEnabledState(t, dataGrid, rowAlternationEnabled); + }; + + const functionalTest = (matrixOptions: MatrixOptions) => { + + test(`Should have correct applied styles with ${getTestParams(matrixOptions)}`, async ({ page }) => { + await createDataGrid(matrixOptions); + + await page.locator('.dx-datagrid').first().isVisible(); + + await verifyGridStyles(t, dataGrid, matrixOptions); + + const rowIdx = matrixOptions.hasMasterDetail ? 8 : 5; + const colIdx = matrixOptions.hasMasterDetail ? 5 : 4; + const deleteBtn = matrixOptions.hasFixedColumn + ? dataGrid.getFixedDataRow(rowIdx).locator('.dx-command-edit').nth(colIdx).element + : page.locator('.dx-data-row').nth(rowIdx).locator('.dx-command-edit').nth(colIdx); + + await (deleteBtn).click(); + + await verifyGridStyles(t, dataGrid, matrixOptions); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts new file mode 100644 index 000000000000..59129a561e1d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts @@ -0,0 +1,47 @@ +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('HoveringRows', () => { + 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('Hover over rows in the middle', async ({ page }) => { + await createWidget(page, 'dxDataGrid', + { + dataSource: getData(20, 3), + hoverStateEnabled: true, + }, + ); + + const firstRow = page.locator('.dx-data-row').nth(10); + const secondRow = page.locator('.dx-data-row').nth(11); + + await (firstRow.element).hover() + .expect(firstRow.isHovered) + .ok(); + + await (secondRow.element).hover() + .expect(firstRow.isHovered) + .notOk() + .expect(secondRow.isHovered) + .ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1286265_deletedRowHeight.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1286265_deletedRowHeight.spec.ts new file mode 100644 index 000000000000..766bfe9bf37a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1286265_deletedRowHeight.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('DataGrid deleted row height consistency T1286265', () => { + 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 ROW_INDEX = 1; + + // visual: generic.light + // visual: generic.light.compact + // visual: material.blue.light + // visual: material.blue.light.compact + // visual: fluent.blue.light + // visual: fluent.blue.light.compact + + test('When DataGrid has fixed column row height should not change when marked as deleted - generic.light', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'John Smith' }, + { id: 2, name: 'Jane Johnson' }, + { id: 3, name: 'Mike Wilson' }, + ], + keyExpr: 'id', + height: 300, + showBorders: true, + showRowLines: true, + columns: [ + { dataField: 'id', width: 50, fixed: true }, + { dataField: 'name', width: 150 }, + ], + editing: { + mode: 'batch', + allowDeleting: true, + }, + }); + + // Arrange + // Get the initial height of the row at index + const initialRow = page.locator('.dx-data-row').nth(ROW_INDEX); + const initialRowHeight = await initialRow.element.clientHeight; + + // Act - mark the row as deleted + await dataGrid.apiDeleteRow(ROW_INDEX); + + // Assert - check if the row is marked as deleted + expect(await page.locator('.dx-data-row').nth(ROW_INDEX).isRemoved); + await t.ok('Row should be marked as deleted'); + + // Get the height of the deleted row + const deletedRow = page.locator('.dx-data-row').nth(ROW_INDEX); + const deletedRowHeight = await deletedRow.element.clientHeight; + + // Assert - check if the height remains consistent + expect(await deletedRowHeight); + await t.eql(initialRowHeight, 'Row height should not change when marked as deleted'); + + // Take a screenshot for visual verification + await testScreenshot(page, 'datagrid-deleted-row-height-row-lines-and-fixed-column.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T838734_alternateRowSizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T838734_alternateRowSizes.spec.ts new file mode 100644 index 000000000000..8bde81d6a672 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T838734_alternateRowSizes.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')}`; + +test.describe('Grouping Panel - Borders with enabled alternate rows', () => { + 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 generateData = (rowCount) => new Array(rowCount).fill(null).map((_, idx) => ({ + A: `A_${idx}`, + B: `B_${idx}`, + C: `C_${idx}`, + })); + + test('Alternate rows should be the same size', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: generateData(10), + columns: ['A', 'B', { + dataField: 'C', + cellTemplate: ($container, { value }) => { + const $root = $('
'); + $('
') + .text('C template') + .appendTo($root); + $('
') + .text(value) + .appendTo($root); + $root.appendTo($container); + }, + }], + onCellPrepared: ({ cellElement, value }) => { + if (typeof value === 'string' && value.startsWith('B')) { + // @ts-expect-error todo check + cellElement.html(` +
+
B template:
+
${value}
+
+ `); + } + }, + showRowLines: false, + rowAlternationEnabled: true, + }); + + await testScreenshot(page, 'T838734_alternate-rows-same-size.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/iconSizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/iconSizes.spec.ts new file mode 100644 index 000000000000..62cfd60b0183 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/iconSizes.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('Icon Sizes', () => { + 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'); + }); + // visual: fluent.blue.light.compact + + test('Correct icon sizes (T1207612)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(3)].map((_, index) => ({ id: index, text: `item ${index}`, group: `group ${index % 2}` })), + keyExpr: 'id', + width: 550, + columns: [ + { dataField: 'id' }, + { dataField: 'text', sortOrder: 'asc' }, + { dataField: 'group', groupIndex: 0 }, + { dataField: 'hidden', hidingPriority: 0 }, + ], + editing: { + allowAdding: true, + allowUpdating: true, + allowDeleting: true, + }, + showBorders: true, + filterValue: ['Id', '>=', 0], + filterPanel: { visible: true }, + headerFilter: { visible: true }, + filterRow: { visible: true }, + groupPanel: { visible: true }, + searchPanel: { visible: true }, + selection: { mode: 'multiple' }, + rowDragging: { allowReordering: true }, + columnChooser: { enabled: true }, + columnHidingEnabled: true, + masterDetail: { enabled: true }, + export: { enabled: true }, + pager: { + visible: true, + allowedPageSizes: [5, 10, 'all'], + showPageSizeSelector: true, + showInfo: true, + showNavigationButtons: true, + }, + }); + + await testScreenshot(page, 'icon-sizes.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/markup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/markup.spec.ts new file mode 100644 index 000000000000..461d7a9778b8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/markup.spec.ts @@ -0,0 +1,37 @@ +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('Icon Sizes', () => { + 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('Load panel should support string height and width', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + columns: [ + 'field1', 'field2', 'field3', + ], + width: 700, + loadPanel: { + enabled: true, + height: '400px', + width: '330px', + }, + }); + + await dataGrid.apiBeginCustomLoading('test'); + + expect(await dataGrid.getLoadPanel().getContent().getStyleProperty('height')); + await t.eql('400px'); + expect(await dataGrid.getLoadPanel().getContent().getStyleProperty('width')); + await t.eql('330px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/noDataText.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/noDataText.spec.ts new file mode 100644 index 000000000000..3036c9d0b53f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/noDataText.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('No Data', () => { + 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('The noDataText element should be rendered when a lookup column is filtered (T1293839)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Name: 'John', Lookup: 1 }, + { ID: 2, Name: 'Jane', Lookup: 2 }, + ], + keyExpr: 'ID', + columns: ['Name', { + dataField: 'Lookup', + lookup: { + dataSource: [ + { ID: 1, Text: 'Item 1' }, + { ID: 2, Text: 'Item 2' }, + ], + valueExpr: 'ID', + displayExpr: 'Text', + }, + }], + showBorders: true, + filterRow: { visible: true }, + onEditorPreparing(e) { + e.updateValueTimeout = 0; + }, + }); + + // arrange + const nameFilterInput = page.locator('.dx-datagrid-filter-row td').nth(0).getEditorInput().element; + const lookupFilterEditor = dataGrid.getFilterEditor(1, SelectBox); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // act + await (lookupFilterEditor.element).click(); + + // assert + expect(await lookupFilterEditor.isVisible()()).toBeTruthy(); + + // act + const lookupList = await lookupFilterEditor.getList(); + const lookupItem = lookupList.getItem(1); + await (lookupItem.element).click(); + await (nameFilterInput).fill('test'); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + await testScreenshot(page, 'T1293839-grid-no-data-text-rendered.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/masterDetail.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/masterDetail.spec.ts new file mode 100644 index 000000000000..d01975ddb5b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/masterDetail.spec.ts @@ -0,0 +1,77 @@ +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('Master detail', () => { + 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'); + }); + // visual: material.blue.light + // visual: generic.light + + test('Checkbox align right in masterdetail (T1045321) generic.light', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Prefix: 'Mr.', + }], + keyExpr: 'ID', + showBorders: true, + selection: { + mode: 'multiple', + }, + columns: [ + { + dataField: 'Prefix', + caption: 'Title', + width: 400, + }, + ], + masterDetail: { + autoExpandAll: true, + enabled: true, + template(container) { + ($('
') as any) + .dxTreeList({ + columnAutoWidth: true, + showBorders: true, + selection: { + mode: 'multiple', + }, + dataSource: [{ + ID: 1, + Title: 'CEO', + Hire_Date: '1995-01-15', + }], + rootValue: -1, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + columns: [ + { + dataField: 'Title', + caption: 'Position', + width: 200, + }, + { + dataField: 'Hire_Date', + dataType: 'date', + width: 200, + }, + ], + showRowLines: true, + }) + .appendTo(container); + }, + }, + }); + + await testScreenshot(page, 'T1045321.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts new file mode 100644 index 000000000000..2a3f9a1ca77a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.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('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'); + }); + + async function createDataGridWithPager(page: any): Promise { + const dataSource = Array.from({ length: 100 }, (_, room) => ({ name: 'Alex', phone: '555555', room })); + return createWidget(page, 'dxDataGrid', { + dataSource, + columns: ['name', 'phone', 'room'], + paging: { + pageSize: 5, + pageIndex: 5, + }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [5, 10, 20], + showInfo: true, + showNavigationButtons: true, + }, + }); + } + ); + + test('Full size pager', async ({ page }) => { + await createDataGridWithPager(); + + const pager = page.locator('.dx-pager'); + await page.setViewportSize({ width: 900, height: 600 }); + expect(await pager.locator('.dx-page-size').nth(0).evaluate(el => el.classList.contains('dx-selection'))); + await t.ok('page size 5 selected'); + expect(await pager.locator('.dx-page').filter({hasText: '6'}).evaluate(el => el.classList.contains('dx-selection'))); + await t.ok('page 6 selected'); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 6 of 20 (100 items)'); + expect(await page.locator('.dx-data-row').nth(29).locator('td').nth(2).textContent()); + await t.eql('29'); + // set page sige to 10 + await (pager.locator('.dx-page-size').nth(1).element).click(); + expect(await page.locator('.dx-data-row').nth(10 * 6 - 1).locator('td').nth(2).textContent()); + await t.eql('59'); + // set page index 7 + await (pager.locator('.dx-page').filter({hasText: '7'}).element).click(); + expect(await page.locator('.dx-data-row').nth(10 * 7 - 1).locator('td').nth(2).textContent()); + await t.eql('69'); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 7 of 10 (100 items)'); + // navigate to prev page (6) + await (pager.locator('.dx-navigate-button.dx-prev-button').element).click(); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 6 of 10 (100 items)'); + // navigate to next page (7) + await (pager.locator('.dx-navigate-button.dx-next-button').element).click(); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 7 of 10 (100 items)'); + + await testScreenshot(page, 'pager-full-allpages.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/functional.spec.ts new file mode 100644 index 000000000000..fc0fb552273f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/functional.spec.ts @@ -0,0 +1,160 @@ +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('Row dragging.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'); + }); + + /* eslint-disable @typescript-eslint/no-misused-promises */ + + const CLASS = { ...DataGridClassNames, ...ClassNames }; + + const isPlaceholderVisible = ClientFunction(() => $(`.${CLASS.sortablePlaceholder}`).is(':visible'), { dependencies: { CLASS } }); + + const getPlaceholderOffset = ClientFunction(() => $(`.${CLASS.sortablePlaceholder}`).offset(), { dependencies: { CLASS } }); + + const getRowsViewLeftOffset = ClientFunction(() => $(`#container .${CLASS.dataGridRowsView}`).offset()?.left, { dependencies: { CLASS } }); + + const getDraggingElementLeftOffset = ClientFunction(() => $(`.${CLASS.sortableDragging}`).offset()?.left, { dependencies: { CLASS } }); + + const getDraggingElementScrollPosition = ClientFunction(() => { + const $dataGrid = $(`.${CLASS.sortableDragging}`).find(`.${CLASS.dataGrid}`).first().parent(); + const dataGridInstance = $dataGrid.data('dxDataGrid'); + const scrollableInstance = dataGridInstance.getScrollable(); + + return { + left: scrollableInstance.scrollLeft(), + top: scrollableInstance.scrollTop(), + }; + }, { dependencies: { CLASS } }); + + const getFreeSpaceRowOffset = ClientFunction(() => { + const $freeSpaceRow = $('#container').find(`.${CLASS.dataGridRowsView} table .${CLASS.freeSpaceRow}`).first(); + + return $freeSpaceRow?.offset(); + }, { dependencies: { CLASS } }); + + const scrollTo = ClientFunction((x, y) => { + window.scrollTo(x, y); + }); + + const generateData = (rowCount, columnCount): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + ); + + // T903351 + + test('The placeholder should appear when a cross-component dragging rows after scrolling the window', async ({ page }) => { + await t.maximizeWindow(); + await page.evaluate(() => { + $('body').css('display', 'flex'); + $('#container, #otherContainer').css({ + display: 'inline-block', + 'margin-top': '800px', + width: '50%', + }); + }); + + return Promise.all([ + createWidget(page, 'dxDataGrid', { + width: 400, + dataSource: [ + { + id: 1, name: 'Name 1', age: 19, + }, + { + id: 2, name: 'Name 2', age: 11, + }, + { + id: 3, name: 'Name 3', age: 15, + }, + { + id: 4, name: 'Name 4', age: 16, + }, + { + id: 5, name: 'Name 5', age: 25, + }, + { + id: 6, name: 'Name 6', age: 18, + }, + { + id: 7, name: 'Name 7', age: 21, + }, + { + id: 8, name: 'Name 8', age: 14, + }, + ], + columns: ['name', 'age'], + rowDragging: { + group: 'shared', + }, + }), + createWidget(page, 'dxTreeList', { + columnAutoWidth: true, + 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, + }, + ], + autoExpandAll: true, + columns: ['name', 'age'], + rowDragging: { + group: 'shared', + }, + }, '#otherContainer'), + ]); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + await scrollTo(0, 10000); + await dataGrid.moveRow(6, 500, 0, true); + await dataGrid.moveRow(6, 550, 0); + + expect(await isPlaceholderVisible()).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/visual.spec.ts new file mode 100644 index 000000000000..66761f56ae28 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/visual.spec.ts @@ -0,0 +1,62 @@ +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('Row dragging.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'); + }); + // T1179218 + + test('Rows should appear correctly during dragging when virtual scrolling is enabled and rowDragging.dropFeedbackMode = "push"', async ({ page }) => { + await t.maximizeWindow(); + return createWidget(page, 'dxDataGrid', { + height: 440, + keyExpr: 'id', + scrolling: { + mode: 'virtual', + }, + dataSource: [...new Array(100)].fill(null).map((_, index) => ({ id: index })), + columns: ['id'], + rowDragging: { + allowReordering: true, + dropFeedbackMode: 'push', + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // drag the row down + await dataGrid.moveRow(0, 30, 150, true); + await dataGrid.moveRow(0, 30, await getOffsetToTriggerAutoScroll(0, 1, 'down')); + + // waiting for autoscrolling + await page.waitForTimeout(2000); + + expect(await page.locator('.dx-data-row').nth(99).locator('td').nth(1).textContent()); + await t.eql('99'); + expect(await isScrollAtEnd('vertical')); + await t.ok(); + + // drag the row up + await dataGrid.moveRow(0, 30, await getOffsetToTriggerAutoScroll(0, 1)); + + // waiting for autoscrolling + await page.waitForTimeout(2000); + + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(1).textContent()); + await t.eql('0'); + expect(await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTop())); + await t.eql(0); + + await testScreenshot(page, 'T1179218-virtual-scrolling-dragging-row.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts new file mode 100644 index 000000000000..bd1ab5ee96d3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } 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('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'); + }); + + async function getMaxRightOffset(dataGrid: DataGrid): Promise { + const scrollWidth = await dataGrid.getScrollWidth(); + const rowsViewWidth = await dataGrid.getRowsView().clientWidth; + return scrollWidth - rowsViewWidth; + } + + async function getRightScrollOffset(dataGrid: DataGrid): Promise { + const maxHorizontalOffset = await getMaxRightOffset(dataGrid); + const scrollLeft = await dataGrid.getScrollLeft(); + return maxHorizontalOffset - scrollLeft; + } + + function 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; + } + + async function getTestLoadCount(page: any): Promise { + return ClientFunction(() => (window as any).testLoadCount as number)(); + } + + ); + + test('DataGrid should set the scrollbar position to the left on resize (T934842)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(1, 50), + columnWidth: 100, + }); + + // act + await page.setViewportSize({ width: 900, height: 250 }); + + // assert + expect(await dataGrid.getScrollLeft()).toBe(0); + + // act + await page.setViewportSize({ width: 700, height: 250 }); + + // assert + expect(await dataGrid.getScrollLeft()).toBe(0); + + // act + await page.setViewportSize({ width: 600, height: 250 }); + + // assert + expect(await dataGrid.getScrollLeft()).toBe(0); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts new file mode 100644 index 000000000000..9dcc4e8117cf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.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('Search Panel', () => { + 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'); + }); + // T1046688 + // visual: material.blue.light + + test('searchPanel has correct view inside masterDetail', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ column1: 'first' }], + columns: ['column1'], + masterDetail: { + enabled: true, + template(container) { + ($('
') as any) + .dxDataGrid({ + searchPanel: { + visible: true, + width: 240, + placeholder: 'Search...', + }, + columns: ['detail1'], + dataSource: [], + }) + .appendTo(container); + }, + }, + }); + + // act + await (page.locator('.dx-data-row').nth(0).locator('.dx-command-edit').nth(0)).click(); + + const masterRow = page.locator('.dx-master-detail-row').nth(0); + const masterGrid = masterRow.getDataGrid(); + + // assert + await testScreenshot(page, 'T1046688.searchPanel.png', { element: masterGrid.element }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts new file mode 100644 index 000000000000..61a7ee623995 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('XSS', () => { + 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'); + }); + + .beforeEach(async (t) => { + await t + .setNativeDialogHandler((type) => { + if (type === 'alert') { + throw Error('XSS alert was invoked!'); + } + }) + .navigateTo(url(__dirname, './pages/XSS.html')); + }) + .afterEach(async (t) => { + await t.navigateTo(url(__dirname, '../../../container.html')); + }); + + test('The XSS script does not run when the markup has been replaced with text', async ({ page }) => { + const filterBuilder = new FilterBuilder('#filter-builder'); + const group = filterBuilder.getField(0, 'groupOperation'); + + await (group.element).click(); + expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); + await (FilterBuilder.getPopupTreeViewNode()).click(); + expect(await true); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/selection.spec.ts new file mode 100644 index 000000000000..4ecae689181d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/selection.spec.ts @@ -0,0 +1,48 @@ +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('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('selectAll state should be correct after unselect item if refresh(true) is called inside onSelectionChanged (T1048081)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + ], + keyExpr: 'id', + selectedRowKeys: [1, 2], + paging: { + pageSize: 3, + }, + selection: { + mode: 'multiple', + }, + onSelectionChanged(e) { + e.component.refresh(true); + }, + }); + + const firstRowSelectionCheckBox = new CheckBox(page.locator('.dx-data-row').nth(0).locator('td').nth(0).locator('.dx-editor-cell')); + const selectAllCheckBox = new CheckBox( + page.locator('.dx-header-row').nth(0).locator('td').nth(0).locator('.dx-editor-cell'), + ); + + // act + await (firstRowSelectionCheckBox.element).click(); + // assert + expect(await selectAllCheckBox.option('value')).toBe(undefined); + expect(await firstRowSelectionCheckBox.option('value')).toBe(false); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/sorting/sorting.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/sorting/sorting.spec.ts new file mode 100644 index 000000000000..226343cdfa86 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/sorting/sorting.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('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'); + }); + test('Filter expression should be valid when sortingMethod, remoteOperations, and autoNavigateToFocusedRow are specified (T1200546)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => { + const sampleData = Array.from({ length: 20 }, (_, i) => ({ ID: i + 1, Name: `Name ${i + 1}` })); + const sampleAPI = new (window as any).DevExpress.data.ArrayStore(sampleData); + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'ID', + load(o) { + if (o.filter && typeof o.filter[0] === 'function') { + return Promise.reject(); + } + return Promise.all([sampleAPI.load(o), sampleAPI.totalCount(o)]).then((res) => ({ + data: res[0], + totalCount: res[1], + })); + }, + }); + return { + dataSource: store, + remoteOperations: true, + columns: ['ID', { + dataField: 'Name', + sortOrder: 'asc', + sortingMethod() { + return 1; + }, + }], + paging: { pageSize: 5 }, + scrolling: { mode: 'virtual' }, + height: 200, + showBorders: true, + focusedRowEnabled: true, + focusedRowKey: 18, + autoNavigateToFocusedRow: true, + }; + }); + + // assert + expect(await dataGrid.dataRows.count); + await t.eql(6); + expect(await dataGrid.getErrorRow().exists); + await t.eql(false); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts new file mode 100644 index 000000000000..e51702009e5b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts @@ -0,0 +1,76 @@ +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 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('State Storing', () => { + 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 makeLocalStorageJsonInvalid = ClientFunction(() => { + window.localStorage.testStorageKey = '{]'; + }); + + const dataGridConfig = { + dataSource: [ + { id: 0, text: 'item 1' }, + { id: 1, text: 'item 2' }, + { id: 2, text: 'item 3' }, + { id: 3, text: 'item 4' }, + ], + columnFixing: { + enabled: true, + }, + keyExpr: 'id', + stateStoring: { + enabled: true, + }, + scrolling: { + mode: 'virtual' as any, + }, + customizeColumns(columns) { + columns[0].fixed = true; + columns[0].fixedPosition = 'sticky'; + }, + }; + + test('The Grid should load if JSON in localStorage is invalid and stateStoring enabled', async ({ page }) => { + await makeLocalStorageJsonInvalid(); + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { A: 1, B: 2, C: 3 }, + { A: 4, B: 5, C: 6 }, + { A: 7, B: 8, C: 9 }, + ], + stateStoring: { + enabled: true, + storageKey: 'testStorageKey', + }, + }); + + const secondCell = page.locator('.dx-data-row').nth(1).locator('td').nth(1); + const consoleMessages = await t.getBrowserConsoleMessages(); + + expect(await secondCell.element().innerText).toBe('5'); + const isWarningExist = !!consoleMessages.warn.find((message) => message.startsWith('W1022')); + expect(await isWarningExist).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts new file mode 100644 index 000000000000..9b12c43654e1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts @@ -0,0 +1,47 @@ +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('Summary', () => { + 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('Group footer summary should be focusable', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, value: 1 }, + { id: 2, value: 1 }, + { id: 3, value: 1 }, + { id: 4, value: 1 }, + ], + columns: [ + 'id', + { + dataField: 'value', + groupIndex: 0, + }, + ], + summary: { + groupItems: [ + { + column: 'id', + summaryType: 'count', + showInGroupFooter: true, + }, + ], + }, + }); + + await (page.locator('.dx-data-row').nth(4).locator('td').nth(1)).click(); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'group-summary-focused.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts new file mode 100644 index 000000000000..d4580b0a5470 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.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('Tagbox 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'); + }); + // T1228720 + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('Datagrid tagbox column should not look broken', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + showBorders: true, + allowColumnResizing: true, + dataSource: [{ id: 1, items: [1, 2, 3, 4, 5] }], + columns: [ + 'id', + { + dataField: 'items', + editCellTemplate(container, cellInfo) { + ($('
') as any) + .dxTagBox({ + dataSource: Array.from({ length: 10 }, (_, index) => ({ + id: index + 1, + text: `item ${index + 1}`, + })), + value: cellInfo.value, + valueExpr: 'id', + displayExpr: 'text', + onValueChanged(e) { + cellInfo.setValue(e.value); + }, + onSelectionChanged() { + cellInfo.component.updateDimensions(); + }, + searchEnabled: true, + }) + .appendTo(container); + }, + }, + ], + editing: { mode: 'batch', allowUpdating: true }, + }); + + await (page.locator('.dx-data-row').nth(0).locator('td').nth(1)).click(); + await testScreenshot(page, 'T1228720-grid-tagbox-on-edit.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/toast.spec.ts new file mode 100644 index 000000000000..8b7974218969 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/toast.spec.ts @@ -0,0 +1,25 @@ +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 DataGrid', () => { + 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('Toast should be visible after calling and should be not visible after default display time', async ({ page }) => { + createWidget(page, 'dxDataGrid', {}); + + await page.locator('.dx-datagrid').first().isVisible(); + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').showErrorToast()); + expect(await page.locator('.dx-toast').isVisible()).toBeTruthy(); + await testScreenshot(page, 'ai-column__toast__at-the-right-position.png', { element: page.locator('#container') }); + expect(await page.locator('.dx-toast').isVisible()).toBeFalsy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts new file mode 100644 index 000000000000..bfc8f01ecb66 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts @@ -0,0 +1,60 @@ +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('Validation', () => { + 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((repaintChangesOnly) => { + + test(`Navigation with tab without saving should not throw an error (repaintChangesOnly: ${repaintChangesOnly})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + id: 1, + col2: 30, + col3: 240, + }, + { + id: 2, + col2: 15, + col3: 120, + }], + keyExpr: 'id', + repaintChangesOnly, + columnAutoWidth: true, + showBorders: true, + paging: { + enabled: false, + }, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [{ + dataField: 'col2', + validationRules: [{ type: 'required' }], + }, { + dataField: 'col3', + validationRules: [{ type: 'required' }], + }], + }); + + await (grid.locator('td').nth(0, 0)).click(); + + const editor = grid.locator('td').nth(0, 0).locator('.dx-editor-cell'); + + await (editor.element).fill('123'); + await page.keyboard.press('tab'); + + expect(await true).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts new file mode 100644 index 000000000000..29207a4937cf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts @@ -0,0 +1,65 @@ +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 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('Validation', () => { + 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'; + + ); + + test('Validation popup screenshot', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 2), + height: 400, + showBorders: true, + columns: [{ + dataField: 'field_0', + validationRules: [{ type: 'required' }], + }, { + dataField: 'field_1', + validationRules: [{ type: 'required' }], + }], + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + }); + + await t.maximizeWindow(); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); + await page.keyboard.press('ctrl+a backspace enter'); + + // act + await testScreenshot(page, 'validation-popup.png', { element: page.locator('#container') }); + + // assert + expect(await dataGrid.getRevertTooltip().exists); + await t.ok(); + expect(await dataGrid.getInvalidMessageTooltip().exists); + await t.ok(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts new file mode 100644 index 000000000000..99b0f8930825 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts @@ -0,0 +1,55 @@ +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('Virtual 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'); + }); + const generateData = (rowCount: number, columnCount: number): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + test('DataGrid should not scroll back to the focused cell after horizontal scrolling to the right when columnRenderingMode is virtual', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 450, + dataSource: generateData(10, 30), + columnWidth: 100, + scrolling: { + columnRenderingMode: 'virtual', + }, + }); + + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(0).focused); + await t.ok(); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 50 }); + + expect(await dataGrid.getScrollLeft()).toBe(50); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 100 }); + + expect(await dataGrid.getScrollLeft()); + await t.eql(100); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/visual.spec.ts new file mode 100644 index 000000000000..4b20faf34aef --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/visual.spec.ts @@ -0,0 +1,73 @@ +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('Virtual 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'); + }); + + const showDataGrid = ClientFunction(() => { + $('#wrapperContainer').css('display', ''); + }); + const generateData = (rowCount: number, columnCount: number): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + const generateColumns = (columnCount: number): Column[] => [...new Array(columnCount)] + .map((_, index) => ({ + dataField: `field${index + 1}`, + })); + + // T1090735 + + test('The updateDimensions method should render the grid if a container was hidden and columnRenderingMode is virtual', async ({ page }) => { + await t.maximizeWindow(); + + await page.evaluate(() => { + $('#container').wrap('
'); + }); + + return createWidget(page, 'dxDataGrid', { + height: 440, + dataSource: generateData(150, 500), + columnWidth: 100, + scrolling: { + columnRenderingMode: 'virtual', + }, + }); + + expect(await page.locator('#wrapperContainer').isVisible()); + await t.notOk(); + + await showDataGrid(); + + await page.waitForTimeout(200); + expect(await page.locator('#wrapperContainer').isVisible()); + await t.ok(); + + await dataGrid.apiUpdateDimensions(); + + await testScreenshot(page, 'T1090735-grid-virtual-columns.png', { element: '#wrapperContainer' }); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts new file mode 100644 index 000000000000..7320feb9c8cc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts @@ -0,0 +1,81 @@ +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 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('FixedColumns - 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'); + }); + // visual: generic.light + // visual: generic.light.compact + // visual: material.blue + // visual: material.blue.compact + // visual: fluent.blue + // visual: fluent.blue.compact + [false, true].forEach( + (showRowLines) => { + // T1268664 + const showRowLinesState = `showRowLines=${showRowLines ? 'true' : 'false'}`; + + test(`Row height for selected, focus and edit state should not differ from the default one if ${showRowLinesState}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(13, 40), + keyExpr: 'field_0', + columnFixing: { + enabled: true, + }, + groupPanel: { + visible: true, + }, + editing: { + allowUpdating: true, + mode: 'row', + }, + showColumnHeaders: true, + columnAutoWidth: true, + allowColumnReordering: true, + allowColumnResizing: true, + focusedRowEnabled: true, + showRowLines, + selection: { + mode: 'multiple', + }, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[11].fixed = true; + columns[11].fixedPosition = 'right'; + columns[12].fixed = true; + columns[12].fixedPosition = 'right'; + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `datagrid_default_state_with_${showRowLinesState}.png`, { element: page.locator('#container') }); + + await (page.locator('.dx-data-row').nth(2).locator('.dx-command-edit').nth(41).locator('.dx-link').nth(0)).click(); + await (page.locator('.dx-data-row').nth(3).locator('.dx-command-edit').nth(0)).click(); + await (page.locator('.dx-data-row').nth(4).locator('td').nth(4)).click(); + + await testScreenshot(page, `datagrid_selected_focused_edit_state_with_${showRowLinesState}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts new file mode 100644 index 000000000000..909437fedca9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts @@ -0,0 +1,45 @@ +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 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 Fixing', () => { + 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'); + }); + // visual: generic.light + // visual: material.blue + // visual: fluent.blue + + test('Fixed columns: Check context menu items', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 5), + width: '100%', + columnFixing: { + enabled: true, + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await t.rightClick(page.locator('.dx-header-row').nth(0).element); + await (dataGrid.getContextMenu().getItemByText('Set Fixed Position')).click(); + await testScreenshot(page, 'sticky_columns_context_menu.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts new file mode 100644 index 000000000000..e0ed7545beb4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.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')}`; + +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('FixedColumns - Focus Overlay', () => { + 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('Focus overlay should be displayed correctly if sticky columns are turned on', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 40), + columnFixing: { + enabled: true, + }, + groupPanel: { + visible: true, + }, + width: 800, + showColumnHeaders: true, + columnAutoWidth: true, + allowColumnReordering: true, + allowColumnResizing: true, + summary: { + totalItems: [{ + column: 'field_1', + summaryType: 'count', + }, { + column: 'field_6', + summaryType: 'count', + }], + groupItems: [{ + column: 'field_0', + summaryType: 'count', + showInGroupFooter: false, + alignByColumn: true, + }, + { + column: 'field_11', + summaryType: 'count', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'field_6', + summaryType: 'count', + showInGroupFooter: true, + }], + }, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[11].fixed = true; + columns[11].fixedPosition = 'right'; + columns[12].fixed = true; + columns[12].fixedPosition = 'right'; + + columns.splice(15, 5, { + caption: 'Band column 1', + columns: [{ + caption: 'Nested column 1', + columns: ['field_15', 'field_16'], + }, + 'field_17', + { + caption: 'Nested column 2', + columns: ['field_18', 'field_19'], + }], + }); + + columns.splice(25, 4, { + caption: 'Band column 2', + columns: [ + 'field_29', + { + caption: 'Nested column 3', + columns: ['field_30', 'field_31'], + }, + 'field_32', + ], + }); + + columns[0].hidingPriority = 0; + columns[columns.length - 1].hidingPriority = 1; + columns[columns.length - 2].hidingPriority = 2; + columns[columns.length - 3].hidingPriority = 3; + + columns[1].groupIndex = 0; + columns[2].groupIndex = 1; + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await (page.locator('.dx-group-row').nth(0).getCell(1).element).click(); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'datagrid_group_row_focused.png', { element: page.locator('#container') }); + + await (page.locator('.dx-data-row').nth(2).locator('.dx-command-edit').nth(40).getAdaptiveButton()).click(); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'datagrid_adaptive_item_focused.png', { element: page.locator('#container') }); + + await (dataGrid.getGroupFooterRow().nth(0), { offsetX: 5, offsetY: 5 }).click(); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'datagrid_group_footer_row_focused.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts new file mode 100644 index 000000000000..f2b91e9aa8e3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.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')}`; + +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('Reorder 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'); + }); + test('Move left fixed column to the right', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + columnAutoWidth: true, + allowColumnReordering: true, + columnWidth: 100, + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + columns[7].fixed = true; + columns[7].fixedPosition = 'left'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await t.drag(page.locator('.dx-header-row').nth(0).locator('td').nth(0), 400, 0); + + await testScreenshot(page, 'move_left_fixed_column_to_right.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts new file mode 100644 index 000000000000..7b62c6f83a9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.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')}`; + +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('Resize columns - nextColumn mode', () => { + 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 scrollTo = ClientFunction((x = 0, y = 0) => { + window.scrollTo(x, y); + }); + [false, true].forEach((rtlEnabled) => { + + test(`Resize first fixed column width with left position (rtlEnabled = ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + rtlEnabled, + columnAutoWidth: true, + allowColumnResizing: true, + columnWidth: 200, + columnResizingMode: 'nextColumn', + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + }, + }); + + // arrange + const columnIndex = rtlEnabled ? 23 : 1; + const scrollLeft = rtlEnabled ? -10000 : 10000; + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await dataGrid.resizeHeader(columnIndex, 100); + + await testScreenshot(page, `resize_first_fixed_column_with_left_position_1_(nextColumn_mode_and_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: scrollLeft }); + + await testScreenshot(page, `resize_first_fixed_column_with_left_position_2_(nextColumn_mode_and_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // assert + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts new file mode 100644 index 000000000000..633b834ce05d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts @@ -0,0 +1,63 @@ +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 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; +}; + +// TODO: import defaultConfig from sticky helpers or inline the data + +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'); + }); + test('The simulated scrollbar should display correctly when there are sticky columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 984, + columnAutoWidth: true, + scrolling: { + useNative: false, + }, + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + // arrange + const scrollbarVerticalThumbTrack = page.locator('.dx-scrollbar-horizontal .dx-scrollable-scroll'); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await (scrollbarVerticalThumbTrack).hover(); + await testScreenshot(page, 'simulated_scrollbar_with_sticky_columns_1.png', { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 1500 }); + + await testScreenshot(page, 'simulated_scrollbar_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts new file mode 100644 index 000000000000..d833b9201c2b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts @@ -0,0 +1,49 @@ +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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('Sticky columns - Adaptability', () => { + 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'); + }); + [false, true].forEach((rtlEnabled) => { + + test(`Sticky columns with adaptive detail row (rtlEnabled = ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + width: 800, + rtlEnabled, + customizeColumns(columns) { + columns.forEach((column, index) => { + if (index < 3) { + column.hidingPriority = index; + } + + column.width = 200; + }); + }, + columnHidingEnabled: true, + }); + + const scrollLeft = rtlEnabled ? -10000 : 10000; + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.apiExpandAdaptiveDetailRow(1); + + await testScreenshot(page, `adaptability_sticky_columns_with_adaptive_detail_row_1_(rtlEnabled_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: scrollLeft }); + + await testScreenshot(page, `adaptability_sticky_columns_with_adaptive_detail_row_2_(rtlEnabled_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withBandColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withBandColumns.spec.ts new file mode 100644 index 000000000000..4a57dbc8203d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withBandColumns.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('Band sticky 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'); + }); + [false, true].forEach((rtlEnabled) => { + // T1279722 + + test(`Headers and filter row should display correctly after scrolling to the max right position when there is a grouped column (rtl=${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + field0: 1, field1: 1, field2: 1, field3: 1, field4: 1, field5: 1, field6: 1, field7: 1, + }, + ], + keyExpr: 'field0', + width: 500, + columnWidth: 100, + columns: [{ + dataField: 'field0', + fixed: true, + fixedPosition: rtlEnabled ? 'right' : 'left', + }, { + caption: 'Band', + fixed: true, + fixedPosition: rtlEnabled ? 'right' : 'left', + columns: [{ + dataField: 'field1', + groupIndex: 0, + }, 'field2'], + }, 'field3', 'field4', 'field5', 'field6', 'field7'], + showBorders: true, + filterRow: { visible: true }, + rtlEnabled, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + await testScreenshot(page, `T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts new file mode 100644 index 000000000000..88226bc73fe5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.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')}`; + +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('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'); + }); + test('Fixed columns should work when drag and drop rows are enabled', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + 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'; + }, + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'datagrid_sticky_columns_with_drag_and_drop.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts new file mode 100644 index 000000000000..6b89e4a62b22 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('Sticky columns - 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('The row edit mode: Edit row when there are sticky columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + editing: { + mode: 'row', + allowUpdating: true, + }, + scrolling: { + showScrollbar: 'never', + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.apiEditRow(1); + await (page.locator('.dx-data-row').nth(1).locator('td').nth(1)).click(); + + await testScreenshot(page, 'edit_row_with_sticky_columns_1.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + await testScreenshot(page, 'edit_row_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts new file mode 100644 index 000000000000..8014ea3fdd43 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('Sticky columns - Filter 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'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('Filter row with sticky columns (generic.light theme)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + filterRow: { + visible: true, + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await (page.locator('.dx-header-row').getFilterRow().getFilterCell(1).element).click(); + + await testScreenshot(page, 'filter_row_with_sticky_columns_1.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + await testScreenshot(page, 'filter_row_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts new file mode 100644 index 000000000000..ad97b2d350a5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts @@ -0,0 +1,83 @@ +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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('FixedColumns - Grouping', () => { + 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'); + }); + [false, true].forEach((rtlEnabled) => { + + test(`Sticky columns with grouping & summary (rtl=${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + rtlEnabled, + customizeColumns(columns) { + columns[2].groupIndex = 0; + }, + summary: { + groupItems: [{ + column: 'OrderNumber', + summaryType: 'count', + displayFormat: '{0} orders', + }, { + column: 'City', + summaryType: 'max', + valueFormat: 'currency', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'TotalAmount', + summaryType: 'max', + valueFormat: 'currency', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'TotalAmount', + summaryType: 'sum', + valueFormat: 'currency', + displayFormat: 'Total: {0}', + showInGroupFooter: true, + }], + totalItems: [{ + column: 'OrderNumber', + summaryType: 'count', + displayFormat: '{0} orders', + }, { + column: 'SaleAmount', + summaryType: 'max', + valueFormat: 'currency', + }, { + column: 'TotalAmount', + summaryType: 'max', + valueFormat: 'currency', + }, { + column: 'TotalAmount', + summaryType: 'sum', + valueFormat: 'currency', + displayFormat: 'Total: {0}', + }], + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `grouping-scroll-begin-rtl=${rtlEnabled}.png`, { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 500 : 100 }); + await testScreenshot(page, `grouping-scroll-center=${rtlEnabled}.png`, { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + await testScreenshot(page, `grouping-scroll-end=${rtlEnabled}.png`, { element: page.locator('#container') }); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts new file mode 100644 index 000000000000..380f1dc5510f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts @@ -0,0 +1,85 @@ +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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('Fixed Columns - keyboard navigation', () => { + 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 navigateToNextCell = async (t, $headerCell) => { + // act + await t + .pressKey('tab'); + + // assert + await t + .expect($headerCell.isFocused) + .ok(); + }; + + const navigateToPrevCell = async (t, $headerCell) => { + // act + await t + .pressKey('shift+tab'); + + // assert + await t + .expect($headerCell.isFocused) + .ok(); + }; + + ); + + test('Headers navigation by Tab key when there are fixed columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + width: 600, + customizeColumns(columns) { + columns[4].width = 125; + columns[4].fixed = true; + columns[4].fixedPosition = 'sticky'; + }, + }); + + // arrange + const headers = page.locator('.dx-header-row'); + const headerRow = headers.getHeaderRow(0); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await (headerRow.locator('td').nth(0).element).click(); + + // assert + expect(await headerRow.locator('td').nth(0).isFocused); + await t.ok(); + + // act + await navigateToNextCell(t, headerRow.locator('td').nth(1)); + await navigateToNextCell(t, headerRow.locator('td').nth(2)); + await navigateToNextCell(t, headerRow.locator('td').nth(3)); + + await testScreenshot(page, 'fixed_columns_headers_navigation_by_tab_1.png', { element: page.locator('#container') }); + + // act + await navigateToNextCell(t, headerRow.locator('td').nth(4)); + await navigateToNextCell(t, headerRow.locator('td').nth(5)); + + await testScreenshot(page, 'fixed_columns_headers_navigation_by_tab_2.png', { element: page.locator('#container') }); + + // act + await navigateToNextCell(t, headerRow.locator('td').nth(6)); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts new file mode 100644 index 000000000000..5d1617ff7ce7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts @@ -0,0 +1,42 @@ +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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('FixedColumns - MasterDetail', () => { + 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('Sticky columns with master-detail', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + masterDetail: { + enabled: true, + template(container) { + $(container) + .text('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'); + }, + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.apiExpandRow(1); + + await testScreenshot(page, 'masterdetail-scroll-begin.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 100 }); + await testScreenshot(page, 'masterdetail-scroll-center.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + await testScreenshot(page, 'masterdetail-scroll-end.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts new file mode 100644 index 000000000000..0518870b7e5f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts @@ -0,0 +1,47 @@ +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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('Sticky columns - Multi Row Header 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'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('The multi row header columns should have vertical borders when a column is fixed (generic.light theme) (T1282595)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + columns: [ + { + dataField: 'ID', + fixed: true, + }, + { + caption: 'Order', + columns: [ + 'OrderNumber', + 'OrderDate', + ], + }, + 'SaleAmount', + 'Terms', + ], + showBorders: true, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'multi_row_header_columns.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts new file mode 100644 index 000000000000..85e51bcaa1ab --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts @@ -0,0 +1,39 @@ +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')}`; + +// TODO: import defaultConfig from sticky helpers or inline the data + +test.describe('Sticky columns - Row 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'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('The selected row should be displayed correctly when there are sticky columns (generic.light theme)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + selection: { + mode: 'multiple', + }, + selectedRowKeys: [4], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'row_selection_with_sticky_columns_1.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + await testScreenshot(page, 'row_selection_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts new file mode 100644 index 000000000000..cdcffb5562a4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.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 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; +}; + +// TODO: import groupingDataSource from sticky helpers or inline the data + +test.describe('Sticky columns - Virtual 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'); + }); + test('Fixed columns with sticky position should not work', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 100), + columnWidth: 100, + showColumnLines: true, + scrolling: { + columnRenderingMode: 'virtual', + }, + customizeColumns(columns) { + columns[0].fixed = true; + columns[1].fixed = true; + + columns[3].fixed = true; + columns[3].fixedPosition = 'sticky'; + }, + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'virtual_columns_with_sticky_columns_1.png', { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 150 }); + + await testScreenshot(page, 'virtual_columns_with_sticky_columns_2.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts new file mode 100644 index 000000000000..7045573e51db --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.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')}`; + +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('Sticky columns - 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'); + }); + test('Fixed columns should display correctly when scrolling vertically quickly', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(400, 15), + height: 700, + columnWidth: 100, + showColumnLines: true, + scrolling: { + mode: 'virtual', + // @ts-expect-error private option + updateTimeout: 3000, + }, + customizeColumns(columns) { + columns[0].fixed = true; + + columns[1].fixed = true; + columns[1].fixedPosition = 'right'; + columns[2].fixed = true; + columns[2].fixedPosition = 'right'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 500 }); + await page.waitForTimeout(100); + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 1000 }); + await page.waitForTimeout(100); + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 1500 }); + await page.waitForTimeout(100); + + await testScreenshot(page, 'fixed_columns_with_virtual_scrolling_1.png', { element: page.locator('#container') }); + + // waiting for size update + await page.waitForTimeout(3000); + + await testScreenshot(page, 'fixed_columns_with_virtual_scrolling_2.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts new file mode 100644 index 000000000000..92ad170985d9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts @@ -0,0 +1,91 @@ +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 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; +}; + +const borderConfigs = [ + { showColumnLines: true, showBorders: true }, + { showColumnLines: false, showBorders: true }, + { showColumnLines: false, showBorders: false }, + { showColumnLines: true, showBorders: false }, +]; + +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'); + }); + borderConfigs.forEach(({ showColumnLines, showBorders }) => { + [true, false].forEach((rtlEnabled) => { + + test(`Band sticky columns: left and right positions (showColumnLines = ${showColumnLines}, showBorders = ${showBorders})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 984, + showColumnLines, + showBorders, + rtlEnabled, + columnAutoWidth: true, + customizeColumns: (columns) => { + columns.push({ + caption: 'Band column 1', + fixed: true, + fixedPosition: 'left', + columns: [{ + caption: 'Nested band column 1', + columns: [ + { dataField: 'field_11', name: 'child_1' }, + { dataField: 'field_12', name: 'child_2' }, + ], + }, { dataField: 'field_13', name: 'child_3' }, { + caption: 'Nested band column 2', + columns: [ + { dataField: 'field_14', name: 'child_4' }, + { dataField: 'field_15', name: 'child_5' }, + ], + }], + }, { + caption: 'Band column 2', + fixed: true, + fixedPosition: 'right', + columns: [ + { dataField: 'field_16', name: 'child_6' }, + { + caption: 'Nested band column 3', + columns: [ + { dataField: 'field_17', name: 'child_7' }, + { dataField: 'field_18', name: 'child_8' }, + ], + }, + { dataField: 'field_19', name: 'child_9' }, + ], + }); + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `band-columns-1-(case-1)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + + await testScreenshot(page, `band-columns-2-(case-1)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts new file mode 100644 index 000000000000..3f741b671ef5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts @@ -0,0 +1,82 @@ +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 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; +}; + +const borderConfigs = [ + { showColumnLines: true, showBorders: true }, + { showColumnLines: false, showBorders: true }, + { showColumnLines: false, showBorders: false }, + { showColumnLines: true, showBorders: false }, +]; + +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'); + }); + borderConfigs.forEach(({ showColumnLines, showBorders }) => { + [true, false].forEach((rtlEnabled) => { + + test(`Sticky column + Band sticky column + Sticky column: sticky positions (showColumnLines = ${showColumnLines}, showBorders = ${showBorders})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 984, + showColumnLines, + showBorders, + rtlEnabled, + columnAutoWidth: true, + customizeColumns: (columns) => { + columns[1].fixed = true; + columns[1].fixedPosition = 'sticky'; + + columns.splice(2, 0, { + caption: 'Band column 1', + fixed: true, + fixedPosition: 'sticky', + columns: [{ + caption: 'Nested band column 1', + columns: [ + { dataField: 'field_11', name: 'child_1' }, + { dataField: 'field_12', name: 'child_2' }, + ], + }, { dataField: 'field_13', name: 'child_3' }, { + caption: 'Nested band column 2', + columns: [ + { dataField: 'field_14', name: 'child_4' }, + { dataField: 'field_15', name: 'child_5' }, + ], + }], + }); + + columns[3].fixed = true; + columns[3].fixedPosition = 'sticky'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `band-columns-1-(case-8)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + + await testScreenshot(page, `band-columns-2-(case-8)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts new file mode 100644 index 000000000000..dc7144d356f1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts @@ -0,0 +1,62 @@ +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 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; +}; + +const borderConfigs = [ + { showColumnLines: true, showBorders: true }, + { showColumnLines: false, showBorders: true }, + { showColumnLines: false, showBorders: false }, + { showColumnLines: true, showBorders: false }, +]; + +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'); + }); + borderConfigs.forEach(({ showColumnLines, showBorders }) => { + [true, false].forEach((rtlEnabled) => { + + test(`Sticky columns with left position (showColumnLines = ${showColumnLines}, showBorders = ${showBorders}, rtlEnabled = ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 884, + showColumnLines, + showBorders, + rtlEnabled, + columnAutoWidth: true, + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `left-position-1(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + + await testScreenshot(page, `left-position-2(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/autocomplete/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/autocomplete/common.spec.ts new file mode 100644 index 000000000000..18c7e111b8ce --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/autocomplete/common.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Autocomplete_placeholder', () => { + 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('Placeholder is visible after items option change when value is not chosen (T1099804)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'autocomplete'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxAutocomplete', { + width: '100%', + placeholder: 'Choose a value', + }, '#autocomplete'); + + const autocomplete = page.locator('#autocomplete'); + + await autocomplete.option('items', [1, 2, 3]); + + await testScreenshot(page, 'Autocomplete placeholder if value is not choosen.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/calendar/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/common.spec.ts new file mode 100644 index 000000000000..a1d463add5da --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/common.spec.ts @@ -0,0 +1,364 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, setClassAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const STATE_HOVER_CLASS = 'dx-state-hover'; + const STATE_ACTIVE_CLASS = 'dx-state-active'; + const CALENDAR_CELL_CLASS = 'dx-calendar-cell'; + const CALENDAR_TODAY_CLASS = 'dx-calendar-today'; + const CALENDAR_SELECTED_DATE_CLASS = 'dx-calendar-selected-date'; + const CALENDAR_EMPTY_CELL_CLASS = 'dx-calendar-empty-cell'; + const CALENDAR_OTHER_VIEW_CLASS = 'dx-calendar-other-view'; + const CALENDAR_CONTOURED_DATE_CLASS = 'dx-calendar-contoured-date'; + + const GESTURE_COVER_CLASS = 'dx-gesture-cover'; + + test('Caption button text should be ellipsis when width is limit', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + width: 150, + value: new Date(2021, 9, 17), + }); + + await testScreenshot(page, 'Calendar with limit width.png', { element: '#container' }); + + }); + + test('Grabbing cursor should be shown during swipe', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + }); + + const calendar = page.locator('#container'); + + await calendar.showGestureCover(); + + const gestureCover = page.locator(`.${GESTURE_COVER_CLASS}`); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('auto'); + + await calendar.swipeStart(); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('grabbing'); + + await calendar.swipe(0.4); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('grabbing'); + + await calendar.swipeEnd(); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('auto'); + + }); + + test('Cells on month view should have hover state class after hover when zoomLevel has been changed from "year" to "month" by click on cell', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + zoomLevel: 'year', + value: new Date(2021, 9, 17), + }); + + const calendar = page.locator('#container'); + + await page.click(calendar.getView().getMonthCellByDate(new Date(2021, 9, 17))); + + const targetCell = calendar.getView().getCellByDate(new Date(2021, 9, 19)); + await targetCell.hover() + .expect(targetCell.hasClass(STATE_HOVER_CLASS)) + .eql(true); + + }); + + test('Calendar with showWeekNumbers rendered correct', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2022, 0, 1), + showWeekNumbers: true, + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers.png', { element: '#container' }); + + }); + + test('Calendar with showWeekNumbers rendered correct for last week of year value', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 11, 31), + showWeekNumbers: true, + weekNumberRule: 'firstDay', + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers last week.png', { element: '#container' }); + + }); + + test('Calendar with showWeekNumbers rendered correct with rtlEnabled', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2022, 0, 1), + showWeekNumbers: true, + rtlEnabled: true, + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers rtl=true.png', { element: '#container' }); + + }); + + test('Calendar with showWeekNumbers rendered correct with cellTemplate', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2022, 0, 1), + showWeekNumbers: true, + cellTemplate(cellData, cellIndex) { + const italic = $('').css('font-style', 'italic') + .text(cellData.text); + const bold = $('').css('font-weight', '900') + .text(cellData.text); + return cellIndex === -1 ? bold : italic; + }, + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers and cell template.png', { element: '#container' }); + + }); + + ['multiple', 'range'].forEach((selectionMode) => { + test(`Calendar with ${selectionMode} selectionMode rendered correct`, async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(2023, 0, 5), new Date(2023, 0, 17), new Date(2023, 1, 2)], + selectionMode, + }); + + await testScreenshot(page, `Calendar with ${selectionMode} selectionMode.png`, { element: '#container' }); + + }); + + test(`Week cell click selection (selectionMode=${selectionMode})`, async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(2023, 0, 5), new Date(2023, 0, 17), new Date(2023, 1, 2)], + selectionMode, + showWeekNumbers: true, + firstDayOfWeek: 1, + disabledDates: ({ date }) => { + const day = date.getDay(); + return day === 1 || day === 4 || day === 0; + }, + }); + + const calendar = page.locator('#container'); + + await page.click(calendar.getView().getWeekNumberCellByIndex(3)); + + await testScreenshot(page, `Week cell click selection (selectionMode=${selectionMode}).png`, { element: '#container' }); + + }); + }); + + test('Calendar with multiview rendered correct', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(2023, 0, 5), new Date(2023, 1, 14)], + selectionMode: 'range', + viewsCount: 2, + }); + + await testScreenshot(page, 'Calendar with multiview.png', { element: '#container' }); + + }); + + ['month', 'year', 'decade', 'century'].forEach((zoomLevel) => { + test(`Calendar ${zoomLevel} view rendered correct`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 400px; height: 400px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + zoomLevel, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + await testScreenshot(page, `Calendar ${zoomLevel} view.png`, { element: '#container' }); + + }); + + test(`Calendar ${zoomLevel} view rendered correct in RTL`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 400px; height: 400px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + zoomLevel, + rtlEnabled: true, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + await testScreenshot(page, `Calendar ${zoomLevel} view in RTL mode.png`, { element: '#container' }); + + }); + + test(`Calendar ${zoomLevel} view with today button rendered correct`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 800px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + width: 450, + height: 450, + zoomLevel, + showTodayButton: true, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + await testScreenshot(page, `Calendar ${zoomLevel} view with today button.png`, { element: '#container' }); + + }); + }); + + test('Calendar with disabled dates rendered correct', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + showTodayButton: true, + showWeekNumbers: true, + min: new Date(2021, 9, 10), + disabledDates: [new Date(2021, 9, 18)], + }); + + await testScreenshot(page, 'Calendar with disabled dates.png', { element: '#container' }); + + }); + + [CALENDAR_CELL_CLASS, CALENDAR_TODAY_CLASS].forEach((cellClass) => { + const testName = `Calendar ${cellClass === CALENDAR_TODAY_CLASS ? 'today ' : ''}cell styles`; + + test(testName, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 800px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + currentDate: new Date(2021, 9, 15), + }, '#calendar'); + + + const calendar = page.locator('#calendar'); + + const startCellDate = new Date(2021, 9, 3); + const view = calendar.getView(); + + let cellOffset = 0; + + for (const cellTypeClass of [ + cellClass, + `${cellClass} ${CALENDAR_OTHER_VIEW_CLASS}`, + `${cellClass} ${CALENDAR_OTHER_VIEW_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_OTHER_VIEW_CLASS} ${CALENDAR_EMPTY_CELL_CLASS}`, + `${cellClass} ${CALENDAR_EMPTY_CELL_CLASS}`, + `${cellClass} ${CALENDAR_EMPTY_CELL_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_CONTOURED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_CONTOURED_DATE_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_SELECTED_DATE_CLASS}`, + ]) { + for (const stateClass of [ + '', + STATE_HOVER_CLASS, + STATE_ACTIVE_CLASS, + ]) { + const cellClasses = `${cellTypeClass} ${stateClass}`; + + await setClassAttribute(page, view.getCellByOffset(startCellDate, cellOffset), cellClasses); + + const cellNumber = startCellDate.getDate() + cellOffset; + const cellId = `cell-${cellNumber}`; + await appendElementTo(page, '#container', 'div', cellId); + + await page.evaluate(() => { + $(`#${cellId}`).text(`${cellNumber} - ${cellClasses}`); + }); + + cellOffset += 1; + } + } + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + ['year', 'decade', 'century'].forEach((zoomLevel) => { + const testName = `Calendar ${zoomLevel} view cell styles`; + + test(testName, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 800px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + currentDate: new Date(2021, 9, 17), + zoomLevel, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + const calendar = page.locator('#calendar'); + + const startCellDate = new Date(2021, 9, 3); + const view = calendar.getView(); + + let cellOffset = 0; + + for (const cellTypeClass of [ + STATE_HOVER_CLASS, + STATE_ACTIVE_CLASS, + CALENDAR_TODAY_CLASS, + CALENDAR_OTHER_VIEW_CLASS, + CALENDAR_EMPTY_CELL_CLASS, + CALENDAR_CONTOURED_DATE_CLASS, + CALENDAR_SELECTED_DATE_CLASS, + `${CALENDAR_CONTOURED_DATE_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + ]) { + const cellClasses = `${cellTypeClass}`; + + await setClassAttribute(page, view.getCellByIndex(cellOffset), cellClasses); + + const cellNumber = startCellDate.getDate() + cellOffset; + const cellId = `cell-${cellNumber}`; + await appendElementTo(page, '#container', 'div', cellId); + + await page.evaluate(() => { + $(`#${cellId}`).text(`${cellNumber} - ${cellClasses}`); + }); + + cellOffset += 1; + } + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + + test(`Calendar with range selectionMode rendered correct (maxZoomLevel=${zoomLevel})`, async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(1023, 0, 5), new Date(1023, 0, 17), new Date(1099, 1, 2)], + selectionMode: 'range', + maxZoomLevel: zoomLevel, + }); + + await testScreenshot(page, `Calendar with range selection (maxZoomLevel=${zoomLevel}).png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/calendar/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/keyboard.spec.ts new file mode 100644 index 000000000000..86d0bfd9db07 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/keyboard.spec.ts @@ -0,0 +1,147 @@ +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('Calendar keyboard navigation', () => { + 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 CALENDAR_SELECTED_DATE_CLASS = 'dx-calendar-selected-date'; + const CALENDAR_CONTOURED_DATE_CLASS = 'dx-calendar-contoured-date'; + + test('Tab navigation order prevButton-caption-nextButton-viewdWrapper-todayButton', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + showTodayButton: true, + }); + + const calendar = page.locator('#container'); + + await page.locator('body').click() + .pressKey('tab'); + + await page.expect(calendar.getNavigatorPrevButton().isFocused) + .ok() + .pressKey('tab') + .expect(calendar.getNavigatorCaption().isFocused) + .ok() + .pressKey('tab') + .expect(calendar.getNavigatorNextButton().isFocused) + .ok() + .pressKey('tab'); + + const cell = calendar.getView().getCellByDate(new Date(2021, 9, 17)); + await page.expect(cell.hasClass(CALENDAR_CONTOURED_DATE_CLASS)) + .ok() + .expect(cell.hasClass(CALENDAR_SELECTED_DATE_CLASS)) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('Enter'); + + const currentDate = await calendar.option('value') as Date; + const today = new Date(); + + currentDate.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + expect(currentDate).toBe(today); + + const todayCell = calendar.getView().getCellByDate(today); + + await page.expect(todayCell.hasClass(CALENDAR_SELECTED_DATE_CLASS)) + .ok(); + + }); + + test('focusin and focusout event handlers should not be called on tab navigate inside calendar', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + showTodayButton: true, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const calendar = page.locator('#container'); + + await page.locator('body').click() + .pressKey('tab'); + + await page.expect(calendar.getNavigatorPrevButton().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getNavigatorCaption().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getNavigatorNextButton().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + const cell = calendar.getView().getCellByDate(new Date(2021, 9, 17)); + await page.expect(cell.hasClass(CALENDAR_CONTOURED_DATE_CLASS)) + .ok() + .expect(cell.hasClass(CALENDAR_SELECTED_DATE_CLASS)) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getTodayButton().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + expect(calendar.isFocused).toBeFalsy() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(1); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/alertList.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/alertList.spec.ts new file mode 100644 index 000000000000..55ed67b5077e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/alertList.spec.ts @@ -0,0 +1,79 @@ +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('ChatAlertList', () => { + 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.clientScripts([ + { module: 'mockdate' }, + { content: 'window.MockDate = MockDate;' }, + ])('Alertlist appearance', async ({ page }) => { + const chat = page.locator('#container'); + + await testScreenshot(page, 'Alertlist with one error.png', { element: '#container' }); + + await chat.option('alerts', [ + { id: 1, message: 'Error Message 1. Error Description...' }, + { id: 2, message: 'Error Message 2. Message was not sent' }, + { id: 3, message: 'Error Message 3. An unexpected issue occurred while processing your request. Please check your internet connection or contact support for further assistance.' }, + ]); + + await testScreenshot(page, 'Alertlist with long text in error.png', { + element: '#container', + }); + + await chat.option('rtlEnabled', true); + + await testScreenshot(page, 'Alertlist appearance in RTL mode.png', { element: '#container' }); + }).before(async () => { + await page.evaluate(() => { + (window as any).MockDate.set('2024/10/18'); + }); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const msInDay = 86400000; + const today = new Date('2024/10/18').setHours(7, 22, 0, 0); + const yesterday = today - msInDay; + + const items = [{ + timestamp: yesterday, + author: userSecond, + text: 'Message text 1', + }, { + timestamp: yesterday, + author: userSecond, + text: 'Message text 2', + }, { + timestamp: today, + author: userFirst, + text: 'Message text 3', + }, { + timestamp: today, + author: userFirst, + text: 'Message text 4', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + alerts: [{ id: 1, message: 'Error Message 1. Error Description...' }], + }); + }).after(async () => { + await page.evaluate(() => { + (window as any).MockDate.reset(); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/avatar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/avatar.spec.ts new file mode 100644 index 000000000000..f372de003e9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/avatar.spec.ts @@ -0,0 +1,68 @@ +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.html')}`; + +test.describe('ChatAvatar', () => { + 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('Chat: avatar', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First User'); + const userSecond = createUser(2, 'Second User'); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + }, '#chat'); + + await testScreenshot(page, 'Avatar with two word initials.png', { element: '#chat' }); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'First', avatarUrl); + const userSecond = createUser(2, 'Second', avatarUrl); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await chat.option('items', items); + + await testScreenshot(page, 'Avatar with image.png', { element: '#chat' }); + + }); + + test('Chat: showAvatar set to false', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First User'); + const userSecond = createUser(2, 'Second User'); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + showAvatar: false, + }, '#chat'); + + await testScreenshot(page, 'Avatar with showAvatar set to false.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/confirmationPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/confirmationPopup.spec.ts new file mode 100644 index 000000000000..dc140419bcfc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/confirmationPopup.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('ChatConfirmationPopup', () => { + 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('Chat: confirmation popup', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = [ + { author: userFirst, text: 'AAA' }, + { author: userFirst, text: 'BBB' }, + { author: userSecond, text: 'CCC' }, + ]; + + await createWidget(page, 'dxChat', { + items, + editing: { + allowDeleting: true, + }, + user: userSecond, + width: 400, + height: 600, + showDayHeaders: false, + rtlEnabled: true, + }); + + const chat = page.locator('#container'); + + await rightClick(chat.getMessage(2)).pressKey('down').pressKey('enter'); + + await testScreenshot(page, 'Confirmation popup is shown.png', { + element: '#container', + }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBox.spec.ts new file mode 100644 index 000000000000..103b59da94fe --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBox.spec.ts @@ -0,0 +1,107 @@ +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.html')}`; + +test.describe('ChatMessageBox', () => { + 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('Chat: messagebox', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + const shortText = getShortText(); + const longText = getLongText(false, 5); + + await chat.focus(); + await testScreenshot(page, 'Messagebox when chat has focus.png', { element: '#chat' }); + + await typeText(chat.getInput(), shortText); + await testScreenshot(page, 'Messagebox when input contains short text.png', { element: '#chat' }); + + await typeText(chat.getInput(), longText); + await testScreenshot(page, 'Messagebox when input contains long text.png', { element: '#chat' }); + + await page.keyboard.press('Tab'); + await testScreenshot(page, 'Messagebox when send button has focus.png', { element: '#chat' }); + + }); + + test('Chat: messagebox with editing preview', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = [{ + author: userFirst, + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, { + author: userSecond, + text: 'Short message', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + editing: { + allowUpdating: true, + }, + width: 400, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + await rightClick(chat.getMessage(0)); + await click(chat.getContextMenuItem(0)); + + await testScreenshot(page, 'Messagebox with editing preview.png', { + element: '#chat', + }); + + }); + + test('Chat: messagebox with attachments and informer', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 812, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + await typeText(chat.getInput(), getLongText(false, 4)); + await chat.option({ + fileUploaderOptions: { + value: [ + ...attachments, + ...attachments, + ...attachments, + ], + }, + }); + + await chat.focus(); + await testScreenshot(page, 'Messagebox with attachments and informer.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBubble.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBubble.spec.ts new file mode 100644 index 000000000000..87df1ab04c8e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBubble.spec.ts @@ -0,0 +1,84 @@ +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.html')}`; + +test.describe('ChatMessageBubble', () => { + 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('Chat: messagebubble', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 400, + height: 650, + }, '#chat'); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + let items = generateMessages(2, userFirst, userSecond, true, false, 2); + + await chat.option({ items, user: userSecond }); + await testScreenshot(page, 'Bubbles with long text.png', { element: '#chat' }); + + items = generateMessages(2, userFirst, userSecond, true, true, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Bubbles with long text with line breaks.png', { element: '#chat' }); + + }); + + test('Chat: messagebubble with images and files', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 600, + height: 650, + }, '#chat'); + + const chat = page.locator('#chat'); + + const user = createUser(1, 'ImageUser'); + + const imageMessages = [ + generateImageMessage(user, '../../../apps/demos/images/products/1.png'), + generateImageMessage(user, '../../../apps/demos/images/products/1-small.png'), + ]; + + await chat.option({ items: imageMessages }); + await testScreenshot(page, 'Bubbles with images.png', { element: '#chat' }); + + const fileMessages = [ + generateFileMessage(user), + generateFileMessage(user, true), + ]; + + await chat.option({ + width: 700, + height: 720, + items: fileMessages, + }); + await testScreenshot(page, 'Bubbles with files.png', { element: '#chat' }); + + await chat.option({ + width: 600, + height: 600, + items: [generateFileMessageWithoutText(user)], + }); + await testScreenshot(page, 'Bubble with files without text.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageGroup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageGroup.spec.ts new file mode 100644 index 000000000000..053fdcd051fc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageGroup.spec.ts @@ -0,0 +1,174 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatMessageGroup', () => { + 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 AVATAR_SELECTOR = '.dx-avatar'; + const CHAT_WRAPPER_STYLES = 'display: flex; flex-wrap: wrap; gap: 2px; width: 1270px; padding: 20px; transform: scale(0.9);'; + + test('Chat: messagegroup, avatar rendering', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First'); + const items = generateMessages(3, userFirst); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + items, + }, '#chat'); + + await testScreenshot(page, 'Avatar has correct position.png', { element: '#chat' }); + + await setStyleAttribute(page, Selector(AVATAR_SELECTOR), 'width: 64px; height: 64px'); + await testScreenshot(page, 'Avatar sizes do not affect indentation between bubbles.png', { element: '#chat' }); + + }); + + test('Chat: messagegroup, information', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, getLongText()); + const userSecond = createUser(2, getLongText()); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + }, '#chat'); + + await testScreenshot(page, 'Information row with long user name.png', { element: '#chat' }); + + }); + + test('Chat: messagegroup, bubbles', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + let items = generateMessages(1, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items, user: userSecond }); + await testScreenshot(page, 'Messagegroup with 1 bubble.png', { element: '#chat' }); + + items = generateMessages(2, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Messagegroup with 2 bubbles.png', { element: '#chat' }); + + items = generateMessages(3, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Messagegroup with 3 bubbles.png', { element: '#chat' }); + + items = generateMessages(4, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Messagegroup with 4 bubbles.png', { element: '#chat' }); + + }); + + test('Messagegroup scenarios in disabled state', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat-wrapper'); + await setStyleAttribute(page, '#chat-wrapper', CHAT_WRAPPER_STYLES); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + await asyncForEach([1, 2, 3, 4], async (bubbleCount, idx) => { + const chatId = `#chat_${idx}`; + await appendElementTo(page, '#chat-wrapper', 'div', `chat_${idx}`); + + const items = generateMessages(bubbleCount, userFirst, userSecond, false, false, 4, 2); + + await createWidget(page, 'dxChat', { + items, + disabled: true, + user: userSecond, + width: 250, + height: 400, + }, chatId); + + const chat = new Chat(chatId); + await chat.repaint(); + }); + + await testScreenshot(page, 'Messagegroup appearance in disabled state.png', { element: '#chat-wrapper' }); + + }); + + test('Messagegroup scenarios in RTL mode', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat-wrapper'); + await setStyleAttribute(page, '#chat-wrapper', CHAT_WRAPPER_STYLES); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + await asyncForEach([1, 2, 3, 4], async (bubbleCount, idx) => { + const chatId = `#chat_${idx}`; + await appendElementTo(page, '#chat-wrapper', 'div', `chat_${idx}`); + + const items = generateMessages(bubbleCount, userFirst, userSecond, false, false, 4, 2); + + await createWidget(page, 'dxChat', { + items, + rtlEnabled: true, + user: userSecond, + width: 250, + height: 400, + }, chatId); + + const chat = new Chat(chatId); + await chat.repaint(); // NOTE: WA to make it stable in Material theme. + }); + + await testScreenshot(page, 'Messagegroup appearance in RTL mode.png', { element: '#chat-wrapper' }); + + }); + + test('MessageGroup with edited messages', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = generateMessages(2, userFirst, userSecond, false, false, 2, 2, true); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + }, '#chat'); + + await testScreenshot(page, 'MessageGroup with edited messages.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageList.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageList.spec.ts new file mode 100644 index 000000000000..24d63275c864 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageList.spec.ts @@ -0,0 +1,362 @@ +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('ChatMessageList', () => { + 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('Messagelist empty view scenarios', async ({ page }) => { + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + }); + + const chat = page.locator('#container'); + + await testScreenshot(page, 'Messagelist empty state.png', { element: '#container' }); + + await chat.option('rtlEnabled', true); + + await testScreenshot(page, 'Messagelist empty in RTL mode.png', { element: '#container' }); + + await chat.option({ + disabled: true, + rtlEnabled: false, + }); + + await testScreenshot(page, 'Messagelist empty in disabled state.png', { element: '#container' }); + + await chat.option({ + width: 200, + height: 400, + disabled: false, + }); + + await testScreenshot(page, 'Messagelist empty with limited dimensions.png', { element: '#container' }); + + }); + + test('Messagelist appearance with scrollbar', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = generateMessages(17, userFirst, userSecond, true, false, 2); + + await createWidget(page, 'dxChat', { + items, + user: userSecond, + width: 400, + height: 600, + showDayHeaders: false, + onMessageEntered: (e) => { + const { component, message } = e; + + component.renderMessage(message); + }, + }); + + const chat = page.locator('#container'); + + await page.hover(chat.messageList); + + await testScreenshot(page, 'Messagelist with a lot of messages.png', { element: '#container' }); + + await ClientFunction( + () => { + const instance = chat.getInstance(); + instance.renderMessage({ + author: instance.option('user') as User, + text: 'Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit. Sed do eiusmod tempor \nincididunt ut labore et dolore magna aliqua. Ut enim ad minim \nveniam, quis nostrud exercitation ullamco laboris nisi ut aliquip \nnex ea commodo consequat.', + }); + }, + { dependencies: { chat } }, + )(); + + await testScreenshot(page, 'Messagelist scrollbar position after call renderMessage().png', { element: '#container' }); + + await page.typeText(chat.getInput(), getLongText()) + .pressKey('shift+enter'); + + await testScreenshot(page, 'Messagelist scrollbar position after typing in textarea.png', { element: '#container' }); + + await page.keyboard.press('Enter'); + + await testScreenshot(page, 'Messagelist scrollbar position after send.png', { element: '#container' }); + + const scrollable = chat.getScrollable(); + const topOffset = (await scrollable.scrollOffset()).top; + + await scrollable.scrollTo({ top: topOffset - 100 }); + + await page.typeText(chat.getInput(), getLongText()); + + await testScreenshot(page, 'Messagelist scrollbar middle position after typing in textarea.png', { element: '#container' }); + + }); + + test('Messagelist should scrolled to the latest messages after being rendered inside an invisible element', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = generateMessages(17, userFirst, userSecond, true, false, 2); + + await createWidget(page, 'dxTabPanel', { + width: 400, + height: 600, + deferRendering: true, + templatesRenderAsynchronously: true, + dataSource: [{ + title: 'Tab_1', + collapsible: true, + text: 'Tab_1 content', + }, { + title: 'Tab_2', + collapsible: true, + template: ClientFunction(() => ($('
') as any).dxChat({ + items, + user: userSecond, + }), { dependencies: { items, userSecond } }), + }], + }); + + const tabPanel = page.locator('#container'); + + await tabPanel.tabs.getItem(1).element.click(); + + await testScreenshot(page, 'Messagelist scroll position after rendering in invisible container.png', { element: '#container' }); + + }); + + test('Messagelist with deleted items', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + isDeleted: true, + }, { + author: userSecond, + text: 'CCC', + isDeleted: true, + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + }); + + await testScreenshot(page, 'Messagelist without message template and with deleted messages.png', { element: '#container' }); + + }); + + test('Messagelist with deleted items and custom template', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + isDeleted: true, + }, { + author: userSecond, + text: 'CCC', + isDeleted: true, + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + messageTemplate: ({ message }, container) => { + if (message.isDeleted) { + $('
').text(`${message.author.name} deleted this message`).appendTo(container); + return; + } + $('
').text(message.text).appendTo(container); + }, + }); + + await testScreenshot(page, 'Messagelist with message template and deleted messages.png', { element: '#container' }); + + }); + + test('Messagelist with messageTemplate', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + }, { + author: userSecond, + text: 'CCC', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + onMessageEntered: ({ component, message }) => { + message.timestamp = undefined; + component.renderMessage(message); + }, + messageTemplate: ({ message }, container) => { + $('
').text(`${message.author.name} says: ${message.text}`).appendTo(container); + }, + }); + + const chat = page.locator('#container'); + + await testScreenshot(page, 'Messagelist with message template.png', { element: '#container' }); + + await chat.getInput().fill('New last message') + .pressKey('enter'); + + await testScreenshot(page, 'Messagelist with message template after new message add.png', { element: '#container' }); + + }); + + test('Messagelist options showDayHeaders, showUserName and showMessageTimestamp set to false work', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + }, { + author: userSecond, + text: 'CCC', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + showUserName: false, + showMessageTimestamp: false, + }); + + await testScreenshot(page, + 'Messagelist with showDayHeaders, showUserName and showMessageTimestamp options set to false.png', + { element: '#container' }, + + }); + + test('Message list with editing context menu', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = [ + { author: userFirst, text: 'AAA' }, + { author: userFirst, text: 'BBB' }, + { author: userSecond, text: 'CCC' }, + ]; + + await createWidget(page, 'dxChat', { + items, + editing: { + allowUpdating: true, + allowDeleting: true, + }, + user: userSecond, + width: 400, + height: 600, + showDayHeaders: false, + }); + + const chat = page.locator('#container'); + + await page.rightClick(chat.getMessage(2)) + .pressKey('down') + .pressKey('down'); + + await testScreenshot(page, 'Messagelist with editing context menu.png', { element: '#container' }); + + }); + + test.clientScripts([ + { module: 'mockdate' }, + { content: 'window.MockDate = MockDate;' }, + ])('Messagelist with date headers', async ({ page }) => { + + await testScreenshot(page, 'Messagelist with date headers.png', { element: '#container' }); + }).before(async () => { + await page.evaluate(() => { + (window as any).MockDate.set('2024/10/27'); + }); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const msInDay = 86400000; + const today = new Date('2024/10/27').setHours(7, 22, 0, 0); + const yesterday = today - msInDay; + + const items = [{ + timestamp: new Date('05.01.2024'), + author: userFirst, + text: 'AAA', + }, { + timestamp: new Date('06.01.2024'), + author: userFirst, + text: 'BBB', + }, { + timestamp: new Date('06.01.2024'), + author: userSecond, + text: 'CCC', + }, { + timestamp: yesterday, + author: userSecond, + text: 'DDD', + }, { + timestamp: today, + author: userFirst, + text: 'EEE', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userSecond, + width: 400, + height: 600, + }); + }).after(async () => { + await page.evaluate(() => { + (window as any).MockDate.reset(); + delete (window as any).MockDate; + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/typingIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/typingIndicator.spec.ts new file mode 100644 index 000000000000..123baef94ec3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/typingIndicator.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatTypingIndicator', () => { + 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 CHAT_TYPINGINDICATOR_CIRCLE_CLASS = 'dx-chat-typingindicator-circle'; + const waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('Item123somevalu*op ', 400)); + + test('Chat: typing indicator with emptyview', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `.${CHAT_TYPINGINDICATOR_CIRCLE_CLASS} { animation: none !important; }`); + + const typingUsers = [ + { name: 'Elodie Montclair' }, + ]; + + await waitFont(); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + typingUsers, + }); + + await testScreenshot(page, 'Typing indicator with emptyview.png', { + element: '#container', + }); + + }); + + test('Chat: typing indicator with a lot of items', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + await insertStylesheetRulesToPage(page, `.${CHAT_TYPINGINDICATOR_CIRCLE_CLASS} { animation: none !important; }`); + + const userFirst = createUser(1, 'Marie-Claire Dubois'); + const userSecond = createUser(2, 'Jean-Pierre Martin'); + + const items = generateMessages(27, userFirst, userSecond); + + const typingUsers = [userFirst]; + + await createWidget(page, 'dxChat', { + user: userSecond, + width: 400, + height: 600, + items, + typingUsers, + }, '#chat'); + + const chat = page.locator('#chat'); + + await chat.repaint(); + + await testScreenshot(page, 'Typing indicator with a lot of items.png', { element: '#chat' }); + + }); + + test('Chat: typing indicator', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + await insertStylesheetRulesToPage(page, `.${CHAT_TYPINGINDICATOR_CIRCLE_CLASS} { animation: none !important; }`); + + const userFirst = createUser(1, 'Elise Moreau'); + const userSecond = createUser(2, 'Pierre Martin'); + + const items = generateMessages(5, userFirst, userSecond); + + const typingUsers = [userFirst]; + + await waitFont(); + + await createWidget(page, 'dxChat', { + user: userSecond, + width: 400, + height: 600, + items, + typingUsers, + }, '#chat'); + + await testScreenshot(page, 'Typing indicator with 1 user.png', { element: '#chat' }); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'Camille'); + const userSecond = createUser(2, 'Sophie'); + const userThird = createUser(3, 'Antoine'); + const userFourth = createUser(4, 'Julien'); + + await chat.option('typingUsers', [userFirst, userSecond]); + await testScreenshot(page, 'Typing indicator with 2 users.png', { element: '#chat' }); + + await chat.option('typingUsers', [userFirst, userSecond, userThird]); + await testScreenshot(page, 'Typing indicator with 3 users.png', { element: '#chat' }); + + await chat.option('typingUsers', [userFirst, userSecond, userThird, userFourth]); + await testScreenshot(page, 'Typing indicator with 4 users.png', { element: '#chat' }); + + await chat.option('typingUsers', [{ name: 'Marie-Francoise Isabelle Antoinette de La Rochefoucauld' }]); + await testScreenshot(page, 'Typing indicator with long name.png', { element: '#chat' }); + + await chat.option('typingUsers', [{}]); + await testScreenshot(page, 'Typing indicator without name.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/common.spec.ts new file mode 100644 index 000000000000..22cca87c5b28 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/common.spec.ts @@ -0,0 +1,122 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const valueModes = [false, true, undefined]; + + const CHECKBOX_CLASS = 'dx-checkbox'; + const READONLY_STATE_CLASS = 'dx-state-readonly'; + const DEFAULT_STATE_CLASS = ''; + const ACTIVE_STATE_CLASS = 'dx-state-active'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + const DISABLED_STATE_CLASS = 'dx-state-disabled'; + const INVALID_STATE_CLASS = 'dx-invalid'; + + [false, true].forEach((isColumnCountStyle) => { + test(`Render ${!isColumnCountStyle ? 'default' : 'with column-count style on container'}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', `padding: 5px; width: 300px; height: 200px; ${isColumnCountStyle ? 'column-count: 2' : ''}`); + + await insertStylesheetRulesToPage(page, `.${CHECKBOX_CLASS} { display: block; }`); + + await appendElementTo(page, '#container', 'div', 'checked'); + await createWidget(page, 'dxCheckBox', { value: true, text: 'checked' }, '#checked'); + + await appendElementTo(page, '#container', 'div', 'unchecked'); + await createWidget(page, 'dxCheckBox', { value: false, text: 'unchecked' }, '#unchecked'); + + await appendElementTo(page, '#container', 'div', 'indeterminate'); + await createWidget(page, 'dxCheckBox', { value: undefined, text: 'indeterminate' }, '#indeterminate'); + + // rtl + await appendElementTo(page, '#container', 'div', 'checkedRTL'); + await createWidget(page, 'dxCheckBox', { value: true, text: 'checked', rtlEnabled: true }, '#checkedRTL'); + + await appendElementTo(page, '#container', 'div', 'uncheckedRTL'); + await createWidget(page, 'dxCheckBox', { value: false, text: 'unchecked', rtlEnabled: true }, '#uncheckedRTL'); + + await appendElementTo(page, '#container', 'div', 'indeterminateRTL'); + await createWidget(page, 'dxCheckBox', { value: undefined, text: 'indeterminate', rtlEnabled: true }, '#indeterminateRTL'); + + + await testScreenshot(page, `Checkbox states${isColumnCountStyle ? ' with column count style' : ''}.png`, { element: '#container' }); + + }); + }); + + test('Checkbox appearance', async ({ page }) => { + + for (const state of [ + DEFAULT_STATE_CLASS, + READONLY_STATE_CLASS, + DISABLED_STATE_CLASS, + HOVER_STATE_CLASS, + ACTIVE_STATE_CLASS, + FOCUSED_STATE_CLASS, + `${FOCUSED_STATE_CLASS} ${HOVER_STATE_CLASS}`, + INVALID_STATE_CLASS, + `${INVALID_STATE_CLASS} ${FOCUSED_STATE_CLASS}`, + ] as string[]) { + await page.evaluate(() => { + $('#container').append($('
').text(`State: ${state}`).css('fontSize', '10px')); + }); + + for (const iconSize of [undefined, 25]) { + for (const text of [undefined, 'Label text']) { + for (const rtlEnabled of [false, true]) { + for (const value of valueModes) { + const id = `dx${new Guid()}`; + await appendElementTo(page, '#container', 'div', id, {}); + + await createWidget(page, 'dxCheckBox', { + text, + value, + rtlEnabled, + iconSize, + }, `#${id}`); + await setClassAttribute(page, `#${id}`, state); + } + } + } + + for (const rtlEnabled of [false, true]) { + const id = `dx${new Guid()}`; + await appendElementTo(page, '#container', 'div', id, {}); + + await createWidget(page, 'dxCheckBox', { + text: 'Label text', + width: 50, + rtlEnabled, + }, `#${id}`); + await setClassAttribute(page, `#${id}`, state); + } + } + } + + await insertStylesheetRulesToPage(page, '.dx-checkbox.dx-widget { display: inline-flex; vertical-align: middle; margin-inline: 10px; }'); + + await testScreenshot(page, 'CheckBox appearance.png'); + + for (const scale of [1.15, 0.67]) { + await page.evaluate(() => { + $('#container').css('transform', `scale(${scale})`); + }); + + await testScreenshot(page, `CheckBox appearance in scaled container, scale=${scale}.png`); + } + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/validationMessage.spec.ts new file mode 100644 index 000000000000..3da0d3d3b675 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/validationMessage.spec.ts @@ -0,0 +1,82 @@ +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('CheckBox_ValidationMessage', () => { + 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('ValidationMessage integrated in editor should not raise any errors when it is placed inside of form and has name "style" (T941581)', async ({ page }) => { + + await createWidget(page, 'dxCheckBox', { + name: 'style', + }); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'required', + message: 'it is required', + }], + }); + + const checkBox = page.locator('#container'); + await checkBox.click() + .click(checkBox.element) + .expect(true).ok(); + + }); + + test('ValidationMessage integrated in editor should not raise any errors when it is placed inside of form that has inline style with scale (T941581)', async ({ page }) => { + + await createWidget(page, 'dxCheckBox', {}); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'required', + message: 'it is required', + }], + }); + + const checkBox1 = page.locator('#container'); + await checkBox1.click() + .click(checkBox1.element) + .expect(true).ok(); + + }); + + const positions = ['top', 'right', 'bottom', 'left']; + positions.forEach((position) => { + test(`CheckBox ValidationMessage position is correct (${position})`, async ({ page }) => { + + await createWidget(page, 'dxCheckBox', { + text: 'Click me!', + elementAttr: { style: 'margin: 50px 0 0 100px;' }, + validationMessagePosition: position, + }); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'required', + message: 'it is required', + }], + }); + + + const checkBox1 = page.locator('#container'); + await checkBox1.click() + .click(checkBox1.element) + .expect(true).ok(); + + await testScreenshot(page, `Checkbox validation message with ${position} position.png`); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/colorbox/colorbox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/colorbox/colorbox.spec.ts new file mode 100644 index 000000000000..9b173100a471 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/colorbox/colorbox.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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('Colorbox should display full placeholder', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'colorBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxColorBox', { + width: '100%', + placeholder: 'I am a very long placeholder', + }, '#colorBox'); + + await testScreenshot(page, 'Colorbox with placeholder.png', { element: '#container' }); + + }); + + ['#00ffff', 'rgb(0,255,255)', 'rgba(0,255,255,1)', 'aqua'].forEach((inputText) => { + ['enter', 'tab'].forEach((key) => { + test(`input value=${inputText} should be formatted to rgba after apply on ${key} key press`, async ({ page }) => { + await createWidget(page, 'dxColorBox', { + editAlphaChannel: true, + }, '#container'); + + const colorBox = page.locator('#container'); + const expectedValue = 'rgba(0, 255, 255, 1)'; + + await page.click(colorBox.input); + + await page.typeText(colorBox.input, inputText) + .pressKey(key) + .expect(colorBox.option('text')) + .eql(expectedValue) + .expect(colorBox.option('value')) + .eql(expectedValue); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/common.spec.ts new file mode 100644 index 000000000000..21094db6fe6e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/common.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox render', () => { + 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 DATEBOX_CLASS = 'dx-datebox'; + const DROP_DOWN_EDITOR_ACTIVE_CLASS = 'dx-dropdowneditor-active'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const pickerTypes: DatePickerType[] = ['calendar', 'list', 'native', 'rollers']; + const types: DateType[] = ['date', 'datetime', 'time']; + + const createDateBox = async (options?: Properties, state?: string): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxDateBox', { + width: 220, + label: 'label text', + showClearButton: true, + value: new Date(2021, 9, 17, 16, 34), + ...options, + }, `#${id}`); + + if (state) { + await setClassAttribute(page, `#${id}`, state); + } + + return id; + }; + + test('DateBox styles', async ({ page }) => { + + await testScreenshot(page, 'Datebox.png'); + + for (const state of [DROP_DOWN_EDITOR_ACTIVE_CLASS, FOCUSED_STATE_CLASS] as any[]) { + for (const id of t.ctx.ids) { + await setClassAttribute(page, `#${id}`, state); + } + + await testScreenshot(page, `Datebox ${state.replaceAll('dx-', '').replaceAll('dropdowneditor-', '').replaceAll('state-', '')}.png`); + + for (const id of t.ctx.ids) { + await removeClassAttribute(page.locator(`#${id}`), state); + } + } + + });.before(async ({ page }) => { + t.ctx.ids = []; + + await insertStylesheetRulesToPage(page, `.${DATEBOX_CLASS} { display: inline-block; margin: 5px; }`); + + for (const stylingMode of stylingModes) { + for (const type of types) { + const options = { + stylingMode, + type, + }; + for (const pickerType of pickerTypes) { + const id = await createDateBox({ ...options, pickerType }); + + t.ctx.ids.push(id); + } + + const id = await createDateBox({ ...options, rtlEnabled: true }); + t.ctx.ids.push(id); + } + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBox.spec.ts new file mode 100644 index 000000000000..6cd3da9b1de4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBox.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterialBased } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const ITEM_HEIGHT = 40; + + const TIME_VIEW_FIELD_CLASS = 'dx-timeview-field'; + const SELECT_BOX_CONTAINER_CLASS = 'dx-selectbox-container'; + + if (!isMaterialBased()) { + [[11, 12, 1925], [10, 23, 2001]].forEach(([month, day, year]) => { + test(`Rollers should be scrolled correctly when value is changed to ${day}/${month}/${year} using kbn and valueChangeEvent=keyup (T948310)`, async ({ page }) => { + await createWidget(page, 'dxDateBox', { + pickerType: 'rollers', + openOnFieldClick: false, + useMaskBehavior: true, + valueChangeEvent: 'keyup', + }); + + const dateBox = page.locator('#container'); + const { dropDownEditorButton } = dateBox; + + await page.click(dropDownEditorButton); + + await page.click(DateBox.getDoneButton()); + + await page.typeText(dateBox.input, `${month}${day}${year}`); + + await page.click(dropDownEditorButton); + + const views = { + month: month - 1, + day: day - 1, + year: year - 1900, + }; + for (const viewName of Object.keys(views)) { + const scrollTop = await DateBox.getRollerScrollTop(viewName); + + expect(scrollTop).toBe(views[viewName] * ITEM_HEIGHT, `${viewName} view is scrolled correctly`); + }); + + }); + }); + } + + test('DateBox with datetime and root element as container (T1193495)', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + value: new Date(2022, 10, 23, 17, 23), + type: 'datetime', + pickerType: 'calendar', + opened: true, + width: 300, + dropDownOptions: { + container: '#container', + }, + }, '#container'); + + await testScreenshot(page, 'DateBox with datetime and root element as container.png', { element: '#container' }); + + }); + + test('DateBox with datetime and opened AM/PM select (T1312677)', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + value: new Date(2022, 10, 23, 17, 23), + type: 'datetime', + pickerType: 'calendar', + opened: true, + dropDownOptions: { + container: '#container', + }, + }, '#container'); + + const timeViewSelect = page.locator(`#container .${TIME_VIEW_FIELD_CLASS} .${SELECT_BOX_CONTAINER_CLASS}`); + + await page.click(timeViewSelect); + + await testScreenshot(page, + 'DateBox with datetime and opened AMPM select.png', + { element: '#container' }, + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBoxGeometry.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBoxGeometry.spec.ts new file mode 100644 index 000000000000..71a9a26f05d5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBoxGeometry.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('DateBox (datetime) geometry (T896846)', () => { + 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 waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('1234567890APM/:', 400)); + + test('Geometry is good', async ({ page }) => { + + await waitFont(); + + await createWidget(page, 'dxDateBox', { + pickerType: 'calendar', + width: 200, + value: new Date(1.5e12), + }); + + const dateBox = page.locator('#container'); + + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with calendar.png'); + + await dateBox.option('opened', false); + await dateBox.option('type', 'datetime'); + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with datetime.png'); + + await dateBox.option('opened', false); + await dateBox.option({ showAnalogClock: false }); + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with datetime without analog clock.png'); + + await dateBox.option('opened', false); + await dateBox.option({ displayFormat: 'HH:mm', calendarOptions: { visible: false }, showAnalogClock: true }); + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with datetime without calendar.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/keyboard.spec.ts new file mode 100644 index 000000000000..924d9a5818c0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/keyboard.spec.ts @@ -0,0 +1,624 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox keyboard navigation', () => { + 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('DateBox should be closed by press esc key when navigator element in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); + + test('DateBox should be closed by press esc key when views wrapper in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getViewsWrapper().focused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); + + test('DateBox should be closed by press esc key when today/cancel/apply button in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getTodayButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getApplyButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getCancelButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); + + test('dateBox keyboard navigation via `tab` key if applyValueMode is useButtons, input -> prev -> caption -> next -> views -> today -> apply -> cancel -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: true, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getApplyButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getCancelButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.input.focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `shift+tab` key if applyValueMode is useButtons, input -> cancel -> apply -> today -> views -> next -> caption -> prev -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: false, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getCancelButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getApplyButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` key if applyValueMode is instantly, input -> prev -> caption -> next -> views -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: true, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.input.focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `shift+tab` key if applyValueMode is instantly, input -> views -> next -> caption -> prev -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: false, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` and `shift+tab` when calendar is not focusable', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + calendarOptions: { + focusStateEnabled: false, + }, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` and `shift+tab` when navigator prev button is not focusable', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + min: new Date(), + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` should close popup when there is no focusable elements', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'instantly', + calendarOptions: { + focusStateEnabled: false, + }, + }, '#container'); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/label.spec.ts new file mode 100644 index 000000000000..bb7bf8793f1d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/label.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, removeStylesheetRulesFromPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox_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 DATEBOX_CLASS = 'dx-datebox'; + + const stylingModes = ['outlined', 'underlined', 'filled']; + const visibleLabelModes = ['floating', 'static', 'outside']; + + test('Symbol parts in label should not be cropped', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 600px; padding: 8px;'); + + for (const stylingMode of stylingModes) { + for (const labelMode of visibleLabelModes) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { }); + + await createWidget(page, 'dxDateBox', { + label: 'qwerty QWERTY 1234567890', + stylingMode, + labelMode, + value: new Date(1900, 0, 1), + }, `#${id}`); + } + } + + await testScreenshot(page, 'Datebox label symbols.png', { element: '#container' }); + + }); + + test('DateBox with buttons container', async ({ page }) => { + + for (const stylingMode of stylingModes) { + for (const buttons of [ + ['clear'], + ['clear', 'dropDown'], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear', 'dropDown'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }, 'dropDown'], + ['clear', 'dropDown', { name: 'custom', location: 'after', options: { icon: 'home' } }], + ]) { + for (const isValid of [true, false]) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { }); + + await createWidget(page, 'dxDateBox', { + value: new Date(2021, 9, 17), + stylingMode, + buttons, + showClearButton: true, + isValid, + }, `#${id}`); + } + } + } + + await insertStylesheetRulesToPage(page, `#container { display: flex; flex-wrap: wrap; } .${DATEBOX_CLASS} { width: 220px; margin: 2px; }`); + + await testScreenshot(page, 'DateBox render with buttons container.png'); + + await removeStylesheetRulesFromPage(page, ); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/validationMessage.spec.ts new file mode 100644 index 000000000000..f54857a0d5ce --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/validationMessage.spec.ts @@ -0,0 +1,54 @@ +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.html')}`; + +test.describe('DateBox ValidationMessagePosition', () => { + 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 positions = ['top', 'right', 'bottom', 'left']; + + test('DateBox ValidationMessage position is correct', async ({ page }) => { + + for (const id of t.ctx.ids) { + const dateBox = page.locator(`#${id}`); + await dateBox.option('value', new Date(2022, 6, 14)); + } + + await testScreenshot(page, 'Datebox validation message.png'); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + for (const position of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + + t.ctx.ids.push(id); + await createWidget(page, 'dxDateBox', { + elementAttr: { style: 'display: inline-block; margin: 50px 100px 0 0;' }, + width: 150, + height: 40, + validationMessageMode: 'always', + validationMessagePosition: position, + }, `#${id}`); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'range', + max: new Date(1), + message: 'out of range', + }], + }, `#${id}`); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/behavior.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/behavior.spec.ts new file mode 100644 index 000000000000..eb48e2e7d6ce --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/behavior.spec.ts @@ -0,0 +1,1453 @@ +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('DateRangeBox behavior (applyValueMode=\'instantly\')', () => { + 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('Open by click on startDate input and select date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and reselect start date in calendar, value: ["2021/09/17", null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([null, new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(27)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 23)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar > startDate, value: ["2021/09/17", "2021/09/28"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 28)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 21), new Date(2021, 9, 28)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/21"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 21)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 26), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(31)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 27), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(32)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 28), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 26)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar < startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(9)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 5), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(8)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 4), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 4), new Date(2021, 9, 6)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(4); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar = endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar = startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar = startDate -> endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Value in calendar should be updated by click on clear button if popup is open', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + opened: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + showClearButton: true, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .click(dateRangeBox.clearButton) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Value in calendar should be updated after change start date value by keyboard and click on endDate input if popup is open', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + opened: false, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + showClearButton: true, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Backspace') + .typeText(dateRangeBox.getStartDateBox().input, '0'); + + await page.expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([new Date(2020, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2020, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Value in calendar should be updated after change start date value by keyboard and press `tab` if popup is open', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + opened: false, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + showClearButton: true, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Backspace') + .pressKey('backspace') + .typeText(dateRangeBox.getStartDateBox().input, '19'); + + await page.expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([new Date(2019, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2019, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await dateRangeBox.getEndDateBox().input.fill('10/24/2023') + .click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([new Date(2019, 9, 17), new Date(2023, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2019, 9, 17), new Date(2023, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + }); + + test('Value should be saved after select range in calendar and click on apply button, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + }); + + test('Value should not be saved after select range and click on cancel button', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(10)) + .click(dateRangeBox.getCalendarCell(21)) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getCancelButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + }); + + test('Open by click on startDate input and reselect start date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(27)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 23)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar > startDate, value: ["2021/09/17", "2021/09/28"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 28)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 28)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 21), new Date(2021, 9, 28)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/21"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 21)], + openOnFieldClick: true, + width: 500, + applyValueMode: 'useButtons', + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(31)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(32)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 28), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 26)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar < startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(9)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(8)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 4), new Date(2021, 9, 6)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar = endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + }); + + test('Open by click on endDate input and select date in calendar = startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar = startDate -> endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/calendar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/calendar.spec.ts new file mode 100644 index 000000000000..1c3c94d84da5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/calendar.spec.ts @@ -0,0 +1,673 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox range 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 STATE_HOVER_CLASS = 'dx-state-hover'; + + test('DateRangeBox calendar appearance after change rtl mode in runtime', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 10, 30)], + openOnFieldClick: true, + opened: true, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await dateRangeBox.option('rtlEnabled', true); + + await testScreenshot(page, 'DRB appearance after change rtl mode in runtime.png', { element: '#container' }); + + }); + + test('Cells classes after hover cells, value: [null, null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(0) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/12')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(0) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/25')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(0) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Cells classes after hover date < startDate & date > startDate, currentSelection: startDate, value: [new Date(2021, 9, 17), null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/12')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/25')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Cells classes after hover date < startDate, currentSelection: endDate, value: [new Date(2021, 9, 17), null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/12')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Cells classes after hover and select date > startDate, currentSelection: endDate, value: [new Date(2021, 9, 17), null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/24')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(8) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(1) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(1); + + }); + + test('Selected range if endDate = startDate, currentSelection: startDate', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 18), new Date(2021, 9, 18)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(1) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(1) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await testScreenshot(page, 'DRB range, endDate = startDate.png', { element: '#container' }); + + await page.hover(calendar.getCellByDate('2021/10/18')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(1) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(1) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Start date cell in selected range', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 10, 6)], + openOnFieldClick: true, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.hover(calendar.getCellByDate('2021/10/01')); + + await testScreenshot(page, 'DRB range, startDate is start in row, hover is start in view.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/31')) + .click(dateRangeBox.getStartDateBox().input) + .hover(calendar.getCellByDate('2021/10/16')); + + await testScreenshot(page, 'DRB range, startDate is end in view & start row, hover is end row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/23')) + .click(dateRangeBox.getStartDateBox().input) + .hover(calendar.getCellByDate('2021/10/03')); + + await testScreenshot(page, 'DRB range, startDate is end cell row, hover is start in row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 8, 1)); + + await page.click(calendar.getCellByDate('2021/10/01')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 8, 1)); + + await page.hover(calendar.getCellByDate('2021/09/30')); + + await testScreenshot(page, 'DRB range, startDate is start in view, hover is end in view.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 8, 1)); + + await page.click(calendar.getCellByDate('2021/09/30')) + .click(dateRangeBox.getStartDateBox().input) + .hover(calendar.getCellByDate('2021/09/15')); + + await testScreenshot(page, 'DRB range, startDate is end in view, hover inside row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 7, 1)); + + await page.click(calendar.getCellByDate('2021/09/15')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 7, 1)); + + await page.hover(calendar.getCellByDate('2021/08/01')); + + await testScreenshot(page, 'DRB range, startDate inside row, hover is start in view & row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await page.click(calendar.getCellByDate('2021/08/01')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await page.hover(calendar.getCellByDate('2021/07/31')); + + await testScreenshot(page, 'DRB range, startDate is start view & row, hover is end view & row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/07/31')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await page.hover(calendar.getCellByDate('2021/07/02')); + + await testScreenshot(page, 'DRB range, startDate is end in view & row, hover inside row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 4, 1)); + + await page.hover(calendar.getCellByDate('2021/05/01')); + + await testScreenshot(page, 'DRB range, hover is start in view & end cell row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/05/01')) + .click(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, 'DRB range, startDate cell is start in view & end cell row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 1, 1)); + + await page.hover(calendar.getCellByDate('2021/02/28')); + + await testScreenshot(page, 'DRB range, hover is end in view & start in row.png', { element: '#container' }); + + }); + + test('End date cell in selected range', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 23)], + openOnFieldClick: true, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2021/10/24')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/10/31')); + + await testScreenshot(page, 'DRB range, endDate is start in row, hover is end view & start row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/25')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/01')); + + await testScreenshot(page, 'DRB range, endDate is cell inside row, hover is start in view.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/30')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/30')); + + await testScreenshot(page, 'DRB range, endDate is end cell row, hover is end in view.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/31')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/21')); + + await testScreenshot(page, 'DRB range, endDate is end in view & start row, hover is start row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/11/01')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/21')); + + await testScreenshot(page, 'DRB range, endDate is start in view, hover is end in row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 11, 15)); + + await page.click(calendar.getCellByDate('2021/12/31')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 12, 15)); + + await page.hover(calendar.getCellByDate('2022/01/01')); + + await testScreenshot(page, 'DRB range, endDate is end in view, hover is start view & end row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2022/01/01')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 12, 15)); + + await page.hover(calendar.getCellByDate('2022/01/25')); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 12, 15)); + + await testScreenshot(page, 'DRB range, endDate is start view & end cell row, hover inside row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 3, 15)); + + await page.hover(calendar.getCellByDate('2022/04/30')); + + await testScreenshot(page, 'DRB range, hover is end in view & end cell row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2022/04/30')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 2, 15)); + await testScreenshot(page, 'DRB range, endDate is end in view & end cell row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 3, 15)); + + await page.hover(calendar.getCellByDate('2022/05/01')); + + await testScreenshot(page, 'DRB range, hover is start in view & start in row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2022/05/01')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 3, 15)); + + await testScreenshot(page, 'DRB range, endDate is start in view & start in row.png', { element: '#container' }); + + }); + + test('Cell in range', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2020, 2, 2), new Date(2024, 2, 2)], + openOnFieldClick: true, + opened: true, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2023, 2, 1)); + + await testScreenshot(page, 'DRB range cells, start in view and end in row & vise versa.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await testScreenshot(page, 'DRB range cells, start in view and in row & end in view and in row.png', { element: '#container' }); + + }); + + test('Disabled dates on start date select (disableOutOfRangeSelection: true)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + width: 500, + disableOutOfRangeSelection: true, + calendarOptions: { + currentDate: new Date('2020/02/20'), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2020/02/20')); + + await testScreenshot(page, 'DRB disabled dates before start date select.png', { element: '#container' }); + + }); + + test('Disabled dates on end date select (disableOutOfRangeSelection: true)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + width: 500, + disableOutOfRangeSelection: true, + calendarOptions: { + currentDate: new Date('2020/02/20'), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2020/02/22')); + + await testScreenshot(page, 'DRB disabled dates after end date select.png', { element: '#container' }); + + }); + + test('Disabled dates on inputs focus (disableOutOfRangeSelection: true)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2020/02/20'), new Date('2020/02/22')], + width: 500, + disableOutOfRangeSelection: true, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input) + .hover(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, 'DRB disabled dates on popup opening.png', { element: '#container' }); + + await page.click(dateRangeBox.getEndDateBox().input) + .hover(dateRangeBox.getEndDateBox().input); + + await testScreenshot(page, 'DRB disabled dates on end date input focus.png', { element: '#container' }); + + await page.click(dateRangeBox.getStartDateBox().input) + .hover(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, 'DRB disabled dates on start date input focus.png', { element: '#container' }); + + }); + + test(`Hovered cell should have "${STATE_HOVER_CLASS}" class after one date selected (disableOutOfRangeSelection=true)`, async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + disableOutOfRangeSelection: true, + calendarOptions: { + currentDate: new Date('2020/02/20'), + }, + }, '#container'); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2020/02/20')); + + const targetCell = calendar.getView().getCellByDate(new Date('2020/02/22')); + await targetCell.hover() + .expect(targetCell.hasClass(STATE_HOVER_CLASS)) + .eql(true); + + }); + + test('Dates selection with focusStateEnabled=false', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2020/02/20'), new Date('2020/02/22')], + width: 500, + focusStateEnabled: false, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + const calendar = dateRangeBox.getCalendar(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(calendar.getCellByDate('2020/02/10')); + + await page.click(calendar.getCellByDate('2020/02/25')); + + const expectedStartDate = new Date('2020/02/10'); + const expectedEndDate = new Date('2020/02/25'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, expectedEndDate]); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/common.spec.ts new file mode 100644 index 000000000000..64edf65b36d6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/common.spec.ts @@ -0,0 +1,136 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox render', () => { + 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 DATERANGEBOX_CLASS = 'dx-daterangebox'; + const DROP_DOWN_EDITOR_ACTIVE_CLASS = 'dx-dropdowneditor-active'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const READONLY_STATE_CLASS = 'dx-state-readonly'; + const DISABLED_STATE_CLASS = 'dx-state-disabled'; + + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const labelModes: LabelMode[] = ['static', 'floating', 'hidden', 'outside']; + + const TEST_VALUE = [new Date(2021, 9, 17, 16, 34), new Date(2021, 9, 18, 16, 34)]; + + const createDateRangeBox = async ( + options?: DateRangeBoxProperties, + state?: string, + ): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { }); + + const config: any = { + width: 500, + labelMode: 'static', + endDateLabel: 'static', + startDateLabel: 'qwertyQWERTYg', + showClearButton: true, + ...options, + }; + + await createWidget(page, 'dxDateRangeBox', config, `#${id}`); + + if (state) { + await setClassAttribute(page, `#${id}`, state); + await setClassAttribute(page, `#${id} .dx-start-datebox`, state); + } + + return id; + }; + + test('DateRangeBox styles', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `.${DATERANGEBOX_CLASS} { display: inline-flex; margin: 5px; }`); + + for (const stylingMode of stylingModes) { + for (const state of [ + DROP_DOWN_EDITOR_ACTIVE_CLASS, + FOCUSED_STATE_CLASS, + HOVER_STATE_CLASS, + READONLY_STATE_CLASS, + DISABLED_STATE_CLASS, + ] as any[] + ) { + await createDateRangeBox({ value: TEST_VALUE, stylingMode }, state); + } + } + + await createDateRangeBox({ value: TEST_VALUE, rtlEnabled: true }); + await createDateRangeBox({ value: TEST_VALUE, isValid: false }); + + await testScreenshot(page, 'DateRangeBox styles.png', { element: '#container' }); + + }); + + test('DateRangeBox with buttons container', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '#container { display: flex; flex-wrap: wrap; gap: 4px; }'); + + const testButtons: DropDownEditorProperties['buttons'][] = [ + ['clear'], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear', 'dropDown'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }, 'dropDown'], + [{ name: 'custom', location: 'before', options: { icon: 'home' } }, 'clear', 'dropDown'], + ]; + + for (const buttons of testButtons) { + await createDateRangeBox({ + value: TEST_VALUE, + buttons, + }); + await createDateRangeBox({ + value: TEST_VALUE, + buttons, + rtlEnabled: true, + }); + } + + await testScreenshot(page, 'DateRangeBox with buttons container.png', { element: '#container' }); + + }); + + labelModes.forEach((labelMode) => { + test('Custom placeholders and labels appearance', async ({ page }) => { + const dateRangeBox = page.locator(`#${t.ctx.id}`); + + await testScreenshot(page, `Placeholder and label by default labelMode=${labelMode}.png`, { element: '#container' }); + + await page.click(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, `Placeholder and label on start date input focus labelMode=${labelMode}.png`, { element: '#container' }); + + await page.click(dateRangeBox.getEndDateBox().input); + + await testScreenshot(page, `Placeholder and label on end date input labelMode=${labelMode} focus.png`, { element: '#container' }); + + });.before(async ({ page }) => { + await setAttribute(page, '#container', 'style', 'width: 800px; height: 300px; padding-top: 10px;'); + + t.ctx.id = await createDateRangeBox({ + labelMode, + width: 600, + openOnFieldClick: false, + startDateLabel: 'first date', + endDateLabel: 'second date', + startDatePlaceholder: 'enter start date', + endDatePlaceholder: 'enter end date', + }); + await appendElementTo(page, '#container', 'div', t.ctx.id); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/focus.spec.ts new file mode 100644 index 000000000000..2c86c12ec545 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/focus.spec.ts @@ -0,0 +1,674 @@ +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('DateRangeBox focus state', () => { + 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('DateRangeBox & DateBoxes should have focus class if inputs are focused by tab', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + width: 500, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab') + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab') + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & DateBoxes should have focus class if inputs are focused by click', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + width: 500, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.getEndDateBox().input) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetX: -50 }) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & Start DateBox should have focus class after click on drop down button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused if dateRangeBox open by click on drop down button and endDateBox was focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await dateRangeBox.getEndDateBox().element.click(); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.dropDownButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('onFocusIn should be called only after first click on drop down button', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.element, { offsetX: -20, offsetY: -20 }); + + await page.expect(dateRangeBox.option('opened')) + .ok(); + + await page.expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.element, { offsetX: -20, offsetY: -20 }); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + }); + + test('onFocusIn should be called only on focus of startDate input', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2021/09/17'), new Date('2021/10/24')], + openOnFieldClick: true, + width: 500, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 8, 8), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .click(dateRangeBox.getCalendarCell(20)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 8, 8), new Date(2021, 8, 18)]) + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('shift+tab') + .wait(100) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('shift+tab') + .wait(100) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(1); + + }); + + test('Click by separator element should focus DateRangeBox or leave active input focused without call onFocusIn event handler', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + width: 500, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.separator); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.separator); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.separator); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(page.locator('body'), { offsetX: -50 }) + .expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(1); + + }); + + test('EndDateBox should be stay focused after close popup by click on drop down button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2021/09/17'), new Date('2021/10/24')], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.element, { offsetX: -20, offsetY: -20 }); + + await page.expect(dateRangeBox.option('opened')) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 8, 8), new Date(2021, 9, 24)]) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('DateRangeBox & StartDateBox should be focused after click on clear button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + showClearButton: true, + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await dateRangeBox.getEndDateBox().element.click(); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.clearButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused and stay opened after click on clear button when popup is opened', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + showClearButton: true, + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + }, '#container'); + + const dateRangeBox = page.locator('#container'); + + await dateRangeBox.getStartDateBox().element.click(); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.clearButton); + + await page.waitForTimeout(500); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused after click on clear button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + showClearButton: true, + value: [null, '2021/10/24'], + openOnFieldClick: false, + opened: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input) + .click(dateRangeBox.dropDownButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.clearButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused if startDateBox open by keyboard, alt+down, alt+up', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused if endDateBox open and close by keyboard, alt+down, alt+up', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Opened dateRangeBox should not be closed after click on inputs, openOnFieldClick: true', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + opened: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('Opened dateRangeBox should be closed after outside click, openOnFieldClick: true', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + width: 500, + openOnFieldClick: true, + opened: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(page.locator('body'), { offsetX: -50 }); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + // TODO: find way to reproduce focus using accessKey accessKey + test.skip('DateRangeBox and StartDateBox should have focus class after focus via accessKey', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + accessKey: 'x', + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.keyboard.press('alt+x'); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/keyboard.spec.ts new file mode 100644 index 000000000000..4d7a5a3a4e16 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/keyboard.spec.ts @@ -0,0 +1,1260 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox keyboard navigation', () => { + 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 initialValue = [new Date('2021/10/17'), new Date('2021/11/24')]; + + const getDateByOffset = (date: Date | string, offset: number) => { + const resultDate = new Date(date); + + return new Date(resultDate.setDate(resultDate.getDate() + offset)); + }; + + test('DateRangeBox should be opened and close by press alt+down and alt+up respectively when startDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be opened and close by press alt+down and alt+up respectively when endDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be opened by press alt+down if startDate input is focused and close by press alt+up if endDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when startDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when endDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when today/cancel/apply button in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when navigator element in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when views wrapper in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should not be closed by press tab key on startDate input', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + calendarOptions: { + focusStateEnabled: false, + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .notOk(); + + }); + + test('DateRangeBox keyboard navigation via `tab` key if applyValueMode is useButtons, start -> end -> prev -> caption -> next -> views -> today -> apply -> cancel -> start -> end', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + }); + + test('DateRangeBox keyboard navigation via `shift+tab` key if applyValueMode is useButtons, end -> start -> cancel -> apply -> today -> views -> next -> caption -> prev -> end -> start', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: false, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + }); + + test('DateRangeBox keyboard navigation via `tab` key if applyValueMode is instantly, start -> end -> prev -> caption -> next -> views -> start -> end', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('DateRangeBox keyboard navigation via `shift+tab` key if applyValueMode is instantly, end -> start -> views -> next -> caption -> prev -> end -> start', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: false, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox should not be closed by press shift+tab key on endDate input', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + calendarOptions: { + focusStateEnabled: false, + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab') + .wait(100); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab') + .wait(100); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + [ + { key: 'left', offsetInDays: -1 }, + { key: 'right', offsetInDays: 1 }, + { key: 'up', offsetInDays: -7 }, + { key: 'down', offsetInDays: 7 }, + ].forEach(({ key, offsetInDays }) => { + test(`DateRangeBox start value should be changed after after opening and navigation by '${key}' key and click on 'enter' key`, async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: initialValue, + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql(initialValue); + + await page.pressKey(key) + .pressKey('enter'); + + const expectedStartDate = getDateByOffset(initialValue[0], offsetInDays); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, new Date(initialValue[1])]); + + }); + + test('Selection in calendar should be started with current startDate value after select startDate if endDate is not specified', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: [initialValue[0], null], + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton); + + await page.pressKey(key) + .pressKey('enter'); + + await page.keyboard.press('ArrowRight') + .pressKey('right') + .pressKey('right') + .pressKey('right') + .pressKey('right') + .pressKey('enter'); + + const expectedStartDate = getDateByOffset(initialValue[0], offsetInDays); + const expectedEndDate = getDateByOffset(expectedStartDate, 5); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, expectedEndDate]); + + }); + + test('Selection in calendar should be started with endDate value after select startDate if endDate is specified', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: initialValue, + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton); + + await page.keyboard.press('ArrowLeft') + .pressKey('enter'); + + await page.pressKey(key) + .pressKey('enter'); + + const expectedStartDate = getDateByOffset(initialValue[0], -1); + const expectedEndDate = getDateByOffset(initialValue[1], offsetInDays); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, expectedEndDate]); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/validationMessage.spec.ts new file mode 100644 index 000000000000..aab8ad799033 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/validationMessage.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox validation message position', () => { + 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 DATERANGEBOX_CLASS = 'dx-daterangebox'; + + const validationMessagePositions = ['auto', 'bottom', 'left', 'right', 'top']; + + const createFormWithDateRangeBox = async (validationMessagePosition: string): Promise => { + const id = `${`dx${new Guid()}`}`; + await appendElementTo(page, '#container', 'div', id, { }); + + const config: any = { + width: '100%', + labelLocation: 'top', + formData: { + DateRange: ['2021/09/17', null], + }, + colCount: 1, + items: [{ + dataField: 'DateRange', + editorType: 'dxDateRangeBox', + label: { + text: 'Date Range', + }, + validationRules: [{ + type: 'required', + message: 'Some message', + }], + editorOptions: { + startDatePlaceholder: 'Start Date', + endDatePlaceholder: 'End Date', + validationMessageMode: 'always', + validationMessagePosition, + }, + }], + }; + + await createWidget(page, 'dxForm', config, `#${id}`); + + return id; + }; + + test('The validation message overlay for DateRangeBox should be correctly positioned before and after opening', async ({ page }) => { + + for (const id of t.ctx.ids) { + await new Form(`#${id}`).validate(); + } + + await testScreenshot(page, 'The validation message overlay position for DateRangeBox before opening.png', { element: '#container' }); + + for (const id of t.ctx.ids) { + const form = page.locator(`#${id}`); + const dateRangeBox = page.locator(`#${id} .${DATERANGEBOX_CLASS}`); + + await form.validate(); + + await page.click(dateRangeBox.dropDownButton) + .click(dateRangeBox.dropDownButton); + } + + await testScreenshot(page, 'The validation message overlay position for DateRangeBox after opening.png', { element: '#container' }); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + await insertStylesheetRulesToPage(page, ` + #container { width: 900px; height: 800px; display: flex; flex-direction: column; padding: 50px; } + .dx-form { margin: 25px 50px; } + `); + + for (const validationMessagePosition of validationMessagePositions) { + const id = await createFormWithDateRangeBox(validationMessagePosition); + t.ctx.ids.push(id); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/T1245111_dropDownBox_height.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/T1245111_dropDownBox_height.spec.ts new file mode 100644 index 000000000000..3c68e6a5d232 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/T1245111_dropDownBox_height.spec.ts @@ -0,0 +1,43 @@ +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('Grid on Drop Down Box', () => { + 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'); + }); + + , + + // T1245111 + test('DataGrid on dropDownBox should appear correctly on window resize', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: Array.from({ length: 100 }, (_, index) => ({ + Value: index + 1, + Text: `item ${index + 1}`, + })), + dropDownOptions: { + width: 'auto', + }, + contentTemplate: (e) => ($('
') as any).dxDataGrid({ + dataSource: e.component.getDataSource(), + }), + }); + + const dropDownBox = page.locator('#container'); + const overlay = page.locator('.dx-overlay-content'); + + await click(dropDownBox); + await overlay.hover(); + await resizeWindow(800, 800); + await testScreenshot(page, 'T1245111-dropDownBox-resize.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/popup.spec.ts new file mode 100644 index 000000000000..929446ca54dd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/popup.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('Drop Down Box\'s 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'); + }); + + const BUTTON_CLASS = 'dx-dropdowneditor-button'; + + test('Popup should have correct height when DropDownBox is opened first time (T1130045)', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dropDownOptions: { + templatesRenderAsynchronously: true, + }, + contentTemplate: '
', + }); + + await page.locator(`.${BUTTON_CLASS}`).click(); + + await testScreenshot(page, 'Popup has correct height on the first opening.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/common.spec.ts new file mode 100644 index 000000000000..3a2f15f32e6c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/common.spec.ts @@ -0,0 +1,125 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Drop Down 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'); + }); + + const DROP_DOWN_BUTTON_CLASS = 'dx-dropdownbutton'; + const BUTTON_GROUP_CLASS = 'dx-buttongroup'; + + const stylingModes = ['text', 'outlined', 'contained']; + const types = ['normal', 'default', 'danger', 'success']; + + test('Item collection should be updated after direct option changing (T817436)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dropDownButton1', { }); + await appendElementTo(page, '#container', 'div', 'dropDownButton2', { }); + + await createWidget(page, 'dxDropDownButton', { + items: [{ text: 'text1' }, { text: 'text2' }], + displayExpr: 'text', + }, '#dropDownButton1'); + + await createWidget(page, 'dxDropDownButton', { + dataSource: [{ text: 'text1' }, { text: 'text2' }], + displayExpr: 'text', + }, '#dropDownButton2'); + + const dropDownButton1 = page.locator('#dropDownButton1'); + const dropDownButton2 = page.locator('#dropDownButton2'); + + await dropDownButton1.click(); + const list1 = await dropDownButton1.getList(); + await dropDownButton2.click(); + const list2 = await dropDownButton2.getList(); + + await page.expect(list1.getItem().isDisabled).notOk() + .expect(list2.getItem().isDisabled).notOk(); + + await dropDownButton1.option('items[0].disabled', true); + await dropDownButton2.option('dataSource[0].disabled', true); + + await dropDownButton1.click() + .expect(list1.getItem().isDisabled) + .ok() + .click(dropDownButton2.element) + .expect(list2.getItem().isDisabled) + .ok(); + + }); + + [undefined, 120].forEach((width) => { + types.forEach((type) => { + if (width && type !== 'normal') { + return; + } + + test('DropDownButton renders correctly', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `.${DROP_DOWN_BUTTON_CLASS}.dx-widget { display: inline-flex; vertical-align: middle; margin: 2px; } .${BUTTON_GROUP_CLASS} { vertical-align: middle; }`); + + await testScreenshot(page, `DropDownButton render${width ? ' with fixed width' : ''}${type !== 'normal' ? `, type=${type}` : ''}.png`); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + for (const rtlEnabled of [false, true]) { + for (const stylingMode of stylingModes) { + await appendElementTo(page, '#container', 'div', `${stylingMode}-${rtlEnabled}`, { fontSize: '10px' }); + await page.evaluate(() => { + $(`#${stylingMode}-${rtlEnabled}`).text(`StylingMode: ${stylingMode}, rtlEnabled: ${rtlEnabled}`); + }); + + for (const splitButton of [true, false]) { + for (const showArrowIcon of [true, false]) { + for (const icon of ['image', '']) { + for (const text of ['', 'Text']) { + const id = `${`dx${new Guid()}`}`; + + t.ctx.ids.push(id); + await appendElementTo(page, '#container', 'div', id, { }); + await createWidget(page, 'dxDropDownButton', { + width, + rtlEnabled, + items: [{ text: 'text1' }, { text: 'text2' }], + displayExpr: 'text', + type, + text, + icon, + stylingMode, + showArrowIcon, + splitButton, + }, `#${id}`); + } + } + } + } + } + } + }); + }); + }); + + [false, true].forEach((splitButton) => { + test('Button template', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + splitButton, + width: 200, + template: () => $('
Custom text
'), + }); + + await testScreenshot(page, `Button template, splitButton=${splitButton}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/popup.spec.ts new file mode 100644 index 000000000000..6a5360569ca3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/popup.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')}`; + +test.describe('Drop Down Button\'s 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('Popup should have correct position when DropDownButton is placed in the right bottom(T1034931)', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + items: [1, 2, 3, 4, 5, 6, 7], + elementAttr: { style: 'position: absolute; right: 10px; bottom: 10px;' }, + opened: true, + }); + + const dropDownButton = page.locator('#container'); + const dropDownButtonRect = { + top: await dropDownButton.element.getBoundingClientRectProperty('top'), + left: await dropDownButton.element.getBoundingClientRectProperty('left'), + }; + + const popupContent = page.locator('.dx-overlay-content'); + const popupContentRect = { + bottom: await popupContent.getBoundingClientRectProperty('bottom'), + left: await popupContent.getBoundingClientRectProperty('left'), + }; + + await page.expect(Math.abs(dropDownButtonRect.left - popupContentRect.left)) + .lt(1) + .expect(Math.abs(dropDownButtonRect.left - popupContentRect.left)) + .lt(1); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/fileManager/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/fileManager/common.spec.ts new file mode 100644 index 000000000000..f5e271d73600 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/fileManager/common.spec.ts @@ -0,0 +1,31 @@ +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('FileManager', () => { + 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('Custom DropDown width for Material and Fluent themes', async ({ page }) => { + await createWidget(page, 'dxFileManager', { + name: 'fileManager', + fileSystemProvider: [], + height: 450, + }); + + const viewModeButton = page.locator('.dx-filemanager-toolbar-viewmode-item'); + + await page.click(viewModeButton); + + await testScreenshot(page, 'drop down width.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/fileUploader/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/fileUploader/index.spec.ts new file mode 100644 index 000000000000..b239cde24e8c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/fileUploader/index.spec.ts @@ -0,0 +1,35 @@ +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('FileUploader - file list visibility', () => { + 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 TEST_FILE = './images/test-image-1.png'; + + [true, false].forEach((showFileList) => { + test(`FileUploader with showFileList: ${showFileList} - after file selected`, async ({ page }) => { + await createWidget(page, 'dxFileUploader', { showFileList }); + + const fileUploader = page.locator('#container'); + + await fileUploader.input.setInputFiles([TEST_FILE]); + + await testScreenshot(page, `fileuploader-show-filelist-${showFileList}.png`, { + element: '#container', + }); + + await clearUpload(fileUploader.input); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/common.spec.ts new file mode 100644 index 000000000000..321cd7141e1c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/common.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container-extended.html')}`; + +test.describe('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'); + }); + + const MENU_ITEM_CLASS = 'dx-menu-item'; + const SUBMENU_CLASS = 'dx-submenu'; + + [false, true].forEach((toolbar) => { + const selector = toolbar ? '#otherContainer' : '#container'; + const clickTarget = toolbar ? '#otherContainer .dx-bold-format' : '#container'; + const baseScreenName = toolbar ? 'htmleditor-with-toolbar' : 'htmleditor-without-toolbar'; + + test(`T1025549 - ${baseScreenName}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'box-sizing: border-box; height: 200px; width: 200px'); + await setStyleAttribute(page, '#otherContainer', 'box-sizing: border-box; height: 200px; width: 200px'); + await appendElementTo(page, '#container', 'div', 'editor'); + await appendElementTo(page, '#otherContainer', 'div', 'editorWithToolbar'); + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: '100%', + value: Array(100).fill('string').join('\n'), + }, '#editor'); + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: '100%', + value: Array(100).fill('string').join('\n'), + toolbar: { + items: ['bold', 'color'], + }, + }, '#editorWithToolbar'); + + + await testScreenshot(page, `${baseScreenName}.png`, { element: selector }); + + await page.click(Selector(clickTarget)); + + await testScreenshot(page, `${baseScreenName}-focused.png`, { element: selector }); + + }); + }); + + test('AI toolbar item', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 500, + width: 350, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = page.locator('#container'); + + await testScreenshot(page, 'htmleditor-ai-toolbar-item.png', { element: '#container' }); + + await page.click(htmlEditor.toolbar.getItemByName('ai')) + .click(page.locator(`.${SUBMENU_CLASS}`).find(`.${MENU_ITEM_CLASS}`).nth(5)); + + await testScreenshot(page, 'htmleditor-ai-toolbar-item-expanded.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageFromDevice.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageFromDevice.spec.ts new file mode 100644 index 000000000000..bd9f312fad11 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageFromDevice.spec.ts @@ -0,0 +1,123 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - upload image from device', () => { + 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 TEST_IMAGE_PATH_1 = './images/test-image-1.png'; + const TEST_IMAGE_PATH_2 = './images/test-image-2.png'; + + test('Image from device should be inserted', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await click(htmlEditor.toolbar.getItemByName('image')); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(true); + + const { fileUploader } = htmlEditor.dialog.addImageFileForm; + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_1]); + + const file = fileUploader.getFile(); + + expect(file.fileName).toBe('test-image-1.png') + + .expect(file.fileSize) + .eql('7 KB') + + .expect(file.statusMessage) + .eql('Ready to upload'); + + await fileUploader.getFile().cancelButton.element.click(); + expect(fileUploader.fileCount).toBe(0); + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(true); + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_2]); + + await testScreenshot(page, 'editor-before-click-add-button-from-device.png'); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(false); + + await htmlEditor.dialog.footerToolbar.addButton.element.click(); + + await testScreenshot(page, 'editor-after-add-image-from-device.png', { element: htmlEditor.content }); + + }); + + test('Image should be validated and inserted from device', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file'], + fileUploaderOptions: { + maxFileSize: 8500, + }, + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await click(htmlEditor.toolbar.getItemByName('image')); + + const { fileUploader } = htmlEditor.dialog.addImageFileForm; + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_2]); + + const file = fileUploader.getFile(); + + expect(file.fileName).toBe('test-image-2.png') + + .expect(file.fileSize) + .eql('10 KB') + + .expect(file.validationMessage) + .eql('File is too large'); + + await fileUploader.getFile().cancelButton.element.click() + .expect(fileUploader.fileCount) + .eql(0); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(true); + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_1]); + expect(file.fileName).toBe('test-image-1.png') + + .expect(file.fileSize) + .eql('7 KB') + + .expect(file.statusMessage) + .eql('Ready to upload'); + + await testScreenshot(page, 'editor-before-click-add-button-and-validation.png'); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(false); + + await htmlEditor.dialog.footerToolbar.addButton.element.click(); + + await testScreenshot(page, 'editor-after-click-add-button-and-validation.png', { element: htmlEditor.content }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageUrl.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageUrl.spec.ts new file mode 100644 index 000000000000..fd6ffe71f40e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageUrl.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterial } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - add image url', () => { + 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 ADD_IMAGE_POPUP_CONTENT_SELECTOR = '.dx-htmleditor-add-image-popup .dx-overlay-content'; + + test('Image uploader from url appearance', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')); + + await htmlEditor.dialog.addImageUrlForm.lockButton.element.click(); + + await htmlEditor.dialog.addImageUrlForm.url.element.click(); + + await testScreenshot(page, 'Image uploader from url appearance.png', { element: ADD_IMAGE_POPUP_CONTENT_SELECTOR }); + + }); + + test('Image url should be validate before wil be inserted by add button click', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + expect(htmlEditor.dialog.addImageUrlForm.url.isInvalid).toBe(true); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await testScreenshot(page, 'add-validated-url-image-by-click.png', { element: htmlEditor.content }); + + }); + + test('Image url should be validate before wil be inserted by add enter press', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')); + + await page.keyboard.press('Enter') + .expect(htmlEditor.dialog.addImageUrlForm.url.isInvalid) + .eql(true); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .pressKey('enter'); + + await testScreenshot(page, 'editor-add-validated-url-image-by-enter.png', { element: htmlEditor.content }); + + }); + + test('Image url should be updated', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.text) + .eql(isMaterial() ? 'ADD' : 'Add'); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await testScreenshot(page, 'editor-add-url-image-before-updated.png', { element: htmlEditor.content }); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.text) + .eql(isMaterial() ? 'UPDATE' : 'Update'); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_2, { + paste: true, + replace: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await testScreenshot(page, 'editor-add-url-image-after-updated.png', { element: htmlEditor.content }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/common.spec.ts new file mode 100644 index 000000000000..874d5c1c0bb5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/common.spec.ts @@ -0,0 +1,152 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - 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 TEST_IMAGE_PATH_1 = './images/test-image-1.png'; + + const ADD_IMAGE_POPUP_CONTENT_SELECTOR = '.dx-htmleditor-add-image-popup .dx-overlay-content'; + + test('TabPanel in HtmlEditor must have correct borders', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file', 'url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await click(htmlEditor.toolbar.getItemByName('image')); + + await testScreenshot(page, 'tabpanel-in-htmleditor.png', { + element: ADD_IMAGE_POPUP_CONTENT_SELECTOR, + }); + + }); + + test('Add button should be enabled after switch to url form', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file', 'url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .eql(true); + + await htmlEditor.dialog.tabs.getItem(1).element.click(); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(false) + + .typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + }); + + test('Add button should be disable after switch to image upload form', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url', 'file'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.addButton.element) + .expect(htmlEditor.dialog.addImageUrlForm.url.isInvalid) + .ok(); + + await htmlEditor.dialog.tabs.getItem(1).element.click(); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBeTruthy(); + + const { fileUploader } = htmlEditor.dialog.addImageFileForm; + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_1]) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + }); + + test('AddImage form shouldn\'t lead to side effects in other forms', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file', 'url'], + }, + toolbar: { items: ['image', 'link', 'color'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .ok() + + .expect(htmlEditor.dialog.footerToolbar.cancelButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.cancelButton.element); + + await page.click(htmlEditor.toolbar.getItemByName('link')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .expect(htmlEditor.dialog.footerToolbar.cancelButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await page.click(htmlEditor.toolbar.getItemByName('color')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .expect(htmlEditor.dialog.footerToolbar.cancelButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.cancelButton.element); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/aiDialog/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/aiDialog/common.spec.ts new file mode 100644 index 000000000000..0c7fb16f04e5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/aiDialog/common.spec.ts @@ -0,0 +1,286 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor: AIDialog', () => { + 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 MENU_ITEM_CLASS = 'dx-menu-item'; + const SUBMENU_CLASS = 'dx-submenu'; + const LOADINDICATOR_SEGMENT_CLASS = 'dx-loadindicator-segment'; + const LOADINDICATOR_CONTENT_CLASS = 'dx-loadindicator-content'; + const LOADINDICATOR_ICON_CLASS = 'dx-loadindicator-icon'; + const LOADINDICATOR_SEGMENT_INNER_CLASS = 'dx-loadindicator-segment-inner'; + + const longResult = getLongText(false, 10); + + export async function openAIDialog( + t: TestController, + command: number, + option?: number, + ): Promise { + const htmlEditor = page.locator('#container'); + await page.click(htmlEditor.toolbar.getItemByName('ai')) + .click(page.locator(`.${SUBMENU_CLASS} .${MENU_ITEM_CLASS}`).nth(command)); + + if (option !== undefined) { + await page.click(page.locator(`.${SUBMENU_CLASS} .${MENU_ITEM_CLASS}`).nth(command) + .find(`.${SUBMENU_CLASS} .${MENU_ITEM_CLASS}`).nth(0)); + } + + return htmlEditor; + } + + [ + { name: 'with-no-options', command: 0, option: undefined }, + { name: 'with-options', command: 4, option: 0 }, + ].forEach(({ name, command, option }) => { + test(`initial state ${name}`, async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + + await openAIDialog(t, command, option); + await testScreenshot(page, `htmleditor-ai-dialog-initial-state-${name}.png`, { element: '#container' }); + + }); + }); + + test('resize window when initial state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = await openAIDialog(t, 0); + + await resizeWindow(400, 600); + + const aiDialog = htmlEditor.getAIDialog(); + const menuButton = aiDialog.getMenuButton(); + + await menuButton.click(); + await testScreenshot(page, 'htmleditor-ai-dialog-initial-state-resize-window.png', { element: '#container' }); + + }); + + test('generating state', async ({ page }) => { + + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS}, + .${LOADINDICATOR_CONTENT_CLASS}, + .${LOADINDICATOR_ICON_CLASS}, + .${LOADINDICATOR_SEGMENT_INNER_CLASS} { + animation: none !important; + opacity: 1 !important; + } + + .${LOADINDICATOR_SEGMENT_CLASS} { + transform: scale(1) !important; + } + `); + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + changeStyle() {}, + }, + toolbar: { + items: ['ai'], + }, + }); + + await openAIDialog(t, 4, 0); + await testScreenshot(page, 'htmleditor-ai-dialog-generating-state.png', { element: '#container' }); + + }); + + [ + { + name: 'short-result', + result: 'result', + }, + { + name: 'long-result', + result: longResult, + }, + ].forEach(({ name, result }) => { + test(`resultReady state with ${name}`, async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + result, + changeStyle(_, { onComplete }) { onComplete(this.result); }, + }, + toolbar: { + items: ['ai'], + }, + }); + + + const htmlEditor = await openAIDialog(t, 4, 0); + const aiDialog = htmlEditor.getAIDialog(); + const splitButton = aiDialog.getSplitButton(); + + await splitButton.click(); + + await testScreenshot(page, `htmleditor-ai-dialog-result-ready-state-with-${name}.png`, { element: '#container' }); + + }); + }); + + test('asking state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + await openAIDialog(t, 7); + await testScreenshot(page, 'htmleditor-ai-dialog-asking-state.png', { element: '#container' }); + + }); + + test('askAI result ready state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + result: longResult, + execute(_, { onComplete }) { onComplete(this.result); }, + }, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = await openAIDialog(t, 7); + const aiDialog = htmlEditor.getAIDialog(); + const promptTextArea = aiDialog.getPromptTextArea(); + const generateButton = aiDialog.getGenerateButton(); + + await promptTextArea.getInput().fill('request'); + await generateButton.click(); + + await testScreenshot(page, 'htmleditor-ai-dialog-ask-ai-result-ready-state.png', { element: '#container' }); + + }); + + test('result ready after canceletion', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + summarize() { return () => {}; }, + }, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = await openAIDialog(t, 0); + const aiDialog = htmlEditor.getAIDialog(); + const cancelButton = aiDialog.getCancelButton(); + + await cancelButton.click(); + + await testScreenshot(page, 'htmleditor-ai-dialog-result-ready-after-canceletion.png', { element: '#container' }); + + }); + + test('error state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + summarize(_, { onError }) { onError(); }, + }, + toolbar: { + items: ['ai'], + }, + }); + + await openAIDialog(t, 0); + await testScreenshot(page, 'htmleditor-ai-dialog-error-state.png', { element: '#container' }); + + }); + + [ + { state: 'initial', configuration: {} }, + { + state: 'generating', + configuration: { + summarize() {}, + }, + }, + { + state: 'result-ready', + configuration: { + result: longResult, + summarize(_, { onComplete }) { onComplete(this.result); }, + }, + }, + { + state: 'error', + configuration: { + summarize(_, { onError }) { onError(); }, + }, + }, + ].forEach(({ state, configuration }) => { + test(`${state} state on small screen`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS}, + .${LOADINDICATOR_CONTENT_CLASS}, + .${LOADINDICATOR_ICON_CLASS}, + .${LOADINDICATOR_SEGMENT_INNER_CLASS} { + animation: none !important; + opacity: 1 !important; + } + `); + + await createWidget(page, 'dxHtmlEditor', { + height: 700, + aiIntegration: { ...configuration }, + toolbar: { + items: ['ai'], + }, + }); + + + await openAIDialog(t, 0); + await testScreenshot(page, `htmleditor-ai-dialog-${state}-state-on-small-screen.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/format.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/format.spec.ts new file mode 100644 index 000000000000..e87c9cb5aa97 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/format.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-extended.html')}`; + +test.describe('HtmlEditor - formats', () => { + 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('HtmlEditor should keep actual format after "enter" key pressed (T922236)', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 400, + width: 200, + toolbar: { + items: [ + 'bold', + { + name: 'font', + acceptedValues: ['Arial', 'Terminal'], + }, + ], + }, + }); + + const selectBox = page.locator('.dx-font-format'); + + await selectBox.click(); + + const list = await selectBox.getList(); + + await list.getItem().element.click(); + + expect(selectBox.value).toBe('Arial') + .pressKey('k') + .pressKey('enter') + .expect(selectBox.value) + .eql('Arial'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/list.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/list.spec.ts new file mode 100644 index 000000000000..76df09aaf6b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/list.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - lists', () => { + 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 orderedListMarkup = ` +
    +
  1. Item 1 +
      +
    1. +
        +
      1. +
      +
    +
  2. +
  3. Item 2 +
      +
    1. +
        +
      1. +
      +
    +
  4. +
+ `; + + const orderedListWithTextMarkup = ` +

Text

+
    +
  1. Text +
      +
    1. 1
    2. +
    3. 2
    4. +
    +
  2. +
  3. Text +
      +
    1. 1
    2. +
    3. 2
    4. +
    +
  4. +
+ `; + + test('ordered list numbering sequence should reset for each list item (T1220554)', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: 200, + value: orderedListMarkup, + }); + + await testScreenshot(page, 'htmleditor-ordered-list-appearance.png', { element: '#container' }); + + }); + + test('should reset nested ordered list counters when preceded by text (T1320286)', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: 200, + value: orderedListWithTextMarkup, + }); + + await testScreenshot(page, 'htmleditor-ordered-list-text-appearance.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/loadIndIcator/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/loadIndIcator/common.spec.ts new file mode 100644 index 000000000000..ad3a366b2a8a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/loadIndIcator/common.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const LOADINDICATOR_SEGMENT_CLASS = 'dx-loadindicator-segment'; + const LOADINDICATOR_CONTENT_CLASS = 'dx-loadindicator-content'; + const LOADINDICATOR_ICON_CLASS = 'dx-loadindicator-icon'; + const LOADINDICATOR_SEGMENT_INNER_CLASS = 'dx-loadindicator-segment-inner'; + + ['circle', 'sparkle'].forEach((animationType) => { + test(`LoadIndicator: start stage of the ${animationType} animation`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS}, + .${LOADINDICATOR_CONTENT_CLASS}, + .${LOADINDICATOR_ICON_CLASS}, + .${LOADINDICATOR_SEGMENT_INNER_CLASS} { + animation: none !important; + opacity: 1 !important; + } + `); + + if (animationType === 'sparkle') { + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS} { + transform: scale(1) !important; + } + `); + } + + await createWidget(page, 'dxLoadIndicator', { + width: 128, + height: 128, + animationType, + }); + + + await testScreenshot(page, `LoadIndicator with ${animationType} animation.png`, { + element: '#container', + }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/lookup/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/lookup/common.spec.ts new file mode 100644 index 000000000000..057b5479b719 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/lookup/common.spec.ts @@ -0,0 +1,169 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, isMaterial, isMaterialBased } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const LOOKUP_FIELD_CLASS = 'dx-lookup-field'; + const OVERLAY_CLASS = 'dx-overlay-content'; + + const stylingModes = ['outlined', 'underlined', 'filled']; + const labelModes = ['static', 'floating', 'hidden', 'outside']; + + test('Popup should not be closed if lookup is placed at the page bottom (T1018037)', async ({ page }) => { + await createWidget(page, 'dxLookup', { + items: [1, 2, 3], + usePopover: false, + }); + + const lookup = page.locator('#container'); + + const { getInstance } = lookup; + await page.evaluate(() => { + const $element = (getInstance() as any).$element(); + $element.css({ top: window.innerHeight - $element.height() }); + }); + + await lookup.open(); + + await page.expect(await lookup.isOpened()) + .ok(); + + }); + + if (isMaterial()) { + test('Popup should be flipped if lookup is placed at the page bottom', async ({ page }) => { + + await ClientFunction(() => { + const $element = $('#container'); + $element.css({ top: $(window).height() - $element.height() }); + })(); + + await createWidget(page, 'dxLookup', { + items: [1, 2, 3], + usePopover: false, + opened: true, + dropDownOptions: { + hideOnParentScroll: false, + }, + }); + + + const popupWrapper = page.locator('.dx-overlay-wrapper'); + const popupContent = page.locator('.dx-overlay-content'); + + const popupWrapperTop = await popupWrapper.getBoundingClientRectProperty('top'); + const popupContentTop = await popupContent.getBoundingClientRectProperty('top'); + + expect(popupContentTop).toBeLessThan(popupWrapperTop); + + }); + } + + if (!isMaterialBased()) { + test('Popover should have correct vertical position (T1048128)', async ({ page }) => { + await createWidget(page, 'dxLookup', { + items: Array.from(Array(100).keys()), + }); + + const lookup = page.locator('#container'); + await lookup.open(); + + const popoverArrow = page.locator('.dx-popover-arrow'); + + const lookupElementBottom = await lookup.element.getBoundingClientRectProperty('bottom'); + const popoverArrowTop = await popoverArrow.getBoundingClientRectProperty('top'); + + expect(lookupElementBottom).toBe(popoverArrowTop); + + }); + } + + test('Check popup height with no found data option', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: [], searchEnabled: true }); + + await page.locator(`.${LOOKUP_FIELD_CLASS}`).click(); + await hover(`.${OVERLAY_CLASS}`); + + await testScreenshot(page, 'Lookup with no found data.png'); + + }); + + test('Check popup height in loading state', async ({ page }) => { + await createWidget(page, 'dxLookup', { + dataSource: { + load() { + return new Promise((resolve) => { + setTimeout(() => { + resolve([1, 2, 3]); + }, 5000); + }); + }, + }, + valueExpr: 'id', + displayExpr: 'text', + }); + + await page.locator(`.${LOOKUP_FIELD_CLASS}`).click(); + await hover(`.${OVERLAY_CLASS}`); + + await testScreenshot(page, 'Lookup in loading.png'); + + }); + + test('Lookup appearance', async ({ page }) => { + + await testScreenshot(page, 'Lookup appearance.png'); + + for (const id of t.ctx.ids) { + await setStyleAttribute(page, `#${id}`, 'width: fit-content;'); + } + + await testScreenshot(page, 'Lookup width adjust to fit its content.png'); + + for (const id of t.ctx.ids) { + await setStyleAttribute(page, `#${id}`, 'width: 100px;'); + } + + await testScreenshot(page, 'Lookup appearance with limited width.png'); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + await insertStylesheetRulesToPage(page, '#container { display: grid; align-items: end; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; gap: 5px; }'); + + for (const stylingMode of stylingModes) { + for (const labelMode of labelModes) { + for (const rtlEnabled of [true, false]) { + for (const value of [null, 'Item_text_2']) { + const id = `${`dx${new Guid()}`}`; + + t.ctx.ids.push(id); + await appendElementTo(page, '#container', 'div', id, { }); + + const options: any = { + items: ['Item_text_1', 'Item_text_2'], + label: 'label text', + labelMode, + stylingMode, + rtlEnabled, + value, + }; + + await createWidget(page, 'dxLookup', options, `#${id}`); + } + } + } + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/numberBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/numberBox/label.spec.ts new file mode 100644 index 000000000000..59cf8d8c8f89 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/numberBox/label.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('NumberBox_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 NUMBERBOX_CLASS = 'dx-numberbox'; + + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const buttonsList: (TextEditorButton | NumberBoxPredefinedButton)[][] = [ + ['clear'], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear', 'spins'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }, 'spins'], + ['clear', 'spins', { name: 'custom', location: 'after', options: { icon: 'home' } }], + [{ name: 'custom', location: 'before', options: { icon: 'home' } }, 'clear', 'spins'], + ]; + + const createNumberBox = async (options?: Properties): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxNumberBox', { + value: Math.PI, + showClearButton: true, + showSpinButtons: true, + ...options, + }, `#${id}`); + + return id; + }; + test('Label for dxNumberBox', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '#container { display: flex; flex-direction: column; width: 300px; height: 400px; gap: 8px; }'); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '#container .dx-widget, #container .dx-widget input { font-family: sans-serif; }'); + } + + for (const stylingMode of stylingModes) { + const options = { + width: '100%', + label: 'label text', + stylingMode, + }; + await createNumberBox({ + ...options, + // @ts-expect-error string instead of number + value: 'text', + }); + await createNumberBox({ + ...options, + value: 123, + }); + } + + await testScreenshot(page, 'NumberBox label.png'); + + }); + + test('NumberBox with buttons container', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `#container { display: flex; flex-wrap: wrap; } .${NUMBERBOX_CLASS} { width: 220px; margin: 2px; }`); + + for (const stylingMode of stylingModes) { + for (const buttons of buttonsList) { + await createNumberBox({ stylingMode, buttons }); + } + + await createNumberBox({ stylingMode, rtlEnabled: true }); + await createNumberBox({ stylingMode, isValid: false }); + } + + await testScreenshot(page, 'NumberBox render with buttons container.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/dialog.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/dialog.spec.ts new file mode 100644 index 000000000000..941965c753bb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/dialog.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Dialog', () => { + 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 DX_DIALOG_CLASS = 'dx-dialog'; + + [ + 'alert', + 'confirm', + 'custom', + ].forEach((dialogType) => { + test(`Dialog appearance (${dialogType})`, async ({ page }) => { + + const dialogArgs = dialogType === 'custom' + ? { title: 'custom', messageHtml: 'message', buttons: [{ text: 'Custom button' }] } + : dialogType; + + await page.evaluate(() => { + const dialogFunction = (window as any).DevExpress.ui.dialog[dialogType]; + + if (dialogType === 'custom') { + dialogFunction(dialogArgs).show(); + } else { + dialogFunction(dialogArgs); + } + }); + + + await testScreenshot(page, `Dialog appearance (${dialogType}).png`); + + await page.evaluate(() => { + $(`.${DX_DIALOG_CLASS}`).remove(); + }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.drag.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.drag.spec.ts new file mode 100644 index 000000000000..bfeeaa50c089 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.drag.spec.ts @@ -0,0 +1,162 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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('Popup can not be dragged outside of the container (window)', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 100, + height: 100, + visible: true, + dragEnabled: true, + animation: undefined, + }); + + const popup = page.locator('#container'); + + const content = popup.getContent(); + const toolbar = popup.getToolbar(); + + const popupRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await (async () => { + const box = await toolbar.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 + -10000, box.y + box.height / 2 + -10000, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['bottom', 'left', 'top', 'right'], async (prop) => { + popupRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupRect.top).toBe(0); + + expect(popupRect.left).toBe(0); + + await (async () => { + const box = await toolbar.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 + 10000, box.y + box.height / 2 + 10000, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['bottom', 'left', 'top', 'right'], async (prop) => { + popupRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupRect.bottom).toBe(700); + + expect(popupRect.right).toBe(700); + + }); + + test('Popup can not be dragged if content bigger than container', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'popup', {}); + await appendElementTo(page, '#container', 'div', 'popupContainer', { width: '99px', height: '99px' }); + + await createWidget(page, 'dxPopup', { + position: { of: '#popupContainer' }, + container: '#popupContainer', + visible: true, + width: 100, + height: 100, + animation: undefined, + }, '#popup'); + + const popup = page.locator('#popup'); + + const content = popup.getContent(); + const toolbar = popup.getToolbar(); + + const popupPosition: { top: number; left: number } = { + top: 0, left: 0, + }; + + const newPopupPosition: { top: number; left: number } = { + top: 0, left: 0, + }; + + await asyncForEach(['left', 'top'], async (prop) => { + popupPosition[prop] = await content.getBoundingClientRectProperty(prop); + }); + + await (async () => { + const box = await toolbar.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 + 50, box.y + box.height / 2 + 50, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['left', 'top'], async (prop) => { + newPopupPosition[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupPosition.top).toBe(newPopupPosition.top); + + expect(popupPosition.left).toBe(newPopupPosition.left); + + }); + + test('Popup can be dragged outside of the container if dragOutsideBoundary is enabled', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 100, + height: 100, + visible: true, + dragEnabled: true, + dragOutsideBoundary: true, + animation: undefined, + }); + + const popup = page.locator('#container'); + + const content = popup.getContent(); + const toolbar = popup.getToolbar(); + + const popupPosition: { top: number; left: number } = { + top: 0, left: 0, + }; + + await (async () => { + const box = await toolbar.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 + -10000, box.y + box.height / 2 + -10000, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['left', 'top'], async (prop) => { + popupPosition[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupPosition.top).toBeLessThan(0); + + expect(popupPosition.left).toBeLessThan(0); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.spec.ts new file mode 100644 index 000000000000..f4f1a34d90b7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.spec.ts @@ -0,0 +1,167 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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('Popup should be centered regarding the container even if container is animated (T920408)', async ({ page }) => { + await page.waitForTimeout(500); + + const wrapper = page.locator('#content .dx-overlay-wrapper'); + const content = wrapper.find('.dx-overlay-content'); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + });.before(async ({ page }) => { + await appendElementTo(page, '#container', 'div', 'content', {}); + await setStyleAttribute(page, '#content', 'width: 100%; height: 100%;'); + await createWidget(page, 'dxPopup', { + width: 600, + height: 400, + visible: true, + }, undefined, { disableFxAnimation: false }); + + await appendElementTo(page, '#container', 'div', 'innerContainer', {}); + await page.waitForTimeout(500); + + await createWidget(page, 'dxPopup', { + position: { of: '#content' }, + container: '#content', + visible: true, + width: 100, + height: 100, + }, '#innerContainer', { disableFxAnimation: false }); + }); + + test('Popup wrapper left top corner should be the same as the container right left corner even if container is animated', async ({ page }) => { + await page.waitForTimeout(500); + + const wrapper = page.locator('#content .dx-overlay-wrapper'); + const container = wrapper.parent(); + + const wrapperRect: { top: number; left: number } = { top: 0, left: 0 }; + const containerRect: { top: number; left: number } = { top: 0, left: 0 }; + + await asyncForEach(['left', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + containerRect[prop] = await container.getBoundingClientRectProperty(prop); + }); + + await page.expect(wrapperRect.top) + .within(containerRect.top - 0.5, containerRect.top + 0.5); + + await page.expect(wrapperRect.left) + .within(containerRect.left - 0.5, containerRect.left + 0.5); + + });.before(async ({ page }) => { + await appendElementTo(page, '#container', 'div', 'content', {}); + await setStyleAttribute(page, '#content', 'width: 100%; height: 100%;'); + await createWidget(page, 'dxPopup', { + width: 600, + height: 400, + visible: true, + }, undefined, { disableFxAnimation: false }); + + await appendElementTo(page, '#container', 'div', 'innerContainer', {}); + await page.waitForTimeout(500); + + await createWidget(page, 'dxPopup', { + position: { of: '#content' }, + container: '#content', + visible: true, + width: 100, + height: 100, + }, '#innerContainer', { disableFxAnimation: false }); + }); + + test('There should not be any errors when position.of is html (T946851)', async ({ page }) => { + await createWidget(page, 'dxPopup', { + position: { of: 'html' }, + visible: true, + }); + + expect(true).toBeTruthy(); + + }); + + test('Popup should be centered regarding the window after position.boundary is set to window', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 300, + height: 200, + visible: true, + animation: undefined, + position: { + boundary: '#otherContainer', + }, + onShown: ClientFunction((e) => { + e.component.option('position.boundary', window); + }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + const initialRect: { + bottom: number; + top: number; + left: number; + right: number; + } = { + bottom: 0, + top: 0, + left: 0, + right: 0, + }; + const wrapperRect = initialRect; + const contentRect = initialRect; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await popup + .getWrapper() + .getBoundingClientRectProperty(prop); + contentRect[prop] = await popup.getContent() + .getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/resizeObserverIntegration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/resizeObserverIntegration.spec.ts new file mode 100644 index 000000000000..ff5fbb902da6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/resizeObserverIntegration.spec.ts @@ -0,0 +1,242 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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('Popup should be centered regarding the container even if content dimension is changed during animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await popup.show(); + await setStyleAttribute(page, '#content', 'width: 300px; height: 300px;'); + await page.waitForTimeout(100); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('Popup should be centered regarding the container even if popup dimension option is changed during animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await popup.show(); + await setStyleAttribute(page, '#content', 'width: 300px; height: 300px;'); + await page.waitForTimeout(100); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('Popup should be centered regarding the container even if content dimension is changed', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + animation: null, + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await popup.show(); + await setStyleAttribute(page, '#content', 'width: 300px; height: 300px;'); + await page.waitForTimeout(100); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('popup should be repositioned after window resize', async ({ page }) => { + await createWidget(page, 'dxPopup', { + animation: null, + visible: true, + width: 100, + height: 100, + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('Popup dimensions should be correct after width or height animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + visible: true, + animation: { + show: { + from: { width: '10px', height: '10px' }, + to: { width: '300px', height: '300px' }, + }, + }, + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + const content = popup.getContent(); + + await page.waitForTimeout(500); + + const contentRect: { width: number; height: number } = { + width: 0, height: 0, + }; + + await asyncForEach(['width', 'height'], async (prop) => { + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(contentRect.width).toBe(300); + + expect(contentRect.height).toBe(300); + + }); + + test('Showing and shown events should be raised only once even after resize during animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await page.evaluate(() => { + (window as any).shownCallCount = 0; + (window as any).showingCallCount = 0; + }); + + const incShown = async () => page.evaluate(() => { ((window as any).shownCallCount as number) += 1; }); + const incShowing = async () => page.evaluate(() => { ((window as any).showingCallCount as number) += 1; }); + + const getShownCounter = async () => page.evaluate(() => (window as any).shownCallCount); + const getShowingCounter = async () => page.evaluate(() => (window as any).shownCallCount); + + await popup.option({ + onShown: incShown, + onShowing: incShowing, + }); + + await popup.show(); + + await page.expect(await getShownCounter()) + .eql(1); + await page.expect(await getShowingCounter()) + .eql(1); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/scrolling.spec.ts new file mode 100644 index 000000000000..81d26162ed25 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/scrolling.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo, insertStylesheetRulesToPage, isMaterialBased } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Popup 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 POPUP_CONTENT_CLASS = 'dx-popup-content'; + + if (!isMaterialBased()) { + [false, true].forEach((shading) => { + [false, true].forEach((enableBodyScroll) => { + [false, true].forEach((fullScreen) => { + test(`Popup native scrolling, shading: ${shading}, enableBodyScroll: ${enableBodyScroll}, fullScreen: ${fullScreen}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable-container', { height: '2000px', overflowY: 'auto' }); + await appendElementTo(page, '#scrollable-container', 'div', 'scrollable-content', { height: '3000px' }); + + await appendElementTo(page, '#scrollable-content', 'div', 'inner-container', { + width: '500px', height: '500px', border: '1px solid black', overflow: 'auto', + }); + + await ClientFunction(() => { + const $content = $('
'); + + for (let i = 0; i < 100; i += 1) { + $content.append(`
${i}
`); + } + + $('#scrollable-content').append($content); + })(); + + await appendElementTo(page, '#inner-container', 'div', 'inner-content', { width: '2000px', height: '2000px' }); + await appendElementTo(page, '#scrollable-container', 'div', 'popup', {}); + + await createWidget(page, 'dxPopup', { + width: 400, + height: 400, + shading, + enableBodyScroll, + fullScreen, + contentTemplate: ($content) => { + const popupContent = '\ +
Description
\ +
In the heart of LA\'s business district, the Downtown Inn has a welcoming staff and award winning restaurants that remain open 24 hours a day. Use our conference room facilities to conduct meetings and have a drink at our beautiful rooftop bar.
\ +
\ +
\ +
\ +
Features
\ +
\ +
Concierge
\ +
Restaurant
\ +
Valet Parking
\ +
Fitness Center
\ +
Sauna
\ +
Airport Shuttle
\ +
\ +
\ +
\ +
\ +
Rooms
\ +
\ +
Climate control
\ +
Air conditioning
\ +
Coffee/tea maker
\ +
Iron/ironing
\ +
\ +
\ +
\ +
\ +
In the heart of LA\'s business district, the Downtown Inn has a welcoming staff and award winning restaurants that remain open 24 hours a day. Use our conference room facilities to conduct meetings and have a drink at our beautiful rooftop bar.
\ + '; + + $content.html(popupContent); + }, + }, '#popup'); + + + const popup = page.locator('#popup'); + + const checkBodyStyles = async ({ paddingRight, overflow }) => { + await page.expect(getComputedPropertyValue('body', 'padding-right')) + .eql(paddingRight) + .expect(getComputedPropertyValue('body', 'overflow')) + .eql(overflow) + .expect(getComputedPropertyValue('body', 'position')) + .eql('static') + .expect(getComputedPropertyValue('body', 'top')) + .eql('auto') + .expect(getComputedPropertyValue('body', 'left')) + .eql('auto'); + }; + + const checkPopupStyles = async ({ overflow, overScrollBehavior }) => { + await page.expect(getComputedPropertyValue(`.${POPUP_CONTENT_CLASS}`, 'overflow')) + .eql(overflow) + .expect(getComputedPropertyValue(`.${POPUP_CONTENT_CLASS}`, 'overscroll-behavior')) + .eql(overScrollBehavior); + }; + + await checkBodyStyles({ paddingRight: '0px', overflow: 'visible' }); + + await insertStylesheetRulesToPage(page, 'body { padding-right: 10px; overflow: auto; }'); + + await page.evaluate(() => { + window.scrollTo(0, 300); + }); + + await page.expect(getDocumentScrollTop()) + .eql(300); + + await checkBodyStyles({ paddingRight: '10px', overflow: 'auto' }); + await page.expect(getDocumentScrollTop()) + .eql(300); + + await popup.show(); + + await checkPopupStyles({ overflow: 'auto', overScrollBehavior: 'contain' }); + await checkBodyStyles({ paddingRight: enableBodyScroll ? '10px' : '25px', overflow: enableBodyScroll ? 'auto' : 'hidden' }); + await page.expect(getDocumentScrollTop()) + .eql(300); + + await popup.hide(); + + await checkBodyStyles({ paddingRight: '10px', overflow: 'auto' }); + await page.expect(getDocumentScrollTop()) + .eql(300); + + }); + }); + }); + }); + } +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toast.spec.ts new file mode 100644 index 000000000000..2e81baa5ea2a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toast.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const types = ['info', 'warning', 'error', 'success']; + const STACK_CONTAINER_SELECTOR = '.dx-toast-stack'; + + const showToast = ClientFunction( + (type) => { + (window as any).DevExpress.ui.notify( + { + message: `Toast ${type}`, + type, + displayTime: 35000000, + animation: { + show: { + type: 'fade', duration: 0, + }, + hide: { type: 'fade', duration: 0 }, + }, + }, + { + position: 'top center', + direction: 'down-push', + }, + + }, + + const hideAllToasts = async () => page.evaluate(() => { + (window as any).DevExpress.ui.hideToasts(); + }); + + test('Toasts', async ({ page }) => { + + await Promise.all(types.map((type) => showToast(type))); + + await insertStylesheetRulesToPage(page, `${STACK_CONTAINER_SELECTOR} { padding: 20px; }`); + await setClassAttribute(page, Selector(STACK_CONTAINER_SELECTOR), `dx-theme-${(process.env.theme ?? 'fluent.blue.light')}-typography`); + + await testScreenshot(page, 'Toasts.png', { element: STACK_CONTAINER_SELECTOR }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toolbarIntegration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toolbarIntegration.spec.ts new file mode 100644 index 000000000000..5251548c9df2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toolbarIntegration.spec.ts @@ -0,0 +1,242 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Popup_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'); + }); + + const COMPONENT_SELECTOR = '#container'; + const CLOSE_BUTTON_SELECTOR = '.dx-closebutton'; + const ANIMATION_DELAY = 500; + + [ + { name: 'dxPopup', Class: Popup }, + { name: 'dxPopover', Class: Popover }, + ].forEach(({ name, Class }) => { + ['bottom', 'top'].forEach((toolbar) => { + [true, false].forEach((rtlEnabled) => { + test(`Extended toolbar should be used in ${name},rtlEnabled=${rtlEnabled},toolbar=${toolbar}`, async ({ page }) => { + + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-content, .dx-overlay-content input { font-family: sans-serif !important; }'); + } + await createWidget(page, name as 'dxPopup' | 'dxPopover', { + showCloseButton: true, + contentTemplate: () => $('
').text('\ + Lorem Ipsum is simply dummy text of the printing and typesetting industry.\ + Lorem Ipsum has been the industrys standard dummy text ever since the 1500s,\ + when an unknown printer took a galley of type and scrambled it to make a type specimen book.\ + '), + width: '60%', + height: 300, + showTitle: true, + rtlEnabled, + visible: true, + animation: undefined, + target: COMPONENT_SELECTOR, + hideOnOutsideClick: true, + toolbarItems: [{ + location: 'before', + widget: 'dxButton', + options: { + icon: 'back', + }, + toolbar, + }, { + location: 'before', + widget: 'dxButton', + locateInMenu: 'auto', + options: { + icon: 'refresh', + }, + toolbar, + }, { + location: 'center', + locateInMenu: 'never', + template() { + return $('
Popup\'s title
'); + }, + toolbar, + }, { + location: 'after', + widget: 'dxSelectBox', + locateInMenu: 'auto', + options: { + width: 140, + items: [1, 2, 3, 4, 5], + value: 3, + }, + toolbar, + }, { + location: 'after', + widget: 'dxButton', + locateInMenu: 'auto', + options: { + icon: 'plus', + }, + toolbar, + }, { + locateInMenu: 'always', + widget: 'dxButton', + options: { + icon: 'save', + text: 'Save', + }, + toolbar, + }, { + widget: 'dxButton', + toolbar: toolbar === 'top' + ? 'bottom' + : 'top', + location: 'before', + options: { + icon: 'email', + }, + }, { + widget: 'dxButton', + toolbar: toolbar === 'top' + ? 'bottom' + : 'top', + location: 'after', + options: { + text: 'Close', + }, + }], + }); + + + const instance = new Class(COMPONENT_SELECTOR); + + if (toolbar === 'top') { + const topToolbar = new Toolbar(instance.getToolbar()); + await topToolbar.option('overflowMenuVisible', true); + } else { + const bottomToolbar = new Toolbar(instance.getBottomToolbar()); + await bottomToolbar.option('overflowMenuVisible', true); + } + + await hover(Selector(CLOSE_BUTTON_SELECTOR)); + + await testScreenshot(page, `${name.replace('dx', '')}_${toolbar}_toolbar_menu,rtlEnabled=${rtlEnabled}.png`); + + }); + }); + }); + }); + + function getItemConfig( + text: string, + toolbar: 'top' | 'bottom' = 'top', + location: 'before' | 'center' | 'after' = 'after', + locateInMenu: 'auto' | 'none' = 'none', + ) { + return { + text, + toolbar, + locateInMenu, + location, + }; + } + + const toolbarItems = [ + getItemConfig('First Item'), + getItemConfig('Second Item', 'top', 'after', 'auto'), + getItemConfig('Third Item', 'top', 'after', 'auto'), + getItemConfig('!@#$%^&*()-+=[]{}<>|:;.,!?~^*_(){}<>[]:-=+', 'bottom', 'before'), + getItemConfig('First Item', 'bottom'), + getItemConfig('Second Item', 'bottom', 'after', 'auto'), + getItemConfig('Third Item', 'bottom', 'after', 'auto'), + ]; + + const baseConfiguration = { + title: '!@#$%^&*()-+=[]{}<>|:;.,!?~^*_(){}<>[]:-=+', + width: 'auto', + height: 'auto', + showCloseButton: false, + contentTemplate: () => $('
') + .width(300) + .height(300), + }; + + test('Popup toolbars with wide elements and overflow menu if hidden on init with toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopup', { + ...baseConfiguration, + toolbarItems, + visible: false, + }); + + const instance = new Popup(COMPONENT_SELECTOR); + await instance.option({ visible: true }); + + await page.wait(ANIMATION_DELAY) + .click(instance.getOverflowButton().element); + + await testScreenshot(page, 'Popup toolbars with wide elements and overflow menu before items rebinding.png'); + + const items = await instance.option('toolbarItems'); + items[2].visible = false; + await instance.option('toolbarItems', [...items]); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Popup toolbars with wide elements and overflow menu after items rebinding.png'); + + }); + + test('Popup toolbars with wide elements and overflow menu if hidden on init with no toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopup', { + ...baseConfiguration, + toolbarItems: [], + visible: false, + }); + + const instance = new Popup(COMPONENT_SELECTOR); + await instance.option({ visible: true, toolbarItems }); + + await page.wait(ANIMATION_DELAY) + .click(instance.getOverflowButton().element); + + await testScreenshot(page, 'Toolbar before items rebinding if it was hidden without items on init.png'); + + const items = await instance.option('toolbarItems'); + items[2].visible = false; + await instance.option('toolbarItems', [...items]); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Toolbar after items rebinding if it was hidden without items on init.png'); + + }); + + test('Popup toolbars with wide elements and overflow menu if shown on init with toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopup', { + ...baseConfiguration, + toolbarItems, + visible: true, + }); + + const instance = new Popup(COMPONENT_SELECTOR); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Toolbar before items rebinding if it was visible with items on init.png'); + + const items = await instance.option('toolbarItems'); + items[2].visible = false; + await instance.option('toolbarItems', [...items]); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Toolbar after items rebinding if it was visible with items on init.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/common.spec.ts new file mode 100644 index 000000000000..682e19800b9c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/common.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Radio Group', () => { + 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('Radio buttons placed into the template should not be selected after clicking the parent radio button (T816449)', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { + items: [{}, {}, {}], + itemTemplate: () => ($('
') as any).dxRadioGroup({ + dataSource: [{}, {}, {}], + layout: 'horizontal', + }), + }); + + const parentGroup = page.locator('#container'); + const firstChildGroup = new RadioGroup(parentGroup.getItem().content.child().nth(0)); + const secondChildGroup = new RadioGroup(parentGroup.getItem(1).content.child()); + const thirdChildGroup = new RadioGroup(parentGroup.getItem(2).content.child()); + + const checkGroup = async ( + group: RadioGroup, + firstChecked = false, + secondChecked = false, + thirdChecked = false, + ): Promise => { + await page.expect(group.getItem().radioButton.isChecked).eql(firstChecked) + .expect(group.getItem(1).radioButton.isChecked).eql(secondChecked) + .expect(group.getItem(2).radioButton.isChecked) + .eql(thirdChecked); + }; + + await checkGroup(parentGroup); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await parentGroup.getItem().radioButton.element.click(); + await checkGroup(parentGroup, true); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await parentGroup.getItem(1).radioButton.element.click(); + await checkGroup(parentGroup, false, true); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await parentGroup.getItem(2).radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await firstChildGroup.getItem().radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup, true); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await secondChildGroup.getItem(1).radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup, true); + await checkGroup(secondChildGroup, false, true); + await checkGroup(thirdChildGroup); + + await thirdChildGroup.getItem(2).radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup, true); + await checkGroup(secondChildGroup, false, true); + await checkGroup(thirdChildGroup, false, false, true); + + }); + + test('Dot of Radio button placed in scaled container should have valid centering(T1165339)', async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 100px;'); + + await appendElementTo(page, '#container', 'div', 'radioGroup'); + await setStyleAttribute(page, '#radioGroup', 'transform: scale(0.7);'); + + await createWidget(page, 'dxRadioGroup', { + items: ['One', 'Two', 'Three'], + value: 'Two', + }, '#radioGroup'); + + await testScreenshot(page, 'RadioGroup in scaled container.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/validationMessage.spec.ts new file mode 100644 index 000000000000..4bdd66814d3c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/validationMessage.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')}`; + +test.describe('Radio Group Validation Message', () => { + 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 RADIO_GROUP_CLASS = 'dx-radiogroup'; + + test('message position is right (T1020449)', async ({ page }) => { + await createWidget(page, 'dxForm', { + width: 300, + height: 400, + items: [{ + itemType: 'simple', + dataField: 'PropertyNameId', + editorOptions: { + dataSource: ['HR Manager', 'IT Manager'], + layout: 'horizontal', + }, + editorType: 'dxRadioGroup', + validationRules: [{ + type: 'required', + message: 'The PropertyNameId field is required.', + }], + }, { + itemType: 'button', + horizontalAlignment: 'left', + buttonOptions: { + text: 'Register', + type: 'success', + useSubmitBehavior: true, + }, + }], + }); + + const form = page.locator('#container'); + + await form.validate(); + + const radioGroup = page.locator(`.${RADIO_GROUP_CLASS}`); + + await radioGroup.focus(); + + await testScreenshot(page, 'RadioGroup horizontal validation.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/actionButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/actionButton.spec.ts new file mode 100644 index 000000000000..4dc64e88e723 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/actionButton.spec.ts @@ -0,0 +1,189 @@ +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.html')}`; + +test.describe('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'); + }); + + const purePressKey = async (t, key): Promise => { + await page.pressKey(key) + .wait(100); + }; + + test('Click on action button should correctly work with SelectBox containing the field template (T811890)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + fieldTemplate: (value) => ($('
') as any).dxTextBox({ value }), + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + onClick: () => { + (getInstance() as any).option('value', 'item2'); + (getInstance() as any).focus(); // NOTE: need because of editor input rerendering + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + await selectBox.click(); + await purePressKey(t, 'alt+up'); + expect(selectBox.isFocused).toBeTruthy() + .expect(await selectBox.isOpened()) + .notOk(); + + const actionButton = selectBox.getButton(0); + await actionButton.click() + .expect(selectBox.isFocused).ok() + .expect(selectBox.value) + .eql('item2'); + + }); + + test('Click on action button after typing should correctly work with SelectBox containing the field template (T811890)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + fieldTemplate: (value) => ($('
') as any).dxTextBox({ value }), + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + onClick: () => { + (getInstance() as any).option('value', 'item2'); + (getInstance() as any).focus(); // NOTE: need because of editor input rerendering + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + await selectBox.click(); + await purePressKey(t, 'alt+up'); + expect(selectBox.isFocused).toBeTruthy() + .expect(await selectBox.isOpened()) + .notOk(); + + const actionButton = selectBox.getButton(0); + + await selectBox.input.fill('tt'); + await actionButton.click() + .expect(selectBox.isFocused).ok() + .expect(selectBox.value) + .eql('item2'); + + }); + + test('editor can be focused out after click on action button', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + onClick: () => { + (getInstance() as any).option('value', 'item2'); + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + await selectBox.click(); + expect(selectBox.isFocused).toBeTruthy(); + + const actionButton = selectBox.getButton(0); + await actionButton.click() + .expect(selectBox.isFocused).ok(); + + await purePressKey(t, 'tab'); + expect(selectBox.isFocused).toBeFalsy(); + + }); + + test('selectbox should not be opened after click on disabled action button (T1117453)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + value: 'item1', + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + type: 'default', + disabled: true, + onClick: () => { + (getInstance() as any).option('value', 'item2'); + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + const actionButton = selectBox.getButton(0); + await actionButton.click() + .expect(selectBox.isFocused) + .notOk() + .expect(await selectBox.isOpened()) + .notOk() + .expect(selectBox.value) + .eql('item1'); + + }); + + test('SelectBox: positioning content in the custom dropdown button', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'selectBox'); + + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + value: 'item1', + dropDownButtonTemplate() { + return 'X'; + }, + }, '#container'); + + await testScreenshot(page, 'SelectBox Customize DropDown Button.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/common.spec.ts new file mode 100644 index 000000000000..de52779b9a96 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/common.spec.ts @@ -0,0 +1,123 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('SelectBox placeholder', () => { + 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('Placeholder is visible after items option change when value is not chosen (T1099804)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'selectBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxSelectBox', { + width: '100%', + placeholder: 'Choose a value', + }, '#selectBox'); + + const selectBox = page.locator('#selectBox'); + + await selectBox.option('items', [1, 2, 3]); + await testScreenshot(page, 'SelectBox placeholder after items change if value is not choosen.png', { element: '#container' }); + + }); + + test('Pages should be loaded consistently after closing the dropdown popup and filtering the data (T1274576)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'selectBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxSelectBox', () => { + const data: { id: number; text: string; anotherId: number }[] = []; + + for (let index = 0; index < 100; index += 1) { + data.push({ + id: index + 1, + text: `item ${index + 1}`, + anotherId: index % 2 === 0 ? 1 : 2, + }); + } + + const sampleAPI = new (window as any).DevExpress.data.ArrayStore({ key: 'id', data }); + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load(loadOptions) { + return new Promise((resolve) => { + setTimeout(() => { + sampleAPI.load(loadOptions).done((items) => { + resolve(items); + }); + }, 100); + }); + }, + totalCount(loadOptions) { + return sampleAPI.totalCount(loadOptions); + }, + byKey(key) { + return sampleAPI.byKey(key); + }, + }); + + return { + dataSource: { + store, + paginate: true, + pageSize: 6, + }, + valueExpr: 'id', + displayExpr: 'text', + }; + }, '#selectBox'); + + const selectBox = page.locator('#selectBox'); + + await selectBox.option('opened', true); + + const list = await selectBox.getList(); + const items = list.getItems(); + + expect(items.count).toBe(12) + .expect(items.nth(0).textContent) + .eql('item 1') + .expect(items.nth(11).textContent) + .eql('item 12'); + + const scrollingDistance = 50; + await list.scrollTo(scrollingDistance); + await page.waitForTimeout(500); + + await page.locator('body').click(); + + const { getInstance } = selectBox; + + await ClientFunction( + () => { + const dataSource = (getInstance() as any).getDataSource(); + dataSource.filter(['anotherId', '=', 2]); + dataSource.load(); + }, + { dependencies: { getInstance } }, + )(); + await page.waitForTimeout(500); + + await selectBox.option('opened', true); + + await page.waitForTimeout(500); + + expect(items.count).toBe(12) + .expect(items.nth(0).textContent) + .eql('item 2') + .expect(items.nth(11).textContent) + .eql('item 24'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/label.spec.ts new file mode 100644 index 000000000000..23ba23e39e0d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/label.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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 labelMods = ['floating', 'static', 'outside']; + const stylingModes = ['outlined', 'underlined', 'filled']; + + stylingModes.forEach((stylingMode) => { + labelMods.forEach((labelMode) => { + test(`Label for dxSelectBox labelMode=${labelMode} stylingMode=${stylingMode}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 400px; padding: 8px;'); + + await appendElementTo(page, '#container', 'div', 'selectBox1'); + await appendElementTo(page, '#container', 'div', 'selectBox2'); + + await createWidget(page, 'dxSelectBox', { + width: 100, + label: 'label', + text: '', + labelMode, + stylingMode, + }, '#selectBox1'); + + await createWidget(page, 'dxSelectBox', { + label: `this label is ${'very '.repeat(10)}long`, + text: `this content is ${'very '.repeat(10)}long`, + items: ['item1', 'item2'], + labelMode, + stylingMode, + }, '#selectBox2'); + + + const selectBox2 = page.locator('#selectBox2'); + + await page.locator('#selectBox2').click(); + + await testScreenshot(page, `SelectBox with label-labelMode=${labelMode}-stylingMode=${stylingMode}.png`, { element: '#container' }); + + await click(await selectBox2.getPopup()); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/popup.spec.ts new file mode 100644 index 000000000000..2bd12c665a33 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/popup.spec.ts @@ -0,0 +1,138 @@ +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('popup height after load', () => { + 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('SelectBox without data', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await testScreenshot(page, 'SelectBox no data.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox has a correct popup height for the first opening if the pageSize is equal to dataSource length (T942881)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3], + paginate: true, + pageSize: 3, + }); + + await testScreenshot(page, 'SelectBox pagesize equal datasource items count.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox has a correct popup height for the first opening if the pageSize is less than dataSource items count', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3], + paginate: true, + pageSize: 2, + }); + + await testScreenshot(page, 'SelectBox pagesize less datasource items count.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox has a correct popup height for the first opening if the pageSize is more than dataSource items count', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3], + paginate: true, + pageSize: 5, + }); + + await testScreenshot(page, 'SelectBox pagesize more datasource items count.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox does not change a popup height after load the last page', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3, 4, 5], + paginate: true, + pageSize: 2, + }); + + const list = await selectBox.getList(); + await list.scrollTo(100); + + await testScreenshot(page, 'SelectBox popup height after last page load.png'); + + await click(await selectBox.getPopup()); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/toolbarIntegration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/toolbarIntegration.spec.ts new file mode 100644 index 000000000000..bd7ae52b3897 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/toolbarIntegration.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('SelectBox as Toolbar item', () => { + 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('SelectBox should correctly render its buttons if editor is rendered as a Toolbar item with fieldTemplate (T949859)', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { + widget: 'dxSelectBox', + options: { + buttons: [ + { + name: 'test', + options: { + text: 'test', + }, + }, + ], + fieldTemplate: (_, wrapper) => { + ($('
').appendTo(wrapper) as any).dxTextBox(); + }, + items: [1, 2, 3, 4], + }, + }, + ], + }); + + const selectBox = page.locator('#container'); + const actionButton = selectBox.getButton(0); + + await page.expect(actionButton.getText().innerText) + .eql(isMaterial() ? 'TEST' : 'test'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/slider/slider.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/slider/slider.spec.ts new file mode 100644 index 000000000000..8118350c0ef4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/slider/slider.spec.ts @@ -0,0 +1,29 @@ +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('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('Slider appearance', async ({ page }) => { + await createWidget(page, 'dxSlider', { + tooltip: { + enabled: true, + showMode: 'always', + position: 'bottom', + }, + }); + + await testScreenshot(page, 'slider-appearance.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/common.spec.ts new file mode 100644 index 000000000000..8a9173b90058 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/common.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')}`; + +test.describe('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('Keyboard navigation should work then tagBox is focused or list is focused', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + applyValueMode: 'useButtons', + }); + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + expect(tagBox.isFocused).toBeTruthy() + .expect(await tagBox.isOpened()) + .ok(); + + const list = await tagBox.getList(); + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + const firstItemCheckBox = list.getItem().checkBox; + const secondItemCheckBox = list.getItem(1).checkBox; + const thirdItemCheckBox = list.getItem(2).checkBox; + + await t + // List is focused + .pressKey('tab') + .expect(selectAllCheckBox.isFocused).ok() + .pressKey('down down down') + .expect(thirdItemCheckBox.isFocused) + .ok() + .pressKey('down') + .expect(selectAllCheckBox.isFocused) + .ok() + .pressKey('up up up') + .expect(firstItemCheckBox.isFocused) + .ok() + .expect(firstItemCheckBox.isChecked) + .notOk() + .pressKey('space') + .expect(firstItemCheckBox.isChecked) + .ok() + .pressKey('enter') + .expect(firstItemCheckBox.isChecked) + .notOk() + + // TagBox is focused + .pressKey('shift+tab') + .expect(tagBox.isFocused) + .ok() + .pressKey('down') + .expect(secondItemCheckBox.isFocused) + .ok() + .pressKey('down down') + .expect(selectAllCheckBox.isFocused) + .ok() + .pressKey('up up up') + .expect(firstItemCheckBox.isFocused) + .ok() + .expect(firstItemCheckBox.isChecked) + .notOk() + .pressKey('space') + .expect(firstItemCheckBox.isChecked) + .ok() + .pressKey('enter') + .expect(firstItemCheckBox.isChecked) + .notOk(); + + }); + + test('Select all checkbox should be focused by tab and closed by escape (T389453)', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + applyValueMode: 'useButtons', + }); + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + expect(tagBox.isFocused).toBeTruthy() + .expect(await tagBox.isOpened()) + .ok(); + + const list = await tagBox.getList(); + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + + await page.keyboard.press('Tab') + .expect(tagBox.isFocused).notOk() + .expect(selectAllCheckBox.isFocused) + .ok() + + .pressKey('shift+tab') + .expect(tagBox.isFocused) + .ok() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('tab') + .expect(tagBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .ok(); + + await page.keyboard.press('esc'); + + expect(tagBox.isFocused).toBeTruthy() + .expect(await tagBox.isOpened()) + .notOk(); + + }); + + test('TagBox with selection controls', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + items: [1, 2, 3, 4, 5, 6, 7], + showSelectionControls: true, + width: 300, + }); + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + await testScreenshot(page, 'TagBox with selection controls.png'); + + }); + + test('Placeholder is visible after items option change when value is not chosen (T1099804)', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + width: 300, + placeholder: 'Choose a value', + }); + + const tagBox = page.locator('#container'); + + await tagBox.option('items', [1, 2, 3]); + + await testScreenshot(page, 'TagBox placeholder if value is not choosen.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/label.spec.ts new file mode 100644 index 000000000000..bac1230be689 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/label.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TagBox_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 stylingModes = ['outlined', 'underlined', 'filled']; + const labelModes = ['static', 'floating', 'hidden', 'outside']; + + stylingModes.forEach((stylingMode) => { + test(`Label for dxTagBox stylingMode=${stylingMode}`, async ({ page }) => { + + const componentOptions = { + label: 'label text', + items: [...Array(10)].map((_, i) => `item${i}`), + value: [...Array(5)].map((_, i) => `item${i}`), + stylingMode, + }; + + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '#container .dx-widget { font-family: sans-serif }'); + } + + await appendElementTo(page, '#container', 'div', 'tagBox1', { }); + await appendElementTo(page, '#container', 'div', 'tagBox2', { }); + + await createWidget(page, 'dxTagBox', { + ...componentOptions, + multiline: false, + }, '#tagBox1'); + + await createWidget(page, 'dxTagBox', { + ...componentOptions, + multiline: true, + }, '#tagBox2'); + + + await page.locator('#tagBox2').click(); + + await testScreenshot(page, `TagBox label with stylingMode=${stylingMode}.png`); + + }); + + labelModes.forEach((labelMode) => { + test(`Label shouldn't be cutted for dxTagBox in stylingMode=${stylingMode}, labelMode=${labelMode} (T1104913)`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'top: 250px;'); + + await createWidget(page, 'dxTagBox', { + width: 200, + label: 'Label text', + labelMode, + stylingMode, + dataSource: { + load() { + return new Promise((resolve) => { + resolve([ + { text: 'item_1' }, + { text: 'item_2' }, + { text: 'item_3' }, + { text: 'item_4' }, + ]); + }); + }, + paginate: true, + pageSize: 20, + }, + }); + + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + const screenshotName = `TagBox label with stylingMode=${stylingMode},labelMode=${labelMode}.png`; + + await tagBox.click(); + await tagBox.click(); + + await testScreenshot(page, screenshotName); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textArea/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/index.spec.ts new file mode 100644 index 000000000000..c37716ba7ff9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/index.spec.ts @@ -0,0 +1,182 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TextArea_Height', () => { + 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 text = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.'; + + test('TextArea should have correct height when height is 7em & maxHeight is 5em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + maxHeight: '5em', + height: '7em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, height=7em & maxHeight=5em.png', { element: '#container' }); + + }); + + test('TextArea should have correct height when height is 5em & maxHeight is 7em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + maxHeight: '7em', + height: '5em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, height=5em & maxHeight=7em.png', { element: '#container' }); + + }); + + test('TextArea should have correct height when maxHeight is 5em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + maxHeight: '5em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight=5em.png', { element: '#container' }); + + }); + + test('TextArea with font-size style has correct height when maxHeight option is 5em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px; font-size: 12px;'); + + const config = { + maxHeight: '5em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight=5em, font-size=12px.png', { element: '#container' }); + + }); + + test('TextArea has correct height when maxHeight is not defined', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px;'); + + const config = { + width: '100%', + value: text, + autoResizeEnabled: true, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + value: text + text, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight is not defined.png', { element: '#container' }); + + }); + + test('Height of TextArea input should have the correct height when the maxHeight option is set to 80px (T1221869)', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + value: text, + width: '100%', + maxHeight: 80, + autoResizeEnabled: true, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight=80px.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textArea/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/label.spec.ts new file mode 100644 index 000000000000..9dd02751ca0a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/label.spec.ts @@ -0,0 +1,67 @@ +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.html')}`; + +test.describe('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 labelModes = ['floating', 'static', 'outside']; + const stylingModes = ['outlined', 'underlined', 'filled']; + + test('Label scroll input dxTextArea', async ({ page }) => { + await createWidget(page, 'dxTextArea', { + height: 50, + width: 200, + text: `this content is ${'very '.repeat(10)}long`, + label: 'label text', + }); + + const textArea = page.locator('#container'); + + await scroll(textArea.getInput(), 0, 20); + + await testScreenshot(page, 'TextArea label after scroll.png', { element: '#container' }); + + }); + + stylingModes.forEach((stylingMode) => { + labelModes.forEach((labelMode) => { + test(`Label for dxTextArea labelMode=${labelMode} stylingMode=${stylingMode}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'textArea1', { }); + await appendElementTo(page, '#container', 'div', 'textArea2', { }); + + await createWidget(page, 'dxTextArea', { + width: 100, + label: 'label', + text: '', + labelMode, + stylingMode, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + label: `this label is ${'very '.repeat(10)}long`, + text: `this content is ${'very '.repeat(10)}long`, + items: ['item1', 'item2'], + labelMode, + stylingMode, + }, '#textArea2'); + + + await page.locator('#textArea2').click(); + + await testScreenshot(page, `TextArea with label-labelMode=${labelMode}-stylingMode=${stylingMode}.png`); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/label.spec.ts new file mode 100644 index 000000000000..db3197e78796 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/label.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, setClassAttribute, insertStylesheetRulesToPage, removeStylesheetRulesFromPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TextBox_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 visibleLabelModes: LabelMode[] = ['floating', 'static', 'outside']; + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const buttonsList: (string | TextEditorButton)[][] = [ + ['clear'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear'], + ['clear', { name: 'custom', location: 'before', options: { icon: 'home' } }], + ]; + + const TEXTBOX_CLASS = 'dx-textbox'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + const READONLY_STATE_CLASS = 'dx-state-readonly'; + const INVALID_STATE_CLASS = 'dx-invalid'; + + const createTextBox = async (options?: Properties, state?: string): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxTextBox', { + labelMode: 'floating', + stylingMode: 'outlined', + text: 'Text', + label: 'Label Text', + ...options, + }, `#${id}`); + + if (state) { + await setClassAttribute(page, `#${id}`, state); + } + + return id; + }; + + [ + { labelMode: 'static', expectedWidths: { generic: 82, material: 68, fluent: 74 } }, + { labelMode: 'floating', expectedWidths: { generic: 82, material: 68, fluent: 74 } }, + { labelMode: 'outside', expectedWidths: { generic: 'none', material: 'none', fluent: 'none' } }, + ].forEach(({ labelMode, expectedWidths }) => { + test(`Label max-width should be changed after container width was changed, labelMode is ${labelMode}`, async ({ page }) => { + const textBox = page.locator('#container'); + + const expectedWidth = expectedWidths[(process.env.theme ?? 'fluent.blue.light')]; + + await page.expect(textBox.getLabel().getStyleProperty('max-width')) + .eql(expectedWidth === 'none' ? 'none' : `${expectedWidth}px`); + + await setStyleAttribute(page, page.locator(`#${await textBox.element.getAttribute('id')}`), `width: ${t.ctx.initialWidth + t.ctx.deltaWidth}px;`); + + await page.expect(textBox.getLabel().getStyleProperty('max-width')) + .eql(expectedWidth === 'none' ? 'none' : `${expectedWidth + t.ctx.deltaWidth}px`); + + });.before(async ({ page }) => { + t.ctx.initialWidth = 100; + t.ctx.deltaWidth = 300; + + await createWidget(page, 'dxTextBox', { + width: t.ctx.initialWidth, + label: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + labelMode, + }); + }); + }); + + test('Textbox render', async ({ page }) => { + + for (const stylingMode of stylingModes) { + for (const labelMode of visibleLabelModes) { + for (const placeholder of ['Placeholder', '']) { + await createTextBox({ + text: undefined, + placeholder, + stylingMode, + labelMode, + }); + } + + await createTextBox({ text: 'Text value' }); + await createTextBox({ rtlEnabled: true }); + } + for (const placeholder of ['Placeholder', '']) { + await createTextBox({ + text: undefined, + placeholder, + stylingMode, + label: undefined, + }); + } + await createTextBox({ label: undefined, text: 'Text value' }); + await createTextBox({ label: undefined, rtlEnabled: true }); + } + + await insertStylesheetRulesToPage(page, `.${TEXTBOX_CLASS} { display: inline-block; vertical-align: middle; width: 60px; margin: 5px; }`); + + await testScreenshot(page, 'Textbox render with limited width.png', { element: '#container' }); + + await removeStylesheetRulesFromPage(page, ); + + await insertStylesheetRulesToPage(page, `.${TEXTBOX_CLASS} { display: inline-block; vertical-align: middle; width: 260px; margin: 5px; }`); + + await testScreenshot(page, 'Textbox render.png'); + + }); + + test('Textbox states', async ({ page }) => { + + const states = [ + HOVER_STATE_CLASS, + FOCUSED_STATE_CLASS, + READONLY_STATE_CLASS, + INVALID_STATE_CLASS, + `${INVALID_STATE_CLASS} ${FOCUSED_STATE_CLASS}`, + ]; + for (const state of states) { + for (const placeholder of ['Placeholder', '']) { + await createTextBox({ + text: undefined, + placeholder, + }, state); + } + + await createTextBox({ text: 'Text value' }, state); + await createTextBox({ rtlEnabled: true }, state); + } + + await insertStylesheetRulesToPage(page, `.${TEXTBOX_CLASS} { display: inline-block; vertical-align: middle; width: 260px; margin: 5px; }`); + + await testScreenshot(page, 'Textbox states.png', { element: '#container' }); + + }); + + test('Textbox with buttons container', async ({ page }) => { + + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '#container .dx-widget { font-family: sans-serif }'); + } + + for (const stylingMode of stylingModes) { + for (const buttons of buttonsList) { + await createTextBox({ stylingMode, buttons, showClearButton: true }); + await createTextBox({ + stylingMode, buttons, showClearButton: true, isValid: false, + }); + } + } + + await insertStylesheetRulesToPage(page, '#container { display: flex; flex-wrap: wrap; gap: 4px; }'); + + await testScreenshot(page, 'Textbox with buttons container.png'); + + }); + + stylingModes.forEach((stylingMode) => { + test(`TextBox should not be hovered after hover of outside label, stylingMode=${stylingMode}`, async ({ page }) => { + await createWidget(page, 'dxTextBox', { + value: 'text', + label: 'Label text', + labelMode: 'outside', + stylingMode, + width: 500, + }); + + const textBox = page.locator('#container'); + + await page.hover(textBox.getLabelSpan()) + .expect(textBox.isHovered) + .notOk(); + + }); + + test(`TextBox should be focused after click on outside label, stylingMode=${stylingMode}`, async ({ page }) => { + await createWidget(page, 'dxTextBox', { + value: 'text', + label: 'Label text', + labelMode: 'outside', + stylingMode, + width: 500, + }); + + const textBox = page.locator('#container'); + + await page.click(textBox.getLabelSpan()) + .expect(textBox.isFocused) + .ok(); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textBox/mask.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/mask.spec.ts new file mode 100644 index 000000000000..e5074ad7ed78 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/mask.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TextBox_mask', () => { + 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('"!" character should not be accepted if mask restricts it (T1156419)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'textBox', { }); + + await createWidget(page, 'dxTextBox', { + mask: '9', + }, '#textBox'); + + const textBox = page.locator('#textBox'); + const { input } = textBox; + + await input.fill('!') + .expect(input.value).eql('_'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/validationMessage.spec.ts new file mode 100644 index 000000000000..e2a6290617ad --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/validationMessage.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ValidationMessage', () => { + 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 TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; + + test('Validation Message position should be correct after change visibility of parent container (T1095900)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'textbox', {}); + + await createWidget(page, 'dxTextBox', { + value: 'a', + validationMessageMode: 'always', + }, '#textbox'); + + await createWidget(page, 'dxValidator', { + validationRules: [ + { + type: 'required', + }, + ], + }, '#textbox'); + + await addFocusableElementBefore('#container'); + + await page.locator(`.${TEXTEDITOR_INPUT_CLASS}`).click() + .pressKey('backspace') + .pressKey('enter') + .click(page.locator('#focusable-start')); + + await setAttribute(page, '#container', 'hidden', 'true'); + await removeAttribute('#container', 'hidden'); + + await testScreenshot(page, 'Textbox validation message.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/accordion/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/accordion/common.spec.ts new file mode 100644 index 000000000000..81f6f5ebfe99 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/accordion/common.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accordion_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('Accordion items render (T865742)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'accordion'); + await appendElementTo(page, '#container', 'div', 'accordion2'); + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 50px;'); + + const items: any[] = [ + { title: 'Some text 1', icon: 'coffee' }, + { title: 'Some text 2' }, + { title: 'Some text 3' }, + ]; + + await createWidget(page, 'dxAccordion', { items, width: 500 }, '#accordion'); + await createWidget(page, 'dxAccordion', { items, rtlEnabled: true, width: 500 }, '#accordion2'); + + const screenshotName = 'Accordion items render.png'; + + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); + + test('Icon-only button should be rendered correctly (T851081)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'accordion'); + + const itemTitleTemplate = () => ($('
') as any).dxButton({ icon: 'coffee' }); + + await createWidget(page, 'dxAccordion', { dataSource: [{}], itemTitleTemplate }, '#accordion'); + + const screenshotName = 'Accordion with icon-only button.png'; + + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/button/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/button/common.spec.ts new file mode 100644 index 000000000000..0958c340ad20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/button/common.spec.ts @@ -0,0 +1,134 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setStyleAttribute, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + ['text', 'outlined', 'contained'].forEach((stylingMode) => { + const testName = `Buttons, stylingMode=${stylingMode}`; + test(testName, async ({ page }) => { + + const typedButtons = ['danger', 'default', 'normal', 'success'].map((type: any) => ({ + type, + text: `${type[0].toUpperCase()}${type.slice(1)}`, + })); + const iconButtons = [ + { icon: 'find', text: 'Find' }, + { icon: 'find' }, + { + icon: ` + + `, + }, + ]; + const buttons = [ + ...typedButtons, + ...iconButtons, + ]; + + await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); + await setAttribute(page, '#container', 'style', 'width: fit-content; padding: 8px;'); + + const states = ['default', 'focused', 'hover', 'active', 'selected', 'disabled']; + + for (const state of states) { + await appendElementTo(page, '#container', 'div', `mode${state}`, {}); + await setAttribute(page, `#mode${state}`, 'style', 'display: flex; gap: 8px; margin-bottom: 16px;'); + await addCaptionTo(`#mode${state}`, state); + + await Promise.all(buttons.map( + (_, index) => appendElementTo(page, `#mode${state}`, 'div', `button-${state}-${index}`, {}), + )); + + await Promise.all(buttons.map( + (defaultConfig, index) => createWidget(page, 'dxButton', { + ...defaultConfig, + stylingMode, + disabled: state === 'disabled', + }, `#button-${state}-${index}`), + )); + + if (state !== 'default' && state !== 'disabled') { + await Promise.all( + buttons.map((_, index) => setClassAttribute(page, `#button-${state}-${index}`, `dx-state-${state}`)), + + } + } + + + await testScreenshot(page, `${testName}.png`, { + element: '#container', + }); + + }); + }); + + test('Button in rtl modes', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: fit-content; padding: 8px; display: grid; grid-template-columns: repeat(3, auto); grid-gap: 16px;'); + + const buttons = [ + { icon: 'find', text: 'Button text' }, + { icon: 'find', text: 'Long button text' }, + { icon: 'find', text: 'Long button text', width: 150 }, + { icon: 'find', text: 'Button text', rtlEnabled: true }, + { icon: 'find', text: 'Long button text', rtlEnabled: true }, + { + icon: 'find', text: 'Long button text', width: 150, rtlEnabled: true, + }, + ]; + + await Promise.all(buttons.map( + (_, index) => appendElementTo(page, '#container', 'div', `button-${index}`, {}), + )); + + await Promise.all(buttons.map( + (config, index) => createWidget(page, 'dxButton', { + ...config, + }, `#button-${index}`), + )); + + await testScreenshot(page, 'Button in rtl modes.png', { + element: '#container', + }); + + }); + + test('Button: svg icon as background should be fit within icon element (T1178813)', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-icon-custom { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB4klEQVR4nO3WTYiPURQG8N/M+ByEjQUxRQpJKSkhFigLo8jGQqGURDZY2CMpliI2PhZslLKxJBaUCCkUMxELja8pwoxuncW7mZn3fe9fb2meuqt7n+c8995z7zmMojVox1G81RDOYhBvmgi+LIKncakJA5cj+ABWNGHgQxg4pyG8xxmM/VcBVuMCXuIbenELe9GFiViH47iLd+jHq0jOBXUDT8a1QoINNX6OMP8Lx+KplkYnHoTARxyOnUzCHOzATXzHHzzHSWzATEzHRtwuGEnzpXExSA8xY5h1bZgwzHwHHodWStZSWBy76sMseTgQwb9gVVnSqSCle8vBuLi+pLWtCvFRkJZnGlgTOk+qEj8FcWqmgd2hc74q8UcQx2ca2BU66Q+phN4gzs40sDJ0XlT9A+4FcX2mgY4oz0nrSBXiiTofxxDYElVyIOpF+shGxNow0IMxLTCxJ37MpHm6DKENz4KwXWtwJ/T2lyXsLJzClMzg3YWaUlqrHfeDeCVOpQ464xUknX1VyYvwtXB3lZ5SrL8a/Kd1G5Zu/A6RVFqXxjFujl4w7e4zXkfvsBXTMA83gpeakyUysClEBmuM/uiWstEVPUJPXEvKj0NYGDVjPg5GtvdF+b2Oua0IPor/G38BnW+XcSzQwtUAAAAASUVORK5CYII="); }'); + + await setStyleAttribute(page, '#container', 'width: 300px; height: 200px;'); + await appendElementTo(page, '#container', 'div', 'button'); + await appendElementTo(page, '#container', 'div', 'fixedWidthButton'); + await appendElementTo(page, '#container', 'div', 'iconOnlyButton'); + + await createWidget(page, 'dxButton', { + text: 'svg icon', + icon: 'custom', + }, '#button'); + + await createWidget(page, 'dxButton', { + text: 'fixed width + svg icon', + icon: 'custom', + width: 200, + }, '#fixedWidthButton'); + + await createWidget(page, 'dxButton', { + icon: 'custom', + }, '#iconOnlyButton'); + + await testScreenshot(page, 'Button with svg icon as background.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingAction.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingAction.spec.ts new file mode 100644 index 000000000000..cdd377a86196 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingAction.spec.ts @@ -0,0 +1,126 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FloatingAction - default theme', () => { + 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 OVERLAY_CONTENT_CLASS = 'dx-overlay-content'; + const FA_MAIN_BUTTON_CLASS = 'dx-fa-button-main'; + + const setGlobalConfig = async () => page.evaluate(() => { + (window as any).DevExpress.config({ + floatingActionButtonConfig: { + icon: 'edit', + shading: false, + position: { + of: '#container', + my: 'right bottom', + at: 'right bottom', + offset: '-16 -16', + }, + }, + }); + }); + + for (const label of ['Add Row', '']) { + for (const icon of ['home', '']) { + test(`FAB with two speed dial action buttons after opening, label: ${label}, icon: ${icon}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 300px; height: 300px;'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action-trash'); + + await setGlobalConfig(); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-wrapper { font-family: sans-serif !important; }'); + } + + await createWidget(page, 'dxSpeedDialAction', { + label, + icon, + index: 1, + visible: true, + }, '#speed-dial-action'); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Remove Row', + icon: 'trash', + index: 2, + visible: true, + }, '#speed-dial-action-trash'); + + + await page.locator('body').click() + .click(page.locator(`.${FA_MAIN_BUTTON_CLASS} .${OVERLAY_CONTENT_CLASS}`)); + + await testScreenshot(page, `FAB is opened with two speed dial actions,label='${label}',icon='${icon}'.png`, { + element: '#container', + }); + + }); + + test(`FAB with one speed dial action button, label: ${label}, icon: ${icon}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 300px; height: 300px;'); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-wrapper { font-family: sans-serif !important; }'); + } + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + + await setGlobalConfig(); + + await createWidget(page, 'dxSpeedDialAction', { + label, + icon, + visible: true, + }, '#speed-dial-action'); + + + await testScreenshot(page, `FAB with one speed dial action button,label='${label}',icon='${icon}'.png`, { element: '#container' }); + + }); + } + } + + test('FAB with two speed dial action buttons', async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 300px; height: 300px;'); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-wrapper { font-family: sans-serif !important; }'); + } + + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action-trash'); + + await setGlobalConfig(); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Add row', + icon: 'plus', + index: 1, + visible: true, + }, '#speed-dial-action'); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Remove Row', + icon: 'trash', + index: 2, + visible: true, + }, '#speed-dial-action-trash'); + + await testScreenshot(page, 'FAB with two speed dial action buttons.png', { + element: '#container', + }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingActionInGrid.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingActionInGrid.spec.ts new file mode 100644 index 000000000000..e85e52e1972b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingActionInGrid.spec.ts @@ -0,0 +1,92 @@ +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.html')}`; + +test.describe('FloatingAction with Grid', () => { + 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 scrollWindowTo = async (position: object) => { + await ClientFunction( + () => { + (window as any).scroll(position); + }, + { + dependencies: { + position, + }, + }, + )(); + }; + + const generateData = (count) => { + const items: Record[] = []; + + for (let i = 0; i < count; i += 1) { + items.push({ + ID: i, + NAME: 'Name', + Full_Name: 'Full name', + }); + } + + return items; + }; + + [undefined, '#grid'].forEach((positionOf) => { + test(`FAB with grid, position.of is ${positionOf}`, async ({ page }) => { + + const dataGrid = page.locator('#grid'); + + await page.expect(dataGrid.isReady()) + .ok(); + + await page.evaluate(() => { + (window as any).DevExpress.ui.repaintFloatingActionButton(); + }); + + await testScreenshot(page, `FAB with grid, position.of is ${positionOf}, before scrolling.png`); + + await scrollWindowTo({ top: 10000000 }); + + await page.expect(dataGrid.isReady()) + .ok(); + + await testScreenshot(page, `FAB with grid, position.of is ${positionOf}, after scrolling.png`); + + });.before(async ({ page }) => { + await page.evaluate(() => { + $('#container').wrap('
'); + }); + + await resizeWindow(1000, 400); + + await appendElementTo(page, '#container', 'div', 'grid'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + + await createWidget(page, 'dxDataGrid', { + dataSource: generateData(20), + }, '#grid'); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Add row', + icon: 'plus', + position: { + of: positionOf, + }, + }, '#speed-dial-action'); + }).after(async () => { + await page.evaluate(() => { + $('#container').unwrap(); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/common.spec.ts new file mode 100644 index 000000000000..9abf0c9fe93d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/common.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const typedItems: any[] = ['danger', 'default', 'normal', 'success'].map((type: any) => ({ type, text: type })); + const iconItems: any[] = [ + { icon: 'find', text: 'find' }, + { icon: 'find' }, + ]; + const items: any[] = [ + ...typedItems, + ...iconItems, + ]; + + test('ButtonGroup styling', async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: fit-content; padding: 8px; display: flex; gap: 16px; flex-direction: column;'); + await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); + + const stylingModes = ['text', 'outlined', 'contained']; + + await Promise.all(stylingModes.map((mode) => appendElementTo(page, '#container', 'div', `buttongroup-${mode}`, {}))); + await Promise.all(stylingModes.map((stylingMode) => createWidget(page, 'dxButtonGroup', { + items, + stylingMode, + selectionMode: 'none', + }, `#buttongroup-${stylingMode}`))); + + await testScreenshot(page, 'ButtonGroup styling.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/selection.spec.ts new file mode 100644 index 000000000000..499dc491ba2e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/selection.spec.ts @@ -0,0 +1,80 @@ +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('ButtonGroup_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('selected class should not be added to the button after hovering (T1222079)', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { + items: [ + { text: 'Button_1' }, + { text: 'Button_2' }, + ], + selectedItemKeys: ['Button_1'], + disabled: true, + }); + + const buttonGroup = page.locator('#container'); + + await buttonGroup.option('disabled', false); + + await buttonGroup.getItem(1).element.click(); + + await page.expect(buttonGroup.getItem(1).isSelected) + .ok() + .expect(buttonGroup.isItemSelected(1)) + .ok(); + + await page.hover(buttonGroup.getItem(0).element); + + await page.expect(buttonGroup.getItem(0).isSelected) + .notOk() + .expect(buttonGroup.isItemSelected(0)) + .notOk(); + + }); + test('selected class should be set after reenabling (T1308601)', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { + items: [ + { text: 'Button_1' }, + { text: 'Button_2' }, + ], + selectedItemKeys: ['Button_1'], + }); + + const buttonGroup = page.locator('#container'); + + await buttonGroup.option('disabled', true); + await buttonGroup.option('disabled', false); + + await buttonGroup.getItem(1).element.click(); + + await buttonGroup.option('disabled', true); + await buttonGroup.option('disabled', false); + + await buttonGroup.getItem(0).element.click(); + + await page.expect(buttonGroup.getItem(0).isSelected) + .ok() + .expect(buttonGroup.isItemSelected(0)) + .ok(); + + await page.hover(buttonGroup.getItem(1).element); + + await page.expect(buttonGroup.getItem(0).isSelected) + .ok() + .expect(buttonGroup.isItemSelected(0)) + .ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/common.spec.ts new file mode 100644 index 000000000000..9eb08a6c0a99 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/common.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ContextMenu_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('ContextMenu items render', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'contextMenu'); + await setStyleAttribute(page, '#container', 'width: 300px; height: 200px;'); + + await insertStylesheetRulesToPage(page, '.custom-class { box-shadow: 0 0 0 2px green !important; }'); + + const menuItems: any[] = [ + { text: 'remove', icon: 'remove', items: [{ text: 'item_1' }, { text: 'item_2' }] }, + { text: 'user', icon: 'user' }, + { text: 'coffee', icon: 'coffee' }, + ]; + + await createWidget(page, 'dxContextMenu', { + cssClass: 'custom-class', + items: menuItems, + target: 'body', + position: { + offset: '10 10', + }, + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + + await contextMenu.show(); + await click(contextMenu.items.nth(0)); + + const screenshotName = 'ContextMenu items render.png'; + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); + + test('ContextMenu selected focused item', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'contextMenu'); + await setStyleAttribute(page, '#container', 'width: 150px; height: 200px;'); + + await insertStylesheetRulesToPage(page, '.custom-class { border: 2px solid green !important; }'); + + const menuItems: any[] = [ + { text: 'remove', icon: 'remove', selected: true }, + { text: 'user', icon: 'user' }, + { text: 'coffee', icon: 'coffee' }, + ]; + + await createWidget(page, 'dxContextMenu', { + cssClass: 'custom-class', + items: menuItems, + target: 'body', + position: { + offset: '10 10', + }, + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + + await contextMenu.show(); + await page.keyboard.press('ArrowDown'); + + const screenshotName = 'ContextMenu selected focused item.png'; + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/contextMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/contextMenu.spec.ts new file mode 100644 index 000000000000..b6abb53005ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/contextMenu.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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('Context menu should be shown in the same position when item was added in runtime (T755681)', async ({ page }) => { + + const menuTargetID = 'menuTarget'; + await appendElementTo(page, '#container', 'div', 'contextMenu'); + await appendElementTo(page, '#container', 'button', menuTargetID, { + width: '150px', height: '50px', backgroundColor: 'steelblue', + }); + + await createWidget(page, 'dxContextMenu', { + items: [{ text: 'item1' }], + showEvent: 'dxclick', + target: `#${menuTargetID}`, + onShowing: (e) => { + if (!(window as any).isItemAdded) { + setTimeout(() => { + (window as any).isItemAdded = true; + const items = e.component.option('items'); + items.push({ text: 'item 2' }); + e.component.option('items', items); + }, 1000); + } + }, + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + const target = page.locator('#menuTarget'); + + await page.click(target) + .expect(page.locator('.dx-context-menu').exists).ok('Context menu element should exist') + .expect(contextMenu.overlay.getContent().getStyleProperty('visibility')) + .eql('visible'); + + const initialOverlayOffset = await contextMenu.overlay.getOverlayOffset(); + + await page.expect(contextMenu.getItemCount()).eql(1); + + await page.expect(contextMenu.getItemCount()).eql(2) + .expect(contextMenu.overlay.getOverlayOffset()).eql(initialOverlayOffset); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/scrolling.spec.ts new file mode 100644 index 000000000000..37b297c164b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/scrolling.spec.ts @@ -0,0 +1,42 @@ +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.html')}`; + +test.describe('ContextMenu_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('ContextMenu items render', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'contextMenu'); + + const items: any[] = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + items[98].items = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + await createWidget(page, 'dxContextMenu', { + items, + target: 'body', + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + + await contextMenu.show(); + + await page.keyboard.press('ArrowDown') + .pressKey('up') + .pressKey('right') + .pressKey('up'); + + await testScreenshot(page, 'ContextMenu scrolling.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/drawer/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/drawer/common.spec.ts new file mode 100644 index 000000000000..4d27993c2bea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/drawer/common.spec.ts @@ -0,0 +1,118 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + ['overlap', 'shrink', 'push'].forEach((openedStateMode: OpenedStateMode) => { + const testName = `Drawer, openedStateMode=${openedStateMode}, shading=true`; + test(testName, async ({ page }) => { + + await createDrawer({ + options: { openedStateMode }, + }); + + + await testScreenshot(page, `${testName}.png`); + + }); + }); + + ['top', 'bottom', 'left', 'right'].forEach((position: Position) => { + const testName = `Drawer, position=${position}, shading=true`; + test(testName, async ({ page }) => { + + await createDrawer({ + options: { position }, + }); + + + await testScreenshot(page, `${testName}.png`); + + }); + }); + + test('Drawer hidden', async ({ page }) => { + + await createDrawer({ + createOuterContent: ($container) => { + ($('
').appendTo($container) as any).dxButton({ + text: 'Hide Drawer', + onClick: () => ($(`#${$container.attr('id')} #drawer`) as any).dxDrawer('instance').hide(), + }); + }, + }); + + await page.locator('#container #hideDrawerBtn').click(); + + await testScreenshot(page, 'Drawer hidden.png'); + + }); + + [{ + testCase: 'Menu inside drawer', + selector: '.dx-menu-item', + createDrawerContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxMenu({ + dataSource: [{ text: 'item1 very long text wider than panel', items: [{ text: 'item1/item1 very long text wider than panel' }, { text: 'item1/item2' }] }], + }); + }, + }, { + testCase: 'SelectBox inside drawer', + selector: '.dx-texteditor-container', + createDrawerContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxSelectBox({ + dataSource: ['item1 very long text wider than panel', 'item2'], + }); + }, + }, { + testCase: 'Menu outside drawer', + selector: '.dx-menu-item', + createOuterContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxMenu({ + dataSource: [{ text: 'item1 very long text wider than panel', items: [{ text: 'item1/item1 very long text wider than panel' }, { text: 'item1/item2' }] }], + }); + }, + }, { + testCase: 'SelectBox outside drawer', + selector: '.dx-texteditor-container', + createOuterContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxSelectBox({ + dataSource: ['item1 very long text wider than panel', 'item2'], + }); + }, + }].forEach(({ + testCase, createDrawerContent, createOuterContent, selector, + }) => { + const testName = `Drawer z-index, ${testCase}, shading=true`; + test(testName, async ({ page }) => { + + await createDrawer({ + createDrawerContent, + createOuterContent, + testInPopup: true, + }); + + + await page.locator(`#container #content ${selector}`).click(); + + await testScreenshot(page, `${testName}_container.png`); + + await page.locator('#showPopupBtn').click(); + await page.locator(`#popup1_template #content ${selector}`).click(); + + await testScreenshot(page, `${testName}_popup.png`); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/form/itemTypes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/form/itemTypes.spec.ts new file mode 100644 index 000000000000..461be49b64ee --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/form/itemTypes.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('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('GroupItem', async ({ page }) => { + await createWidget(page, 'dxForm', { + items: [ + { + itemType: 'group', + items: ['item1'], + captionTemplate: () => $('Custom caption template'), + }, + ], + }); + + await testScreenshot(page, 'Group caption template.png', { element: '#container' }); + + }); + + test('TabbedItem', async ({ page }) => { + await createWidget(page, 'dxForm', { + width: 500, + items: [ + { + itemType: 'tabbed', + tabPanelOptions: { deferRendering: false }, + tabs: [ + { + title: 'tab1', + items: ['item1'], + }, + ], + }, + ], + }); + + await testScreenshot(page, 'TabbedItem.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/form/labels.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/form/labels.spec.ts new file mode 100644 index 000000000000..48e4844d0936 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/form/labels.spec.ts @@ -0,0 +1,252 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage, removeStylesheetRulesFromPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('Item123somevalu*op ', 400)); + + [false, true].forEach((rtlEnabled) => { + ['left', 'right', 'top'].forEach((formLabelLocation) => { + ['outside', 'floating', 'hidden', 'static'].forEach((formLabelMode) => { + const testName = `Form,rtl=${rtlEnabled},lMode=${formLabelMode},lLoc=${formLabelLocation}`; + + test(testName, async ({ page }) => { + + await waitFont(); + + const getGroup = (visible: boolean, alignment: HorizontalAlignment) => ({ + itemType: 'group', + caption: `Label visible: ${visible}, label alignment: ${alignment}`, + colCount: 3, + items: [ + { + dataField: 'field1', + label: { visible, alignment }, + editorType: 'dxTextBox', + }, + { + dataField: 'field2', + label: { visible, alignment }, + editorType: 'dxTextBox', + editorOptions: { + value: 'dxTextBox', + }, + }, + { + dataField: 'field3', + label: { visible, alignment }, + editorType: 'dxCheckBox', + editorOptions: { + value: true, + text: 'dxCheckBox', + }, + }, + ], + }); + + const items = [true, false].flatMap( + (labelVisible) => { + const alignments: HorizontalAlignment[] = labelVisible && formLabelLocation === 'top' + ? ['left', 'center', 'right'] + : ['left']; + + return alignments.map((labelAlignment) => getGroup(labelVisible, labelAlignment)); + }, + + await createWidget(page, 'dxForm', { + rtlEnabled, + width: 1000, + labelMode: formLabelMode, + labelLocation: formLabelLocation, + items, + }); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + }); + + [true, false].forEach((alignItemLabelsInAllGroups) => { + [true, false].forEach((alignItemLabels) => { + const testName = `Align items,lblMode=outside,alignInAllGrp=${alignItemLabelsInAllGroups},alignInGrp=${alignItemLabels}`; + test(testName, async ({ page }) => { + + const options = { + labelMode: 'outside', + labelLocation: 'left', + alignItemLabelsInAllGroups, + colCount: 2, + width: 1000, + items: [ + { + itemType: 'group', + caption: 'Group1', + colSpan: 1, + alignItemLabels, + items: [ + { dataField: 'field1', label: { text: 'field1' }, editorType: 'dxTextBox' }, + { dataField: 'field2', label: { text: 'field2 long text' }, editorType: 'dxTextBox' }, + { dataField: 'field3', label: { text: 'CheckBox1' }, editorType: 'dxCheckBox' }, + { dataField: 'field4', label: { text: 'CheckBox2 long text' }, editorType: 'dxCheckBox' }, + ], + }, + { + itemType: 'group', + caption: 'Group2', + colSpan: 1, + alignItemLabels, + items: [ + { dataField: 'field5', label: { text: 'short text' }, editorType: 'dxTextBox' }, + { dataField: 'field6', label: { text: 'field2 very long text' }, editorType: 'dxTextBox' }, + { dataField: 'field7', label: { text: 'CheckBox1 text' }, editorType: 'dxCheckBox' }, + { dataField: 'field8', label: { text: 'CheckBox2 very long text' }, editorType: 'dxCheckBox' }, + ], + }, + { + itemType: 'group', + caption: 'Group3', + colSpan: 2, + alignItemLabels, + items: [ + { dataField: 'field9', label: { text: 'short text' }, editorType: 'dxTextBox' }, + { dataField: 'field10', label: { text: 'field2 very long text' }, editorType: 'dxTextBox' }, + { dataField: 'field11', label: { text: 'ChBx1 very very long text' }, editorType: 'dxCheckBox' }, + { dataField: 'field12', label: { text: 'ChBx2 very long text' }, editorType: 'dxCheckBox' }, + ], + }, + ], + }; + + await createWidget(page, 'dxForm', options); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + + test('Item label position properties, labelMode=outside', async ({ page }) => { + + const options = { + labelMode: 'outside', + width: 500, + items: [ + { dataField: 'Left', label: { location: 'left' }, editorType: 'dxTextBox' }, + { dataField: 'Top left', label: { location: 'top', alignment: 'left' }, editorType: 'dxTextBox' }, + { dataField: 'Top center', label: { location: 'top', alignment: 'center' }, editorType: 'dxTextBox' }, + { dataField: 'Top right', label: { location: 'top', alignment: 'right' }, editorType: 'dxTextBox' }, + { dataField: 'Right', label: { location: 'right' }, editorType: 'dxTextBox' }, + ], + }; + + await createWidget(page, 'dxForm', options); + + await testScreenshot(page, 'Item label position properties, labelMode=outside.png', { element: '#container' }); + + }); + + test('Color of the mark (T882067)', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 400, + width: 1000, + formData: { + firstName: 'John', + lastName: 'Heart', + position: 'CEO', + }, + items: [ + { dataField: 'firstName', isRequired: true }, + { dataField: 'lastName', isOptional: true }, + 'position', + ], + requiredMark: '!', + optionalMark: 'opt', + showOptionalMark: true, + }); + + const screenshotName = 'Form color of the mark.png'; + + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); + + test('Form labels should have correct width after render in invisible container', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'form'); + await insertStylesheetRulesToPage(page, '#container { display: none; }'); + + await createWidget(page, 'dxForm', { + width: 1000, + labelLocation: 'left', + formData: { + ID: 1, + FirstName: 'John', + LastName: 'Heart', + Position: 'CEO', + OfficeNo: '901', + BirthDate: new Date(1964, 2, 16), + HireDate: new Date(1995, 0, 15), + Address: '351 S Hill St.', + City: 'Los Angeles', + State: 'CA', + ZipCode: '90013', + Phone: '+1(213) 555-9392', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + }, + colCount: 2, + items: [{ + itemType: 'group', + caption: 'System Information', + items: ['ID', 'FirstName', 'LastName', 'HireDate', 'Position', 'OfficeNo'], + }, { + itemType: 'group', + caption: 'Personal Data', + items: ['BirthDate', { + itemType: 'group', + caption: 'Home Address', + items: ['Address', 'City', 'State', 'ZipCode'], + }], + }, { + itemType: 'group', + caption: 'Contact Information', + items: [{ + itemType: 'tabbed', + tabPanelOptions: { + deferRendering: true, + }, + tabs: [{ + title: 'Phone', + items: ['Phone'], + }, { + title: 'Skype', + items: ['Skype'], + }, { + title: 'Email', + items: ['Email'], + }], + }], + }], + }, '#form'); + + await removeStylesheetRulesFromPage(page, ); + + await testScreenshot(page, 'Form labels width after render in invisible container.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/form/layout.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/form/layout.spec.ts new file mode 100644 index 000000000000..f62ef04142cb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/form/layout.spec.ts @@ -0,0 +1,275 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + const waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('Item123somevalu*op ', 400)); + + test('SimpleItem: item1_cSpan_2', async ({ page }) => { + + await waitFont(); + await setAttribute(page, '#container', 'style', 'width: 500px;'); + + for (let colCount = 1; colCount <= 4; colCount += 1) { + const formId = `form${colCount}`; + + await appendElementTo(page, '#container', 'div', formId); + await page.evaluate(({ sel, caption }) => { document.querySelector(sel)?.insertAdjacentText('beforebegin', caption); }, { sel: `#${formId}`, caption: `colCount = ${colCount}` }); + + const formOptions = { + elementAttr: { style: 'margin-bottom: 20px' }, + labelMode: 'static', + colCount, + items: [{ dataField: 'item_1', colSpan: 2 }], + }; + + await createWidget(page, 'dxForm', formOptions, `#${formId}`); + } + + await testScreenshot(page, 'SimpleItem,item1_cSpan_2.png', { element: '#container' }); + + }); + + [[1, 2], [2, 1], [2, 2]].forEach(([colSpan1, colSpan2]) => { + const testName = `SimpleItem,item1_cSpan_${colSpan1},item2_cSpan_${colSpan2}`; + test(testName, async ({ page }) => { + + await waitFont(); + await setAttribute(page, '#container', 'style', 'width: 600px;'); + + for (let colCount = 1; colCount <= 4; colCount += 1) { + const formId = `form${colCount}`; + + await appendElementTo(page, '#container', 'div', formId); + await page.evaluate(({ sel, caption }) => { document.querySelector(sel)?.insertAdjacentText('beforebegin', caption); }, { sel: `#${formId}`, caption: `colCount = ${colCount}` }); + + const formOptions = { + elementAttr: { style: 'margin-bottom: 20px' }, + labelMode: 'static', + colCount, + items: [ + { dataField: `item_1_span_${colSpan1}`, colSpan: colSpan1 }, + { dataField: `item_2_span_${colSpan2}`, colSpan: colSpan2 }, + ], + }; + + await createWidget(page, 'dxForm', formOptions, `#${formId}`); + } + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + [false, true].forEach((rtlEnabled) => { + [1, 2, 3, 4, 5, 6].forEach((itemsCount) => { + const testName = `colCount,rtl_${rtlEnabled},itemsCount_${itemsCount}`; + test(testName, async ({ page }) => { + + await waitFont(); + const containerStyle = ` + display: grid; + grid-template-columns: repeat(3, 300px); + grid-template-rows: 0px auto; + grid-auto-flow: column; + grid-gap: 30px; + width: 960px;`; + await setAttribute(page, '#container', 'style', containerStyle); + + for (let colCount = 1; colCount <= 3; colCount += 1) { + const formId = `form${colCount + 1}`; + + await appendElementTo(page, '#container', 'div', formId); + await page.evaluate(({ sel, caption }) => { document.querySelector(sel)?.insertAdjacentText('beforebegin', caption); }, { sel: `#${formId}`, caption: `colCount = ${colCount}` }); + + const formOptions = { + colCount, + rtlEnabled, + labelMode: 'static', + items: Array(itemsCount).fill(null).map((_, i) => ({ dataField: `item_${i + 1}` })), + }; + + await createWidget(page, 'dxForm', formOptions, `#${formId}`); + } + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + + ['left', 'right', 'top'].forEach((labelLocation) => { + test('widget alignment (T1086611)', async ({ page }) => { + + await waitFont(); + + await createWidget(page, 'dxForm', { + labelLocation, + colCount: 2, + width: 1000, + formData: {}, + items: [{ + dataField: 'FirstName', + editorType: 'dxTextBox', + }, { + dataField: 'Position', + editorType: 'dxSelectBox', + }, { + dataField: 'BirthDate', + editorType: 'dxDateBox', + }, { + dataField: 'Notes', + editorType: 'dxTextArea', + }], + }); + + + await testScreenshot(page, `Form with labelLocation=${labelLocation}.png`, { element: '#container' }); + + }); + }); + + [() => 'xs', () => 'md', () => 'lg'].forEach((screenByWidth) => { + const testName = `Form item padding with screenByWidth=${screenByWidth()}`; + test(`${testName} (T1088451)`, async ({ page }) => { + await createWidget(page, 'dxForm', { + screenByWidth, + width: 1000, + formData: {}, + items: [ + 'Name1', 'Name2', + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + colCount: 2, + items: [ + { + dataField: 'Name3', + }, + { + dataField: 'Name4', + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + colCount: 2, + items: [ + { + itemType: 'group', + colCount: 2, + items: ['Name7', 'Name8'], + }, + { + itemType: 'group', + colCount: 2, + items: ['Name9', 'Name10'], + }, + ], + }, + ], + }, + ], + }, + ], + }, + 'Name11', 'Name12', + ], + }); + + await waitFont(); + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('Validation errors persist after resize', async ({ page }) => { + await createWidget(page, 'dxForm', { + colCountByScreen: { + xs: 1, + sm: 2, + md: 2, + lg: 2, + }, + items: [ + { + dataField: 'name', + editorType: 'dxTextBox', + validationRules: [{ type: 'required' }], + }, + { + dataField: 'birthDate', + editorType: 'dxDateBox', + validationRules: [{ type: 'required' }], + }, + { + dataField: 'role', + editorType: 'dxSelectBox', + editorOptions: { + dataSource: ['Dev', 'QA', 'PM'], + }, + validationRules: [{ type: 'required' }], + }, + { + dataField: 'agree', + editorType: 'dxCheckBox', + editorOptions: { + text: 'I agree', + }, + validationRules: [{ + type: 'custom', + validationCallback: () => false, + message: 'Required', + }], + }, + ], + }); + + const form = page.locator('#container'); + + await waitFont(); + await form.validate(); + + await resizeWindow(400, 800); + + await testScreenshot(page, 'form_validation_errors_after_resize.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/gallery/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/gallery/common.spec.ts new file mode 100644 index 000000000000..24ccdb222ec9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/gallery/common.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Click on indicator', () => { + 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 YELLOW_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXYzi8wA8AA9sBsq0bEHsAAAAASUVORK5CYII='; + const BLACK_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY1hSWg4AA1EBkakDs38AAAAASUVORK5CYII='; + const RED_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/i5aQsABQcCYPaWuk8AAAAASUVORK5CYII='; + + test('click on indicator item should change selected item', async ({ page }) => { + await createWidget(page, 'dxGallery', { + height: 300, + showIndicator: true, + items: [BLACK_PIXEL, RED_PIXEL, YELLOW_PIXEL], + }); + + const gallery = page.locator('#container'); + const secondIndicatorItem = gallery.getIndicatorItem(1); + + await secondIndicatorItem.click() + .expect(secondIndicatorItem.isSelected).ok(); + + }); + + [true, false].forEach((showIndicator) => { + test(`Gallery. Check normal and focus state. showIndicator=${showIndicator}`, async ({ page }) => { + + await createWidget(page, 'dxGallery', { + height: 110, + showIndicator, + items: [BLACK_PIXEL, RED_PIXEL, YELLOW_PIXEL], + itemTemplate(item: string) { + const result = $('
'); + + $('') + .attr({ src: item }) + .height(100) + .width(100) + .appendTo(result); + + return result; + }, + }); + + await setAttribute(page, '#container', 'style', 'width: 120px; height: 120px;'); + + + await testScreenshot(page, `Gallery. showIndicator=${showIndicator}.png`, { element: '#container' }); + + await page.locator('#container').click(); + + await testScreenshot(page, `Focused gallery. showIndicator=${showIndicator}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/common.spec.ts new file mode 100644 index 000000000000..b1350ba6dd63 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/common.spec.ts @@ -0,0 +1,376 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterialBased, isFluent } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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('Should focus first item after changing selection mode (T811770)', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + }); + + const list = page.locator('#container'); + const { selectAll } = list; + const firstItemRadioButton = list.getItem().radioButton; + + await list.focus(); + + expect(selectAll.checkBox.isFocused).toBeTruthy(); + + await list.option('selectionMode', 'single'); + + await list.focus(); + + expect(firstItemRadioButton.isFocused).toBeTruthy(); + + }); + + test('There is hover class in hovered list item (T1110076)', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + selectionMode: 'single', + }); + + const list = page.locator('#container'); + + const firstItem = list.getItem(0); + + await dispatchEvent(firstItem.element, 'mousedown'); + await list.repaint(); + await dispatchEvent(firstItem.element, 'mouseup'); + + const secondItem = list.getItem(1); + + expect(secondItem.isHovered).toBeFalsy() + .hover(secondItem.element) + .expect(secondItem.isHovered) + .ok(); + + }); + + test('List selection should work with keyboard arrows (T718398)', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + }); + + const list = page.locator('#container'); + const firstItemCheckBox = list.getItem().checkBox; + const secondItemCheckBox = list.getItem(1).checkBox; + const thirdItemCheckBox = list.getItem(2).checkBox; + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + + await list.focus(); + + expect(selectAllCheckBox.isFocused).toBeTruthy() + + .pressKey('down') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(firstItemCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(firstItemCheckBox.isFocused) + .notOk() + .expect(secondItemCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(secondItemCheckBox.isFocused) + .notOk() + .expect(thirdItemCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(thirdItemCheckBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(firstItemCheckBox.isFocused) + .ok() + + .pressKey('up') + .expect(firstItemCheckBox.isFocused) + .notOk() + .expect(selectAll.isFocused) + .ok() + + .pressKey('up') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(thirdItemCheckBox.isFocused) + .ok() + + .pressKey('up') + .expect(thirdItemCheckBox.isFocused) + .notOk() + .expect(secondItemCheckBox.isFocused) + .ok() + + .pressKey('tab') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(secondItemCheckBox.isFocused) + .notOk(); + + }); + + test('Should save focused checkbox', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + }); + + const list = page.locator('#container'); + const secondItemCheckBox = list.getItem(1).checkBox; + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + + await list.focus(); + + expect(selectAllCheckBox.isFocused).toBeTruthy() + + .pressKey('down down') + .expect(secondItemCheckBox.isFocused) + .ok() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('shift+tab') + .expect(secondItemCheckBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('tab') + .expect(secondItemCheckBox.isFocused) + .ok() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('up up') + .expect(selectAllCheckBox.isFocused) + .ok() + .expect(secondItemCheckBox.isFocused) + .notOk() + + .pressKey('shift+tab') + .expect(secondItemCheckBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('tab') + .expect(selectAllCheckBox.isFocused) + .ok() + .expect(secondItemCheckBox.isFocused) + .notOk(); + + }); + + test('Grouped list can not reorder items (T727360)', async ({ page }) => { + + const data = [ + { group: 'group1', value: '11' }, + { group: 'group1', value: '12' }, + { group: 'group1', value: '13' }, + { group: 'group2', value: '21' }, + { group: 'group2', value: '22' }, + { group: 'group2', value: '23' }, + { group: 'group2', value: '24' }, + { group: 'group2', value: '25' }, + { group: 'group2', value: '26' }, + { group: 'group2', value: '27' }, + { group: 'group2', value: '28' }, + { group: 'group2', value: '29' }, + { group: 'group2', value: '20' }, + { group: 'group3', value: '31' }, + { group: 'group3', value: '32' }, + { group: 'group3', value: '33' }, + { group: 'group3', value: '34' }, + { group: 'group3', value: '35' }, + { group: 'group3', value: '36' }, + { group: 'group3', value: '37' }, + { group: 'group3', value: '38' }, + { group: 'group3', value: '39' }, + { group: 'group3', value: '30' }, + ]; + + await createWidget(page, 'dxList', { + dataSource: { + store: data, + group: 'group', + }, + itemDragging: { + allowReordering: true, + }, + collapsibleGroups: true, + grouped: true, + itemTemplate: ({ value }, _, el) => el.append($('').text(value)), + }); + + const list = page.locator('#container'); + const firstGroup = list.getGroup(); + const secondGroup = list.getGroup(1); + const thirdGroup = list.getGroup(2); + + await page.click(secondGroup.header) + .click(thirdGroup.header) + + .dragToElement(firstGroup.getItem().reorderHandle, firstGroup.getItem(1).element) + .expect(firstGroup.getItem().text) + .eql(isFluent() ? '11' : '12') + .expect(firstGroup.getItem(1).text) + .eql(isFluent() ? '12' : '11') + + .click(firstGroup.header) + .click(secondGroup.header) + + .dragToElement(secondGroup.getItem().reorderHandle, secondGroup.getItem(1).element) + .expect(secondGroup.getItem().text) + .eql(isFluent() ? '21' : '22') + .expect(secondGroup.getItem(1).text) + .eql(isFluent() ? '22' : '21') + + .click(secondGroup.header) + .click(thirdGroup.header) + + .dragToElement(thirdGroup.getItem().reorderHandle, thirdGroup.getItem(1).element) + .expect(thirdGroup.getItem().text) + .eql(isMaterialBased() ? '31' : '32') + .expect(thirdGroup.getItem(1).text) + .eql(isMaterialBased() ? '32' : '31'); + + }); + + test('Grouped List with nested List should able to reorder items (T845082)', async ({ page }) => { + + const data = [ + { group: 'group1', text: 'value11' }, + { + group: 'group1', + text: 'value12', + template: ClientFunction((_data, _index, element) => ($('
').appendTo(element) as any).dxList({ + items: ['value121', 'value122', 'value123'], + itemTemplate: (data, _index, element) => { + $(element) + .text(data) + .parent() + .addClass('nested-item'); + }, + })), + }, + { group: 'group1', text: 'value13' }, + ]; + + await createWidget(page, 'dxList', { + dataSource: { + store: data, + group: 'group', + }, + itemDragging: { + allowReordering: true, + }, + collapsibleGroups: true, + grouped: true, + }); + + const list = page.locator('#container'); + const group = list.getGroup(); + + await page.expect(group.getItem(0).text).eql('value11') + .drag(group.getItem().reorderHandle, 0, await group.getItem(1).element.clientHeight) + .expect(group.getItem(1).text) + .eql('value11'); + + }); + + test('Disabled item should be focused on tab press to match accessibility criteria', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: [{ text: 'item1' }, { text: 'item2' }], + searchEnabled: true, + }); + + const list = page.locator('#container'); + const { searchInput } = list; + const firstItem = list.getItem(); + const secondItem = list.getItem(1); + + await page.click(searchInput) + .pressKey('tab') + .expect(firstItem.isFocused).ok() + .expect(secondItem.isFocused) + .notOk(); + + await list.option('items[0].disabled', true); + + expect(firstItem.isDisabled).toBeTruthy() + + .click(searchInput) + .pressKey('tab') + .expect(firstItem.isFocused) + .ok() + .expect(secondItem.isFocused) + .notOk(); + + }); + + test('The delete button should be displayed correctly after the list item focused (T1216108)', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: [{ + text: 'item 1', + icon: 'user', + }], + allowItemDeleting: true, + itemDeleteMode: 'static', + }); + + const list = page.locator('#container'); + + await list.focus(); + + await testScreenshot(page, 'List delete button when item is focused.png'); + + }); + + test('The button icon in custom template should be displayed correctly after the list item focused (T1216108)', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: [{ text: 'item 1' }], + itemTemplate: (_, __, element) => { + const button = ($('
') as any).dxButton({ + text: 'custom', + icon: 'home', + }); + + element.append(button); + }, + }); + + const list = page.locator('#container'); + + await list.focus(); + + await testScreenshot(page, 'List icon in button when item is focused.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/focus.spec.ts new file mode 100644 index 000000000000..8314b267b51d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/focus.spec.ts @@ -0,0 +1,154 @@ +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('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'); + }); + + const LIST_ITEM_DELETE_BUTTON = 'dx-list-static-delete-button'; + + const createList = (selectionMode, allowItemDeleting = false) => createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode, + allowItemDeleting, + }); + + [true, false].forEach((focusStateEnabled) => { + test(`Should${focusStateEnabled ? '' : ' not'} focus item when deleting when focusStateEnabled=${focusStateEnabled} (T1226030)`, async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + selectionMode: 'none', + allowItemDeleting: true, + itemDeleteMode: 'static', + focusStateEnabled, + }); + + const list = page.locator('#container'); + const firstItem = list.getItem(0); + const $firstDeleteBtn = firstItem.element.find(`.${LIST_ITEM_DELETE_BUTTON}`); + + await page.click($firstDeleteBtn) + .expect(firstItem.isFocused) + .eql(focusStateEnabled); + + }); + }); + + test('Should apply styles on selectAll checkbox after tab button press', async ({ page }) => { + await createList('all'); + + const list = page.locator('#container'); + + await page.keyboard.press('Tab') + .expect(list.selectAll.checkBox.isFocused) + .ok(); + + }); + + test('Should apply styles on selectAll checkbox after enter button press on it', async ({ page }) => { + await createList('all'); + + const list = page.locator('#container'); + + await page.keyboard.press('Tab') + .pressKey('enter') + .expect(list.selectAll.checkBox.isChecked) + .ok(); + + }); + + ['single', 'multiple'].forEach((selectionMode) => { + test(`Should apply styles on list item after tab button press, ${selectionMode} mode`, async ({ page }) => { + await createList(selectionMode); + + const list = page.locator('#container'); + + await page.keyboard.press('Tab') + .expect(list.getItem(0).isFocused) + .ok(); + + }); + + test(`Should apply styles on list item after enter button press on it, ${selectionMode} mode`, async ({ page }) => { + await createList(selectionMode); + + const list = page.locator('#container'); + + const firstItem = list.getItem(0); + const firstItemType = selectionMode === 'single' ? firstItem.radioButton : firstItem.checkBox; + + await page.keyboard.press('Tab') + .pressKey('enter') + .expect(firstItemType.isChecked) + .ok(); + + }); + }); + + test('Should select next item after delete by keyboard', async ({ page }) => { + await createList('none', true); + + const list = page.locator('#container'); + const firstItem = list.getItem(0); + + await page.expect(list.getVisibleItems().count).eql(3) + .click(firstItem.element) + .pressKey('delete'); + + const item = list.getItem(0); + + expect(item.isFocused).toBeTruthy(); + expect(item.text).toBe('item2'); + await expect(list.getItems().count).eql(2); + + }); + + test('Should select previous item after delete last item', async ({ page }) => { + await createList('none', true); + + const list = page.locator('#container'); + const lastItem = list.getItem(2); + + await page.expect(list.getVisibleItems().count).eql(3) + .click(lastItem.element) + .pressKey('delete'); + + const item = list.getItem(1); + + expect(item.isFocused).toBeTruthy(); + expect(item.text).toBe('item2'); + await expect(list.getItems().count).eql(2); + + }); + + [[2, 0], [1, 2]].forEach(([selectItemIdx, deleteItemIdx]) => { + test(`Should not change selection after delete another (not selected) item (${selectItemIdx}, ${deleteItemIdx})`, async ({ page }) => { + await createList('none', true); + + const list = page.locator('#container'); + const itemToSelect = list.getItem(selectItemIdx); + const itemToDelete = list.getItem(deleteItemIdx); + + await page.expect(list.getVisibleItems().count).eql(3) + .click(itemToSelect.element) + .click(itemToDelete.element.find('.dx-button')); + + const item = list.getItem(deleteItemIdx > selectItemIdx ? selectItemIdx : selectItemIdx - 1); + + expect(item.isFocused).toBeTruthy(); + expect(item.text).toBe(`item${selectItemIdx + 1}`); + await expect(list.getItems().count).eql(2); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/grouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/grouping.spec.ts new file mode 100644 index 000000000000..6b001c8bfa74 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/grouping.spec.ts @@ -0,0 +1,110 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Grouping', () => { + 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('Grouped list appearance', async ({ page }) => { + await createWidget(page, 'dxList', { + width: 300, + dataSource: [ + { + key: 'group_1', + items: ['item_1_1', 'item_1_2', 'item_1_3'], + expanded: false, + }, + { + key: 'group_2', + items: [ + { text: 'item_2_1', disabled: true }, + { text: 'item_2_2', icon: 'home' }, + { text: 'item_2_3', showChevron: true, badge: 'item_2_3' }, + { text: 'item_2_4', badge: 'item_2_4' }, + 'item_2_5', + ], + }, + { + key: 'group_3', + items: ['item_3_1', 'item_3_2', 'item_3_3'], + expanded: false, + }, + ], + collapsibleGroups: true, + grouped: true, + allowItemDeleting: true, + itemDeleteMode: 'static', + itemDragging: { + allowReordering: true, + }, + }); + + const list = page.locator('#container'); + + await list.getItem(2).element.click() + .pressKey('down'); + + await testScreenshot(page, 'Grouped list appearance, header focused.png', { element: '#container' }); + + await page.click(list.getGroup(0).header) + .click(list.getGroup(2).header) + .click(list.getItem(4).element) + .hover(list.getGroup(1).header); + + await testScreenshot(page, 'Grouped list appearance, item focused, header hovered.png', { element: '#container' }); + + await list.option('collapsibleGroups', false); + + await testScreenshot(page, 'Grouped list appearance,collapsibleGroups=false.png', { element: '#container' }); + + }); + + test('Grouped list appearance with template', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; padding: 8px; width: fit-content;'); + + const dataSource = [ + { key: 'One', items: ['1_1', '1_2', '1_3'] }, + { key: 'Two', items: ['2_1', '2_2', '2_3'] }, + { key: 'Three', items: ['3_1', '3_2', '3_3'] }, + ]; + + await Promise.all([false, true].map((rtlEnabled) => appendElementTo(page, '#container', 'div', `list-rtl-${rtlEnabled}`))); + await Promise.all([false, true].map((rtlEnabled) => createWidget(page, 'dxList', { + dataSource, + width: 300, + groupTemplate(data) { + const wrapper = $('
'); + + $(`${data.key}`).appendTo(wrapper); + $('
second row
').appendTo(wrapper); + + return wrapper; + }, + collapsibleGroups: true, + grouped: true, + rtlEnabled, + }, `#list-rtl-${rtlEnabled}`))); + + const list = page.locator('#list-rtl-false'); + const list2 = page.locator('#list-rtl-true'); + + await page.click(list.getGroup(0).header) + .click(list.getGroup(2).header) + .click(list2.getGroup(0).header) + .click(list2.getGroup(2).header) + .click('#container'); + + await testScreenshot(page, 'Grouped list appearance with template.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/paging.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/paging.spec.ts new file mode 100644 index 000000000000..3b935cac0312 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/paging.spec.ts @@ -0,0 +1,222 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + function generateData(count) { + const items: { id: number }[] = []; + + for (let i = 0; i < count; i += 1) { + items.push({ id: i + 1 }); + } + return items; + } + + test('Should initiate load next pages if items on the first pages are invisible', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id > 8, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 10 : 12) + .expect(list.getVisibleItems().count) + .eql(isMaterial() ? 2 : 4); + + await testScreenshot(page, 'List loading with first items invisible.png', { element: '#container' }); + + }); + + test('Should initiate load next page if all items in the current load are invisible, pageLoadMode: scrollBottom (T1092746)', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id <= 4 || data.id > 8, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(100); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 4 : 10) + .expect(list.getVisibleItems().count) + .eql(isMaterial() ? 4 : 6); + + await testScreenshot(page, 'List loading with middle items invisible.png', { element: '#container' }); + + }); + + test('Should initiate load next page if some items in the current load are invisible, pageLoadMode: scrollBottom', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id <= 4 || data.id === 8 || data.id === 11, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(100); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 4 : 12) + .expect(list.getVisibleItems().count) + .eql(isMaterial() ? 4 : 6); + + await testScreenshot(page, 'List loading with part items invisible on loaded page.png', { element: '#container' }); + + }); + + test('Should initiate load next page if all items on next pages are invisible', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id <= 4, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(100); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 4 : 12) + .expect(list.getVisibleItems().count) + .eql(4); + + await testScreenshot(page, 'List loading with last items invisible.png', { element: '#container' }); + + }); + + test('Should not initiate load next page if not reach the bottom when pullRefreshEnabled is true', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + pullRefreshEnabled: true, + height: 130, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(1); + + await page.expect(list.getItems().count) + .eql(4); + + }); + + test('Should initiate load next page on select last item by keyboard', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 3, + }, + pullRefreshEnabled: true, + height: 160, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.focus(); + + await page.expect(list.getItems().count) + .eql(6); + + await page.keyboard.press('ArrowDown') + .pressKey('down') + .pressKey('down') + .pressKey('down') + .pressKey('down'); + + await page.expect(list.getItems().count) + .eql(9); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/search.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/search.spec.ts new file mode 100644 index 000000000000..058d858a85b3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/search.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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('List with search bar appearance', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; padding: 8px; width: fit-content;'); + + const dataSource = Array.from({ length: 8 }, (_, i) => `Item_${i + 1}`); + const selectionModes = ['none', 'single', 'multiple', 'all']; + + await Promise.all(selectionModes.map((mode) => appendElementTo(page, '#container', 'div', `list-${mode}`))); + await Promise.all(selectionModes.map((mode) => createWidget(page, 'dxList', { + dataSource, + height: 400, + width: 200, + searchEnabled: true, + showSelectionControls: true, + selectionMode: mode, + }, `#list-${mode}`))); + + await testScreenshot(page, 'List with search.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/menu/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/common.spec.ts new file mode 100644 index 000000000000..92a0502af221 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/common.spec.ts @@ -0,0 +1,171 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Menu_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('Menu items render', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'menu'); + await setAttribute(page, '#container', 'style', 'box-sizing: border-box; width: 400px; height: 400px; padding: 8px;'); + await insertStylesheetRulesToPage(page, '.custom-class { border: 2px solid green !important }'); + + const menuItems: any[] = [ + { + 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', + items: [ + { + text: 'user', + icon: 'user', + selected: true, + }, + { + text: 'save', + icon: 'save', + }, + ], + }, + { + text: 'coffee', + icon: 'coffee', + disabled: true, + }, + ]; + + await createWidget(page, 'dxMenu', { items: menuItems, cssClass: 'custom-class' }, '#menu'); + + const menu = new Menu(); + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('down') + .pressKey('right'); + + await testScreenshot(page, 'Menu render items.png', { element: '#container' }); + + await page.click(menu.getItem(1)) + .pressKey('down'); + + await testScreenshot(page, 'Menu selected focused item.png', { + element: '#container', + }); + + }); + + [true, false].forEach((adaptivityEnabled) => { + test(`Menu item with link, adaptivityEnabled=${adaptivityEnabled}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'menu'); + await setAttribute(page, '#container', 'style', 'width: 200px; height: 400px;'); + + const items: any[] = [{ + text: 'Items 1', + items: [{ + text: 'Item 1', + }, { + text: 'Item 2', + icon: 'bookmark', + url: 'https://js.devexpress.com/', + }, { + icon: 'more', + url: 'https://js.devexpress.com/', + }, { + text: 'Item 4', + url: 'https://js.devexpress.com/', + }], + }]; + + if (adaptivityEnabled) { + items.push( + { text: 'Items 2' }, + { text: 'Items 3' }, + { text: 'Items 4' }, + + } + + await createWidget(page, 'dxMenu', { + adaptivityEnabled, + items, + }, '#menu'); + + + const menu = new Menu(adaptivityEnabled); + + if (adaptivityEnabled) { + await click(menu.getHamburgerButton()); + } + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('down'); + + await testScreenshot(page, `Menu item with link and icon focused, adaptivityEnabled=${adaptivityEnabled}.png`); + + await page.keyboard.press('ArrowDown') + .pressKey('down'); + + await testScreenshot(page, `Menu item with link focused, adaptivityEnabled=${adaptivityEnabled}.png`); + + }); + }); + + test('Menu scrolling', async ({ page }) => { + + const items: any[] = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + items[98].items = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + await createWidget(page, 'dxMenu', { + items: [ + { + text: 'root', + items, + }, + ], + showFirstSubmenuMode: 'onClick', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('up') + .pressKey('right') + .pressKey('up'); + + await testScreenshot(page, 'Menu scrolling.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/menu/delimiter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/delimiter.spec.ts new file mode 100644 index 000000000000..b1dd709262b4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/delimiter.spec.ts @@ -0,0 +1,150 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Menu_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 items: any[] = [ + { text: 'Category 1' }, + { + text: 'Category 2', + items: [ + { text: 'Item long name 2-1' }, + { text: 'Item long name 2-2' }, + ], + }, + { + text: 'Category 3', + items: [ + { text: 'Item 1' }, + { text: 'Item 2' }, + ], + }, + { + text: 'Category 4', + items: [ + { text: 'Item long name 4-1' }, + { text: 'Item long name 4-2' }, + ], + }, + ]; + + ['horizontal', 'vertical'].forEach((orientation) => { + const testName = `Menu delimiter, orientation=${orientation}`; + test(testName, async ({ page }) => { + await createWidget(page, + 'dxMenu', + { + items, + orientation, + }, + '#container', + + const menu = new Menu(); + + await page.click(menu.getItem(2)) + .pressKey('down'); + + await testScreenshot(page, `${testName}.png`); + + if (orientation === 'horizontal') { + await page.click(menu.getItem(1)) + .pressKey('down'); + + await testScreenshot(page, `${testName}, wide submenu.png`); + } + + }); + }); + + ['horizontal', 'vertical'].forEach((orientation) => { + ['bottom', 'right', 'bottom right'].forEach((collision) => { + const testName = `Menu delimiter ${collision} collision, orientation=${orientation}`; + test(testName, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'menu'); + const additionalStyles = { + bottom: 'justify-content: start;', + right: 'align-content: start;', + }; + await setAttribute(page, '#container', 'style', `width: 500px; height: 500px; display: grid; ${additionalStyles[collision] ?? ''}`); + + await createWidget(page, + 'dxMenu', + { + elementAttr: { + style: 'align-self: end; justify-self: end;', + }, + items, + orientation, + }, + '#menu', + + const menu = new Menu(); + + await click(menu.getItem(3)); + + await testScreenshot(page, `${testName}.png`); + + }); + }); + }); + + test('Menu delimiter appearance when the Menu is used as a toolbar item', async ({ page }) => { + + const toolbarItems = [ + { + location: 'before', + widget: 'dxMenu', + options: { + items: [{ + text: 'Video Players', + }, { + text: 'Televisions', + items: [{ + id: '2_1', + text: 'SuperLCD 42', + }, { + id: '2_2', + text: 'SuperLED 42', + }], + }], + }, + }, { + location: 'before', + widget: 'dxButton', + options: { + icon: 'undo', + }, + }, { + location: 'before', + widget: 'dxButton', + options: { + icon: 'redo', + }, + }, + ]; + + await createWidget(page, 'dxToolbar', { + items: toolbarItems, + width: '100%', + }, '#container'); + + const menu = new Menu(); + + await click(menu.getItem(1)); + + await testScreenshot(page, 'Menu delimiter, menu as toolbar item.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/menu/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/keyboard.spec.ts new file mode 100644 index 000000000000..34c03d55e373 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/keyboard.spec.ts @@ -0,0 +1,137 @@ +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('Menu_keyboard', () => { + 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('keyboard navigation should work after click on a root item if showFirstSubmenuMode is "onClick"', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onClick', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('right') + .pressKey('down'); + + const focusedElement = menu.getItem(2); + + expect(focusedElement.innerText).toBe('item_1_1_1') + .expect(menu.isElementFocused(focusedElement)) + .eql(true); + + }); + + test('keyboard navigation should work after hover a root item if showFirstSubmenuMode is "onHover"', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onHover', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.locator('body').click() + .hover(menu.getItem(0)) + .pressKey('down') + .pressKey('right') + .pressKey('down'); + + const focusedElement = menu.getItem(2); + + expect(focusedElement.innerText).toBe('item_1_1_1') + .expect(menu.isElementFocused(focusedElement)) + .eql(true); + + }); + + test('menu should be closed after press on "escape" key when submenu was shown by click, showFirstSubmenuMode="onClick" (T1115916)', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onClick', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.locator('body').click() + .click(menu.getItem(0)); + + const submenu = menu.getSubMenuInstance(menu.getItem(0)); + + await page.expect(submenu.option('visible')) + .eql(true) + .pressKey('esc') + .expect(submenu.option('visible')) + .eql(false); + + }); + + test('menu should be closed after press on "escape" key when submenu was shown by hover, showFirstSubmenuMode="onHover" (T1115916)', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onHover', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.locator('body').click() + .hover(menu.getItem(0)); + + const submenu = menu.getSubMenuInstance(menu.getItem(0)); + + await page.expect(submenu.option('visible')) + .eql(true) + .pressKey('esc') + .expect(submenu.option('visible')) + .eql(false); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollView/scrollView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollView/scrollView.spec.ts new file mode 100644 index 000000000000..38cb8960eb62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollView/scrollView.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ScrollView', () => { + 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'); + }); + + type ScrollableDirection = 'both' | 'horizontal' | 'vertical'; + + [150, 300].forEach((scrollableContentSize) => { + (['vertical', 'horizontal'] as ScrollableDirection[]).forEach((direction) => { + ['onHover', 'always', 'onScroll', 'never'].forEach((showScrollbar) => { + const scrollableContainerSize = 200; + const scrollBarVisibleAfterMouseEnter = (showScrollbar === 'always' || showScrollbar === 'onHover') && scrollableContentSize > scrollableContainerSize; + const scrollBarVisibleAfterMouseLeave = showScrollbar === 'always' && scrollableContentSize > scrollableContainerSize; + + test(`Scroll visibility on mouseEnter/mouseLeave, showScrollbar: '${showScrollbar}', direction: '${direction}', content ${scrollableContentSize < scrollableContainerSize ? 'less' : 'more'} than container (T817096)`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollView'); + await appendElementTo(page, '#scrollView', 'div', 'innerScrollViewContent', { + width: `${scrollableContentSize}px`, height: `${scrollableContentSize}px`, backgroundColor: 'steelblue', + }); + + await createWidget(page, 'dxScrollView', { + width: scrollableContainerSize, + height: scrollableContainerSize, + useNative: false, + direction, + showScrollbar, + }, '#scrollView'); + + + const scrollView = new ScrollView('#scrollView', { direction }); + + await expect(scrollView.scrollbar.isScrollVisible()).eql(scrollBarVisibleAfterMouseLeave); + await hover(scrollView.getContainer()); + await expect(scrollView.scrollbar.isScrollVisible()).eql(scrollBarVisibleAfterMouseEnter); + await page.locator('body').click(); + await expect(scrollView.scrollbar.isScrollVisible()).eql(scrollBarVisibleAfterMouseLeave); + + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/integration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/integration.spec.ts new file mode 100644 index 000000000000..f0a9a732e754 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/integration.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Integration_DataGrid', () => { + 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((useNative) => { + test(`The rows in the fixed column are not aligned when the grid is encapsulated inside a element, useNative: ${useNative} (T1071725)`, async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 200px;'); + + await appendElementTo(page, '#container', 'table', 'outerTable', {}); + await appendElementTo(page, '#outerTable', 'tr', 'outerTableTR', {}); + await appendElementTo(page, '#outerTableTR', 'td', 'outerTableTD', {}); + await appendElementTo(page, '#outerTableTR', 'div', 'grid', {}); + + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + field1: 'test1', field2: 'test2', + }, + ], + scrolling: { + useNative, + }, + width: 300, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { dataField: 'field1', fixed: true }, + { dataField: 'field2' }, + ], + hoverStateEnabled: true, + }, '#grid'); + + + await testScreenshot(page, `Grid with scrollable wrapped in td,useNative=${useNative}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/scrollable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/scrollable.spec.ts new file mode 100644 index 000000000000..8099e8747848 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/scrollable.spec.ts @@ -0,0 +1,478 @@ +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.html')}`; + +test.describe('Scrollable_ScrollToElement', () => { + 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'); + }); + + type ScrollableDirection = 'both' | 'horizontal' | 'vertical'; + + (['both'] as ScrollableDirection[]).forEach((direction) => { + test(`ScrollToElement, element less container,direction=${direction}`, async ({ page }) => { + + const positions = [ + { initialScrollOffset: { top: 80, left: 80 }, position: 'elementInsideContainer' }, + { initialScrollOffset: { top: 0, left: 0 }, position: 'fromTopLCorner' }, + { initialScrollOffset: { top: 0, left: 80 }, position: 'fromTop' }, + { initialScrollOffset: { top: 0, left: 160 }, position: 'fromTopRCorner' }, + { initialScrollOffset: { top: 80, left: 160 }, position: 'fromR' }, + { initialScrollOffset: { top: 160, left: 160 }, position: 'fromBRCorner' }, + { initialScrollOffset: { top: 160, left: 80 }, position: 'fromB' }, + { initialScrollOffset: { top: 160, left: 0 }, position: 'fromBLCorner' }, + { initialScrollOffset: { top: 80, left: 0 }, position: 'fromL' }, + // part + { initialScrollOffset: { top: 125, left: 125 }, position: 'part-fromTopLCorner' }, + { initialScrollOffset: { top: 125, left: 80 }, position: 'part-fromTop' }, + { initialScrollOffset: { top: 125, left: 45 }, position: 'part-fromTopRCorner' }, + { initialScrollOffset: { top: 80, left: 45 }, position: 'part-fromR' }, + { initialScrollOffset: { top: 45, left: 45 }, position: 'part-fromBRCorner' }, + { initialScrollOffset: { top: 45, left: 80 }, position: 'part-fromB' }, + { initialScrollOffset: { top: 45, left: 125 }, position: 'part-fromBLCorner' }, + { initialScrollOffset: { top: 80, left: 125 }, position: 'part-fromL' }, + ]; + + for (const useNative of [true, false]) { + for (const rtlEnabled of [true, false]) { + for (const { initialScrollOffset } of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { + border: '1px solid black', + display: 'inline-block', + }); + + await appendElementTo(page, `#${id}`, 'div', `${id}scrollableContent`, { + width: '250px', + height: '250px', + border: '1px solid #0b837a', + backgroundColor: 'lightskyblue', + }); + + await appendElementTo(page, `#${id}scrollableContent`, 'div', `${id}element`, { + position: 'absolute', + boxSizing: 'border-box', + left: '100px', + top: '100px', + height: '50px', + width: '50px', + backgroundColor: '#2bb97f', + border: '5px solid red', + margin: '5px', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + direction, + rtlEnabled, + showScrollbar: 'always', + }, `#${id}`); + + const scrollable = new Scrollable(`#${id}`, { useNative, direction }); + + await scrollable.scrollTo(initialScrollOffset); + await scrollable.scrollToElement(`#${id}element`); + } + } + } + + + await testScreenshot(page, `ScrollToElement, element less container direction=${direction}.png`); + + }); + + test(`ScrollToElement, element more container,direction=${direction}`, async ({ page }) => { + + const positions = [ + { initialScrollOffset: { top: 0, left: 0 }, position: 'fromTLCorner' }, + { initialScrollOffset: { top: 0, left: 40 }, position: 'fromTLPart' }, + { initialScrollOffset: { top: 0, left: 120 }, position: 'fromTRPart' }, + { initialScrollOffset: { top: 0, left: 160 }, position: 'fromTRCorner' }, + + { initialScrollOffset: { top: 40, left: 160 }, position: 'fromRTPart' }, + { initialScrollOffset: { top: 120, left: 160 }, position: 'fromRBPart' }, + + { initialScrollOffset: { top: 160, left: 160 }, position: 'fromBRCorner' }, + { initialScrollOffset: { top: 160, left: 120 }, position: 'fromBRPart' }, + { initialScrollOffset: { top: 160, left: 40 }, position: 'fromBLPart' }, + { initialScrollOffset: { top: 160, left: 0 }, position: 'fromBLCorner' }, + + { initialScrollOffset: { top: 120, left: 0 }, position: 'fromLBPart' }, + { initialScrollOffset: { top: 40, left: 0 }, position: 'fromLTPart' }, + + // from inside + + { initialScrollOffset: { top: 40, left: 60 }, position: 'fromInsideTL' }, + { initialScrollOffset: { top: 40, left: 100 }, position: 'fromInsideTR' }, + { initialScrollOffset: { top: 60, left: 120 }, position: 'fromInsideRT' }, + { initialScrollOffset: { top: 100, left: 120 }, position: 'fromInsideRB' }, + { initialScrollOffset: { top: 120, left: 100 }, position: 'fromInsideBR' }, + { initialScrollOffset: { top: 120, left: 60 }, position: 'fromInsideBL' }, + { initialScrollOffset: { top: 100, left: 40 }, position: 'fromInsideLB' }, + { initialScrollOffset: { top: 60, left: 40 }, position: 'fromInsideLT' }, + ]; + + for (const useNative of [true, false]) { + for (const rtlEnabled of [true, false]) { + for (const { initialScrollOffset } of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { + border: '1px solid black', + display: 'inline-block', + }); + + await appendElementTo(page, `#${id}`, 'div', `${id}scrollableContent`, { + width: '250px', + height: '250px', + border: '1px solid #0b837a', + backgroundColor: 'lightskyblue', + }); + + await appendElementTo(page, `#${id}scrollableContent`, 'div', `${id}element`, { + position: 'absolute', + boxSizing: 'border-box', + left: '20px', + top: '20px', + height: '200px', + width: '200px', + backgroundColor: '#2bb97f', + border: '5px solid red', + margin: '5px', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + direction, + showScrollbar: 'always', + rtlEnabled, + }, `#${id}`); + + const scrollable = new Scrollable(`#${id}`, { useNative, direction }); + + await scrollable.scrollTo(initialScrollOffset); + await scrollable.scrollToElement(`#${id}element`); + } + } + } + + + await testScreenshot(page, `ScrollToElement, element more container direction=${direction}.png`); + + }); + + test(`ScrollToElement with scaling scale(1.5),direction=${direction}`, async ({ page }) => { + + const positions = [ + { initialScrollOffset: { top: 0, left: 0 }, position: 'fromTLCorner' }, + { initialScrollOffset: { top: 0, left: 290 }, position: 'fromTRCorner' }, + { initialScrollOffset: { top: 290, left: 290 }, position: 'fromBRCorner' }, + { initialScrollOffset: { top: 290, left: 0 }, position: 'fromBLCorner' }, + + { initialScrollOffset: { top: 0, left: 160 }, position: 'fromT' }, + { initialScrollOffset: { top: 160, left: 290 }, position: 'fromR' }, + { initialScrollOffset: { top: 290, left: 160 }, position: 'fromB' }, + { initialScrollOffset: { top: 160, left: 0 }, position: 'fromL' }, + + // from inside + + { initialScrollOffset: { top: 165, left: 175 }, position: 'fromInsideTLPart' }, + { initialScrollOffset: { top: 140, left: 140 }, position: 'fromInsideRBPart' }, + ]; + + for (const useNative of [true, false]) { + for (const rtlEnabled of [true, false]) { + for (const { initialScrollOffset } of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { + border: '1px solid black', + display: 'inline-block', + }); + + await appendElementTo(page, `#${id}`, 'div', `${id}scrollableContent`, { + width: '250px', + height: '250px', + border: '1px solid #0b837a', + backgroundColor: 'lightskyblue', + transform: 'scale(1.5)', + transformOrigin: '0 0', + }); + + await appendElementTo(page, `#${id}scrollableContent`, 'div', `${id}element`, { + position: 'absolute', + boxSizing: 'border-box', + left: '20px', + top: '20px', + height: '200px', + width: '200px', + backgroundColor: '#2bb97f', + border: '5px solid red', + margin: '5px', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + direction, + showScrollbar: 'always', + rtlEnabled, + }, `#${id}`); + + const scrollable = new Scrollable(`#${id}`, { useNative, direction }); + + await scrollable.scrollTo(initialScrollOffset); + await scrollable.scrollToElement(`#${id}element`); + } + } + } + + + await testScreenshot(page, `ScrollToElement with scaling scale(1.5),direction=${direction}.png`); + + }); + }); + + (['horizontal'] as ScrollableDirection[]).forEach((direction) => { + [false, true].forEach((useNative) => { + [false, true].forEach((useSimulatedScrollbar) => { + test(`Scroll offset after resize, rtlEnabled: true, useNative: '${useNative}', useSimulatedScrollbar: '${useSimulatedScrollbar}, container.width = 75 -> 50 -> 75 -> 100 -> 75`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '100px', height: '100px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 50, + height: 50, + useNative, + rtlEnabled: true, + useSimulatedScrollbar, + direction: 'horizontal', + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: 50, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(24, 26); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(100); + + await expect(await scrollable.scrollOffset()).eql({ left: 0, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + expect(left).toBe(0); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + }); + + [1, 10, 20].forEach((scrollOffset) => { + test(`Scroll offset after resize, rtlEnabled: true, useNative: '${useNative}', useSimulatedScrollbar: '${useSimulatedScrollbar}, scrollTo(Right - ${scrollOffset}), container.width = 75 -> 50 -> 100 -> 75 -> 50`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '100px', height: '100px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 50, + height: 50, + useNative, + rtlEnabled: true, + useSimulatedScrollbar, + direction: 'horizontal', + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + + await scrollable.scrollTo({ left: 50 - scrollOffset }); + await scrollable.update(); + + await scrollable.setContainerCssWidth(75); + + let expectedScrollOffset = (await scrollable.getMaxScrollOffset()).horizontal - scrollOffset; + await page.expect((await scrollable.scrollOffset()).left) + .within(expectedScrollOffset - 1, expectedScrollOffset + 1); + await expect((await scrollable.scrollOffset()).top).eql(0); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = expectedScrollOffset * 0.75; + await expect(left).within(expectedTranslateValue - 1, expectedTranslateValue + 1); + } + + await scrollable.setContainerCssWidth(50); + + expectedScrollOffset = (await scrollable.getMaxScrollOffset()).horizontal - scrollOffset; + await page.expect((await scrollable.scrollOffset()).left) + .within(expectedScrollOffset - 1, expectedScrollOffset + 1); + await expect((await scrollable.scrollOffset()).top).eql(0); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = expectedScrollOffset * 0.5; + await expect(left).within(expectedTranslateValue - 1, expectedTranslateValue + 1); + } + + await scrollable.setContainerCssWidth(100); + + await expect(await scrollable.scrollOffset()).eql({ left: 0, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + expect(left).toBe(0); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: 50, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(24, 26); + } + + }); + }); + + [30, 40, 50].forEach((scrollOffset) => { + test(`Scroll offset after resize, rtlEnabled: true, useNative: '${useNative}', useSimulatedScrollbar: '${useSimulatedScrollbar}, scrollTo(${scrollOffset}), container.width = 75 -> 50 -> 100 -> 75 -> 50`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '100px', height: '100px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 50, + height: 50, + useNative, + rtlEnabled: true, + useSimulatedScrollbar, + direction: 'horizontal', + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + + await scrollable.scrollTo({ left: scrollOffset }); + await scrollable.update(); + + await scrollable.setContainerCssWidth(75); + + const expectedScrollOffset = scrollOffset - 25; + await page.expect((await scrollable.scrollOffset()).left) + .within(expectedScrollOffset - 0.5, expectedScrollOffset + 0.5); + await expect((await scrollable.scrollOffset()).top).eql(0); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = expectedScrollOffset * 0.75; + await expect(left).within(expectedTranslateValue - 0.5, expectedTranslateValue + 0.5); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: scrollOffset, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = scrollOffset * 0.5; + await expect(left).within(expectedTranslateValue - 0.5, expectedTranslateValue + 0.5); + } + + await scrollable.setContainerCssWidth(100); + + await expect(await scrollable.scrollOffset()).eql({ left: 0, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + expect(left).toBe(0); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: 50, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(24, 26); + } + + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/visibility.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/visibility.spec.ts new file mode 100644 index 000000000000..d6601df33b87 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/visibility.spec.ts @@ -0,0 +1,67 @@ +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.html')}`; + +test.describe('Scrollable_visibility_integration', () => { + 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'); + }); + + type ScrollableDirection = 'both' | 'horizontal' | 'vertical'; + + (['both'] as ScrollableDirection[]).forEach((direction) => { + [false, true].forEach((useNative) => { + [false, true].forEach((rtlEnabled) => { + [false, true].forEach((useSimulatedScrollbar) => { + test(`Scroll should save position on dxhiding when scroll is hidden, dir: ${direction}, useNative: ${useNative}, useSimulatedScrollbar: ${useSimulatedScrollbar}, rtlEnabled: ${rtlEnabled}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '200px', height: '200px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + rtlEnabled, + useSimulatedScrollbar, + direction, + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + await scrollable.scrollTo({ left: 10, top: 20 }); + + const expectedScrollOffsetValue = { left: 10, top: 20 }; + await expect(await scrollable.scrollOffset()).eql(expectedScrollOffsetValue); + + await testScreenshot(page, `Scroll position before hide, useNative=${useNative},rtl=${rtlEnabled},useSimScrollbar=${useSimulatedScrollbar}.png`, { element: page.locator('#scrollable') }); + + await page.expect(compareResults.isValid()) + .ok(); + + await scrollable.triggerHidingEvent(); + await scrollable.hide(); + await scrollable.scrollTo({ left: 0, top: 0 }); + await scrollable.show(); + await scrollable.triggerShownEvent(); + + await expect(await scrollable.scrollOffset()).eql(expectedScrollOffsetValue); + await testScreenshot(page, `Scroll position after show, useNative=${useNative},rtl=${rtlEnabled},useSimScrollbar=${useSimulatedScrollbar}.png`, { element: page.locator('#scrollable') }); + + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/common.spec.ts new file mode 100644 index 000000000000..60bd7e662e31 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/common.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')}`; + +test.describe('Splitter_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 getScreenshotName = (state: string) => `Splitter apearance - handle in ${state} state.png`; + + test('ResizeHandle appearance in inactive state, allowKeyboardNavigation', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 600, + height: 300, + dataSource: [{ + text: 'pane_1', + }, { + text: 'pane_2', + resizable: false, + }], + }); + + await testScreenshot(page, getScreenshotName('inactive'), { element: '#container' }); + + }); + + test('ResizeHandle appearance in different states, allowKeyboardNavigation', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 600, + height: 300, + dataSource: [{ + text: 'pane_1', + collapsible: true, + }, { + text: 'pane_2', + collapsible: true, + }], + }); + + const splitter = page.locator('#container'); + + await click(page.locator('body'), { offsetX: -50 }); + + await testScreenshot(page, getScreenshotName('normal'), { element: '#container' }); + + await hover(splitter.resizeHandles.nth(0)); + + await testScreenshot(page, getScreenshotName('hover'), { element: '#container' }); + + await page.dispatchEvent(splitter.resizeHandles.nth(0), 'mousedown') + .wait(500); + + await testScreenshot(page, getScreenshotName('active'), { element: '#container' }); + + await dispatchEvent(splitter.resizeHandles.nth(0), 'mouseup'); + + await click(splitter.resizeHandles.nth(0)); + + await testScreenshot(page, getScreenshotName('focused'), { element: '#container' }); + + }); + + ['horizontal', 'vertical'].forEach((orientation) => { + test(`Splitter appearance, orientation='${orientation}'`, async ({ page }) => { + + await createWidget(page, 'dxSplitter', { + orientation, + width: 600, + height: 300, + dataSource: [{ + text: 'pane_1', collapsible: true, + }, { + text: 'pane_2', collapsible: true, + }, + ], + }); + + + await testScreenshot(page, `Splitter appearance, orientation='${orientation}'.png`, { element: '#container' }); + + }); + + test(`Nested Splitter appearance, orientation='${orientation}'`, async ({ page }) => { + + await createWidget(page, 'dxSplitter', { + orientation, + width: 600, + height: 300, + dataSource: [{ text: 'Pane_1', collapsible: true }, + { + splitter: { + orientation: orientation === 'horizontal' ? 'vertical' : 'horizontal', + dataSource: [{ + text: 'Pane_2_1', collapsible: true, + }, { + text: 'Pane_2_2', collapsible: true, + }, { + text: 'Pane_2_3', collapsible: true, + }], + }, + collapsible: true, + }, + { text: 'Pane_3', collapsible: true }, + ], + }); + + + await testScreenshot(page, `Nested Splitter appearance, orientation='${orientation}'.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/events.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/events.spec.ts new file mode 100644 index 000000000000..76044b505f4e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/events.spec.ts @@ -0,0 +1,44 @@ +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('Splitter_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('Panes should not be able to resize when onResizeStart event canceled', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 408, + height: 408, + onResizeStart(e) { + const { event } = e; + event.cancel = true; + }, + dataSource: [{ size: '200px' }, { size: '200px' }], + }); + + const splitter = page.locator('#container'); + + await (async () => { + const box = await splitter.resizeHandles.nth(0).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 + 0, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await expect(splitter.option('items[0].size')).eql(200); + await expect(splitter.option('items[1].size')).eql(200); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/integration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/integration.spec.ts new file mode 100644 index 000000000000..ae5e0c8c008e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/integration.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('Splitter_integration', () => { + 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 splitter pane should be rendered with the correct ratio inside the tab content of TabPanel if pane.size uses pixels', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + width: '100%', + height: 300, + deferRendering: true, + templatesRenderAsynchronously: true, + dataSource: [{ + title: 'Tab_1', + collapsible: true, + text: 'Tab_1 content', + }, { + title: 'Tab_2', + collapsible: true, + template: () => ($('
') as any).dxSplitter({ + orientation: 'horizontal', + allowKeyboardNavigation: true, + dataSource: [{ + size: '100px', + text: 'Pane_1', + collapsible: true, + template: () => $('
').text('Pane_1'), + }, { + collapsible: true, + splitter: { + orientation: 'vertical', + dataSource: [{ + text: 'Pane_2_1', + collapsible: true, + template: () => $('
').text('Pane_2_1'), + }, { + text: 'Pane_2_2', + collapsible: true, + template: () => $('
').text('Pane_2_2'), + }], + }, + }], + }), + }], + }); + + const tabPanel = page.locator('#container'); + + await tabPanel.tabs.getItem(1).element.click() + .click(tabPanel.multiView.element); + + await testScreenshot(page, 'Splitter in tab content, pane_1.size=`100px`.png', { element: '#container' }); + + await resizeWindow(600, 400); + + await testScreenshot(page, 'Splitter in tab content after window resize, pane_1.size=`100px`.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/keyboard.spec.ts new file mode 100644 index 000000000000..b6ccf162e564 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/keyboard.spec.ts @@ -0,0 +1,101 @@ +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('Splitter_keyboard', () => { + 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 next resize handle should be focused after tab press', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 400, + height: 400, + dataSource: [ + { text: 'Pane_1' }, + { text: 'Pane_2' }, + { text: 'Pane_2' }, + ], + }); + + const splitter = page.locator('#container'); + + await page.click(splitter.resizeHandles.nth(0)); + + await page.expect(splitter.getResizeHandle(0).isFocused) + .ok() + .expect(splitter.getResizeHandle(1).isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(splitter.getResizeHandle(0).isFocused) + .notOk() + .expect(splitter.getResizeHandle(1).isFocused) + .ok(); + + }); + + test('The previous resize handle should be focused after shift+tab press', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 400, + height: 400, + dataSource: [ + { text: 'Pane_1' }, + { text: 'Pane_2' }, + { text: 'Pane_2' }, + ], + }); + + const splitter = page.locator('#container'); + + await page.click(splitter.resizeHandles.nth(1)); + + await page.expect(splitter.getResizeHandle(1).isFocused) + .ok() + .expect(splitter.getResizeHandle(0).isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(splitter.getResizeHandle(1).isFocused) + .notOk() + .expect(splitter.getResizeHandle(0).isFocused) + .ok(); + + }); + + [true, false].forEach((allowKeyboardNavigation) => { + test(`The resize handle should not change its focused state after the pane collapses, allowKeyboardNavigation=${allowKeyboardNavigation}`, async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 400, + height: 400, + allowKeyboardNavigation, + dataSource: [ + { text: 'Pane_1', collapsible: true }, + { text: 'Pane_2' }, + ], + }); + + const splitter = page.locator('#container'); + + await page.click(splitter.getResizeHandle(0).getCollapsePrev()); + + if (allowKeyboardNavigation) { + await page.expect(splitter.getResizeHandle(0).isFocused) + .ok(); + } else { + await page.expect(splitter.getResizeHandle(0).isFocused) + .notOk(); + } + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/resize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/resize.spec.ts new file mode 100644 index 000000000000..0c299ce9f001 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/resize.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('Splitter_integration', () => { + 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('non resizable pane should not change its size during resize', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: '100%', + height: 300, + dataSource: [{ + text: 'Pane_1', + }, { + text: 'Pane_1', + }, { + text: 'Pane_3', + size: '300px', + resizable: false, + }], + }); + + const splitter = page.locator('#container'); + + await page.expect(splitter.getItem(2).element.clientWidth) + .eql(300); + + await resizeWindow(400, 400); + + await page.expect(splitter.getItem(2).element.clientWidth) + .eql(145); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/stepper/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/stepper/common.spec.ts new file mode 100644 index 000000000000..e75111c18968 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/stepper/common.spec.ts @@ -0,0 +1,192 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Stepper_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 commonItems: any[] = [ + { icon: 'cart', label: 'Cart' }, + { icon: 'clipboardtasklist', label: 'Shipping Info' }, + { icon: 'gift', label: 'Promo Code', optional: true }, + { icon: 'packagebox', label: 'Checkout' }, + { icon: 'checkmarkcircle', label: 'Ordered' }, + ]; + + ['horizontal', 'vertical'].forEach((orientation) => { + test('Stepper common properties', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await appendElementTo(page, '#container', 'div', 'stepper2'); + + const containerStyle = orientation === 'horizontal' ? 'width: 800px; flex-direction: column;' : 'height: 600px; width: 400px'; + await setAttribute(page, '#container', 'style', `display: flex; gap: 40px; ${containerStyle}`); + + const stepperOptions = { + selectedIndex: 4, + orientation, + dataSource: commonItems, + }; + + const stepperRTLOptions = { + ...stepperOptions, + rtlEnabled: true, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + await createWidget(page, 'dxStepper', stepperRTLOptions, '#stepper2'); + + + await testScreenshot(page, `Stepper orient=${orientation}.png`, { + element: '#container', + }); + + }); + }); + + test('Stepper text overflow in horizontal orientation', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await setAttribute(page, '#container', 'style', 'width: 200px; height: 150px; overflow: auto;'); + + await appendElementTo(page, '#otherContainer', 'div', 'stepper2'); + await setAttribute(page, '#otherContainer', 'style', 'width: 400px; height: 150px; overflow: auto;'); + + await setAttribute(page, '#parentContainer', 'style', 'width: 400px;'); + + const stepperOptions = { + dataSource: commonItems, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper2'); + + await testScreenshot(page, 'Stepper text overflow orient=horizontal.png', { element: '#parentContainer' }); + + }); + + test('Stepper text overflow in vertical orientation', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await appendElementTo(page, '#container', 'div', 'stepper2'); + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; width: 400px'); + + const stepperOptions = { + dataSource: commonItems, + width: 120, + height: 400, + orientation: 'vertical', + }; + + const stepperRTLOptions = { + ...stepperOptions, + rtlEnabled: true, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + await createWidget(page, 'dxStepper', stepperRTLOptions, '#stepper2'); + + await testScreenshot(page, 'Stepper text overflow orient=vertical.png', { element: '#container' }); + + }); + + [true, false].forEach((selectOnFocus) => { + test('Stepper item states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 150px;'); + + const dataSource: any[] = [ + { label: 'Default' }, + { label: 'Valid', isValid: true, optional: true }, + { label: 'Invalid', isValid: false, optional: true }, + { + label: 'Disabled', icon: 'packagebox', disabled: true, optional: true, + }, + { label: 'Disabled Valid', disabled: true, isValid: true }, + { label: 'Disabled Invalid', disabled: true, isValid: false }, + { label: 'With Text', text: 'T', optional: true }, + ]; + + const stepperOptions = { + selectOnFocus, + dataSource, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + + const state = selectOnFocus ? 'selected' : 'focused'; + + await page.keyboard.press('Tab'); + await testScreenshot(page, `Stepper 1st step selected,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper valid step ${state},selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper invalid step ${state},selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper disabled step focused,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper disabled valid step focused,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper disabled invalid step focused,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper last step ${state},selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + }); + }); + + test('Stepper completed item states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 150px;'); + + const dataSource: any[] = [ + { label: 'Default' }, + { label: 'Valid', isValid: true, optional: true }, + { label: 'Invalid', isValid: false, optional: true }, + { label: 'With Text', text: 'T', optional: true }, + ]; + + const stepperOptions = { + selectOnFocus: false, + dataSource, + selectedIndex: 3, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + const stepper = page.locator('#container'); + await stepper.getItem(3).element.click(); + + await page.keyboard.press('ArrowLeft'); + await testScreenshot(page, 'Completed invalid step focused.png', { element: '#stepper' }); + + await page.keyboard.press('ArrowLeft'); + await testScreenshot(page, 'Completed valid step focused.png', { element: '#stepper' }); + + await page.keyboard.press('ArrowLeft'); + await testScreenshot(page, 'Completed step focused.png', { element: '#stepper' }); + + await page.locator('body').click(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/common.spec.ts new file mode 100644 index 000000000000..ca5bb1375e60 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/common.spec.ts @@ -0,0 +1,365 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TabPanel_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 TABS_RIGHT_NAV_BUTTON_CLASS = 'dx-tabs-nav-button-right'; + const TABS_LEFT_NAV_BUTTON_CLASS = 'dx-tabs-nav-button-left'; + + ['with scrolling', 'without scrolling'].forEach((mode) => { + const testName = `TabPanel borders ${mode}`; + test(testName, async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + const tabPanelOptions = { + dataSource, + itemTemplate: (_, __, itemElement) => { + ($('
').css('marginTop', '10px') as any) + .dxTabs({ + items: [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ], + width: 300, + showNavButtons: true, + }) + .appendTo(itemElement); + }, + height: 120, + width: mode === 'with scrolling' ? 300 : 900, + showNavButtons: true, + }; + + await createWidget(page, 'dxTabPanel', tabPanelOptions); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('TabPanel text-overflow with tabsPosition left', async ({ page }) => { + + const dataSource: any[] = [ + { icon: 'user', text: 'John Heart', title: 'John Heart' }, + { icon: 'user', text: 'Marina Elizabeth Thomas Grace Sophia', title: 'Mariya Elizabeth Thomas Grace Sophia' }, + { icon: 'user', text: 'Robert Reagan', title: 'Robert Reagan' }, + { icon: 'user', text: 'Greta Sims', title: 'Greta Sims' }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + width: 600, + height: 250, + tabsPosition: 'left', + showNavButtons: true, + }); + + await testScreenshot(page, 'TabPanel text-overflow when tabsPosition is left.png', { element: '#container' }); + + await setAttribute(page, '.dx-tabs-wrapper', 'style', 'max-width: 130px;'); + + await testScreenshot(page, 'TabPanel text-overflow when tabs wrapper width is limited.png', { element: '#container' }); + + }); + + test('TabPanel focus borders after change selectedIndex in runtime', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 120, + width: 300, + }); + + const tabPanel = page.locator('#container'); + await tabPanel.option('selectedIndex', 1); + + await testScreenshot(page, 'TabPanel focus borders.png', { element: '#container' }); + + }); + + test('TabPanel navigation buttons hover', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + const tabPanelOptions = { + dataSource, + height: 120, + width: 400, + showNavButtons: true, + selectedIndex: 2, + useInkRipple: false, + }; + + await createWidget(page, 'dxTabPanel', tabPanelOptions); + + await page.locator('body').click(); + + const rightNavButton = page.locator(`.${TABS_RIGHT_NAV_BUTTON_CLASS}`); + await page.click(rightNavButton) + .hover(rightNavButton); + + await testScreenshot(page, 'TabPanel right navigation button hovered.png', { element: '#container' }); + + const leftNavButton = page.locator(`.${TABS_LEFT_NAV_BUTTON_CLASS}`); + await leftNavButton.hover(); + + await testScreenshot(page, 'TabPanel left navigation button hovered.png', { element: '#container' }); + + }); + + ['top', 'right', 'bottom', 'left'].forEach((tabsPosition) => { + const testName = `TabPanel without focus,tabsPosition=${tabsPosition}`; + test(testName, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabpanel'); + await appendElementTo(page, '#container', 'div', 'tabpanel-rtl'); + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; flex-direction: column; width: fit-content;'); + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + const tabPanelOptions = { + dataSource, + height: 250, + width: 450, + tabsPosition, + useInkRipple: false, + }; + + await createWidget(page, 'dxTabPanel', tabPanelOptions, '#tabpanel'); + await createWidget(page, 'dxTabPanel', { ...tabPanelOptions, rtlEnabled: true }, '#tabpanel-rtl'); + + + await page.locator('body').click(); + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('TabPanel item focus when clicking on multiview', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 250, + width: 450, + useInkRipple: false, + }); + + const tabPanel = page.locator('#container'); + await tabPanel.multiView.element.click(); + await testScreenshot(page, 'TabPanel item focus when clicking on multiview.png', { element: '#container' }); + + }); + + const positions = ['top', 'left', 'right', 'bottom']; + + positions.forEach((tabsPosition) => { + test(`TabPanel border appearance when it placed inside the content of TabPanel with=${tabsPosition}`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-tabpanel { margin: 10px }'); + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 700, + width: 500, + tabsPosition, + selectedIndex: 1, + deferRendering: true, + itemTemplate: ClientFunction(() => { + const $container = $('
'); + + positions.forEach((position) => { + const $tabPanel = ($('
') as any).dxTabPanel({ + height: 120, + tabsPosition: position, + dataSource, + }); + + $container.append($tabPanel); + + $container.append($('
')); + }); + + return $container; + }, { + dependencies: { dataSource, positions }, + }), + }); + + + await testScreenshot(page, `Nested TabPanel borders appearance,tabsPos=${tabsPosition}.png`, { element: '#container' }); + + }); + }); + + test('TabPanel tabs min-width', async ({ page }) => { + + const dataSource: any[] = [ + { text: 'ok', title: 'ok' }, + { icon: 'comment' }, + { icon: 'user' }, + { icon: 'money' }, + { text: 'ok', title: 'ok', icon: 'search' }, + { text: 'alignright', title: 'alignright', icon: 'alignright' }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 250, + width: 900, + useInkRipple: false, + }); + + await testScreenshot(page, 'TabPanel tabs min-width.png', { element: '#container' }); + + }); + + ['left', 'right'].forEach((tabsPosition) => { + test('TabPanel should be shown correctly even if there is only one tab', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 120, + width: 300, + tabsPosition, + }); + + + await testScreenshot(page, `TabPanel with single tab, tabPosition=${tabsPosition}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/focus.spec.ts new file mode 100644 index 000000000000..8498da849ec8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/focus.spec.ts @@ -0,0 +1,266 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + // T821726 + test('[{0: selected}, {1}] -> click to tabs[1] -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1', 'Item 2'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.tabs.getItem(1).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); + + test('[{0: selected}] -> click to multiView -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.multiView.getItem(0).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); + + test('[{0: selected}, {1}, {2}] -> click to tabs[1] -> navigate to tabs[2] -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1', 'Item 2', 'Item 3'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.tabs.getItem(1).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .ok() + .expect(tabPanel.tabs.getItem(2).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(2).isFocused) + .notOk(); + + await page.keyboard.press('ArrowRight') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(2).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(2).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(2).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(2).isFocused) + .notOk(); + + }); + + test('[{0: selected}, {1}] -> click to multiView -> navigate to tabs[1] -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1', 'Item 2'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.multiView.getItem(0).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk(); + + await page.keyboard.press('ArrowRight') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk(); + + }); + + test('[{0: selected}] -> click to multiView -> press "tab" -> press "tab"', async () => { + await createWidget(page, 'dxTabPanel', { + items: ['Item 1'], + }); + + const tabPanel = page.locator('#container'); + + await tabPanel.multiView.getItem(0).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok(); + + await page.keyboard.press('Tab') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + await page.keyboard.press('Tab') + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); + + test('[{0: selected}] -> focusin by press "tab" -> press "tab"', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await page.click(page.locator('body'), { offsetY: 400 }) + .pressKey('tab') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok(); + + await page.keyboard.press('Tab') + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/tabs/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/tabs/common.spec.ts new file mode 100644 index 000000000000..2ef152c7c4c0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/tabs/common.spec.ts @@ -0,0 +1,201 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Tabs_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 TAB_CLASS = 'dx-tab'; + + test('Tabs background color', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + await setAttribute(page, '#container', 'style', 'width: 400px; background: #fff000 !important;'); + + const dataSource: any[] = [ + { text: 'John Heart' }, + { text: 'Marina Thomas' }, + { text: 'Robert Reagan' }, + { text: 'Greta Sims' }, + ]; + + await createWidget(page, 'dxTabs', { dataSource }, '#tabs'); + + await testScreenshot(page, 'Tabs background color.png', { element: '#container' }); + + }); + + test('Tabs text-overflow with vertical orientation', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; width: fit-content;'); + + const iconPositions = ['start', 'end', 'top']; + const dataSource: any[] = [ + { icon: 'user', text: 'John Heart' }, + { icon: 'user', text: 'Marina Elizabeth Thomas Grace Sophia Alexander Benjamin Olivia Nicholas Victoria Michael Emily' }, + { icon: 'user', text: 'Robert Reagan' }, + { icon: 'user', text: 'Greta Sims' }, + ]; + + await Promise.all(iconPositions.map((iconPosition) => appendElementTo(page, '#container', 'div', `tabs-${iconPosition}`))); + await Promise.all(iconPositions.map((iconPosition) => createWidget(page, 'dxTabs', { + dataSource, + iconPosition, + width: 130, + orientation: 'vertical', + }, `#tabs-${iconPosition}`))); + + await testScreenshot(page, 'Tabs text-overflow.png', { element: '#container' }); + + }); + + [true, false].forEach((rtlEnabled) => { + test('Tabs icon position', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; flex-direction: column; gap: 20px; width: 800px'); + + const iconPositions = ['start', 'end', 'top', 'bottom']; + const dataSource: any[] = [ + { text: 'user', badge: '1' }, + { text: 'comment', icon: 'comment', badge: 'text' }, + { icon: 'user' }, + { icon: 'money' }, + ]; + + await Promise.all(iconPositions.map((iconPosition) => appendElementTo(page, '#container', 'div', `tabs-${iconPosition}`))); + await Promise.all(iconPositions.map((iconPosition) => createWidget(page, 'dxTabs', { + dataSource, + iconPosition, + rtlEnabled, + }, `#tabs-${iconPosition}`))); + + + await testScreenshot(page, `Tabs icon position,rtl=${rtlEnabled}.png`, { element: '#container' }); + + }); + }); + + test('Tabs with width: auto in flex container', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + await setAttribute(page, '#container', 'style', 'display: flex; width: 800px;'); + + const dataSource: any[] = [ + { text: 'ok' }, + { icon: 'comment' }, + { icon: 'user' }, + { icon: 'money' }, + { text: 'ok', icon: 'search' }, + { text: 'alignright', icon: 'alignright' }, + ]; + + await createWidget(page, 'dxTabs', { dataSource, width: 'auto' }, '#tabs'); + + await testScreenshot(page, 'Tabs with width auto.png', { element: '#tabs' }); + + }); + + ['primary', 'secondary'].forEach((stylingMode) => { + ['horizontal', 'vertical'].forEach((orientation) => { + test('Tabs item selected states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + await appendElementTo(page, '#container', 'div', 'tabs-rtl'); + await setAttribute(page, '#container', 'style', `display: flex; gap: 40px; flex-direction: ${orientation === 'horizontal' ? 'column' : 'row'}; width: fit-content;`); + + const dataSource: any[] = [ + { text: 'John Heart' }, + { text: 'Marina Thomas', disabled: true }, + { text: 'Robert Reagan' }, + { text: 'Greta Sims' }, + { text: 'Olivia Peyton' }, + { text: 'Ed Holmes' }, + { text: 'Wally Hobbs' }, + { text: 'Brad Jameson' }, + ]; + + const tabsOptions = { + dataSource, + orientation, + stylingMode, + width: orientation === 'horizontal' ? 450 : 'auto', + height: orientation === 'horizontal' ? 'auto' : 250, + selectedItem: dataSource[2], + showNavButtons: true, + }; + + await createWidget(page, 'dxTabs', tabsOptions, '#tabs'); + await createWidget(page, 'dxTabs', { ...tabsOptions, rtlEnabled: true }, '#tabs-rtl'); + + + await testScreenshot(page, `Tabs item selected, orientation=${orientation}, stylingMode=${stylingMode}.png`, { element: '#container' }); + + }); + }); + }); + + test('Tabs item states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + + const dataSource: any[] = [ + { text: 'John Heart' }, + { text: 'Marina Thomas', disabled: true }, + { text: 'Robert Reagan' }, + { text: 'Greta Sims' }, + { text: 'Olivia Peyton' }, + { text: 'Ed Holmes' }, + { text: 'Wally Hobbs' }, + { text: 'Brad Jameson' }, + ]; + + const tabsOptions = { + dataSource, + selectOnFocus: false, + showNavButtons: true, + width: 600, + useInkRipple: false, + }; + + await createWidget(page, 'dxTabs', tabsOptions, '#tabs'); + + await testScreenshot(page, 'Tabs without focus.png', { element: '#tabs' }); + + await page.keyboard.press('Tab'); + await testScreenshot(page, 'Tabs item focused.png', { element: '#tabs' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, 'Tabs disabled item focused.png', { element: '#tabs' }); + + const thirdItem = page.locator(`.${TAB_CLASS}:nth-child(3)`); + const fourthItem = page.locator(`.${TAB_CLASS}:nth-child(4)`); + + await page.keyboard.press('ArrowRight') + .dispatchEvent(thirdItem, 'mousedown'); + await testScreenshot(page, 'Tabs item active.png', { element: '#tabs' }); + await thirdItem.dispatchEvent('mouseup'); + + await page.click(thirdItem) + .hover(fourthItem); + await testScreenshot(page, 'Tabs item hovered.png', { element: '#tabs' }); + + await page.locator('body').click(); + + await thirdItem.hover(); + await testScreenshot(page, 'Tabs selected item hovered.png', { element: '#tabs' }); + + await thirdItem.dispatchEvent('mousedown'); + await testScreenshot(page, 'Tabs selected item active.png', { element: '#tabs' }); + await thirdItem.dispatchEvent('mouseup'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/common.spec.ts new file mode 100644 index 000000000000..b153ac6a8577 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/common.spec.ts @@ -0,0 +1,111 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toolbar_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'); + }); + + ['never', 'always'].forEach((locateInMenu: LocateInMenuMode) => { + [true, false].forEach((rtlEnabled) => { + test(`Default nested widgets render,items[].locateInMenu=${locateInMenu},rtl=${rtlEnabled}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'toolbar'); + await setAttribute(page, '#container', 'style', 'width: 1184px;'); + + const supportedWidgets: ToolbarItemComponent[] = ['dxAutocomplete', 'dxButton', 'dxCheckBox', 'dxDateBox', 'dxMenu', 'dxSelectBox', 'dxTabs', 'dxTextBox', 'dxButtonGroup', 'dxDropDownButton']; + const toolbarItems: any[] = supportedWidgets.map((widgetName) => ({ + location: 'before', + locateInMenu, + widget: widgetName, + options: { + value: new Date(2021, 9, 17), + stylingMode: 'contained', + text: `${widgetName}`, + icon: 'refresh', + items: [{ text: `${widgetName}`, icon: 'export' }], + iconPosition: widgetName === 'dxTabs' ? 'start' : undefined, + width: locateInMenu === 'never' ? 115 : undefined, + }, + })); + + toolbarItems.push({ + location: 'before', + locateInMenu, + text: 'Some text', + }); + + await createWidget(page, 'dxToolbar', { + items: toolbarItems, + rtlEnabled, + width: locateInMenu === 'auto' ? 50 : '100%', + }, '#toolbar'); + + + const toolbar = page.locator('#toolbar'); + let targetContainer = page.locator('#container'); + + const overflowMenu = toolbar.getOverflowMenu(); + + if (locateInMenu !== 'never') { + await overflowMenu.click(); + + targetContainer = overflowMenu.getPopup().getContent(); + } + + await setStyleAttribute(page, targetContainer, 'background-color: gold;'); + + await testScreenshot(page, `Toolbar widgets render${rtlEnabled ? ' rtl=true' : ''},items[]locateInMenu=${locateInMenu}.png`, { + element: targetContainer, + }); + + }); + }); + }); + + [true, false].forEach((rtlEnabled) => { + test(`Default nested widgets render, rtlEnabled: ${rtlEnabled}`, async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'box-sizing: border-box; width: 400px; height: 400px; padding: 8px;'); + await appendElementTo(page, '#container', 'div', 'toolbar'); + + const supportedWidgets: ToolbarItemComponent[] = ['dxAutocomplete', 'dxButton', 'dxCheckBox', 'dxDateBox', 'dxMenu', 'dxSelectBox', 'dxTabs', 'dxTextBox', 'dxButtonGroup', 'dxDropDownButton']; + const toolbarItems: any[] = supportedWidgets.map((widgetName) => ({ + location: 'before', + widget: widgetName, + options: { + value: new Date(2021, 9, 17), + stylingMode: 'contained', + text: 1, + items: [{ text: 1 }, { text: 2 }], + showClearButton: true, + }, + })); + + toolbarItems.push({ + location: 'after', + text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry', + }); + + await createWidget(page, 'dxToolbar', { + multiline: true, + items: toolbarItems, + rtlEnabled, + }, '#toolbar'); + + + await testScreenshot(page, `Toolbar nested widgets render in multiline rtl=${rtlEnabled}.png`, { + element: '#toolbar', + }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenu.spec.ts new file mode 100644 index 000000000000..77187e097117 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenu.spec.ts @@ -0,0 +1,350 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setClassAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toolbar_OverflowMenu', () => { + 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 BUTTON_CLASS = 'dx-button'; + const ACTIVE_STATE_CLASS = 'dx-state-active'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + + const stylingModes = ['text', 'outlined', 'contained']; + const buttonTypes = ['danger', 'default', 'normal', 'success']; + const stateClasses = [FOCUSED_STATE_CLASS, HOVER_STATE_CLASS, ACTIVE_STATE_CLASS]; + + test('Drop down button should lost hover and active state', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'toolbar'); + await appendElementTo(page, '#container', 'button', 'button', { + width: '50px', height: '50px', backgroundColor: 'steelblue', marginTop: '400px', + }); + + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'item1', locateInMenu: 'always' }, + { text: 'item2', locateInMenu: 'always' }, + { text: 'item3', locateInMenu: 'always' }], + }, '#toolbar'); + + const toolbar = page.locator('#toolbar'); + const dropDownMenu = toolbar.getOverflowMenu(); + + await page.dispatchEvent(dropDownMenu.element, 'mousedown') + .expect(dropDownMenu.isActive) + .ok() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isHovered) + .notOk() + .dispatchEvent(dropDownMenu.element, 'mouseup') + .expect(dropDownMenu.isActive) + .notOk() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isHovered) + .notOk(); + + await dropDownMenu.click() + .expect(dropDownMenu.isActive) + .notOk() + .expect(dropDownMenu.isHovered) + .ok(); + + await page.hover('#button') + .expect(dropDownMenu.isHovered) + .notOk() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isActive) + .notOk(); + + await page.locator('#button').click() + .expect(dropDownMenu.isHovered) + .notOk() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isActive) + .notOk(); + + }); + + test('ButtonGroup item should not have hover and active state', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { + location: 'before', + locateInMenu: 'always', + widget: 'dxButtonGroup', + options: { + items: [{ + icon: 'alignleft', + text: 'Align left', + }, + { + icon: 'aligncenter', + text: 'Center', + }], + selectionMode: 'single', + }, + }, + ], + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + const list = overflowMenu.getList(); + const items = list.getItems(); + + expect(items.count).toBe(1); + + const item = items.nth(0); + const button = item.find(`.${BUTTON_CLASS}`); + + await button.hover() + .dispatchEvent(button, 'mousedown') + .expect(item.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(item.hasClass(FOCUSED_STATE_CLASS)) + .notOk() + .expect(item.hasClass(HOVER_STATE_CLASS)) + .notOk() + .expect(button.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(button.hasClass(FOCUSED_STATE_CLASS)) + .ok() + .expect(button.hasClass(HOVER_STATE_CLASS)) + .ok(); + + await button.dispatchEvent('mouseup') + .expect(item.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(item.hasClass(FOCUSED_STATE_CLASS)) + .notOk() + .expect(item.hasClass(HOVER_STATE_CLASS)) + .notOk() + .expect(button.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(button.hasClass(FOCUSED_STATE_CLASS)) + .ok() + .expect(button.hasClass(HOVER_STATE_CLASS)) + .ok(); + + await page.expect(overflowMenu.option('opened')) + .eql(true) + .click(button) + .expect(overflowMenu.option('opened')) + .eql(false); + + }); + + test('Click on overflow button should prevent popup\'s hideOnOutsideClick', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'item1', locateInMenu: 'always' }, + { text: 'item2', locateInMenu: 'always' }, + { text: 'item3', locateInMenu: 'always' }, + ], + }); + + const toolbar = page.locator('#container'); + const menu = toolbar.getOverflowMenu(); + + await menu.click() + .expect(menu.getPopup().getWrapper().count) + .eql(1); + + await menu.click() + .expect(menu.getPopup().getWrapper().count) + .eql(0); + + }); + + test('Toolbar buttons in menu appearance', async ({ page }) => { + + const items: any[] = stylingModes.flatMap((stylingMode) => buttonTypes.map((type) => ({ + widget: 'dxButton', + locateInMenu: 'always', + options: { + stylingMode, + text: `Button ${stylingMode}`, + type, + icon: 'home', + }, + }))); + + await createWidget(page, 'dxToolbar', { + width: 50, + multiline: false, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttons in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttons in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); + + test('Toolbar buttons as custom template appearance', async ({ page }) => { + + const items: any[] = stylingModes.flatMap((stylingMode) => buttonTypes.map((type) => { + const template = async () => page.evaluate(() => ($('
') as any).dxButton({ + stylingMode, + text: `Button ${stylingMode}`, + type, + icon: 'home', + }), { dependencies: { stylingMode, type } }); + + return { + locateInMenu: 'always', + template, + }; + })); + + await createWidget(page, 'dxToolbar', { + width: 50, + multiline: false, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttons as custom template in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttons as custom template in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); + + test('Toolbar button group appearance', async ({ page }) => { + + const items: any[] = stylingModes.map((stylingMode) => { + const buttons: ButtonGroupItem[] = buttonTypes.map((type) => ({ + text: `ButtonGroup ${stylingMode}`, + type, + icon: 'home', + })); + + return { + widget: 'dxButtonGroup', + locateInMenu: 'always', + options: { + stylingMode, + items: buttons, + }, + }; + }); + + await createWidget(page, 'dxToolbar', { + width: 50, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttonGroup in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttonGroup in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); + + test('Toolbar button group as custom template appearance', async ({ page }) => { + + const items: any[] = stylingModes.map((stylingMode) => { + const buttons: ButtonGroupItem[] = buttonTypes.map((type) => ({ + text: `${stylingMode[0].toUpperCase()}${stylingMode.slice(1)}`, + type, + icon: 'home', + })); + + const template = async () => page.evaluate(() => ($('
') as any).dxButtonGroup({ + width: 490, + stylingMode, + items: buttons, + }), { dependencies: { stylingMode, buttons } }); + + return { + locateInMenu: 'always', + template, + }; + }); + + await createWidget(page, 'dxToolbar', { + width: 50, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttonGroup as custom template in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttonGroup as custom template in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenuPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenuPopup.spec.ts new file mode 100644 index 000000000000..d8ca2844c95c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenuPopup.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toolbar_OverflowMenu_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'); + }); + + const generateItems = (count) => { + const items: { text: string; locateInMenu: string }[] = []; + + for (let i = 0; i <= count; i += 1) { + items.push({ text: `item${i}`, locateInMenu: 'always' }); + } + + return items; + }; + + test('Popup automatically update its height on window resize', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, 'Toolbar menu popup before window resize.png'); + + await resizeWindow(300, 300); + + await testScreenshot(page, 'Toolbar menu popup after window resize.png'); + + }); + + test('Popup should be position correctly with the window border collision', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + width: 50, + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, 'Toolbar menu popup collision with window border.png'); + + }); + + [true, false].forEach((rtlEnabled) => { + test(`Popup under container should be limited in height,rtlEnabled=${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + rtlEnabled, + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, `Toolbar menu popup under container rtl=${rtlEnabled}.png`); + + }); + + test(`Popup above container should be limited in height,rtlEnabled=${rtlEnabled}`, async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'margin-top: 200px'); + + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + rtlEnabled, + }); + + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, `Toolbar menu popup above container rtl=${rtlEnabled}.png`); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/treeView/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/treeView/common.spec.ts new file mode 100644 index 000000000000..9abbf9444398 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/treeView/common.spec.ts @@ -0,0 +1,264 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('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'); + }); + + test('Treeview search, selectAll item and nodes should be focused in DOM elements order when navigating with tab and shift+tab', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + searchEnabled: true, + showCheckBoxesMode: 'selectAll', + items: employees, + }); + + const treeView = page.locator('#container'); + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + const searchTextBox = treeView.getSearchTextBox(); + const node = treeView.getNode(0); + + await page.keyboard.press('Tab') + .expect(searchTextBox.isFocused) + .ok() + .pressKey('tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok() + .pressKey('shift+tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('shift+tab') + .expect(searchTextBox.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing showCheckBoxesMode from normal to selectAll at runtime', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + showCheckBoxesMode: 'normal', + items: employees, + }); + + const treeView = page.locator('#container'); + const node = treeView.getNode(0); + + await treeView.option('showCheckBoxesMode', 'selectAll'); + + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + + await page.keyboard.press('Tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing showCheckBoxesMode from none to selectAll at runtime', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + showCheckBoxesMode: 'none', + items: employees, + }); + + const treeView = page.locator('#container'); + const node = treeView.getNode(0); + + await treeView.option('showCheckBoxesMode', 'selectAll'); + + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + + await page.keyboard.press('Tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing showCheckBoxesMode at runtime with search enabled', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + searchEnabled: true, + showCheckBoxesMode: 'normal', + items: employees, + }); + + const treeView = page.locator('#container'); + const searchBar = treeView.getSearchTextBox(); + const node = treeView.getNode(0); + + await treeView.option('showCheckBoxesMode', 'selectAll'); + + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + + await page.keyboard.press('Tab') + .expect(searchBar.isFocused) + .ok() + .pressKey('tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing search panel mode at runtime', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + searchEnabled: false, + showCheckBoxesMode: 'selectAll', + items: employees, + }); + + const treeView = page.locator('#container'); + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + const node = treeView.getNode(0); + + await treeView.option('searchEnabled', 'true'); + + const searchBar = treeView.getSearchTextBox(); + + await page.keyboard.press('Tab') + .expect(searchBar.isFocused) + .ok() + .pressKey('tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview node container should be focused after selectAll item when navigating with tab when no search bar is present', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + showCheckBoxesMode: 'selectAll', + items: employees, + }); + + const treeView = page.locator('#container'); + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + const node = treeView.getNode(0); + + await page.keyboard.press('Tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('TreeView: height should be calculated correctly when searchEnabled is true (T1138605)', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + width: 300, + height: 350, + searchEnabled: true, + items: employees, + itemTemplate(item) { + return `
${item.fullName} (${item.position})
`; + }, + }); + + const treeView = page.locator('#container'); + const scrollable = treeView.getScrollable(); + + await scrollable.scrollTo({ top: 1000 }); + + await testScreenshot(page, 'TreeView scrollable has correct height.png', { element: '#container' }); + + }); + + [true, false].forEach((rtlEnabled) => { + ['selectAll', 'normal', 'none'].forEach((showCheckBoxesMode) => { + const testName = `TreeView selection showCheckBoxesMode=${showCheckBoxesMode},rtl=${rtlEnabled}`; + test(testName, async ({ page }) => { + + await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); + + await createWidget(page, 'dxTreeView', { + items: employees, + width: 300, + selectionMode: 'multiple', + showCheckBoxesMode, + rtlEnabled, + itemTemplate(item) { + return `
${item.fullName} (${item.position})
`; + }, + }); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + + ['normal', 'none'].forEach((showCheckBoxesMode) => { + const testName = `Treeview with custom icons showCheckBoxesMode=${showCheckBoxesMode}`; + test(testName, async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: employees, + width: 300, + showCheckBoxesMode, + expandIcon: 'add', + collapseIcon: 'minus', + itemTemplate(item) { + return `
${item.fullName} (${item.position})
`; + }, + }); + + await click(page.locator('.dx-treeview-item').nth(1)); + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('TreeView checkBox focus styles', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: [{ + ID: '1', + text: 'Item 1', + expanded: true, + items: [ + { + ID: '1_1', + text: 'Item 1_1', + selected: true, + }, { + ID: '1_2', + text: 'Item 1_2', + }, + ], + }], + width: 300, + showCheckBoxesMode: 'normal', + }); + + await page.keyboard.press('Tab'); + + await testScreenshot(page, 'Treeview indeterminate CheckBox focus.png', { element: '#container' }); + + await page.keyboard.press('ArrowDown'); + + await testScreenshot(page, 'Treeview checked CheckBox focus.png', { element: '#container' }); + + await page.keyboard.press('ArrowDown'); + + await testScreenshot(page, 'Treeview unchecked CheckBox focus.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts new file mode 100644 index 000000000000..e52f5b943f4f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts @@ -0,0 +1,21 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('a11y - contrast', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler a11y: Insufficient contrast of day numbers in the MonthView', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'month', + currentDate: new Date(2020, 10, 25), + }); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'month_day_number_contrast.png', { element: scheduler }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/adaptive.weekView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/adaptive.weekView.spec.ts new file mode 100644 index 000000000000..842a48beba2d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/adaptive.weekView.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const sampleData = [ + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 40), endDate: new Date(2017, 4, 22, 11, 40) }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2017, 4, 22, 12, 0), endDate: new Date(2017, 4, 22, 13, 0), allDay: true }, +]; + +const sampleDataNotRoundedMinutes = [ + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 10), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 23, 9, 5), endDate: new Date(2017, 4, 23, 11, 40) }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2017, 4, 24, 12, 12), endDate: new Date(2017, 4, 24, 13, 30) }, +]; + +const roughEqual = (actual: number, expected: number): boolean => { + const epsilon = 1.5; + return Math.abs(expected - actual) <= epsilon; +}; + +const createScheduler = async (page, data, width = '100%'): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: ['week'], + currentView: 'week', + adaptivityEnabled: true, + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + height: 600, + width, + }); +}; + +test.describe('Week view in adaptive mode', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Compact appointment should be center by vertical alignment', async ({ page }) => { + await createScheduler(page, sampleDataNotRoundedMinutes); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(0); + + const collectorsCount = await page.locator('.dx-scheduler-appointment-collector').count(); + expect(collectorsCount).toBe(3); + + const firstCollector = page.locator('.dx-scheduler-appointment-collector').nth(0); + const firstBox = await firstCollector.boundingBox(); + expect(roughEqual(firstBox!.y, 150)).toBeTruthy(); + + const secondCollector = page.locator('.dx-scheduler-appointment-collector').nth(1); + const secondBox = await secondCollector.boundingBox(); + expect(roughEqual(secondBox!.y, 150)).toBeTruthy(); + + const thirdCollector = page.locator('.dx-scheduler-appointment-collector').nth(2); + const thirdBox = await thirdCollector.boundingBox(); + expect(roughEqual(thirdBox!.y, 450)).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/API.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/API.spec.ts new file mode 100644 index 000000000000..9b53b735d185 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/API.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Agenda:API', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Html elements should be absent in Agenda view', async ({ page }) => { + const data = [ + { text: 'Website Re-Design Plan', ownerId: [4, 1, 2], roomId: [1, 2, 3], priorityId: 2, startDate: new Date('2021-05-24T16:30:00.000Z'), endDate: new Date('2021-05-24T18:30:00.000Z'), recurrenceRule: 'FREQ=WEEKLY', allDay: true }, + { text: 'Book Flights to San Fran for Sales Trip', ownerId: 2, roomId: 2, priorityId: 1, startDate: new Date('2021-05-24T19:00:00.000Z'), endDate: new Date('2021-05-24T20:00:00.000Z'), allDay: true }, + { text: 'Final Budget Review', ownerId: 1, roomId: 1, priorityId: 1, startDate: new Date('2021-05-25T19:00:00.000Z'), endDate: new Date('2021-05-25T20:35:00.000Z') }, + { text: 'New Brochures', ownerId: 4, roomId: 3, priorityId: 2, startDate: new Date('2021-05-25T21:30:00.000Z'), endDate: new Date('2021-05-25T22:45:00.000Z') }, + { text: 'Install New Database', ownerId: 2, roomId: 3, priorityId: 1, startDate: new Date('2021-05-26T16:45:00.000Z'), endDate: new Date('2021-05-26T18:15:00.000Z') }, + { text: 'Approve New Online Marketing Strategy', ownerId: 4, roomId: 2, priorityId: 1, startDate: new Date('2021-05-26T19:00:00.000Z'), endDate: new Date('2021-05-26T21:00:00.000Z') }, + { text: 'Upgrade Personal Computers', ownerId: 2, roomId: 2, priorityId: 2, startDate: new Date('2021-05-26T22:15:00.000Z'), endDate: new Date('2021-05-26T23:30:00.000Z') }, + ]; + + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 4, 25), + showAllDayPanel: true, + crossScrollingEnabled: true, + focusStateEnabled: true, + height: 600, + }); + + const scheduler = page.locator('#container'); + + await expect(scheduler.locator('.dx-scheduler-all-day-panel')).not.toBeVisible(); + await expect(scheduler.locator('.dx-scheduler-sidebar-scrollable')).not.toBeVisible(); + + const workSpace = scheduler.locator('.dx-scheduler-work-space'); + const hasBothScrollbar = await workSpace.evaluate((el) => el.classList.contains('dx-scheduler-work-space-both-scrollbar')); + expect(hasBothScrollbar).toBe(false); + + const cell0Text = await scheduler.locator('.dx-scheduler-date-table-cell').nth(0).textContent(); + expect(cell0Text).toBe(''); + + await expect(scheduler.locator('.dx-scheduler-fixed-appointments')).not.toBeVisible(); + await expect(scheduler.locator('.dx-scheduler-header-panel')).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/adaptive.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/adaptive.spec.ts new file mode 100644 index 000000000000..321e7c891b77 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/adaptive.spec.ts @@ -0,0 +1,47 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const createScheduler = async (page, groups: undefined | string[], rtlEnabled: boolean): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Website Re-Design Plan', priorityId: 2, startDate: new Date(2021, 4, 21, 16, 30), endDate: new Date(2021, 4, 21, 18, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', priorityId: 2, startDate: new Date(2021, 4, 21, 17), endDate: new Date(2021, 4, 21, 18) }, + { text: 'Install New Database', priorityId: 1, startDate: new Date(2021, 4, 21, 16), endDate: new Date(2021, 4, 21, 19, 15) }, + { text: 'Approve New Online Marketing Strategy', priorityId: 1, startDate: new Date(2021, 4, 21, 19), endDate: new Date(2021, 4, 21, 21) }, + ], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 4, 21), + rtlEnabled, + groups, + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [ + { text: 'Low Priority', id: 1, color: '#1e90ff' }, + { text: 'High Priority', id: 2, color: '#ff9747' }, + ], + label: 'Priority', + }], + }); +}; + +test.describe('Agenda:adaptive', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [false, true].forEach((rtlEnabled) => { + [ + { groups: undefined, text: 'without-groups' }, + { groups: ['priorityId'], text: 'groups' }, + ].forEach((testCase) => { + test(`${testCase.text} adaptive rtl=${rtlEnabled}`, async ({ page }) => { + await createScheduler(page, testCase.groups, rtlEnabled); + await testScreenshot(page, `agenda-${testCase.text}-adaptive-rtl=${rtlEnabled}.png`); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/editing.spec.ts new file mode 100644 index 000000000000..6b4e13e8e33f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/editing.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Agenda:Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('It should be possible to delete an appointment', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'App 1', startDate: new Date(2021, 1, 1, 12), endDate: new Date(2021, 1, 1, 13) }, + { text: 'App 2', startDate: new Date(2021, 1, 2, 12), endDate: new Date(2021, 1, 2, 13) }, + { text: 'App 3', startDate: new Date(2021, 1, 3, 12), endDate: new Date(2021, 1, 3, 13) }, + { text: 'App 4', startDate: new Date(2021, 1, 4, 12), endDate: new Date(2021, 1, 4, 13) }, + ], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 1, 1), + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'App 1' }); + await appointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const count = await page.locator('.dx-scheduler-appointment').count(); + expect(count).toBe(3); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts new file mode 100644 index 000000000000..4d1fad5ffcbf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const hasWarningCode = (message: string) => message.startsWith('W1023'); + +test.describe('Agenda:KeyField', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'agenda'].forEach((currentView) => { + test(`Warning should be thrown in console in case currentView='${currentView}'(T1100758)`, async ({ page }) => { + const consoleMessages: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') { + consoleMessages.push(msg.text()); + } + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week', 'agenda'], + currentView, + currentDate: new Date(2021, 2, 28), + height: 600, + }); + + const isWarningExist = consoleMessages.some(hasWarningCode); + expect(isWarningExist).toBeTruthy(); + }); + }); + + test('Wrong behavior: editing recurrence appointment does not affect to appointment data source(T1100758)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date('2021-03-29T16:30:00.000Z'), + endDate: new Date('2021-03-29T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + }], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 2, 28), + recurrenceEditMode: 'series', + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + await subjectInput.fill('Updated'); + + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); + + const updatedAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Updated' }); + await expect(updatedAppointment.first()).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts new file mode 100644 index 000000000000..9870b190a1fe --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts @@ -0,0 +1,67 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const data = [ + { text: 'Website Re-Design Plan', ownerId: [4, 1, 2], roomId: [1, 2, 3], priorityId: 2, startDate: new Date('2021-05-24T16:30:00.000Z'), endDate: new Date('2021-05-24T18:30:00.000Z'), recurrenceRule: 'FREQ=WEEKLY', allDay: true }, + { text: 'Book Flights to San Fran for Sales Trip', ownerId: 2, roomId: 2, priorityId: 1, startDate: new Date('2021-05-24T19:00:00.000Z'), endDate: new Date('2021-05-24T20:00:00.000Z'), allDay: true }, + { text: 'Final Budget Review', ownerId: 1, roomId: 1, priorityId: 1, startDate: new Date('2021-05-25T19:00:00.000Z'), endDate: new Date('2021-05-25T20:35:00.000Z') }, + { text: 'New Brochures', ownerId: 4, roomId: 3, priorityId: 2, startDate: new Date('2021-05-25T21:30:00.000Z'), endDate: new Date('2021-05-25T22:45:00.000Z') }, + { text: 'Install New Database', ownerId: 2, roomId: 3, priorityId: 1, startDate: new Date('2021-05-26T16:45:00.000Z'), endDate: new Date('2021-05-26T18:15:00.000Z') }, + { text: 'Approve New Online Marketing Strategy', ownerId: 4, roomId: 2, priorityId: 1, startDate: new Date('2021-05-26T19:00:00.000Z'), endDate: new Date('2021-05-26T21:00:00.000Z') }, + { text: 'Upgrade Personal Computers', ownerId: 2, roomId: 2, priorityId: 2, startDate: new Date('2021-05-26T22:15:00.000Z'), endDate: new Date('2021-05-26T23:30:00.000Z') }, +]; + +const resourcesData = [ + { fieldExpr: 'roomId', allowMultiple: true, dataSource: [{ text: 'Room 1', id: 1, color: '#00af2c' }, { text: 'Room 2', id: 2, color: '#56ca85' }, { text: 'Room 3', id: 3, color: '#8ecd3c' }], label: 'Room' }, + { fieldExpr: 'priorityId', allowMultiple: true, dataSource: [{ text: 'High priority', id: 1, color: '#cc5c53' }, { text: 'Low priority', id: 2, color: '#ff9747' }], label: 'Priority' }, + { fieldExpr: 'ownerId', allowMultiple: true, dataSource: [{ text: 'Samantha Bright', id: 1, color: '#727bd2' }, { text: 'John Heart', id: 2, color: '#32c9ed' }, { text: 'Todd Hoffman', id: 3, color: '#2a7ee4' }, { text: 'Sandra Johnson', id: 4, color: '#7b49d3' }], label: 'Owner' }, +]; + +const createScheduler = async (page, rtlEnabled: boolean, resources: any[] | undefined, groups: string[] | undefined): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 4, 25), + resources, + rtlEnabled, + groups, + height: 600, + }); +}; + +test.describe('Agenda:layout', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [false, true].forEach((rtlEnabled) => { + [undefined, resourcesData].forEach((resources) => { + test(`Agenda test layout(rtl=${rtlEnabled}, resources=${!!resources})`, async ({ page }) => { + await createScheduler(page, rtlEnabled, resources, undefined); + await testScreenshot(page, `agenda-layout-rtl=${rtlEnabled}-resources=${!!resources}.png`); + }); + }); + }); + + [false, true].forEach((rtlEnabled) => { + test(`Agenda test layout with groups(rtl=${rtlEnabled})`, async ({ page }) => { + await createScheduler(page, rtlEnabled, resourcesData, ['roomId']); + await testScreenshot(page, `agenda-layout-groups-rtl=${rtlEnabled}.png`); + }); + }); + + test('Agenda test appointment state', async ({ page }) => { + await createScheduler(page, false, resourcesData, undefined); + + const finalBudget = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Final Budget Review' }).first(); + await finalBudget.hover(); + await testScreenshot(page, 'agenda-layout-appointment-state-hover.png'); + + const newBrochures = page.locator('.dx-scheduler-appointment').filter({ hasText: 'New Brochures' }).first(); + await newBrochures.click(); + await testScreenshot(page, 'agenda-layout-appointment-state-click.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts new file mode 100644 index 000000000000..b533a545381e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts @@ -0,0 +1,32 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Agenda:view switching', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('View switching should work for empty agenda', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: new Date(2021, 4, 25, 0), + endDate: new Date(2021, 4, 25, 1), + text: 'Test Appointment', + }], + views: ['day', 'agenda'], + currentView: 'day', + currentDate: new Date(2021, 4, 25), + height: 600, + }); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.option('currentDate', new Date(2021, 4, 26)); + instance.option('currentView', 'agenda'); + }); + + await testScreenshot(page, 'switch-to-agenda-without-appointments.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts new file mode 100644 index 000000000000..0ce426801ea9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Agenda:Tooltip', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Tooltip date should be equal to date of current appointment(T1037028)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2021, 1, 1, 12), + endDate: new Date(2021, 1, 1, 13), + recurrenceRule: 'FREQ=HOURLY;COUNT=5', + }], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 1, 1), + height: 600, + }); + + const appointmentName = 'Text'; + + for (let index = 0; index < 5; index += 1) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.hideAppointmentTooltip(); + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(index); + await appointment.click(); + + const tooltipDate = await page.locator('.dx-tooltip-appointment-item-content-date').first().innerText(); + const appointmentTime = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + + expect(tooltipDate).toBe(appointmentTime); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/deleteRecurrence.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/deleteRecurrence.spec.ts new file mode 100644 index 000000000000..9a2b5b478dac --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/deleteRecurrence.spec.ts @@ -0,0 +1,115 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler API - deleteRecurrence', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('should delete recurrent appointment if mode is "series"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ type: 'day', intervalCount: 3 }], + currentView: 'day', + currentDate: new Date(2022, 3, 12), + startDayHour: 8, + endDayHour: 13, + onAppointmentDeleting: ((e: any) => { + e.component.deleteRecurrence(e.appointmentData, e.targetedAppointmentData.startDate, 'series'); + e.cancel = true; + }) as any, + dataSource: [{ + text: 'test-appt', + startDate: new Date(2022, 3, 12, 8), + endDate: new Date(2022, 3, 12, 9), + apptColor: 1, + recurrenceRule: 'FREQ=DAILY;COUNT=4', + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }); + await appointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(0); + }); + + test('should exclude from recurrence if mode is "occurrence"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ type: 'day', intervalCount: 3 }], + currentView: 'day', + currentDate: new Date(2022, 3, 12), + startDayHour: 8, + endDayHour: 12, + onAppointmentDeleting: ((e: any) => { + e.component.deleteRecurrence(e.appointmentData, e.targetedAppointmentData.startDate, 'occurrence'); + e.cancel = true; + }) as any, + dataSource: [{ + text: 'test-appt', + startDate: new Date(2022, 3, 12, 8), + endDate: new Date(2022, 3, 12, 9), + apptColor: 1, + recurrenceRule: 'FREQ=DAILY;COUNT=4', + }], + }); + + const appointment0 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }).first(); + await appointment0.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(2); + }); + + test('should show delete recurrence dialog if mode is "dialog"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ type: 'day', intervalCount: 3 }], + currentView: 'day', + currentDate: new Date(2022, 3, 12), + startDayHour: 8, + endDayHour: 13, + onAppointmentDeleting: ((e: any) => { + e.component.deleteRecurrence(e.appointmentData, e.targetedAppointmentData.startDate, 'dialog'); + e.cancel = true; + }) as any, + dataSource: [{ + text: 'test-appt', + startDate: new Date(2022, 3, 12, 8), + endDate: new Date(2022, 3, 12, 9), + apptColor: 1, + recurrenceRule: 'FREQ=DAILY;COUNT=4', + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }).first(); + await appointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await expect(deleteButton).toBeVisible(); + await deleteButton.click(); + + await page.waitForTimeout(100); + const count1 = await page.locator('.dx-scheduler-appointment').count(); + expect(count1).toBe(3); + + const dialogAppointmentBtn = page.locator('.dx-dialog').locator('.dx-dialog-button').first(); + await dialogAppointmentBtn.click(); + + await page.waitForTimeout(100); + const count2 = await page.locator('.dx-scheduler-appointment').count(); + expect(count2).toBe(2); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/resourceRequestCount.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/resourceRequestCount.spec.ts new file mode 100644 index 000000000000..2658f159a066 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/resourceRequestCount.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler API - request counting', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + // TODO: RequestLogger from TestCafe has no direct Playwright equivalent. + // These tests require network interception via page.route() to count requests. + // Skipping for now as they need API mocking infrastructure. + + test.skip('Request should be requested only once for color appointments (week)', async () => { + // Requires page.route() setup for resource API mock + }); + + test.skip('Request should be requested only once for color appointments (agenda)', async () => { + // Requires page.route() setup for resource API mock + }); + + test.skip('Request should be requested only once for grouping', async () => { + // Requires page.route() setup for resource API mock + }); + + test.skip('should be no requests for no grouping and appointments without color', async () => { + // Requires page.route() setup for resource API mock + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.functional.spec.ts new file mode 100644 index 000000000000..a064ac39f52c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.functional.spec.ts @@ -0,0 +1,163 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const openAppointmentPopup = async (page: any, appointment?: any, isRecurring = false) => { + await page.evaluate(({ appt, recurring, sel }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + instance.showAppointmentPopup(appt, !appt, recurring); + }, { appt: appointment, recurring: isRecurring, sel: SCHEDULER_SELECTOR }); + await page.locator('.dx-scheduler-appointment-popup').waitFor({ state: 'visible' }); +}; + +const clickRecurrenceSettingsButton = async (page: any) => { + await page.locator('.dx-recurrence-editor .dx-button').click(); +}; + +const roughEqualClientBoundingRect = ( + a: { width: number; height: number; top: number; left: number }, + b: { width: number; height: number; top: number; left: number }, +): boolean => ( + Math.abs(a.width - b.width) < 1 + && Math.abs(a.height - b.height) < 1 + && Math.abs(a.top - b.top) < 1 + && Math.abs(a.left - b.left) < 1 +); + +test.describe('Appointment Form: Functional', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Subject text editor should have focus after returning from recurrence form', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + await page.locator('.dx-recurrence-back-button').click(); + + const textInput = page.locator('.dx-scheduler-appointment-popup .dx-texteditor-input').first(); + await expect(textInput).toBeFocused(); + }); + + test('Recurrence start date editor should have focus after opening recurrence settings', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + const startDateInput = page.locator('.dx-recurrence-editor .dx-datebox .dx-texteditor-input').first(); + await expect(startDateInput).toBeFocused(); + }); + + test('Popup should not change dimensions when switching groups and recurrence group height is larger', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + items: [ + { + name: 'mainGroup', + items: ['repeatGroup'], + }, + 'recurrenceGroup', + ], + }, + }, + }); + + await openAppointmentPopup(page); + const contentElement = page.locator('.dx-popup-content'); + const boundingClientRect1 = await contentElement.boundingBox(); + + await page.evaluate((sel) => { + const instance = ($(sel) as any).dxScheduler('instance'); + const popup = instance.getAppointmentPopup(); + const form = popup.$content().find('.dx-form').dxForm('instance'); + const repeatEditor = form.getEditor('recurrenceRule'); + repeatEditor.option('value', 'FREQ=WEEKLY'); + }, SCHEDULER_SELECTOR); + + const boundingClientRect2 = await contentElement.boundingBox(); + + await page.locator('.dx-recurrence-back-button').click(); + const boundingClientRect3 = await contentElement.boundingBox(); + + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect2!)).toBe(true); + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect3!)).toBe(true); + }); + + test('Popup should not change dimensions when switching groups and main group height is larger', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + items: [ + 'mainGroup', + { + name: 'recurrenceGroup', + items: ['recurrenceStartDateGroup'], + }, + ], + }, + }, + }); + + await openAppointmentPopup(page); + const contentElement = page.locator('.dx-popup-content'); + const boundingClientRect1 = await contentElement.boundingBox(); + + await page.evaluate((sel) => { + const instance = ($(sel) as any).dxScheduler('instance'); + const popup = instance.getAppointmentPopup(); + const form = popup.$content().find('.dx-form').dxForm('instance'); + const repeatEditor = form.getEditor('recurrenceRule'); + repeatEditor.option('value', 'FREQ=WEEKLY'); + }, SCHEDULER_SELECTOR); + + const boundingClientRect2 = await contentElement.boundingBox(); + + await page.locator('.dx-recurrence-back-button').click(); + const boundingClientRect3 = await contentElement.boundingBox(); + + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect2!)).toBe(true); + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect3!)).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.visual.spec.ts new file mode 100644 index 000000000000..2334cd886042 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.visual.spec.ts @@ -0,0 +1,319 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const getResources = (withIcons = false) => ([ + { + fieldExpr: 'assigneeId', + allowMultiple: true, + label: 'Assignee', + dataSource: [ + { text: 'Samantha Bright', id: 1, color: '#727bd2' }, + { text: 'John Heart', id: 2, color: '#32c9ed' }, + { text: 'Todd Hoffman', id: 3, color: '#2a7ee4' }, + { text: 'Sandra Johnson', id: 4, color: '#7b49d3' }, + ], + icon: withIcons ? 'user' : undefined, + }, + { + fieldExpr: 'roomId', + label: 'Room', + dataSource: [ + { text: 'Room 1', id: 1, color: '#00af2c' }, + ], + icon: withIcons ? 'conferenceroomfilled' : undefined, + }, + { + fieldExpr: 'priorityId', + label: 'Priority', + dataSource: [ + { text: 'High', id: 1, color: '#cc5c53' }, + ], + icon: withIcons ? 'tags' : undefined, + }, +]); + +const openAppointmentPopup = async (page: any, appointment?: any, isRecurring = false) => { + await page.evaluate(({ appt, recurring, sel }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + instance.showAppointmentPopup(appt, !appt, recurring); + }, { appt: appointment, recurring: isRecurring, sel: SCHEDULER_SELECTOR }); + await page.locator('.dx-scheduler-appointment-popup').waitFor({ state: 'visible' }); +}; + +test.describe('Appointment Form: Main Form', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + { isRecurringAppointment: false, isAllDay: true }, + { isRecurringAppointment: false, isAllDay: false }, + { isRecurringAppointment: true, isAllDay: true }, + { isRecurringAppointment: true, isAllDay: false }, + ].forEach(({ isRecurringAppointment, isAllDay }) => { + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: isAllDay, + recurrenceRule: isRecurringAppointment ? 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10' : undefined, + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + }; + + test(`appointment main form (recurring=${isRecurringAppointment},allDay=${isAllDay})`, async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [appointment], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + await openAppointmentPopup(page, appointment, isRecurringAppointment); + + await testScreenshot( + page, + `scheduler__appointment__main-form (recurring=${isRecurringAppointment},allDay=${isAllDay}).png`, + { element: page.locator('.dx-popup-content') }, + ); + }); + + test(`appointment main form with resources and timezones (recurring=${isRecurringAppointment},allDay=${isAllDay})`, async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [appointment], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(), + editing: { + allowTimeZoneEditing: true, + }, + }); + + await openAppointmentPopup(page, appointment, isRecurringAppointment); + + await testScreenshot( + page, + `scheduler__appointment__main-form__with-resources-and-timezones (recurring=${isRecurringAppointment},allDay=${isAllDay}).png`, + { element: page.locator('.dx-popup-content') }, + ); + }); + }); + + test('main form with resources that have icons', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + }); + + await openAppointmentPopup(page, appointment, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__with-resources-with-icons.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('appointment form readonly state', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(), + editing: { + allowUpdating: false, + allowTimeZoneEditing: true, + }, + }); + + await openAppointmentPopup(page, appointment, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__readonly.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('main form on mobile screen', async ({ page }) => { + await page.setViewportSize({ width: 450, height: 1000 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + editing: { + form: { + iconsShowMode: 'both', + }, + }, + }); + + await openAppointmentPopup(page, undefined, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__mobile.png', + ); + }); + + test('appointment form resource with multiple selection', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + assigneeId: [1, 2, 3, 4], + roomId: 1, + priorityId: 1, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + editing: { + allowUpdating: true, + }, + }); + + await openAppointmentPopup(page, appointment, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__resource-with-multiple-selection.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('appointment main form with opened startDate calendar', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + await openAppointmentPopup(page, appointment, false); + + const startDateDropDown = page.locator('.dx-scheduler-appointment-popup .dx-first-row .dx-dropdowneditor-button'); + await startDateDropDown.first().click(); + + await page.locator('.dx-calendar').waitFor({ state: 'visible' }); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__startDate-calendar-opened.png', + ); + }); + + test('Recurrence settings button should have correct focus state', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + await openAppointmentPopup(page, appointment, true); + + await page.locator('.dx-recurrence-editor').click(); + await page.keyboard.press('Tab'); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-settings-button__focus-state.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('appointment form with labelMode=static', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + editing: { + allowUpdating: true, + form: { + labelMode: 'static', + }, + }, + }); + + await openAppointmentPopup(page, undefined, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__with-labelMode-static.png', + { element: page.locator('.dx-popup-content') }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/recurrence-form.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/recurrence-form.visual.spec.ts new file mode 100644 index 000000000000..f55096888d4e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/recurrence-form.visual.spec.ts @@ -0,0 +1,214 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const openAppointmentPopup = async (page: any, appointment?: any, isRecurring = false) => { + await page.evaluate(({ appt, recurring, sel }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + instance.showAppointmentPopup(appt, !appt, recurring); + }, { appt: appointment, recurring: isRecurring, sel: SCHEDULER_SELECTOR }); + await page.locator('.dx-scheduler-appointment-popup').waitFor({ state: 'visible' }); +}; + +const selectRepeatValue = async (page: any, frequency: string) => { + await page.evaluate(({ sel, freq }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + const popup = instance.getAppointmentPopup(); + const form = popup.$content().find('.dx-form').dxForm('instance'); + const repeatEditor = form.getEditor('recurrenceRule'); + const freqMap: Record = { + Hourly: 'FREQ=HOURLY', + Daily: 'FREQ=DAILY', + Weekly: 'FREQ=WEEKLY', + Monthly: 'FREQ=MONTHLY', + Yearly: 'FREQ=YEARLY', + }; + repeatEditor.option('value', freqMap[freq] || freq); + }, { sel: SCHEDULER_SELECTOR, freq: frequency }); +}; + +const clickRecurrenceSettingsButton = async (page: any) => { + await page.locator('.dx-recurrence-editor .dx-button').click(); +}; + +test.describe('Appointment Form: Recurrence Form', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['Hourly', 'Daily', 'Weekly', 'Monthly', 'Yearly'].forEach((frequency) => { + test(`recurrence form in ${frequency} frequency`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2024, 0, 1), + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2024-01-01T10:00:00'), + endDate: new Date('2024-01-01T11:00:00'), + }; + + await openAppointmentPopup(page, appointment, false); + await selectRepeatValue(page, frequency); + + await testScreenshot( + page, + `scheduler__appointment__recurrence-form__${frequency.toLowerCase()}.png`, + { element: page.locator('.dx-popup-content') }, + ); + }); + }); + + test('recurrence form with icons', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + iconsShowMode: 'both', + }, + }, + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=1', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__with-icons.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('recurrence form readonly state', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2024, 0, 1), + editing: { + allowUpdating: false, + }, + }); + + const appointment = { + text: 'Readonly Recurrent Appointment', + startDate: new Date('2024-01-01T10:00:00'), + endDate: new Date('2024-01-01T11:00:00'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, false); + await clickRecurrenceSettingsButton(page); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__readonly.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('recurrence form on mobile screen', async ({ page }) => { + await page.setViewportSize({ width: 450, height: 1000 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + iconsShowMode: 'both', + }, + }, + }); + + await openAppointmentPopup(page, undefined, false); + await selectRepeatValue(page, 'Weekly'); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__mobile.png', + ); + }); + + test('recurrence form with labelMode=static', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + allowUpdating: true, + popup: { + width: 420, + height: 500, + }, + form: { + iconsShowMode: 'both', + labelMode: 'static', + items: [ + 'mainGroup', + { + name: 'recurrenceGroup', + items: [ + 'recurrenceStartDateGroup', + 'recurrenceRuleGroup', + { + name: 'recurrenceEndGroup', + items: [ + 'recurrenceEndIcon', + { + name: 'recurrenceEndEditor', + label: { + visible: true, + location: 'top', + }, + }, + ], + }, + ], + }, + ], + }, + }, + }); + + const appointment = { + text: 'Readonly Recurrent Appointment', + startDate: new Date('2024-01-01T10:00:00'), + endDate: new Date('2024-01-01T11:00:00'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__with-labelMode-static.png', + { element: page.locator('.dx-popup-content') }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentOverlapping/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentOverlapping/basic.spec.ts new file mode 100644 index 000000000000..a2f4adf3f336 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentOverlapping/basic.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SIMPLE_DATA = [ + { + text: 'Appointment 1', + startDate: new Date(2017, 4, 24, 13, 0), + endDate: new Date(2017, 4, 25, 12, 30), + }, + { + text: 'Appointment 2', + startDate: new Date(2017, 4, 24, 15, 0), + endDate: new Date(2017, 4, 24, 16, 30), + }, + { + text: 'Appointment 3', + startDate: new Date(2017, 4, 25, 9, 0), + endDate: new Date(2017, 4, 25, 10, 30), + }, + { + text: 'Appointment 4', + startDate: new Date(2017, 4, 25, 11, 0), + endDate: new Date(2017, 4, 25, 12, 30), + }, + { + text: 'Appointment 5', + startDate: new Date(2017, 4, 25, 11, 0), + endDate: new Date(2017, 4, 25, 12, 0), + allDay: true, + }, +]; + +const ALL_DAY_DATA = [ + { + text: 'Appointment 1', + startDate: new Date(2017, 4, 21, 9, 0), + endDate: new Date(2017, 4, 24, 10, 30), + allDay: true, + }, + { + text: 'Appointment 2', + startDate: new Date(2017, 4, 22, 11, 0), + endDate: new Date(2017, 4, 22, 12, 0), + allDay: true, + }, + { + text: 'Appointment 3', + startDate: new Date(2017, 4, 25, 9, 0), + endDate: new Date(2017, 4, 25, 10, 30), + }, + { + text: 'Appointment 4', + startDate: new Date(2017, 4, 25, 11, 0), + endDate: new Date(2017, 4, 25, 12, 0), + allDay: true, + }, +]; + +const SCHEDULER_DEFAULT_OPTIONS = { + views: ['week'], + width: 940, + currentView: 'week', + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + height: 900, +}; + +test.describe('Appointment overlapping in Scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Multi-day appointment should not overlap other appointments when specific width is set, \'auto\' mode (T864456)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...SCHEDULER_DEFAULT_OPTIONS, + dataSource: SIMPLE_DATA, + }); + + const collectorsCount = await page.locator('.dx-scheduler-appointment-collector').count(); + expect(collectorsCount).toBe(3); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }).nth(1); + const box = await appointment.boundingBox(); + + expect(Math.round(box!.height)).toBe(266); + expect(Math.round(box!.width)).toBe(94); + }); + + test('Simple appointment should not overlap allDay appointment when specific width is set, \'auto\' mode (T864456)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...SCHEDULER_DEFAULT_OPTIONS, + dataSource: ALL_DAY_DATA, + }); + + const collectorsCount = await page.locator('.dx-scheduler-appointment-collector').count(); + expect(collectorsCount).toBe(1); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 4' }); + const box = await appointment.boundingBox(); + expect(box!.y).toBeCloseTo(138.828125, 0); + }); + + test('Crossing allDay appointments should not overlap each other (T893674)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...SCHEDULER_DEFAULT_OPTIONS, + dataSource: ALL_DAY_DATA, + }); + + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }); + const secondAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 2' }); + + const firstBox = await firstAppointment.boundingBox(); + const secondBox = await secondAppointment.boundingBox(); + + expect(firstBox!.y).not.toBe(secondBox!.y); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/T1017889.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/T1017889.spec.ts new file mode 100644 index 000000000000..b5fcaeb2791e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/T1017889.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Timeline Appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('all-day and ordinary appointments should overlap each other correctly in timeline views (T1017889)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Google AdWords Strategy', + startDate: new Date(2021, 1, 1, 10), + endDate: new Date(2021, 1, 1, 11), + allDay: true, + }, { + text: 'Brochure Design Review', + startDate: new Date(2021, 1, 1, 11, 30), + endDate: new Date(2021, 1, 1, 12, 30), + }], + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 1), + firstDayOfWeek: 1, + startDayHour: 10, + endDayHour: 20, + cellDuration: 60, + height: 580, + }); + + await testScreenshot(page, 'timeline-overlapping-appointments.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/adaptive.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/adaptive.spec.ts new file mode 100644 index 000000000000..d9b8320f4d36 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/adaptive.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); +const MOBILE_SIZE: [width: number, height: number] = [500, 700]; + +test.describe('Appointments with adaptive', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month'].forEach((view) => { + test(`should correctly render appointment collectors (view:${view})`, async ({ page }) => { + await page.setViewportSize({ width: MOBILE_SIZE[0], height: MOBILE_SIZE[1] }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2023-12-20T10:00:00', + endDate: '2024-01-20T12:00:00', + allDay: true, + text: 'all-day #0 (long)', + }, + { + startDate: '2023-12-31T10:00:00', + endDate: '2024-01-06T12:00:00', + allDay: true, + text: 'all-day #1 (week-long)', + }, + { + startDate: '2024-01-01T10:00:00', + endDate: '2024-01-05T12:00:00', + allDay: true, + text: 'all-day #2', + }, + { + startDate: '2024-01-02T10:00:00', + endDate: '2024-01-04T12:00:00', + allDay: true, + text: 'all-day #3', + }, + { + startDate: '2024-01-03T10:00:00', + endDate: '2024-01-03T12:00:00', + allDay: true, + text: 'all-day #4 (single-day)', + }, + { + startDate: '2024-12-30T10:00:00', + endDate: '2024-01-20T12:00:00', + text: 'usual #0 (long)', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T22:00:00', + text: 'usual #1 (day-long)', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T02:30:00', + text: 'usual #2 (short)', + }, + { + startDate: '2024-01-03T02:30:00', + endDate: '2024-01-03T22:00:00', + text: 'usual #3 (day-long)', + }, + ], + adaptivityEnabled: true, + currentView: 'week', + currentDate: '2024-01-01T00:00:00', + }); + + await testScreenshot(page, `adaptive_appts_view-${view}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + }); + + test('should correctly render long appointments with disabled allDayPanel ()', async ({ page }) => { + await page.setViewportSize({ width: MOBILE_SIZE[0], height: MOBILE_SIZE[1] }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2023-12-20T00:00:00', + endDate: '2024-01-20T02:00:00', + text: '#0 (long)', + }, + { + startDate: '2023-12-31T00:00:00', + endDate: '2024-01-06T02:00:00', + text: '#1 (week-long)', + }, + { + startDate: '2024-01-01T00:00:00', + endDate: '2024-01-05T02:00:00', + text: '#2', + }, + { + startDate: '2024-01-02T00:00:00', + endDate: '2024-01-04T02:00:00', + text: '#3', + }, + { + startDate: '2024-01-03T00:00:00', + endDate: '2024-01-03T02:00:00', + text: '#4 (single-day)', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T22:00:00', + text: '#5', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T02:30:00', + text: '#6', + }, + { + startDate: '2024-01-03T02:30:00', + endDate: '2024-01-03T22:00:00', + text: '#7', + }, + ], + adaptivityEnabled: true, + allDayPanelMode: 'hidden', + showAllDayPanel: false, + currentView: 'week', + currentDate: '2024-01-01T00:00:00', + }); + + await testScreenshot(page, 'adaptive_long-appts-without-all-day-panel_view-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDay.spec.ts new file mode 100644 index 000000000000..20a42365d633 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDay.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const data = [{ + text: '0', + startDate: new Date(2021, 3, 1), + endDate: new Date(2021, 3, 4), +}, { + text: '1', + startDate: new Date(2021, 3, 2), + endDate: new Date(2021, 3, 5, 0, 0, 1), +}, { + text: '2', + startDate: new Date(2021, 3, 2, 1), + endDate: new Date(2021, 3, 4, 23, 59), +}, { + text: '3 - Skip', + startDate: new Date(2021, 3, 3), + endDate: new Date(2021, 3, 4, 23, 59, 59), +}]; + +test.describe('Scheduler - All day appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should skip weekend days in workWeek', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: [{ + type: 'workWeek', + intervalCount: 2, + startDate: new Date(2021, 2, 4), + }], + maxAppointmentsPerCell: 'unlimited', + currentView: 'workWeek', + currentDate: new Date(2021, 3, 5), + height: 300, + }); + + await testScreenshot(page, 'workweek_all-day_appointments_skip_weekend.png'); + }); + + test('it should skip weekend days in timelineWorkWeek', async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-cell-sizes-horizontal { width: 4px; }'); + + await createWidget(page, 'dxScheduler', { + width: 970, + height: 300, + dataSource: data, + cellDuration: 60, + views: [{ + type: 'timelineWorkWeek', + intervalCount: 2, + }], + maxAppointmentsPerCell: 'unlimited', + currentView: 'timelineWorkWeek', + currentDate: new Date(2021, 3, 2), + }); + + await testScreenshot(page, 'timeline-work-week_all-day_appointments_skip_weekend.png'); + }); + + test('should work correctly for unsorted dataSource', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 3, + text: '3', + startDate: new Date('2020-11-23T00:00:00.000'), + endDate: new Date('2020-11-28T00:00:00.000'), + allDay: true, + }, { + id: 5, + text: '5', + startDate: new Date('2020-11-27T00:00:00.000'), + endDate: new Date('2020-11-27T00:00:00.000'), + allDay: true, + }, { + id: 1, + text: '1', + startDate: new Date('2020-11-25T22:20:00.000'), + endDate: new Date('2020-11-26T12:30:00.000'), + }], + views: ['week'], + currentView: 'week', + showAllDayPanel: true, + currentDate: new Date(2020, 10, 25), + height: 600, + }); + + await testScreenshot(page, 'allDay-unsorted-datasource.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDayEndsAtMidnight.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDayEndsAtMidnight.spec.ts new file mode 100644 index 000000000000..737889ce1aed --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDayEndsAtMidnight.spec.ts @@ -0,0 +1,110 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const VIEW_RANGE_HOURS = [ + [undefined, undefined], + [6, undefined], + [undefined, 18], + [6, 18], +]; + +const setViewOptions = (startDayHour: number | undefined, endDayHour: number | undefined) => { + const viewOptions: { startDayHour?: number; endDayHour?: number } = {}; + if (startDayHour) viewOptions.startDayHour = startDayHour; + if (endDayHour) viewOptions.endDayHour = endDayHour; + + return viewOptions; +}; + +test.describe('Scheduler - All day appointments ends at midnight', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month', 'timelineDay', 'timelineMonth'].forEach((view) => { + VIEW_RANGE_HOURS.forEach(([startDayHour, endDayHour]) => { + test( + `all-day appointment ends at midnight. view=${view}, startDayHour=${startDayHour}, endDayHour=${endDayHour} (T1128938)`, + async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'One day', + startDate: '2023-01-01T00:00:00', + endDate: '2023-01-01T00:00:00', + allDay: true, + }, + { + text: 'Two days', + startDate: '2023-01-01T00:00:00', + endDate: '2023-01-02T00:00:00', + allDay: true, + }, + ], + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss', + currentView: view, + currentDate: '2023-01-01T00:00:00', + height: 800, + cellDuration: 360, + maxAppointmentsPerCell: 2, + ...setViewOptions(startDayHour, endDayHour), + }); + + await testScreenshot( + page, + `midnight_all-day-appt_view=${view}_start=${startDayHour}_end=${endDayHour}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }, + ); + }); + }); + + [ + 'timelineDay', + 'timelineMonth', + ].forEach((view) => { + test(`all-day appointment ends at midnight of the next month. view=${view} (T1122382)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'Two days', + startDate: '2022-12-31T00:00:00', + endDate: '2023-01-01T00:00:00', + allDay: true, + }, + ], + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss', + currentView: view, + currentDate: '2022-12-31T00:00:00', + height: 800, + }); + + await page.evaluate((scrollDate) => { + ($('#container') as any).dxScheduler('scrollTo', new Date(scrollDate)); + }, '2022-12-31T23:59:00'); + + await testScreenshot( + page, + `midnight-next-month_all-day-appt_view=${view}_first.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(100); + + await page.evaluate((scrollDate) => { + ($('#container') as any).dxScheduler('scrollTo', new Date(scrollDate)); + }, '2023-01-01T00:01:00'); + + await testScreenshot( + page, + `midnight-next-month_all-day-appt_view=${view}_second.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/appointment_collector.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/appointment_collector.spec.ts new file mode 100644 index 000000000000..a110a9e9ba66 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/appointment_collector.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [{ + text: 'appointment1', + startDate: new Date('2021-04-02T07:30:00.000Z'), + endDate: new Date('2021-04-02T09:00:00.000Z'), +}, { + text: 'appointment2', + startDate: new Date('2021-04-02T07:35:00.000Z'), + endDate: new Date('2021-04-02T09:05:00.000Z'), +}]; +const config = { + dataSource, + timeZone: 'America/Los_Angeles', + currentDate: new Date(2021, 3, 2), + maxAppointmentsPerCell: 1, +}; + +test.describe('Appointment Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week', 'month', 'timelineDay', 'timelineWeek', 'timelineMonth'].forEach((view) => { + test(`appointmentCollectorTemplate should render with appointments data on ${view} view`, async ({ page }) => { + await page.evaluate(({ cfg, ds, viewName }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + ...cfg, + dataSource: ds, + views: [viewName], + currentView: viewName, + appointmentCollectorTemplate(data: any) { + (window as any).appointmentCollectorArgsData = data; + return document.createElement('div'); + }, + }); + devExpress.fx.off = true; + }, { cfg: config, ds: dataSource, viewName: view }); + + const renderedData = await page.evaluate(() => (window as any).appointmentCollectorArgsData); + + expect(renderedData).toEqual({ + appointmentCount: 1, + isCompact: ['day', 'week'].includes(view), + items: [dataSource[1]], + }); + }); + + test(`appointmentCollectorTemplate in view config should render with appointments data on ${view} view`, async ({ page }) => { + await page.evaluate(({ cfg, ds, viewName }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + ...cfg, + dataSource: ds, + views: [{ + type: viewName, + appointmentCollectorTemplate(data: any) { + (window as any).appointmentCollectorArgsData = data; + return document.createElement('div'); + }, + }], + currentView: viewName, + }); + devExpress.fx.off = true; + }, { cfg: config, ds: dataSource, viewName: view }); + + const renderedData = await page.evaluate(() => (window as any).appointmentCollectorArgsData); + + expect(renderedData).toEqual({ + appointmentCount: 1, + isCompact: ['day', 'week'].includes(view), + items: [dataSource[1]], + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/dependendOptions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/dependendOptions.spec.ts new file mode 100644 index 000000000000..e8c2cbe88a5e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/dependendOptions.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment dependend options', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('cellDuration (T1076138)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test-appt', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11, 20), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + endDayHour: 18, + width: 600, + height: 600, + cellDuration: 20, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'cellDuration', 30); + }); + + const clientHeight = await appointment.evaluate((el) => el.clientHeight); + expect(clientHeight).toBeGreaterThanOrEqual(132); + expect(clientHeight).toBeLessThanOrEqual(133); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/displayArguments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/displayArguments.spec.ts new file mode 100644 index 000000000000..e432f175135e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/displayArguments.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Display* arguments in appointment templates and events', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [undefined, 'America/Los_Angeles'].forEach((timeZone) => { + test(`displayStartDate and displayEndDate arguments should be right with timeZone='${timeZone}'`, async ({ page }) => { + await page.evaluate(({ tz }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + timeZone: tz, + dataSource: [], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 1, 15), + startDayHour: 9, + height: 600, + + onAppointmentClick(model: any) { + const { displayStartDate, displayEndDate } = model.targetedAppointmentData; + (window as any).testDisplayValue = `${displayStartDate.toLocaleTimeString('en-US', { hour12: false })} ${displayEndDate.toLocaleTimeString('en-US', { hour12: false })}`; + }, + + appointmentTooltipTemplate(model: any) { + const { displayStartDate, displayEndDate } = model.targetedAppointmentData; + return `${displayStartDate.toLocaleTimeString('en-US', { hour12: false })} ${displayEndDate.toLocaleTimeString('en-US', { hour12: false })}`; + }, + + appointmentTemplate(model: any) { + const { displayStartDate, displayEndDate } = model.targetedAppointmentData; + return `${displayStartDate.toLocaleTimeString('en-US', { hour12: false })} ${displayEndDate.toLocaleTimeString('en-US', { hour12: false })}`; + }, + }); + devExpress.fx.off = true; + }, { tz: timeZone }); + + const etalon = '09:30:00 10:00:00'; + + const cell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + const textEditor = page.locator('.dx-scheduler-appointment-popup .dx-textbox input'); + await textEditor.fill('text'); + await page.locator('.dx-popup-done').click(); + + const appointmentText = await page.locator('.dx-scheduler-appointment').nth(0).innerText(); + expect(appointmentText).toBe(etalon); + + await page.locator('.dx-scheduler-appointment').nth(0).click(); + const tooltipText = await page.locator('.dx-scheduler-appointment-tooltip-wrapper .dx-list-item').nth(0).innerText(); + expect(tooltipText).toBe(etalon); + + const testDisplayValue = await page.evaluate(() => (window as any).testDisplayValue); + expect(testDisplayValue).toBe(etalon); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/legacyEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/legacyEditing.spec.ts new file mode 100644 index 000000000000..da1c5b6a8df4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/legacyEditing.spec.ts @@ -0,0 +1,119 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SCHEDULER_SELECTOR = '#container'; +const INITIAL_APPOINTMENT_TITLE = 'appointment'; +const ADDITIONAL_TITLE_TEXT = '-updated'; +const UPDATED_APPOINTMENT_TITLE = `${INITIAL_APPOINTMENT_TITLE}${ADDITIONAL_TITLE_TEXT}`; + +test.describe('Appointment Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly update appointment if dataSource is a simple array', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + const subjectInput = page.locator('.dx-popup-wrapper .dx-textbox input').first(); + await subjectInput.click(); + await subjectInput.fill(UPDATED_APPOINTMENT_TITLE); + + const inputValue = await subjectInput.inputValue(); + expect(inputValue).toBe(UPDATED_APPOINTMENT_TITLE); + + await page.locator('.dx-popup-done').click(); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: UPDATED_APPOINTMENT_TITLE })).toBeVisible(); + }); + + test('Should correctly update appointment if dataSource is a Store with key array', async ({ page }) => { + await page.evaluate(({ selector, title }) => { + const $scheduler = ($(selector) as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + dataSource: new devExpress.data.DataSource({ + store: { + type: 'array', + key: 'id', + data: [{ + id: 1, + text: title, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + }, + }), + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + editing: { legacyForm: true }, + }).dxScheduler('instance'); + devExpress.fx.off = true; + }, { selector: SCHEDULER_SELECTOR, title: INITIAL_APPOINTMENT_TITLE }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + const subjectInput = page.locator('.dx-popup-wrapper .dx-textbox input').first(); + await subjectInput.click(); + await subjectInput.fill(UPDATED_APPOINTMENT_TITLE); + + const inputValue = await subjectInput.inputValue(); + expect(inputValue).toBe(UPDATED_APPOINTMENT_TITLE); + + await page.locator('.dx-popup-done').click(); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: UPDATED_APPOINTMENT_TITLE })).toBeVisible(); + }); + + test('Appointment EditForm screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + editing: { legacyForm: true }, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + await testScreenshot(page, 'appointment-popup-screenshot.png', { + element: appointment, + }); + + await expect(page.locator('.dx-popup-wrapper')).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/allDay.spec.ts new file mode 100644 index 000000000000..f21ac69442c7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/allDay.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: All day', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 1, 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`All day appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_26', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + allDayPanelMode: 'allDay', + }); + + await testScreenshot( + page, + `all-day-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-all-day-appointments') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/day.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/day.spec.ts new file mode 100644 index 000000000000..1a7f84402e6d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/day.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Day', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`Day appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_1', + startDate: new Date(2021, 3, 27, 9), + endDate: new Date(2021, 3, 27, 9, 30), + }, { + text: 'test_2', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_3', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_4', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_5', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_6', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_7', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_8', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_9', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_10', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11), + }, { + text: 'test_1', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_12', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_13', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_14', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_15', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11, 30), + }, { + text: 'test_16', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 12, 30), + }, { + text: 'test_17', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 14), + }, { + text: 'test_18', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 13, 30), + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + height: 700, + width: 500, + }); + + await testScreenshot( + page, + `day-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/month.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/month.spec.ts new file mode 100644 index 000000000000..8bd02e9e6025 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/month.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Month', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 1, 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`Month appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_26', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['month'], + currentView: 'month', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 700, + }); + + await testScreenshot( + page, + `month-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/timeline.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/timeline.spec.ts new file mode 100644 index 000000000000..7cb57d4de91e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/timeline.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Timeline', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 1, 3, 10, 20].forEach((maxAppointmentsPerCellValue) => { + test(`Timeline appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_1', + startDate: new Date(2021, 3, 27, 9), + endDate: new Date(2021, 3, 27, 9, 30), + }, { + text: 'test_2', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_3', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_4', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_5', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_6', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_7', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_8', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_9', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_10', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11), + }, { + text: 'test_1', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_12', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_13', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_14', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_15', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11, 30), + }, { + text: 'test_16', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 12, 30), + }, { + text: 'test_17', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 14), + }, { + text: 'test_18', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 13, 30), + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + height: 700, + }); + + await testScreenshot( + page, + `timeline-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/week.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/week.spec.ts new file mode 100644 index 000000000000..8fcea5b4d2db --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/week.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Week', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`Week appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_1', + startDate: new Date(2021, 3, 27, 9), + endDate: new Date(2021, 3, 27, 9, 30), + }, { + text: 'test_2', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_3', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_4', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_5', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_6', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_7', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_8', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_9', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_10', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11), + }, { + text: 'test_1', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_12', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_13', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_14', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_15', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11, 30), + }, { + text: 'test_16', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 12, 30), + }, { + text: 'test_17', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 14), + }, { + text: 'test_18', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 13, 30), + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 700, + }); + + await testScreenshot( + page, + `week-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday.spec.ts new file mode 100644 index 000000000000..cd9f5c8ed323 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday.spec.ts @@ -0,0 +1,297 @@ +import { test, expect } from '@playwright/test'; +import type { Page, Locator } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const getAppointment = (page: Page, title: string, index = 0): Locator => page.locator('.dx-scheduler-appointment').filter({ hasText: title }).nth(index); + +const checkAllDayAppointment = async ( + page: Page, + title: string, + index: number, + reduceType: 'head' | 'body' | 'tail' | undefined, + width: number, +): Promise => { + const appointment = getAppointment(page, title, index); + const isReduced = reduceType !== undefined; + + const hasReducedIcon = await appointment.locator('.dx-scheduler-appointment-reduced-icon').count(); + expect(hasReducedIcon > 0).toBe(isReduced); + + const isHead = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-head')); + expect(isHead).toBe(reduceType === 'head'); + + const isBody = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-body')); + expect(isBody).toBe(reduceType === 'body'); + + const isTail = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-tail')); + expect(isTail).toBe(reduceType === 'tail'); + + const isAllDay = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-all-day-appointment')); + expect(isAllDay).toBeTruthy(); + + const clientWidth = await appointment.evaluate((el) => el.clientWidth); + expect(clientWidth).toBeGreaterThanOrEqual(width - 1); + expect(clientWidth).toBeLessThanOrEqual(width + 1); +}; + +const checkRegularAppointment = async ( + page: Page, + title: string, + index: number, + reduceType: 'head' | 'body' | 'tail' | undefined, + height: number, +): Promise => { + const appointment = getAppointment(page, title, index); + const isReduced = reduceType !== undefined; + + const hasReducedIcon = await appointment.locator('.dx-scheduler-appointment-reduced-icon').count(); + expect(hasReducedIcon > 0).toBe(isReduced); + + const isHead = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-head')); + expect(isHead).toBe(reduceType === 'head'); + + const isBody = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-body')); + expect(isBody).toBe(reduceType === 'body'); + + const isTail = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-tail')); + expect(isTail).toBe(reduceType === 'tail'); + + const clientHeight = await appointment.evaluate((el) => el.clientHeight); + expect(clientHeight).toBeGreaterThanOrEqual(height - 1); + expect(clientHeight).toBeLessThanOrEqual(height + 1); +}; + +test.describe('Scheduler - Multiday appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should render multi-day and multi-view appointments correctly if allDayPanelMode is "hidden"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'appt-00', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 22, 10, 30), + }, { + text: 'appt-01', + startDate: new Date(2021, 2, 25, 9), + endDate: new Date(2021, 3, 6, 8, 30), + }], + views: ['week', 'month', 'timelineMonth'], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + allDayPanelMode: 'hidden', + }); + + let appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(4); + + await checkRegularAppointment(page, 'appt-00', 0, undefined, 200); + await checkRegularAppointment(page, 'appt-01', 0, 'head', 100); + for (let i = 1; i < appointmentCount - 2; i += 1) { + await checkRegularAppointment(page, 'appt-01', i, 'body', 200); + } + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(7); + + for (let i = 0; i < appointmentCount; i += 1) { + await checkRegularAppointment(page, 'appt-01', i, 'body', 200); + } + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(3); + await checkRegularAppointment(page, 'appt-01', 0, 'body', 200); + await checkRegularAppointment(page, 'appt-01', 1, 'body', 200); + await checkRegularAppointment(page, 'appt-01', 2, 'tail', 50); + }); + + test('it should render all-day appointments if allDayPanelMode is "all"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'appt-00', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 22, 10, 30), + allDay: true, + }, { + text: 'appt-01', + startDate: new Date(2021, 2, 25, 9), + endDate: new Date(2021, 3, 6, 8, 30), + }], + views: ['week', 'month', 'timelineMonth'], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + allDayPanelMode: 'all', + }); + + let appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(2); + await checkAllDayAppointment(page, 'appt-00', 0, undefined, 109); + await checkAllDayAppointment(page, 'appt-01', 0, 'head', 337); + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + await checkAllDayAppointment(page, 'appt-01', 0, 'body', 793); + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + await checkAllDayAppointment(page, 'appt-01', 0, 'tail', 337); + }); + + test('it should render all-day and multi-day appointments if allDayPanelMode is "allDay"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'allDay', + startDate: new Date(2021, 2, 22), + allDay: true, + }, { + text: 'multiDay', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 25, 9, 30), + }], + views: ['week', 'month', 'timelineMonth'], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + allDayPanelMode: 'allDay', + }); + + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + + await checkAllDayAppointment(page, 'allDay', 0, undefined, 117); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 151); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 151); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 151); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + }); + + test('it should correctly change allDayPanelOption at runtime', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + dataSource: [ + { + text: 'allDay', + startDate: new Date(2021, 2, 22), + allDay: true, + }, + { + text: 'multiDay', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 25, 9, 30), + }], + views: ['week', 'workWeek'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + maxAppointmentsPerCell: 2, + startDayHour: 8, + endDayHour: 12, + }); + + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(2); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkAllDayAppointment(page, 'multiDay', 0, undefined, 417); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'allDay'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 303); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'hidden'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + await expect(page.locator('.dx-scheduler-all-day-table-cell')).toHaveCount(0); + await checkRegularAppointment(page, 'allDay', 0, undefined, 303); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 303); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'allDay'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 303); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'all'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(2); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkAllDayAppointment(page, 'multiDay', 0, undefined, 417); + }); + + test('it should correctly handle allDayPanelMode for the workspace', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'allDay', + startDate: new Date(2021, 2, 22), + allDay: true, + }, { + text: 'multiDay', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 25, 9, 30), + }], + views: [ + 'week', + { + type: 'week', + name: 'weekAllDay', + allDayPanelMode: 'allDay', + }, + ], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + }); + + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(2); + + await checkAllDayAppointment(page, 'allDay', 0, undefined, 109); + await checkAllDayAppointment(page, 'multiDay', 0, undefined, 451); + + await page.locator('.dx-tabs-item').filter({ hasText: 'weekAllDay' }).click(); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + + await checkAllDayAppointment(page, 'allDay', 0, undefined, 109); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 200); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 200); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 200); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 150); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday_screenshot.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday_screenshot.spec.ts new file mode 100644 index 000000000000..1a471c03c3c0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday_screenshot.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler - Multiday appointments (screenshot)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + 'week', + 'month', + 'timelineMonth', + ].forEach((currentView) => { + test(`it should not cut multiday appointment in ${currentView} view`, async ({ page }) => { + await createWidget( + page, + 'dxScheduler', + { + width: 900, + height: 400, + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 28, 8), + endDate: new Date(2021, 3, 4, 8), + }], + views: ['week', 'month', 'timelineMonth'], + currentView, + currentDate: new Date(2021, 3, 4), + startDayHour: 12, + }, + ); + + await testScreenshot(page, `multiday-appointment_${currentView}.png`, { + element: page.locator('#container'), + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/onAppointmentDeleting.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/onAppointmentDeleting.spec.ts new file mode 100644 index 000000000000..289a145bd0e5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/onAppointmentDeleting.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const data = [ + { + text: 'Brochure Design Review', + startDate: new Date(2021, 3, 27, 1, 30), + endDate: new Date(2021, 3, 27, 2, 30), + }, +]; + +test.describe('onAppointmentDeleting event', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [{ + cancel: false, + expectedCount: 0, + }, { + cancel: true, + expectedCount: 1, + }].forEach(({ cancel, expectedCount }) => { + test(`UI behaviour should be valid in case argument pass boolean value, e.cancel=${cancel}`, async ({ page }) => { + await page.evaluate(({ appointmentData, cancelValue }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + dataSource: appointmentData, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 1, + endDayHour: 7, + height: 600, + cellDuration: 30, + onAppointmentDeleting(e: any) { + e.cancel = cancelValue; + }, + }); + devExpress.fx.off = true; + }, { appointmentData: data, cancelValue: cancel }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await page.locator('.dx-scheduler-appointment-tooltip-wrapper .dx-tooltip-appointment-item-delete-button').click(); + + await expect(page.locator('.dx-scheduler-appointment')).toHaveCount(expectedCount); + }); + + test(`UI behaviour should be valid in case argument pass Promise resolved, e.cancel=${cancel}`, async ({ page }) => { + await page.evaluate(({ appointmentData, cancelValue }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + dataSource: appointmentData, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 1, + endDayHour: 7, + height: 600, + cellDuration: 30, + onAppointmentDeleting(e: any) { + e.cancel = new Promise((resolve) => { + resolve(cancelValue); + }); + }, + }); + devExpress.fx.off = true; + }, { appointmentData: data, cancelValue: cancel }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await page.locator('.dx-scheduler-appointment-tooltip-wrapper .dx-tooltip-appointment-item-delete-button').click(); + + await expect(page.locator('.dx-scheduler-appointment')).toHaveCount(expectedCount); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/resources.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/resources.spec.ts new file mode 100644 index 000000000000..382409f411fc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/resources.spec.ts @@ -0,0 +1,207 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [{ + text: 'test-appt-1', + priorityId: 1, + typeId: 2, + startDate: new Date('2021-05-26T06:45:00.000Z'), + endDate: new Date('2021-05-26T09:15:00.000Z'), +}, { + text: 'test-appt-2', + priorityId: 2, + typeId: 1, + startDate: new Date('2021-05-26T06:45:00.000Z'), + endDate: new Date('2021-05-26T09:15:00.000Z'), +}]; + +const priorityData = [{ + text: 'Low Priority', + id: 1, + color: 'rgb(252, 182, 94)', +}, { + text: 'High Priority', + id: 2, + color: 'rgb(225, 142, 146)', +}]; + +test.describe('Appointment resources', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Resource color should be correct if group is set in "views"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + dataSource, + views: [{ + type: 'workWeek', + startDayHour: 9, + endDayHour: 18, + groups: ['priorityId'], + }], + currentView: 'workWeek', + currentDate: new Date(2021, 4, 25), + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: priorityData, + label: 'Priority', + }, { + fieldExpr: 'typeId', + allowMultiple: false, + dataSource: [{ + id: 1, + color: '#b6d623', + }, { + id: 2, + color: '#679ec5', + }], + }], + }); + + const appointment1 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt-1' }); + const appointment2 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt-2' }); + + const color1 = await appointment1.evaluate((el) => getComputedStyle(el).backgroundColor); + const color2 = await appointment2.evaluate((el) => getComputedStyle(el).backgroundColor); + + expect(color1).toBe(priorityData[0].color); + expect(color2).toBe(priorityData[1].color); + }); + + test('Scheduler should renders correctly if resource dataSource is not set', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + width: 800, + dataSource: [{ + text: 'Appt-1', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 12), + }, { + text: 'Appt-2', + startDate: new Date(2021, 3, 29, 11), + endDate: new Date(2021, 3, 29, 13), + }], + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + endDayHour: 14, + resources: [{ + fieldExpr: 'roomId', + label: 'Room', + }], + }); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appt-1' })).toBeVisible(); + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appt-2' })).toBeVisible(); + }); + + test('Resource with allowMultiple should be set correctly for new the appointment (T1075028)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + endDayHour: 14, + resources: [{ + fieldExpr: 'test_Id', + allowMultiple: true, + dataSource: [{ + text: 'Test-0', + id: 1, + }, { + text: 'Test-1', + id: 2, + }], + label: 'MultipleResource', + }], + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + await expect(page.locator('.dx-scheduler-appointment-popup')).toBeVisible(); + + const resourceTagBox = page.locator('.dx-tagbox'); + await expect(resourceTagBox).toBeVisible(); + await resourceTagBox.click(); + + const tagBoxPopup = page.locator('.dx-tagbox-popup-wrapper'); + await expect(tagBoxPopup).toBeVisible(); + + await tagBoxPopup.locator('.dx-list-item').first().click(); + + const tags = page.locator('.dx-tagbox .dx-tag'); + await expect(tags).toHaveCount(1); + }); + + test('Resource color should be correct for the complex resource id without grouping', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 6, 10), + views: ['week'], + currentView: 'week', + editing: true, + dataSource: [{ + text: 'a', + allDay: true, + startDate: new Date(2015, 6, 10, 0), + endDate: new Date(2015, 6, 10, 0, 30), + ownerId: { _value: 'guid-1' }, + }, { + text: 'b', + allDay: true, + startDate: new Date(2015, 6, 10, 0), + endDate: new Date(2015, 6, 10, 0, 30), + ownerId: { _value: 'guid-2' }, + }, { + text: 'c', + startDate: new Date(2015, 6, 10, 2), + endDate: new Date(2015, 6, 10, 2, 30), + ownerId: { _value: 'guid-3' }, + }], + resources: [ + { + field: 'ownerId', + dataSource: [ + { + id: { _value: 'guid-1' }, + text: 'one', + color: 'rgb(255, 0, 0)', + }, + { + id: { _value: 'guid-2' }, + text: 'two', + color: 'rgb(0, 128, 0)', + }, + { + id: { _value: 'guid-3' }, + text: 'three', + color: 'rgb(255, 255, 0)', + }, + ], + }, + ], + scrolling: { + orientation: 'vertical', + }, + height: 600, + }); + + const appointmentA = page.locator('.dx-scheduler-appointment').filter({ hasText: 'a' }); + const appointmentB = page.locator('.dx-scheduler-appointment').filter({ hasText: 'b' }); + const appointmentC = page.locator('.dx-scheduler-appointment').filter({ hasText: 'c' }); + + const colorA = await appointmentA.evaluate((el) => getComputedStyle(el).backgroundColor); + const colorB = await appointmentB.evaluate((el) => getComputedStyle(el).backgroundColor); + const colorC = await appointmentC.evaluate((el) => getComputedStyle(el).backgroundColor); + + expect(colorA).toBe('rgb(255, 0, 0)'); + expect(colorB).toBe('rgb(0, 128, 0)'); + expect(colorC).toBe('rgb(255, 255, 0)'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineMonth.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineMonth.spec.ts new file mode 100644 index 000000000000..59e8d0e5e6cf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineMonth.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointments in TimelineMonth', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Appointments should have correct order', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2016, 1, 2), + dataSource: [ + { + text: 'appt-01', + startDate: new Date(2016, 1, 1, 9, 0), + endDate: new Date(2016, 1, 1, 10, 30), + }, { + text: 'appt-02', + startDate: new Date(2016, 1, 1, 11, 30), + endDate: new Date(2016, 1, 1, 14, 15), + }, { + text: 'appt-03', + startDate: new Date(2016, 1, 1, 15, 15), + endDate: new Date(2016, 1, 1, 17, 15), + }, { + text: 'appt-04', + startDate: new Date(2016, 1, 1, 18, 45), + endDate: new Date(2016, 1, 1, 20, 15), + }, { + text: 'appt-05', + startDate: new Date(2016, 1, 2, 8, 15), + endDate: new Date(2016, 1, 2, 10, 45), + }, { + text: 'appt-06', + startDate: new Date(2016, 1, 2, 12, 0), + endDate: new Date(2016, 1, 2, 13, 45), + }, { + text: 'appt-07', + startDate: new Date(2016, 1, 2, 15, 30), + endDate: new Date(2016, 1, 2, 17, 30), + }, { + text: 'appt-08', + startDate: new Date(2016, 1, 3, 8, 15), + endDate: new Date(2016, 1, 3, 9, 0), + }, { + text: 'appt-09', + startDate: new Date(2016, 1, 3, 10, 0), + endDate: new Date(2016, 1, 3, 11, 15), + }, { + text: 'appt-10', + startDate: new Date(2016, 1, 3, 11, 45), + endDate: new Date(2016, 1, 3, 13, 45), + }, { + text: 'appt-11', + startDate: new Date(2016, 1, 3, 14, 0), + endDate: new Date(2016, 1, 3, 16, 45), + }, + ], + views: ['timelineMonth'], + currentView: 'timelineMonth', + maxAppointmentsPerCell: 'unlimited', + height: 505, + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + firstDayOfWeek: 0, + width: 800, + }); + + await testScreenshot(page, 'timelineMonth-appt-order.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineWorkWeek.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineWorkWeek.spec.ts new file mode 100644 index 000000000000..596872f8c1e9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineWorkWeek.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const CELL_WIDTH = 200; + +test.describe('Appointments in TimelineWorkWeek', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Appointments should have correct width', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2024, 0, 29), + dataSource: [ + { + text: 'appt-01', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-02', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 4, 14, 0), + }, + { + text: 'appt-03', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 6, 10, 0), + }, + { + text: 'appt-04', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 6, 18, 0), + }, + { + text: 'appt-05', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 3, 10, 0), + }, + { + text: 'appt-06', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 3, 18, 0), + }, + { + text: 'appt-07', + startDate: new Date(2024, 1, 4, 13, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-08', + startDate: new Date(2024, 1, 1, 10, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-09', + startDate: new Date(2024, 1, 1, 19, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-10', + startDate: new Date(2024, 1, 4, 10, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-11', + startDate: new Date(2024, 1, 4, 17, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + ], + views: [{ + type: 'timelineWorkWeek', + intervalCount: 2, + maxAppointmentsPerCell: 'unlimited', + }], + currentView: 'timelineWorkWeek', + startDayHour: 12, + endDayHour: 16, + cellDuration: 60, + }); + + const getApptWidth = (title: string) => + page.locator('.dx-scheduler-appointment').filter({ hasText: title }).evaluate( + (el) => el.style.width, + ); + + expect(await getApptWidth('appt-01')).toBe(`${CELL_WIDTH * (3 + 4 * 2 + 2)}px`); + expect(await getApptWidth('appt-02')).toBe(`${CELL_WIDTH * (3 + 4)}px`); + expect(await getApptWidth('appt-03')).toBe(`${CELL_WIDTH * (3 + 4 * 2)}px`); + expect(await getApptWidth('appt-04')).toBe(`${CELL_WIDTH * (3 + 4 * 3)}px`); + expect(await getApptWidth('appt-05')).toBe(`${CELL_WIDTH * (3 + 4)}px`); + expect(await getApptWidth('appt-06')).toBe(`${CELL_WIDTH * (3 + 4)}px`); + expect(await getApptWidth('appt-07')).toBe(`${CELL_WIDTH * (4 + 2)}px`); + expect(await getApptWidth('appt-08')).toBe(`${CELL_WIDTH * (4 * 3 + 2)}px`); + expect(await getApptWidth('appt-09')).toBe(`${CELL_WIDTH * (4 * 2 + 2)}px`); + expect(await getApptWidth('appt-10')).toBe(`${CELL_WIDTH * (4 + 2)}px`); + expect(await getApptWidth('appt-11')).toBe(`${CELL_WIDTH * (4 + 2)}px`); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/workWeek/interval.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/workWeek/interval.spec.ts new file mode 100644 index 000000000000..805f839d4f9a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/workWeek/interval.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Appointments with adaptive', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly render scheduler in workWeek view with interval, skipping weekends (T1243027)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2024-01-05T01:00:00', + endDate: '2024-01-07T01:00:00', + text: 'Ends in weekend', + color: 'red', + }, + { + startDate: '2024-01-07T01:00:00', + endDate: '2024-01-08T01:00:00', + text: 'Starts in weekend', + color: 'blue', + }, + { + startDate: '2024-01-05T01:00:00', + endDate: '2024-01-08T01:00:00', + text: 'Goes over weekend', + color: 'green', + }, + ], + views: [{ + name: 'myView', + type: 'workWeek', + allDayPanelMode: 'allDay', + intervalCount: 2, + maxAppointmentsPerCell: 'unlimited', + }], + currentView: 'myView', + currentDate: '2024-01-01', + height: 600, + resources: [{ + fieldExpr: 'color', + dataSource: [ + { id: 'red', color: 'red' }, + { id: 'blue', color: 'blue' }, + { id: 'green', color: 'green' }, + ], + label: 'Room', + }], + }); + + await testScreenshot(page, 'work_week_interval-2.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/bothDirectionsVirtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/bothDirectionsVirtualScrolling.spec.ts new file mode 100644 index 000000000000..9c8fd7243b6c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/bothDirectionsVirtualScrolling.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SELECTED_CELL_CLASS = 'dx-state-focused'; + +const createSchedulerWidget = async (page: any, options = {}) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2020, 8, 20), + cellDuration: 60, + height: 300, + width: 400, + scrolling: { mode: 'virtual' }, + resources: [{ + fieldExpr: 'resourceId0', + dataSource: [{ id: 0 }, { id: 1 }], + }], + ...options, + }); +}; + +const scrollTo = async (page: any, x: number, y: number) => { + await page.evaluate(({ scrollX, scrollY }: { scrollX: number; scrollY: number }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: scrollY, x: scrollX }); + }, { scrollX: x, scrollY: y }); + await page.waitForTimeout(300); +}; + +const baseConfig = { + scrolling: { mode: 'virtual', orientation: 'both' }, + views: [{ type: 'week', intervalCount: 3 }], + currentView: 'week', +}; + +test.describe('Scheduler: Cells Selection in Both Directions Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Selected cells shouldn\'t disappear on scroll', async ({ page }) => { + await createSchedulerWidget(page, { ...baseConfig }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 1000, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBe(0); + + await scrollTo(page, 0, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); + + test('Selection should work in month view', async ({ page }) => { + await createSchedulerWidget(page, { + ...baseConfig, + views: [{ type: 'month', groupOrientation: 'horizontal' }], + currentView: 'month', + groups: ['resourceId0'], + resources: [{ + fieldExpr: 'resourceId0', + dataSource: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }], + }], + }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 1000, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBe(0); + + await scrollTo(page, 0, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/cellsSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/cellsSelection.spec.ts new file mode 100644 index 000000000000..5cafcc8821e1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/cellsSelection.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler: Cells Selection in Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Selection should work correctly with all-day panel appointments', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 11, 9), + dataSource: [{ + startDate: new Date(2021, 11, 9), + endDate: new Date(2021, 11, 9), + allDay: true, + text: 'Appointment', + }], + }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }).click(); + await page.locator('.dx-scheduler-date-table-cell').first().click(); + + const selectedCount = await page.locator('.dx-scheduler-date-table-cell.dx-state-focused').count(); + expect(selectedCount).toBe(1); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/virtualScrollingCellSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/virtualScrollingCellSelection.spec.ts new file mode 100644 index 000000000000..ff37edf61d93 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/virtualScrollingCellSelection.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SELECTED_CELL_CLASS = 'dx-state-focused'; + +const createSchedulerWidget = async (page: any, options = {}) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2020, 8, 20), + cellDuration: 60, + height: 300, + width: 400, + scrolling: { mode: 'virtual' }, + resources: [{ + fieldExpr: 'resourceId0', + dataSource: [{ id: 0 }, { id: 1 }], + }], + ...options, + }); +}; + +const scrollTo = async (page: any, x: number, y: number) => { + await page.evaluate(({ scrollX, scrollY }: { scrollX: number; scrollY: number }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: scrollY, x: scrollX }); + }, { scrollX: x, scrollY: y }); + await page.waitForTimeout(300); +}; + +test.describe('Scheduler: Cells Selection in Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [true, false].forEach((showAllDayPanel) => { + test(`Selected cells shouldn't disappear on scroll when showAllDayPanel is equal to ${showAllDayPanel}`, async ({ page }) => { + await createSchedulerWidget(page, { showAllDayPanel }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 0, 500); + + await scrollTo(page, 0, 0); + + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); + }); + + test('Selection should work in month view', async ({ page }) => { + await createSchedulerWidget(page, { + views: [{ type: 'month', intervalCount: 30 }], + currentView: 'month', + }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 0, 1500); + + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBe(0); + + await scrollTo(page, 0, 0); + + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dataSource/load.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dataSource/load.spec.ts new file mode 100644 index 000000000000..795ae2b5ed25 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dataSource/load.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler - DataSource loading', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly load items with post processing', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: { + store: [ + { text: 'appt-0', startDate: new Date(2021, 3, 26, 9, 30), endDate: new Date(2021, 3, 26, 11, 30) }, + { text: 'appt-1', startDate: new Date(2021, 3, 27, 9, 30), endDate: new Date(2021, 3, 27, 11, 30) }, + { text: 'appt-2', startDate: new Date(2021, 3, 28, 9, 30), endDate: new Date(2021, 3, 28, 11, 30) }, + ], + postProcess: ((items: any[]) => [items[0]]) as any, + }, + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + endDayHour: 19, + height: 600, + width: 800, + }); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + + const appointment0 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'appt-0' }); + await expect(appointment0).toBeVisible(); + }); + + test('it should not call additional DataSource loads after repaint', async ({ page }) => { + await page.evaluate(() => { + (window as any).testOptions = { loadCount: 0 }; + (window as any).widget = ($('#container') as any) + .dxScheduler({ + dataSource: { + store: new (window as any).DevExpress.data.ArrayStore({ + data: [], + onLoaded: () => { (window as any).testOptions.loadCount! += 1; }, + }), + }, + }).dxScheduler('instance'); + }); + + await page.evaluate(() => { (window as any).widget.repaint(); }); + await page.evaluate(() => { (window as any).widget.repaint(); }); + await page.evaluate(() => { (window as any).widget.repaint(); }); + + await page.evaluate(() => { + const store = (window as any).widget.getDataSource().store(); + store.push([{ type: 'update', key: 0, data: {} }]); + }); + await page.waitForTimeout(200); + + const loadCount = await page.evaluate(() => (window as any).testOptions.loadCount); + expect(loadCount).toBe(2); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/deleteAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/deleteAppointments.spec.ts new file mode 100644 index 000000000000..e18240615bb1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/deleteAppointments.spec.ts @@ -0,0 +1,108 @@ +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('Delete appointments', () => { + 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 createRecurrenceData = (): Record[] => [{ + Text: 'Text', + StartDate: new Date(2017, 4, 22, 1, 30, 0, 0), + EndDate: new Date(2017, 4, 22, 2, 30, 0, 0), + RecurrenceRule: 'FREQ=DAILY', +}]; + +const createScheduler = async (data): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: ['week'], + currentView: 'week', + currentDate: new Date(2017, 4, 22), + textExpr: 'Text', + startDateExpr: 'StartDate', + endDateExpr: 'EndDate', + allDayExpr: 'AllDay', + recurrenceRuleExpr: 'RecurrenceRule', + recurrenceExceptionExpr: 'RecurrenceException', + }); +}; + +const createSimpleData = (): Record[] => [{ + Text: 'Text', + StartDate: new Date(2017, 4, 22, 1, 30, 0, 0), + EndDate: new Date(2017, 4, 22, 2, 30, 0, 0), +}, { + Text: 'Text2', + StartDate: new Date(2017, 4, 22, 12, 0, 0, 0), + EndDate: new Date(2017, 4, 22, 13, 0, 0, 0), +}]; + +test.meta({ unstable: true })('Recurrence appointments should be deleted by click on \'delete\' button', async (t) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(6) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).element) + + .expect(scheduler.appointmentTooltip.element.exists) + .ok() + .click(scheduler.appointmentTooltip.deleteButton) + .click(Scheduler.getDeleteRecurrenceDialog().appointment) + .wait(100) + + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(5); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + + .click(scheduler.appointmentTooltip.deleteButton) + .click(Scheduler.getDeleteRecurrenceDialog().series) + + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(0); +}).before(async () => createScheduler(createRecurrenceData())); + +test.meta({ unstable: true })('Recurrence appointments should be deleted by press \'delete\' key', async (t) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(6) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).element) + .pressKey('delete') + .click(Scheduler.getDeleteRecurrenceDialog().appointment) + .wait(100) + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(5); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + .pressKey('delete') + .click(Scheduler.getDeleteRecurrenceDialog().series) + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(0); +}).before(async () => createScheduler(createRecurrenceData())); + +test('Common appointments should be deleted by click on \'delete\' button and press \'delete\' key', async ({ page }) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(2) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).element) + .click(scheduler.appointmentTooltip.deleteButton) + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(1); + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(1) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text2' }).element) + .pressKey('delete') + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(0); +}).before(async () => createScheduler(createSimpleData())); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts new file mode 100644 index 000000000000..79c367c5bd90 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Drag-n-drop to fake cell', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should not select cells outside the scheduler(T1040795)', async ({ page }) => { + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); + await appendElementTo(page, '#container', 'div', { id: 'fake', style: 'width: 400px; height: 100px;' }); + await page.evaluate(() => { + $('#fake').addClass('scheduler-date-table-cell'); + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'app', + startDate: new Date(2021, 3, 26, 2), + endDate: new Date(2021, 3, 26, 2, 30), + }], + views: ['day'], + currentDate: new Date(2021, 3, 26), + height: 200, + width: 400, + }, '#scheduler'); + + const element = page.locator('#scheduler .dx-scheduler-appointment').filter({ hasText: 'app' }); + + const box = await element.boundingBox(); + await element.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 200, { steps: 10 }); + await page.mouse.up(); + + const hasDraggableClass = await page.locator('#fake').evaluate( + (el) => el.classList.contains('dx-scheduler-date-table-droppable-cell'), + ); + expect(hasDraggableClass).toBe(false); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts new file mode 100644 index 000000000000..0c7aeae92287 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts @@ -0,0 +1,60 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('T1017720', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Drag-n-drop appointment above SVG element(T1017720)', async ({ page }) => { + await createWidget(page, 'dxChart', { + width: '100%', + height: 1300, + series: { type: 'bar', color: '#ffaa66' }, + }); + + await page.evaluate(() => { + const scheduler = $('
'); + (scheduler as any).dxScheduler({ + width: '100%', + height: '100%', + startDayHour: 11, + dataSource: [{ + text: 'text', + startDate: new Date(2021, 6, 27, 11), + endDate: new Date(2021, 6, 27, 14), + allDay: false, + }], + views: ['week'], + currentDate: new Date(2021, 6, 27, 12), + currentView: 'week', + }); + ($('#container') as any).dxPopup({ + width: '90%', + height: '90%', + visible: true, + contentTemplate: () => scheduler, + }); + }); + + const scheduler = page.locator('#scheduler'); + const draggableAppointment = scheduler.locator('.dx-scheduler-appointment').filter({ hasText: 'text' }); + const workSpace = scheduler.locator('.dx-scheduler-work-space'); + + let box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 330, box!.y + box!.height / 2, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-right(T1017720).png', { element: workSpace }); + + box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 330, box!.y + box!.height / 2 + 70, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-left(T1017720).png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1080232.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1080232.spec.ts new file mode 100644 index 000000000000..938c81ba8119 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1080232.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment (T1080232)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly drag external item to the appointment after drag appointment', async ({ page }) => { + await appendElementTo(page, '#container', 'div', { id: 'list' }); + + await page.evaluate(() => { + $('#list').append('
drag-item
').addClass('drag-item'); + }); + + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); + + await createWidget(page, 'dxSortable', { + group: 'resourceGroup', + }, '#list'); + + await createWidget(page, 'dxScheduler', { + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + dataSource: [{ + text: 'Appt-01', + startDate: new Date(2021, 3, 26, 10), + endDate: new Date(2021, 3, 26, 11), + }, { + text: 'Appt-02', + startDate: new Date(2021, 3, 26, 12), + endDate: new Date(2021, 3, 26, 13), + }], + views: ['day'], + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + width: 800, + height: 600, + appointmentTemplate: new Function('e', '_', 'element', ` + var newData = e.appointmentData; + return element + .text(newData.text) + .dxSortable({ + group: 'resourceGroup', + data: [newData], + onAdd: function() { + element.attr('data-status', 'Added'); + }, + }); + `) as any, + }, '#scheduler'); + + const appt01 = page.locator('#scheduler .dx-scheduler-appointment').filter({ hasText: 'Appt-01' }); + const appt02 = page.locator('#scheduler .dx-scheduler-appointment').filter({ hasText: 'Appt-02' }); + const cell01 = page.locator('#scheduler .dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(0); + const dragItem = page.locator('.drag-item'); + + await appt01.dragTo(cell01); + + const appt01Box = await appt01.boundingBox(); + expect(appt01Box!.y).toBeCloseTo(183, 0); + + await dragItem.dragTo(appt02); + + const dataStatus = await appt02.locator('.dx-item-content').getAttribute('data-status'); + expect(dataStatus).toBe('Added'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1118059.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1118059.spec.ts new file mode 100644 index 000000000000..2af270b08587 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1118059.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SCHEDULER_SELECTOR = '#scheduler'; + +const markup = '
' + + '
drag container
' + + '
top right space
' + + '
' + + '
' + + '
left space
' + + '
' + + '
'; + +test.describe('T1118059', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('After drag to draggable component, should be called onAppointmentDeleting event only', async ({ page }) => { + await page.evaluate(() => { + (window as any).eventName = ''; + }); + + await setStyleAttribute(page, '#container', 'display: flex; flex-direction: column;'); + + await page.evaluate((m) => { + $('#container').append(m); + }, markup); + + await createWidget(page, 'dxDraggable', { + group: 'appointmentsGroup', + }, '#drag-container'); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'All day test app 1', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'All day test app 2', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'Regular test app', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11), + }], + views: [{ + type: 'day', + intervalCount: 2, + }], + onAppointmentUpdated: new Function(`window.eventName = 'onAppointmentUpdated';`) as any, + onAppointmentUpdating: new Function(`window.eventName = 'onAppointmentUpdating';`) as any, + onAppointmentDeleting: new Function(`window.eventName = 'onAppointmentDeleting';`) as any, + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + width: 500, + appointmentDragging: { + group: 'appointmentsGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + }, + }, SCHEDULER_SELECTOR); + + const regularApp = page.locator(`${SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: 'Regular test app' }); + const dragContainer = page.locator('#drag-container'); + + await regularApp.dragTo(dragContainer); + + await page.waitForTimeout(500); + + const eventName = await page.evaluate(() => (window as any).eventName); + expect(eventName).toBe('onAppointmentDeleting'); + }); + + test('After drag over component area, shouldn\'t called onAppointment* data events and appointment shouldn\'t change position', async ({ page }) => { + await page.evaluate(() => { + (window as any).eventName = ''; + }); + + await setStyleAttribute(page, '#container', 'display: flex; flex-direction: column;'); + + await page.evaluate((m) => { + $('#container').append(m); + }, markup); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'All day test app 1', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'All day test app 2', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'Regular test app', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11), + }], + views: [{ + type: 'day', + intervalCount: 2, + }], + onAppointmentUpdated: new Function(`window.eventName = 'onAppointmentUpdated';`) as any, + onAppointmentUpdating: new Function(`window.eventName = 'onAppointmentUpdating';`) as any, + onAppointmentDeleting: new Function(`window.eventName = 'onAppointmentDeleting';`) as any, + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + width: 500, + }, SCHEDULER_SELECTOR); + + const allDayApp2 = page.locator(`${SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: 'All day test app 2' }); + const spaceRight = page.locator('#space-right'); + + await allDayApp2.dragTo(spaceRight); + + let eventName = await page.evaluate(() => (window as any).eventName); + expect(eventName).toBe(''); + + const allDayTimeText = await allDayApp2.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(allDayTimeText).toContain('April 27'); + + const regularApp = page.locator(`${SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: 'Regular test app' }); + const leftRight = page.locator('#left-right'); + + await regularApp.dragTo(leftRight); + + eventName = await page.evaluate(() => (window as any).eventName); + expect(eventName).toBe(''); + + const regularTimeText = await regularApp.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(regularTimeText).toContain('10:30 AM - 11:00 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1235433.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1235433.spec.ts new file mode 100644 index 000000000000..c42ecc75eb34 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1235433.spec.ts @@ -0,0 +1,157 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const createScheduler = (view: string) => ({ + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Book 1', + startDate: new Date('2021-02-02T18:00:00.000Z'), + endDate: new Date('2021-02-02T19:00:00.000Z'), + priority: 1, + }, { + text: 'Book 2', + startDate: new Date('2021-02-03T01:00:00.000Z'), + endDate: new Date('2021-02-03T02:15:00.000Z'), + priority: 1, + }, { + text: 'Book 3', + startDate: new Date('2021-02-09T01:00:00.000Z'), + endDate: new Date('2021-02-09T02:15:00.000Z'), + priority: 1, + }, + ], + views: [view], + currentView: view, + currentDate: new Date('2021-02-02T17:00:00.000Z'), + firstDayOfWeek: 0, + scrolling: { mode: 'virtual' }, + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + groups: ['priority'], + useDropDownViewSwitcher: false, + resources: [{ + fieldExpr: 'priority', + dataSource: [ + { id: 1, text: 'Low Priority', color: 'green' }, + { id: 2, text: 'High Priority', color: 'blue' }, + ], + label: 'Priority', + }], + height: 580, +}); + +const scrollTo = async (page: any, x: number, y: number) => { + await page.evaluate(({ scrollX, scrollY }: { scrollX: number; scrollY: number }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: scrollY, x: scrollX }); + }, { scrollX: x, scrollY: y }); +}; + +const dragAppointmentByCircle = async ( + page: any, + appointmentText: string, + labels: string[], + descriptions: string[], +) => { + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentText }); + + const box0 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box0!.x + box0!.width / 2 - 200, box0!.y + box0!.height / 2, { steps: 10 }); + await page.mouse.up(); + + let ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[0]); + let ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[0]); + + const box1 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box1!.x + box1!.width / 2, box1!.y + box1!.height / 2 + 200, { steps: 10 }); + await page.mouse.up(); + + ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[1]); + ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[1]); + + const box2 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box2!.x + box2!.width / 2 + 200, box2!.y + box2!.height / 2, { steps: 10 }); + await page.mouse.up(); + + ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[2]); + ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[2]); + + const box3 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box3!.x + box3!.width / 2, box3!.y + box3!.height / 2 - 200, { steps: 10 }); + await page.mouse.up(); + + ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[3]); + ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[3]); +}; + +const appointmentDescriptions = ['Group: Low Priority', 'Group: High Priority', 'Group: High Priority', 'Group: Low Priority']; +const appointment1Times = ['9:00 AM - 10:00 AM', '9:00 AM - 10:00 AM', '10:00 AM - 11:00 AM', '10:00 AM - 11:00 AM']; +const appointment2Times = ['4:00 PM - 5:15 PM', '4:00 PM - 5:15 PM', '5:00 PM - 6:15 PM', '5:00 PM - 6:15 PM']; + +test.describe('Scheduler Drag-and-Drop inside Group', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('T1235433: Scheduler - Drag-n-Drop works inside the group with virtual scrolling (timelineDay)', async ({ page }) => { + await createWidget(page, 'dxScheduler', createScheduler('timelineDay')); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + await dragAppointmentByCircle(page, 'Book 1', appointment1Times, appointmentDescriptions); + await scrollTo(page, 1400, 0); + await dragAppointmentByCircle(page, 'Book 2', appointment2Times, appointmentDescriptions); + }); + + test('T1235433: Scheduler - Drag-n-Drop works inside the group with virtual scrolling (timelineWorkWeek)', async ({ page }) => { + await createWidget(page, 'dxScheduler', createScheduler('timelineWorkWeek')); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + await scrollTo(page, 2400, 0); + await dragAppointmentByCircle(page, 'Book 1', appointment1Times, appointmentDescriptions); + await scrollTo(page, 3400, 0); + await dragAppointmentByCircle(page, 'Book 2', appointment2Times, appointmentDescriptions); + }); + + test('T1235433: Scheduler - Drag-n-Drop works inside the group with virtual scrolling (timelineMonth)', async ({ page }) => { + await createWidget(page, 'dxScheduler', createScheduler('timelineMonth')); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + await dragAppointmentByCircle(page, 'Book 1', [ + 'February 1, 2021', + 'February 1, 2021', + 'February 2, 2021', + 'February 2, 2021', + ], appointmentDescriptions); + await scrollTo(page, 1000, 0); + await dragAppointmentByCircle(page, 'Book 3', [ + 'February 7, 2021', + 'February 7, 2021', + 'February 8, 2021', + 'February 8, 2021', + ], appointmentDescriptions); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1263508.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1263508.spec.ts new file mode 100644 index 000000000000..594659aba913 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1263508.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const DRAGGABLE_ITEM_CLASS = 'dx-card'; +const draggingGroupName = 'appointmentsGroup'; + +test.describe('Scheduler Drag-and-Drop Fix', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler - The \'Cannot read properties of undefined (reading \'getTime\')\' error is thrown on an attempt to drag an outside element if the previous drag operation was canceled', async ({ page }) => { + const tasks = [{ text: 'Brochures' }]; + + await page.evaluate(() => { + $('
', { id: 'list' }).appendTo('#parentContainer'); + }); + + await page.evaluate((tasksArr) => { + tasksArr.forEach((task: any) => { + $('
', { + class: 'dx-card', + text: task.text, + }).appendTo('#list'); + }); + }, tasks); + + for (const task of tasks) { + await createWidget(page, 'dxDraggable', { + group: draggingGroupName, + data: task, + clone: true, + onDragStart: new Function('e', 'e.itemData = e.fromData;') as any, + }, `.${DRAGGABLE_ITEM_CLASS}:contains(${task.text})`); + } + + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Book', + startDate: new Date('2021-04-26T19:00:00.000Z'), + endDate: new Date('2021-04-26T20:00:00.000Z'), + }, + ], + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + editing: true, + appointmentDragging: { + group: draggingGroupName, + onDragEnd: new Function('e', 'e.cancel = e.event.ctrlKey;') as any, + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + }, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Book' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(5).locator('.dx-scheduler-date-table-cell').nth(0); + const draggableItem = page.locator(`.${DRAGGABLE_ITEM_CLASS}`).filter({ hasText: 'Brochures' }); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + // TODO: This test requires disabling mouseup events during drag, then pressing escape. + const targetBox = await targetCell.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(targetBox!.x + targetBox!.width / 2, targetBox!.y + targetBox!.height / 2, { steps: 10 }); + await page.keyboard.press('Escape'); + await page.mouse.up(); + + await expect(draggableItem).toBeVisible(); + + await draggableItem.dragTo(targetCell); + + const newAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochures' }); + await expect(newAppointment).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T697037.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T697037.spec.ts new file mode 100644 index 000000000000..25b15ed5d916 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T697037.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('T697037', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Recurrence exception date should equal date of appointment, which excluded from recurrence(T697037)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: '2018-11-26T02:00:00Z', + endDate: '2018-11-26T02:15:00Z', + recurrenceRule: 'FREQ=DAILY;COUNT=5', + recurrenceException: '', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2018, 10, 26), + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ssZ', + timeZone: 'Etc/UTC', + showAllDayPanel: false, + recurrenceEditMode: 'occurrence', + onAppointmentUpdating: new Function('e', ` + window.recurrenceException = e.newData.recurrenceException; + `) as any, + }); + + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(3); + const appointments = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + const appointment = appointments.nth(2); + + await appointment.dragTo(targetCell); + + const recurrenceException = await page.evaluate(() => (window as any).recurrenceException); + expect(recurrenceException).toBe('20181128T020000Z'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts new file mode 100644 index 000000000000..c12f9e6efcbf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts @@ -0,0 +1,159 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; + +const appointmentCollectorData = [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 3, 9, 30), endDate: new Date(2019, 3, 3, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 11, 0) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 3, 9, 45), endDate: new Date(2019, 3, 3, 11, 15) }, + { text: 'Customer Workshop', startDate: new Date(2019, 3, 3, 11, 0), endDate: new Date(2019, 3, 3, 12, 0) }, + { text: 'Prepare 2015 Marketing Plan', startDate: new Date(2019, 3, 3, 11, 0), endDate: new Date(2019, 3, 3, 13, 30) }, + { text: 'Create Icons for Website', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 11, 30) }, +]; + +test.describe('Drag-and-drop behaviour for the appointment tooltip', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Drag-n-drop between a scheduler table cell and the appointment tooltip', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: appointmentCollectorData, + maxAppointmentsPerCell: 2, + width: 1000, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(5); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await tooltipItem.dragTo(targetCell); + await expect(tooltipItem).not.toBeVisible(); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + await expect(appointment).toBeVisible(); + + const height = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('76px'); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:30 AM - 10:30 AM'); + + const targetCell2 = page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(2); + await appointment.dragTo(targetCell2); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + await expect(appointment).not.toBeVisible(); + }); + + test('Drag-n-drop to the cell on the left should work in week view (T1005115)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2019, 3, 1), + views: ['week'], + currentView: 'week', + dataSource: [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 3, 9, 30), endDate: new Date(2019, 3, 3, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 10, 30) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 3, 9, 45), endDate: new Date(2019, 3, 3, 11, 15) }, + ], + maxAppointmentsPerCell: 2, + height: 800, + startDayHour: 9, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '1' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2); + + await collector.click(); + await tooltipItem.dragTo(targetCell); + + await testScreenshot(page, 'drag-n-drop-from-tooltip-to-left-cell-in-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + + test('Drag-n-drop in the same table cell', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: appointmentCollectorData, + maxAppointmentsPerCell: 2, + width: 1000, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + const box = await tooltipItem.boundingBox(); + await tooltipItem.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 90, { steps: 10 }); + await page.mouse.up(); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + await expect(tooltipItem).toBeVisible(); + }); + + test('Drag-n-drop to the cell below should work in month view (T1005115)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2019, 3, 1), + views: ['month'], + currentView: 'month', + dataSource: [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 3, 9, 30), endDate: new Date(2019, 3, 3, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 11, 0) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 3, 9, 45), endDate: new Date(2019, 3, 3, 11, 15) }, + ], + maxAppointmentsPerCell: 2, + height: 800, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '1 more' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(3); + + await collector.click(); + await tooltipItem.dragTo(targetCell); + + await testScreenshot(page, 'drag-n-drop-from-tooltip-to-cell-below-in-month.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts new file mode 100644 index 000000000000..4a4bc637e4fa --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts @@ -0,0 +1,225 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 9, 0), + endDate: new Date(2019, 3, 1, 9, 30), + resourceId: 0, + }, + { + text: 'Update NDA Agreement', + startDate: new Date(2019, 3, 1, 9, 0), + endDate: new Date(2019, 3, 1, 10, 0), + resourceId: 1, + }, + { + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 1, 9, 0), + endDate: new Date(2019, 3, 1, 10, 30), + resourceId: 2, + }, +]; + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Drag-and-drop appointments in the Scheduler basic views', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week', 'workWeek'].forEach((view) => { + test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0); + + await draggableAppointment.dragTo(targetCell); + + const height = await draggableAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('38px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('11:00 AM - 11:30 AM'); + }); + }); + + test('Drag-n-drop in the "month" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['month'], + currentView: 'month', + dataSource, + height: 834, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await draggableAppointment.dragTo(targetCell); + + const height = await draggableAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('23.8281px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:00 AM - 9:30 AM'); + }); + + test('Drag-n-drop when browser has horizontal scroll', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: [{ + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 6, 9, 0), + endDate: new Date(2019, 3, 6, 10, 30), + resourceId: 2, + }], + width: 1800, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); + const box = await draggableAppointment.boundingBox(); + + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 250, box!.y + box!.height / 2 - 50, { steps: 10 }); + await page.mouse.up(); + + const isAllDay = await draggableAppointment.evaluate((el) => { + return el.closest('.dx-scheduler-all-day-appointments') !== null; + }); + expect(isAllDay).toBe(true); + }); + + test('Drag-n-drop when browser has vertical scroll', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: [{ + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 1, 21, 0), + endDate: new Date(2019, 3, 1, 21, 30), + resourceId: 2, + }], + height: 1800, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(25).locator('.dx-scheduler-date-table-cell').nth(0); + + await draggableAppointment.dragTo(targetCell); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:30 PM - 10:00 PM'); + }); + + test('Drag recurrent appointment occurrence from collector (T832887)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + firstDayOfWeek: 2, + startDayHour: 4, + maxAppointmentsPerCell: 1, + dataSource: [{ + text: 'Recurrence one', + startDate: new Date(2019, 2, 26, 8, 0), + endDate: new Date(2019, 2, 26, 10, 0), + recurrenceException: '', + recurrenceRule: 'FREQ=DAILY', + }, { + text: 'Non-recurrent appointment', + startDate: new Date(2019, 2, 26, 7, 0), + endDate: new Date(2019, 2, 26, 11, 0), + }, { + text: 'Recurrence two', + startDate: new Date(2019, 2, 26, 8, 0), + endDate: new Date(2019, 2, 26, 10, 0), + recurrenceException: '', + recurrenceRule: 'FREQ=DAILY', + }], + currentDate: new Date(2019, 2, 26), + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Recurrence two' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2); + const popup = page.locator('.dx-dialog'); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await tooltipItem.dragTo(targetCell); + await expect(tooltipItem).not.toBeVisible(); + + await popup.locator('.dx-dialog-button').first().click(); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Recurrence two' }); + await expect(appointment).toBeVisible(); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('4:00 AM - 6:00 AM'); + + await expect(collector).not.toBeVisible(); + }); + + test('Drag-n-drop the appointment to the left column to the cell that has the same time', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + timeZone: 'Etc/GMT', + dataSource: [{ + text: 'Test appointment', + startDate: new Date('2022-09-08T10:00:00.000Z'), + endDate: new Date('2022-09-08T10:30:00.000Z'), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date('2022-09-09T10:00:00.000Z'), + startDayHour: 9, + width: 600, + height: 600, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test appointment' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2); + + await draggableAppointment.dragTo(targetCell); + + await testScreenshot(page, 'drag-n-drop-appointment-to-left-column.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentInEqualCellIndexes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentInEqualCellIndexes.spec.ts new file mode 100644 index 000000000000..280b7507050c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentInEqualCellIndexes.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const FIRST_SCHEDULER_SELECTOR = 'scheduler-first'; +const SECOND_SCHEDULER_SELECTOR = 'scheduler-second'; +const EXPECTED_APPOINTMENT_TIME = '1:00 AM - 2:00 AM'; + +const TEST_APPOINTMENT = { + text: 'My appointment', + startDate: new Date(2021, 3, 30, 1), + endDate: new Date(2021, 3, 30, 2), +}; + +const getSchedulerOptions = (dataSource: any[]) => ({ + dataSource, + currentView: 'workWeek', + currentDate: new Date(2021, 3, 26), + width: 600, + appointmentDragging: { + group: 'testDragGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + }, +}); + +test.describe('Drag-n-drop appointments between two schedulers with equal cell indexes (T1094035)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should not lose drag-n-dropped appointment in the second scheduler', async ({ page }) => { + await setStyleAttribute(page, '#container', 'display: flex;'); + await appendElementTo(page, '#container', 'div', { id: FIRST_SCHEDULER_SELECTOR }); + await appendElementTo(page, '#container', 'div', { id: SECOND_SCHEDULER_SELECTOR }); + + await createWidget(page, 'dxScheduler', getSchedulerOptions([TEST_APPOINTMENT]), `#${FIRST_SCHEDULER_SELECTOR}`); + await createWidget(page, 'dxScheduler', getSchedulerOptions([]), `#${SECOND_SCHEDULER_SELECTOR}`); + + const appointmentToMove = page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-row`).nth(2).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + + const movedAppointment = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const timeText = await movedAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain(EXPECTED_APPOINTMENT_TIME); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentWithDataSource.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentWithDataSource.spec.ts new file mode 100644 index 000000000000..61e389f8c184 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentWithDataSource.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const FIRST_SCHEDULER_SELECTOR = 'scheduler-first'; +const SECOND_SCHEDULER_SELECTOR = 'scheduler-second'; +const EXPECTED_APPOINTMENT_TIME = '12:00 AM - 1:00 AM'; + +const TEST_APPOINTMENT = { + id: 10, + text: 'My appointment', + startDate: new Date(2021, 3, 28, 1), + endDate: new Date(2021, 3, 28, 2), +}; + +const getBaseSchedulerOptions = (currentDate: Date) => ({ + currentDate, + currentView: 'workWeek', + width: 600, + appointmentDragging: { + group: 'testDragGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + }, +}); + +test.describe('Drag-n-drop appointments between two schedulers with async DataSource (T1094033)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should set correct start and end dates in drag&dropped appointment', async ({ page }) => { + await setStyleAttribute(page, '#container', 'display: flex;'); + await appendElementTo(page, '#container', 'div', { id: FIRST_SCHEDULER_SELECTOR }); + await appendElementTo(page, '#container', 'div', { id: SECOND_SCHEDULER_SELECTOR }); + + // TODO: Original test uses ClientFunction-based DataSourceMock for async data source + await page.evaluate(({ options, selector, appointments }: any) => { + class DataSourceMock { + key = 'id'; + private data: any[]; + constructor(initialData: any[] = []) { this.data = initialData; } + load = () => Promise.resolve(this.data); + insert = (value: any) => { this.data = [...this.data, value]; return Promise.resolve(); }; + update = (key: any, value: any) => { + this.data = this.data.map((item: any) => item.id === key ? value : item); + return Promise.resolve(); + }; + remove = (id: any) => { this.data = this.data.filter((item: any) => item.id !== id); return Promise.resolve(); }; + } + (window as any).DevExpress.fx.off = true; + ($(selector) as any).dxScheduler({ ...options, dataSource: new DataSourceMock(appointments) }); + }, { + options: getBaseSchedulerOptions(new Date(2021, 3, 26)), + selector: `#${FIRST_SCHEDULER_SELECTOR}`, + appointments: [TEST_APPOINTMENT], + }); + + await createWidget(page, 'dxScheduler', { + ...getBaseSchedulerOptions(new Date(2021, 4, 26)), + dataSource: [], + }, `#${SECOND_SCHEDULER_SELECTOR}`); + + const appointmentToMove = page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-row`).nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + await page.waitForTimeout(500); + + const movedAppointment = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const timeText = await movedAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain(EXPECTED_APPOINTMENT_TIME); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/removeDroppableCellClass.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/removeDroppableCellClass.spec.ts new file mode 100644 index 000000000000..2421bfe6532d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/removeDroppableCellClass.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const FIRST_SCHEDULER_SELECTOR = 'scheduler-first'; +const SECOND_SCHEDULER_SELECTOR = 'scheduler-second'; +const METHODS_TO_CANCEL = ['onDragStart', 'onDragMove', 'onDragEnd', 'onRemove', 'onAdd']; + +const TEST_APPOINTMENT = { + id: 10, + text: 'My appointment', + startDate: new Date(2021, 3, 28, 1), + endDate: new Date(2021, 3, 28, 2), +}; + +const getSchedulerOptions = (dataSource: any[], currentDate: Date, cancelMethodName: string) => ({ + dataSource, + currentDate, + currentView: 'workWeek', + width: 600, + appointmentDragging: { + group: 'testDragGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + [cancelMethodName]: new Function('e', 'e.cancel = true;') as any, + }, +}); + +test.describe('Cancel drag-n-drop when dragging an appointment from one scheduler to another', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + METHODS_TO_CANCEL.forEach((methodName) => { + test(`Should remove drag-n-drop classes if event was canceled in method ${methodName}`, async ({ page }) => { + await setStyleAttribute(page, '#container', 'display: flex;'); + await appendElementTo(page, '#container', 'div', { id: FIRST_SCHEDULER_SELECTOR }); + await appendElementTo(page, '#container', 'div', { id: SECOND_SCHEDULER_SELECTOR }); + + await createWidget(page, 'dxScheduler', getSchedulerOptions([TEST_APPOINTMENT], new Date(2021, 3, 26), methodName), `#${FIRST_SCHEDULER_SELECTOR}`); + await createWidget(page, 'dxScheduler', getSchedulerOptions([], new Date(2021, 4, 26), methodName), `#${SECOND_SCHEDULER_SELECTOR}`); + + const appointmentToMove = page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-row`).nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + + const droppableCellFirst = await page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-date-table-droppable-cell`).count(); + const droppableCellSecond = await page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-droppable-cell`).count(); + + expect(droppableCellFirst).toBe(0); + expect(droppableCellSecond).toBe(0); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts new file mode 100644 index 000000000000..98d7f625e80d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const APPOINTMENT_DRAG_SOURCE_CLASS = 'dx-scheduler-appointment-drag-source'; + +test.describe('Cancel appointment Drag-and-Drop', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('on escape - date should not changed when it\'s pressed during dragging (T832754)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + _draggingMode: 'default', + height: 600, + views: ['day'], + currentView: 'day', + cellDuration: 30, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2020, 9, 14, 10, 0), + endDate: new Date(2020, 9, 14, 10, 30), + }], + currentDate: new Date(2020, 9, 14), + showAllDayPanel: false, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0); + + // TODO: Original test uses MouseUpEvents.disable/enable to prevent mouseup during drag. + // Simulating: drag without releasing, press escape, then release. + const targetBox = await targetCell.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(targetBox!.x + targetBox!.width / 2, targetBox!.y + targetBox!.height / 2, { steps: 10 }); + await page.keyboard.press('Escape'); + await page.mouse.up(); + + const dragSourceCount = await page.locator(`.${APPOINTMENT_DRAG_SOURCE_CLASS}`).count(); + expect(dragSourceCount).toBe(0); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('10:00 AM - 10:30 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts new file mode 100644 index 000000000000..ec0145e6a6a3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Drag-n-drop appointment after resize (T835545)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week', 'month', 'timelineDay', 'timelineWeek', 'timelineMonth'].forEach((view) => { + test(`After drag-n-drop appointment, size of appointment shouldn't change in the '${view}' view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + startDayHour: 9, + currentDate: new Date(2017, 4, 1), + dataSource: [{ + text: 'app', + startDate: new Date(2017, 4, 1, 9, 0), + endDate: new Date(2017, 4, 1, 10, 0), + }], + }); + + const element = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); + + const initSize = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + + const bottomHandle = element.locator('.dx-resizable-handle-bottom'); + const rightHandle = element.locator('.dx-resizable-handle-right'); + const isVertical = await bottomHandle.count() > 0; + + const handle = isVertical ? bottomHandle : rightHandle; + const handleBox = await handle.boundingBox(); + await page.mouse.move(handleBox!.x + handleBox!.width / 2, handleBox!.y + handleBox!.height / 2); + await page.mouse.down(); + await page.mouse.move(handleBox!.x + handleBox!.width / 2 + 50, handleBox!.y + handleBox!.height / 2 + 50, { steps: 5 }); + await page.mouse.up(); + + const sizeAfterResize = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + + if (isVertical) { + expect(sizeAfterResize.height).toBeGreaterThan(initSize.height); + } else { + expect(sizeAfterResize.width).toBeGreaterThan(initSize.width); + } + + const sizeBeforeDrag = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + const positionBeforeDrag = await element.evaluate((el) => ({ + left: el.clientLeft, + top: el.clientTop, + })); + + const box = await element.boundingBox(); + await page.mouse.move(box!.x, box!.y); + await page.mouse.down(); + await page.mouse.move(box!.x + 10, box!.y + 10, { steps: 5 }); + await page.mouse.up(); + + const sizeAfterDrag = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + const positionAfterDrag = await element.evaluate((el) => ({ + left: el.clientLeft, + top: el.clientTop, + })); + + expect(sizeBeforeDrag.width).toBe(sizeAfterDrag.width); + expect(sizeBeforeDrag.height).toBe(sizeAfterDrag.height); + expect(positionBeforeDrag.left).toBe(positionAfterDrag.left); + expect(positionBeforeDrag.top).toBe(positionAfterDrag.top); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts new file mode 100644 index 000000000000..41f775c1645f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const INITIAL_APPOINTMENT = { + text: 'Test', + startDate: '2023-01-01T01:00:00', + endDate: '2023-01-01T02:00:00', +}; + +const TEST_CASES = [ + { + view: 'month', + expectedToItemData: { text: 'Test', startDate: '2023-01-05T01:00:00', endDate: '2023-01-05T02:00:00' }, + }, + { + view: 'week', + expectedToItemData: { text: 'Test', startDate: '2023-01-05T00:00:00', endDate: '2023-01-05T01:00:00', allDay: true }, + }, + { + view: 'timelineDay', + expectedToItemData: { text: 'Test', startDate: '2023-01-01T01:30:00', endDate: '2023-01-01T02:30:00', allDay: false }, + }, +]; + +test.describe('Scheduler dragging - drag events', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + TEST_CASES.forEach(({ view, expectedToItemData }) => { + test(`Should fire correct events with correct itemData inside during drag-n-drop in ${view} view.`, async ({ page }) => { + await page.evaluate(() => { + (window as any).clientTestingResults = { + onDragStartItemData: [], + onDragMoveItemData: [], + onDragEndItemData: [], + onDragEndToItemData: [], + }; + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [INITIAL_APPOINTMENT], + currentView: view, + currentDate: '2023-01-01', + appointmentDragging: { + onDragStart: new Function('e', 'window.clientTestingResults.onDragStartItemData.push(Object.assign({}, e.itemData));') as any, + onDragMove: new Function('e', 'window.clientTestingResults.onDragMoveItemData.push(Object.assign({}, e.itemData));') as any, + onDragEnd: new Function('e', 'window.clientTestingResults.onDragEndItemData.push(Object.assign({}, e.itemData)); window.clientTestingResults.onDragEndToItemData.push(Object.assign({}, e.toItemData));') as any, + }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await appointment.dragTo(targetCell); + + const results = await page.evaluate(() => (window as any).clientTestingResults); + + expect(results.onDragStartItemData.length).toBe(1); + expect(results.onDragStartItemData[0]).toEqual(INITIAL_APPOINTMENT); + + for (const itemData of results.onDragMoveItemData) { + expect(itemData).toEqual(INITIAL_APPOINTMENT); + } + + expect(results.onDragEndItemData.length).toBe(1); + expect(results.onDragEndToItemData.length).toBe(1); + expect(results.onDragEndItemData[0]).toEqual(INITIAL_APPOINTMENT); + expect(results.onDragEndToItemData[0]).toEqual(expectedToItemData); + }); + }); + + test('Should block appointment dragging while onAppointmentUpdating Promise is pending (T1308596)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test Appointment', + startDate: new Date(2023, 0, 2, 10, 0), + endDate: new Date(2023, 0, 2, 11, 0), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2023, 0, 2), + height: 600, + onAppointmentUpdating: new Function('e', 'e.cancel = new Promise(function(resolve) { setTimeout(function() { resolve(false); }, 5000); });') as any, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Appointment' }); + const targetCell1 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(2); + const targetCell2 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(5); + + const initialPosition = await appointment.boundingBox(); + + await appointment.dragTo(targetCell1); + await appointment.dragTo(targetCell2); + await appointment.dragTo(targetCell2); + await appointment.dragTo(targetCell2); + + await page.waitForTimeout(6000); + + const positionAfterPromiseResolved = await appointment.boundingBox(); + const cell1Position = await targetCell1.boundingBox(); + + expect(positionAfterPromiseResolved!.x).not.toBe(initialPosition!.x); + expect(positionAfterPromiseResolved!.x).toBe(cell1Position!.x); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts new file mode 100644 index 000000000000..595f54f0feb9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [{ + fieldExpr: 'resourceId', + dataSource: [{ id: 0, color: '#e01e38' }, { id: 1, color: '#f98322' }, { id: 2, color: '#1e65e8' }], + label: 'Color', + }], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Drag-n-drop from another draggable area', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Drag-n-drop an appointment when "cellDuration" changes dynamically', async ({ page }) => { + await appendElementTo(page, '#container', 'div', { id: 'drag-area' }); + + await page.evaluate(() => { + $('
').text('New Brochures').addClass('item').appendTo('#drag-area'); + }); + + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); + + await createWidget(page, 'dxDraggable', { + group: 'draggableGroup', + data: { text: 'New Brochures' }, + onDragStart: new Function('e', 'e.itemData = e.fromData;') as any, + }, '#group'); + + await createWidget(page, 'dxDraggable', { group: 'draggableGroup' }, '#drag-area'); + + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + appointmentDragging: { + group: 'draggableGroup', + onAdd: new Function('e', 'e.component.addAppointment(e.itemData); e.itemElement.remove();') as any, + }, + }, '#scheduler'); + + await page.evaluate(() => { + ($('#scheduler') as any).dxScheduler('instance').option('cellDuration', 10); + }); + + const dragItem = page.locator('.item'); + const targetCell = page.locator('#scheduler .dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await dragItem.dragTo(targetCell); + + const appointment = page.locator('#scheduler .dx-scheduler-appointment').first(); + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:00 AM - 9:10 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/insideScheduler/removeDroppableCellClass.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/insideScheduler/removeDroppableCellClass.spec.ts new file mode 100644 index 000000000000..6173419cedf7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/insideScheduler/removeDroppableCellClass.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const METHODS_TO_CANCEL = ['onDragStart', 'onDragMove', 'onDragEnd']; + +const TEST_APPOINTMENT = { + id: 10, + text: 'My appointment', + startDate: new Date(2021, 3, 28, 1), + endDate: new Date(2021, 3, 28, 2), +}; + +test.describe('Cancel drag-n-drop when dragging an appointment inside the scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + METHODS_TO_CANCEL.forEach((methodName) => { + test(`Should remove drag-n-drop classes if event was canceled in method ${methodName}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [TEST_APPOINTMENT], + currentDate: new Date(2021, 3, 28), + currentView: 'workWeek', + width: 600, + appointmentDragging: { + [methodName]: new Function('e', 'e.cancel = true;') as any, + }, + }); + + const appointmentToMove = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + + const droppableCellCount = await page.locator('.dx-scheduler-date-table-droppable-cell').count(); + expect(droppableCellCount).toBe(0); + + const isDraggableSource = await appointmentToMove.evaluate( + (el) => el.classList.contains('dx-scheduler-appointment-drag-source'), + ); + expect(isDraggableSource).toBe(false); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts new file mode 100644 index 000000000000..a363c9093c63 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts @@ -0,0 +1,175 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Outlook dragging base tests', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Basic drag-n-drop movements in groups', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 26, 8, 30), + endDate: new Date(2021, 2, 26, 11, 0), + priorityId: 1, + }], + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [ + { text: 'Low Priority', id: 1, color: '#1e90ff' }, + { text: 'High Priority', id: 2, color: '#ff9747' }, + ], + label: 'Priority', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 26), + startDayHour: 8, + height: 600, + width: 1000, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const workSpace = page.locator('.dx-scheduler-work-space'); + + let box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 330, box!.y + box!.height / 2 + 70, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-orange-group.png', { element: workSpace }); + + box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 330, box!.y + box!.height / 2 + 70, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-blue-group.png', { element: workSpace }); + }); + + test('Basic drag-n-drop movements', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 1000, + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const ws = page.locator('.dx-scheduler-work-space'); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-right.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-left.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-bottom.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-top.png', { element: ws }); + }); + + ['timelineWeek', 'timelineMonth'].forEach((currentView) => { + const dataSource = currentView === 'timelineWeek' + ? [{ text: 'Website Re-Design Plan', startDate: new Date(2021, 2, 21, 9, 30), endDate: new Date(2021, 2, 21, 10, 45) }] + : [{ text: 'Website Re-Design Plan', startDate: new Date(2021, 2, 2, 9, 30), endDate: new Date(2021, 2, 3, 11, 0) }]; + + test(`Basic drag-n-drop movements in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource, + views: ['timelineWeek', 'timelineMonth'], + currentView, + currentDate: new Date(2021, 2, 21), + startDayHour: 9, + height: 600, + width: 1000, + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const ws = page.locator('.dx-scheduler-work-space'); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 250, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, `drag-n-drop-${currentView}-to-right.png`, { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 250, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, `drag-n-drop-${currentView}-to-left.png`, { element: ws }); + }); + }); + + test('Narrow appointment dragging on minimal distance should be expected(1171520)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 1, 2), + endDate: new Date(2021, 1, 2, 1), + }], + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + cellDuration: 1440, + height: 300, + }); + + const ws = page.locator('.dx-scheduler-work-space'); + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + + let box = await appt.boundingBox(); + await page.mouse.move(box!.x + 10, box!.y + box!.height / 2); + await page.mouse.down(); + await page.mouse.move(box!.x + 10 - 10, box!.y + box!.height / 2, { steps: 5 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-short-app-min-dist-to-left.png', { element: ws }); + + box = await appt.boundingBox(); + await page.mouse.move(box!.x + 10, box!.y + box!.height / 2); + await page.mouse.down(); + await page.mouse.move(box!.x + 10 + 195, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-short-app-to-right.png', { element: ws }); + + box = await appt.boundingBox(); + await page.mouse.move(box!.x + 10, box!.y + box!.height / 2); + await page.mouse.down(); + await page.mouse.move(box!.x + 10 + 200, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-short-app-to-right-on-next-cell.png', { element: ws }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts new file mode 100644 index 000000000000..6e914bceeb75 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts @@ -0,0 +1,66 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../../tests/container.html'); + +test.describe('Outlook dragging, for case scheduler in container', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Dragging should be work right in case dxScheduler placed in dxTabPanel', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + + ($('#container') as any).dxTabPanel({ + items: [{ + title: 'Info', + text: 'This is Info Tab', + }, { + title: 'Contacts', + text: 'This is Contacts Tab', + disabled: true, + }], + itemTemplate: () => { + const scheduler = $('
'); + (scheduler as any).dxScheduler({ + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 30, 11), + endDate: new Date(2021, 2, 30, 12), + }], + views: ['week', 'month'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 600, + }); + return scheduler; + }, + }); + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 120, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-bottom.png'); + + box = await appt.boundingBox(); + await appt.hover(); + 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, 'dxScheduler-placed-in-dxTabPanel-drag-to-top.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-right.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts new file mode 100644 index 000000000000..7711a94932ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts @@ -0,0 +1,59 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../../tests/container.html'); + +test.describe('Outlook dragging, for case scheduler in container with transform style', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Dragging should be work right in case dxScheduler placed in container with transform style', async ({ page }) => { + await setStyleAttribute(page, '#container', 'margin-top: 100px; margin-left: 100px; transform: translate(0px, 0px);'); + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 24, 11), + endDate: new Date(2021, 2, 24, 12), + }], + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 800, + }, '#scheduler'); + + const appt = page.locator('#scheduler .dx-scheduler-appointment').first(); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 120, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-bottom.png'); + + box = await appt.boundingBox(); + await appt.hover(); + 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, 'dxScheduler-placed-in-transform-container-drag-to-top.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-right.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 230, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-left.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts new file mode 100644 index 000000000000..0215e758ebee --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts @@ -0,0 +1,59 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, setStyleAttribute } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Outlook dragging base tests in shifted container', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Basic drag-n-drop movements in shifted container', async ({ page }) => { + await setStyleAttribute(page, '#container', 'margin-left: 50px; margin-top: 70px;'); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 950, + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const ws = page.locator('.dx-scheduler-work-space'); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-right-in-shifted-container.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-left-in-shifted-container.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-bottom-in-shifted-container.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-top-in-shifted-container.png', { element: ws }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts new file mode 100644 index 000000000000..8b2400966c4d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { text: 'Brochure Design Review', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 9, 30), resourceId: 0 }, + { text: 'Update NDA Agreement', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 10, 0), resourceId: 1 }, + { text: 'Staff Productivity Report', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 10, 30), resourceId: 2 }, +]; + +const defaultSchedulerOptions = { + views: ['day'], dataSource: [], + resources: [{ fieldExpr: 'resourceId', dataSource: [{ id: 0, color: '#e01e38' }, { id: 1, color: '#f98322' }, { id: 2, color: '#1e65e8' }], label: 'Color' }], + width: 1666, height: 833, startDayHour: 9, firstDayOfWeek: 1, maxAppointmentsPerCell: 5, currentView: 'day', currentDate: new Date(2019, 3, 1), +}; + +test.describe('Drag-and-drop appointments in the Scheduler timeline views', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => { + test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...defaultSchedulerOptions, views: [view], currentView: view, dataSource }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await draggableAppointment.dragTo(targetCell); + + const width = await draggableAppointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('200px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('11:00 AM - 11:30 AM'); + }); + }); + + test('Drag-n-drop in the "timelineMonth" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...defaultSchedulerOptions, views: ['timelineMonth'], currentView: 'timelineMonth', dataSource }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await draggableAppointment.dragTo(targetCell); + + const height = await draggableAppointment.evaluate((el) => parseInt(getComputedStyle(el).height, 10)); + expect(height).toBeGreaterThanOrEqual(139); + expect(height).toBeLessThanOrEqual(140); + + const width = await draggableAppointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('200px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:00 AM - 9:30 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts new file mode 100644 index 000000000000..2d263b616e53 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts @@ -0,0 +1,43 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Drag-and-drop appointments in the Scheduler with vertical grouping', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should drag appoinment to the previous day`s cell (T1025952)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'appointment', + startDate: new Date(2021, 3, 21, 9, 30), + endDate: new Date(2021, 3, 21, 10), + priorityId: 1, + }], + views: [{ type: 'week', groupOrientation: 'vertical' }], + currentView: 'week', + currentDate: new Date(2021, 3, 21), + groups: ['priorityId'], + resources: [{ + dataSource: [{ text: 'Low Priority', id: 1 }, { text: 'High Priority', id: 2 }], + fieldExpr: 'priorityId', + displayExpr: 'name', + allowMultiple: false, + }], + startDayHour: 9, + endDayHour: 12, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'appointment' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(1); + + await appointment.dragTo(targetCell); + + await testScreenshot(page, 'drag-n-drop-previous-day-cell.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupHeaderLongNamesCss.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupHeaderLongNamesCss.spec.ts new file mode 100644 index 000000000000..6f10abe859ff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupHeaderLongNamesCss.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const resources = [ + { + text: 'Very Long Priority Name for High Priority Tasks and Urgent Matters', + id: 1, + color: '#ff9747', + }, + { + text: 'Extremely Long Priority Name for Medium Priority Tasks and Regular Work', + id: 2, + color: '#24ff50', + }, + { + text: 'Super Long Priority Name for Low Priority Tasks and Background Activities', + id: 3, + color: '#3366ff', + }, +]; + +const dataSource = [ + { + text: 'Team Meeting', + startDate: new Date(2021, 3, 21, 10, 0), + endDate: new Date(2021, 3, 21, 11, 30), + priorityId: 1, + }, + { + text: 'Code Review', + startDate: new Date(2021, 3, 21, 14, 0), + endDate: new Date(2021, 3, 21, 15, 0), + priorityId: 2, + }, + { + text: 'Planning Session', + startDate: new Date(2021, 3, 22, 9, 0), + endDate: new Date(2021, 3, 22, 12, 0), + priorityId: 3, + }, +]; + +const DEFAULT_OPTIONS = { + currentDate: new Date(2021, 3, 21), + height: 600, + width: 1000, + startDayHour: 9, + endDayHour: 16, + crossScrollingEnabled: true, + showCurrentTimeIndicator: false, + showAllDayPanel: false, + groups: ['priorityId'], + views: [{ + type: 'workWeek', + name: 'Vertical Grouping', + groupOrientation: 'vertical', + cellDuration: 60, + intervalCount: 2, + }, + { + type: 'workWeek', + name: 'Horizontal Grouping', + groupOrientation: 'horizontal', + cellDuration: 30, + intervalCount: 2, + }, { + type: 'month', + name: 'Group By Date', + groupOrientation: 'horizontal', + }, 'agenda'], + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: resources, + label: 'Priority', + }], + dataSource, +}; + +const CELL_SIZE_CSS = ` + #container .dx-scheduler-group-header { + width: auto; + } + #container .dx-scheduler-group-flex-container, + #container .dx-scheduler-work-space-vertical-group-table, + #container .dx-scheduler-sidebar-scrollable { + flex: 0 0 auto; + } +`; + +test.describe('Scheduler: Group Header CSS for Long Resource Names', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Group header CSS should work with vertical grouping and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'Vertical Grouping' }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-vertical-grouping-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + test('Group header CSS should work with horizontal grouping and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'Horizontal Grouping' }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-horizontal-grouping-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + test('Group header CSS should work with group by date and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'Group By Date', groupByDate: true }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-group-by-date-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + test('Group header CSS should work with agenda view and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'agenda' }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-agenda-view-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupingByDate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupingByDate.spec.ts new file mode 100644 index 000000000000..14101b106a00 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupingByDate.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const priorityData = [ + { + text: 'Low Priority', + id: 1, + color: '#1e90ff', + }, { + text: 'High Priority', + id: 2, + color: '#ff9747', + }, +]; + +const dataSource = [ + { + text: 'Website Re-Design Plan', + priorityId: 2, + startDate: new Date(2018, 4, 21, 9, 30), + endDate: new Date(2018, 4, 21, 11, 30), + }, { + text: 'Book Flights to San Fran for Sales Trip', + priorityId: 1, + startDate: new Date(2018, 4, 24, 10, 0), + endDate: new Date(2018, 4, 24, 12, 0), + }, { + text: 'Install New Router in Dev Room', + priorityId: 1, + startDate: new Date(2018, 4, 20, 13), + endDate: new Date(2018, 4, 20, 15, 30), + }, +]; + +const createScheduler = async (page: any, options = {}) => { + await createWidget(page, 'dxScheduler', { + views: ['week'], + dataSource: [], + resources: [ + { + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: priorityData, + label: 'Priority', + }, + ], + groups: ['priorityId'], + crossScrollingEnabled: true, + groupByDate: false, + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'week', + currentDate: new Date(2018, 4, 21), + ...options, + }); +}; + +test.describe('Drag-and-drop appointments into allDay panel in the grouped Scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Drag-n-drop between dateTable and allDay panel, groupByDate=true', async ({ page }) => { + await createScheduler(page, { + dataSource, + groupByDate: true, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const allDayCell = page.locator('.dx-scheduler-all-day-table-cell').nth(1); + + await draggableAppointment.dragTo(allDayCell); + + await expect(draggableAppointment).toBeVisible(); + + const isAllDay = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const appointments = instance.option('dataSource'); + const appt = appointments.find((a: any) => a.text === 'Website Re-Design Plan'); + return appt?.allDay === true; + }); + expect(isAllDay).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/monthViewVerticalGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/monthViewVerticalGrouping.spec.ts new file mode 100644 index 000000000000..9de66edd0c58 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/monthViewVerticalGrouping.spec.ts @@ -0,0 +1,59 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Month view vertical grouping', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scrolling: usual. Shouldn\'t overlap the next group with long all-day appointment in the month view (T1122185)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'Appointment group 1', + groupId: 1, + startDate: '2021-04-29T14:00:00Z', + endDate: '2021-06-20T14:00:00Z', + allDay: true, + }, + ], + views: [{ + type: 'month', + groupOrientation: 'vertical', + }], + currentView: 'month', + currentDate: '2021-04-29T00:00:00Z', + groups: ['groupId'], + resources: [ + { + fieldExpr: 'groupId', + allowMultiple: false, + dataSource: [{ + text: 'Group 1', + id: 1, + color: '#ff0000', + }, { + text: 'Group 2', + id: 2, + color: '#0000ff', + }], + label: 'Group', + }, + ], + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + + await testScreenshot(page, 'month-view_vertical-grouping_fist-app-part_T1122185.png', { element: workSpace }); + + await nextButton.click(); + await testScreenshot(page, 'month-view_vertical-grouping_middle-app-part_T1122185.png', { element: workSpace }); + + await nextButton.click(); + await testScreenshot(page, 'month-view_vertical-grouping_last-app-part_T1122185.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/overflow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/overflow.spec.ts new file mode 100644 index 000000000000..933fc45e36b2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/overflow.spec.ts @@ -0,0 +1,82 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler: Grouping overflow', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month'].forEach((viewType) => { + ['vertical', 'horizontal'].forEach((groupOrientation) => { + ['hidden', 'allDay'].forEach((allDayPanelMode) => { + [[9, 14, 60], [0, 24, 360]].forEach(([startDayHour, endDayHour, cellDuration]) => { + const allParams = `${viewType}-${groupOrientation}-${allDayPanelMode}-${startDayHour}-${endDayHour}`; + + test(`Long appointments should not overflow group view (${allParams})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: '1', + priorityId: 1, + startDate: '2021-04-19T16:30:00', + endDate: '2021-04-25T18:30:00', + }, { + text: '2', + priorityId: 2, + startDate: '2021-04-19T16:30:00', + endDate: '2021-04-25T18:30:00', + }, { + text: '3', + priorityId: 3, + startDate: '2021-04-19T16:30:00', + endDate: '2021-04-25T18:30:00', + }, + ], + views: [{ + type: viewType, + name: 'myView', + groupOrientation, + }], + cellDuration, + currentView: 'myView', + currentDate: new Date(2021, 3, 21), + allDayPanelMode, + startDayHour, + endDayHour, + groups: ['priorityId'], + resources: [ + { + fieldExpr: 'priorityId', + dataSource: [ + { + text: 'Low Priority', + id: 1, + color: '#1e90ff', + }, { + text: 'High Priority', + id: 2, + color: '#ff9747', + }, + { + text: 'Custom', + id: 3, + color: 'red', + }, + ], + }, + ], + showAllDayPanel: false, + }); + + await testScreenshot(page, `group-overflow-(${allParams}).png`, { + element: page.locator('.dx-scheduler'), + }); + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/resourceCellTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/resourceCellTemplate.spec.ts new file mode 100644 index 000000000000..dfc2322fc07b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/resourceCellTemplate.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('ResourceCellTemplate', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('resourceCellTemplate layout should be rendered right in the agenda view', async ({ page }) => { + const currentDate = new Date(2017, 4, 25); + + await page.evaluate(({ date }) => { + const currentDateValue = new Date(date); + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ + text: 'appointment', + startDate: currentDateValue, + endDate: currentDateValue, + resource: 1, + }], + views: ['agenda'], + currentView: 'agenda', + currentDate: currentDateValue, + resourceCellTemplate() { + return 'Custom resource text'; + }, + groups: ['resource'], + resources: [{ + fieldExpr: 'resource', + dataSource: [{ + text: 'Resource text', + id: 1, + }], + label: 'Resource', + }], + height: 600, + }); + }, { date: currentDate.toISOString() }); + + const groupHeader = page.locator('.dx-scheduler-group-header').first(); + await expect(groupHeader).toHaveText('Custom resource text'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/smoothCellLines.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/smoothCellLines.spec.ts new file mode 100644 index 000000000000..02adcc847f8a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/smoothCellLines.spec.ts @@ -0,0 +1,37 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const resourcesData = [...Array(20).keys()].map((num: number) => ({ + text: `Name ${num}`, + id: num, +})); + +test.describe('Scheduler: SmoothCellLines', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('The group panel and date table stay in sync during scrolling on material themes (T1146448)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['timelineWeek'], + currentView: 'timelineWeek', + groups: ['ownerId'], + currentDate: new Date(2021, 1, 2), + resources: [{ fieldExpr: 'ownerId', dataSource: resourcesData, label: 'Owner' }], + height: 600, + }); + + const scrollable = page.locator('.dx-scheduler-date-table-scrollable .dx-scrollable-container'); + await scrollable.evaluate((el) => { el.scrollTop = 1100; }); + + await page.waitForTimeout(300); + + await testScreenshot(page, 'scrolling-vertical', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/customization.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/customization.spec.ts new file mode 100644 index 000000000000..eaec342ac2dc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/customization.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const customToolbarItems = [ + { + location: 'before', + name: 'dateNavigator', + options: { + items: [ + { key: 'today', text: 'Today' }, + 'prev', + 'next', + 'dateInterval', + ], + }, + }, + { + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { icon: 'plus' }, + }, + 'viewSwitcher', +]; + +test.describe('Scheduler header customization', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler default toolbar should works', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + }); + + await testScreenshot(page, 'scheduler-default toolbar.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler toolbar should be hided', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + toolbar: { + visible: false, + items: [ + { location: 'before', name: 'viewSwitcher' }, + { location: 'after', name: 'dateNavigator' }, + ], + }, + }); + + await expect(page.locator('.dx-scheduler-header')).not.toBeVisible(); + + await testScreenshot(page, 'scheduler-hidden-toolbar.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + [ + { toolbar: { items: customToolbarItems }, description: 'custom toolbar' }, + { toolbar: { items: ['today', 'dateNavigator', 'viewSwitcher'] }, description: 'toolbar with today' }, + { toolbar: { disabled: true, items: customToolbarItems }, description: 'disabled toolbar' }, + ].forEach(({ toolbar, description }) => { + test(`Scheduler ${description} should works`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + toolbar, + }); + + await testScreenshot(page, `scheduler-${description}.png`, { + element: page.locator('.dx-scheduler-header'), + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/dateNavigator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/dateNavigator.spec.ts new file mode 100644 index 000000000000..e9fc6ac5d8d2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/dateNavigator.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Date navigator', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [{ + agendaDuration: 20, + result: '11-30 May 2021', + }, { + agendaDuration: 40, + result: '11 May-19 Jun 2021', + }].forEach(({ agendaDuration, result }) => { + test(`Caption of date navigator should be valid after change view to Agenda with agendaDuration=${agendaDuration}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: [{ + type: 'agenda', + agendaDuration, + }, 'month'], + currentView: 'month', + currentDate: new Date(2021, 4, 11), + height: 600, + }); + + const viewSwitcherMonthButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Month' }); + await viewSwitcherMonthButton.click(); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + await expect(caption).toHaveText(result); + }); + }); + + test('Current date in Calendar should be respond on prev and next buttons of Navigator', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + width: 600, + height: 400, + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + + await caption.click(); + await testScreenshot(page, 'initial-calendar-state.png'); + + await nextButton.click(); + await nextButton.click(); + await nextButton.click(); + await caption.click(); + await testScreenshot(page, 'calendar-state-after-next-button-click.png'); + + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await caption.click(); + await testScreenshot(page, 'calendar-state-after-prev-button-click.png'); + }); + + test('Current date in Navigator should be respond on Current date of Calendar', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + width: 600, + height: 400, + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await caption.click(); + + const calendarNextButton = page.locator('.dx-calendar .dx-calendar-navigator-next-view'); + const calendarPrevButton = page.locator('.dx-calendar .dx-calendar-navigator-previous-view'); + const calendarCells = page.locator('.dx-calendar-body td.dx-calendar-cell'); + + await calendarNextButton.click(); + await calendarCells.nth(20).click(); + + await testScreenshot(page, 'navigator-state-after-calendar-next-button-click.png'); + + await caption.click(); + await calendarPrevButton.click(); + await calendarPrevButton.click(); + await calendarCells.nth(15).click(); + + await testScreenshot(page, 'navigator-state-after-calendar-prev-button-click.png'); + }); + + test('Current date in navigator should be updated if scheduler currentDate is changed', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + width: 600, + height: 400, + }); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2022, 2, 28)); + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + await caption.click(); + + await testScreenshot( + page, + 'navigator-state-after-change-currentDate-option.png', + { element: page.locator('.dx-calendar') }, + ); + }); + + test('Calendar should be have right appearance', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + await caption.click(); + + await testScreenshot( + page, + 'right-calendar-appearance.png', + { element: page.locator('.dx-calendar') }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header.spec.ts new file mode 100644 index 000000000000..5b546c3cfbef --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header.spec.ts @@ -0,0 +1,178 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('dateNavigator buttons should not be selected after clicking', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day'], + height: 580, + }); + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await nextButton.click(); + + await expect(prevButton).not.toHaveClass(/dx-item-selected/); + await expect(caption).not.toHaveClass(/dx-item-selected/); + await expect(nextButton).not.toHaveClass(/dx-item-selected/); + }); + + test('dateNavigator buttons should have "contained" styling mode with generic theme', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day'], + height: 580, + }); + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await expect(prevButton).toHaveClass(/dx-button-mode-contained|dx-button-mode-text/); + await expect(caption).toHaveClass(/dx-button-mode-contained|dx-button-mode-text/); + await expect(nextButton).toHaveClass(/dx-button-mode-contained|dx-button-mode-text/); + }); + + const testData = [ + { + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-29T16:30:00.000Z'), + endDate: new Date('2021-03-29T18:30:00.000Z'), + }, { + text: 'Book Flights to San Fran for Sales Trip', + startDate: new Date('2021-03-29T19:00:00.000Z'), + endDate: new Date('2021-03-29T20:00:00.000Z'), + allDay: true, + }, { + 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'), + }, { + text: 'Approve Personal Computer Upgrade Plan', + startDate: new Date('2021-03-30T17:00:00.000Z'), + endDate: new Date('2021-03-30T18:00:00.000Z'), + }, { + text: 'Final Budget Review', + startDate: new Date('2021-03-30T19:00:00.000Z'), + endDate: new Date('2021-03-30T20:35:00.000Z'), + }, { + text: 'New Brochures', + startDate: new Date('2021-03-30T21:30:00.000Z'), + endDate: new Date('2021-03-30T22:45:00.000Z'), + }, { + text: 'Install New Database', + startDate: new Date('2021-03-31T16:45:00.000Z'), + endDate: new Date('2021-03-31T18:15:00.000Z'), + }, { + text: 'Approve New Online Marketing Strategy', + startDate: new Date('2021-03-31T19:00:00.000Z'), + endDate: new Date('2021-03-31T21:00:00.000Z'), + }, { + text: 'Upgrade Personal Computers', + startDate: new Date('2021-03-31T22:15:00.000Z'), + endDate: new Date('2021-03-31T23:30:00.000Z'), + }, { + text: 'Customer Workshop', + startDate: new Date('2021-04-01T18:00:00.000Z'), + endDate: new Date('2021-04-01T19:00:00.000Z'), + allDay: true, + }, { + text: 'Prepare 2021 Marketing Plan', + startDate: new Date('2021-04-01T18:00:00.000Z'), + endDate: new Date('2021-04-01T20:30:00.000Z'), + }, { + text: 'Brochure Design Review', + startDate: new Date('2021-04-01T21:00:00.000Z'), + endDate: new Date('2021-04-01T22:30:00.000Z'), + }, { + text: 'Create Icons for Website', + startDate: new Date('2021-04-02T17:00:00.000Z'), + endDate: new Date('2021-04-02T18:30:00.000Z'), + }, { + text: 'Upgrade Server Hardware', + startDate: new Date('2021-04-02T21:30:00.000Z'), + endDate: new Date('2021-04-02T23:00:00.000Z'), + }, { + text: 'Submit New Website Design', + startDate: new Date('2021-04-02T23:30:00.000Z'), + endDate: new Date('2021-04-03T01:00:00.000Z'), + }, { + text: 'Launch New Website', + startDate: new Date('2021-04-02T19:20:00.000Z'), + endDate: new Date('2021-04-02T21:00:00.000Z'), + }, + ]; + + const SCROLLBAR_STYLES = ` + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5); + } + .dx-scheduler-date-table-scrollable .dx-scrollable-container { + overflow: scroll !important; + } + `; + + test('Scheduler: maintain layout after horizontal scroll (T1306971)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: testData, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 730, + crossScrollingEnabled: true, + width: 500, + }); + + await insertStylesheetRulesToPage(page, SCROLLBAR_STYLES); + + await page.waitForTimeout(100); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').repaint(); + }); + + await page.waitForTimeout(100); + + await testScreenshot(page, 'T1306971__scheduler__horizontal-scrolling__before.png', { + element: page.locator('.dx-scheduler'), + }); + + const maxScrollLeft = await page.evaluate(() => { + const container = document.querySelector('.dx-scheduler-date-table-scrollable .dx-scrollable-container'); + if (!container) return 0; + return container.scrollWidth - container.clientWidth; + }); + + const scrollableContainer = page.locator('.dx-scheduler-date-table-scrollable .dx-scrollable-container'); + await scrollableContainer.evaluate((el, scrollLeft) => { el.scrollLeft = scrollLeft; }, maxScrollLeft); + + const finalScrollLeft = await scrollableContainer.evaluate((el) => el.scrollLeft); + + expect(maxScrollLeft).toBeGreaterThan(0); + expect(finalScrollLeft).toBeGreaterThan(0); + + await page.waitForTimeout(500); + + await testScreenshot(page, 'T1306971__scheduler__horizontal-scrolling__after.png', { + element: page.locator('.dx-scheduler'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header_material.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header_material.spec.ts new file mode 100644 index 000000000000..3abf8272f503 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header_material.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, isMaterialBased, isMaterial } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header: material theme', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('dateNavigator buttons should have "text" styling mode', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day'], + height: 580, + }); + + const expectedClass = isMaterialBased() ? 'dx-button-mode-text' : 'dx-button-mode-contained'; + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await expect(prevButton).toHaveClass(new RegExp(expectedClass)); + await expect(caption).toHaveClass(new RegExp(expectedClass)); + await expect(nextButton).toHaveClass(new RegExp(expectedClass)); + }); + + test('viewSwitcher dropdown button popup should have a specified class', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day', 'week'], + height: 580, + }); + + const dropDownButton = page.locator('.dx-scheduler-view-switcher .dx-dropdownbutton'); + await dropDownButton.click(); + + const viewSwitcherDropDownButtonContent = page.locator('.dx-scheduler-view-switcher-dropdown-button-content'); + const count = await viewSwitcherDropDownButtonContent.count(); + + expect(count).toBe(isMaterial() ? 1 : 0); + }); + + test('The toolbar should not display if the config is empty', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2020, 2, 2), + currentView: 'day', + views: ['day', 'week'], + height: 580, + toolbar: { items: [] }, + }); + + await testScreenshot(page, 'scheduler-with-empty-toolbar-config.png'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').option('toolbar', { items: ['viewSwitcher'] }); + }); + + await testScreenshot(page, 'scheduler-with-non-empty-toolbar-config.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/multiline_header.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/multiline_header.spec.ts new file mode 100644 index 000000000000..202c7c9631e4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/multiline_header.spec.ts @@ -0,0 +1,43 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const buttons = Array.from({ length: 12 }).map((_, index) => ({ + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { text: `Button ${index}` }, +})); + +test.describe('Scheduler multiline header', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [true, false].forEach((multiline) => { + test(`Scheduler [multiline=${multiline}] toolbar`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['day', 'week', 'workWeek', 'month'], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 27), + height: 200, + toolbar: { + multiline, + items: [ + 'dateNavigator', + ...buttons, + 'viewSwitcher', + ], + }, + }); + + await testScreenshot( + page, + `scheduler-multiline-${multiline}-toolbar.png`, + { element: page.locator('.dx-scheduler-header') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/sizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/sizes.spec.ts new file mode 100644 index 000000000000..73d0bdb0b9d9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/sizes.spec.ts @@ -0,0 +1,48 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const buttons = Array.from({ length: 4 }).map((_, index) => ({ + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { text: `Button ${index}` }, +})); + +test.describe('Scheduler header sizes', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('items inside toolbar menu should stretch', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 320, + currentDate: new Date('2025-05-02T07:59:01.167Z'), + toolbar: { + items: ['today', 'dateNavigator', ...buttons, { + location: 'after', + locateInMenu: 'auto', + name: 'viewSwitcher', + }], + }, + }); + + const menuButton = page.locator('.dx-scheduler-header .dx-toolbar-menu-container .dx-dropdownmenu, .dx-scheduler-header .dx-toolbar-menu-container .dx-button'); + await menuButton.click(); + + await testScreenshot(page, 'scheduler-toolbar-menu.png'); + }); + + test('Scheduler header should have correct sizes', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date('2025-05-02T07:59:01.167Z'), + toolbar: { items: ['today', 'dateNavigator', ...buttons, 'viewSwitcher'] }, + }); + + await testScreenshot(page, 'scheduler-toolbar.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/todayButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/todayButton.spec.ts new file mode 100644 index 000000000000..a6fe8ed12f41 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/todayButton.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header today button', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler today button should works', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + toolbar: { items: ['today', 'dateNavigator', 'viewSwitcher'] }, + }); + + const todayButton = page.locator('.dx-scheduler-header .dx-button').filter({ hasText: /today/i }).first(); + await todayButton.click(); + + const currentDate = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentDate'); + }); + + const today = new Date(); + const currentDateObj = new Date(currentDate); + currentDateObj.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + expect(currentDateObj.getTime()).toBe(today.getTime()); + }); + + test('Scheduler today button should use indicatorTime', async ({ page }) => { + const indicatorTime = new Date(2023, 3, 27); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + indicatorTime, + toolbar: { items: ['today', 'dateNavigator', 'viewSwitcher'] }, + }); + + const todayButton = page.locator('.dx-scheduler-header .dx-button').filter({ hasText: /today/i }).first(); + await todayButton.click(); + + const currentDate = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentDate'); + }); + + const currentDateObj = new Date(currentDate); + expect(currentDateObj.getFullYear()).toBe(indicatorTime.getFullYear()); + expect(currentDateObj.getMonth()).toBe(indicatorTime.getMonth()); + expect(currentDateObj.getDate()).toBe(indicatorTime.getDate()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/toolbar_option_change.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/toolbar_option_change.spec.ts new file mode 100644 index 000000000000..2fa87d46fe0f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/toolbar_option_change.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const createSchedulerWidget = async (page: any) => { + await createWidget(page, 'dxScheduler', { + views: ['day', 'week'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + height: 200, + width: 500, + }); +}; + +const buttons = Array.from({ length: 7 }).map((_, index) => ({ + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { text: `Button ${index}` }, +})); + +const setSchedulerOption = async (page: any, optionPath: string, value: any) => { + await page.evaluate(({ sel, opt, val }) => { + ($(sel) as any).dxScheduler('instance').option(opt, val); + }, { sel: SCHEDULER_SELECTOR, opt: optionPath, val: value }); +}; + +test.describe('Scheduler: Toolbar options change', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler should change toolbar item location', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items[0].location', 'after'); + + await testScreenshot(page, 'scheduler-toolbar-location-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should change toolbar', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar', { items: [{ template: 'Custom text' }] }); + + await testScreenshot(page, 'scheduler-toolbar-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should hide and show toolbar', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.visible', false); + await expect(page.locator('.dx-scheduler-header')).not.toBeVisible(); + + await setSchedulerOption(page, 'toolbar.visible', true); + await expect(page.locator('.dx-scheduler-header')).toBeVisible(); + }); + + test('Scheduler should change toolbar items', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items', buttons); + + await testScreenshot(page, 'scheduler-toolbar-items-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should change toolbar item option', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items[0].options.text', 'Changed text'); + + await testScreenshot(page, 'scheduler-toolbar-item-option-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should change toolbar options / integration', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items', buttons); + await setSchedulerOption(page, 'toolbar.multiline', true); + + await testScreenshot(page, 'scheduler-toolbar-property-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + + await setSchedulerOption(page, 'toolbar', { multiline: false }); + + await testScreenshot(page, 'scheduler-toolbar-changed-2.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/viewSwitcher.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/viewSwitcher.spec.ts new file mode 100644 index 000000000000..bc91aaa662d3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/viewSwitcher.spec.ts @@ -0,0 +1,114 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header - View switcher', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly switch a differently typed views (T1080992)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [ + 'day', + { + type: 'week', + name: 'Some week', + }, + ], + }); + + const dayButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Day' }); + await dayButton.click(); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const someWeekButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Some week' }); + await someWeekButton.click(); + + const isWeekView = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentView') === 'Some week'; + }); + expect(isWeekView).toBe(true); + + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const isDayView = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentView') === 'day'; + }); + expect(isDayView).toBe(true); + }); + + const defaultSelectBoxValue = 'Samantha Bright'; + + test('Changing view does not reset toolbar items state', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['week', 'month'], + currentView: 'week', + currentDate: new Date(2021, 3, 27), + toolbar: { + items: [ + { + location: 'before', + widget: 'dxSelectBox', + options: { items: [defaultSelectBoxValue] }, + }, + 'viewSwitcher', + ], + }, + }); + + const selectBox = page.locator('.dx-selectbox'); + await selectBox.click(); + const listItem = page.locator('.dx-list-item').first(); + await listItem.click(); + + const selectBoxValue = await selectBox.locator('input').inputValue(); + expect(selectBoxValue).toBe(defaultSelectBoxValue); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const monthButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Month' }); + await monthButton.click(); + + const isMonthView = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentView') === 'month'; + }); + expect(isMonthView).toBe(true); + + const selectBoxValueAfter = await selectBox.locator('input').inputValue(); + expect(selectBoxValueAfter).toBe(defaultSelectBoxValue); + }); + + [true, false].forEach((useDropDownViewSwitcher) => { + test(`view switcher should not be displayed if views has only one element when useDropDownViewSwitcher: ${useDropDownViewSwitcher}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2020, 2, 2), + currentView: 'day', + views: ['day'], + useDropDownViewSwitcher, + height: 580, + }); + + await testScreenshot( + page, + `toolbar-without-view-switcher-(useDropDownViewSwitcher=${useDropDownViewSwitcher}).png`, + { element: page.locator('.dx-scheduler-header') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/hotkeysBehaviour/hotkeysBehaviour.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/hotkeysBehaviour/hotkeysBehaviour.spec.ts new file mode 100644 index 000000000000..f256f37f3e03 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/hotkeysBehaviour/hotkeysBehaviour.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const hotkeyDataSource = [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Install New Router in Dev Room', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Final Budget Review', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'New Brochures', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Approve New Online Marketing Strategy', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, +]; + +const defaultSchedulerOptions = { + views: ['month'], + dataSource: [], + width: 1402, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'month', + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Hotkeys for appointments update and navigation', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month'].forEach((view) => { + test(`Navigate between appointments in the "${view}" view (Tab/Shift+Tab)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const secondAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Book Flights to San Fran for Sales Trip' }); + + await firstAppointment.click(); + let isFocused = await firstAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + + await page.keyboard.press('Tab'); + isFocused = await firstAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(false); + isFocused = await secondAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + + await page.keyboard.press('Shift+Tab'); + isFocused = await secondAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(false); + isFocused = await firstAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + }); + + test(`Remove appointment in the "${view}" view (Del)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.click(); + await page.keyboard.press('Delete'); + await expect(appointment).not.toBeVisible(); + }); + + test(`Show appointment popup in the "${view}" view (Enter)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.click(); + await page.keyboard.press('Enter'); + await expect(page.locator('.dx-scheduler-appointment-popup')).toBeVisible(); + }); + + test(`Navigate between tooltip appointments in the "${view}" view (Up/Down)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '3' }); + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('Enter'); + + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).not.toBeVisible(); + await expect(page.locator('.dx-scheduler-appointment-popup')).toBeVisible(); + }); + }); + + test('Navigate between toolbar items', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['day', 'week'], + currentView: 'day', + }); + + const toolbar = page.locator('.dx-scheduler-header'); + await toolbar.click(); + await page.keyboard.press('Tab'); + + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const isFocused = await prevButton.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/appointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/appointments.spec.ts new file mode 100644 index 000000000000..34623ea91f5a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/appointments.spec.ts @@ -0,0 +1,145 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, insertStylesheetRulesToPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SCHEDULER_SELECTOR = '#container'; + +const colors = [ + '#74d57b', '#1db2f5', '#f5564a', '#97c95c', '#ffc720', '#eb3573', + '#a63db8', '#ffaa66', '#2dcdc4', '#c34cb9', '#3d44ec', '#4ddcca', + '#2ec98d', '#ef9e44', '#45a5cc', '#a067bd', '#3d44ec', '#4ddcca', + '#3ff6ca', '#f665aa', '#d1c974', '#ff6741', '#ee53dc', '#795ac3', + '#ff7d8a', '#4cd482', '#9d67cc', '#5ab1ef', '#68e18f', '#4dd155', +]; + +const resources = colors.map((color, index) => ({ text: `Resource ${index + 1}`, id: index + 1, color })); +const resourceCount = 30; + +const getPseudoRandomDuration = (durationState: number): number => { + const durationMin = Math.floor((durationState % 23) / 3 + 5) * 15; + return durationMin * 60 * 1000; +}; + +const generateAppointments = () => { + const startDay = new Date(2021, 1, 1); + const endDay = new Date(2021, 1, 6); + let appointments: any[] = []; + let durationState = 1; + const durationIncrement = 19; + + resources.slice(0, resourceCount).forEach((resource) => { + let startDate = startDay; + while (startDate.getTime() < endDay.getTime()) { + durationState += durationIncrement; + const endDate = new Date(startDate.getTime() + getPseudoRandomDuration(durationState)); + appointments.push({ startDate, endDate, resourceId: resource.id }); + durationState += durationIncrement; + startDate = new Date(endDate.getTime() + getPseudoRandomDuration(durationState)); + } + }); + + appointments = appointments.filter(({ startDate, endDate }) => ( + startDate.getDay() === endDate.getDay() + && startDate.getHours() >= 7 + && endDate.getHours() <= 19)); + + return appointments.map((a, i) => ({ ...a, text: `[Appointment ${i + 1}]` })); +}; + +const dataSource = generateAppointments(); +const appointmentCount = dataSource.length; + +const getConfig = () => ({ + views: [{ type: 'timelineWorkWeek', name: 'Timeline', groupOrientation: 'vertical' }, 'week'], + dataSource, + resources: [{ fieldExpr: 'resourceId', label: 'Resource', dataSource: resources }], + groups: ['resourceId'], + scrolling: { mode: 'virtual' }, + height: 600, + cellDuration: 60, + startDayHour: 8, + endDayHour: 20, + showAllDayPanel: false, + currentView: 'Timeline', + currentDate: new Date(2021, 1, 2), +}); + +const cellStyles = '#container .dx-scheduler-cell-sizes-vertical { height: 100px; } #container .dx-scheduler-cell-sizes-horizontal { width: 150px; }'; + +test.describe('KeyboardNavigation.Appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['virtual', 'standard'].forEach((scrollingMode) => { + test(`focus next appointment on single tab (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + await page.keyboard.press('Tab'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 2]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`focus next appointment on 5 tab (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + for (let i = 0; i < 5; i++) { + await page.keyboard.press('Tab'); + } + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 6]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`focus last appointment on End (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + await page.keyboard.press('End'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: `[Appointment ${appointmentCount}]` }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`should focus appointment after close edit popup (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Escape'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 2]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`should focus next appointment on tab after any appointment was clicked (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 15]' }).click(); + await page.keyboard.press('Tab'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 16]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/dateTable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/dateTable.spec.ts new file mode 100644 index 000000000000..d0479f15436c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/dateTable.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const PARENT_SELECTOR = '#parentContainer'; +const SCHEDULER_SELECTOR = '#container'; +const BOTTOM_BTN_ID = 'bottom-btn'; +const BOTTOM_BTN_SELECTOR = `#${BOTTOM_BTN_ID}`; + +test.describe('KeyboardNavigation.DateTable', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week'].forEach((currentView) => { + test(`Should pass focus to the next elements after date table on Mac devices (view: ${currentView})`, async ({ page }) => { + await appendElementTo(page, PARENT_SELECTOR, 'button', { id: BOTTOM_BTN_ID }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2024-01-01T01:00:00', + endDate: '2024-01-01T02:00:00', + text: 'Usual', + }, + { + startDate: '2024-01-01T01:00:00', + endDate: '2024-01-01T02:00:00', + text: 'All-day', + allDay: true, + }, + ], + height: 300, + currentDate: '2024-01-01', + currentView, + }); + + await page.evaluate((sel) => { + ($(sel) as any) + .dxScheduler('instance') + .getWorkSpaceScrollable() + .option('useNative', true); + }, SCHEDULER_SELECTOR); + + const allDayAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'All-day' }); + await allDayAppointment.click(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + const bottomBtn = page.locator(BOTTOM_BTN_SELECTOR); + const isFocused = await bottomBtn.evaluate((el) => document.activeElement === el); + expect(isFocused).toBe(true); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/documentScrollPrevented.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/documentScrollPrevented.spec.ts new file mode 100644 index 000000000000..71a66bd7d3d6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/documentScrollPrevented.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('KeyboardNavigation.DocumentScrollPrevented', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Document should not scroll on \'End\' press when appointment is focused', async ({ page }) => { + await page.evaluate(() => { + document.body.style.height = '2000px'; + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 10), endDate: new Date(2015, 1, 9, 11) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 12), endDate: new Date(2015, 1, 9, 13) }, + ], + height: 300, + currentView: 'day', + currentDate: new Date(2015, 1, 9), + }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }).click(); + const expectedScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + await page.keyboard.press('End'); + const actualScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + expect(actualScrollTop).toBe(expectedScrollTop); + + await page.evaluate(() => { document.body.style.height = ''; }); + }); + + test('Document should not scroll on \'Home\' press when appointment is focused', async ({ page }) => { + await page.evaluate(() => { + document.body.style.height = '2000px'; + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 10), endDate: new Date(2015, 1, 9, 11) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 12), endDate: new Date(2015, 1, 9, 13) }, + ], + height: 300, + currentView: 'day', + currentDate: new Date(2015, 1, 9), + }); + + await page.evaluate(() => window.scrollTo(0, 100)); + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }).click(); + const expectedScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + await page.keyboard.press('Home'); + const actualScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + expect(actualScrollTop).toBe(expectedScrollTop); + + await page.evaluate(() => { document.body.style.height = ''; }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/adaptive.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/adaptive.spec.ts new file mode 100644 index 000000000000..4c64199a1a57 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/adaptive.spec.ts @@ -0,0 +1,152 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +const resourceDataSource = [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', +}]; + +const views = [ + 'day', 'week', 'month', + 'timelineDay', 'timelineWeek', 'timelineMonth', +]; + +const verticalViews = views.map((viewType) => ({ + type: viewType, + groupOrientation: 'vertical', +})); + +const horizontalViews = views.map((viewType) => ({ + type: viewType, + groupOrientation: 'horizontal', +})); + +const createDataSetForScreenShotTests = (): Record[] => { + const result: any[] = []; + for (let day = 1; day < 25; day++) { + result.push({ + text: '1 appointment', + startDate: new Date(2020, 6, day, 0), + endDate: new Date(2020, 6, day, 1), + priorityId: 0, + }); + result.push({ + text: '2 appointment', + startDate: new Date(2020, 6, day, 1), + endDate: new Date(2020, 6, day, 2), + priorityId: 1, + }); + result.push({ + text: '3 appointment', + startDate: new Date(2020, 6, day, 3), + endDate: new Date(2020, 6, day, 5), + allDay: true, + priorityId: 0, + }); + } + return result; +}; + +test.describe('Scheduler: Adaptive layout in 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'); + }); + + [false, true].forEach((rtlEnabled) => { + [false, true].forEach((crossScrollingEnabled) => { + test(`Adaptive views layout test, crossScrollingEnabled=${crossScrollingEnabled}${rtlEnabled ? ' in RTL' : ''}`, async ({ page }) => { + await page.setViewportSize({ width: 400, height: 600 }); + + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + height: 600, + views, + currentView: 'day', + crossScrollingEnabled, + rtlEnabled, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `view=${view}-crossScrolling=${crossScrollingEnabled}${rtlEnabled ? '-rtl' : ''}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Adaptive views layout test crossScrollingEnabled=${crossScrollingEnabled} when horizontal grouping${rtlEnabled ? ' and RTL are' : ' is'} used`, async ({ page }) => { + await page.setViewportSize({ width: 400, height: 600 }); + + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + height: 600, + views: horizontalViews, + currentView: 'day', + crossScrollingEnabled, + rtlEnabled, + groups: ['priorityId'], + resources: resourceDataSource, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `view=${view}-crossScrolling=${crossScrollingEnabled}-horizontal${rtlEnabled ? '-rtl' : ''}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Adaptive views layout test, crossScrollingEnabled=${crossScrollingEnabled} when vertical grouping${rtlEnabled ? ' and RTL are' : ' is'} used`, async ({ page }) => { + await page.setViewportSize({ width: 400, height: 600 }); + + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + height: 600, + views: verticalViews, + currentView: 'day', + crossScrollingEnabled, + rtlEnabled, + groups: ['priorityId'], + resources: resourceDataSource, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `view=${view}-crossScrolling=${crossScrollingEnabled}-vertical${rtlEnabled ? '-rtl' : ''}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/resize/browserResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/resize/browserResize.spec.ts new file mode 100644 index 000000000000..384d715d0486 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/resize/browserResize.spec.ts @@ -0,0 +1,87 @@ +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('Layout:BrowserResize', () => { + 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 = [ + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30), roomId: 1 }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2017, 4, 22, 12, 0), endDate: new Date(2017, 4, 22, 13, 0), allDay: true, roomId: 2 }, + { text: 'Install New Router in Dev Room', startDate: new Date(2017, 4, 22, 14, 30), endDate: new Date(2017, 4, 22, 15, 30), roomId: 3 }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2017, 4, 23, 10, 0), endDate: new Date(2017, 4, 23, 11, 0) }, + { text: 'Final Budget Review', startDate: new Date(2017, 4, 23, 12, 0), endDate: new Date(2017, 4, 23, 13, 35), roomId: 1 }, + { text: 'New Brochures', startDate: new Date(2017, 4, 23, 14, 30), endDate: new Date(2017, 4, 23, 15, 45), roomId: 2 }, + { text: 'Install New Database', startDate: new Date(2017, 4, 24, 9, 45), endDate: new Date(2017, 4, 24, 11, 15), roomId: 1 }, + { text: 'Approve New Online Marketing Strategy', startDate: new Date(2017, 4, 24, 12, 0), endDate: new Date(2017, 4, 24, 14, 0) }, + { text: 'Upgrade Personal Computers', startDate: new Date(2017, 4, 24, 15, 15), endDate: new Date(2017, 4, 24, 16, 30), roomId: 1 }, + { text: 'Customer Workshop', startDate: new Date(2017, 4, 25, 11, 0), endDate: new Date(2017, 4, 25, 12, 0), allDay: true }, + { text: 'Prepare 2015 Marketing Plan', startDate: new Date(2017, 4, 25, 11, 0), endDate: new Date(2017, 4, 25, 13, 30) }, + { text: 'Brochure Design Review', startDate: new Date(2017, 4, 25, 14, 0), endDate: new Date(2017, 4, 25, 15, 30), roomId: 3 }, + { text: 'Create Icons for Website', startDate: new Date(2017, 4, 26, 10, 0), endDate: new Date(2017, 4, 26, 11, 30), roomId: 2 }, + { text: 'Upgrade Server Hardware', startDate: new Date(2017, 4, 26, 14, 30), endDate: new Date(2017, 4, 26, 16, 0) }, + { text: 'Submit New Website Design', startDate: new Date(2017, 4, 26, 16, 30), endDate: new Date(2017, 4, 26, 18, 0) }, + { text: 'Launch New Website', startDate: new Date(2017, 4, 26, 12, 20), endDate: new Date(2017, 4, 26, 14, 0) }, + ]; + + const resourceDataSource = [ + { text: 'Room 1', id: 1, color: '#00af2c' }, + { text: 'Room 2', id: 2, color: '#56ca85' }, + { text: 'Room 3', id: 3, color: '#8ecd3c' }, + ]; + + [{ + currentView: 'agenda', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'day', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'week', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'month', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'timelineDay', + currentDate: new Date(2017, 4, 26), + }].forEach(({ currentView, currentDate }) => { + test(`Appointment layout after resize should be rendered right in '${currentView}'`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: [currentView], + currentView, + currentDate, + resources: [{ + fieldExpr: 'roomId', + dataSource: resourceDataSource, + }], + startDayHour: 9, + height: 600, + }); + + await testScreenshot( + page, + `browser-resize-currentView=${currentView}-before-resize.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + + await page.setViewportSize({ width: 600, height: 600 }); + + await testScreenshot( + page, + `browser-resize-currentView=${currentView}-after-resize.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/allDayPanel/allDayPanelMode.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/allDayPanel/allDayPanelMode.spec.ts new file mode 100644 index 000000000000..0cb72e54e806 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/allDayPanel/allDayPanelMode.spec.ts @@ -0,0 +1,100 @@ +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('Layout:AllDayPanelMode', () => { + 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'); + }); + + [ + { + testCaseName: 'Usual appointment', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T10:00:00', text: 'Usual appt' }], + modesOrder: ['all', 'allDay', 'hidden'], + expectedCollapsed: [true, true, false], + expectedVisible: [true, true, false], + }, + { + testCaseName: 'Usual appointment reverse', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T10:00:00', text: 'Usual appt' }], + modesOrder: ['hidden', 'allDay', 'all'], + expectedCollapsed: [false, true, true], + expectedVisible: [false, true, true], + }, + { + testCaseName: 'Long appointment', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2024-01-01T00:00:00', text: 'Long appt' }], + modesOrder: ['all', 'allDay', 'hidden'], + expectedCollapsed: [false, true, false], + expectedVisible: [true, true, false], + }, + { + testCaseName: 'Long appointment reverse', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2024-01-01T00:00:00', text: 'Long appt' }], + modesOrder: ['hidden', 'allDay', 'all'], + expectedCollapsed: [false, true, false], + expectedVisible: [false, true, true], + }, + { + testCaseName: 'All-day appointment', + dataSource: [{ + startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T00:00:00', text: 'All-day appt', allDay: true, + }], + modesOrder: ['all', 'allDay', 'hidden'], + expectedCollapsed: [false, false, false], + expectedVisible: [true, true, false], + }, + { + testCaseName: 'All-day appointment reverse', + dataSource: [{ + startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T00:00:00', text: 'All-day appt', allDay: true, + }], + modesOrder: ['hidden', 'allDay', 'all'], + expectedCollapsed: [false, false, false], + expectedVisible: [false, true, true], + }, + ].forEach(({ + testCaseName, dataSource, modesOrder, expectedCollapsed, expectedVisible, + }) => { + test(`${testCaseName}: AllDayPanel visibility and collapsed state should be correct in runtime`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'week', + currentDate: '2023-12-01', + dataSource, + }); + + for (let idx = 0; idx < modesOrder.length; idx++) { + const mode = modesOrder[idx]; + + await page.evaluate((m: string) => { + ($('#container') as any).dxScheduler('instance').option('allDayPanelMode', m); + }, mode); + + const isCollapsed = await page.evaluate(() => { + const allDayTable = document.querySelector('.dx-scheduler-all-day-table'); + if (!allDayTable) return false; + const row = allDayTable.querySelector('tr'); + if (!row) return false; + return row.getBoundingClientRect().height === 0; + }); + + const isVisible = await page.locator('.dx-scheduler-all-day-table-row').count() > 0; + + expect(isCollapsed).toBe( + expectedCollapsed[idx], + ); + expect(isVisible).toBe( + expectedVisible[idx], + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/allDayExpr.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/allDayExpr.spec.ts new file mode 100644 index 000000000000..d9aa27e5a8da --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/allDayExpr.spec.ts @@ -0,0 +1,57 @@ +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('Layout:Appointments:allDayExpr', () => { + 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'); + }); + + [{ + config: { + allDayExpr: 'AllDay', + }, + data: { + AllDay: true, + }, + }, { + config: {}, + data: { + allDay: true, + }, + }].forEach(({ config, data }) => { + test(`All day appointment should be render valid in case without endDate property with allDayExpr=${(config as any).allDayExpr}(T1155630)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'MY EVENT', + startDate: new Date(2023, 2, 19, 23, 45), + ...data, + }], + views: ['week', 'timelineWeek'], + currentView: 'week', + cellDuration: 360, + startDayHour: 18, + currentDate: new Date(2023, 2, 21), + height: 600, + ...config, + }); + + await testScreenshot(page, `week-all-day-expr-${(config as any).allDayExpr}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + + await page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Timeline Week' }).click(); + + await testScreenshot(page, `timelineWeek-all-day-expr-${(config as any).allDayExpr}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/longAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/longAppointment.spec.ts new file mode 100644 index 000000000000..478be07363e6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/longAppointment.spec.ts @@ -0,0 +1,56 @@ +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('Layout:Appointments:AllDay', () => { + 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('Long all day appointment should be render, if him ended on next view day in currentView: day(T1021963)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ allDay: true, startDate: new Date(2021, 2, 28), endDate: new Date(2021, 2, 29) }], + views: ['day'], currentView: 'day', currentDate: new Date(2021, 2, 28), + startDayHour: 9, width: 400, height: 600, + }); + + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const workSpace = page.locator('.dx-scheduler-work-space'); + + await prevButton.click(); + await testScreenshot(page, '27-march-day-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '28-march-day-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '29-march-day-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '30-march-day-view.png', { element: workSpace }); + }); + + test('Long all day appointment should be render, if him ended on next view day in currentView: week', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ allDay: true, startDate: new Date(2021, 2, 27), endDate: new Date(2021, 3, 4) }], + views: ['week'], currentView: 'week', currentDate: new Date(2021, 2, 28), + startDayHour: 9, width: 600, height: 600, + }); + + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const workSpace = page.locator('.dx-scheduler-work-space'); + + await prevButton.click(); + await testScreenshot(page, '21-27-march-week-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '28-march-3-apr-week-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '4-10-apr-week-view.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/collector.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/collector.spec.ts new file mode 100644 index 000000000000..4d5b49c55aeb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/collector.spec.ts @@ -0,0 +1,63 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, generateOptionMatrix } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Appointments collector', () => { + 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 collector has correct offset when adaptivityEnabled=true (T1024299)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + adaptivityEnabled: true, currentDate: new Date(2021, 7, 1), + views: ['timelineMonth'], currentView: 'timelineMonth', + dataSource: [{ text: 'text', startDate: new Date(2021, 7, 1), endDate: new Date(2021, 7, 2) }], + height: 300, + }); + await testScreenshot(page, 'appointment-collector-adaptability-timelineMonth.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + + const getSchedulerBaseOptions = (view: string) => { + const count = 20; + const day = 1; + const allDayAppointments = Array(Math.round(count / 4)).fill({ + allDay: true, text: 'text', startDate: new Date(2021, 7, day, 0), endDate: new Date(2021, 7, day, 2), + }); + const regularAppointments = Array(Math.round((count * 3) / 4)).fill({ + text: 'text', startDate: new Date(2021, 7, day, 0), endDate: new Date(2021, 7, day, 2), + }); + const width = ['month', 'week'].includes(view) ? 800 : 500; + const height = ['month'].includes(view) ? 500 : 300; + return { currentDate: new Date(2021, 7, day), views: [view], currentView: view, dataSource: [...allDayAppointments, ...regularAppointments], height, width }; + }; + + generateOptionMatrix({ view: ['week', 'month', 'timelineWeek'], adaptivityEnabled: [true, false] }) + .forEach(({ view, adaptivityEnabled }) => { + test(`Appointment collector has correct offset when view=${view} adaptivityEnabled=${adaptivityEnabled}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { adaptivityEnabled, ...getSchedulerBaseOptions(view) }); + await testScreenshot(page, `appointment-collector-${view}-adapt(${adaptivityEnabled}).png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); + + test('Appointment collector has correct offset when month view with double interval', async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...getSchedulerBaseOptions('month'), views: [{ type: 'month', intervalCount: 2 }] }); + await testScreenshot(page, 'appointment-collector-month-double-interval.png', { element: page.locator('.dx-scheduler-work-space') }); + }); + + generateOptionMatrix({ view: ['week', 'month', 'timelineWeek'], rtlEnabled: [false, true] }) + .forEach(({ view, rtlEnabled }) => { + test(`Appointment collector has correct offset when view=${view} rtlEnabled=${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...getSchedulerBaseOptions(view), rtlEnabled }); + await testScreenshot(page, `appointment-collector-${view}-rtl(${rtlEnabled}).png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/dataSource.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/dataSource.spec.ts new file mode 100644 index 000000000000..36279705c996 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/dataSource.spec.ts @@ -0,0 +1,38 @@ +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('DataSource', () => { + 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 key should be deleted when removing an appointment from series (T1024213)', async ({ page }) => { + await page.evaluate(() => { + const devExpress = (window as any).DevExpress; + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: new devExpress.data.DataSource({ + store: { type: 'array', key: 'appointmentId', data: [{ + startDate: new Date(2021, 6, 12, 10), endDate: new Date(2021, 6, 12, 11), + text: 'Test Appointment', recurrenceRule: 'FREQ=DAILY;COUNT=3', appointmentId: 0, + }] }, + }), + recurrenceEditMode: 'occurrence', views: ['week'], currentView: 'week', + startDayHour: 9, currentDate: new Date(2021, 6, 12, 10), height: 600, + }); + }); + await page.locator('.dx-scheduler-appointment').nth(1).dblclick(); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /done|save/i }).click(); + await testScreenshot(page, 'exclude-appointment-from-series-via-form-editing.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/disable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/disable.spec.ts new file mode 100644 index 000000000000..3f73eeff05e4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/disable.spec.ts @@ -0,0 +1,53 @@ +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:disable', () => { + 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 popup should be readOnly if appointment is disabled', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [ + { disabled: true, text: 'A', startDate: new Date(2021, 4, 27, 0, 30), endDate: new Date(2021, 4, 27, 1), recurrenceRule: 'FREQ=DAILY;UNTIL=20210615T205959Z' }, + { disabled: false, text: 'B', startDate: new Date(2021, 4, 27, 1), endDate: new Date(2021, 4, 27, 1, 30), recurrenceRule: 'FREQ=DAILY;UNTIL=20210615T205959Z' }, + { disabled: () => true, text: 'C', startDate: new Date(2021, 4, 27, 1, 30), endDate: new Date(2021, 4, 27, 2), recurrenceRule: 'FREQ=WEEKLY;UNTIL=20210615T205959Z' }, + { disabled: () => false, text: 'D', startDate: new Date(2021, 4, 27, 2), endDate: new Date(2021, 4, 27, 2, 30), recurrenceRule: 'FREQ=WEEKLY;UNTIL=20210615T205959Z' }, + ], + recurrenceEditMode: 'series', views: ['week'], currentView: 'week', currentDate: new Date(2021, 4, 27), + }); + }); + + await testScreenshot(page, 'disabled-appointments-in-grid.png'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'A' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'A' }).click(); + await testScreenshot(page, 'disabled-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'B' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'B' }).click(); + await testScreenshot(page, 'enabled-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'C' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'C' }).click(); + await testScreenshot(page, 'disabled-by-function-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'D' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'D' }).click(); + await testScreenshot(page, 'enabled-by-function-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/longAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/longAppointments.spec.ts new file mode 100644 index 000000000000..96f5d62f4c2f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/longAppointments.spec.ts @@ -0,0 +1,55 @@ +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('Layout:Appointments:longAppointments(T1086079)', () => { + 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 dataSource = [{ text: 'Website Re-Design Plan', startDate: new Date('2021-02-29T01:30:00.000Z'), endDate: new Date('2021-02-29T14:30:00.000Z'), recurrenceRule: 'FREQ=DAILY' }]; + const appointmentName = 'Website Re-Design Plan'; + + test('Control should be render top part of recurrent long appointment in day view(T1086079)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { timeZone: 'America/Los_Angeles', dataSource, cellDuration: 120, views: ['day'], currentView: 'day', currentDate: new Date(2021, 2, 30), startDayHour: 2, endDayHour: 22, height: 600 }); + await testScreenshot(page, 'long-appointment-day-view-T1086079.png', { element: page.locator('.dx-scheduler-work-space') }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(0).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 29 5:30 PM - March 30 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(1).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 30 5:30 PM - March 31 6:30 AM'); + + await page.locator('.dx-scheduler-navigator-next').click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(0).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 30 5:30 PM - March 31 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(1).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 31 5:30 PM - April 1 6:30 AM'); + }); + + test('Control should be render top part of recurrent long appointment in week view(T1086079)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { timeZone: 'America/Los_Angeles', dataSource, cellDuration: 120, views: ['week'], currentView: 'week', currentDate: new Date(2021, 2, 30), startDayHour: 2, endDayHour: 22, height: 600 }); + await testScreenshot(page, 'long-appointment-week-view-T1086079.png', { element: page.locator('.dx-scheduler-work-space') }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(0).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 27 5:30 PM - March 28 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(1).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 28 5:30 PM - March 29 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(2).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 28 5:30 PM - March 29 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(3).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 29 5:30 PM - March 30 6:30 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/noSubject.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/noSubject.spec.ts new file mode 100644 index 000000000000..325c47d57b3b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/noSubject.spec.ts @@ -0,0 +1,38 @@ +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('Layout:Appointments:noSubject', () => { + 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 viewsList = ['day', 'week', 'workWeek', 'month', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth', 'agenda']; + const timelineViews = ['timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth']; + + viewsList.forEach((currentView) => { + test(`Appointment without text should display "(No subject)" in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ startDate: new Date(2021, 0, 1, 10, 30), endDate: new Date(2021, 0, 1, 12, 0), text: '' }], + views: viewsList, currentView, currentDate: new Date(2021, 0, 1), + startDayHour: 9, endDayHour: 18, height: 600, width: 600, + }); + + if (timelineViews.includes(currentView)) { + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2021, 0, 1, 10, 30)); + }); + await page.waitForTimeout(300); + } + + await testScreenshot(page, `appointment-no-subject-${currentView}.png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/recurrence.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/recurrence.spec.ts new file mode 100644 index 000000000000..22ca3d78557e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/recurrence.spec.ts @@ -0,0 +1,28 @@ +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('AppointmentForm screenshot 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'); + }); + + ['day', 'week', 'workWeek', 'month', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth', 'agenda'].forEach((currentView) => { + [true, false].forEach((rtlEnabled) => { + test(`Recurrent appointment in ${currentView} view and ${rtlEnabled ? 'rtl' : 'non-rtl'} mode`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ text: 'Long Long Long Long Long Long Long Long Long Description', startDate: new Date(2021, 0, 1, 1, 30), endDate: new Date(2021, 0, 1, 3, 0), recurrenceRule: 'FREQ=DAILY;COUNT=30' }], + currentDate: new Date(2021, 0, 4), height: 600, currentView, rtlEnabled, + }); + await testScreenshot(page, `recurrent-appointment-in-${currentView}_view-and-${rtlEnabled ? 'rtl' : 'non-rtl'}_mode.png`, { element: page.locator('.dx-scheduler') }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/two-schedulers.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/two-schedulers.spec.ts new file mode 100644 index 000000000000..7e4234974f62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/two-schedulers.spec.ts @@ -0,0 +1,51 @@ +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('Layout:Appointments:two-schedulers', () => { + 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 dragging should work properly with two dxSchedulers(T1020820)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + maxAppointmentsPerCell: 'unlimited', + dataSource: [ + { text: 'Website Re-Design Plan', startDate: new Date('2021-03-29T16:30:00.000Z'), endDate: new Date('2021-03-29T18:30:00.000Z') }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date('2021-03-29T19:00:00.000Z'), endDate: new Date('2021-03-29T20:00:00.000Z'), allDay: true }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date('2021-03-30T17:00:00.000Z'), endDate: new Date('2021-03-30T18:00:00.000Z') }, + { text: 'Final Budget Review', startDate: new Date('2021-03-30T19:00:00.000Z'), endDate: new Date('2021-03-30T20:35:00.000Z') }, + { text: 'Install New Database', startDate: new Date('2021-03-31T16:45:00.000Z'), endDate: new Date('2021-03-31T18:15:00.000Z') }, + ], + views: ['month'], currentView: 'month', currentDate: new Date(2021, 2, 29), startDayHour: 9, height: 400, + }); + await createWidget(page, 'dxScheduler', { + maxAppointmentsPerCell: 'unlimited', + dataSource: [ + { text: 'Helen', startDate: new Date('2021-03-29T16:30:00.000Z'), endDate: new Date('2021-04-29T18:30:00.000Z') }, + { text: 'Alex', startDate: new Date('2021-03-29T19:00:00.000Z'), endDate: new Date('2021-04-29T20:00:00.000Z') }, + ], + views: ['day'], currentView: 'day', currentDate: new Date(2021, 2, 29), startDayHour: 9, height: 400, + }, '#otherContainer'); + + await testScreenshot(page, 'before-dragging(T1020820).png'); + + const appointment = page.locator('#container .dx-scheduler-appointment').filter({ hasText: 'Install New Database' }); + const box = await appointment.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 - 100, { steps: 20 }); + await page.mouse.up(); + } + + await testScreenshot(page, 'after-dragging(T1020820).png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/visible.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/visible.spec.ts new file mode 100644 index 000000000000..30d70fc7b490 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/visible.spec.ts @@ -0,0 +1,32 @@ +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('Layout:Appointments:visible', () => { + 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'); + }); + + [1, 0].forEach((maxAppointmentsPerCell) => { + [true, false, undefined].forEach((visible) => { + test(`Appointments should be filtered by visible property(visible='${visible}', maxAppointmentsPerCell='${maxAppointmentsPerCell}'`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Recurrence app', roomId: [1], startDate: new Date(2021, 3, 13, 1, 30), endDate: new Date(2021, 3, 13, 2, 30), recurrenceRule: 'FREQ=DAILY', visible }, + { text: 'Simple app', roomId: [1], startDate: new Date(2021, 3, 12, 3), endDate: new Date(2021, 3, 12, 4), visible }, + ], + views: [{ type: 'week', name: 'Numeric Mode', maxAppointmentsPerCell }], + currentView: 'Numeric Mode', currentDate: new Date(2021, 3, 15), height: 600, + }); + await testScreenshot(page, `filtering-visible=${visible}-maxAppointmentsPerCell=${maxAppointmentsPerCell}.png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizes.spec.ts new file mode 100644 index 000000000000..6b470bed2d5a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizes.spec.ts @@ -0,0 +1,87 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Cell Sizes', () => { + 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 views = [{ + type: 'week', + groupOrientation: 'horizontal', + }, { + type: 'month', + groupOrientation: 'horizontal', + }, { + type: 'timelineWeek', + groupOrientation: 'vertical', + }, { + type: 'timelineMonth', + groupOrientation: 'vertical', + }]; + + const createSchedulerOnPage = async ( + page: any, + additionalProps: Record, + ): Promise => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [], + crossScrollingEnabled: true, + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority 1', id: 0, color: '#24ff50' }, + { text: 'Low Priority 2', id: 1, color: '#ff9747' }, + { text: 'Low Priority 3', id: 2, color: '#24ff50' }, + { text: 'High Priority 1', id: 3, color: '#ff9747' }, + { text: 'High Priority 2', id: 4, color: '#24ff50' }, + { text: 'High Priority 3', id: 5, color: '#ff9747' }, + ], + label: 'Priority', + }], + ...additionalProps, + }); + }; + + test('Cell sizes customization should work', async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-cell-sizes-vertical { height: 150px; } #container .dx-scheduler-cell-sizes-horizontal { width: 150px; }'); + await createSchedulerOnPage(page, { views }); + + for (const { type } of views) { + await page.evaluate((viewType: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', viewType); + }, type); + + await testScreenshot(page, `custom-cell-sizes-in-${type}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + } + }); + + test('Cell sizes customization should work when all-day panel is enabled', async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-cell-sizes-vertical { height: 150px; } #container .dx-scheduler-cell-sizes-horizontal { width: 150px; }'); + await createSchedulerOnPage(page, { + views, + showAllDayPanel: true, + currentView: 'week', + }); + + await testScreenshot(page, 'custom-cell-sizes-with-all-day-panel-in-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizesCss.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizesCss.spec.ts new file mode 100644 index 000000000000..338153fc6349 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizesCss.spec.ts @@ -0,0 +1,64 @@ +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('Scheduler: Layout Customization: Cell Sizes CSS classes', () => { + 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 HORIZONTAL_SIZE_CLASSNAME = 'dx-scheduler-cell-sizes-horizontal'; + const VERTICAL_SIZE_CLASSNAME = 'dx-scheduler-cell-sizes-vertical'; + const CELL_SIZE_CSS = ` +#container .${HORIZONTAL_SIZE_CLASSNAME} { width: 300px; } +#container .${VERTICAL_SIZE_CLASSNAME} { height: 300px; } +`; + + const cases = [ + { views: ['day'], crossScrollingEnabled: false, expected: { width: 'skipCheck', height: 300, hasHorizontalClass: false, hasVerticalClass: true } }, + { views: ['day'], crossScrollingEnabled: true, expected: { width: 'skipCheck', height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + { views: ['week', 'workWeek', 'month'], crossScrollingEnabled: false, expected: { width: 'skipCheck', height: 300, hasHorizontalClass: false, hasVerticalClass: true } }, + { views: ['week', 'workWeek', 'month'], crossScrollingEnabled: true, expected: { width: 300, height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + { views: ['timelineDay', 'timelineWeek', 'timelineMonth'], crossScrollingEnabled: false, expected: { width: 300, height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + { views: ['timelineDay', 'timelineWeek', 'timelineMonth'], crossScrollingEnabled: true, expected: { width: 300, height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + ]; + + cases.forEach(({ views, expected, crossScrollingEnabled }) => { + views.forEach((view) => { + test.skip(`Cells should have correct sizes and css classes (view:${view}, crossScrolling:${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: view, + currentDate: '2024-01-01', + crossScrollingEnabled, + }); + + const cell = page.locator('.dx-scheduler-date-table-cell').first(); + const box = await cell.boundingBox(); + const hasHorizontalClass = await cell.evaluate( + (el, cls) => el.classList.contains(cls), HORIZONTAL_SIZE_CLASSNAME, + ); + const hasVerticalClass = await cell.evaluate( + (el, cls) => el.classList.contains(cls), VERTICAL_SIZE_CLASSNAME, + ); + + if (typeof expected.width === 'number' && box) { + expect(box.width).toBe(expected.width); + } + if (box) { + expect(box.height).toBe(expected.height); + } + expect(hasHorizontalClass).toBe(expected.hasHorizontalClass); + expect(hasVerticalClass).toBe(expected.hasVerticalClass); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/groupPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/groupPanel.spec.ts new file mode 100644 index 000000000000..56b11b0dbd3f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/groupPanel.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Group Panel', () => { + 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 views = [ + { type: 'week', groupOrientation: 'vertical' }, + { type: 'month', groupOrientation: 'vertical' }, + { type: 'timelineWeek', groupOrientation: 'vertical' }, + { type: 'timelineMonth', groupOrientation: 'vertical' }, + ]; + + [false, true].forEach((crossScrollingEnabled) => { + test(`Group panel customization should work (crossScrollingEnabled=${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-group-header { width: 200px;}'); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [{ + text: 'Create Report on Customer Feedback', + startDate: new Date(2021, 4, 1, 14), + endDate: new Date(2021, 4, 1, 15), + priorityId: 0, + }, { + text: 'Review Customer Feedback Report', + startDate: new Date(2021, 4, 9, 9, 30), + endDate: new Date(2021, 4, 9, 11), + priorityId: 0, + }], + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', + }], + views, + crossScrollingEnabled, + }); + + for (const view of views) { + await page.evaluate((viewType: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', viewType); + }, view.type); + + await testScreenshot( + page, + `custom-group-panel-in-${view.type}-cross-scrolling=${crossScrollingEnabled}.png`, + { element: page.locator('.dx-scheduler') }, + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/headerPanel.spec.ts new file mode 100644 index 000000000000..bbeb4a667e9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/headerPanel.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Header Panel', () => { + 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 views = [ + { type: 'week', groupOrientation: 'horizontal' }, + { type: 'month', groupOrientation: 'horizontal' }, + { type: 'timelineWeek', groupOrientation: 'horizontal' }, + { type: 'timelineMonth', groupOrientation: 'horizontal' }, + ]; + + [false, true].forEach((crossScrollingEnabled) => { + test(`Header panel customization should work (crossScrollingEnabled=${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-group-header, #container .dx-scheduler-header-panel-cell { height: 100px; }'); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [{ + text: 'Create Report on Customer Feedback', + startDate: new Date(2021, 4, 11, 14), + endDate: new Date(2021, 4, 11, 15), + priorityId: 0, + }, { + text: 'Review Customer Feedback Report', + startDate: new Date(2021, 4, 9, 9, 30), + endDate: new Date(2021, 4, 9, 11), + priorityId: 0, + }], + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', + }], + views, + crossScrollingEnabled, + }); + + for (const view of views) { + await page.evaluate((viewType: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', viewType); + }, view.type); + + await testScreenshot( + page, + `custom-header-panel-in-${view.type}-cross-scrolling=${crossScrollingEnabled}.png`, + { element: page.locator('.dx-scheduler') }, + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/timePanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/timePanel.spec.ts new file mode 100644 index 000000000000..276ea4c0797e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/timePanel.spec.ts @@ -0,0 +1,81 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Time Panel', () => { + 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'); + }); + + [false, true].forEach((crossScrollingEnabled) => { + ['week', 'agenda'].forEach((view) => { + test(`Time panel customization should work in ${view} view (crossScrollingEnabled=${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-time-panel { width: 150px;}'); + + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [{ + text: 'Create Report on Customer Feedback', + startDate: new Date('2021-05-11T22:15:00.000Z'), + endDate: new Date('2021-05-12T00:30:00.000Z'), + }, { + text: 'Review Customer Feedback Report', + startDate: new Date('2021-05-11T23:15:00.000Z'), + endDate: new Date('2021-05-12T01:30:00.000Z'), + }, { + text: 'Customer Feedback Report Analysis', + startDate: new Date('2021-05-12T16:30:00.000Z'), + endDate: new Date('2021-05-12T17:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + }, { + text: 'Prepare Shipping Cost Analysis Report', + startDate: new Date('2021-05-12T19:30:00.000Z'), + endDate: new Date('2021-05-12T20:30:00.000Z'), + }, { + text: 'Provide Feedback on Shippers', + startDate: new Date('2021-05-12T21:15:00.000Z'), + endDate: new Date('2021-05-12T23:00:00.000Z'), + }, { + text: 'Select Preferred Shipper', + startDate: new Date('2021-05-13T00:30:00.000Z'), + endDate: new Date('2021-05-13T03:00:00.000Z'), + }, { + text: 'Complete Shipper Selection Form', + startDate: new Date('2021-05-13T15:30:00.000Z'), + endDate: new Date('2021-05-13T17:00:00.000Z'), + }, { + text: 'Upgrade Server Hardware', + startDate: new Date('2021-05-13T19:00:00.000Z'), + endDate: new Date('2021-05-13T21:15:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + }, { + text: 'Upgrade Personal Computers', + startDate: new Date('2021-05-13T21:45:00.000Z'), + endDate: new Date('2021-05-13T23:30:00.000Z'), + }], + views: [view], + currentView: view, + crossScrollingEnabled, + }); + + await testScreenshot( + page, + `custom-time-panel-in-${view}-cross-scrolling=${crossScrollingEnabled}.png`, + { element: '#container' }, + ); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/allDay.spec.ts new file mode 100644 index 000000000000..b2dc26bd5029 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/allDay.spec.ts @@ -0,0 +1,133 @@ +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('Layout:AppointmentForm:AllDay', () => { + 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('Start and end dates should be reflect the current day(appointment is already available case)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + .click(scheduler.appointmentTooltip.getListItem('Text').element); + + await testScreenshot(page, 'appointment-form-before-click-all-day.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'appointment-form-after-click-all-day.png'); + + await (appointmentPopup.doneButton).click(); + + await testScreenshot(page, 'all-day-appointment-on-tables.png'); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + .click(scheduler.appointmentTooltip.getListItem('Text').element); + + await testScreenshot(page, 'appointment-form-after-render-on-table.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'appointment-form-after-switch-off-all-day.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2021, 3, 28, 10), + endDate: new Date(2021, 3, 28, 12), + }], + editing: { legacyForm: true }, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 600, + }); +}); + +test('Start and end dates should be reflect the current day(create new appointment case)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(3).dblclick()); + + await testScreenshot(page, 'new-appointment-form-before-click-all-day.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'new-appointment-form-after-click-all-day.png'); + + await (appointmentPopup.doneButton).click(); + + await testScreenshot(page, 'new-all-day-appointment-on-tables.png'); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: '' }).click().element) + .click(scheduler.appointmentTooltip.getListItem('').element); + + await testScreenshot(page, 'new-appointment-form-after-render-on-table.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'new-appointment-form-after-switch-off-all-day.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + editing: { legacyForm: true }, + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 600, + }); +}); + +test('StartDate and endDate should have correct type after "allDay" and "repeat" option are changed (T1002864)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0).dblclick()); + + await testScreenshot(page, + 'form-before-change-allday-and-reccurence-options.png', + { element: appointmentPopup.content }, + ); + + await (appointmentPopup.allDayElement).click() + .click(appointmentPopup.recurrenceElement); + + await testScreenshot(page, + 'form-after-change-allday-and-reccurence-options.png', + { element: appointmentPopup.content }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 1, 1), + editing: { legacyForm: true }, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/common.spec.ts new file mode 100644 index 000000000000..315727121a78 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/common.spec.ts @@ -0,0 +1,48 @@ +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('AppointmentForm screenshot 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'); + }); + +); + +// visual: generic.light +// visual: fluent.blue.light +// visual: material.blue.light +test('Appointemt form tests', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 1, 1), + editing: { legacyForm: true }, + // --- test --- +// Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0).dblclick()); + + await testScreenshot(page, 'initial-form.png', { + element: appointmentPopup.content, + }); + + await (appointmentPopup.allDayElement).click() + .click(appointmentPopup.recurrenceElement); + + await testScreenshot(page, 'allday-and-reccurence-form.png', { + element: appointmentPopup.content, + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/integerFormatNumberBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/integerFormatNumberBox.spec.ts new file mode 100644 index 000000000000..5221847aa8bc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/integerFormatNumberBox.spec.ts @@ -0,0 +1,49 @@ +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('Layout:AppointmentForm:IntegerFormatNumberBox', () => { + 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('dxNumberBox should not allow to enter not integer chars(T1002864)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); + + await t + .typeText(appointmentPopup.repeatEveryElement, '.,2', { speed: 0.5 }); + + await testScreenshot(page, 'dx-number-boxes-not-integer-chars.png', { + element: appointmentPopup.content, + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 3, 26, 10), + endDate: new Date(2021, 3, 26, 11), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;UNTIL=20220114T205959Z', + }], + editing: { legacyForm: true }, + views: ['day', 'week', 'workWeek', 'month'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 600, + recurrenceEditMode: 'series', +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/base/resources.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/base/resources.spec.ts new file mode 100644 index 000000000000..586fd3e78c75 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/base/resources.spec.ts @@ -0,0 +1,106 @@ +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 resourceDataSource = [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', +}]; + +const createDataSetForScreenShotTests = (): Record[] => { + const result: any[] = []; + for (let day = 1; day < 25; day++) { + result.push({ + text: '1 appointment', startDate: new Date(2020, 6, day, 0), + endDate: new Date(2020, 6, day, 1), priorityId: 0, + }); + result.push({ + text: '2 appointment', startDate: new Date(2020, 6, day, 1), + endDate: new Date(2020, 6, day, 2), priorityId: 1, + }); + result.push({ + text: '3 appointment', startDate: new Date(2020, 6, day, 3), + endDate: new Date(2020, 6, day, 5), allDay: true, priorityId: 0, + }); + } + return result; +}; + +test.describe('Scheduler: Resources layout', () => { + 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'); + }); + + [undefined, resourceDataSource].forEach((resourcesValue) => { + ['agenda', 'day', 'week', 'month', 'workWeek'].forEach((view) => { + test(`Base views layout test with resources(view='${view}'), resource=${!!resourcesValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + views: [view], + currentView: view, + resources: resourcesValue, + height: 600, + }); + + await page.locator('.dx-scheduler-header').click(); + await page.locator('.dx-scheduler-appointment').filter({ hasText: '1 appointment' }).first().click(); + await expect(page.locator('.dx-tooltip-appointment-item')).toBeVisible(); + + await testScreenshot(page, `resource(view=${view}-resource=${!!resourcesValue}).png`); + }); + }); + }); + + [undefined, resourceDataSource].forEach((resourcesValue) => { + ['timelineDay', 'timelineWeek', 'timelineMonth', 'timelineWorkWeek'].forEach((view) => { + test(`Timeline views layout test with resources(view='${view}'), resource=${!!resourcesValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + views: [view], + currentView: view, + resources: resourcesValue, + height: 600, + }); + + await page.locator('.dx-scheduler-header').click(); + await page.locator('.dx-scheduler-appointment').filter({ hasText: '1 appointment' }).first().click(); + await expect(page.locator('.dx-tooltip-appointment-item')).toBeVisible(); + + await testScreenshot(page, `resource(view=${view}-resource=${!!resourcesValue}).png`); + }); + }); + }); + + test('Scheduler should have correct height in month view (T927862)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['month'], + currentView: 'month', + height: 800, + }); + + const result = await page.evaluate(() => { + const dateTable = document.querySelector('.dx-scheduler-date-table'); + const scrollable = document.querySelector('.dx-scheduler-date-table-scrollable'); + if (!dateTable || !scrollable) return { match: false }; + const dtRect = dateTable.getBoundingClientRect(); + const scRect = scrollable.getBoundingClientRect(); + return { match: Math.abs(dtRect.bottom - scRect.bottom) < 1 }; + }); + + expect(result.match).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/groups/groups.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/groups/groups.spec.ts new file mode 100644 index 000000000000..6be648015097 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/groups/groups.spec.ts @@ -0,0 +1,94 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +const resourceDataSource = [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', +}]; + +const createDataSetForScreenShotTests = (): Record[] => { + const result: any[] = []; + for (let day = 1; day < 25; day++) { + result.push({ + text: '1 appointment', startDate: new Date(2020, 6, day, 0), + endDate: new Date(2020, 6, day, 1), priorityId: 0, + }); + result.push({ + text: '2 appointment', startDate: new Date(2020, 6, day, 1), + endDate: new Date(2020, 6, day, 2), priorityId: 1, + }); + result.push({ + text: '3 appointment', startDate: new Date(2020, 6, day, 3), + endDate: new Date(2020, 6, day, 5), allDay: true, priorityId: 0, + }); + } + return result; +}; + +test.describe('Scheduler: Groups layout', () => { + 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'); + }); + + ['vertical', 'horizontal'].forEach((groupOrientation) => { + ['agenda', 'day', 'week', 'workWeek', 'month'].forEach((view) => { + test(`Base views layout test with groups(view='${view}', groupOrientation=${groupOrientation})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + startDayHour: 0, + endDayHour: 4, + views: [{ + type: view, + name: view, + groupOrientation, + }], + currentView: view, + crossScrollingEnabled: true, + resources: resourceDataSource, + groups: ['priorityId'], + height: 700, + }); + + await testScreenshot(page, `groups(view=${view}-orientation=${groupOrientation}).png`); + }); + }); + }); + + ['vertical', 'horizontal'].forEach((groupOrientation) => { + ['timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth'].forEach((view) => { + test(`Timeline views layout test with groups(view='${view}', groupOrientation=${groupOrientation})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + startDayHour: 0, + endDayHour: 4, + views: [{ + type: view, + name: view, + groupOrientation, + }], + currentView: view, + crossScrollingEnabled: true, + resources: resourceDataSource, + groups: ['priorityId'], + height: 700, + }); + + await testScreenshot(page, `groups(view=${view}-orientation=${groupOrientation}).png`); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts new file mode 100644 index 000000000000..7cd12100ca13 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts @@ -0,0 +1,45 @@ +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:appointmentTemplate', () => { + 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'); + }); + + ['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'agenda'].forEach((currentView) => { + test(`appointmentTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { + await page.evaluate((view: string) => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [ + { startDate: new Date(2017, 4, 21, 0, 30), endDate: new Date(2017, 4, 21, 2, 30) }, + { startDate: new Date(2017, 4, 22, 0, 30), endDate: new Date(2017, 4, 22, 2, 30) }, + { startDate: new Date(2017, 4, 23, 0, 30), endDate: new Date(2017, 4, 23, 2, 30) }, + { startDate: new Date(2017, 4, 24, 0, 30), endDate: new Date(2017, 4, 24, 2, 30) }, + { startDate: new Date(2017, 4, 25, 0, 30), endDate: new Date(2017, 4, 25, 2, 30) }, + { startDate: new Date(2017, 4, 26, 0, 30), endDate: new Date(2017, 4, 26, 2, 30) }, + { startDate: new Date(2017, 4, 27, 0, 30), endDate: new Date(2017, 4, 27, 2, 30) }, + ], + views: [view], currentView: view, currentDate: new Date(2017, 4, 25), + appointmentTemplate(appointment: any) { + const result = $('
'); + const startDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.startDate }); + const endDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.endDate }); + result.append(startDateBox, endDateBox); + return result; + }, + height: 600, + }); + }, currentView); + await testScreenshot(page, `appointment-template-currentView=${currentView}.png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplateTargetedData.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplateTargetedData.spec.ts new file mode 100644 index 000000000000..7ff867618a20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplateTargetedData.spec.ts @@ -0,0 +1,280 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, generateOptionMatrix } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:appointmentTemplate:targetedData', () => { + 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'); + }); + +Appointment, + Orientation, + ScrollMode, + ViewType, +} from 'devextreme/ui/scheduler'; + +); + +const getResourceCount = ( + viewType: ViewType, + scrollMode: ScrollMode, + groupOrientation: Orientation, +): number => { + if ( + (viewType === 'workWeek' + || viewType === 'timelineWorkWeek' + || viewType === 'week' + || viewType === 'timelineWeek') + && (scrollMode === 'standard' && groupOrientation === 'horizontal') + ) { + return 2; + } + + if (scrollMode === 'standard') { + return 10; + } + + return 30; +}; + +const getGroupAppointmentDates = (viewType: ViewType): Date[] => { + const isWorkWeek = viewType === 'workWeek' || viewType === 'timelineWorkWeek'; + + if (isWorkWeek || viewType === 'week' || viewType === 'timelineWeek') { + const [ + dayCount, + startDate, + ] = isWorkWeek + ? [5, new Date(2024, 0, 1, 8)] + : [7, new Date(2023, 11, 31, 8)]; + + return Array.from( + { length: 12 * dayCount }, + (_, index) => { + const result = new Date(startDate); + result.setDate(result.getDate() + Math.floor(index / 12)); + result.setHours(result.getHours() + (index % 12)); + return result; + }, + ); + } + + if (viewType === 'month' || viewType === 'timelineMonth') { + return Array.from( + { length: 31 }, + (_, index) => { + const result = new Date(2024, 0, 1, 8); + result.setDate(result.getDate() + index); + return result; + }, + ); + } + + const startDate = viewType === 'agenda' + ? new Date(2024, 0, 1, 8) + : new Date(2024, 0, 2, 8); + + return Array.from( + { length: 12 }, + (_, index) => { + const result = new Date(startDate); + result.setHours(result.getHours() + index); + return result; + }, + ); +}; + +const viewTypes: ViewType[] = [ + 'agenda', + 'day', + 'week', + 'workWeek', + 'month', + 'timelineDay', + 'timelineWeek', + 'timelineWorkWeek', + 'timelineMonth', +]; + +const groupOrientations: Orientation[] = ['horizontal', 'vertical']; +const scrollModes: ScrollMode[] = ['standard', 'virtual']; +const rtlEnabledOptions: boolean[] = [false, true]; + +const testOptions = generateOptionMatrix({ + viewType: viewTypes, + groupOrientation: groupOrientations, + scrollMode: scrollModes, + rtlEnabled: rtlEnabledOptions, +}, [ + // Not supported + { + viewType: 'agenda', + scrollMode: 'virtual', + }, + // Not supported + { + viewType: 'agenda', + groupOrientation: 'horizontal', + }, + { + viewType: 'day', + groupOrientation: 'vertical', + scrollMode: 'standard', + }, + { + viewType: 'week', + groupOrientation: 'vertical', + scrollMode: 'standard', + }, + { + viewType: 'workWeek', + groupOrientation: 'vertical', + scrollMode: 'standard', + }, + { + viewType: 'day', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'week', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'workWeek', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'month', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'timelineDay', + groupOrientation: 'vertical', + rtlEnabled: true, + }, + { + viewType: 'timelineWeek', + groupOrientation: 'vertical', + rtlEnabled: true, + }, + { + viewType: 'timelineWorkWeek', + groupOrientation: 'vertical', + rtlEnabled: true, + }, + { + viewType: 'timelineMonth', + groupOrientation: 'vertical', + rtlEnabled: true, + }, +]); + +// NOTE: no assertions are present, checking but not throwing an error in a template function +testOptions.forEach(({ + viewType, + groupOrientation, + scrollMode, + rtlEnabled, +}) => { + test(`targetedAppointmentData should be correct with groups (viewType="${viewType}", groupOrientation="${groupOrientation}", scrollMode="${scrollMode}", rtlEnabled="${rtlEnabled}") (T1205120)`, async (t) => { + const currentDate = new Date(2024, 0, 2); + const HOUR = 1000 * 60 * 60; + const resourceCount = getResourceCount(viewType, scrollMode, groupOrientation); + const appointmentDates = getGroupAppointmentDates(viewType); + + const datesToCheck = [ + appointmentDates[0], + appointmentDates[Math.floor(appointmentDates.length / 3)], + appointmentDates[appointmentDates.length - 1], + ]; + + const groupsToCheck = [ + { groupId: 0 }, + { groupId: Math.floor(resourceCount / 3) }, + { groupId: resourceCount - 1 }, + ]; + + const resourceDataSource = Array.from({ length: resourceCount }, (_, index) => ({ + id: index, + text: `Resource ${index}`, + })); + + const appointments = resourceDataSource.reduce((acc, resource) => acc.concat( + appointmentDates + .map((date) => ({ + text: resource.text, + startDate: date, + endDate: new Date(date.getTime() + HOUR / 2), + groupId: resource.id, + })), + ), []); + + await createWidget(page, 'dxScheduler', { + rtlEnabled, + height: 600, + width: 800, + currentDate, + startDayHour: 8, + endDayHour: 20, + scrolling: { + mode: scrollMode, + }, + groups: ['groupId'], + views: [ + { + type: viewType, + groupOrientation, + }, + ], + currentView: viewType, + dataSource: appointments, + resources: [ + { + fieldExpr: 'groupId', + allowMultiple: true, + dataSource: resourceDataSource, + label: 'Employees', + displayExpr: 'id', + }, + ], + appointmentTemplate(model, _, element) { + const { groupId: targetedId } = model.targetedAppointmentData; + const { groupId } = model.appointmentData; + + if (groupId !== targetedId[0]) { + throw new Error('Group ID and targeted ID are mismatched'); + } + + element.append(`tid[${targetedId}] gid[${groupId}]`); + return element; + }, + }); + + if (scrollMode !== 'virtual') { + return; + } + + const scrollOptions = generateOptionMatrix({ + date: datesToCheck, + group: groupsToCheck, + }); + + // eslint-disable-next-line no-restricted-syntax + for (const { date, group } of scrollOptions) { + await scrollToDate(date, group); + await await page.waitForTimeout(50); + } + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts new file mode 100644 index 000000000000..202f29e9b62a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:CellTemplate', () => { + 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'); + }); + + ['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'timelineMonth'].forEach((currentView) => { + test(`dataCellTemplate and dateCellTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { + await page.evaluate((view: string) => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [], views: [view], currentView: view, currentDate: new Date(2017, 4, 25), + showAllDayPanel: false, + dataCellTemplate(itemData: any) { return ($('
') as any).dxDateBox({ type: 'time', value: itemData.startDate }); }, + dateCellTemplate(itemData: any) { return ($('
') as any).dxTextBox({ value: new Intl.DateTimeFormat('en-US').format(itemData.date) }); }, + height: 600, + }); + }, currentView); + await testScreenshot(page, `data-cell-template-currentView=${currentView}.png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); + + test('[T1251590] Async dateCellTemplate should be rendered only once', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ startDate: '2024-01-01T01:00:00', endDate: '2024-01-01T02:00:00', allDay: true }], + dateCellTemplate(_: any, __: any, itemElement: any) { setTimeout(() => { itemElement.append('TEST'); }, 0); }, + currentDate: '2024-01-01', currentView: 'week', + }); + }); + await page.waitForTimeout(100); + expect(await page.locator('.dx-scheduler-header-panel-cell').nth(0).textContent()).toBe('TEST'); + }); + + test('[T1251590] Async dateCellTemplate should be rendered only once if has reference props (grouping)', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ startDate: '2024-01-01T01:00:00', endDate: '2024-01-01T02:00:00', allDay: true }], + groups: ['groupId'], + resources: [{ label: 'group', fieldExpr: 'groupId', dataSource: [{ text: 'A', id: 0, color: '#00af2c' }] }], + dateCellTemplate(_: any, __: any, itemElement: any) { setTimeout(() => { itemElement.append('TEST'); }, 0); }, + currentDate: '2024-01-01', currentView: 'week', + }); + }); + await page.waitForTimeout(100); + expect(await page.locator('.dx-scheduler-header-panel-cell').nth(0).textContent()).toBe('TEST'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts new file mode 100644 index 000000000000..eb39d4f747cf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts @@ -0,0 +1,36 @@ +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:appointmentTooltipTemplate', () => { + 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('appointmentTooltipTemplate layout should be rendered right', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ startDate: new Date(2017, 4, 25, 0, 30), endDate: new Date(2017, 4, 25, 2, 30) }], + views: ['workWeek'], currentView: 'workWeek', currentDate: new Date(2017, 4, 25), + appointmentTooltipTemplate(appointment: any) { + const result = $('
'); + const startDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.startDate }); + const endDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.endDate }); + result.append(startDateBox, endDateBox); + return result; + }, + height: 600, + }); + }); + await page.locator('.dx-scheduler-appointment').first().click(); + await testScreenshot(page, 'appointment-tooltip-template.png', { element: page.locator('.dx-scheduler') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/currentTimeIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/currentTimeIndicator.spec.ts new file mode 100644 index 000000000000..ac623716506d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/currentTimeIndicator.spec.ts @@ -0,0 +1,126 @@ +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('Scheduler: Current Time Indication', () => { + 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('Current time indicator should be placed correctly when there are many groups and orientation is horizontal', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentDate: new Date(2021, 7, 1), + height: 400, + width: 700, + startDayHour: 5, + indicatorTime: new Date(2021, 7, 1, 6), + currentView: 'day', + views: ['day', 'week'], + groups: ['groupId'], + resources: [{ + fieldExpr: 'groupId', + label: 'group', + dataSource: [ + { text: 'Group 1', id: 1 }, + { text: 'Group 2', id: 2 }, + { text: 'Group 3', id: 3 }, + { text: 'Group 4', id: 4 }, + { text: 'Group 5', id: 5 }, + { text: 'Group 6', id: 6 }, + ], + }], + }); + + for (const view of ['day', 'week']) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `current-time-indicator-in-${view}-with-many-groups.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + const TIMELINE_VIEWS = ['timelineDay', 'timelineWeek', 'timelineMonth']; + + [ + 'none', + 'vertical', + 'horizontal', + ].forEach((grouping) => { + [ + { view: 'day', cellDuration: 240 }, + { view: 'week', cellDuration: 240 }, + { view: 'timelineDay', cellDuration: 360 }, + { view: 'timelineWeek', cellDuration: 360 }, + { view: 'timelineMonth', cellDuration: 60 }, + ].forEach(({ view, cellDuration }) => { + [ + [0, 24], + [6, 18], + ].forEach(([startDayHour, endDayHour]) => { + [ + '2023-12-03T00:00:00', + '2023-12-03T06:30:00', + '2023-12-03T12:00:00', + '2023-12-03T17:30:00', + '2023-12-03T23:59:59', + ].forEach((indicatorTime) => { + if (grouping === 'horizontal' && TIMELINE_VIEWS.includes(view)) { + return; + } + if (view === 'timelineMonth' && startDayHour !== 0 && endDayHour !== 24) { + return; + } + + test(`Current time indicator should be rendered correctly (view: ${view}, now: ${indicatorTime}, grouping: ${grouping}, startDayHour: ${startDayHour}, endDayHour: ${endDayHour})`, async ({ page }) => { + const additionalOptions = grouping === 'none' + ? { + views: [{ type: view, name: 'TEST_VIEW' }], + } + : { + views: [{ type: view, name: 'TEST_VIEW', groupOrientation: grouping }], + groups: ['any'], + resources: [{ + fieldExpr: 'any', + dataSource: [ + { text: 'Group_0', id: 0 }, + { text: 'Group_1', id: 1 }, + ], + }], + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'TEST_VIEW', + shadeUntilCurrentTime: true, + currentDate: indicatorTime, + startDayHour, + endDayHour, + indicatorTime, + cellDuration, + ...additionalOptions, + }); + + await testScreenshot( + page, + `current-time-indicator_${view}_${indicatorTime.replace(/:/g, '-')}_g-${grouping}_${startDayHour}_${endDayHour}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shader.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shader.spec.ts new file mode 100644 index 000000000000..74e4209d7295 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shader.spec.ts @@ -0,0 +1,123 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Current Time Indication: Shader', () => { + 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 views = ['day', 'week', 'timelineDay', 'timelineWeek', 'timelineMonth']; + const style = ` +.dx-scheduler-date-time-shader-top::before, +.dx-scheduler-date-time-shader-bottom::before, +.dx-scheduler-timeline .dx-scheduler-date-time-shader::before, +.dx-scheduler-date-time-shader-all-day { + background-color: red !important; +}`; + + const baseOptions = { + dataSource: [], + currentDate: new Date(2021, 7, 1), + height: 400, + width: 700, + startDayHour: 5, + indicatorTime: new Date(2021, 7, 1, 6), + currentView: 'day', + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', + }], + shadeUntilCurrentTime: true, + }; + + [false, true].forEach((crossScrollingEnabled) => { + test(`Shader should be displayed correctly when crossScrollingEnabled=${crossScrollingEnabled}`, async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + await createWidget(page, 'dxScheduler', { + ...baseOptions, + views, + crossScrollingEnabled, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `shader-in-${view}-crossScrolling=${crossScrollingEnabled}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Shader should be displayed correctly when crossScrollingEnabled=${crossScrollingEnabled} and horizontal grouping is used`, async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + await createWidget(page, 'dxScheduler', { + ...baseOptions, + views: [ + { type: 'day', groupOrientation: 'horizontal' }, + { type: 'week', groupOrientation: 'horizontal' }, + { type: 'tiemlineDay', groupOrientation: 'horizontal' }, + { type: 'timelineWeek', groupOrientation: 'horizontal' }, + { type: 'timelineMonth', groupOrientation: 'horizontal' }, + ], + crossScrollingEnabled, + groups: ['priorityId'], + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `shader-in-${view}-crossScrolling=${crossScrollingEnabled}-horizontal-grouping.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Shader should be displayed correctly when crossScrollingEnabled=${crossScrollingEnabled} and vertical grouping is used`, async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + await createWidget(page, 'dxScheduler', { + ...baseOptions, + views: [ + { type: 'day', groupOrientation: 'vertical' }, + { type: 'week', groupOrientation: 'vertical' }, + { type: 'tiemlineDay', groupOrientation: 'vertical' }, + { type: 'timelineWeek', groupOrientation: 'vertical' }, + { type: 'timelineMonth', groupOrientation: 'vertical' }, + ], + crossScrollingEnabled, + groups: ['priorityId'], + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `shader-in-${view}-crossScrolling=${crossScrollingEnabled}-vertical-grouping.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shaderVirtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shaderVirtualScrolling.spec.ts new file mode 100644 index 000000000000..ae978f6f6431 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shaderVirtualScrolling.spec.ts @@ -0,0 +1,91 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Current Time Indication: Shader with Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 2560, height: 600 }); + 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 style = ` +.dx-scheduler-date-time-shader-top::before, +.dx-scheduler-date-time-shader-bottom::before, +.dx-scheduler-timeline .dx-scheduler-date-time-shader::before, +.dx-scheduler-date-time-shader-all-day { + background-color: red !important; +}`; + + const resources = [ + { text: 'Room 1', id: 1, color: '#cb6bb2' }, + { text: 'Room 2', id: 2, color: '#56ca85' }, + { text: 'Room 3', id: 3, color: '#1e90ff' }, + { text: 'Room 4', id: 4, color: '#ff9747' }, + { text: 'Room 5', id: 5, color: '#ff6a00' }, + { text: 'Room 6', id: 6, color: '#ffc0cb' }, + ]; + + test('Should render shader correct with virtual scrolling without current time indicator', async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'week', + views: ['week'], + groups: ['roomId'], + resources: [{ fieldExpr: 'roomId', dataSource: resources, label: 'Room' }], + startDayHour: 8, + endDayHour: 18, + currentDate: new Date(2025, 9, 15), + height: 400, + shadeUntilCurrentTime: true, + scrolling: { mode: 'virtual' }, + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-start.png'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo( + new Date(2025, 9, 15, 17, 30), { roomId: 6 }, + ); + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-end.png'); + }); + + test('Should render shader correctly with virtual scrolling and current time indicator', async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'week', + views: ['week'], + groups: ['roomId'], + resources: [{ fieldExpr: 'roomId', dataSource: resources, label: 'Room' }], + startDayHour: 8, + endDayHour: 18, + currentDate: new Date(2025, 9, 15), + indicatorTime: new Date(2025, 9, 15, 17, 30), + height: 400, + shadeUntilCurrentTime: true, + scrolling: { mode: 'virtual' }, + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-start-with-current-time-indicator.png'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo( + new Date(2025, 9, 15, 17, 30), { roomId: 6 }, + ); + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-end-with-current-time-indicator.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts new file mode 100644 index 000000000000..dcbd468c04e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts @@ -0,0 +1,39 @@ +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('Scheduler: View with cross-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('Scrollable synchronization should work after changing current date (T1027231)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: 'week', name: 'Horizontal Grouping', groupOrientation: 'horizontal', cellDuration: 30, intervalCount: 2 }], + currentView: 'Horizontal Grouping', crossScrollingEnabled: true, currentDate: new Date(2021, 3, 21), + groups: ['priorityId'], + resources: [{ fieldExpr: 'priorityId', allowMultiple: false, dataSource: [{ text: 'Low Priority', id: 1, color: '#1e90ff' }, { text: 'High Priority', id: 2, color: '#ff9747' }], label: 'Priority' }], + height: 600, + }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2021, 4, 5)); }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2021, 4, 15), { priorityId: 2 }); }); + await testScreenshot(page, 'cross-scrolling-sync.png', { element: page.locator('.dx-scheduler-work-space') }); + }); + + test('Scrollable should be prepared correctly after change visibility (T1032171)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], views: ['timelineMonth'], currentView: 'timelineMonth', currentDate: new Date(2021, 1, 2), + firstDayOfWeek: 0, startDayHour: 8, endDayHour: 20, cellDuration: 60, visible: false, height: 400, + }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').option('visible', true); }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2021, 1, 12)); }); + await testScreenshot(page, 'cross-scrolling-sync-visibility.png', { element: page.locator('.dx-scheduler-work-space') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts new file mode 100644 index 000000000000..ee337653d49c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts @@ -0,0 +1,33 @@ +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('Layout:Views:Day:AllDay', () => { + 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'); + }); + + [1, 2].forEach((intervalCount) => { + ['horizontal', 'vertical'].forEach((groupOrientation) => { + [true, false].forEach((showAllDayPanel) => { + test(`Day view with interval and crossScrollingEnabled(groupOrientation='${groupOrientation}', showAllDayPanel='${showAllDayPanel}', intervalCount='${intervalCount}') layout test`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + resources: [{ fieldExpr: 'roomId', dataSource: [{ text: 'Room 1', id: 1 }, { text: 'Room 2', id: 2 }], label: 'Room' }], + dataSource: [], views: [{ name: 'dayView', type: 'day', intervalCount, groupOrientation }], + currentView: 'dayView', currentDate: new Date(2021, 2, 25), height: 600, + groups: ['roomId'], showAllDayPanel, crossScrollingEnabled: true, + }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().option('useNative', true); }); + await testScreenshot(page, `day-orientation=${groupOrientation}-allDay=${showAllDayPanel}-interval=${intervalCount}.png`, { element: page.locator('.dx-scheduler') }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts new file mode 100644 index 000000000000..03d77d985a11 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts @@ -0,0 +1,21 @@ +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('Scheduler: View with first day of week', () => { + 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('WorkWeek should generate correct start view date', async ({ page }) => { + await createWidget(page, 'dxScheduler', { views: ['workWeek'], currentView: 'workWeek', firstDayOfWeek: 1, currentDate: new Date(2021, 11, 12), height: 600 }); + await testScreenshot(page, 'work-week-first-day-of-week.png', { element: page.locator('.dx-scheduler') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts new file mode 100644 index 000000000000..836e6d43da4f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts @@ -0,0 +1,49 @@ +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('Layout: Views: IntervalCount with StartDate', () => { + 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'); + }); + + [ + { view: 'timelineDay', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 4, 8), intervalCount: 6 }, + { view: 'week', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'timelineWeek', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'workWeek', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'timelineWorkWeek', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'month', currentDate: new Date(2020, 5, 11), startDate: new Date(2020, 3, 8), intervalCount: 6 }, + { view: 'timelineMonth', currentDate: new Date(2020, 5, 11), startDate: new Date(2020, 3, 8), intervalCount: 6 }, + ].forEach(({ view, currentDate, startDate, intervalCount }) => { + test(`startDate should work in ${view} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: view, intervalCount, startDate }], currentView: view, currentDate, dataSource: [], crossScrollingEnabled: true, + }); + await testScreenshot(page, `start-date-in-${view}.png`); + await page.locator('.dx-scheduler-date-table-cell').first().dblclick(); + await testScreenshot(page, `start-date-in-${view}-with-form.png`); + }); + }); + + [ + { view: 'week', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + { view: 'timelineWeek', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + { view: 'workWeek', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + { view: 'timelineWorkWeek', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + ].forEach(({ view, currentDate, startDate, intervalCount }) => { + test(`startDate should work in ${view} view when it indicates the same week as the start as currentDate`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: view, intervalCount, startDate }], currentView: view, currentDate, dataSource: [], crossScrollingEnabled: true, + }); + await testScreenshot(page, `complex-start-date-in-${view}.png`); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts new file mode 100644 index 000000000000..7a0418208328 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts @@ -0,0 +1,21 @@ +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('Scheduler: Material theme without all-day panel', () => { + 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('Week view without all-day panel should be rendered correctly', async ({ page }) => { + await createWidget(page, 'dxScheduler', { dataSource: [], currentDate: new Date(2020, 6, 15), views: ['week'], currentView: 'week', height: 500 }); + await testScreenshot(page, 'week-without-all-day-panel.png', { element: page.locator('.dx-scheduler-work-space') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts new file mode 100644 index 000000000000..3f25f1605f86 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts @@ -0,0 +1,37 @@ +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('Scheduler Timeline: Cross-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('Timeline should have Cross-Scrolling enabled', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 400, width: 800, currentDate: new Date(2021, 1, 2), dataSource: [], + views: ['timelineDay'], currentView: 'timelineDay', startDayHour: 8, endDayHour: 20, cellDuration: 60, + showAllDayPanel: false, groups: ['humanId'], + resources: [{ fieldExpr: 'humanId', dataSource: [ + { id: 0, text: 'David Carter', color: '#74d57b' }, { id: 1, text: 'Emma Lewis', color: '#1db2f5' }, + { id: 2, text: 'Noah Hill', color: '#f5564a' }, { id: 3, text: 'William Bell', color: '#97c95c' }, + { id: 4, text: 'Jane Jones', color: '#ffc720' }, { id: 5, text: 'Violet Young', color: '#eb3573' }, + { id: 6, text: 'Samuel Perry', color: '#a63db8' }, { id: 7, text: 'Luther Murphy', color: '#ffaa66' }, + { id: 8, text: 'Craig Morris', color: '#2dcdc4' }, + ], label: 'Employee' }], + }); + const hasBothScrollbars = await page.evaluate(() => { + const scrollable = document.querySelector('.dx-scheduler-work-space .dx-scrollable'); + if (!scrollable) return false; + return scrollable.scrollHeight > scrollable.clientHeight && scrollable.scrollWidth > scrollable.clientWidth; + }); + expect(hasBothScrollbars).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts new file mode 100644 index 000000000000..fffed2a6a417 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts @@ -0,0 +1,29 @@ +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('Scheduler Timeline: Grouping', () => { + 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'); + }); + + ['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => { + test(`${view} view - header panel should contain group rows if horizontal grouping`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + groupOrientation: 'horizontal', + views: [{ type: 'timelineDay', groupOrientation: 'horizontal' }], + currentView: 'timelineDay', groups: ['one'], + resources: [{ fieldExpr: 'one', dataSource: [{ id: 1, text: 'a' }, { id: 2, text: 'b' }] }], + }); + const groupCellCount = await page.locator('.dx-scheduler-header-panel .dx-scheduler-group-header').count(); + expect(groupCellCount).toBe(2); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts new file mode 100644 index 000000000000..dcf4eb54ab7b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts @@ -0,0 +1,22 @@ +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('Scheduler: Layout Views: Timeline Month', () => { + 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('Header cells should be aligned with date-table cells in timeline-month when current date changes', async ({ page }) => { + await createWidget(page, 'dxScheduler', { currentDate: new Date(2020, 10, 1), currentView: 'timelineMonth', height: 600, views: ['timelineMonth'], crossScrollingEnabled: true }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2020, 11, 1)); }); + await testScreenshot(page, 'timeline-month-change-current-date.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts new file mode 100644 index 000000000000..0e4d06082a49 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts @@ -0,0 +1,136 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('Legacy appointment popup form', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Subject and description fields should be empty after showing popup on empty cell', async ({ page }) => { + const APPOINTMENT_TEXT = 'Website Re-Design Plan'; + + await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + width: 600, + editing: { legacyForm: true }, + dataSource: [{ + text: APPOINTMENT_TEXT, + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 11, 30), + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + const subjectValue = await subjectInput.inputValue(); + expect(subjectValue).toBe(APPOINTMENT_TEXT); + + const descriptionInput = popup.locator('.dx-texteditor-input').nth(1); + await descriptionInput.fill('temp'); + + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(5); + await cell.dblclick(); + + const subjectValueAfter = await subjectInput.inputValue(); + expect(subjectValueAfter).toBe(''); + + const descriptionValueAfter = await descriptionInput.inputValue(); + expect(descriptionValueAfter).toBe(''); + }); + + test('Appointment should have correct form data on consecutive shows (T832711)', async ({ page }) => { + const APPOINTMENT_TEXT = 'Google AdWords Strategy'; + + await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2017, 4, 25), + endDayHour: 20, + editing: { legacyForm: true }, + dataSource: [{ + text: APPOINTMENT_TEXT, + startDate: new Date(2017, 4, 1), + endDate: new Date(2017, 4, 5), + allDay: true, + }], + height: 580, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + await expect(popup).toBeVisible(); + + const allDaySwitch = popup.locator('.dx-switch'); + await allDaySwitch.click(); + + const cancelButton = popup.locator('.dx-popup-cancel.dx-button'); + await cancelButton.click(); + + await expect(popup).not.toBeVisible(); + + await appointment.dblclick(); + await expect(popup).toBeVisible(); + }); + + test('From elements for disabled appointments should be read only (T835731)', async ({ page }) => { + const APPOINTMENT_TEXT = 'Install New Router in Dev Room'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: APPOINTMENT_TEXT, + startDate: new Date(2017, 4, 22, 14, 30), + endDate: new Date(2017, 4, 25, 15, 30), + disabled: true, + recurrenceRule: 'FREQ=DAILY', + }], + editing: { legacyForm: true }, + currentView: 'week', + recurrenceEditMode: 'series', + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + + const subjectValue = await subjectInput.inputValue(); + expect(subjectValue).toBe(APPOINTMENT_TEXT); + }); + + test('AppointmentForm should display correct dates in work-week when firstDayOfWeek is used', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['workWeek'], + currentView: 'workWeek', + editing: { legacyForm: true }, + currentDate: new Date(2021, 5, 28), + startDayHour: 5, + height: 600, + firstDayOfWeek: 2, + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(4); + await cell.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('6/28/2021, 6:00 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts new file mode 100644 index 000000000000..62c23e3902bf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment Popup errors check', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Appointment popup should not raise error if appointment is recursive', async ({ page }) => { + const consoleErrors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Meeting of Instructors', + startDate: new Date('2020-11-01T17:00:00.000Z'), + endDate: new Date('2020-11-01T17:15:00.000Z'), + recurrenceRule: 'FREQ=DAILY;BYDAY=TU;UNTIL=20201203', + }], + currentView: 'month', + currentDate: new Date(2020, 10, 25), + height: 600, + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Meeting of Instructors' }); + await appointment.dblclick(); + + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + expect(consoleErrors.length).toBe(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts new file mode 100644 index 000000000000..c9d186e1b390 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const schedulerOptions = { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 30, 11), + endDate: new Date(2021, 2, 30, 12), + recurrenceRule: 'FREQ=DAILY;UNTIL=20211029T205959Z', + }], + recurrenceEditMode: 'series', + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 600, + editing: { legacyForm: true }, +}; + +test.describe('Appointment popup form:date editors', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Form date editors should pass numeric chars according by date mask', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + await subjectInput.click(); + + await page.keyboard.press('Tab'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + await startDateInput.fill('111111111111'); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('11/11/1111, 11:11 AM'); + }); + + test('Form date editors should not pass chars according by date mask', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + await startDateInput.click(); + await startDateInput.fill('TEXT'); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('3/30/2021, 11:00 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts new file mode 100644 index 000000000000..d571bc3a2d65 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const TEST_TITLE = 'Test'; + +test.describe('Appointment form: expressions', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('text: expression should work', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-12-10', + cellDuration: 240, + dataSource: [{ + textCustom: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + textExpr: 'textCustom', + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_TITLE }); + await expect(appointment.first()).toBeVisible(); + + await appointment.first().dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + const value = await subjectInput.inputValue(); + expect(value).toBe(TEST_TITLE); + }); + + test('text: nested expression should work', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-12-10', + cellDuration: 240, + dataSource: [{ + nested: { textCustom: TEST_TITLE }, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + textExpr: 'nested.textCustom', + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_TITLE }); + await appointment.first().dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + const value = await subjectInput.inputValue(); + expect(value).toBe(TEST_TITLE); + }); + + test('Appointment popup should has correct width when the nested recurrenceRuleExpr option is set', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + text: TEST_TITLE, + nestedA: { nestedB: { nestedC: { recurrenceRuleCustom: 'FREQ=DAILY' } } }, + }], + currentDate: '2023-12-10', + cellDuration: 240, + recurrenceEditMode: 'series', + recurrenceRuleExpr: 'nestedA.nestedB.nestedC.recurrenceRuleCustom', + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_TITLE }); + await appointment.first().dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const form = popup.locator('.dx-scheduler-form'); + await expect(form).toBeVisible(); + + const content = popup.locator('.dx-popup-content'); + await testScreenshot(page, 'form_recurrence-editor-first-opening_nested-expr.png', { element: content }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts new file mode 100644 index 000000000000..19708e6a496f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment Form: recurrence editor', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should not reset the recurrence editor value after the repeat toggling', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { legacyForm: true }, + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const recurrenceSwitch = popup.locator('.dx-recurrence-switch-container .dx-switch'); + await recurrenceSwitch.click(); + + await recurrenceSwitch.click(); + await recurrenceSwitch.click(); + + const content = popup.locator('.dx-popup-content'); + await testScreenshot(page, 'recurrence-editor_after-hide.png', { element: content }); + }); + + test('Should correctly create usual appointment after repeat toggling', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { legacyForm: true }, + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const recurrenceSwitch = popup.locator('.dx-recurrence-switch-container .dx-switch'); + await recurrenceSwitch.click(); + await recurrenceSwitch.click(); + + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + }); + + test('Should correctly create recurrent appointment', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { legacyForm: true }, + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const recurrenceSwitch = popup.locator('.dx-recurrence-switch-container .dx-switch'); + await recurrenceSwitch.click(); + + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(7); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts new file mode 100644 index 000000000000..d5971773a952 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment Form', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Invoke showAppointmentPopup method should not raise error if value of currentDate property as a string', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25).toISOString(), + height: 600, + editing: { legacyForm: true }, + }); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.showAppointmentPopup(); + }); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('3/25/2021, 12:00 AM'); + }); + + test('Show appointment popup if deferredRendering is false (T1069753)', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.ui.dxPopup.defaultOptions({ + options: { deferRendering: false }, + }); + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 2, 29, 10), + endDate: new Date(2021, 2, 29, 11), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 12, + width: 400, + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + await expect(popup).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts new file mode 100644 index 000000000000..b299108cdbb9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [{ + text: 'Watercolor Landscape', + startDate: new Date('2020-06-01T17:30:00.000Z'), + endDate: new Date('2020-06-01T19:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + startDateTimeZone: 'Etc/GMT+10', + endDateTimeZone: 'US/Alaska', +}]; + +test.describe('Layout:AppointmentForm:TimezoneEditors(T1080932)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test.skip('TimeZone editors should be have data after hide forms data(T1080932)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource, + onAppointmentFormOpening: ((e: any) => { + e.form.itemOption('mainGroup.text', 'visible', false); + }) as any, + editing: { allowTimeZoneEditing: true, legacyForm: true }, + recurrenceEditMode: 'series', + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 6, 25), + startDayHour: 9, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const inputs = popup.locator('.dx-texteditor-input'); + const startTzValue = await inputs.nth(1).inputValue(); + expect(startTzValue).toBe('(GMT -10:00) Etc - GMT+10'); + }); + + test.skip('TimeZone editors should be have data in default case(T1080932)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource, + editing: { allowTimeZoneEditing: true, legacyForm: true }, + recurrenceEditMode: 'series', + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 6, 25), + startDayHour: 9, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const inputs = popup.locator('.dx-texteditor-input'); + const startTzValue = await inputs.nth(2).inputValue(); + expect(startTzValue).toBe('(GMT -10:00) Etc - GMT+10'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/loadingPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/loadingPanel.spec.ts new file mode 100644 index 000000000000..21e496d5d80e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/loadingPanel.spec.ts @@ -0,0 +1,55 @@ +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('Scheduler loading panel', () => { + 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 SCHEDULER_SELECTOR = '#container'; +const INITIAL_APPOINTMENT_TITLE = 'appointment'; +const ADDITIONAL_TITLE_TEXT = '-updated'; + +); + +test('Save appointment loading panel screenshot', async ({ page }) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = scheduler.getAppointment(INITIAL_APPOINTMENT_TITLE); + const { appointmentPopup } = scheduler; + + await (appointment.element).dblclick() + .click(appointmentPopup.textEditor.element) + .typeText(appointmentPopup.textEditor.element, ADDITIONAL_TITLE_TEXT) + .click(appointmentPopup.saveButton.element); + + await testScreenshot(page, 'save-appointment-loading-panel-screenshot.png', { element: page.locator('.dx-scheduler') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + onAppointmentUpdating: (e) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + e.cancel = new Promise((resolve, reject) => {}); + }, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/diferrentInvtervalCounts.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/diferrentInvtervalCounts.spec.ts new file mode 100644 index 000000000000..821205404694 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/diferrentInvtervalCounts.spec.ts @@ -0,0 +1,48 @@ +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('Scheduler: different intervalCount option 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('Interval count: 1, February of 2021', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ + type: 'month', + intervalCount: 1, + }], + currentView: 'month', + firstDayOfWeek: 1, + currentDate: new Date(2021, 1, 1), + }); + + await testScreenshot(page, 'month-february-2021.png', { element: page.locator('.dx-scheduler-work-space') }); + }); + + test('Interval count: 12', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ + type: 'month', + intervalCount: 12, + }], + height: 600, + currentView: 'month', + currentDate: new Date(2023, 6, 1), + }); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2024, 6, 1)); + }); + + await testScreenshot(page, 'month-interval-count-12.png', { element: page.locator('.dx-scheduler-work-space') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-february-2021.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-february-2021.png new file mode 100644 index 000000000000..14f09cd7ebc5 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-february-2021.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-interval-count-12.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-interval-count-12.png new file mode 100644 index 000000000000..bc0dae28e509 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-interval-count-12.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-2-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-2-rows-.png new file mode 100644 index 000000000000..4d20fb1d1bde Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-2-rows-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-3-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-3-rows-.png new file mode 100644 index 000000000000..e505040b74a9 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-3-rows-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-all-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-all-rows-.png new file mode 100644 index 000000000000..781289d45321 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-all-rows-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-2-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-2-rows-.png new file mode 100644 index 000000000000..dc71d1f5fcb6 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-2-rows-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-3-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-3-rows-.png new file mode 100644 index 000000000000..23a4e4d7d74c Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-3-rows-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-all-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-all-rows-.png new file mode 100644 index 000000000000..ead340cb0cf8 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-all-rows-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-false-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-false-.png new file mode 100644 index 000000000000..bd7b752b6584 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-false-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-true-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-true-.png new file mode 100644 index 000000000000..17c1ad6e0d3c Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-true-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-false-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-false-.png new file mode 100644 index 000000000000..23188b517c2c Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-false-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-true-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-true-.png new file mode 100644 index 000000000000..a2aa9157a924 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-true-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-false-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-false-.png new file mode 100644 index 000000000000..b938622efd1d Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-false-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-true-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-true-.png new file mode 100644 index 000000000000..09c96c157bad Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-true-.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-february.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-february.png new file mode 100644 index 000000000000..1c81ed2ebb45 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-february.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-january.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-january.png new file mode 100644 index 000000000000..8406434f8223 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-january.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-february-2021.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-february-2021.png new file mode 100644 index 000000000000..14f09cd7ebc5 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-february-2021.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-with-appointment.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-with-appointment.png new file mode 100644 index 000000000000..c3f0c83a9507 Binary files /dev/null and b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-with-appointment.png differ diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/longAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/longAppointments.spec.ts new file mode 100644 index 000000000000..e212e04b880b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/longAppointments.spec.ts @@ -0,0 +1,121 @@ +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('Scheduler: long appointments in month view', () => { + 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 appointments = [ + { + text: 'Appointment spans 3 rows', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 0, 24), + }, + { + text: 'Appointment spans all rows', + startDate: new Date(2019, 11, 29), + endDate: new Date(2020, 1, 8, 15), + }, + { + text: 'Appointment spans 2 rows', + startDate: new Date(2020, 0, 17), + endDate: new Date(2020, 0, 20), + }, + ]; + + for (const rtlEnabled of [false, true]) { + for (const appointment of appointments) { + test(`Long appointment (rtl=${rtlEnabled}, text=${appointment.text})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [appointment], + views: ['month'], + currentView: 'month', + rtlEnabled, + currentDate: new Date(2020, 0, 1), + }); + + await testScreenshot(page, + `month-long-appointment(rtl=${rtlEnabled}, text=${appointment.text}).png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + } + } + + for (const rtlEnabled of [false, true]) { + test(`Long appointment several months (rtl=${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 2, 10), + }], + views: ['month'], + currentView: 'month', + rtlEnabled, + currentDate: new Date(2020, 0, 1), + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + + await testScreenshot(page, + `month-long-appointment-several-months-january(rtl=${rtlEnabled}).png`, + { element: workSpace }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(300); + + await testScreenshot(page, + `month-long-appointment-several-months-february(rtl=${rtlEnabled}).png`, + { element: workSpace }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(300); + + await testScreenshot(page, + `month-long-appointment-several-months-march(rtl=${rtlEnabled}).png`, + { element: workSpace }, + ); + }); + } + + test('Long recurrence appointment several months', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 0, 10), + recurrenceRule: 'FREQ=DAILY;INTERVAL=5', + }], + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 0, 1), + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + + await testScreenshot(page, + 'month-long-recurrence-appointment-several-months-january.png', + { element: workSpace }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(300); + + await testScreenshot(page, + 'month-long-recurrence-appointment-several-months-february.png', + { element: workSpace }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/outOfDayHours.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/outOfDayHours.spec.ts new file mode 100644 index 000000000000..79815af3fef2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/outOfDayHours.spec.ts @@ -0,0 +1,50 @@ +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('Scheduler: take into account start and end day hour', () => { + 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 show appointment in month view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: '2024-01-01T11:00:00', + endDate: '2024-01-01T12:00:00', + text: 'test', + }], + startDayHour: 11, + endDayHour: 22, + currentDate: '2024-01-01', + views: ['month', 'timelineMonth'], + currentView: 'month', + }); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' })).toBeVisible(); + }); + + test('Should not show appointment in month view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: '2024-01-01T11:00:00', + endDate: '2024-01-01T12:00:00', + text: 'test', + }], + startDayHour: 13, + endDayHour: 22, + currentDate: '2024-01-01', + views: ['month', 'timelineMonth'], + currentView: 'month', + }); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' })).toHaveCount(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/regression-detection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/regression-detection.spec.ts new file mode 100644 index 000000000000..7f1cd3bc0b61 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/regression-detection.spec.ts @@ -0,0 +1,49 @@ +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('Regression detection: verify Playwright catches visual changes', () => { + 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 with intervalCount=1 should match baseline', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ + type: 'month', + intervalCount: 1, + }], + currentView: 'month', + firstDayOfWeek: 1, + currentDate: new Date(2021, 1, 1), + }); + + await testScreenshot(page, 'regression-month-february-2021.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + + test('Month view with appointments should match baseline', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test Appointment', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 0, 10), + }], + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 0, 1), + }); + + await testScreenshot(page, 'regression-month-with-appointment.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/navigator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/navigator.spec.ts new file mode 100644 index 000000000000..de00da595837 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/navigator.spec.ts @@ -0,0 +1,92 @@ +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('Scheduler: Navigator', () => { + 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 createScheduler = async (options = {}): Promise => { + await createWidget(page, 'dxScheduler', extend(options, { + dataSource: [], + currentDate: new Date(2017, 4, 18), + firstDayOfWeek: 1, + height: 600, + views: ['week', 'month'], + })); +}; + +test('Navigator can change week when current date interval is more than diff between current date and `max` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `next` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `next` must be disabled after change 1 week earlier + + await (page.locator('.dx-scheduler-navigator-next')).click() + .expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + max: new Date(2017, 4, 24), + currentView: 'week', +})); + +test('Navigator can change week when current date interval is more than diff between current date and `min` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `prev` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `prev` must be disabled after change 1 week later + + await (page.locator('.dx-scheduler-navigator-previous')).click() + .expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + min: new Date(2017, 4, 13), + currentView: 'week', +})); + +test('Navigator can change month when current date interval is more than diff between current date and `max` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `next` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `next` must be disabled after change 1 week earlier + + await (page.locator('.dx-scheduler-navigator-next')).click() + .expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + max: new Date(2017, 5, 15), + currentView: 'month', +})); + +test('Navigator can change month when current date interval is more than diff between current date and `min` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `prev` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `prev` must be disabled after change 1 week later + + await (page.locator('.dx-scheduler-navigator-previous')).click() + .expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + min: new Date(2017, 3, 28), + currentView: 'month', +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/appointmentTooltip.timeZone.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/appointmentTooltip.timeZone.spec.ts new file mode 100644 index 000000000000..b2f50683aaf2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/appointmentTooltip.timeZone.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment tooltip with recurrence appointment and custom time zone', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Time in appointment tooltip should has valid value (T848058)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Stand-up meeting', + startDate: '2017-05-22T15:30:00.000Z', + endDate: '2017-05-22T15:45:00.000Z', + recurrenceRule: 'FREQ=DAILY', + startDateTimeZone: 'America/Los_Angeles', + endDateTimeZone: 'America/Los_Angeles', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2017, 4, 25), + startDayHour: 8, + timeZone: 'America/Los_Angeles', + height: 600, + }); + + const appointments = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Stand-up meeting' }); + const appointmentCount = await appointments.count(); + + for (let i = 0; i < appointmentCount; i += 1) { + await appointments.nth(i).click(); + const tooltipDate = await page.locator('.dx-tooltip-appointment-item-content-date').first().textContent(); + expect(tooltipDate).toBe('8:30 AM - 8:45 AM'); + } + }); + + test('The only one displayed part of recurrence appointment must have correct offset after DST(T1034216)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'Europe/Moscow', + startDateTimeZoneExpr: 'TimeZone', + endDateTimeZoneExpr: 'TimeZone', + views: ['month', 'week'], + currentView: 'month', + currentDate: '2021-12-01', + dataSource: [{ + text: 'apt', + startDate: '2021-09-01T01:00:00-07:00', + endDate: '2021-09-01T02:00:00-07:00', + recurrenceException: '', + recurrenceRule: 'FREQ=MONTHLY;BYDAY=WE,FR;BYSETPOS=1;UNTIL=20211231T235959Z', + TimeZone: 'America/Los_Angeles', + }], + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'apt' }); + await appointment.click(); + const tooltipDate = await page.locator('.dx-tooltip-appointment-item-content-date').first().textContent(); + expect(tooltipDate).toBe('December 2 12:00 PM - 1:00 PM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/basic.spec.ts new file mode 100644 index 000000000000..245db0d2d347 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/basic.spec.ts @@ -0,0 +1,129 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Rendering of the recurrence appointments in Scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Drag-n-drop recurrence appointment between dateTable and allDay panel', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Simple recurrence appointment', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 11, 0), + recurrenceRule: 'FREQ=DAILY;COUNT=7', + }], + startDayHour: 1, + recurrenceEditMode: 'series', + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 3, 1), + height: 600, + width: 800, + }); + + await testScreenshot(page, 'basic-recurrence-appointment-init.png'); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Simple recurrence appointment' }).first(); + const allDayCell = page.locator('.dx-scheduler-all-day-table-cell').nth(0); + + await appointment.dragTo(allDayCell); + await page.waitForTimeout(300); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(7); + + await page.waitForTimeout(500); + await testScreenshot(page, 'basic-recurrence-appointment-after-drag.png'); + }); + + test('Appointments in DST should not have offset when recurring appointment timezone not equal to scheduler timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/New_York', + dataSource: [{ + text: 'Recurrence', + startDate: new Date('2021-03-13T19:00:00.000Z'), + endDate: new Date('2021-03-13T19:30:00.000Z'), + recurrenceRule: 'FREQ=DAILY;COUNT=1000', + startDateTimeZone: 'America/New_York', + endDateTimeZone: 'America/New_York', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 13), + firstDayOfWeek: 1, + height: 600, + width: 800, + }); + + const appt0 = page.locator('.dx-scheduler-appointment').nth(0); + const time0 = await appt0.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time0).toContain('2:00 PM - 2:30 PM'); + + const appt1 = page.locator('.dx-scheduler-appointment').nth(1); + const time1 = await appt1.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time1).toContain('2:00 PM - 2:30 PM'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2021, 10, 1)); + }); + + const appt0b = page.locator('.dx-scheduler-appointment').nth(0); + const time0b = await appt0b.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time0b).toContain('2:00 PM - 2:30 PM'); + + const appt1b = page.locator('.dx-scheduler-appointment').nth(1); + const time1b = await appt1b.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time1b).toContain('2:00 PM - 2:30 PM'); + }); + + test('Appointments in end of DST should have correct offset', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Phoenix', + dataSource: [{ + text: 'Recurrence', + startDate: new Date('2021-03-13T19:00:00.000Z'), + endDate: new Date('2021-03-13T19:30:00.000Z'), + recurrenceRule: 'FREQ=DAILY;COUNT=1000', + startDateTimeZone: 'America/New_York', + endDateTimeZone: 'America/New_York', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 10, 1), + firstDayOfWeek: 1, + height: 600, + width: 800, + }); + + const appt5 = page.locator('.dx-scheduler-appointment').nth(5); + const time5 = await appt5.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time5).toContain('11:00 AM - 11:30 AM'); + + const appt6 = page.locator('.dx-scheduler-appointment').nth(6); + const time6 = await appt6.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time6).toContain('12:00 PM - 12:30 PM'); + }); + + test('Appointment displayed without errors if it was only one DST in year(T1037853)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Recurrence', + startDate: new Date(1942, 3, 29, 0), + endDate: new Date(1942, 3, 29, 1), + recurrenceRule: 'FREQ=DAILY;COUNT=2', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(1942, 3, 29), + height: 600, + }); + + const appt = page.locator('.dx-scheduler-appointment').nth(0); + await expect(appt).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/dialog.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/dialog.spec.ts new file mode 100644 index 000000000000..97947fd3e75d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/dialog.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const INITIAL_APPOINTMENT_TITLE = 'appointment'; + +test.describe('Recurrence dialog', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Recurrence edit dialog screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + const dialog = page.locator('.dx-dialog'); + await expect(dialog).toBeVisible(); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'recurrence-edit-dialog-screenshot.png', { element: scheduler }); + }); + + test('Recurrence delete dialog screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const dialog = page.locator('.dx-dialog'); + await expect(dialog).toBeVisible(); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'recurrence-delete-dialog-screenshot.png', { element: scheduler }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/rerenderOnResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/rerenderOnResize.spec.ts new file mode 100644 index 000000000000..adc044ebcf33 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/rerenderOnResize.spec.ts @@ -0,0 +1,96 @@ +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 createScheduler = async (page, container: string, options?: Record): Promise => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2020, 8, 7), + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + scrolling: { + mode: 'virtual', + }, + currentView: 'Timeline', + views: [{ + type: 'timelineWorkWeek', + name: 'Timeline', + groupOrientation: 'vertical', + }], + dataSource: [{ + startDate: new Date(2020, 8, 7, 8), + endDate: new Date(2020, 8, 7, 9), + text: 'test', + }], + ...options, + }, container); +}; + +test.describe('Re-render on resize', () => { + 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 should re-rendered on window resize-up (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 800, height: 400 }); + + await createScheduler(page, '#container', { currentView: 'workWeek' }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toMatch(/transform: translate\(0px, 0px\); width: \d+\.\d+px; height: \d+px; background-color: red;/); + }); + + test('Appointment should not re-rendered on window resize when width and height not set (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 300, height: 300 }); + + await createScheduler(page, '#container'); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toBe('transform: translate(0px, 30px); width: 200px; height: 70px; background-color: red;'); + }); + + test('Appointment should not re-rendered on window resize when width and height have percent value (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 300, height: 400 }); + + await createScheduler(page, '#container', { width: '100%', height: '100%' }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toBe('transform: translate(0px, 30px); width: 200px; height: 70px; background-color: red;'); + }); + + test('Appointment should not re-rendered on window resize when width and height have static value (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 300, height: 300 }); + + await createScheduler(page, '#container', { width: 600, height: 400 }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toBe('transform: translate(0px, 30px); width: 200px; height: 61.7539px; background-color: red;'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1255474.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1255474.spec.ts new file mode 100644 index 000000000000..782c58eba4bf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1255474.spec.ts @@ -0,0 +1,48 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const appointmentText = 'Book Flights to San Fran for Sales Trip'; + +test.describe('Resize appointment that cross DTC time', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Resize appointment that cross DTC time', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + allDayPanelMode: 'allDay', + height: 600, + width: 800, + firstDayOfWeek: 7, + dataSource: [{ + text: appointmentText, + startDate: new Date('2021-03-28T17:00:00.000Z'), + endDate: new Date('2021-03-28T18:00:00.000Z'), + TimeZone: 'Europe/Belgrade', + allDay: true, + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentText }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'T1255474-resize-all-day-appointment.png', { element: scheduler }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1294528.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1294528.spec.ts new file mode 100644 index 000000000000..3bc5a21ffc89 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1294528.spec.ts @@ -0,0 +1,174 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Resize all day panel appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [true, false].forEach((rtlEnabled) => { + test(`Resize all day appointment rtlEnabled=${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 1, 9), + currentView: 'week', + firstDayOfWeek: 0, + rtlEnabled, + height: 400, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 10), + allDay: true, + }], + width: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const { right, left } = { + right: appointment.locator('.dx-resizable-handle-right'), + left: appointment.locator('.dx-resizable-handle-left'), + }; + const text = 'Appointment: February 9, 2015, All day'; + const startDateExtendedText = 'Appointment: February 8, 2015 - February 9, 2015, All day'; + const endDateExtendedText = 'Appointment: February 9, 2015 - February 10, 2015, All day'; + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel1 = await appointment.getAttribute('aria-label'); + expect(ariaLabel1).toBe(rtlEnabled ? startDateExtendedText : endDateExtendedText); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel2 = await appointment.getAttribute('aria-label'); + expect(ariaLabel2).toBe(text); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel3 = await appointment.getAttribute('aria-label'); + expect(ariaLabel3).toBe(rtlEnabled ? endDateExtendedText : startDateExtendedText); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel4 = await appointment.getAttribute('aria-label'); + expect(ariaLabel4).toBe(text); + }); + }); + + test('Resize long appointment rtlEnabled=true', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 1, 9), + currentView: 'week', + firstDayOfWeek: 0, + rtlEnabled: true, + height: 400, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 10, 10), + }], + width: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const right = appointment.locator('.dx-resizable-handle-right'); + const left = appointment.locator('.dx-resizable-handle-left'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel1 = await appointment.getAttribute('aria-label'); + expect(ariaLabel1).toBe('Appointment: February 8, 2015, 12:00 AM - February 10, 2015, 10:00 AM'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel2 = await appointment.getAttribute('aria-label'); + expect(ariaLabel2).toBe('Appointment: February 9, 2015, 12:00 AM - February 10, 2015, 10:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel3 = await appointment.getAttribute('aria-label'); + expect(ariaLabel3).toBe('Appointment: February 9, 2015, 12:00 AM - February 12, 2015, 12:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel4 = await appointment.getAttribute('aria-label'); + expect(ariaLabel4).toBe('Appointment: February 9, 2015, 12:00 AM - February 11, 2015, 12:00 AM'); + }); + + test('Resize long appointment rtlEnabled=false', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 1, 9), + currentView: 'week', + firstDayOfWeek: 0, + rtlEnabled: false, + height: 400, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 10, 10), + }], + width: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const right = appointment.locator('.dx-resizable-handle-right'); + const left = appointment.locator('.dx-resizable-handle-left'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel1 = await appointment.getAttribute('aria-label'); + expect(ariaLabel1).toBe('Appointment: February 9, 2015, 8:00 AM - February 12, 2015, 12:00 AM'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel2 = await appointment.getAttribute('aria-label'); + expect(ariaLabel2).toBe('Appointment: February 9, 2015, 8:00 AM - February 11, 2015, 12:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel3 = await appointment.getAttribute('aria-label'); + expect(ariaLabel3).toBe('Appointment: February 8, 2015, 12:00 AM - February 11, 2015, 12:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel4 = await appointment.getAttribute('aria-label'); + expect(ariaLabel4).toBe('Appointment: February 9, 2015, 12:00 AM - February 11, 2015, 12:00 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/allDay.spec.ts new file mode 100644 index 000000000000..e3913db30d3a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/allDay.spec.ts @@ -0,0 +1,48 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Resize appointments in All Day Panel', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Resize in the workWeek view between weeks', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ + type: 'workWeek', + intervalCount: 2, + startDate: new Date(2021, 5, 29), + }], + currentDate: new Date(2021, 5, 29), + currentView: 'workWeek', + maxAppointmentsPerCell: 'unlimited', + startDayHour: 9, + endDayHour: 13, + dataSource: [ + { text: '1st', startDate: new Date(2021, 5, 29), allDay: true }, + { text: '2nd', startDate: new Date(2021, 6, 7), allDay: true }, + { text: '3rd', startDate: new Date(2021, 6, 1), endDate: new Date(2021, 6, 5), allDay: true }, + ], + }); + + const appointment1 = page.locator('.dx-scheduler-appointment').filter({ hasText: '1st' }); + const appointment2 = page.locator('.dx-scheduler-appointment').filter({ hasText: '2nd' }); + const appointment3 = page.locator('.dx-scheduler-appointment').filter({ hasText: '3rd' }); + + await appointment1.locator('.dx-resizable-handle-right').dragTo(appointment1.locator('.dx-resizable-handle-right'), { targetPosition: { x: 400, y: 0 } }); + await appointment2.locator('.dx-resizable-handle-left').dragTo(appointment2.locator('.dx-resizable-handle-left'), { targetPosition: { x: -400, y: 0 } }); + await appointment3.locator('.dx-resizable-handle-right').dragTo(appointment3.locator('.dx-resizable-handle-right'), { targetPosition: { x: -140, y: 0 } }); + + await testScreenshot(page, 'resize-all-day-workweek-weekend-0.png'); + + await appointment1.locator('.dx-resizable-handle-right').dragTo(appointment1.locator('.dx-resizable-handle-right'), { targetPosition: { x: -400, y: 0 } }); + await appointment2.locator('.dx-resizable-handle-left').dragTo(appointment2.locator('.dx-resizable-handle-left'), { targetPosition: { x: 400, y: 0 } }); + await appointment3.locator('.dx-resizable-handle-right').dragTo(appointment3.locator('.dx-resizable-handle-right'), { targetPosition: { x: 140, y: 0 } }); + + await testScreenshot(page, 'resize-all-day-workweek-weekend-1.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/basic.spec.ts new file mode 100644 index 000000000000..576e07d27f45 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/basic.spec.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 11, 0), + resourceId: 0, + }, +]; + +test.describe('Resize appointments in the Scheduler basic views', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week', 'workWeek'].forEach((view) => { + test(`Resize in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [view], + currentView: view, + dataSource, + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height1 = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height1).toBe('190px'); + + const timeText1 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText1).toContain('10:00 AM - 12:30 PM'); + + const topHandle = appointment.locator('.dx-resizable-handle-top'); + await topHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height2 = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height2).toBe('76px'); + + const timeText2 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText2).toContain('11:30 AM - 12:30 PM'); + }); + }); + + test('Resize in the "month" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + dataSource, + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('400px'); + }); + + test('Resize should work correctly with startDateExpr (T944693)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + startDateExpr: 'start', + dataSource: dataSource.map(({ startDate, ...restProps }) => ({ + ...restProps, + start: startDate, + })), + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('190px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/cancelAppointmentResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/cancelAppointmentResize.spec.ts new file mode 100644 index 000000000000..f62716e5df50 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/cancelAppointmentResize.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSetupOptions = { + timeZone: 'Etc/GMT', + width: 400, + currentDate: '2021-06-01T00:00:00Z', + dataSource: [{ + text: 'Test Resize', + startDate: '2021-06-01T01:00:00Z', + endDate: '2021-06-01T20:00:00Z', + }], + views: [{ + type: 'timelineDay', + intervalCount: 2, + }], + currentView: 'timelineDay', + startDayHour: 0, + cellDuration: 1440, +}; + +test.describe('Cancel appointment Resizing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('onAppointmentUpdating - newDate should be correct after cancel appointment resize and cellDuration=24h (T1070565)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSetupOptions, + onAppointmentUpdating: ((e: any) => { + (window as any).newEndDate = e.newData.endDate; + e.cancel = true; + }) as any, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Resize' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + const etalonEndDateIso = '2021-06-03T00:00:00Z'; + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText1 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText1).toContain('1:00 AM - 8:00 PM'); + + const newEndDate1 = await page.evaluate(() => (window as any).newEndDate); + expect(newEndDate1).toBe(etalonEndDateIso); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText2 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText2).toContain('1:00 AM - 8:00 PM'); + + const newEndDate2 = await page.evaluate(() => (window as any).newEndDate); + expect(newEndDate2).toBe(etalonEndDateIso); + }); + + test('on escape - date should not changed when it is pressed after resize (T1125615)', async ({ page }) => { + await createWidget(page, 'dxScheduler', defaultSetupOptions); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Resize' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(50, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText1 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText1).toContain('1:00 AM - 12:00 AM'); + + await appointment.click(); + await page.keyboard.press('Escape'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(150, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText2 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText2).toContain('1:00 AM - 12:00 AM'); + }); + + test('on escape - date should not changed when it is pressed during resize (T1125615)', async ({ page }) => { + await createWidget(page, 'dxScheduler', defaultSetupOptions); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Resize' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(150, 0, { steps: 5 }); + await page.keyboard.press('Escape'); + await page.mouse.up(); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('1:00 AM - 8:00 PM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/timeline.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/timeline.spec.ts new file mode 100644 index 000000000000..1b7ad364b4e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/timeline.spec.ts @@ -0,0 +1,181 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 11, 0), + resourceId: 0, + }, +]; + +const defaultOptions = { + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Resize appointments in the Scheduler timeline views', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => { + test(`Resize in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultOptions, + views: [view], + currentView: view, + dataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(400, 0, { steps: 10 }); + await page.mouse.up(); + + const width1 = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width1).toBe('800px'); + }); + }); + + test('Resize in the "timelineMonth" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultOptions, + views: ['timelineMonth'], + currentView: 'timelineMonth', + dataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(400, 0, { steps: 10 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('600px'); + }); + + test('Resize appointment on timelineWeek view with custom startDayHour & endDayHour (T804779)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultOptions, + views: [{ + type: 'timelineWeek', startDayHour: 10, endDayHour: 16, cellDuration: 60, + }], + currentView: 'timelineWeek', + currentDate: new Date(2019, 8, 1), + firstDayOfWeek: 0, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2019, 8, 1, 14), + endDate: new Date(2019, 8, 2, 11), + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(-400, 0, { steps: 10 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('200px'); + }); + + test('Resize should work correctly when cell width is not an integer', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: 'timelineDay', cellDuration: 120 }], + currentView: 'timelineDay', + currentDate: new Date(2020, 10, 13), + dataSource: [{ + text: 'Appointment', + startDate: new Date(2020, 10, 13, 0, 0), + endDate: new Date(2020, 10, 13, 2, 0), + }], + width: 2999, + startDayHour: 0, + endDayHour: 24, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('12:00 AM - 4:00 AM'); + }); + + test('Resize in the "timelineDay" view with start and end day hour (T1134583)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appointment', + startDate: new Date(2024, 0, 3, 9, 30), + endDate: new Date(2024, 0, 3, 12, 30), + }], + views: [{ type: 'timelineDay', intervalCount: 3 }], + currentView: 'timelineDay', + currentDate: new Date(2024, 0, 2), + cellDuration: 60, + startDayHour: 10, + endDayHour: 12, + width: 1200, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const width1 = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width1).toBe('600px'); + }); + + test('Resize in the "timelineMonth" view with start and end day hour (T1134583)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appointment', + startDate: new Date(2024, 0, 3, 9, 30), + endDate: new Date(2024, 0, 3, 12, 30), + }], + views: ['timelineMonth'], + currentView: 'timelineMonth', + currentDate: new Date(2024, 0, 2), + cellDuration: 60, + startDayHour: 10, + endDayHour: 12, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('400px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/verticalGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/verticalGrouping.spec.ts new file mode 100644 index 000000000000..54ea30ef076b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/verticalGrouping.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const resourcesData = [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [ + { text: 'Low Priority', id: 1, color: '#1e90ff' }, + { text: 'High Priority', id: 2, color: '#ff9747' }, + ], +}]; + +test.describe('Resize appointments in the Scheduler with vertical grouping', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly calculate group resizing area (T1025952)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'first', startDate: new Date(2021, 3, 21, 9, 30), endDate: new Date(2021, 3, 21, 10), priorityId: 1 }, + { text: 'second', startDate: new Date(2021, 3, 21, 9, 30), endDate: new Date(2021, 3, 21, 10), priorityId: 2 }, + ], + views: [{ type: 'workWeek', groupOrientation: 'vertical' }], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 21), + startDayHour: 9, + endDayHour: 12, + groups: ['priorityId'], + resources: resourcesData, + width: 800, + height: 600, + }); + + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'first' }); + const bottomHandle1 = firstAppointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle1.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height1 = await firstAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height1).toBe('140.594px'); + + const secondAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'second' }); + const bottomHandle2 = secondAppointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle2.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height2 = await secondAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height2).toBe('165.922px'); + }); + + test('Should correctly calculate group resizing area after scroll (T1041672)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'app', startDate: new Date(2021, 3, 21, 9, 30), endDate: new Date(2021, 3, 21, 10), priorityId: 2 }, + ], + views: [{ type: 'week', groupOrientation: 'vertical' }], + currentView: 'week', + currentDate: new Date(2021, 3, 21), + height: 400, + groups: ['priorityId'], + resources: resourcesData, + width: 800, + }); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(2021, 3, 21, 9, 30), { priorityId: 2 }); + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('165.922px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/zooming.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/zooming.spec.ts new file mode 100644 index 000000000000..27d7c148a087 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/zooming.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, insertStylesheetRulesToPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function setZoomLevel(page, zoomLevel: number): Promise { + await page.evaluate((z) => { + $('body').css('zoom', `${z}%`); + }, zoomLevel); +} + +test.describe('Resize appointments - Zooming', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Vertical resize with zooming', async ({ page }) => { + await setZoomLevel(page, 110); + await insertStylesheetRulesToPage(page, '.dx-scheduler-cell-sizes-vertical { height: 43px;}'); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appt-01', + startDate: new Date(2021, 2, 28, 0), + endDate: new Date(2021, 2, 28, 0, 30), + }], + views: ['day'], + currentView: 'day', + cellDuration: 15, + currentDate: new Date(2021, 2, 28), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appt-01' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 430, { steps: 10 }); + await page.mouse.up(); + + const height = await appointment.evaluate((el) => parseInt(getComputedStyle(el).height, 10)); + expect(height).toBe(94); + + await setZoomLevel(page, 0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/scrollTo.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/scrollTo.spec.ts new file mode 100644 index 000000000000..149cd423f060 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/scrollTo.spec.ts @@ -0,0 +1,337 @@ +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('Scheduler: ScrollTo', () => { + 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 createScheduler = async (options): Promise => createWidget(page, 'dxScheduler', options); + +async function scrollToDate(page: Page) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const currentDate = instance.option('currentDate'); + const date = new Date(currentDate.getTime()); + date.setHours(date.getHours() + 6, 30, 0, 0); + instance.scrollTo(date); +}, ); +} + +async function scrollToDateWithGroups(page: Page) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const currentDate = instance.option('currentDate'); + const date = new Date(currentDate.getTime()); + date.setHours(date.getHours() + 6, 30, 0, 0); + instance.scrollTo(date, { priority: 1 }, ); +} +}); + +async function scrollToAllDay(page: Page) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const currentDate = instance.option('currentDate'); + const date = new Date(currentDate.getTime()); + date.setHours(date.getHours() + 6, 30, 0, 0); + instance.scrollTo(date, undefined, true); +}, ); +} + +test('ScrollTo works correctly with week and day views', async ({ page }) => { + // Scheduler on '#container' + + const views = [{ name: 'week', initValue: 0 }, { name: 'day', initValue: 0 }]; + + // eslint-disable-next-line no-restricted-syntax + for (const view of views) { + const { name, initValue } = view; + + await scheduler.option('currentView', name); + await scheduler.option('useNative', true); + await await page.waitForTimeout(1000); + + await scrollToDate(); + await await page.waitForTimeout(1000); + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBeGreaterThan(initValue, `Work space is scrolled in ${name} view`); + } +}).before(async () => createScheduler({ + dataSource: [], + views: ['week', 'day'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, +})); + +test('ScrollTo works correctly with grouping in week view', async ({ page }) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'week'); + await scheduler.option('useNative', true); + await await page.waitForTimeout(1000); + + const initialTop = await page.locator('.dx-scheduler-work-space')Scroll.top; + + await scrollToDateWithGroups(); + await await page.waitForTimeout(1000); + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBeGreaterThan(initialTop, 'Work space is scrolled with groups'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + groups: ['priority'], + resources: [{ + fieldExpr: 'priority', + dataSource: [ + { id: 1, text: 'High Priority' }, + { id: 2, text: 'Low Priority' }, + ], + }], + height: 580, +})); + +test('ScrollTo works correctly with all-day panel', async ({ page }) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'week'); + await scheduler.option('useNative', true); + + const initValue = 0; + const expectedTopValue = 0; + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBe(initValue, 'Work space has init scroll position'); + + await scrollToAllDay(); + await await page.waitForTimeout(3000); + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBe(expectedTopValue, 'Work space is scrolled to all-day panel'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + showAllDayPanel: true, + height: 580, +})); + +test('ScrollTo works correctly with RTL mode', async ({ page }) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'week'); + await scheduler.option('useNative', true); + await scheduler.option('rtlEnabled', true); + await await page.waitForTimeout(1000); + + const initialBrowserTop = await page.locator('.dx-scheduler-work-space')Scroll.top; + + await scrollToDate(); + await await page.waitForTimeout(1000); + + const browserTop = await ClientFunction(() => ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().scrollTop())(); + + expect(browserTop).toBeGreaterThan(initialBrowserTop, 'Work space is scrolled in RTL'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, +})); + +test('ScrollTo works correctly with timeline views (native, sync header/workspace) (T749957)', async (t) => { + // Scheduler on '#container' + + const views = [{ name: 'timelineDay' }, { name: 'timelineWeek' }]; + + // eslint-disable-next-line no-restricted-syntax + for (const view of views) { + const { name } = view; + + await scheduler.option('currentView', name); + await scheduler.option('useNative', true); + await await page.waitForTimeout(200); + + async function getWSLeft(page: Page) { + return page.evaluate(() => ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().scrollLeft(), ); +} + async function getHeaderLeft(page: Page) { + return page.evaluate(() => $('.dx-scheduler-header-scrollable .dx-scrollable-container').scrollLeft(), ); +} + + const initialLeft = await getWSLeft(); + const initialHeaderLeft = await getHeaderLeft(); + + expect(initialLeft).toBe(initialHeaderLeft, `${name}: header/workspace initial sync`); + + await scrollToDate(); + await await page.waitForTimeout(300); + + const left = await getWSLeft(); + const headerLeft = await getHeaderLeft(); + + expect(left).notEql(initialLeft, `${name}: workspace left changed`) + .expect(headerLeft).toBe(left, `${name}: header synchronized with workspace`); + } +}).before(async () => createScheduler({ + dataSource: [], + views: ['timelineDay', 'timelineWeek'], + currentView: 'timelineDay', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, +})); + +test('ScrollTo works correctly in timeline RTL (native, sync header/workspace)', async (t) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'timelineWeek'); + await scheduler.option('useNative', true); + await scheduler.option('rtlEnabled', true); + await await page.waitForTimeout(200); + + async function getWSLeft(page: Page) { + return page.evaluate(() => ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().scrollLeft(), ); +} + async function getHeaderLeft(page: Page) { + return page.evaluate(() => $('.dx-scheduler-header-scrollable .dx-scrollable-container').scrollLeft(), ); +} + + const initialLeft = await getWSLeft(); + const initialHeaderLeft = await getHeaderLeft(); + + expect(initialLeft).toBe(initialHeaderLeft, 'timeline RTL: initial sync'); + + await scrollToDate(); + await await page.waitForTimeout(300); + + const left = await getWSLeft(); + const headerLeft = await getHeaderLeft(); + + expect(left).notEql(initialLeft, 'timeline RTL: workspace left changed') + .expect(headerLeft).toBe(left, 'timeline RTL: header synchronized'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, + rtlEnabled: true, +})); + +[ + // startDayHour: 6:00, endDayHour: 18:00 + { + offset: 0, + targetDate: new Date(2021, 1, 3, 4, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), + }, + { + offset: 0, + targetDate: new Date(2021, 1, 3, 12, 0), + expectedDate: new Date(2021, 1, 3, 12, 0), + }, + { + offset: 0, + targetDate: new Date(2021, 1, 3, 20, 0), + expectedDate: new Date(2021, 1, 3, 18, 0), + }, + + // startDayHour: 18:00, endDayHour: next day 6:00 + { + offset: 720, + targetDate: new Date(2021, 1, 3, 10, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), + }, + { + offset: 720, + targetDate: new Date(2021, 1, 3, 20, 0), + expectedDate: new Date(2021, 1, 3, 20, 0), + }, + { + offset: 720, + targetDate: new Date(2021, 1, 4, 1, 0), + expectedDate: new Date(2021, 1, 4, 1, 0), + }, + { + offset: 720, + targetDate: new Date(2021, 1, 4, 7, 0), + expectedDate: new Date(2021, 1, 4, 6, 0), + }, + + // startDayHour: prev day 18:00, endDayHour: 6:00 + { + offset: -720, + targetDate: new Date(2021, 1, 3, 16, 0), + expectedDate: new Date(2021, 1, 3, 18, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 3, 21, 0), + expectedDate: new Date(2021, 1, 3, 21, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 4, 3, 0), + expectedDate: new Date(2021, 1, 4, 3, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 3, 7, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), + }, +].forEach(({ offset, targetDate, expectedDate }) => { + test(`scrollTo should scroll to date with offset=${offset}, targetDate=${targetDate.toString()} (T1310544)`, async (t) => { + // Scheduler on '#container' + + await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (targetDate).toISOString()); + + const cellData = await scheduler.getCellDataAtViewportCenter(); + + expect(expectedDate.getTime()).toBeGreaterThanOrEqual(cellData.startDate.getTime()) + // eslint-disable-next-line spellcheck/spell-checker + .expect(expectedDate.getTime()).toBeLessThanOrEqual(cellData.endDate.getTime()); + }).before(async () => createScheduler({ + dataSource: [], + views: [{ + type: 'timelineWeek', + offset, + cellDuration: 60, + }], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + startDayHour: 6, + endDayHour: 18, + height: 580, + })); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts new file mode 100644 index 000000000000..6eb74e63c822 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts @@ -0,0 +1,101 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const SCREENSHOT_BASE_NAME = 'recurrent-appointment-timezone-dst__editing'; +const TEST_APPOINTMENT_TEXT = 'Watercolor Landscape'; +const APPOINTMENT_DATETIME = { + winter: { start: new Date('2020-11-01T17:30:00.000Z'), end: new Date('2020-11-01T19:00:00.000Z') }, + summer: { start: new Date('2020-03-08T16:30:00.000Z'), end: new Date('2020-03-08T18:00:00.000Z') }, +}; + +async function configureScheduler(page, { start, end }: { start: Date; end: Date }) { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: start, + endDate: end, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO', + text: TEST_APPOINTMENT_TEXT, + }], + timeZone: 'America/Los_Angeles', + currentView: 'week', + currentDate: start, + startDayHour: 9, + cellDuration: 30, + width: 1000, + height: 585, + }); +} + +test.describe('Editing recurrent appointment in DST time', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Editing popup: winter time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.winter); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const saveButton = popup.locator('.dx-popup-done.dx-button'); + await saveButton.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__popup__winter-time.png`, { element: workSpace }); + }); + + test('Editing popup: summer time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.summer); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const saveButton = popup.locator('.dx-popup-done.dx-button'); + await saveButton.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__popup__summer-time.png`, { element: workSpace }); + }); + + test('Drag-n-drop up: winter time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.winter); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(1); + await appointment.dragTo(targetCell); + + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__drag-n-drop-up__winter-time.png`, { element: workSpace }); + }); + + test('Resize bottom: winter time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.winter); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__resize-bottom__winter-time.png`, { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts new file mode 100644 index 000000000000..ab71a0e3b9ed --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts @@ -0,0 +1,107 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const SCREENSHOT_BASE_NAME = 'without-timezone-recurrent'; +const TEST_TIMEZONES = ['Etc/GMT-10', 'Etc/GMT+1', 'Etc/GMT+10']; + +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; + +async function createTimezoneSelect(page, items: string[]): Promise { + await page.evaluate(({ tzItems }) => { + ($('#container') as any).dxSelectBox({ + items: tzItems, + width: 240, + value: tzItems[1], + onValueChanged(data: any) { + const scheduler = ($('#otherContainer') as any).dxScheduler('instance'); + scheduler.option('timeZone', data.value); + }, + }); + }, { tzItems: items }); +} + +async function selectTimezoneInUI(page, timezoneIdx: number): Promise { + await page.locator('#container').click(); + const listItems = page.locator('.dx-list-item'); + await listItems.nth(timezoneIdx).click(); +} + +test.describe('Recurrent appointments without timezone in scheduler with timezone', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly display the recurrent weekly appointment without timezone', async ({ page }) => { + await createTimezoneSelect(page, TEST_TIMEZONES); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: new Date('2021-04-28T11:00:00.000Z'), + endDate: new Date('2021-04-28T13:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: TEST_TIMEZONES[1], + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, '#otherContainer'); + + const schedulerWorkspace = page.locator('#otherContainer .dx-scheduler-work-space'); + + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__same-timezone'), { element: schedulerWorkspace }); + + await selectTimezoneInUI(page, 0); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__greater-timezone'), { element: schedulerWorkspace }); + + await selectTimezoneInUI(page, 2); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__lower-timezone'), { element: schedulerWorkspace }); + }); + + test('Should correctly display morning weekly recurrent appointment in a greater timezone', async ({ page }) => { + await createTimezoneSelect(page, TEST_TIMEZONES); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test', + startDate: new Date('2021-04-29T15:00:00.000Z'), + endDate: new Date('2021-04-29T17:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=FR', + }], + timeZone: TEST_TIMEZONES[0], + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, '#otherContainer'); + + const schedulerWorkspace = page.locator('#otherContainer .dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-morning-appointment__greater-timezone'), { element: schedulerWorkspace }); + }); + + test('Should correctly display corner weekly recurrent appointments in a greater timezone', async ({ page }) => { + await createTimezoneSelect(page, TEST_TIMEZONES); + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'test 1', startDate: new Date('2021-04-24T14:00:00.000Z'), endDate: new Date('2021-04-24T16:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU' }, + { text: 'test 2', startDate: new Date('2021-05-01T12:00:00.000Z'), endDate: new Date('2021-05-01T14:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA' }, + ], + timeZone: TEST_TIMEZONES[0], + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, '#otherContainer'); + + const schedulerWorkspace = page.locator('#otherContainer .dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-corner-appointments__greater-timezone'), { element: schedulerWorkspace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts new file mode 100644 index 000000000000..48f75d9126fa --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts @@ -0,0 +1,79 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const SCREENSHOT_BASE_NAME = 'timezone-monthly-recurrent'; +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; + +const MINUTES_TO_MILLISECONDS = 60000; +const HOURS_TO_MILLISECONDS = MINUTES_TO_MILLISECONDS * 60; + +const generateTimezoneOffsets = (): Record => { + const result: Record = {}; + new Array(27).fill(0).forEach((_, idx) => { + const timezoneIdx = idx - 14; + if (timezoneIdx < 0) result[`Etc/GMT${timezoneIdx}`] = timezoneIdx * -1; + else if (timezoneIdx > 0) result[`Etc/GMT+${timezoneIdx}`] = timezoneIdx * -1; + else result['Etc/GMT'] = 0; + }); + return result; +}; + +const TIMEZONE_OFFSETS = generateTimezoneOffsets(); + +const getAppointmentTime = (desiredDate: Date, timezone: string): Date => { + const localOffset = desiredDate.getTimezoneOffset() * MINUTES_TO_MILLISECONDS; + const timezoneOffset = TIMEZONE_OFFSETS[timezone] * HOURS_TO_MILLISECONDS; + return new Date(desiredDate.getTime() - localOffset - timezoneOffset); +}; + +async function screenshotTest(page, screenshotName: string): Promise { + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, screenshotName), { element: workSpace }); +} + +const schedulerOptions = (appointmentTimezone: string, schedulerTimezone: string, startDate: Date, endDate: Date, recurrenceRule: string, currentDate?: Date) => ({ + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(startDate, appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(endDate, appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule, + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: currentDate ?? new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, +}); + +test.describe('Monthly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 12, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'same-date__same-timezone'); + }); + + test('greater timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT+10', 'Etc/GMT-2', new Date(2021, 3, 28, 22, 0, 0), new Date(2021, 3, 29, 0, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'same-date__greater-timezone'); + }); + + test('lower timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT-2', 'Etc/GMT+10', new Date(2021, 3, 28, 0, 0, 0), new Date(2021, 3, 28, 2, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'same-date__lower-timezone'); + }); + + test('lower date same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT-2', 'Etc/GMT-2', new Date(2021, 3, 26, 10, 0, 0), new Date(2021, 3, 26, 12, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'lower-date__same-timezone'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts new file mode 100644 index 000000000000..f3aa592ac5b3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts @@ -0,0 +1,84 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const SCREENSHOT_BASE_NAME = 'timezone-weekly-recurrent'; +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; + +const MINUTES_TO_MILLISECONDS = 60000; +const HOURS_TO_MILLISECONDS = MINUTES_TO_MILLISECONDS * 60; + +const generateTimezoneOffsets = (): Record => { + const result: Record = {}; + new Array(27).fill(0).forEach((_, idx) => { + const timezoneIdx = idx - 14; + if (timezoneIdx < 0) result[`Etc/GMT${timezoneIdx}`] = timezoneIdx * -1; + else if (timezoneIdx > 0) result[`Etc/GMT+${timezoneIdx}`] = timezoneIdx * -1; + else result['Etc/GMT'] = 0; + }); + return result; +}; + +const TIMEZONE_OFFSETS = generateTimezoneOffsets(); + +const getAppointmentTime = (desiredDate: Date, timezone: string): Date => { + const localOffset = desiredDate.getTimezoneOffset() * MINUTES_TO_MILLISECONDS; + const timezoneOffset = TIMEZONE_OFFSETS[timezone] * HOURS_TO_MILLISECONDS; + return new Date(desiredDate.getTime() - localOffset - timezoneOffset); +}; + +async function screenshotTest(page, screenshotName: string): Promise { + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, screenshotName), { element: workSpace }); +} + +const makeOptions = (apptTz: string, schedTz: string, start: Date, end: Date, rule: string, currentDate?: Date) => ({ + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(start, apptTz), + startDateTimeZone: apptTz, + endDate: getAppointmentTime(end, apptTz), + endDateTimeZone: apptTz, + recurrenceRule: rule, + text: 'Test', + }], + timeZone: schedTz, + currentView: 'week', + currentDate: currentDate ?? new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, +}); + +test.describe('Weekly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('one day same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 12, 0, 0), 'FREQ=WEEKLY;BYDAY=WE')); + await screenshotTest(page, 'one-appointment__same-timezone'); + }); + + test('one day greater timezone with day shift', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+10', 'Etc/GMT-2', new Date(2021, 3, 28, 22, 0, 0), new Date(2021, 3, 29, 0, 0, 0), 'FREQ=WEEKLY;BYDAY=WE')); + await screenshotTest(page, 'one-appointment__day-shift__greater-timezone'); + }); + + test('one day lower timezone with day shift', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT-10', 'Etc/GMT+2', new Date(2021, 3, 28, 6, 0, 0), new Date(2021, 3, 28, 8, 0, 0), 'FREQ=WEEKLY;BYDAY=WE')); + await screenshotTest(page, 'one-appointment__day-shift__lower-timezone'); + }); + + test('multiple day first week same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 14, 0, 0), 'FREQ=WEEKLY;BYDAY=TU,WE,TH')); + await screenshotTest(page, 'multiple-appointment__first-week__same-timezone'); + }); + + test('multiple day second week same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 14, 0, 0), 'FREQ=WEEKLY;BYDAY=TU,WE,TH', new Date(2021, 4, 5))); + await screenshotTest(page, 'multiple-appointment__second-week__same-timezone'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts new file mode 100644 index 000000000000..8ef5e3ede52b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts @@ -0,0 +1,81 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const SCREENSHOT_BASE_NAME = 'timezone-yearly-recurrent'; +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; +const MINUTES_TO_MILLISECONDS = 60000; +const HOURS_TO_MILLISECONDS = MINUTES_TO_MILLISECONDS * 60; + +const generateTimezoneOffsets = (): Record => { + const result: Record = {}; + new Array(27).fill(0).forEach((_, idx) => { + const timezoneIdx = idx - 14; + if (timezoneIdx < 0) result[`Etc/GMT${timezoneIdx}`] = timezoneIdx * -1; + else if (timezoneIdx > 0) result[`Etc/GMT+${timezoneIdx}`] = timezoneIdx * -1; + else result['Etc/GMT'] = 0; + }); + return result; +}; +const TIMEZONE_OFFSETS = generateTimezoneOffsets(); +const getAppointmentTime = (desiredDate: Date, timezone: string): Date => { + const localOffset = desiredDate.getTimezoneOffset() * MINUTES_TO_MILLISECONDS; + const timezoneOffset = TIMEZONE_OFFSETS[timezone] * HOURS_TO_MILLISECONDS; + return new Date(desiredDate.getTime() - localOffset - timezoneOffset); +}; + +async function screenshotTest(page, screenshotName: string): Promise { + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, screenshotName), { element: workSpace }); +} + +const makeOptions = (apptTz: string, schedTz: string, start: Date, end: Date, currentDate?: Date) => ({ + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(start, apptTz), + startDateTimeZone: apptTz, + endDate: getAppointmentTime(end, apptTz), + endDateTimeZone: apptTz, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedTz, + currentView: 'week', + currentDate: currentDate ?? new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, +}); + +test.describe('Yearly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 12, 0, 0))); + await screenshotTest(page, 'same-date__same-timezone'); + }); + + test('greater timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+10', 'Etc/GMT-2', new Date(2021, 3, 28, 14, 0, 0), new Date(2021, 3, 28, 16, 0, 0))); + await screenshotTest(page, 'same-date__greater-timezone'); + }); + + test('lower timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT-2', 'Etc/GMT+10', new Date(2021, 3, 28, 4, 0, 0), new Date(2021, 3, 28, 6, 0, 0))); + await screenshotTest(page, 'same-date__lower-timezone'); + }); + + test('lower date same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 26, 10, 0, 0), new Date(2021, 3, 26, 12, 0, 0))); + await screenshotTest(page, 'lower-date__same-timezone'); + }); + + test('greater date same timezone next view date', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 29, 10, 0, 0), new Date(2021, 3, 29, 12, 0, 0), new Date(2022, 3, 28))); + await screenshotTest(page, 'greater-date__same-timezone__next-view-date'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/hideTooltip.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/hideTooltip.spec.ts new file mode 100644 index 000000000000..bd3e230a69ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/hideTooltip.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Hide tooltip', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Appointment tooltip should be hidden when drag is started', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['day'], + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 3, 26, 9), + endDate: new Date(2021, 3, 26, 9, 30), + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0); + await appointment.dragTo(targetCell); + + await expect(tooltip).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/tooltipBehavior.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/tooltipBehavior.spec.ts new file mode 100644 index 000000000000..961864026c36 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/tooltipBehavior.spec.ts @@ -0,0 +1,139 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const tooltipDataSource = [{ + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 12, 0), +}]; + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + width: 600, + height: 600, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Appointment tooltip behavior during scrolling in the Scheduler (T755449)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('The tooltip of collector should not scroll page and immediately hide', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [{ + type: 'week', + name: 'week', + maxAppointmentsPerCell: '0', + }], + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + currentView: 'week', + dataSource: [ + { text: 'A', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'B', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'C', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'D', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'E', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'F', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'G', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + ], + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '7' }); + await collector.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + }); + + test('The tooltip should not hide after automatic scrolling during an appointment click', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: tooltipDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + }); + + test('The tooltip should hide after manually scrolling in the browser', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: tooltipDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + await page.evaluate(() => { window.scroll(0, 100); }); + await page.waitForTimeout(500); + + await expect(tooltip).not.toBeVisible(); + }); + + [false, true].forEach((adaptivityEnabled) => { + test(`The tooltip screenshot (adaptivityEnabled=${adaptivityEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: tooltipDataSource, + adaptivityEnabled, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + const tooltipNamePrefix = adaptivityEnabled ? 'mobile' : 'desktop'; + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, `appointment-${tooltipNamePrefix}-tooltip-screenshot.png`, { element: scheduler }); + }); + }); + + test('Collector tooltip focused list item screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Text', startDate: new Date(2017, 4, 22, 9, 30, 0, 0), endDate: new Date(2017, 4, 22, 10, 30, 0, 0) }, + { text: 'Text2', startDate: new Date(2017, 4, 22, 9, 30, 0, 0), endDate: new Date(2017, 4, 22, 10, 30, 0, 0) }, + { text: 'Text3', startDate: new Date(2017, 4, 22, 9, 30, 0, 0), endDate: new Date(2017, 4, 22, 10, 30, 0, 0) }, + ], + views: [{ + type: 'month', + maxAppointmentsPerCell: 1, + }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2 more' }); + await expect(collector).toBeVisible(); + await collector.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + await page.keyboard.press('Tab'); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'collector-tooltip-focused-list-item.png', { element: scheduler }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/twoSchedulers.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/twoSchedulers.spec.ts new file mode 100644 index 000000000000..b7b682550e3e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/twoSchedulers.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')}`; + +test.describe('Interaction of two schedulers', () => { + 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 createScheduler = async (container): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentDate: new Date(2022, 3, 5), + height: 600, + views: ['day'], + currentView: 'day', + }, container); +}; + +test('First scheduler should work after removing second (T1063130)', async ({ page }) => { + // Scheduler on '#container' + const { navigator } = scheduler.toolbar; + + await (navigator.nextButton).click() + .expect(navigator.caption.textContent).toBe('6 April 2022'); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createScheduler('#container'); + await createScheduler('#otherContainer'); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1091980.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1091980.spec.ts new file mode 100644 index 000000000000..738854dcf46e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1091980.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler: Virtual scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly render virtual table if scheduler sizes are set in % (T1091980)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: '100%', + height: '100%', + dataSource: [], + views: [{ + type: 'week', + intervalCount: 10, + }], + currentView: 'week', + currentDate: new Date(2021, 3, 5), + startDayHour: 8, + endDayHour: 20, + crossScrollingEnabled: true, + scrolling: { + mode: 'virtual', + }, + }); + + const allDayCellCount = await page.locator('.dx-scheduler-all-day-table-cell').count(); + expect(allDayCellCount).toBe(24); + + const dateTableCellCount = await page.locator('.dx-scheduler-date-table-cell').count(); + expect(dateTableCellCount).toBe(576); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(2021, 5, 12, 19)); + }); + + const allDayCellCountAfter = await page.locator('.dx-scheduler-all-day-table-cell').count(); + expect(allDayCellCountAfter).toBe(24); + + const dateTableCellCountAfter = await page.locator('.dx-scheduler-date-table-cell').count(); + expect(dateTableCellCountAfter).toBe(576); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1258030.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1258030.spec.ts new file mode 100644 index 000000000000..8d5afe7ccff5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1258030.spec.ts @@ -0,0 +1,39 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function scrollTo(page, x: number, y: number): Promise { + await page.evaluate(({ sx, sy }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: sy, x: sx }); + }, { sx: x, sy: y }); +} + +test.describe('Scheduler: Virtual scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should render recurrence appointment with correct width in month timeline view for virtual scrolling', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 300, + currentView: 'timelineMonth', + views: ['timelineMonth'], + currentDate: new Date(2024, 9, 1), + dataSource: [{ + text: 'appointment', + startDate: new Date(2024, 9, 1), + endDate: new Date(2024, 9, 2), + recurrenceRule: 'FREQ=DAILY', + }], + scrolling: { mode: 'virtual' }, + }); + + await scrollTo(page, 3000, 0); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, 'virtual_scroll_timeline_3000.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1287345.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1287345.spec.ts new file mode 100644 index 000000000000..0a68e5bf4211 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1287345.spec.ts @@ -0,0 +1,43 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, insertStylesheetRulesToPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function scrollTo(page, x: number, y: number): Promise { + await page.evaluate(({ sx, sy }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: sy, x: sx }); + }, { sx: x, sy: y }); +} + +test.describe('Scheduler: Virtual scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Cell width set in css should be correct for virtual scrolling after scroll down (T1287345)', async ({ page }) => { + await insertStylesheetRulesToPage(page, ` + #container .dx-scheduler-cell-sizes-horizontal { + width: 200px !important; + }`); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'week', + scrolling: { + mode: 'virtual', + }, + currentDate: new Date(2021, 2, 28), + height: 300, + }); + + await scrollTo(page, 0, 3000); + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + await nextButton.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, 'virtual_scroll_cell_width.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/appointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/appointments.spec.ts new file mode 100644 index 000000000000..63008b0046c9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/appointments.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, setStyleAttribute, getStyleAttribute } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function scrollToDate(page, date: Date, groups?: Record): Promise { + await page.evaluate(({ d, g }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(d), g); + }, { d: date.toISOString(), g: groups }); +} + +test.describe('Scheduler: Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test.skip('Appointment should not repaint after scrolling if present on viewport', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + width: 800, + currentDate: new Date(2020, 8, 7), + scrolling: { + mode: 'virtual', + orientation: 'both', + outlineCount: 0, + }, + currentView: 'week', + views: [{ + type: 'week', + intervalCount: 10, + }], + dataSource: [{ + startDate: new Date(2020, 8, 13, 2), + endDate: new Date(2020, 8, 13, 3), + text: 'test', + }], + }); + + const element = page.locator('.dx-scheduler-appointment').nth(0); + + await setStyleAttribute(page, element, 'background-color: red;'); + const style1 = await getStyleAttribute(page, element); + expect(style1).toBe('transform: translate(525px, 200px); width: 49px; height: 100px; background-color: red;'); + + await scrollToDate(page, new Date(2020, 8, 17, 4)); + + const style2 = await getStyleAttribute(page, element); + expect(style2).toBe('transform: translate(525px, 200px); width: 49px; height: 100px; background-color: red;'); + }); + + test('The appointment should render correctly when scrolling vertically (T1263428)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 500, + width: 900, + timeZone: 'Europe/Vienna', + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ssxx', + currentDate: new Date(2024, 10, 11, 20, 54, 23, 361), + cellDuration: 20, + firstDayOfWeek: 1, + startDayHour: 12.0, + endDayHour: 18.0, + allDayPanelMode: 'hidden', + scrolling: { + mode: 'virtual', + }, + crossScrollingEnabled: true, + currentView: 'week', + textExpr: 'Subject', + startDateExpr: 'StartDate', + endDateExpr: 'EndDate', + views: [{ + type: 'week', + groupByDate: true, + startDayHour: 6.0, + endDayHour: 22.0, + }], + dataSource: [{ + Subject: 'Website Re-Design Plan', + StartDate: new Date('2024-11-11T12:10:00+0100'), + EndDate: new Date('2024-11-12T21:00:00+0100'), + }], + }); + + await scrollToDate(page, new Date('2024-11-12T09:00:00+0100')); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'T1263428-virtual-scrolling-render-appointment.png', { + element: scheduler, + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/layout.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/layout.spec.ts new file mode 100644 index 000000000000..e6f84f71b1f2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/layout.spec.ts @@ -0,0 +1,143 @@ +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('Scheduler: 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'); + }); + +resources, + createDataSetForScreenShotTests, + views, + horizontalViews, + scrollConfig, + groupedByDateViews, +} from './utils'; + +); + +const createScheduler = async ( + additionalProps: Record, +): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2021, 0, 1), + height: 600, + resources, + views, + currentView: 'day', + scrolling: { mode: 'virtual' }, + startDayHour: 0, + endDayHour: 3, + ...additionalProps, + }); +}; + +test('Virtual scrolling layout in scheduler views', async ({ page }) => { + // --- setup --- +await createScheduler({ + // --- test --- +// Scheduler on '#container' + + // TODO: views[0] is day view and we have a bug in its CSS + // It is not advisable to create screenshots for incorrect layout + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + + await scheduler.option('currentView', view.type); + await scrollToDate(scrollConfig[i].firstDate); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll.png`); + + await scrollToDate(scrollConfig[i].lastDate); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll.png`); + } + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Virtual scrolling layout in scheduler views when horizontal grouping is enabled', async ({ page }) => { + // --- setup --- +await createScheduler({ + views: horizontalViews, + groups: ['resourceId'], + // --- test --- +// Scheduler on '#container' + + // TODO: views[0] is day view and we have a bug in its CSS + // It is not advisable to create screenshots for incorrect layout + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + + await scheduler.option('currentView', view.type); + await scrollToDate(scrollConfig[i].firstDate, { resourceId: 6 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll-horizontal-grouping.png`); + + await scrollToDate(scrollConfig[i].lastDate, { resourceId: 0 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll-horizontal-grouping.png`); + } + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Virtual scrolling layout in scheduler views when grouping by date is enabled', async ({ page }) => { + // --- setup --- +await createScheduler({ + views: groupedByDateViews, + groups: ['resourceId'], + // --- test --- +// Scheduler on '#container' + + // TODO: views[0] is day view and we have a bug in its CSS + // It is not advisable to create screenshots for incorrect layout + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + + await scheduler.option('currentView', view.type); + + await scrollToDate(scrollConfig[i].firstDate, { resourceId: 3 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll-grouping-by-date.png`); + + await scrollToDate(scrollConfig[i].lastDate, { resourceId: 0 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll-grouping-by-date.png`); + } + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Header cells should be aligned with date-table cells in timeline-month when current date changes and virtual scrolling is used', async ({ page }) => { + // --- setup --- +await createScheduler({ + currentDate: new Date(2020, 10, 1), + currentView: 'timelineMonth', + // --- test --- + // Scheduler on '#container' + + await scheduler.option('currentDate', new Date(2020, 11, 1)); + + await testScreenshot(page, 'virtual-scrolling-timeline-month-change-current-date-virtual.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/many-cells.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/many-cells.spec.ts new file mode 100644 index 000000000000..0244515d0af2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/many-cells.spec.ts @@ -0,0 +1,79 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, generateOptionMatrix } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const buildScreenshotName = (viewType: string, orientation: string, step: string) => `virtual-scrolling-many-cells-${viewType}-${orientation}-${step}.png`; + +async function scrollTo(page, date: Date, groups?: Record): Promise { + await page.evaluate(({ d, g }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(d), g); + }, { d: date.toISOString(), g: groups }); +} + +const testCases = generateOptionMatrix({ + viewType: ['month', 'week', 'workWeek'], + groupOrientation: ['horizontal', 'vertical'], +}); + +test.describe('Scheduler: Virtual scrolling (many cells)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + testCases.forEach(({ viewType, groupOrientation }) => { + const resourceCount = 400; + + test(`it should correctly render virtual table if a lot of resources are presented for ${viewType} view and ${groupOrientation} orientation (T1205597, T1137490)`, async ({ page }) => { + const resources = Array.from({ length: resourceCount }, (_, i) => ({ + id: i, + text: `Resource ${i}`, + })); + + const appointmentDateInfo = Array.from({ length: 29 }) + .map((_, i) => ({ + startDate: new Date(2024, 1, i + 1, 1), + endDate: new Date(2024, 1, i + 1, 4), + })); + + const appointments = Array.from({ length: resourceCount }) + .map((_, resourceIndex) => appointmentDateInfo.map(({ startDate, endDate }) => ({ + text: `Appointment for Resource ${resourceIndex}`, + startDate, + endDate, + groupId: resourceIndex, + }))) + .flat(); + + await createWidget(page, 'dxScheduler', { + height: 600, + currentDate: new Date(2024, 1, 1), + dataSource: appointments, + views: [{ + type: viewType, + groupOrientation, + }], + currentView: viewType, + scrolling: { + mode: 'virtual', + }, + groups: ['groupId'], + resources: [{ + fieldExpr: 'groupId', + dataSource: resources, + label: 'Group', + }], + }); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, buildScreenshotName(viewType, groupOrientation, 'start'), { element: scheduler }); + + await scrollTo(page, new Date(2024, 1, 1, 1), { groupId: resourceCount / 2 }); + await testScreenshot(page, buildScreenshotName(viewType, groupOrientation, 'middle'), { element: scheduler }); + + await scrollTo(page, new Date(2024, 1, 1, 1), { groupId: resourceCount - 1 }); + await testScreenshot(page, buildScreenshotName(viewType, groupOrientation, 'end'), { element: scheduler }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/resources.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/resources.spec.ts new file mode 100644 index 000000000000..7aa1b6a1aab4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/resources.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler: Generic theme layout', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly render view if virtual scrolling and groupByDate', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + width: 200, + dataSource: [{ + userId: 1, + startDate: new Date(2022, 0, 16, 14, 30), + endDate: new Date(2022, 0, 16, 15), + }], + currentDate: new Date(2022, 0, 15), + views: ['month'], + currentView: 'month', + groupByDate: true, + groups: ['userId'], + resources: [{ + fieldExpr: 'userId', + allowMultiple: false, + dataSource: [ + { id: 1, text: 'User 1' }, + { id: 2, text: 'User 2' }, + { id: 3, text: 'User 3' }, + { id: 4, text: 'User 4' }, + { id: 5, text: 'User 5' }, + ], + label: 'User', + }], + scrolling: { + mode: 'virtual', + }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await expect(appointment).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/zooming.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/zooming.spec.ts new file mode 100644 index 000000000000..79b4488b4dc8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/zooming.spec.ts @@ -0,0 +1,94 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const resources = [{ + fieldExpr: 'resourceId', + allowMultiple: true, + dataSource: [ + { text: 'Resource 0', id: 0, color: '#20B2AA' }, + { text: 'Resource 1', id: 1, color: '#87CEEB' }, + { text: 'Resource 2', id: 2, color: '#228B22' }, + { text: 'Resource 3', id: 3, color: '#98FB98' }, + { text: 'Resource 4', id: 4, color: '#2E8B57' }, + { text: 'Resource 5', id: 5, color: '#66CDAA' }, + { text: 'Resource 6', id: 6, color: '#008080' }, + { text: 'Resource 7', id: 7, color: '#00FFFF' }, + ], + label: 'Priority', +}]; + +const views = [ + { type: 'day', intervalCount: 7, endDayHour: 8 }, + { type: 'week', intervalCount: 10, endDayHour: 8 }, + { type: 'month' }, + { type: 'timelineDay', intervalCount: 7 }, + { type: 'timelineWeek', intervalCount: 3 }, + { type: 'timelineMonth' }, +]; + +const horizontalViews = views.map((view) => ({ ...view, groupOrientation: 'horizontal' })); + +const scrollConfig = [ + { firstDate: new Date(2021, 0, 7), lastDate: new Date(2021, 0, 1) }, + { firstDate: new Date(2021, 0, 15), lastDate: new Date(2020, 11, 27) }, + { firstDate: new Date(2021, 0, 1), lastDate: new Date(2020, 11, 27) }, + { firstDate: new Date(2021, 0, 7), lastDate: new Date(2021, 0, 1) }, + { firstDate: new Date(2021, 0, 15), lastDate: new Date(2020, 11, 27) }, + { firstDate: new Date(2021, 0, 30), lastDate: new Date(2021, 0, 1) }, +]; + +async function scrollToDate(page, date: Date, groups?: Record): Promise { + await page.evaluate(({ d, g }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(d), g); + }, { d: date.toISOString(), g: groups }); +} + +async function setZoomLevel(page, zoomLevel: number): Promise { + await page.evaluate((z) => { + $('body').css('zoom', `${z}%`); + }, zoomLevel); +} + +async function setOption(page, optionName: string, value: unknown): Promise { + await page.evaluate(({ opt, val }) => { + ($('#container') as any).dxScheduler('instance').option(opt, val); + }, { opt: optionName, val: value }); +} + +test.describe('Scheduler: Virtual Scrolling with Zooming', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Virtual scrolling layout in scheduler views when horizontal grouping is enabled and zooming is used', async ({ page }) => { + await setZoomLevel(page, 125); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 0, 1), + height: 600, + resources, + views: horizontalViews, + currentView: 'day', + scrolling: { mode: 'virtual' }, + startDayHour: 0, + endDayHour: 3, + groups: ['resourceId'], + }); + + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + await setOption(page, 'currentView', view.type); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll-horizontal-grouping-scaling.png`); + + await scrollToDate(page, scrollConfig[i].firstDate, { resourceId: 7 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll-horizontal-grouping-scaling.png`); + } + + await setZoomLevel(page, 0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/workSpace.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/workSpace.spec.ts new file mode 100644 index 000000000000..5942b1526f3a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/workSpace.spec.ts @@ -0,0 +1,373 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scheduler: Workspace', () => { + 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 FIXED_PARENT_CONTAINER_SIZE = ` +#parentContainer { + width: 400px; + height: 500px; +} + +#container { + height: 100%; +} +`; + +const createScheduler = async (options = {}): Promise => { + await createWidget(page, 'dxScheduler', extend(options, { + dataSource: [], + startDayHour: 9, + height: 600, + })); +}; + +const getResourcesDataSource = (count: number) => new Array(count) + .fill(null) + .map((_, idx) => ({ + id: idx, + name: idx.toString(), + })); + +test('Vertical selection between two workspace cells should focus cells between them (T804954)', async ({ page }) => { + // Scheduler on '#container' + + await t + .dragToElement(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0), page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(0)) + .expect(page.locator('.dx-scheduler-date-table-cell').filter('.dx-state-focused').count).toBe(4); +}).before(async () => createScheduler({ + views: [{ name: '2 Days', type: 'day', intervalCount: 2 }], + currentDate: new Date(2015, 1, 9), + currentView: 'day', +})); + +test('Horizontal selection between two workspace cells should focus cells between them', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2019, 4, 1), + height: 250, + // --- test --- +// Scheduler on '#container' + + await t + .dragToElement(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0), page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(3)) + .expect(page.locator('.dx-scheduler-date-table-cell').filter('.dx-state-focused').count) + .eql(4); +}).before(async () => createScheduler({ + views: ['timelineWeek'], + currentDate: new Date(2015, 1, 9), + currentView: 'timelineWeek', + groups: ['roomId'], + resources: [{ + fieldExpr: 'roomId', + label: 'Room', + dataSource: [{ + text: '1', id: 1, + }, { + text: '2', id: 2, + }], + }], +})); + +test('Vertical grouping should work correctly when there is one group', async ({ page }) => { + // --- setup --- + await { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: viewName, + currentDate: '2024-01-01T00:00:00', + crossScrollingEnabled: true, + height: 300, + }; + // --- test --- +// Scheduler on '#container' + + expect(page.locator('.dx-scheduler-date-table-cell').count) + .eql(336); +}).before(async () => createWidget(page, 'dxScheduler', { + views: [{ + type: 'week', + groupOrientation: 'vertical', + }], + currentView: 'week', + dataSource: [], + groups: ['priorityId'], + resources: [{ + field: 'priorityId', + dataSource: [{ id: 1, color: 'black' }], + }], + height: 600, +})); + +async function hideShow(page: Page, container) { + await page.evaluate((container) => { + const instance = ($(container) as any).dxScheduler('instance'); + instance.option('visible', !instance.option('visible')); +}, container); +} + +async function resize(page: Page, container) { + await page.evaluate((container) => { + const instance = ($(container) as any).dxScheduler('instance'); + // eslint-disable-next-line no-underscore-dangle + instance._dimensionChanged(); + // eslint-disable-next-line no-underscore-dangle + instance._workSpace._dimensionChanged(); +}, container); +} + +test('Hidden scheduler should not resize', async ({ page }) => { + await hideShow('#container'); + await resize('#container'); + await hideShow('#container'); + + await testScreenshot(page, 'scheduler-after-hiding-and-resizing.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'Google AdWords Strategy', + ownerId: [2], + startDate: new Date('2021-02-01T16:00:00.000Z'), + endDate: new Date('2021-02-01T17:30:00.000Z'), + priority: 1, + }, + ], + resources: [ + { + fieldExpr: 'priority', + dataSource: [ + { + text: 'Priority 1', + id: 1, + color: '#1e90ff', + }, + ], + label: 'Priority', + }, + ], + groups: ['priority'], + views: [ + { + type: 'timelineMonth', + groupOrientation: 'vertical', + }, + ], + crossScrollingEnabled: true, + currentView: 'timelineMonth', + currentDate: new Date(2021, 1, 1), + height: 400, +})); + +test('All day panel should be hidden when allDayPanelMode=hidden by initializing scheduler', async ({ page }) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-all-day-title').exists) + .eql(false); + + expect(page.locator('.dx-scheduler-all-day-table-row').exists) + .eql(false); +}).before(async () => createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 2, 28), + currentView: 'day', + allDayPanelMode: 'hidden', + dataSource: [{ + text: 'Book Flights to San Fran for Sales Trip', + startDate: new Date('2021-03-28T17:00:00.000Z'), + endDate: new Date('2021-03-28T18:00:00.000Z'), + allDay: true, + }, { + text: 'Customer Workshop', + startDate: new Date('2021-03-29T17:30:00.000Z'), + endDate: new Date('2021-04-03T19:00:00.000Z'), + }], +})); + +// visual: generic.light +// visual: fluent.blue.light +// visual: material.blue.light +test('Month workspace should be scrollable to the last row (T1203250)', async ({ page }) => { + // Scheduler on '#container' + await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (new Date(2019, 5, 8, 0, 0).toISOString())); + + await testScreenshot(page, 'scrollable-month-workspace.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +// visual: generic.light +// visual: generic.dark +// visual: fluent.blue.light +// visual: fluent.blue.dark +// visual: fluent.saas.light +// visual: fluent.saas.dark +// visual: material.blue.light +// visual: material.blue.dark +test('Check cell hover state', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 4, 1), + height: 500, + // --- test --- +// arrange + // Scheduler on '#container' + const firstDateTableCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + // act + await (firstDateTableCell).hover() + .expect(firstDateTableCell.hasClass(CLASS.hoverCell)) + .ok(); + + // assert + await testScreenshot(page, 'scheduler-week-cell-hover-state.png', { element: page.locator('.dx-scheduler-work-space') }); + + await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1).hover()) + .expect(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1).hasClass(CLASS.hoverCell)) + .ok() + ; +}); +}); + +test('Check cell active state', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 4, 1), + height: 500, + // --- test --- +// arrange + // Scheduler on '#container' + const firstDateTableCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + // act + await (firstDateTableCell).hover() + .expect(firstDateTableCell.hasClass(CLASS.hoverCell)) + .ok() + .dispatchEvent(firstDateTableCell, 'mousedown') + .expect(firstDateTableCell.hasClass(CLASS.activeCell)) + .ok(); + + // assert + await testScreenshot(page, 'scheduler-week-cell-active-state.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t + .dispatchEvent(firstDateTableCell, 'mouseup') + .expect(firstDateTableCell.hasClass(CLASS.activeCell)) + .notOk() + .hover(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1)) + .expect(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1).hasClass(CLASS.hoverCell)) + .ok() + ; +}); +}); + +[ + 'day', + 'week', + 'workWeek', + 'month', +].forEach((viewName) => { + test(`[T1225772]: should not have the horizontal scroll in horizontal views when the crossScrollingEnabled: true (view:${viewName})`, async ({ page }) => { + // Scheduler on '#container' + + const scrollableContainer = page.locator('.dx-scheduler-date-table')ScrollableContainer; + const scrollWidth = await scrollableContainer.scrollWidth; + const clientWidth = await scrollableContainer.clientWidth; + const hasHorizontalScroll = scrollWidth > clientWidth; + + expect(hasHorizontalScroll).toBeFalsy(/* workspace has the horizontal scrollbar */); +}); + }); +}); + +// NOTE: Moved "as is" from the QUnit integration.resources.tests (see history) +test('[T716993]: should has horizontal scrollbar with multiple resources and fixed height container', async ({ page }) => { + // --- setup --- +const resourcesDataSource = getResourcesDataSource(10); + + await insertStylesheetRulesToPage(FIXED_PARENT_CONTAINER_SIZE); + return createWidget(page, 'dxScheduler', { + dataSource: [], + groups: ['id'], + resources: [{ + dataSource: resourcesDataSource, + displayExpr: 'name', + valueExpr: 'id', + fieldExpr: 'id', + allowMultiple: false, + }], + crossScrollingEnabled: true, + // --- test --- +// Scheduler on '#container' + + const scrollableContainer = page.locator('.dx-scheduler-date-table')ScrollableContainer; + const scrollWidth = await scrollableContainer.scrollWidth; + const clientWidth = await scrollableContainer.clientWidth; + const hasHorizontalScroll = scrollWidth > clientWidth; + + expect(hasHorizontalScroll).ok('workspace hasn\'t the horizontal scrollbar'); +}); +}); + +test('Scheduler appointments should change color on update resources', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-29T16:30:00.000Z'), + endDate: new Date('2021-03-29T18:30:00.000Z'), + resource: 1, + }], + views: ['week', 'month'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 730, + resources: [{ + fieldExpr: 'resource', + dataSource: [{ id: 1, text: 'res 1', color: 'red' }], + }], + }, '#otherContainer'); + await createWidget(page, 'dxButton', { + text: 'Change resources', + onClick() { + const schedulerWidget = ($('#otherContainer') as any).dxScheduler('instance'); + schedulerWidget.option('resources', [{ + fieldExpr: 'resource', + dataSource: [{ id: 1, text: 'new res 1', color: 'pink' }], + }]); + schedulerWidget.getDataSource().reload(); + }, + }, '#container'); + // --- test --- +// Button on '#container' + // Scheduler on '#otherContainer' + await (button.element).click(); + + await testScreenshot(page, 'scheduler-appointments-should-update-color.png', { element: page.locator('.dx-scheduler-work-space') }); + expect(compareResults.isValid()).ok(compareResults.errorMessages()); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/appointmentCollectorTimezone.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/appointmentCollectorTimezone.spec.ts new file mode 100644 index 000000000000..fe53a11775e9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/appointmentCollectorTimezone.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; + +test.describe('Scheduler - Appointment Collector Timezone', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + MACHINE_TIMEZONES.EuropeBerlin, + ].forEach((machineTimezone) => { + test(`Appointment collector button should have correct date (${machineTimezone})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== machineTimezone, `Skipping: machine timezone is ${browserTimezone}, expected ${machineTimezone}`); + + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-05T15:30:00.000Z'), + endDate: new Date('2021-03-05T17:00: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-05T23:45:00.000Z'), + endDate: new Date('2021-03-06T01:30:00.000Z'), + }, + ], + currentView: 'month', + currentDate: new Date(2021, 2, 1), + maxAppointmentsPerCell: 3, + }); + + const scheduler = page.locator('#container'); + await expect(scheduler).toBeVisible(); + + const collector = page.locator('.dx-scheduler-appointment-collector').first(); + const expectedDate = 'March 5, 2021'; + + const ariaRoleDescription = await collector.getAttribute('aria-roledescription'); + expect(ariaRoleDescription).toContain(expectedDate); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/check.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/check.spec.ts new file mode 100644 index 000000000000..7e60b0b97499 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/check.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +type CheckType = [MachineTimezonesType, string]; +const checks: CheckType[] = [ + [MACHINE_TIMEZONES.AmericaLosAngeles, 'Mon Jan 01 2024 10:00:00 GMT-0800 (Pacific Standard Time)'], + [MACHINE_TIMEZONES.EuropeBerlin, 'Mon Jan 01 2024 10:00:00 GMT+0100 (Central European Standard Time)'], +]; + +test.describe('Runner machine timezone checks', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + checks.forEach(([timezone, expectedResult]) => { + test(`${timezone} check`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + const dateFromBrowser = await page.evaluate( + () => new Date(2024, 0, 1, 10).toString(), + ); + expect(dateFromBrowser).toBe(expectedResult); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/dragAndDropDst.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/dragAndDropDst.spec.ts new file mode 100644 index 000000000000..b3aead60902c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/dragAndDropDst.spec.ts @@ -0,0 +1,186 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, insertStylesheetRulesToPage, generateOptionMatrix } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +interface TestCase { + timezone: MachineTimezonesType; + season: string; + currentDate: string; + startDate: Date; + cellIdxArray: [rowIdx: number, colIdx: number][]; + expectedTopPosition: number[]; +} + +const SCHEDULER_SELECTOR = '#container'; +const APPOINTMENT_TEXT = 'Appointment'; +const CUSTOM_CSS = ` +#container .dx-scheduler-header-panel-cell { + color: rgba(0,0,0,.54); +} + +#container .dx-scheduler-header-panel-cell::before { + display: none; +} + +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const DRAG_Y_OFFSET_PX = 12; + +const getAppointmentFromStartDate = (startDate: Date, offset: number) => { + const minuteMs = 60000; + const appointmentDurationMs = 60 * minuteMs; + return { + startDate: new Date(startDate.getTime() + offset * minuteMs), + endDate: new Date(startDate.getTime() + offset * minuteMs + appointmentDurationMs), + text: APPOINTMENT_TEXT, + }; +}; + +const BERLIN_SUMMER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + season: 'summer', + currentDate: '2024-03-31', + startDate: new Date('2024-03-30T23:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 25, 75, 100, 125, 150, 175], +}; + +const BERLIN_SUMMER_CASE_OFFSET: TestCase = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + season: 'summer', + currentDate: '2024-03-31', + startDate: new Date('2024-03-30T23:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const BERLIN_WINTER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + season: 'winter', + currentDate: '2024-10-27', + startDate: new Date('2024-10-26T22:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const LOS_ANGELES_SUMMER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + season: 'summer', + currentDate: '2024-03-10', + startDate: new Date('2024-03-10T08:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 25, 75, 100, 125, 150, 175], +}; + +const LOS_ANGELES_SUMMER_CASE_OFFSET: TestCase = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + season: 'summer', + currentDate: '2024-03-10', + startDate: new Date('2024-03-10T08:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const LOS_ANGELES_WINTER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + season: 'summer', + currentDate: '2024-11-03', + startDate: new Date('2024-11-03T07:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const ZERO_OFFSET_TEST_CASES = generateOptionMatrix({ + offset: [0], + testCase: [ + BERLIN_SUMMER_CASE, + BERLIN_WINTER_CASE, + LOS_ANGELES_SUMMER_CASE, + LOS_ANGELES_WINTER_CASE, + ], +}); + +const OFFSET_TEST_CASES = generateOptionMatrix({ + offset: [-360, 360], + testCase: [ + BERLIN_SUMMER_CASE_OFFSET, + BERLIN_WINTER_CASE, + LOS_ANGELES_SUMMER_CASE_OFFSET, + LOS_ANGELES_WINTER_CASE, + ], +}); + +test.describe('Scheduler render during DST - drag and drop', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + ...ZERO_OFFSET_TEST_CASES, + ...OFFSET_TEST_CASES, + ].forEach(({ + offset, + testCase: { + timezone, + season, + currentDate, + startDate, + cellIdxArray, + expectedTopPosition, + }, + }) => { + test(`Should drag-n-drop appointment correctly during around DST (${timezone}, ${season}, ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + + const dataSource = [getAppointmentFromStartDate(startDate, offset)]; + await createWidget(page, 'dxScheduler', { + timeZone: timezone, + dataSource, + currentView: 'week', + currentDate, + offset, + showCurrentTimeIndicator: false, + showAllDayPanel: false, + firstDayOfWeek: 4, + cellDuration: 60, + height: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + const initialHeight = await appointment.evaluate((el) => el.getBoundingClientRect().height); + const [[firstCellRowIdx, firstCellColIdx]] = cellIdxArray; + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(firstCellRowIdx) + .locator('.dx-scheduler-date-table-cell').nth(firstCellColIdx); + const firstCellTop = await firstCell.evaluate((el) => el.getBoundingClientRect().top); + + for (let idx = 0; idx < cellIdxArray.length; idx += 1) { + const [rowIdx, colIdx] = cellIdxArray[idx]; + const cell = page.locator('.dx-scheduler-date-table-row').nth(rowIdx) + .locator('.dx-scheduler-date-table-cell').nth(colIdx); + + // TODO: dragToElement with offsetY - Playwright dragTo doesn't support offset on target the same way + await appointment.dragTo(cell); + + const currentHeight = await appointment.evaluate((el) => el.getBoundingClientRect().height); + const currentTop = await appointment.evaluate((el) => el.getBoundingClientRect().top); + const relativeTop = currentTop - firstCellTop; + + expect(currentHeight).toBe(initialHeight); + expect(relativeTop).toBe(expectedTopPosition[idx]); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/recurrence/excludeFromRecurrence_T1225416.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/recurrence/excludeFromRecurrence_T1225416.spec.ts new file mode 100644 index 000000000000..48071ee9b232 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/recurrence/excludeFromRecurrence_T1225416.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, generateOptionMatrix } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +const SCHEDULER_SELECTOR = '#container'; +const MS_IN_MINUTE = 60000; +const MS_IN_HOUR = MS_IN_MINUTE * 60; +const APPOINTMENT_TEXT = 'TEST_APPT'; + +const getAppointments = ( + startDate: Date, + currentView: string, +) => [ + { + startDate, + endDate: new Date(startDate.getTime() + MS_IN_HOUR), + text: APPOINTMENT_TEXT, + recurrenceRule: currentView === 'week' ? 'FREQ=DAILY' : 'FREQ=WEEKLY;BYDAY=FR', + }, +]; + +const getFirstDayOfWeek = (currentView: string) => (currentView === 'week' ? 4 : 0); +const getAppointmentsCount = (currentView: string) => (currentView === 'week' ? 7 : 6); + +test.describe('Scheduler exclude from recurrence', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + generateOptionMatrix({ + timeZone: [undefined, 'America/New_York'] as (string | undefined)[], + currentView: ['week', 'month'], + location: [ + [MACHINE_TIMEZONES.EuropeBerlin, 'summer', '2024-03-31', new Date('2024-01-01T12:00:00Z')], + [MACHINE_TIMEZONES.EuropeBerlin, 'winter', '2024-10-27', new Date('2024-01-01T12:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'summer', '2024-03-10', new Date('2024-01-01T12:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'winter', '2024-11-03', new Date('2024-01-01T12:00:00Z')], + ] as [MachineTimezonesType, string, string, Date][], + }).forEach(({ + timeZone, + currentView, + location: [machineTimezone, caseName, currentDate, startDate], + }) => { + const dataSource = getAppointments(startDate, currentView); + const firstDayOfWeek = getFirstDayOfWeek(currentView); + const appointmentsCount = getAppointmentsCount(currentView); + + test(`Should correctly exclude appointment from recurrence (${currentView}, ${timeZone}, ${machineTimezone}, ${caseName})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== machineTimezone, `Skipping: machine timezone is ${browserTimezone}, expected ${machineTimezone}`); + + await createWidget(page, 'dxScheduler', { + timeZone, + dataSource, + currentDate, + currentView, + firstDayOfWeek, + recurrenceEditMode: 'occurrence', + }); + + const appointments = page.locator('.dx-scheduler-appointment'); + await expect(appointments).toHaveCount(appointmentsCount); + + for (let idx = 0; idx < appointmentsCount; idx += 1) { + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }).first(); + await firstAppointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button'); + await deleteButton.click(); + + await expect(appointments).toHaveCount(appointmentsCount - (idx + 1)); + } + + await expect(appointments).toHaveCount(0); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderCrossDst.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderCrossDst.spec.ts new file mode 100644 index 000000000000..8428bad2c058 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderCrossDst.spec.ts @@ -0,0 +1,175 @@ +import { test } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, insertStylesheetRulesToPage, testScreenshot, generateOptionMatrix } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +const normalizeTimezoneName = (timezone: string): string => timezone.replace(/\//g, '-'); + +const SCHEDULER_SELECTOR = '#container'; +const CUSTOM_CSS = ` +#container .dx-scheduler-header-panel-cell { + color: rgba(0,0,0,.54); +} + +#container .dx-scheduler-header-panel-cell::before { + display: none; +} + +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const SUMMER_BERLIN_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'summer-local', + currentDate: '2024-03-31', + dataSource: [ + { text: '#0', startDate: '2024-03-30T00:00:00', endDate: '2024-03-30T05:00:00' }, + { text: '#1', startDate: '2024-03-31T00:00:00', endDate: '2024-03-31T05:00:00' }, + { text: '#2', startDate: '2024-04-01T00:00:00', endDate: '2024-04-01T05:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T00:00', endDate: '2020-01-01T05:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const SUMMER_BERLIN_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'summer-utc', + currentDate: '2024-03-31', + dataSource: [ + { text: '#0', startDate: '2024-03-29T23:00:00Z', endDate: '2024-03-30T04:00:00Z' }, + { text: '#1', startDate: '2024-03-30T23:00:00Z', endDate: '2024-03-31T04:00:00Z' }, + { text: '#2', startDate: '2024-03-31T23:00:00Z', endDate: '2024-04-01T04:00:00Z' }, + ], +}; + +const WINTER_BERLIN_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'winter-local', + currentDate: '2024-10-27', + dataSource: [ + { text: '#0', startDate: '2024-10-26T01:00:00', endDate: '2024-10-26T04:00:00' }, + { text: '#1', startDate: '2024-10-27T01:00:00', endDate: '2024-10-27T04:00:00' }, + { text: '#2', startDate: '2024-10-28T01:00:00', endDate: '2024-10-28T04:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T01:00', endDate: '2020-01-01T04:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const WINTER_BERLIN_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'winter-utc', + currentDate: '2024-10-27', + dataSource: [ + { text: '#0', startDate: '2024-10-25T23:00:00Z', endDate: '2024-10-26T04:00:00Z' }, + { text: '#1', startDate: '2024-10-26T23:00:00Z', endDate: '2024-10-27T04:00:00Z' }, + { text: '#2', startDate: '2024-10-27T23:00:00Z', endDate: '2024-10-28T04:00:00Z' }, + ], +}; + +const SUMMER_LOS_ANGELES_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'summer-local', + currentDate: '2024-03-10', + dataSource: [ + { text: '#0', startDate: '2024-03-09T00:00:00', endDate: '2024-03-09T05:00:00' }, + { text: '#1', startDate: '2024-03-10T00:00:00', endDate: '2024-03-10T05:00:00' }, + { text: '#2', startDate: '2024-03-11T00:00:00', endDate: '2024-03-11T05:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T00:00', endDate: '2020-01-01T05:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const SUMMER_LOS_ANGELES_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'summer-utc', + currentDate: '2024-03-10', + dataSource: [ + { text: '#0', startDate: '2024-03-09T08:00:00Z', endDate: '2024-03-09T13:00:00Z' }, + { text: '#1', startDate: '2024-03-10T08:00:00Z', endDate: '2024-03-10T13:00:00Z' }, + { text: '#2', startDate: '2024-03-11T08:00:00Z', endDate: '2024-03-11T13:00:00Z' }, + ], +}; + +const WINTER_LOS_ANGELES_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'winter-local', + currentDate: '2024-11-03', + dataSource: [ + { text: '#0', startDate: '2024-11-02T00:00:00', endDate: '2024-11-02T05:00:00' }, + { text: '#1', startDate: '2024-11-03T00:00:00', endDate: '2024-11-03T05:00:00' }, + { text: '#2', startDate: '2024-11-04T00:00:00', endDate: '2024-11-04T05:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T00:00', endDate: '2020-01-01T05:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const WINTER_LOS_ANGELES_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'winter-utc', + currentDate: '2024-11-03', + dataSource: [ + { text: '#0', startDate: '2024-11-02T08:00:00Z', endDate: '2024-11-02T13:00:00Z' }, + { text: '#1', startDate: '2024-11-03T08:00:00Z', endDate: '2024-11-03T13:00:00Z' }, + { text: '#2', startDate: '2024-11-04T08:00:00Z', endDate: '2024-11-04T13:00:00Z' }, + ], +}; + +test.describe('Scheduler render during DST - cross DST rendering', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + generateOptionMatrix({ + currentView: ['week'] as string[], + offset: [-360, 0, 360], + location: [ + SUMMER_BERLIN_LOCAL_DATE_CASE, + SUMMER_BERLIN_UTC_DATE_CASE, + WINTER_BERLIN_LOCAL_DATE_CASE, + WINTER_BERLIN_UTC_DATE_CASE, + SUMMER_LOS_ANGELES_LOCAL_DATE_CASE, + SUMMER_LOS_ANGELES_UTC_DATE_CASE, + WINTER_LOS_ANGELES_LOCAL_DATE_CASE, + WINTER_LOS_ANGELES_UTC_DATE_CASE, + ], + }).forEach(({ + currentView, + offset, + location: { + timezone, + caseName, + currentDate, + dataSource, + }, + }) => { + test(`Should correctly render appointments with local machine date crossing DST (${timezone}, ${caseName}, offset: ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + await createWidget(page, 'dxScheduler', { + dataSource, + currentView, + currentDate, + offset, + showCurrentTimeIndicator: false, + firstDayOfWeek: 4, + cellDuration: 60, + height: 800, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const timezoneName = normalizeTimezoneName(timezone); + await testScreenshot( + page, + `${currentView}_appts-render-cross-dts_t-${timezoneName}-${caseName}_offset-${offset}.png`, + { element: workSpace }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderDst.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderDst.spec.ts new file mode 100644 index 000000000000..106e2afa3391 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderDst.spec.ts @@ -0,0 +1,142 @@ +import { test } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, insertStylesheetRulesToPage, testScreenshot, generateOptionMatrix } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +const normalizeTimezoneName = (timezone: string): string => timezone.replace(/\//g, '-'); + +const SCHEDULER_SELECTOR = '#container'; +const CUSTOM_CSS = ` +#container .dx-scheduler-header-panel-cell { + color: rgba(0,0,0,.54); +} + +#container .dx-scheduler-header-panel-cell::before { + display: none; +} + +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const MS_IN_MINUTE = 60000; + +const generateAppointments = ( + startDate: Date, + durationMin: number, + count: number, + textPrefix = '', +) => new Array(count).fill(null).map((_, idx) => { + const currentStartDate = new Date(startDate.getTime() + durationMin * MS_IN_MINUTE * idx); + const currentEndDate = new Date(currentStartDate.getTime() + durationMin * MS_IN_MINUTE); + return { + text: `${textPrefix}${idx}`, + startDate: currentStartDate, + endDate: currentEndDate, + }; +}); + +test.describe('Scheduler render during DST', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + generateOptionMatrix({ + currentView: ['week'] as string[], + offset: [-360, 0, 360], + location: [ + [MACHINE_TIMEZONES.EuropeBerlin, 'summer', '2024-03-31', new Date('2024-03-28T23:00:00Z')], + [MACHINE_TIMEZONES.EuropeBerlin, 'winter', '2024-10-27', new Date('2024-10-24T22:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'summer', '2024-03-10', new Date('2024-03-08T08:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'winter', '2024-11-03', new Date('2024-11-01T08:00:00Z')], + ] as [MachineTimezonesType, string, string, Date][], + }).forEach(({ + currentView, + offset, + location: [timezone, caseName, currentDate, startDate], + }) => { + const dataSource = generateAppointments(startDate, 60, 120); + + test(`Should correctly render hourly appointments at DST (${timezone}, ${caseName}, offset: ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + await createWidget(page, 'dxScheduler', { + timeZone: timezone, + dataSource, + currentView, + currentDate, + offset, + showCurrentTimeIndicator: false, + firstDayOfWeek: 4, + cellDuration: 60, + height: 800, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const timezoneName = normalizeTimezoneName(timezone); + await testScreenshot( + page, + `${currentView}_usual-appts-render-dts_t-${timezoneName}-${caseName}_offset-${offset}.png`, + { element: workSpace }, + ); + }); + }); + + generateOptionMatrix({ + currentView: ['day'] as string[], + offset: [-60, 0, 60], + location: [ + [MACHINE_TIMEZONES.EuropeBerlin, 'summer', '2024-03-31', new Date('2024-03-30T23:00:00Z')], + [MACHINE_TIMEZONES.EuropeBerlin, 'winter', '2024-10-27', new Date('2024-10-26T22:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'summer', '2024-03-10', new Date('2024-03-10T08:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'winter', '2024-11-03', new Date('2024-11-03T07:00:00Z')], + ] as [MachineTimezonesType, string, string, Date][], + }).forEach(({ + currentView, + offset, + location: [timezone, caseName, currentDate, startDate], + }) => { + const dataSource = [ + ...generateAppointments(startDate, 60, 5, 'A_'), + ...generateAppointments(startDate, 30, 10, 'B_'), + ]; + + test(`Should resolve appointment start cell correctly during DST (${timezone}, ${caseName}, offset: ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + await createWidget(page, 'dxScheduler', { + timeZone: timezone, + dataSource, + currentView, + currentDate, + offset, + showCurrentTimeIndicator: false, + maxAppointmentsPerCell: 'unlimited', + firstDayOfWeek: 4, + cellDuration: 30, + height: 800, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const timezoneName = normalizeTimezoneName(timezone); + await testScreenshot( + page, + `${currentView}_usual-appts-start-cell-dts_t-${timezoneName}-${caseName}_offset-${offset}.png`, + { element: workSpace }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/agenda.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/agenda.spec.ts new file mode 100644 index 000000000000..c5ed0320f432 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/agenda.spec.ts @@ -0,0 +1,51 @@ +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('Offset: Agenda', () => { + 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'); + }); + + [ + 0, + -240, + 240, + ].forEach((offset) => { + test(`Agenda view should not be affected by root offset option (offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2023-09-04T00:00:00', + endDate: '2023-09-04T02:00:00', + text: '#0 04: 00 -> 02', + }, + { + startDate: '2023-09-04T10:00:00', + endDate: '2023-09-04T12:00:00', + text: '#1 04: 10 -> 12', + }, + { + startDate: '2023-09-04T23:00:00', + endDate: '2023-09-05T01:00:00', + text: '#2 04: 22 -> 01', + }, + ], + currentView: 'agenda', + currentDate: '2023-09-03', + height: 800, + offset, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_agenda-not-affected_offset-${offset}.png`, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/apiCallbacks.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/apiCallbacks.spec.ts new file mode 100644 index 000000000000..18cdca838e0b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/apiCallbacks.spec.ts @@ -0,0 +1,380 @@ +// @ts-nocheck +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')}`; + +const SCHEDULER_SELECTOR = '#container'; +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const MINUTE_MS = 60000; +const APPOINTMENT_TITLE = 'Test'; + +const getCellDateWithOffset = (initialDateString: string, offset: number): string => { + const initialDate = new Date(initialDateString); + const cellDate = new Date(initialDate.getTime() + (offset * MINUTE_MS)); + const [result] = cellDate.toISOString().split('.'); + return result; +}; + +const getAppointmentAfterUpdate = (offset: number) => { + switch (offset) { + case 700: + return { + startDate: '2023-09-05T12:40:00', + endDate: '2023-09-05T13:10:00', + text: APPOINTMENT_TITLE, + allDay: false, + }; + case -700: + return { + startDate: '2023-09-05T12:20:00', + endDate: '2023-09-05T12:50:00', + text: APPOINTMENT_TITLE, + allDay: false, + }; + default: + return { + startDate: '2023-09-05T12:00:00', + endDate: '2023-09-05T12:30:00', + text: APPOINTMENT_TITLE, + allDay: false, + }; + } +}; + +const EXPECTED = { + appointmentData: { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + text: APPOINTMENT_TITLE, + }, + targetedAppointmentData: { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + displayStartDate: new Date('2023-09-06T12:30:00'), + displayEndDate: new Date('2023-09-06T13:00:00'), + text: APPOINTMENT_TITLE, + }, +}; + +const STANDARD_DATA_SOURCE = [ + { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + text: APPOINTMENT_TITLE, + }, +]; + +const initClientTesting = async (page, callbacks: string[]) => { + await page.evaluate((cbs) => { + (window as any).clientTesting = (window as any).clientTesting || {}; + cbs.forEach((cb) => { + (window as any).clientTesting[cb] = []; + }); + }, callbacks); +}; + +const getClientResults = async (page, callbackName: string) => { + return page.evaluate((name) => (window as any).clientTesting[name], callbackName); +}; + +const clearClientData = async (page, callbacks: string[]) => { + await page.evaluate((cbs) => { + cbs.forEach((cb) => { + if ((window as any).clientTesting) { + (window as any).clientTesting[cb] = []; + } + }); + }, callbacks); +}; + +test.describe('Offset: Api callbacks', () => { + 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'); + }); + + [ + 0, + -700, + 700, + ].forEach((offset) => { + test(`onAppointmentRendered (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentRendered']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentRendered: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentRendered.push({ appointmentData, targetedAppointmentData }); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await expect(appointment).toBeVisible(); + + const results = await getClientResults(page, 'onAppointmentRendered'); + expect(results[0].appointmentData).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentRendered']); + }); + + test(`onAppointmentAdding and onAppointmentAdded (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentAdding', 'onAppointmentAdded']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentAdding: ({ appointmentData }) => { + win.clientTesting.onAppointmentAdding.push(appointmentData); + }, + onAppointmentAdded: ({ appointmentData }) => { + win.clientTesting.onAppointmentAdded.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const expectedAppointmentData = { + allDay: false, + startDate: getCellDateWithOffset('2023-09-05T01:00:00Z', offset), + endDate: getCellDateWithOffset('2023-09-05T02:00:00Z', offset), + text: '', + recurrenceRule: '', + }; + + const cell = page.locator('.dx-scheduler-date-table-row').nth(1) + .locator('.dx-scheduler-date-table-cell').nth(2); + await cell.dblclick(); + + const saveButton = page.locator('.dx-scheduler-appointment-popup .dx-popup-done'); + await saveButton.click(); + + const addingResults = await getClientResults(page, 'onAppointmentAdding'); + const addedResults = await getClientResults(page, 'onAppointmentAdded'); + + expect(addingResults[0]).toEqual(expectedAppointmentData); + expect(addedResults[0]).toEqual(expectedAppointmentData); + + await clearClientData(page, ['onAppointmentAdding', 'onAppointmentAdded']); + }); + + test(`onAppointmentClick and onAppointmentDbClick (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentClick', 'onAppointmentDblClick']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentClick: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentClick.push({ appointmentData, targetedAppointmentData }); + }, + onAppointmentDblClick: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentDblClick.push({ appointmentData, targetedAppointmentData }); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click(); + await appointment.dblclick(); + + const clickResults = await getClientResults(page, 'onAppointmentClick'); + const dblClickResults = await getClientResults(page, 'onAppointmentDblClick'); + + expect(clickResults[0].appointmentData).toEqual(EXPECTED.appointmentData); + expect(dblClickResults[0].appointmentData).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentClick', 'onAppointmentDblClick']); + }); + + test(`onAppointmentTooltipShowing and onAppointmentFormOpening (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentTooltipShowing', 'onAppointmentFormOpening']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentTooltipShowing: ({ appointments }) => { + const tooltipAppointmentData = appointments?.map(({ appointmentData, currentAppointmentData }) => ({ + appointmentData, + currentAppointmentData, + })); + win.clientTesting.onAppointmentTooltipShowing.push(tooltipAppointmentData); + }, + onAppointmentFormOpening: ({ appointmentData }) => { + win.clientTesting.onAppointmentFormOpening.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + await appointment.dblclick(); + + const tooltipResults = await getClientResults(page, 'onAppointmentTooltipShowing'); + const formResults = await getClientResults(page, 'onAppointmentFormOpening'); + + expect(tooltipResults[0][0].appointmentData).toEqual(EXPECTED.appointmentData); + expect(formResults[0]).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentTooltipShowing', 'onAppointmentFormOpening']); + }); + + test(`onAppointmentDeleting and onAppointmentDeleted (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentDeleting', 'onAppointmentDeleted']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentDeleting: ({ appointmentData }) => { + win.clientTesting.onAppointmentDeleting.push(appointmentData); + }, + onAppointmentDeleted: ({ appointmentData }) => { + win.clientTesting.onAppointmentDeleted.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button'); + await deleteButton.click(); + + const deletingResults = await getClientResults(page, 'onAppointmentDeleting'); + const deletedResults = await getClientResults(page, 'onAppointmentDeleted'); + + expect(deletingResults[0]).toEqual(EXPECTED.appointmentData); + expect(deletedResults[0]).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentDeleting', 'onAppointmentDeleted']); + }); + + test(`onAppointmentUpdating and onAppointmentUpdated (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentUpdating', 'onAppointmentUpdated']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentUpdating: ({ newData, oldData }) => { + win.clientTesting.onAppointmentUpdating.push({ newData, oldData }); + }, + onAppointmentUpdated: ({ appointmentData }) => { + win.clientTesting.onAppointmentUpdated.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const expectedOldData = { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + text: APPOINTMENT_TITLE, + }; + const expectedNewData = getAppointmentAfterUpdate(offset); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + // TODO: t.drag(element, -100, 0) - drag by pixel offset + const box = await appointment.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 page.mouse.up(); + } + + const updatingResults = await getClientResults(page, 'onAppointmentUpdating'); + const updatedResults = await getClientResults(page, 'onAppointmentUpdated'); + + expect(updatingResults[0].newData).toEqual(expectedNewData); + expect(updatingResults[0].oldData).toEqual(expectedOldData); + expect(updatedResults[0]).toEqual(expectedNewData); + + await clearClientData(page, ['onAppointmentUpdating', 'onAppointmentUpdated']); + }); + + test(`onAppointmentContextMenu (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentContextMenu']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentContextMenu: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentContextMenu.push({ appointmentData, targetedAppointmentData }); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click({ button: 'right' }); + + const contextMenuResults = await getClientResults(page, 'onAppointmentContextMenu'); + + expect(contextMenuResults[0].appointmentData).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentContextMenu']); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/currentTimeIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/currentTimeIndicator.spec.ts new file mode 100644 index 000000000000..5ca7f12f2ce9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/currentTimeIndicator.spec.ts @@ -0,0 +1,87 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const getScreenshotName = ( + view: string, + indicatorTime: string, + offset: number, + startDayHour: number, + endDayHour: number, +): string => `offset_time-indicator_view-${view}_now-${indicatorTime.replace(/:/g, '-')}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +const TEST_CASES: [string, string, number, number, number, number][] = [ + ['day', '2023-12-04T00:00:00', 120, 720, 0, 24], + ['day', '2023-12-04T00:00:00', 120, 720, 6, 18], + ['day', '2023-12-04T12:00:00', 120, 1440, 0, 24], + ['day', '2023-12-04T12:00:00', 120, 1440, 6, 18], + ['day', '2023-12-03T00:00:00', 120, -720, 0, 24], + ['day', '2023-12-03T00:00:00', 120, -720, 6, 18], + ['day', '2023-12-02T12:00:00', 120, -1440, 0, 24], + ['day', '2023-12-02T12:00:00', 120, -1440, 6, 18], + ['week', '2023-12-06T00:00:00', 120, 720, 0, 24], + ['week', '2023-12-06T00:00:00', 120, 720, 6, 18], + ['week', '2023-12-06T12:00:00', 120, 1440, 0, 24], + ['week', '2023-12-06T12:00:00', 120, 1440, 6, 18], + ['week', '2023-12-05T00:00:00', 120, -720, 0, 24], + ['week', '2023-12-05T00:00:00', 120, -720, 6, 18], + ['week', '2023-12-04T12:00:00', 120, -1440, 0, 24], + ['week', '2023-12-04T12:00:00', 120, -1440, 6, 18], + ['timelineDay', '2023-12-04T00:00:00', 360, 720, 0, 24], + ['timelineDay', '2023-12-04T00:00:00', 360, 720, 6, 18], + ['timelineDay', '2023-12-04T12:00:00', 360, 1440, 0, 24], + ['timelineDay', '2023-12-04T12:00:00', 360, 1440, 6, 18], + ['timelineDay', '2023-12-03T00:00:00', 360, -720, 0, 24], + ['timelineDay', '2023-12-03T00:00:00', 360, -720, 6, 18], + ['timelineDay', '2023-12-02T12:00:00', 360, -1440, 0, 24], + ['timelineDay', '2023-12-02T12:00:00', 360, -1440, 6, 18], + ['timelineWeek', '2023-12-04T00:00:00', 360, 720, 0, 24], + ['timelineWeek', '2023-12-04T00:00:00', 360, 720, 6, 18], + ['timelineWeek', '2023-12-04T12:00:00', 360, 1440, 0, 24], + ['timelineWeek', '2023-12-04T12:00:00', 360, 1440, 6, 18], + ['timelineWeek', '2023-12-03T00:00:00', 360, -720, 0, 24], + ['timelineWeek', '2023-12-03T00:00:00', 360, -720, 6, 18], + ['timelineWeek', '2023-12-02T12:00:00', 360, -1440, 0, 24], + ['timelineWeek', '2023-12-02T12:00:00', 360, -1440, 6, 18], + ['timelineMonth', '2023-12-04T00:00:00', 120, 720, 0, 24], + ['timelineMonth', '2023-12-04T00:00:00', 120, 720, 6, 18], + ['timelineMonth', '2023-12-04T12:00:00', 120, 1440, 0, 24], + ['timelineMonth', '2023-12-04T12:00:00', 120, 1440, 6, 18], + ['timelineMonth', '2023-12-03T00:00:00', 120, -720, 0, 24], + ['timelineMonth', '2023-12-03T00:00:00', 120, -720, 6, 18], + ['timelineMonth', '2023-12-02T12:00:00', 120, -1440, 0, 24], + ['timelineMonth', '2023-12-02T12:00:00', 120, -1440, 6, 18], +]; + +test.describe('Offset: Current time indicator', () => { + 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_CASES.forEach(([view, indicatorTime, cellDuration, offset, startDayHour, endDayHour]) => { + test(`Should correctly render current time indicator (${view}, now: ${indicatorTime}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: view, + shadeUntilCurrentTime: true, + currentDate: '2023-12-03', + indicatorTime, + cellDuration, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const screenshotName = getScreenshotName(view, indicatorTime, offset, startDayHour, endDayHour); + await testScreenshot(page, screenshotName, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/dragAndDrop.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/dragAndDrop.spec.ts new file mode 100644 index 000000000000..ad005b760c5b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/dragAndDrop.spec.ts @@ -0,0 +1,79 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const APPOINTMENT_TITLE = 'Test'; +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const APPOINTMENTS: Record[]> = { + week: [{ startDate: '2023-09-05T05:00:00', endDate: '2023-09-05T09:00:00', text: APPOINTMENT_TITLE }], + month: [{ startDate: '2023-09-05T10:00:00', endDate: '2023-09-06T15:00:00', text: APPOINTMENT_TITLE }], + timelineMonth: [{ startDate: '2023-09-02T10:00:00', endDate: '2023-09-03T15:00:00', text: APPOINTMENT_TITLE }], + allDayWeek: [{ startDate: '2023-09-05T05:00:00', endDate: '2023-09-05T09:00:00', text: APPOINTMENT_TITLE, allDay: true }], + allDayMonth: [{ startDate: '2023-09-05T10:00:00', endDate: '2023-09-06T15:00:00', text: APPOINTMENT_TITLE, allDay: true }], + allDayTimelineMonth: [{ startDate: '2023-09-02T10:00:00', endDate: '2023-09-03T15:00:00', text: APPOINTMENT_TITLE, allDay: true }], +}; + +const getDragCoordinatesByView = (viewType: string): { x: number; y: number } => { + switch (viewType) { + case 'week': return { x: 150, y: 0 }; + case 'month': return { x: 300, y: 300 }; + default: return { x: 300, y: 0 }; + } +}; + +const getScreenshotName = (viewType: string, offset: number, isAllDay: boolean) => + `offset_drag-n-drop_${isAllDay ? 'all-day' : 'usual'}-appts_${viewType}_offset-${offset}.png`; + +test.describe('Offset: Drag-n-drop appointments', () => { + 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'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.week, isAllDay: false }, + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.allDayWeek, isAllDay: true }, + { views: [{ type: 'month' }], dataSource: APPOINTMENTS.month, isAllDay: false }, + { views: [{ type: 'month' }], dataSource: APPOINTMENTS.allDayMonth, isAllDay: true }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.timelineMonth, isAllDay: false }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.allDayTimelineMonth, isAllDay: true }, + ].forEach(({ views, dataSource, isAllDay }) => { + [0, 735, -735].forEach((offset) => { + test(`Drag-n-drop (view: ${views[0].type}, allDay: ${isAllDay}, offset: ${offset})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource, + views, + currentView: views[0].type, + offset, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + const viewType = views[0].type; + const { x, y } = getDragCoordinatesByView(viewType); + + const box = await appointment.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 + x, box.y + box.height / 2 + y, { steps: 5 }); + await page.mouse.up(); + } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(viewType, offset, isAllDay), { element: workSpace }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/expressions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/expressions.spec.ts new file mode 100644 index 000000000000..5fccc9a2c8c6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/expressions.spec.ts @@ -0,0 +1,90 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const APPOINTMENT_TITLES = { usual: 'Usual', allDay: 'All-day' }; +const APPOINTMENTS = { + week: [ + { StartDate2: '2023-09-06T04:00:00', EndDate2: '2023-09-06T06:00:00', Text2: APPOINTMENT_TITLES.usual }, + { StartDate2: '2023-09-06T00:00:00', EndDate2: '2023-09-06T00:00:00', Text2: APPOINTMENT_TITLES.allDay, AllDay2: true }, + ], +}; + +test.describe('Offset: Appointment expressions', () => { + 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'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.week }, + ].forEach(({ views, dataSource }) => { + [0, 180, -180].forEach((offset) => { + test(`Appointment with expr common test (view: ${views[0].type}, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-05', + height: 800, + dataSource, + views, + currentView: views[0].type, + offset, + startDateExpr: 'StartDate2', + endDateExpr: 'EndDate2', + textExpr: 'Text2', + allDayExpr: 'AllDay2', + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const usualAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + const allDayAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.allDay }); + const viewType = views[0].type; + + await testScreenshot(page, `offset_appt-expr_${viewType}_offset-${offset}.png`, { element: workSpace }); + + let box = await usualAppointment.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 + 100, { steps: 5 }); + await page.mouse.up(); + } + + box = await allDayAppointment.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 page.mouse.up(); + } + + await testScreenshot(page, `offset_appt-expr_drag-n-drop_${viewType}_offset-${offset}.png`, { element: workSpace }); + + const usualResizeBottom = usualAppointment.locator('.dx-resizable-handle-bottom'); + box = await usualResizeBottom.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 + 100, { steps: 5 }); + await page.mouse.up(); + } + + const allDayResizeLeft = allDayAppointment.locator('.dx-resizable-handle-left'); + box = await allDayResizeLeft.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 page.mouse.up(); + } + + await testScreenshot(page, `offset_appt-expr_resize_${viewType}_offset-${offset}.png`, { element: workSpace }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/keyboardNavigation.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/keyboardNavigation.spec.ts new file mode 100644 index 000000000000..806f1896af20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/keyboardNavigation.spec.ts @@ -0,0 +1,75 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const KEYBOARD_ACTIONS: Record = { + day: ['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowUp'], + week: ['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowLeft', 'ArrowUp', 'ArrowUp'], + month: ['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowLeft', 'ArrowUp', 'ArrowUp'], + timelineDay: ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'], + timelineMonth: ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'], +}; + +test.describe('Offset: Keyboard navigation', () => { + 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'); + }); + + [0, -120, 120].forEach((offset) => { + [ + { view: 'day', startCell: [1, 0], keyboardKeys: KEYBOARD_ACTIONS.day }, + { view: 'week', startCell: [3, 3], keyboardKeys: KEYBOARD_ACTIONS.week }, + { view: 'month', startCell: [3, 3], keyboardKeys: KEYBOARD_ACTIONS.month }, + { view: 'timelineDay', startCell: [0, 1], keyboardKeys: KEYBOARD_ACTIONS.timelineDay }, + { view: 'timelineMonth', startCell: [0, 1], keyboardKeys: KEYBOARD_ACTIONS.timelineMonth }, + ].forEach(({ view, startCell, keyboardKeys }) => { + test(`Keyboard navigation should work (view: ${view}, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: view, + offset, + }); + + const [rowIdx, cellIdx] = startCell; + const startCellLocator = page.locator('.dx-scheduler-date-table-row').nth(rowIdx) + .locator('.dx-scheduler-date-table-cell').nth(cellIdx); + + await startCellLocator.click(); + for (const key of keyboardKeys) { + await page.keyboard.press(key); + } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_keyboard_${view}_offset-${offset}.png`, { element: workSpace }); + }); + }); + + test(`Keyboard navigation in the all-day panel should work (view: week, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: 'week', + offset, + }); + + const startCellLocator = page.locator('.dx-scheduler-all-day-table-cell').nth(1); + await startCellLocator.click(); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowLeft'); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_keyboard_week-all-day_offset-${offset}.png`, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/multiCellSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/multiCellSelection.spec.ts new file mode 100644 index 000000000000..5159b0bf7ef1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/multiCellSelection.spec.ts @@ -0,0 +1,87 @@ +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('Offset: Multi cell 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'); + }); + + [0, -120, 120].forEach((offset) => { + [true, false].forEach((rtlEnabled) => { + [ + { view: 'day', dragOptions: { direction: 'increase', from: [0, 0], to: [7, 0] } }, + { view: 'day', dragOptions: { direction: 'decrease', from: [7, 0], to: [0, 0] } }, + { view: 'week', dragOptions: { direction: 'increase_0', from: [0, 2], to: [7, 2] } }, + { view: 'week', dragOptions: { direction: 'decrease_0', from: [7, 2], to: [0, 2] } }, + { view: 'week', dragOptions: { direction: 'increase_1', from: [1, 3], to: [8, 4] } }, + { view: 'week', dragOptions: { direction: 'decrease_1', from: [8, 4], to: [1, 3] } }, + { view: 'week', dragOptions: { direction: 'increase_2', from: [6, 3], to: [6, 5] } }, + { view: 'week', dragOptions: { direction: 'decrease_2', from: [6, 5], to: [6, 3] } }, + { view: 'week', dragOptions: { direction: 'increase_3', from: [0, 0], to: [11, 6] } }, + { view: 'week', dragOptions: { direction: 'decrease_3', from: [11, 6], to: [0, 0] } }, + { view: 'month', dragOptions: { direction: 'increase', from: [2, 2], to: [3, 1] } }, + { view: 'month', dragOptions: { direction: 'decrease', from: [3, 1], to: [2, 2] } }, + { view: 'timelineDay', dragOptions: { direction: 'increase', from: [0, 0], to: [0, 5] } }, + { view: 'timelineDay', dragOptions: { direction: 'decrease', from: [0, 5], to: [0, 0] } }, + { view: 'timelineMonth', dragOptions: { direction: 'increase', from: [0, 0], to: [0, 3] } }, + { view: 'timelineMonth', dragOptions: { direction: 'decrease', from: [0, 3], to: [0, 0] } }, + ].forEach(({ view, dragOptions }) => { + test(`Multi cell selection (view: ${view}, offset: ${offset}, dir: ${dragOptions.direction}, rtl: ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: view, + offset, + rtlEnabled, + }); + + const { direction, from: [fromRow, fromCell], to: [toRow, toCell] } = dragOptions; + const firstCellLocator = page.locator('.dx-scheduler-date-table-row').nth(fromRow) + .locator('.dx-scheduler-date-table-cell').nth(fromCell); + const secondCellLocator = page.locator('.dx-scheduler-date-table-row').nth(toRow) + .locator('.dx-scheduler-date-table-cell').nth(toCell); + + await firstCellLocator.dragTo(secondCellLocator); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + `offset_multi-cell-select_${view}_offset-${offset}_${direction}${rtlEnabled ? '_rtl' : ''}.png`, + { element: workSpace }, + ); + }); + }); + + test(`Multi cell selection in all-day panel (view: week, offset: ${offset}, rtl: ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: 'week', + offset, + }); + + const firstCellLocator = page.locator('.dx-scheduler-all-day-table-cell').nth(0); + const secondCellLocator = page.locator('.dx-scheduler-all-day-table-cell').nth(3); + + await firstCellLocator.dragTo(secondCellLocator); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + `offset_multi-cell-select_week-all-day_offset-${offset}${rtlEnabled ? '_rtl' : ''}.png`, + { element: workSpace }, + ); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/resize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/resize.spec.ts new file mode 100644 index 000000000000..9a734d3980ff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/resize.spec.ts @@ -0,0 +1,170 @@ +import { test } from '@playwright/test'; +import type { Page, Locator } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const APPOINTMENT_TITLES = { usual: 'Usual', allDay: 'All-day' }; +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const APPOINTMENTS: Record[]> = { + week: [ + { startDate: '2023-09-05T05:00:00', endDate: '2023-09-05T09:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-06T00:00:00', text: APPOINTMENT_TITLES.allDay, allDay: true }, + ], + month: [ + { startDate: '2023-09-05T10:00:00', endDate: '2023-09-06T15:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-06T00:00:00', text: APPOINTMENT_TITLES.allDay, allDay: true }, + ], + timelineMonth: [ + { startDate: '2023-09-02T10:00:00', endDate: '2023-09-03T15:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-02T00:00:00', endDate: '2023-09-03T00:00:00', text: APPOINTMENT_TITLES.allDay, allDay: true }, + ], +}; + +enum ResizeType { + startPlus = 'start-plus', + startMinus = 'start-minus', + endPlus = 'end-plus', + endMinus = 'end-minus', +} + +const isVerticalView = (viewType: string, isAllDay: boolean): boolean => !isAllDay && viewType === 'week'; +const isStartResize = (resizeType: ResizeType): boolean => + resizeType === ResizeType.startPlus || resizeType === ResizeType.startMinus; + +const getResizableHandle = (appointment: Locator, viewType: string, resizeType: ResizeType, isAllDay: boolean): Locator => { + if (isVerticalView(viewType, isAllDay) && isStartResize(resizeType)) return appointment.locator('.dx-resizable-handle-top'); + if (isVerticalView(viewType, isAllDay) && !isStartResize(resizeType)) return appointment.locator('.dx-resizable-handle-bottom'); + if (isStartResize(resizeType)) return appointment.locator('.dx-resizable-handle-left'); + return appointment.locator('.dx-resizable-handle-right'); +}; + +const getResizableValues = (viewType: string, resizeType: ResizeType, isAllDay: boolean): { x: number; y: number } => { + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.startPlus) return { x: 0, y: -100 }; + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.startMinus) return { x: 0, y: 50 }; + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.endPlus) return { x: 0, y: 100 }; + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.endMinus) return { x: 0, y: -50 }; + if (resizeType === ResizeType.startPlus) return { x: -100, y: 0 }; + if (resizeType === ResizeType.startMinus) return { x: 50, y: 0 }; + if (resizeType === ResizeType.endPlus) return { x: 100, y: 0 }; + return { x: -50, y: 0 }; +}; + +const doResize = async (page: Page, appointment: Locator, viewType: string, resizeType: ResizeType, isAllDay: boolean): Promise => { + const handle = getResizableHandle(appointment, viewType, resizeType, isAllDay); + const { x, y } = getResizableValues(viewType, resizeType, isAllDay); + const box = await handle.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 + x, box.y + box.height / 2 + y, { steps: 5 }); + await page.mouse.up(); + } +}; + +const getScreenshotName = (viewType: string, resizeType: string, offset: number) => + `offset_resize-appts_${viewType}_${resizeType}_offset-${offset}.png`; + +test.describe('Offset: Resize appointments', () => { + 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'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.timelineMonth }, + ].forEach(({ views, dataSource }) => { + [0, 735, -735].forEach((offset) => { + test(`Appointments resize common (view: ${views[0].type}, offset: ${offset})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', height: 800, dataSource, views, currentView: views[0].type, offset, + }); + + const usualAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + const allDayAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.allDay }); + const viewType = views[0].type; + const workSpace = page.locator('.dx-scheduler-work-space'); + + await doResize(page, usualAppt, viewType, ResizeType.startMinus, false); + await doResize(page, allDayAppt, viewType, ResizeType.startMinus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.startMinus, offset), { element: workSpace }); + + await doResize(page, usualAppt, viewType, ResizeType.startPlus, false); + await doResize(page, allDayAppt, viewType, ResizeType.startPlus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.startPlus, offset), { element: workSpace }); + + await doResize(page, usualAppt, viewType, ResizeType.endMinus, false); + await doResize(page, allDayAppt, viewType, ResizeType.endMinus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.endMinus, offset), { element: workSpace }); + + await doResize(page, usualAppt, viewType, ResizeType.endPlus, false); + await doResize(page, allDayAppt, viewType, ResizeType.endPlus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.endPlus, offset), { element: workSpace }); + }); + }); + }); + + [-720, 720].forEach((offset) => { + test(`Resize with startDayHour/endDayHour (view: week, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { startDate: '2023-09-06T22:00:00', endDate: '2023-09-07T00:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-06T00:00:00', endDate: '2023-09-06T00:00:00', allDay: true, text: APPOINTMENT_TITLES.allDay }, + ], + currentView: 'week', startDayHour: 10, endDayHour: 12, currentDate: '2023-09-07', height: 800, offset, + }); + + const usualAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + const allDayAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.allDay }); + + let box = await usualAppt.locator('.dx-resizable-handle-bottom').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 - 50, { steps: 5 }); await page.mouse.up(); } + + box = await usualAppt.locator('.dx-resizable-handle-top').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 + 50, { steps: 5 }); await page.mouse.up(); } + + box = await allDayAppt.locator('.dx-resizable-handle-left').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 page.mouse.up(); } + + box = await allDayAppt.locator('.dx-resizable-handle-right').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 page.mouse.up(); } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_resize-appts_week_offset-${offset}_startDayHour-10_endDayHour-12.png`, { element: workSpace }); + }); + }); + + [ + { offset: -720, currentDate: '2023-09-07' }, + { offset: 720, currentDate: '2023-09-06' }, + ].forEach(({ offset, currentDate }) => { + test(`Resize with startDayHour/endDayHour (view: timelineDay, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ startDate: '2023-09-06T22:00:00', endDate: '2023-09-07T00:00:00', text: APPOINTMENT_TITLES.usual }], + currentView: 'timelineDay', startDayHour: 10, endDayHour: 12, height: 800, currentDate, offset, + }); + + const usualAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + + let box = await usualAppt.locator('.dx-resizable-handle-left').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 + 200, box.y + box.height / 2, { steps: 5 }); await page.mouse.up(); } + + box = await usualAppt.locator('.dx-resizable-handle-right').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 - 200, box.y + box.height / 2, { steps: 5 }); await page.mouse.up(); } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_resize-appts_timelineDay_offset-${offset}_startDayHour-10_endDayHour-12.png`, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/allDayAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/allDayAppointments.spec.ts new file mode 100644 index 000000000000..910e41f9de1c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/allDayAppointments.spec.ts @@ -0,0 +1,139 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const MS_IN_DAY = 24 * 60 * 60 * 1000; + +interface AppointmentData { + startTime: string; + endTime: string; + endDateShiftDays?: number; + allDay?: boolean; +} + +const getIsoDate = (date: Date, additionalDays = 0): string => { + const dateCopy = new Date(date.getTime() + additionalDays * MS_IN_DAY); + const [dateISO] = dateCopy.toISOString().split('T'); + return dateISO; +}; + +const timeToText = (time: string): string => { + const [hours, minutes] = time.split(':'); + return `${hours}:${minutes}`; +}; + +const generateAppointments = ( + startDateISO: string, + endDateISO: string, + appointments: AppointmentData[], +) => { + const startDate = new Date(startDateISO); + const endDate = new Date(endDateISO); + const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); + const daysCount = Math.ceil((diffTime / MS_IN_DAY) + 1); + + return new Array(daysCount).fill(null).map((_, dayIdx) => { + const date = new Date(startDate.getTime() + MS_IN_DAY * dayIdx); + return new Array(appointments.length).fill(null).map((__, timeIdx) => { + const { startTime, endTime, endDateShiftDays, allDay } = appointments[timeIdx]; + const appointmentIdx = dayIdx * appointments.length + timeIdx; + const appointmentStartISO = getIsoDate(date); + const appointmentEndISO = getIsoDate(date, endDateShiftDays ?? 0); + const [, , dayISO] = appointmentStartISO.split('-'); + const titleText = `#${appointmentIdx}: ${dayISO.padStart(2, '0')} ${allDay ? 'All' : ''} ${timeToText(startTime)}-${timeToText(endTime)}`; + return { + startDate: `${appointmentStartISO}T${startTime}`, + endDate: `${appointmentEndISO}T${endTime}`, + text: titleText, + allDay, + }; + }); + }).flat(); +}; + +const ALL_DAY_APPOINTMENTS_DATA: AppointmentData[] = [ + { startTime: '02:00:00', endTime: '02:00:00', allDay: true, endDateShiftDays: 1 }, + { startTime: '20:30:00', endTime: '23:30:00', allDay: true }, +]; + +const APPOINTMENTS: Record[]> = { + day: [ + ...generateAppointments('2023-09-06', '2023-09-08', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-09-05T14:00:00', endDate: '2023-09-09T16:00:00', text: 'LONG APPT', allDay: true }, + ], + week: [ + ...generateAppointments('2023-09-02', '2023-09-10', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-09-01T14:00:00', endDate: '2023-09-12T16:00:00', text: 'LONG APPT', allDay: true }, + ], + workWeekWithFirstDay: [ + ...generateAppointments('2023-09-05', '2023-09-13', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-09-03T14:00:00', endDate: '2023-09-15T16:00:00', text: 'LONG APPT', allDay: true }, + ], + month: [ + ...generateAppointments('2023-08-26', '2023-10-08', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-08-24T14:00:00', endDate: '2023-10-10T16:00:00', text: 'LONG APPT', allDay: true }, + ], +}; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number, firstDay?: number) => + `view_markup_all-day_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}_first-day-${firstDay}.png`; + +test.describe('Offset: Markup all-day appointments', () => { + 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'); + }); + + [ + { views: [{ type: 'day', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.day }, + { views: [{ type: 'week', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.workWeekWithFirstDay }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineDay', cellDuration: 240, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.day }, + { views: [{ type: 'timelineWeek', cellDuration: 480, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'timelineWorkWeek', cellDuration: 480, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'timelineWorkWeek', cellDuration: 480, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.workWeekWithFirstDay }, + { views: [{ type: 'timelineMonth', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + ].forEach(({ views, dataSource }) => { + [0, 735, 1440, -735, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`All-day appointments render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour}, firstDay: ${(views[0] as any).firstDayOfWeek})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views, + currentView: views[0].type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotName(views[0].type, offset, startDayHour, endDayHour, (views[0] as any).firstDayOfWeek), + { element: workSpace }, + ); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/appointmentsOrdering.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/appointmentsOrdering.spec.ts new file mode 100644 index 000000000000..43c19355ed72 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/appointmentsOrdering.spec.ts @@ -0,0 +1,151 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const MS_IN_DAY = 24 * 60 * 60 * 1000; + +const getIsoDate = (date: Date, additionalDays = 0): string => { + const dateCopy = new Date(date.getTime() + additionalDays * MS_IN_DAY); + const [dateISO] = dateCopy.toISOString().split('T'); + return dateISO; +}; + +const timeToText = (time: string): string => { + const [hours, minutes] = time.split(':'); + return `${hours}:${minutes}`; +}; + +interface AppointmentData { + startTime: string; + endTime: string; + endDateShiftDays?: number; + text?: string; + allDay?: boolean; + recurrenceRule?: string; +} + +const generateAppointments = ( + startDateISO: string, + endDateISO: string, + appointments: AppointmentData[], +) => { + const startDate = new Date(startDateISO); + const endDate = new Date(endDateISO); + const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); + const daysCount = Math.ceil((diffTime / MS_IN_DAY) + 1); + return new Array(daysCount).fill(null).map((_, dayIdx) => { + const date = new Date(startDate.getTime() + MS_IN_DAY * dayIdx); + return new Array(appointments.length).fill(null).map((__, timeIdx) => { + const { startTime, endTime, endDateShiftDays, text, allDay, recurrenceRule } = appointments[timeIdx]; + const appointmentIdx = dayIdx * appointments.length + timeIdx; + const appointmentStartISO = getIsoDate(date); + const appointmentEndISO = getIsoDate(date, endDateShiftDays ?? 0); + const [, , dayISO] = appointmentStartISO.split('-'); + const titleText = `#${appointmentIdx}: ${dayISO.padStart(2, '0')} ${allDay ? 'All' : ''} ${!text ? `${timeToText(startTime)}-${timeToText(endTime)}` : text}`; + return { startDate: `${appointmentStartISO}T${startTime}`, endDate: `${appointmentEndISO}T${endTime}`, text: titleText, allDay, recurrenceRule }; + }); + }).flat(); +}; + +const APPOINTMENTS_TIME: AppointmentData[] = [ + { startTime: '10:15:00', endTime: '16:15:00' }, + { startTime: '17:05:00', endTime: '22:05:00' }, +]; +const APPOINTMENTS_TIMELINE_TIME: AppointmentData[] = [ + { startTime: '04:00:00', endTime: '08:00:00', endDateShiftDays: 1 }, + { startTime: '10:15:00', endTime: '16:15:00', endDateShiftDays: 1 }, + { startTime: '17:05:00', endTime: '22:05:00', endDateShiftDays: 1 }, +]; + +const RECURRENT_APPOINTMENTS_MONTH = [ + { startDate: '2023-08-01T15:00:00', endDate: '2023-08-01T19:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 15-19' }, +]; +const RECURRENT_APPOINTMENTS_MONTH_TIMELINE = [ + { startDate: '2023-08-01T09:00:00', endDate: '2023-08-01T13:00:00', recurrenceRule: 'FREQ=HOURLY;INTERVAL=24', text: 'Hourly 09-13' }, + { startDate: '2023-08-01T15:00:00', endDate: '2023-08-01T19:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 15-19' }, +]; + +const APPOINTMENTS: Record[]> = { + month: [...generateAppointments('2023-08-26', '2023-10-08', APPOINTMENTS_TIME), ...RECURRENT_APPOINTMENTS_MONTH], + timelineMonth: [...generateAppointments('2023-08-31', '2023-09-08', APPOINTMENTS_TIMELINE_TIME), ...RECURRENT_APPOINTMENTS_MONTH_TIMELINE], +}; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number) => + `view_markup_ordering-appts_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +test.describe('Offset: Markup appointments ordering', () => { + 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'); + }); + + [ + { views: [{ type: 'month' }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.timelineMonth }, + ].forEach(({ views, dataSource }) => { + [0, 735, -735, 1440, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Appointments ordering render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views: [views[0]], + currentView: views[0].type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(views[0].type, offset, startDayHour, endDayHour), { element: workSpace }); + }); + }); + }); + }); + + test('Appointments are ordered correctly with both recurrent and usual appointments (T1212573)', async ({ page }) => { + const data = [ + { text: 'Recurr 1', startDate: new Date('2020-11-01T17:30:00.000Z'), endDate: new Date('2020-11-01T19:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10' }, + { text: 'Recurr 2', startDate: new Date('2020-11-01T17:30:00.000Z'), endDate: new Date('2020-11-01T19:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU,WE;COUNT=10' }, + { text: 'Recurr 3', startDate: new Date('2020-11-01T20:00:00.000Z'), endDate: new Date('2020-11-01T21:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU;WKST=TU;INTERVAL=2;COUNT=2' }, + { text: 'Recurr 4', startDate: new Date('2020-11-01T17:00:00.000Z'), endDate: new Date('2020-11-01T17:15:00.000Z'), recurrenceRule: 'FREQ=DAILY;BYDAY=TU;UNTIL=20201203' }, + { text: 'Test 1', startDate: new Date('2020-11-01T15:00:00.000Z'), endDate: new Date('2020-11-01T15:30:00.000Z') }, + { text: 'Test 2', startDate: new Date('2020-11-01T18:00:00.000Z'), endDate: new Date('2020-11-01T18:30:00.000Z') }, + { text: 'Test 3', startDate: new Date('2020-11-02T15:00:00.000Z'), endDate: new Date('2020-11-02T15:30:00.000Z') }, + { text: 'Test 4', startDate: new Date('2020-11-02T18:00:00.000Z'), endDate: new Date('2020-11-02T18:30:00.000Z') }, + { text: 'Test 5', startDate: new Date('2020-11-03T15:00:00.000Z'), endDate: new Date('2020-11-03T15:30:00.000Z') }, + { text: 'Test 6', startDate: new Date('2020-11-03T18:00:00.000Z'), endDate: new Date('2020-11-03T18:30:00.000Z') }, + { text: 'Test 7', startDate: new Date('2020-11-04T15:00:00.000Z'), endDate: new Date('2020-11-04T15:30:00.000Z') }, + { text: 'Test 8', startDate: new Date('2020-11-04T18:00:00.000Z'), endDate: new Date('2020-11-04T18:30:00.000Z') }, + ]; + + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2020-11-07', + height: 800, + dataSource: data, + views: ['timelineMonth'], + currentView: 'timelineMonth', + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, 'view_markup_ordering-appts_T1212573.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/recurrentAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/recurrentAppointments.spec.ts new file mode 100644 index 000000000000..7474eed26ce3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/recurrentAppointments.spec.ts @@ -0,0 +1,161 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const APPOINTMENTS = [ + { startDate: '2023-08-01T10:00:00', endDate: '2023-08-01T14:00:00', recurrenceRule: 'FREQ=HOURLY;INTERVAL=24', text: 'Hourly 10-14' }, + { startDate: '2023-08-01T16:00:00', endDate: '2023-08-01T20:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 16-20' }, + { startDate: '2023-08-01T23:00:00', endDate: '2023-08-02T05:00:00', recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=7', allDay: true, text: 'All day 01 -> 02' }, +]; +const APPOINTMENTS_TIMELINE = [ + { startDate: '2023-08-01T04:00:00', endDate: '2023-08-01T18:00:00', recurrenceRule: 'FREQ=HOURLY;INTERVAL=24', text: 'Hourly 04-18' }, + { startDate: '2023-08-01T20:00:00', endDate: '2023-08-02T10:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 20-10' }, +]; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number, firstDay?: number) => + `view_markup_recurrent-appts_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}_first-day-${firstDay}.png`; + +const getScreenshotNameForEdgeCase = (edgeCaseName: string, viewType: string, offset: number, startDayHour: number, endDayHour: number) => + `view_markup_recurrent-appts_${edgeCaseName}_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +const getViewWithCorrectCellDuration = ( + view: { type: string; cellDuration?: number }, + startDayHour: number, + endDayHour: number, +): { type: string; cellDuration?: number } => { + switch (view.type) { + case 'timelineWeek': + case 'timelineWorkWeek': + return { ...view, cellDuration: (endDayHour - startDayHour) * 60 }; + default: + return view; + } +}; + +test.describe('Offset: Markup recurrent appointments', () => { + 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'); + }); + + [ + { views: [{ type: 'day', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'week', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'timelineDay', cellDuration: 240, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 3 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineMonth', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + ].forEach(({ views, dataSource }) => { + [0, 735, -735].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Recurrence appointments render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour}, firstDay: ${(views[0] as any).firstDayOfWeek})`, async ({ page }) => { + const view = getViewWithCorrectCellDuration(views[0], startDayHour, endDayHour); + + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views: [view], + currentView: view.type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotName(views[0].type, offset, startDayHour, endDayHour, (views[0] as any).firstDayOfWeek), + { element: workSpace }, + ); + }); + }); + }); + }); + + [ + { views: [{ type: 'day', cellDuration: 60 }] }, + { views: [{ type: 'timelineDay', cellDuration: 240 }] }, + ].forEach(({ views }) => { + [ + { + dataSource: [ + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#0 WE 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-02T04:00:00', text: '#1 WE 20:00->04:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#2 TH 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day TH', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + ], + offset: 720, startDayHour: 0, endDayHour: 24, + }, + { + dataSource: [ + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-01T22:00:00', text: '#0 WE 15:00->19:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T23:00:00', endDate: '2023-09-02T01:00:00', text: '#1 WE 23:00->01:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T04:00:00', endDate: '2023-09-01T06:00:00', text: '#2 TH 04:00->06:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day TH', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + ], + offset: 720, startDayHour: 9, endDayHour: 17, + }, + { + dataSource: [ + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#0 TU 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-02T04:00:00', text: '#1 TU 20:00->04:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#2 WE 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day WE', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + ], + offset: -720, startDayHour: 0, endDayHour: 24, + }, + { + dataSource: [ + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-01T22:00:00', text: '#0 TU 15:00->19:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T23:00:00', endDate: '2023-09-02T01:00:00', text: '#1 TU 23:00->01:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T04:00:00', endDate: '2023-09-01T06:00:00', text: '#2 WE 04:00->06:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day WE', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + ], + offset: -720, startDayHour: 9, endDayHour: 17, + }, + ].forEach(({ dataSource, offset, startDayHour, endDayHour }) => { + test(`Recurrence appointments in short day views (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-06', + height: 800, + maxAppointmentsPerCell: 'unlimited', + currentView: views[0].type, + dataSource, + views, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotNameForEdgeCase('short-day-views', views[0].type, offset, startDayHour, endDayHour), + { element: workSpace }, + ); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/usualAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/usualAppointments.spec.ts new file mode 100644 index 000000000000..b45d791ab249 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/usualAppointments.spec.ts @@ -0,0 +1,151 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const MS_IN_DAY = 24 * 60 * 60 * 1000; + +interface AppointmentData { + startTime: string; + endTime: string; + endDateShiftDays?: number; + text?: string; + allDay?: boolean; + recurrenceRule?: string; +} + +const getIsoDate = (date: Date, additionalDays = 0): string => { + const dateCopy = new Date(date.getTime() + additionalDays * MS_IN_DAY); + const [dateISO] = dateCopy.toISOString().split('T'); + return dateISO; +}; + +const timeToText = (time: string): string => { + const [hours, minutes] = time.split(':'); + return `${hours}:${minutes}`; +}; + +const generateAppointments = ( + startDateISO: string, + endDateISO: string, + appointments: AppointmentData[], +) => { + const startDate = new Date(startDateISO); + const endDate = new Date(endDateISO); + const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); + const daysCount = Math.ceil((diffTime / MS_IN_DAY) + 1); + return new Array(daysCount).fill(null).map((_, dayIdx) => { + const date = new Date(startDate.getTime() + MS_IN_DAY * dayIdx); + return new Array(appointments.length).fill(null).map((__, timeIdx) => { + const { startTime, endTime, endDateShiftDays, text, allDay, recurrenceRule } = appointments[timeIdx]; + const appointmentIdx = dayIdx * appointments.length + timeIdx; + const appointmentStartISO = getIsoDate(date); + const appointmentEndISO = getIsoDate(date, endDateShiftDays ?? 0); + const [, , dayISO] = appointmentStartISO.split('-'); + const titleText = `#${appointmentIdx}: ${dayISO.padStart(2, '0')} ${allDay ? 'All' : ''} ${!text ? `${timeToText(startTime)}-${timeToText(endTime)}` : text}`; + return { startDate: `${appointmentStartISO}T${startTime}`, endDate: `${appointmentEndISO}T${endTime}`, text: titleText, allDay, recurrenceRule }; + }); + }).flat(); +}; + +const APPOINTMENTS_TIME: AppointmentData[] = [ + { startTime: '04:00:00', endTime: '08:00:00' }, + { startTime: '10:15:00', endTime: '16:15:00' }, + { startTime: '17:05:00', endTime: '22:05:00' }, + { startTime: '23:00:00', endTime: '03:30:00', endDateShiftDays: 1 }, +]; +const APPOINTMENTS_TIMELINE_TIME: AppointmentData[] = [ + { startTime: '04:00:00', endTime: '08:00:00', endDateShiftDays: 1 }, + { startTime: '10:15:00', endTime: '16:15:00', endDateShiftDays: 1 }, + { startTime: '17:05:00', endTime: '22:05:00', endDateShiftDays: 1 }, + { startTime: '23:00:00', endTime: '03:30:00', endDateShiftDays: 1 }, +]; + +const APPOINTMENTS: Record[]> = { + day: generateAppointments('2023-09-06', '2023-09-08', APPOINTMENTS_TIME), + week: generateAppointments('2023-09-02', '2023-09-10', APPOINTMENTS_TIME), + workWeekWithFirstDay: generateAppointments('2023-09-05', '2023-09-13', APPOINTMENTS_TIME), + month: generateAppointments('2023-08-26', '2023-10-08', APPOINTMENTS_TIME), + timelineDay: generateAppointments('2023-09-06', '2023-09-08', APPOINTMENTS_TIMELINE_TIME), + timelineWeek: generateAppointments('2023-09-02', '2023-09-10', APPOINTMENTS_TIMELINE_TIME), + timelineWeekWithFirstDay: generateAppointments('2023-09-05', '2023-09-13', APPOINTMENTS_TIMELINE_TIME), + timelineMonth: generateAppointments('2023-08-31', '2023-09-08', APPOINTMENTS_TIMELINE_TIME), +}; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number, firstDay?: number) => + `view_markup_usual-appts_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}_first-day-${firstDay}.png`; + +const getViewWithCorrectCellDuration = ( + view: { type: string; cellDuration?: number }, + startDayHour: number, + endDayHour: number, +): { type: string; cellDuration?: number } => { + switch (view.type) { + case 'timelineWeek': + case 'timelineWorkWeek': + return { ...view, cellDuration: (endDayHour - startDayHour) * 60 }; + default: + return view; + } +}; + +test.describe('Offset: Markup usual appointments', () => { + 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'); + }); + + [ + { views: [{ type: 'day', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.day }, + { views: [{ type: 'week', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.workWeekWithFirstDay }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineDay', cellDuration: 240, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.timelineDay }, + { views: [{ type: 'timelineWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.timelineWeek }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.timelineWeek }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.timelineWeekWithFirstDay }, + { views: [{ type: 'timelineMonth', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + ].forEach(({ views, dataSource }) => { + [0, 735, 1440, -735, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Usual appointments render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour}, firstDay: ${(views[0] as any).firstDayOfWeek})`, async ({ page }) => { + const view = getViewWithCorrectCellDuration(views[0], startDayHour, endDayHour); + + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views: [view], + currentView: view.type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotName(views[0].type, offset, startDayHour, endDayHour, (views[0] as any).firstDayOfWeek), + { element: workSpace }, + ); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/virtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/virtualScrolling.spec.ts new file mode 100644 index 000000000000..140422eb0968 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/virtualScrolling.spec.ts @@ -0,0 +1,62 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const APPOINTMENTS = [ + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-05T03:00:00', text: '#0 Usual 05 00:00->03:00' }, + { startDate: '2023-09-05T04:00:00', endDate: '2023-09-05T09:00:00', text: '#1 Usual 05 05:00->09:00' }, + { startDate: '2023-09-05T10:30:00', endDate: '2023-09-05T16:30:00', text: '#2 Usual 05 12:30->16:30' }, + { startDate: '2023-09-05T17:00:00', endDate: '2023-09-05T23:30:00', text: '#3 Usual 05 18:00->22:00' }, + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-05T00:00:00', text: '#4 All-day 05', allDay: true }, +]; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number) => + `view_markup_virtual-scrolling_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +test.describe('Offset: Markup 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'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'month' }], dataSource: APPOINTMENTS }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS }, + ].forEach(({ views, dataSource }) => { + [0, 735, 1440, -735, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Virtual scrolling render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views, + currentView: views[0].type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(views[0].type, offset, startDayHour, endDayHour), { element: workSpace }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright.config.ts b/e2e/testcafe-devextreme/playwright.config.ts new file mode 100644 index 000000000000..d1341ff5fdcc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright.config.ts @@ -0,0 +1,55 @@ +import { defineConfig } from '@playwright/test'; + +const CHROME_FLAGS = [ + '--no-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--disable-partial-raster', + '--disable-skia-runtime-opts', + '--run-all-compositor-stages-before-draw', + '--disable-new-content-rendering-timeout', + '--disable-threaded-animation', + '--disable-threaded-scrolling', + '--disable-checker-imaging', + '--disable-image-animation-resync', + '--use-gl=swiftshader', + '--disable-features=PaintHolding', + '--js-flags=--random-seed=2147483647', + '--font-render-hinting=none', + '--disable-font-subpixel-positioning', +]; + +export default defineConfig({ + testDir: './playwright-tests', + outputDir: './playwright-results', + snapshotDir: './playwright-tests', + snapshotPathTemplate: '{snapshotDir}/{testFileDir}/etalons/{arg}{ext}', + + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.01, + threshold: 0.2, + animations: 'disabled', + }, + }, + + use: { + viewport: { width: 1200, height: 800 }, + screenshot: 'off', + trace: 'off', + launchOptions: { + args: CHROME_FLAGS, + }, + }, + + projects: [ + { + name: 'chromium', + use: { + browserName: 'chromium', + }, + }, + ], + + reporter: [['html', { open: 'never' }]], +}); diff --git a/e2e/testcafe-devextreme/tsconfig.json b/e2e/testcafe-devextreme/tsconfig.json index 59aa03d702c4..21655998716b 100644 --- a/e2e/testcafe-devextreme/tsconfig.json +++ b/e2e/testcafe-devextreme/tsconfig.json @@ -4,5 +4,11 @@ "types": [ "jquery" ] - } + }, + "exclude": [ + "playwright-tests", + "playwright-helpers", + "playwright-results", + "playwright-report" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab9499cad295..e1fa4d4ceaab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,13 +355,13 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: ~21.1.0 - version: 21.1.5(6ufluysnpyscwwzwonpw7avw2i) + version: 21.1.5(o5gxij6rgpqqzvumztenwt44ru) '@angular/animations': specifier: ~21.1.0 version: 21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/cli': specifier: ~21.1.5 - version: 21.1.5(@types/node@20.12.8)(chokidar@5.0.0) + version: 21.1.5(@types/node@25.5.0)(chokidar@5.0.0) '@angular/common': specifier: ~21.1.0 version: 21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -725,7 +725,7 @@ importers: version: 1.1.4 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) + version: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) jest-environment-node: specifier: 29.7.0 version: 29.7.0 @@ -776,7 +776,7 @@ importers: version: 4.0.0 ts-node: specifier: 10.9.2 - version: 10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3) vue-eslint-parser: specifier: 'catalog:' version: 10.0.0(eslint@9.39.4(jiti@2.6.1)) @@ -1095,6 +1095,9 @@ importers: '@eslint/eslintrc': specifier: 'catalog:' version: 3.3.5 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@stylistic/eslint-plugin': specifier: 'catalog:' version: 5.10.0(eslint@9.39.4(jiti@2.6.1)) @@ -1152,6 +1155,12 @@ importers: nconf: specifier: 0.12.1 version: 0.12.1 + pixelmatch: + specifier: ^7.1.0 + version: 7.1.0 + pngjs: + specifier: ^7.0.0 + version: 7.0.0 testcafe: specifier: 3.7.4 version: 3.7.4 @@ -1384,25 +1393,25 @@ importers: version: 7.29.0(@babel/core@7.29.0) '@devextreme-generator/angular': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/build-helpers': specifier: 3.0.12 - version: 3.0.12(h5cwyqq6zwtbaf5nld75dd5oii) + version: 3.0.12(fybkaxaxz3xev2nltvn2o5rw5y) '@devextreme-generator/core': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/declarations': specifier: 3.0.12 version: 3.0.12 '@devextreme-generator/inferno': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/react': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/vue': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@eslint-stylistic/metadata': specifier: 'catalog:' version: 2.13.0 @@ -1450,7 +1459,7 @@ importers: version: 0.14.2 autoprefixer: specifier: 10.4.27 - version: 10.4.27(postcss@8.4.38) + version: 10.4.27(postcss@8.5.8) axe-core: specifier: 'catalog:' version: 4.11.1 @@ -1513,7 +1522,7 @@ importers: version: 18.0.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-config-devextreme: specifier: 'catalog:' - version: 1.1.9(satltipdsoawfxnov7ffi4z7ju) + version: 1.1.9(z5gvwpd2vz7hyhtqnrp7ixzxle) eslint-migration-utils: specifier: workspace:* version: link:../eslint-migration-utils @@ -1525,7 +1534,7 @@ importers: version: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jest: specifier: 29.15.0 - version: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) + version: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) eslint-plugin-jest-formatting: specifier: 3.1.0 version: 3.1.0(eslint@9.39.4(jiti@2.6.1)) @@ -1780,7 +1789,7 @@ importers: version: 2.0.5 ts-jest: specifier: 29.1.2 - version: 29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) + version: 29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) tsc-alias: specifier: 1.8.16 version: 1.8.16 @@ -1860,7 +1869,7 @@ importers: version: 19.2.19(@angular/common@19.2.19(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.20)(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.19(@angular/common@19.2.19(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@babel/eslint-parser': specifier: 'catalog:' - version: 7.28.6(@babel/core@7.29.0)(eslint@9.39.4(jiti@2.6.1)) + version: 7.28.6(@babel/core@7.26.9)(eslint@9.39.4(jiti@2.6.1)) '@eslint-stylistic/metadata': specifier: 'catalog:' version: 2.13.0 @@ -1984,7 +1993,7 @@ importers: version: 29.5.14 ts-jest: specifier: 29.1.3 - version: 29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)))(typescript@5.9.3) packages/devextreme-react: dependencies: @@ -2091,7 +2100,7 @@ importers: version: 15.11.0(typescript@5.9.3) stylelint-config-standard-scss: specifier: 9.0.0 - version: 9.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)) + version: 9.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)) stylelint-scss: specifier: 6.10.0 version: 6.10.0(stylelint@15.11.0(typescript@5.9.3)) @@ -5869,6 +5878,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -11225,6 +11239,11 @@ packages: os: [darwin] deprecated: Upgrade to fsevents v2 to mitigate potential security issues + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -15082,6 +15101,10 @@ packages: resolution: {integrity: sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==} engines: {node: '>=20.x'} + pixelmatch@7.1.0: + resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} + hasBin: true + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -15102,6 +15125,16 @@ packages: resolution: {integrity: sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==} engines: {node: '>=16.0.0'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + plimit-lit@1.6.1: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} @@ -15135,6 +15168,10 @@ packages: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + portfinder@1.0.32: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -19183,13 +19220,13 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-angular@21.1.5(6ufluysnpyscwwzwonpw7avw2i)': + '@angular-devkit/build-angular@21.1.5(o5gxij6rgpqqzvumztenwt44ru)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.5(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2101.5(chokidar@5.0.0)(webpack-dev-server@5.2.2(webpack@5.105.0(@swc/core@1.15.3)(esbuild@0.27.2)))(webpack@5.105.0(@swc/core@1.15.3)(esbuild@0.27.2)) '@angular-devkit/core': 21.1.5(chokidar@5.0.0) - '@angular/build': 21.1.5(yfszryznq3cudajtfbi3mafxu4) + '@angular/build': 21.1.5(jnvohkvmeumnxrrszxgkvmipwy) '@angular/compiler-cli': 21.1.6(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.28.5 '@babel/generator': 7.28.5 @@ -19246,7 +19283,7 @@ snapshots: '@angular/platform-browser': 21.1.6(@angular/animations@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/platform-server': 21.1.6(@angular/common@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.1.6(@angular/animations@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) esbuild: 0.27.2 - jest: 29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) + jest: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) karma: 6.4.4 transitivePeerDependencies: - '@angular/compiler' @@ -19509,7 +19546,7 @@ snapshots: - yaml optional: true - '@angular/build@21.1.5(yfszryznq3cudajtfbi3mafxu4)': + '@angular/build@21.1.5(jnvohkvmeumnxrrszxgkvmipwy)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.5(chokidar@5.0.0) @@ -19518,8 +19555,8 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 - '@inquirer/confirm': 5.1.21(@types/node@20.12.8) - '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1)) + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1)) beasties: 0.3.5 browserslist: 4.28.1 esbuild: 0.27.2 @@ -19540,7 +19577,7 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 undici: 7.24.4 - vite: 7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) + vite: 7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) watchpack: 2.5.0 optionalDependencies: '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1) @@ -19611,13 +19648,13 @@ snapshots: - chokidar - supports-color - '@angular/cli@21.1.5(@types/node@20.12.8)(chokidar@5.0.0)': + '@angular/cli@21.1.5(@types/node@25.5.0)(chokidar@5.0.0)': dependencies: '@angular-devkit/architect': 0.2101.5(chokidar@5.0.0) '@angular-devkit/core': 21.1.5(chokidar@5.0.0) '@angular-devkit/schematics': 21.1.5(chokidar@5.0.0) - '@inquirer/prompts': 7.10.1(@types/node@20.12.8) - '@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@20.12.8))(@types/node@20.12.8)(listr2@9.0.5) + '@inquirer/prompts': 7.10.1(@types/node@25.5.0) + '@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@25.5.0))(@types/node@25.5.0)(listr2@9.0.5) '@modelcontextprotocol/sdk': 1.26.0(zod@4.3.5) '@schematics/angular': 21.1.5(chokidar@5.0.0) '@yarnpkg/lockfile': 1.1.0 @@ -19990,6 +20027,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/eslint-parser@7.28.6(@babel/core@7.26.9)(eslint@9.39.4(jiti@2.6.1))': + dependencies: + '@babel/core': 7.26.9 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + '@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@9.39.4(jiti@2.6.1))': dependencies: '@babel/core': 7.29.0 @@ -22840,9 +22885,9 @@ snapshots: dependencies: tslib: 2.3.1 - '@devextreme-generator/angular@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/angular@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22857,13 +22902,13 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/build-helpers@3.0.12(h5cwyqq6zwtbaf5nld75dd5oii)': + '@devextreme-generator/build-helpers@3.0.12(fybkaxaxz3xev2nltvn2o5rw5y)': dependencies: - '@devextreme-generator/angular': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/inferno': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/preact': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/react': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/angular': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/inferno': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/preact': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/react': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) loader-utils: 2.0.4 typescript: 4.3.5 vinyl: 2.2.1 @@ -22886,10 +22931,10 @@ snapshots: - uglify-js - webpack-cli - '@devextreme-generator/core@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/core@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: code-block-writer: 10.1.1 - eslint-config-devextreme: 0.2.0(yq7mldp4wyckzhqs3zqk5sdezm) + eslint-config-devextreme: 0.2.0(6ypj2vslczvlh7srkoyq3dooyq) prettier: 2.8.8 prettier-eslint: 13.0.0 typescript: 4.3.5 @@ -22912,11 +22957,11 @@ snapshots: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - '@devextreme-generator/inferno@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/inferno@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/preact': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/react': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/preact': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/react': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22931,10 +22976,10 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/preact@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/preact@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/react': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/react': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22949,9 +22994,9 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/react@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/react@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22966,10 +23011,10 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/vue@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/vue@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/angular': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/angular': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) prettier: 2.8.8 transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' @@ -23446,16 +23491,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/checkbox@4.3.2(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/checkbox@4.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23473,13 +23508,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/confirm@5.1.21(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/confirm@5.1.21(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23514,19 +23542,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/core@10.3.2(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/core@10.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23548,14 +23563,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/editor@4.2.23(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/external-editor': 1.0.3(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/editor@4.2.23(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23572,14 +23579,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/expand@4.0.23(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/expand@4.0.23(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23595,13 +23594,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/external-editor@1.0.3(@types/node@20.12.8)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.1 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/external-editor@1.0.3(@types/node@25.5.0)': dependencies: chardet: 2.1.1 @@ -23618,13 +23610,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/input@4.3.1(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/input@4.3.1(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23639,13 +23624,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/number@3.0.23(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/number@3.0.23(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23661,14 +23639,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/password@4.0.23(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/password@4.0.23(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23677,20 +23647,20 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/prompts@7.10.1(@types/node@20.12.8)': + '@inquirer/prompts@7.10.1(@types/node@25.5.0)': dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@20.12.8) - '@inquirer/confirm': 5.1.21(@types/node@20.12.8) - '@inquirer/editor': 4.2.23(@types/node@20.12.8) - '@inquirer/expand': 4.0.23(@types/node@20.12.8) - '@inquirer/input': 4.3.1(@types/node@20.12.8) - '@inquirer/number': 3.0.23(@types/node@20.12.8) - '@inquirer/password': 4.0.23(@types/node@20.12.8) - '@inquirer/rawlist': 4.1.11(@types/node@20.12.8) - '@inquirer/search': 3.2.2(@types/node@20.12.8) - '@inquirer/select': 4.4.2(@types/node@20.12.8) + '@inquirer/checkbox': 4.3.2(@types/node@25.5.0) + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) + '@inquirer/editor': 4.2.23(@types/node@25.5.0) + '@inquirer/expand': 4.0.23(@types/node@25.5.0) + '@inquirer/input': 4.3.1(@types/node@25.5.0) + '@inquirer/number': 3.0.23(@types/node@25.5.0) + '@inquirer/password': 4.0.23(@types/node@25.5.0) + '@inquirer/rawlist': 4.1.11(@types/node@25.5.0) + '@inquirer/search': 3.2.2(@types/node@25.5.0) + '@inquirer/select': 4.4.2(@types/node@25.5.0) optionalDependencies: - '@types/node': 20.12.8 + '@types/node': 25.5.0 '@inquirer/prompts@7.3.2(@types/node@20.11.17)': dependencies: @@ -23730,14 +23700,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/rawlist@4.1.11(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/rawlist@4.1.11(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23755,15 +23717,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/search@3.2.2(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/search@3.2.2(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23783,16 +23736,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/select@4.4.2(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/select@4.4.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23811,10 +23754,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/type@3.0.10(@types/node@20.12.8)': - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/type@3.0.10(@types/node@25.5.0)': optionalDependencies: '@types/node': 25.5.0 @@ -23935,6 +23874,43 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0(node-notifier@9.0.1) + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.12.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + optionalDependencies: + node-notifier: 9.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5))': dependencies: '@jest/console': 30.2.0 @@ -24050,7 +24026,7 @@ snapshots: - ts-node optional: true - '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5))': + '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -24065,7 +24041,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -24555,10 +24531,10 @@ snapshots: '@inquirer/prompts': 7.3.2(@types/node@25.5.0) '@inquirer/type': 1.5.5 - '@listr2/prompt-adapter-inquirer@3.0.5(@inquirer/prompts@7.10.1(@types/node@20.12.8))(@types/node@20.12.8)(listr2@9.0.5)': + '@listr2/prompt-adapter-inquirer@3.0.5(@inquirer/prompts@7.10.1(@types/node@25.5.0))(@types/node@25.5.0)(listr2@9.0.5)': dependencies: - '@inquirer/prompts': 7.10.1(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) + '@inquirer/prompts': 7.10.1(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) listr2: 9.0.5 transitivePeerDependencies: - '@types/node' @@ -25802,6 +25778,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@popperjs/core@2.11.8': {} '@preact/signals-core@1.8.0': {} @@ -27431,9 +27411,9 @@ snapshots: dependencies: vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.46.0)(yaml@2.8.1) - '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1))': dependencies: - vite: 7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) + vite: 7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.46.0)(yaml@2.8.1))': dependencies: @@ -28479,22 +28459,22 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - autoprefixer@10.4.27(postcss@8.4.38): + autoprefixer@10.4.27(postcss@8.5.6): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001776 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.4.38 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - autoprefixer@10.4.27(postcss@8.5.6): + autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001776 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -30259,6 +30239,21 @@ snapshots: - ts-node optional: true + create-jest@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-require@1.1.1: {} cross-env@7.0.3: @@ -31692,14 +31687,14 @@ snapshots: transitivePeerDependencies: - eslint-plugin-import - eslint-config-devextreme@0.2.0(yq7mldp4wyckzhqs3zqk5sdezm): + eslint-config-devextreme@0.2.0(6ypj2vslczvlh7srkoyq3dooyq): dependencies: '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) eslint: 9.39.4(jiti@2.6.1) eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-config-airbnb-typescript: 18.0.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) + eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) eslint-plugin-jest-formatting: 3.1.0(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-qunit: 8.2.5(eslint@9.39.4(jiti@2.6.1)) @@ -31782,25 +31777,6 @@ snapshots: stylelint: 16.22.0(typescript@5.9.3) stylelint-config-standard: 38.0.0(stylelint@16.22.0(typescript@5.9.3)) - eslint-config-devextreme@1.1.9(satltipdsoawfxnov7ffi4z7ju): - dependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - '@typescript-eslint/parser': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - eslint: 9.39.4(jiti@2.6.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-qunit: 8.2.5(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react-perf: 3.3.3(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-rulesdir: 0.2.2 - eslint-plugin-spellcheck: 0.0.20(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1)))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.4(jiti@2.6.1))) - stylelint: 15.11.0(typescript@4.9.5) - stylelint-config-standard: 38.0.0(stylelint@15.11.0(typescript@4.9.5)) - eslint-config-devextreme@1.1.9(sizsemxbssuejtezeqnearawue): dependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) @@ -31839,6 +31815,25 @@ snapshots: stylelint: 16.22.0(typescript@4.9.5) stylelint-config-standard: 38.0.0(stylelint@16.22.0(typescript@4.9.5)) + eslint-config-devextreme@1.1.9(z5gvwpd2vz7hyhtqnrp7ixzxle): + dependencies: + '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) + eslint: 9.39.4(jiti@2.6.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-no-only-tests: 3.3.0 + eslint-plugin-qunit: 8.2.5(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-perf: 3.3.3(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-rulesdir: 0.2.2 + eslint-plugin-spellcheck: 0.0.20(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1)))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.4(jiti@2.6.1))) + stylelint: 15.11.0(typescript@4.9.5) + stylelint-config-standard: 38.0.0(stylelint@15.11.0(typescript@4.9.5)) + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 @@ -31998,17 +31993,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jest@29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5): - dependencies: - '@typescript-eslint/utils': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - eslint: 9.39.4(jiti@2.6.1) - optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - jest: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - eslint-plugin-jest@29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): dependencies: '@typescript-eslint/utils': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) @@ -33111,6 +33095,9 @@ snapshots: nan: 2.22.0 optional: true + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -35091,6 +35078,27 @@ snapshots: - ts-node optional: true + jest-cli@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + optionalDependencies: + node-notifier: 9.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@30.2.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)): dependencies: '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)) @@ -35155,15 +35163,15 @@ snapshots: - ts-node optional: true - jest-cli@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)): + jest-cli@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35335,6 +35343,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.12.8 + ts-node: 10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 @@ -35367,6 +35406,37 @@ snapshots: - supports-color optional: true + jest-config@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 25.5.0 + ts-node: 10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@30.2.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)): dependencies: '@babel/core': 7.29.0 @@ -35567,39 +35637,6 @@ snapshots: - supports-color optional: true - jest-config@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)): - dependencies: - '@babel/core': 7.29.0 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 4.3.0 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.2.0(babel-plugin-macros@3.1.0) - jest-docblock: 30.2.0 - jest-environment-node: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-runner: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.2.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.12.8 - ts-node: 10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - jest-config@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 @@ -36328,6 +36365,20 @@ snapshots: - ts-node optional: true + jest@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + optionalDependencies: + node-notifier: 9.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@30.2.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)): dependencies: '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)) @@ -36374,12 +36425,12 @@ snapshots: - ts-node optional: true - jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)): + jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest-cli: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) optionalDependencies: node-notifier: 9.0.1 transitivePeerDependencies: @@ -38962,6 +39013,10 @@ snapshots: optionalDependencies: '@napi-rs/nice': 1.1.1 + pixelmatch@7.1.0: + dependencies: + pngjs: 7.0.0 + pkce-challenge@5.0.1: {} pkg-dir@4.2.0: @@ -38985,6 +39040,14 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + plimit-lit@1.6.1: dependencies: queue-lit: 1.5.2 @@ -39018,6 +39081,8 @@ snapshots: pngjs@6.0.0: {} + pngjs@7.0.0: {} + portfinder@1.0.32: dependencies: async: 2.6.4 @@ -39103,9 +39168,9 @@ snapshots: dependencies: postcss: 8.5.6 - postcss-scss@4.0.9(postcss@8.5.8): + postcss-scss@4.0.9(postcss@8.5.6): dependencies: - postcss: 8.5.8 + postcss: 8.5.6 postcss-selector-parser@6.1.2: dependencies: @@ -41254,14 +41319,14 @@ snapshots: postcss-html: 1.7.0 stylelint: 16.22.0(typescript@5.9.3) - stylelint-config-recommended-scss@11.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)): + stylelint-config-recommended-scss@11.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.8) + postcss-scss: 4.0.9(postcss@8.5.6) stylelint: 15.11.0(typescript@5.9.3) stylelint-config-recommended: 12.0.0(stylelint@15.11.0(typescript@5.9.3)) stylelint-scss: 4.7.0(stylelint@15.11.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.6 stylelint-config-recommended-vue@1.6.1(postcss-html@1.7.0)(stylelint@16.22.0(typescript@5.9.3)): dependencies: @@ -41291,13 +41356,13 @@ snapshots: dependencies: stylelint: 16.22.0(typescript@5.9.3) - stylelint-config-standard-scss@9.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)): + stylelint-config-standard-scss@9.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)): dependencies: stylelint: 15.11.0(typescript@5.9.3) - stylelint-config-recommended-scss: 11.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)) + stylelint-config-recommended-scss: 11.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)) stylelint-config-standard: 33.0.0(stylelint@15.11.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.6 stylelint-config-standard@33.0.0(stylelint@15.11.0(typescript@5.9.3)): dependencies: @@ -42408,11 +42473,11 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.29.0) - ts-jest@29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5): + ts-jest@29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -42460,17 +42525,17 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.6) - ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): + ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)) + jest: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.7.2 - typescript: 4.9.5 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 @@ -42478,17 +42543,17 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.29.0) - ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.7.2 - typescript: 5.9.3 + typescript: 4.9.5 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 @@ -42582,27 +42647,6 @@ snapshots: optionalDependencies: '@swc/core': 1.15.3 - ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.8 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 4.9.5 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optionalDependencies: - '@swc/core': 1.15.3 - optional: true - ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -43477,7 +43521,7 @@ snapshots: terser: 5.46.0 yaml: 2.8.1 - vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1): + vite@7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -43486,7 +43530,7 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 20.12.8 + '@types/node': 25.5.0 fsevents: 2.3.3 jiti: 2.6.1 less: 4.4.2