Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/actions/run-e2e-suite/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 +
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand All @@ -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
Expand Down Expand Up @@ -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 & )
Expand All @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pr-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
37 changes: 30 additions & 7 deletions tests/e2e/pages/thunderid-web-sample.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<void> {
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<void> {
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();
}

Expand All @@ -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<void> {
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<void> {
Expand All @@ -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<void> {
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});
}

Expand Down
Loading