diff --git a/.github/actions/variables/action.yml b/.github/actions/variables/action.yml index 8eab5e9dc10a..36f5de31052e 100644 --- a/.github/actions/variables/action.yml +++ b/.github/actions/variables/action.yml @@ -41,9 +41,11 @@ runs: export IS_DEPENDABOT="${{ github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' }}" export CYPRESS_failOnSnapshotDiff="0" export CYPRESS_API_URL="http://localhost:1234/" - export SNAPSHOTS_PATH="./projects/demo-integrations/cypress/snapshots" + export CYPRESS_SNAPSHOTS_PATH="./projects/demo-integrations/cypress/snapshots" + export PLAYWRIGHT_SNAPSHOTS_PATH="./projects/demo-playwright/tests-results" export SNAPSHOTS_CACHE_KEY="e2e-cache--${{ github.event.pull_request.head.sha || github.sha }}-${{ github.event.number }}" - export SNAPSHOTS_ARTIFACTS_KEY="e2e-artifacts--${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_id }}-${{ github.event.number }}" + export CYPRESS_SNAPSHOTS_ARTIFACTS_KEY="cypress-e2e-artifacts--${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_id }}-${{ github.event.number }}" + export PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY="playwright-e2e-artifacts--${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_id }}-${{ github.event.number }}" if [[ "$IS_FORK" == "false" && "$IS_DEPENDABOT" == "false" ]]; then export IS_OWNER_MODE="true" @@ -74,6 +76,8 @@ runs: echo "SUPPORT_AUTO_PUSH=$SUPPORT_AUTO_PUSH" >> $GITHUB_ENV echo "CYPRESS_failOnSnapshotDiff=$CYPRESS_failOnSnapshotDiff" >> $GITHUB_ENV echo "CYPRESS_API_URL=$CYPRESS_API_URL" >> $GITHUB_ENV - echo "SNAPSHOTS_PATH=$SNAPSHOTS_PATH" >> $GITHUB_ENV - echo "SNAPSHOTS_ARTIFACTS_KEY=$SNAPSHOTS_ARTIFACTS_KEY" >> $GITHUB_ENV + echo "CYPRESS_SNAPSHOTS_PATH=$CYPRESS_SNAPSHOTS_PATH" >> $GITHUB_ENV + echo "PLAYWRIGHT_SNAPSHOTS_PATH=$PLAYWRIGHT_SNAPSHOTS_PATH" >> $GITHUB_ENV + echo "CYPRESS_SNAPSHOTS_ARTIFACTS_KEY=$CYPRESS_SNAPSHOTS_ARTIFACTS_KEY" >> $GITHUB_ENV + echo "PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY=$PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY" >> $GITHUB_ENV echo "SNAPSHOTS_CACHE_KEY=$SNAPSHOTS_CACHE_KEY" >> $GITHUB_ENV diff --git a/.github/screenshot-bot.config.toml b/.github/screenshot-bot.config.toml index 6eccf2fffbf2..18d2a586d5dd 100644 --- a/.github/screenshot-bot.config.toml +++ b/.github/screenshot-bot.config.toml @@ -1,13 +1,14 @@ # array of RegExp strings to match workflow names # which should be watched by bot workflowWithTests = [ - '.*E2E Summary.*', # all workflows with sub-string "e2e" in their names will be watched by bot + '.*E2E*', # all workflows with sub-string "e2e" in their names will be watched by bot ] # array of RegExp strings to match images inside artifacts (by their path or file name) # which shows difference between two screenshot and which will be added to bot report comment screenshotsDiffsPaths = [ '.*__diff_output__.*', # it is default cypress folder name into which snapshot diffs are put + '.*-diff.png' ] # RegExp string to match images inside artifacts (by their path or file name) diff --git a/.github/workflows/e2e-summary.yml b/.github/workflows/e2e-summary.yml deleted file mode 100644 index 46e1e2efe2db..000000000000 --- a/.github/workflows/e2e-summary.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: ⚙️ E2E Summary report -on: - pull_request: - -jobs: - report: - runs-on: ubuntu-latest - if: ${{ !contains(github.head_ref , 'release/') }} - steps: - - uses: actions/checkout@v3.5.3 - - name: Setup global variables - uses: ./.github/actions/variables - - - name: Wait for e2e tests to succeed - uses: codex-/await-local-workflow-run@v1 - with: - token: ${{ github.token }} - workflow: e2e.yml - timeout_mins: 120 - poll_interval_ms: 60000 - - - name: Download cache / ${{ env.SNAPSHOTS_CACHE_KEY }} - uses: actions/cache/restore@v3.3.1 - with: - path: ${{ env.SNAPSHOTS_PATH }} - key: ${{ env.SNAPSHOTS_CACHE_KEY }} - - - name: Debug output - continue-on-error: true - run: tree ${{ env.SNAPSHOTS_PATH }} - - - name: Check if diff-output exists - id: diff-checker - run: | - echo "diff_exist=$(find ${{ env.SNAPSHOTS_PATH }} -regex '.*\.diff\.png$' | wc -l | sed -e 's/^[[:space:]]*//')" >> $GITHUB_OUTPUT - - - name: Upload artifacts / ${{ env.SNAPSHOTS_ARTIFACTS_KEY }} - if: ${{ steps.diff-checker.outputs.diff_exist != '0' }} - continue-on-error: true - uses: actions/upload-artifact@v3.1.2 - with: - name: ${{ env.SNAPSHOTS_ARTIFACTS_KEY }} - path: ${{ env.SNAPSHOTS_PATH }} - - - name: Fall with an error if diff-output exists - if: ${{ steps.diff-checker.outputs.diff_exist != '0' }} - run: | - find ${{ env.SNAPSHOTS_PATH }} -regex '.*\.diff\.png$' -exec echo "{}" \; - exit 1 - -concurrency: - group: e2e-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ab125f76b318..095ba7836e23 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -22,7 +22,7 @@ jobs: uses: ./.github/actions/nodejs - name: Building demo-app of git-branch without cache - run: npx nx build demo -c next --output-path ${{ env.DIST }} + run: npx nx build demo -c next --output-path ${{ env.DIST }} --base-href='/' - name: Upload cache / ${{ env.CACHE_DIST_KEY }} uses: actions/cache/save@v3.3.1 @@ -97,40 +97,118 @@ jobs: - name: Clean up resources run: npx kill-port --port ${{ env.NG_SERVER_PORT }} - - name: Upload artifacts / ${{ env.SNAPSHOTS_ARTIFACTS_KEY }} + - name: Debug output + continue-on-error: true + run: tree ${{ env.CYPRESS_SNAPSHOTS_PATH }} + + - name: Upload artifacts / ${{ env.CYPRESS_SNAPSHOTS_ARTIFACTS_KEY }} + uses: actions/upload-artifact@v3.1.2 + with: + path: ${{ env.CYPRESS_SNAPSHOTS_PATH }}/**/*.diff.png + name: ${{ env.CYPRESS_SNAPSHOTS_ARTIFACTS_KEY }} + if-no-files-found: ignore + retention-days: 1 + + playwright: + if: ${{ !contains(github.head_ref , 'release/') }} + runs-on: ubuntu-latest + needs: [build-demo] + steps: + - uses: actions/checkout@v3.5.3 + - name: Setup global variables + uses: ./.github/actions/variables + - name: Setup Node.js and Cache + uses: ./.github/actions/nodejs + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Download cache / ${{ env.CACHE_DIST_KEY }} + uses: actions/cache/restore@v3.3.1 + with: + path: dist/demo + key: ${{ env.CACHE_DIST_KEY }} + + - name: Serve ${{ env.DIST }} in background + run: npx nx serve-compiled demo --path ${{ env.DIST }} --port ${{ env.NG_SERVER_PORT }} + + - name: Run screenshot tests on ${{ env.DIST }} + run: npx nx e2e demo-playwright -- --update-snapshots + + - name: Clean up resources + run: npx kill-port --port ${{ env.NG_SERVER_PORT }} + + - name: Download ${{ env.DIST_NEXT }} for serve locally + run: | + git clone \ + --depth 1 \ + --branch snapshots/demo/next/${{ github.base_ref }} \ + https://github.com/Tinkoff/taiga-ui.git ${{ env.DIST_NEXT }} + + - name: Find and replace baseHref for next snapshot + uses: jacobtomlinson/gha-find-replace@v2 + with: + find: '' + replace: '' + include: '${{ env.DIST_NEXT }}/index.html' + regex: false + + - name: Serve ${{ env.DIST_NEXT }} in background + run: npx nx serve-compiled demo --path ${{ env.DIST_NEXT }} --port ${{ env.NG_SERVER_PORT }} + + - name: Run screenshot tests on ${{ env.DIST_NEXT }} + continue-on-error: true + run: npx nx e2e demo-playwright + + - name: Clean up resources + run: npx kill-port --port ${{ env.NG_SERVER_PORT }} + + - name: Debug output + continue-on-error: true + run: tree ${{ env.PLAYWRIGHT_SNAPSHOTS_PATH }} + + - name: Upload artifacts / ${{ env.PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY }} uses: actions/upload-artifact@v3.1.2 with: - path: ${{ env.SNAPSHOTS_PATH }}/**/*.diff.png - name: ${{ env.SNAPSHOTS_ARTIFACTS_KEY }} + path: ${{ env.PLAYWRIGHT_SNAPSHOTS_PATH }}/**/*-diff.png + name: ${{ env.PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY }} if-no-files-found: ignore retention-days: 1 result: if: ${{ !contains(github.head_ref , 'release/') }} name: result - needs: [cypress] + needs: [cypress, playwright] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3.5.3 - name: Setup global variables uses: ./.github/actions/variables - - name: Download artifacts / ${{ env.SNAPSHOTS_ARTIFACTS_KEY }} + - name: Download artifacts / ${{ env.CYPRESS_SNAPSHOTS_ARTIFACTS_KEY }} continue-on-error: true uses: actions/download-artifact@v3.0.2 with: - name: ${{ env.SNAPSHOTS_ARTIFACTS_KEY }} - path: ${{ env.SNAPSHOTS_PATH }} + name: ${{ env.CYPRESS_SNAPSHOTS_ARTIFACTS_KEY }} + path: ./total/cypress - - name: Upload cache / ${{ env.SNAPSHOTS_CACHE_KEY }} - uses: actions/cache/save@v3.3.1 + - name: Download artifacts / ${{ env.PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY }} + continue-on-error: true + uses: actions/download-artifact@v3.0.2 with: - path: ${{ env.SNAPSHOTS_PATH }} - key: ${{ env.SNAPSHOTS_CACHE_KEY }} + name: ${{ env.PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY }} + path: ./total/playwright - - name: Debug output - continue-on-error: true - run: tree ${{ env.SNAPSHOTS_PATH }} + - name: Check if diff-output exists + id: diff-checker + run: | + echo "diff_exist=$(find ./total -regex '.*diff\.png$' | wc -l | sed -e 's/^[[:space:]]*//')" >> $GITHUB_OUTPUT + + - name: Fall with an error if diff-output exists + if: ${{ steps.diff-checker.outputs.diff_exist != '0' }} + run: | + find ./total -regex '.*diff\.png$' -exec echo "{}" \; + exit 1 concurrency: group: e2e-${{ github.workflow }}-${{ github.ref }} diff --git a/.gitignore b/.gitignore index ba6015e7f6d8..f7026e28cc88 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ dist RELEASE_BODY.md *tsbuildinfo .angular +/projects/demo-playwright/tests-results/ +/projects/demo-playwright/tests-report/ +/projects/demo-playwright/snapshots/ +/projects/demo-playwright/playwright/.cache/ diff --git a/package-lock.json b/package-lock.json index 370cc33171ef..39cede597fb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@nx/jest": "16.6.0", "@nx/node": "16.6.0", "@nx/workspace": "16.6.0", + "@playwright/test": "^1.36.2", "@testing-library/cypress": "9.0.0", "@tinkoff/prettier-config": "1.52.1", "@tinkoff/stylelint-config": "1.52.1", @@ -6776,6 +6777,25 @@ "typescript": "^3 || ^4 || ^5" } }, + "node_modules/@playwright/test": { + "version": "1.36.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.2.tgz", + "integrity": "sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.36.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -30354,6 +30374,18 @@ "node": ">=8" } }, + "node_modules/playwright-core": { + "version": "1.36.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.2.tgz", + "integrity": "sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 816e99099997..c25adbdd3e5e 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "@nx/jest": "16.6.0", "@nx/node": "16.6.0", "@nx/workspace": "16.6.0", + "@playwright/test": "^1.36.2", "@testing-library/cypress": "9.0.0", "@tinkoff/prettier-config": "1.52.1", "@tinkoff/stylelint-config": "1.52.1", diff --git a/projects/demo-playwright/playwright.config.ts b/projects/demo-playwright/playwright.config.ts new file mode 100644 index 000000000000..ae340822aed7 --- /dev/null +++ b/projects/demo-playwright/playwright.config.ts @@ -0,0 +1,39 @@ +import {defineConfig, devices} from '@playwright/test'; +import {ViewportSize} from 'playwright-core'; + +const DEFAULT_VIEWPORT: ViewportSize = {width: 700, height: 700}; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: __dirname, + testMatch: `**/*.spec.ts`, + outputDir: `tests-results`, + snapshotDir: `snapshots`, + reporter: process.env.CI ? `github` : [[`html`, {outputFolder: `tests-report`}]], + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 0 : 0, + use: { + baseURL: `http://localhost:${process.env.NG_SERVER_PORT || 3333}`, + trace: `on-first-retry`, + viewport: DEFAULT_VIEWPORT, + }, + projects: [ + { + name: `chromium`, + use: { + ...devices[`Desktop Chrome`], + viewport: DEFAULT_VIEWPORT, + }, + }, + ], + expect: { + toHaveScreenshot: { + animations: `disabled`, + caret: `hide`, + }, + }, +}); diff --git a/projects/demo-playwright/project.json b/projects/demo-playwright/project.json new file mode 100644 index 000000000000..04e370922122 --- /dev/null +++ b/projects/demo-playwright/project.json @@ -0,0 +1,33 @@ +{ + "name": "demo-playwright", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "projects/demo-playwright/cypress", + "projectType": "application", + "prefix": "app", + "targets": { + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "eslint --no-error-on-unmatched-pattern \"**/projects/demo-playwright/**/*\"" + } + }, + "stylelint": { + "executor": "nx:run-commands", + "options": { + "command": "stylelint \"**/demo-playwright/**/*.{css,less}\" --allow-empty-input" + } + }, + "e2e": { + "executor": "nx:run-commands", + "options": { + "command": "playwright test --config projects/demo-playwright/playwright.config.ts" + } + }, + "e2e-ui": { + "executor": "nx:run-commands", + "options": { + "command": "nx e2e demo-playwright -- --ui" + } + } + } +} diff --git a/projects/demo-playwright/tests/example.spec.ts b/projects/demo-playwright/tests/example.spec.ts new file mode 100644 index 000000000000..fa7afbe9d16b --- /dev/null +++ b/projects/demo-playwright/tests/example.spec.ts @@ -0,0 +1,17 @@ +import {expect, test} from '@playwright/test'; + +test(`has title`, async ({page}) => { + await page.goto(`/getting-started`); + + await expect(page).toHaveTitle(`Taiga UI: Getting started`); +}); + +test(`debug screenshot`, async ({page}) => { + await page.goto(`/components/mobile-calendar`); + + await page.locator(`tui-mobile-calendar-example-1 button`).click(); + + await expect(page.locator(`tui-dialog tui-mobile-calendar`)).toHaveScreenshot( + `test-playwright-screenshot.png`, + ); +}); diff --git a/projects/demo/src/modules/components/mobile-calendar/examples/1/index.ts b/projects/demo/src/modules/components/mobile-calendar/examples/1/index.ts index 479402ffe083..c22bb502e12a 100644 --- a/projects/demo/src/modules/components/mobile-calendar/examples/1/index.ts +++ b/projects/demo/src/modules/components/mobile-calendar/examples/1/index.ts @@ -18,7 +18,7 @@ import {map} from 'rxjs/operators'; encapsulation, }) export class TuiMobileCalendarExample1 { - private readonly control = new FormControl(new TuiDay(2024, 9, 3)); + private readonly control = new FormControl(new TuiDay(2024, 10, 3)); private readonly dialog$: Observable;