Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
170 changes: 170 additions & 0 deletions .github/workflows/playwright_tests.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions e2e/testcafe-devextreme/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export default [
{
ignores: [
'node_modules/**',
'playwright-tests/**',
'playwright-helpers/**',
'playwright-results/**',
'playwright-report/**',
],
},
...spellCheckConfig,
Expand Down
27 changes: 16 additions & 11 deletions e2e/testcafe-devextreme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
15 changes: 15 additions & 0 deletions e2e/testcafe-devextreme/playwright-helpers/createWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Page } from '@playwright/test';

export async function createWidget(
page: Page,
widgetName: string,
widgetOptions: Record<string, unknown> | (() => Record<string, unknown>),
selector = '#container',
disableFxAnimation = true,
): Promise<void> {
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 });
}
74 changes: 74 additions & 0 deletions e2e/testcafe-devextreme/playwright-helpers/domUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Page } from '@playwright/test';

export async function setAttribute(
page: Page,
selector: string,
attribute: string,
value: string,
): Promise<void> {
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<string> {
return page.evaluate(
(sel) => document.querySelector(sel)?.getAttribute('style') ?? '',
selector,
);
}

export async function setStyleAttribute(
page: Page,
selector: string,
styleValue: string,
): Promise<void> {
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<void> {
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<void> {
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<string, string>,
): Promise<void> {
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<void> {
await page.evaluate(({ sel, cls }) => {
document.querySelector(sel)?.setAttribute('class', cls);
}, { sel: selector, cls: className });
}
28 changes: 28 additions & 0 deletions e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
type OptionMatrix<T> = {
[K in keyof T]: T[K][];
};

export function generateOptionMatrix<T extends Record<string, unknown>>(
matrix: OptionMatrix<T>,
): T[] {
const keys = Object.keys(matrix) as (keyof T)[];
const combinations: T[] = [];

function generate(index: number, current: Partial<T>): 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;
}
26 changes: 26 additions & 0 deletions e2e/testcafe-devextreme/playwright-helpers/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading