From 812c40f49b7bafb0899f7f3b84239d5586ba3bf0 Mon Sep 17 00:00:00 2001 From: janithjay Date: Thu, 20 Aug 2026 10:27:48 +0530 Subject: [PATCH] Fix nuxt/nextjs E2E flakiness and align CI step naming Signed-off-by: janithjay --- .github/actions/run-e2e-suite/action.yml | 22 ++++++------ .github/workflows/pr-builder.yml | 4 +-- tests/e2e/pages/thunderid-web-sample.page.ts | 37 ++++++++++++++++---- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/.github/actions/run-e2e-suite/action.yml b/.github/actions/run-e2e-suite/action.yml index 4286041..21f4408 100644 --- a/.github/actions/run-e2e-suite/action.yml +++ b/.github/actions/run-e2e-suite/action.yml @@ -30,7 +30,7 @@ runs: version: latest run_install: false - - name: ๐Ÿ—„๏ธ Cache pnpm store + - name: ๐Ÿ—„๏ธ Cache pnpm Store uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ~/.local/share/pnpm/store @@ -42,13 +42,13 @@ runs: shell: bash run: pnpm install --frozen-lockfile - - name: ๐Ÿ”จ Build SDK packages + - name: ๐Ÿ”จ Build SDK Packages shell: bash # Sample apps consume workspace:* SDK packages โ€” this is what actually puts the PR's # changes under test. run: pnpm build - - name: ๐Ÿš€ Install, set up, and start ThunderID (latest, via npx) + - name: ๐Ÿš€ Install, Set up, and Start ThunderID (Latest, via npx) id: install shell: bash working-directory: tests/e2e @@ -86,7 +86,7 @@ runs: done echo "ERROR: ThunderID server did not become ready" && exit 1 - - name: ๐Ÿ”‘ Obtain admin token + - name: ๐Ÿ”‘ Obtain Admin Token id: admin-token shell: bash # Mirrors mint_admin_token() in tests/e2e/run-e2e.sh โ€” the same OAuth2 authorization_code + @@ -146,7 +146,7 @@ runs: echo "::add-mask::$TOKEN" echo "token=$TOKEN" >> "$GITHUB_OUTPUT" - - name: ๐Ÿ“ Import sample app OAuth2 clients + - name: ๐Ÿ“ Import Sample App OAuth2 Clients shell: bash # Mirrors import_sample_apps_config() in tests/e2e/run-e2e.sh โ€” a direct POST to /import, # done in bash instead of through thunder-id/thunderid's import-declarative-config @@ -182,7 +182,7 @@ runs: fi rm -f "$RESPONSE_FILE" - - name: ๐Ÿ”„ Restart the server with security enabled + - name: ๐Ÿ”„ Restart the Server with Security Enabled shell: bash working-directory: ${{ steps.install.outputs.dist_home }} run: | @@ -207,7 +207,7 @@ runs: done echo "ERROR: ThunderID server did not restart" && exit 1 - - name: ๐Ÿ“ Write sample app .env files + - name: ๐Ÿ“ Write Sample App .env Files shell: bash # Each app needs its own OAuth2 client wired in before it starts (see the OAuth2 clients # imported above). The apps' own `prepare-dev.cjs --flow=redirect` helper looks like the @@ -258,7 +258,7 @@ runs: "THUNDERID_SESSION_SECRET=${NUXT_SECRET}" \ "NODE_TLS_REJECT_UNAUTHORIZED=0" - - name: ๐Ÿš€ Start sample apps + - name: ๐Ÿš€ Start Sample Apps shell: bash run: | ( cd samples/browser/quickstart && pnpm exec vite --port 5173 & ) @@ -278,12 +278,12 @@ runs: fi done - - name: ๐ŸŽญ Install Playwright browsers + - name: ๐Ÿ“ฆ Install Playwright Browsers shell: bash working-directory: tests/e2e run: npx playwright install --with-deps chromium - - name: ๐Ÿงช Run E2E tests + - name: ๐ŸŽญ Run Playwright E2E Tests (Sample Apps) shell: bash working-directory: tests/e2e env: @@ -307,7 +307,7 @@ runs: NODE_TLS_REJECT_UNAUTHORIZED: "0" run: npx playwright test - - name: ๐Ÿ“ค Upload Playwright report + - name: ๐Ÿ“ค Upload Playwright Report if: ${{ !cancelled() }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml index 6c0d05a..bab94f5 100644 --- a/.github/workflows/pr-builder.yml +++ b/.github/workflows/pr-builder.yml @@ -100,7 +100,7 @@ jobs: run: pnpm --filter '!./packages/**' --filter '!./samples/**' test e2e: - name: ๐ŸŽญ E2E (sample apps) + name: ๐ŸŽญ Playwright E2E Tests runs-on: ubuntu-latest timeout-minutes: 40 # Least privilege: this job never pushes, comments, or writes to the repo โ€” only the default @@ -113,7 +113,7 @@ jobs: with: persist-credentials: false - - name: ๐ŸŽญ Run E2E suite + - name: ๐ŸŽญ Playwright E2E Tests uses: ./.github/actions/run-e2e-suite with: node-version: ${{ env.NODE_VERSION }} diff --git a/tests/e2e/pages/thunderid-web-sample.page.ts b/tests/e2e/pages/thunderid-web-sample.page.ts index 021ba76..2506e3b 100644 --- a/tests/e2e/pages/thunderid-web-sample.page.ts +++ b/tests/e2e/pages/thunderid-web-sample.page.ts @@ -14,7 +14,7 @@ * covers all four apps; only the base URL differs (see fixtures/sample-apps). */ -import {Page, expect} from '@playwright/test'; +import {Locator, Page, expect} from '@playwright/test'; import {GateLoginPage} from './gate-login.page'; import {Timeouts} from '../constants/timeouts'; @@ -59,9 +59,30 @@ export class ThunderIDWebSamplePage extends GateLoginPage { await this.verifyHomePageLoaded(); } + /** Clicks the dropdown trigger and waits for `target` (a menu item scoped to the dropdown) to + * appear, re-clicking if it doesn't. The redirect landing page is server-rendered, so the + * trigger can be visible (and Playwright-clickable) before React/Vue finishes attaching its + * click handler โ€” the click lands on plain markup and is silently lost, no error, nothing left + * to wait on. A second click after hydration catches up recovers cleanly; this has been + * observed to matter specifically for nuxt/quickstart under CI-level CPU contention, where the + * gap is wide enough to lose the first click outright rather than just render it late. */ + private async openDropdown(target: Locator): Promise { + const trigger = this.page.locator(USER_DROPDOWN_TRIGGER).first(); + for (let attempt = 1; attempt <= 3; attempt++) { + await trigger.click(); + try { + await expect(target).toBeVisible({timeout: 3000}); + return; + } catch (error) { + if (attempt === 3) throw error; + } + } + } + async logout(): Promise { - await this.page.locator(USER_DROPDOWN_TRIGGER).first().click(); - await this.page.getByRole('button', {name: 'Sign Out'}).click(); + const signOutButton = this.page.getByRole('button', {name: 'Sign Out'}); + await this.openDropdown(signOutButton); + await signOutButton.click(); await this.confirmSignOutIfPrompted(); } @@ -70,8 +91,9 @@ export class ThunderIDWebSamplePage extends GateLoginPage { * wire it to an `onClick` page-switch instead (no real navigation) โ€” so this matches on text * rather than a specific role. */ async openTokenDebug(): Promise { - await this.page.locator(USER_DROPDOWN_TRIGGER).first().click(); - await this.page.getByText('Token debug', {exact: true}).click(); + const tokenDebugItem = this.page.getByText('Token debug', {exact: true}); + await this.openDropdown(tokenDebugItem); + await tokenDebugItem.click(); } async verifyTokenDebugLoaded(): Promise { @@ -97,8 +119,9 @@ export class ThunderIDWebSamplePage extends GateLoginPage { * (BaseUserDropdown.ts:359, `onProfileClick`/`profileContent`). Nuxt inherits Vue's via its own * `UserDropdown` wrapper, which delegates to the same `@thunderid/vue` component. */ async openManageProfile(): Promise { - await this.page.locator(USER_DROPDOWN_TRIGGER).first().click(); - await this.page.getByRole('button', {name: /^(Manage Profile|Profile)$/}).click(); + const profileButton = this.page.getByRole('button', {name: /^(Manage Profile|Profile)$/}); + await this.openDropdown(profileButton); + await profileButton.click(); await expect(this.page.getByRole('dialog')).toBeVisible({timeout: Timeouts.ELEMENT_VISIBILITY}); }