From 8db3e4292fa250e591fb36c258c4dd00bb9c7160 Mon Sep 17 00:00:00 2001 From: splincode Date: Tue, 12 Nov 2024 16:11:01 +0300 Subject: [PATCH] ci: deploy e2e report to firebase --- .firebaserc | 15 ++++++++ .github/workflows/auto-merge.yml | 2 +- .github/workflows/build-demo.yml | 1 - .github/workflows/cleanup-resources.yml | 34 +++++++++++++++++ .../{deploy-gh-pages.yml => deploy-prod.yml} | 2 +- ...{deploy-preview.yml => deploy-staging.yml} | 11 +++--- .github/workflows/e2e.yml | 37 ++++++++++++++++--- .gitignore | 1 + firebase.json | 35 +++++++++++++----- projects/demo-playwright/playwright.config.ts | 26 ++++++++----- projects/demo-playwright/project.json | 2 +- .../demo-playwright/tests/toolbar.spec.ts | 33 +++++++++++++---- projects/demo-playwright/utils/goto.ts | 7 ++++ .../demo-playwright/utils/wait-for-fonts.ts | 12 ++++++ .../utils/wait-stable-state.ts | 17 +++++++++ 15 files changed, 192 insertions(+), 43 deletions(-) create mode 100644 .firebaserc create mode 100644 .github/workflows/cleanup-resources.yml rename .github/workflows/{deploy-gh-pages.yml => deploy-prod.yml} (98%) rename .github/workflows/{deploy-preview.yml => deploy-staging.yml} (81%) create mode 100644 projects/demo-playwright/utils/wait-for-fonts.ts create mode 100644 projects/demo-playwright/utils/wait-stable-state.ts diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 000000000..88a47b07d --- /dev/null +++ b/.firebaserc @@ -0,0 +1,15 @@ +{ + "projects": {}, + "targets": { + "taiga-editor": { + "hosting": { + "taiga-editor": [ + "taiga-editor" + ], + "taiga-editor-e2e-report": [ + "taiga-editor-e2e-report" + ] + } + } + } +} diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index a2343f54e..b5d622d50 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -3,7 +3,7 @@ on: pull_request: env: - JOBS_NAME: '[ "Build package", "Build demo", "Lint", "Tests", "E2E" ]' + JOBS_NAME: '[ "Build package", "Lint", "Tests", "E2E" ]' jobs: setup: diff --git a/.github/workflows/build-demo.yml b/.github/workflows/build-demo.yml index 2a405dd7a..0c84124bf 100644 --- a/.github/workflows/build-demo.yml +++ b/.github/workflows/build-demo.yml @@ -1,6 +1,5 @@ name: ⚙️ Build demo on: - pull_request: push: branches: - main diff --git a/.github/workflows/cleanup-resources.yml b/.github/workflows/cleanup-resources.yml new file mode 100644 index 000000000..d2fb52885 --- /dev/null +++ b/.github/workflows/cleanup-resources.yml @@ -0,0 +1,34 @@ +name: 🤖 Cleanup resources after close PR +on: + pull_request: + types: [closed] + +jobs: + purge-cache: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + BRANCH: refs/pull/${{ github.event.number }}/merge + steps: + - uses: actions/checkout@v4.2.2 + continue-on-error: true + - run: | + gh cache list --ref $BRANCH > cache.log && cat cache.log + + for cacheId in $(gh cache list --ref $BRANCH --json id | jq -r .[].id); do + echo "> gh cache delete $cacheId" + gh cache delete $cacheId + done + + delete-firebase-channel: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4.2.2 + - uses: w9jds/firebase-action@v13.25.0 + continue-on-error: true + with: + args: hosting:channel:delete pr${{ github.event.number }}-${{ github.head_ref }} --force + env: + GCP_SA_KEY: ${{ secrets.FIREBASE_TAIGA_PREVIEWS_SA }} + PROJECT_ID: taiga-editor diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-prod.yml similarity index 98% rename from .github/workflows/deploy-gh-pages.yml rename to .github/workflows/deploy-prod.yml index fac39b6ca..e32de49ed 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-prod.yml @@ -4,7 +4,7 @@ on: branches: [main] jobs: - deploy: + prod: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-staging.yml similarity index 81% rename from .github/workflows/deploy-preview.yml rename to .github/workflows/deploy-staging.yml index 879c1ac06..d5e82563e 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-staging.yml @@ -1,23 +1,22 @@ -name: 🚀 Deploy / preview +name: 🚀 Deploy on: pull_request jobs: - build_and_preview: - name: Firebase + staging: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 - uses: taiga-family/ci/actions/setup/variables@v1.93.5 - uses: taiga-family/ci/actions/setup/node@v1.93.5 - run: npx nx build editor-demo - - name: Deploy preview - uses: FirebaseExtended/action-hosting-deploy@v0 + - uses: FirebaseExtended/action-hosting-deploy@v0.9.0 continue-on-error: true if: ${{ env.IS_FORK == 'false' && env.IS_DEPENDABOT == 'false' }} with: - repoToken: ${{ secrets.GITHUB_TOKEN }} firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_TUI_EDITOR }} + repoToken: ${{ secrets.GITHUB_TOKEN }} projectId: taiga-editor + target: taiga-editor expires: 1d concurrency: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d61e28d0b..49a79f9a9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,6 +6,8 @@ jobs: e2e: name: E2E runs-on: ubuntu-latest + env: + PW_RESULT: ./projects/demo-playwright/tests-results steps: - uses: actions/checkout@v4.2.2 - uses: taiga-family/ci/actions/setup/variables@v1.93.5 @@ -32,22 +34,45 @@ jobs: - run: npx nx e2e editor-demo-playwright continue-on-error: true - - uses: actions/upload-artifact@v4.4.3 + - run: tree ${{ env.PW_RESULT }} + - name: Deploy e2e report + id: e2e-report + uses: FirebaseExtended/action-hosting-deploy@v0.9.0 + continue-on-error: true + with: + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_TUI_EDITOR }} + repoToken: ${{ secrets.GITHUB_TOKEN }} + target: taiga-editor-e2e-report + projectId: taiga-editor + disableComment: 'true' + expires: 1d + + - name: Upload artifacts / ${{ env.PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY }} + uses: actions/upload-artifact@v4.4.3 + id: artifact with: - path: ./projects/demo-playwright/tests-results/**/*-diff.png + path: ${{ env.PW_RESULT }}/**/*-retry2/*-diff.png name: ${{ env.PLAYWRIGHT_SNAPSHOTS_ARTIFACTS_KEY }} if-no-files-found: ignore compression-level: 0 retention-days: 1 - - id: diff-checker + - name: Check if diff-output exists + id: diff_checker run: | - echo "diff_exist=$(find ./projects/demo-playwright/tests-results -regex '.*diff\.png$' | wc -l | sed -e 's/^[[:space:]]*//')" >> $GITHUB_OUTPUT - - if: ${{ steps.diff-checker.outputs.diff_exist != '0' }} + echo "diff_exist=$(find ${{ env.PW_RESULT }} -path '*retry2/*' -iname '*-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 ./projects/demo-playwright/tests-results -regex '.*diff\.png$' -exec echo "{}" \; + find ${{ env.PW_RESULT }} -path '*retry2/*' -iname '*-diff.png' -exec echo "{}" \; exit 1 + - uses: daun/playwright-report-summary@v3.6.0 + if: always() + with: + custom-info: 'For more information, [see our report](${{ steps.e2e-report.outputs.details_url }})' + report-file: ${{ env.PW_RESULT }}/test-results.json + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.gitignore b/.gitignore index badd7369f..a627f36df 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ # misc /coverage +test-results.json *.log # System Files diff --git a/firebase.json b/firebase.json index 86e0ea473..0e0d98d59 100644 --- a/firebase.json +++ b/firebase.json @@ -1,12 +1,27 @@ { - "hosting": { - "public": "dist/demo/browser", - "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - } + "$schema": "https://raw.githubusercontent.com/firebase/firebase-tools/master/schema/firebase-config.json", + "hosting": [ + { + "target": "taiga-editor", + "public": "dist/demo/browser", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + }, + { + "target": "taiga-editor-e2e-report", + "public": "projects/demo-playwright/tests-report", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } + ] } diff --git a/projects/demo-playwright/playwright.config.ts b/projects/demo-playwright/playwright.config.ts index bafd4f92e..14c24a504 100644 --- a/projects/demo-playwright/playwright.config.ts +++ b/projects/demo-playwright/playwright.config.ts @@ -1,24 +1,33 @@ +import type {ViewportSize} from '@playwright/test'; import {defineConfig, devices} from '@playwright/test'; -/** - * See https://playwright.dev/docs/test-configuration. - */ +const DEFAULT_VIEWPORT: ViewportSize = {width: 750, height: 700}; + export default defineConfig({ testDir: __dirname, testMatch: '**/*.spec.ts', outputDir: 'tests-results', snapshotDir: 'snapshots', - reporter: process.env.CI ? 'github' : [['html', {outputFolder: 'tests-report'}]], + reporter: [ + ['html', {outputFolder: 'tests-report'}], + ['json', {outputFile: 'tests-results/test-results.json'}], + ], 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 ? 1 : 0, + retries: process.env.CI ? 2 : 0, workers: process.env.CI ? '100%' : '50%', + timeout: 5 * 60 * 1000, use: { baseURL: `http://localhost:${process.env.NG_SERVER_PORT ?? 3333}`, trace: 'on-first-retry', + testIdAttribute: 'automation-id', + actionTimeout: 10_000, contextOptions: { + deviceScaleFactor: 2, reducedMotion: 'reduce', + viewport: DEFAULT_VIEWPORT, + screen: DEFAULT_VIEWPORT, + hasTouch: true, }, }, projects: [ @@ -26,10 +35,7 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome HiDPI'], - viewport: { - width: 720, - height: 1024, - }, + viewport: DEFAULT_VIEWPORT, }, }, ], diff --git a/projects/demo-playwright/project.json b/projects/demo-playwright/project.json index a1035aa2e..04bc4c11a 100644 --- a/projects/demo-playwright/project.json +++ b/projects/demo-playwright/project.json @@ -14,7 +14,7 @@ "e2e-ui": { "executor": "nx:run-commands", "options": { - "command": "nx e2e editor-demo-playwright -- --ui --debug --update-snapshots" + "command": "nx e2e editor-demo-playwright --ui --debug --update-snapshots" } } } diff --git a/projects/demo-playwright/tests/toolbar.spec.ts b/projects/demo-playwright/tests/toolbar.spec.ts index e0f419a9b..3a2866ce6 100644 --- a/projects/demo-playwright/tests/toolbar.spec.ts +++ b/projects/demo-playwright/tests/toolbar.spec.ts @@ -15,9 +15,11 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__color-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await page.locator('[automation-id="toolbar__hilite-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await expect(page.locator('#demo-content tui-editor')).toHaveScreenshot( 'Toolbar-02.png', @@ -30,6 +32,7 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__color-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await expect(page.locator('#demo-content tui-editor')).toHaveScreenshot( 'Toolbar-03.png', @@ -58,6 +61,7 @@ test.describe('Toolbar', () => { await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-07.png'); await page.keyboard.type('awesome library for awesome people'); + await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-08.png'); @@ -75,11 +79,16 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.keyboard.type('\n\n\n\n'); + await page.waitForTimeout(300); + await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp'); + await page.locator('[contenteditable]').scrollIntoViewIfNeeded(); + await page.waitForTimeout(100); await page.locator('[automation-id="toolbar__insert-table-button"]').click(); + await page.waitForTimeout(100); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-11.png'); @@ -105,28 +114,33 @@ test.describe('Toolbar', () => { await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-13.png'); await page.locator('[automation-id="toolbar__ordering-list-button"]').focus(); - await page.waitForTimeout(100); + await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await page .locator('[automation-id="toolbar__un-ordered-list-button"].t-option') .focus(); - await page.waitForTimeout(100); + await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await page.locator('[automation-id="toolbar__font-style-button"]').focus(); - await page.waitForTimeout(100); + await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await page.locator('[contenteditable]').nth(0).focus(); - await page.waitForTimeout(100); + await page.keyboard.type('12345'); + await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-14.png'); await page.locator('[automation-id="toolbar__insert-table-button"]').focus(); - await page.waitForTimeout(100); + await page.keyboard.press('Enter'); + await page.waitForTimeout(300); const cell = page .locator('tui-table-size-selector .t-column') @@ -134,7 +148,6 @@ test.describe('Toolbar', () => { .locator('.t-cell') .nth(1); - await page.waitForTimeout(100); await cell.hover(); await cell.click(); @@ -149,17 +162,24 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__align-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-16.png'); + await page.locator('[contenteditable]').nth(0).focus(); + await page.locator('[automation-id="toolbar__align-button"]').focus(); + await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await page.keyboard.press('ArrowRight'); await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-17.png'); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('Enter'); + await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-18.png'); }); @@ -170,7 +190,6 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.keyboard.press('Meta+A'); await page.keyboard.press('Backspace'); - await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-19.png'); diff --git a/projects/demo-playwright/utils/goto.ts b/projects/demo-playwright/utils/goto.ts index d20e717f1..9dd1a0a2e 100644 --- a/projects/demo-playwright/utils/goto.ts +++ b/projects/demo-playwright/utils/goto.ts @@ -2,6 +2,8 @@ import type {Page} from '@playwright/test'; import {tuiMockDate} from './mock-date'; import {tuiMockImages} from './mock-images'; +import {tuiWaitForFonts} from './wait-for-fonts'; +import {waitStableState} from './wait-stable-state'; interface TuiGotoOptions extends NonNullable[1]> { date?: Date; @@ -39,6 +41,11 @@ export async function tuiGoto( const response = await page.goto(url, playwrightGotoOptions); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + await tuiWaitForFonts(page); + await waitStableState(page.locator('app')); + if (hideHeader) { for (const locator of await page.locator('[tuidocheader]').all()) { if (await locator.isVisible()) { diff --git a/projects/demo-playwright/utils/wait-for-fonts.ts b/projects/demo-playwright/utils/wait-for-fonts.ts new file mode 100644 index 000000000..fe0e6a268 --- /dev/null +++ b/projects/demo-playwright/utils/wait-for-fonts.ts @@ -0,0 +1,12 @@ +import type {Page} from '@playwright/test'; +import {expect} from '@playwright/test'; + +export async function tuiWaitForFonts(page: Page): Promise { + await expect(async () => { + expect(await page.evaluate(() => (document as any).fonts.size)).toBeGreaterThan( + 0, + ); + expect(await page.evaluate(() => (document as any).fonts.ready)).toBeTruthy(); + expect(await page.evaluate(() => (document as any).fonts.status)).toBe('loaded'); + }).toPass({timeout: 30_000}); +} diff --git a/projects/demo-playwright/utils/wait-stable-state.ts b/projects/demo-playwright/utils/wait-stable-state.ts new file mode 100644 index 000000000..67392805e --- /dev/null +++ b/projects/demo-playwright/utils/wait-stable-state.ts @@ -0,0 +1,17 @@ +import type {Locator} from '@playwright/test'; + +export async function waitStableState(locator: Locator): Promise { + try { + const handle = await locator.elementHandle(); + + // https://playwright.dev/docs/actionability#stable + // element is Stable, as in not animating or completed animation + // Element is considered stable when it has maintained + // the same bounding box for at least two consecutive animation frames. + await handle?.waitForElementState('stable'); + + // https://playwright.dev/docs/actionability#visible + // Element is considered visible when it has non-empty bounding box + await handle?.waitForElementState('visible'); + } catch {} +}