diff --git a/.changeset/wicked-doors-buy.md b/.changeset/wicked-doors-buy.md new file mode 100644 index 000000000..556379015 --- /dev/null +++ b/.changeset/wicked-doors-buy.md @@ -0,0 +1,5 @@ +--- +'@asgardeo/react': patch +--- + +Add E2E tests for @asgardeo/react SDK diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 000000000..fe8ae1d53 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,183 @@ +# E2E tests for the React SDK using the teamspace-react sample app. + +name: ๐Ÿงช E2E Tests + +on: + pull_request: + branches: [main] + paths: + - 'packages/**' + - 'samples/teamspace-react/**' + - 'e2e/**' + workflow_dispatch: + inputs: + idp_target: + description: 'IDP target (is, thunder, both)' + required: false + default: 'both' + +env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + e2e-is: + name: ๐Ÿงช E2E (WSO2 IS) + if: >- + github.event_name == 'pull_request' || + github.event.inputs.idp_target == 'is' || + github.event.inputs.idp_target == 'both' + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + node-version: [lts/*] + pnpm-version: [latest] + steps: + - name: โฌ‡๏ธ Checkout + uses: actions/checkout@v4 + + - name: ๐ŸŸข Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: ๐Ÿฅก Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ matrix.pnpm-version }} + run_install: false + + - name: ๐ŸŽˆ Get pnpm store directory + id: get-pnpm-cache-dir + run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: ๐Ÿ”† Cache pnpm modules + uses: actions/cache@v4 + with: + path: ${{ steps.get-pnpm-cache-dir.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: ๐Ÿงฉ Install Dependencies + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ—๏ธ Build SDK packages + run: pnpm build + + - name: ๐ŸŽญ Install Playwright browsers + run: pnpm e2e:install + + - name: ๐Ÿณ Start WSO2 IS + run: pnpm e2e:docker:up:is + + - name: ๐Ÿงช Run E2E redirect tests against IS + run: pnpm e2e -- --idp is --mode redirect + env: + CI: true + + - name: ๐Ÿงช Run E2E embedded tests against IS + run: pnpm e2e -- --idp is --mode embedded + env: + CI: true + + - name: ๐Ÿ“Š Upload redirect test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-is-redirect-report + path: e2e/playwright-report/ + retention-days: 14 + + - name: ๐Ÿ“Š Upload embedded test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-is-embedded-report + path: e2e/playwright-report-embedded/ + retention-days: 14 + + - name: ๐Ÿณ Stop Docker containers + if: always() + run: pnpm e2e:docker:down + + e2e-thunder: + name: ๐Ÿงช E2E (Thunder) + if: >- + github.event_name == 'pull_request' || + github.event.inputs.idp_target == 'thunder' || + github.event.inputs.idp_target == 'both' + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + node-version: [lts/*] + pnpm-version: [latest] + steps: + - name: โฌ‡๏ธ Checkout + uses: actions/checkout@v4 + + - name: ๐ŸŸข Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: ๐Ÿฅก Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ matrix.pnpm-version }} + run_install: false + + - name: ๐ŸŽˆ Get pnpm store directory + id: get-pnpm-cache-dir + run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: ๐Ÿ”† Cache pnpm modules + uses: actions/cache@v4 + with: + path: ${{ steps.get-pnpm-cache-dir.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: ๐Ÿงฉ Install Dependencies + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ—๏ธ Build SDK packages + run: pnpm build + + - name: ๐ŸŽญ Install Playwright browsers + run: pnpm e2e:install + + - name: ๐Ÿณ Start Thunder + run: pnpm e2e:docker:up:thunder + + - name: ๐Ÿงช Run E2E redirect tests against Thunder + run: pnpm e2e -- --idp thunder --mode redirect + env: + CI: true + + - name: ๐Ÿงช Run E2E embedded tests against Thunder + run: pnpm e2e -- --idp thunder --mode embedded + env: + CI: true + + - name: ๐Ÿ“Š Upload redirect test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-thunder-redirect-report + path: e2e/playwright-report/ + retention-days: 14 + + - name: ๐Ÿ“Š Upload embedded test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-thunder-embedded-report + path: e2e/playwright-report-embedded/ + retention-days: 14 + + - name: ๐Ÿณ Stop Docker containers + if: always() + run: pnpm e2e:docker:down diff --git a/.gitignore b/.gitignore index ffcb51e70..e219c4511 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,12 @@ Thumbs.db .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md +# E2E Test artifacts +e2e/.auth/ +e2e/test-results/ +e2e/playwright-report/ +test-results/ + # Experimental Samples # NOTE to Developers: Please use this samples folder for experimental code only and do not commit. samples/__experimental__/ diff --git a/README.md b/README.md index f12ee5578..248daf505 100644 --- a/README.md +++ b/README.md @@ -24,33 +24,42 @@ Follow these simple steps to get started with Asgardeo: -1. Create an account in Asgardeo ๐Ÿ‘‰ [Sign Up](https://asgardeo.io/signup?visitor_id=685a48bc57b3b5.46411343&utm_source=site&utm_medium=organic) +1. Create an account in Asgardeo ๐Ÿ‘‰ + [Sign Up](https://asgardeo.io/signup?visitor_id=685a48bc57b3b5.46411343&utm_source=site&utm_medium=organic) 2. Refer to our **Quick Start Guides** and get started in minutes. - - [React Quick Start](https://wso2.com/asgardeo/docs/quick-starts/react/) - - [Next.js Quick Start](https://wso2.com/asgardeo/docs/quick-starts/nextjs/) + +- [React Quick Start](https://wso2.com/asgardeo/docs/quick-starts/react/) +- [Next.js Quick Start](https://wso2.com/asgardeo/docs/quick-starts/nextjs/) ## Packages -| Package | Description | -| --- | --- | -| [![@asgardeo/javascript](https://img.shields.io/npm/v/@asgardeo/javascript?color=%234B32C3&label=%40asgardeo%2Fjavascript&logo=javascript)](./packages/javascript/) | Framework-agnostic JavaScript Core SDK | -| [![@asgardeo/browser](https://img.shields.io/npm/v/@asgardeo/browser?color=%234B32C3&label=%40asgardeo%2Fbrowser&logo=firefox)](./packages/browser/) | Browser-based JavaScript SDK | -| [![@asgardeo/nextjs](https://img.shields.io/npm/v/@asgardeo/nextjs?color=%23000000&label=%40asgardeo%2Fnext&logo=next.js)](./packages/next/) | Next.js SDK for building applications with Asgardeo | -| [![@asgardeo/node](https://img.shields.io/npm/v/@asgardeo/node?color=%23339933&label=%40asgardeo%2Fnode&logo=node.js)](./packages/node/) | Node.js SDK for server-side integration | -| [![@asgardeo/express](https://img.shields.io/npm/v/@asgardeo/express?color=%23339933&label=%40asgardeo%2Fexpress&logo=express)](./packages/express/) | Express.js SDK for server-side integration | -| [![@asgardeo/nuxt](https://img.shields.io/npm/v/@asgardeo/nuxt?color=%2300DC82&label=%40asgardeo%2Fnuxt&logo=nuxt)](./packages/nuxt/) | Nuxt.js SDK for building applications with Asgardeo | -| [![@asgardeo/react](https://img.shields.io/npm/v/@asgardeo/react?color=%2361DAFB&label=%40asgardeo%2Freact&logo=react)](./packages/react/) | React SDK for building applications with Asgardeo | -| [![@asgardeo/react-router](https://img.shields.io/npm/v/@asgardeo/react-router?color=%2361DAFB&label=%40asgardeo%2Freact-router&logo=react-router)](./packages/react-router/) | Supplementary React Router bindings | -| [![@asgardeo/vue](https://img.shields.io/npm/v/@asgardeo/vue?color=%234FC08D&label=%40asgardeo%2Fvue&logo=vue.js)](./packages/vue/) | Vue.js SDK for building applications with Asgardeo | +| Package | Description | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| [![@asgardeo/javascript](https://img.shields.io/npm/v/@asgardeo/javascript?color=%234B32C3&label=%40asgardeo%2Fjavascript&logo=javascript)](./packages/javascript/) | Framework-agnostic JavaScript Core SDK | +| [![@asgardeo/browser](https://img.shields.io/npm/v/@asgardeo/browser?color=%234B32C3&label=%40asgardeo%2Fbrowser&logo=firefox)](./packages/browser/) | Browser-based JavaScript SDK | +| [![@asgardeo/nextjs](https://img.shields.io/npm/v/@asgardeo/nextjs?color=%23000000&label=%40asgardeo%2Fnext&logo=next.js)](./packages/next/) | Next.js SDK for building applications with Asgardeo | +| [![@asgardeo/node](https://img.shields.io/npm/v/@asgardeo/node?color=%23339933&label=%40asgardeo%2Fnode&logo=node.js)](./packages/node/) | Node.js SDK for server-side integration | +| [![@asgardeo/express](https://img.shields.io/npm/v/@asgardeo/express?color=%23339933&label=%40asgardeo%2Fexpress&logo=express)](./packages/express/) | Express.js SDK for server-side integration | +| [![@asgardeo/nuxt](https://img.shields.io/npm/v/@asgardeo/nuxt?color=%2300DC82&label=%40asgardeo%2Fnuxt&logo=nuxt)](./packages/nuxt/) | Nuxt.js SDK for building applications with Asgardeo | +| [![@asgardeo/react](https://img.shields.io/npm/v/@asgardeo/react?color=%2361DAFB&label=%40asgardeo%2Freact&logo=react)](./packages/react/) | React SDK for building applications with Asgardeo | +| [![@asgardeo/react-router](https://img.shields.io/npm/v/@asgardeo/react-router?color=%2361DAFB&label=%40asgardeo%2Freact-router&logo=react-router)](./packages/react-router/) | Supplementary React Router bindings | +| [![@asgardeo/vue](https://img.shields.io/npm/v/@asgardeo/vue?color=%234FC08D&label=%40asgardeo%2Fvue&logo=vue.js)](./packages/vue/) | Vue.js SDK for building applications with Asgardeo | + +## E2E Tests + +Run End-to-end tests using Playwright. See the [E2E Test Guide](./e2e/README.md) for setup instructions and usage. ## Contribute -Please read [Contributing Guide](CONTRIBUTING.md) for details on how to contribute to Asgardeo JavaScript SDKs. Refer to [General Contribution Guidelines](http://wso2.github.io/) for details on our code of conduct, and the process for submitting pull requests to us. +Please read [Contributing Guide](CONTRIBUTING.md) for details on how to contribute to Asgardeo JavaScript SDKs. Refer to +[General Contribution Guidelines](http://wso2.github.io/) for details on our code of conduct, and the process for +submitting pull requests to us. ### Contributors โค๏ธ -Hats off to all the people who have contributed to this project, including those who created issues and participated in discussions. ๐Ÿ™Œ +Hats off to all the people who have contributed to this project, including those who created issues and participated in +discussions. ๐Ÿ™Œ @@ -58,9 +67,12 @@ Hats off to all the people who have contributed to this project, including those ### Reporting issues -We encourage you to report issues, improvements, and feature requests creating [Github Issues](https://github.com/asgardeo/javascript/issues). +We encourage you to report issues, improvements, and feature requests creating +[Github Issues](https://github.com/asgardeo/javascript/issues). -**Important**: Please be advised that security issues MUST be reported to security@wso2.com, not as GitHub issues, in order to reach the proper audience. We strongly advise following the WSO2 Security Vulnerability Reporting Guidelines when reporting the security issues. +**Important**: Please be advised that security issues MUST be reported to +security@wso2.com, not as GitHub issues, in order to reach the proper audience. +We strongly advise following the WSO2 Security Vulnerability Reporting Guidelines when reporting the security issues. ## License diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000..15ba1fe5d --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,157 @@ +# E2E Tests + +End-to-end tests for the `@asgardeo/react` SDK using [Playwright](https://playwright.dev). Tests run against two +identity providers (IDPs) and cover both redirect-based OAuth and embedded `` component flows. + +## Prerequisites + +- **Docker** โ€” used to run the IDP containers +- **pnpm** โ€” workspace package manager +- **Playwright Chromium** โ€” install with `pnpm e2e:install` + +## Quick Start + +```bash +# 1. Install Playwright browsers +pnpm e2e:install + +# 2. Start IDP containers +pnpm e2e:docker:up + +# 3. Wait for containers to be healthy, then run tests +pnpm e2e -- --idp is +``` + +## Running Tests + +All e2e tests are run via a single script: + +```bash +pnpm e2e -- [--idp is|thunder|all] [--mode redirect|embedded|all] [--headed] +``` + +| Flag | Values | Default | Description | +| ---------- | ----------------------------- | ---------- | -------------------------- | +| `--idp` | `is`, `thunder`, `all` | `is` | Which IDP to test against | +| `--mode` | `redirect`, `embedded`, `all` | `redirect` | Sign-in mode to test | +| `--headed` | _(flag)_ | off | Run in headed browser mode | + +### Examples + +```bash +# IS redirect tests (default) +pnpm e2e + +# Thunder embedded tests +pnpm e2e -- --idp thunder --mode embedded + +# All tests, all IDPs, headed +pnpm e2e -- --idp all --mode all --headed + +# IS both modes +pnpm e2e -- --idp is --mode all +``` + +### Docker Commands + +```bash +pnpm e2e:docker:up # Start all IDP containers +pnpm e2e:docker:down # Stop and remove all containers + volumes +pnpm e2e:docker:up:is # Start only WSO2 IS +pnpm e2e:docker:up:thunder # Start only Thunder +``` + +## Sign-In Modes + +The sample app (`teamspace-react`) supports two sign-in modes, controlled by the presence of the `signInUrl` prop in +`AsgardeoProvider`: + +- **Redirect** โ€” No `signInUrl` set. `SignInPage` calls `signIn()` which redirects the browser to the IDP's login page. + After authentication, the IDP redirects back with an authorization code. +- **Embedded** โ€” `signInUrl` is set (via `VITE_ASGARDEO_SIGN_IN_URL`). `SignInPage` renders the SDK's `` + component inline, which communicates directly with the IDP without leaving the app. + +The e2e launch script (`e2e/setup/launch-dev-server.ts`) writes the `.env` file before starting Vite. For embedded mode, +it includes `VITE_ASGARDEO_SIGN_IN_URL`; for redirect mode, it omits it. + +## Project Structure + +``` +e2e/ +โ”œโ”€โ”€ docker-compose.yml # IDP container definitions +โ”œโ”€โ”€ playwright.redirect.config.ts # Playwright config for redirect tests +โ”œโ”€โ”€ playwright.embedded.config.ts # Playwright config for embedded tests +โ”œโ”€โ”€ scripts/ +โ”‚ โ””โ”€โ”€ run-e2e.sh # Test runner script +โ”œโ”€โ”€ setup/ +โ”‚ โ”œโ”€โ”€ launch-dev-server.ts # IDP setup + .env writer + Vite launcher +โ”‚ โ”œโ”€โ”€ constants.ts # Shared constants (sample app URLs, test user) +โ”‚ โ”œโ”€โ”€ global-teardown.ts # Cleanup after test run +โ”‚ โ”œโ”€โ”€ wait-for-idp.ts # Health-check poller +โ”‚ โ”œโ”€โ”€ http-utils.ts # HTTP helpers for setup APIs +โ”‚ โ”œโ”€โ”€ is/ +โ”‚ โ”‚ โ”œโ”€โ”€ constants.ts # IS-specific config +โ”‚ โ”‚ โ”œโ”€โ”€ app-registration.ts # DCR + Application Management API +โ”‚ โ”‚ โ””โ”€โ”€ user-provisioning.ts # SCIM2 test user creation +โ”‚ โ””โ”€โ”€ thunder/ +โ”‚ โ”œโ”€โ”€ constants.ts # Thunder-specific config +โ”‚ โ”œโ”€โ”€ app-registration.ts # App client ID + redirect URI patching +โ”‚ โ””โ”€โ”€ user-provisioning.ts # Thunder test user creation +โ”œโ”€โ”€ fixtures/ +โ”‚ โ””โ”€โ”€ base.fixture.ts # Shared Playwright test fixture +โ”œโ”€โ”€ helpers/ +โ”‚ โ”œโ”€โ”€ auth-helpers.ts # IDP-agnostic sign-in/sign-out helpers +โ”‚ โ”œโ”€โ”€ selectors.ts # Shared UI selectors (SDK components, dashboard) +โ”‚ โ”œโ”€โ”€ is/ +โ”‚ โ”‚ โ”œโ”€โ”€ auth-helpers.ts # IS login page interaction +โ”‚ โ”‚ โ””โ”€โ”€ selectors.ts # IS-specific selectors +โ”‚ โ””โ”€โ”€ thunder/ +โ”‚ โ”œโ”€โ”€ auth-helpers.ts # Thunder Gate login page interaction +โ”‚ โ””โ”€โ”€ selectors.ts # Thunder-specific selectors +โ”œโ”€โ”€ tests/ +โ”‚ โ”œโ”€โ”€ is/ +โ”‚ โ”‚ โ”œโ”€โ”€ redirect/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sign-in.spec.ts # IS redirect sign-in tests +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sign-out.spec.ts # IS sign-out tests +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ user-profile.spec.ts +โ”‚ โ”‚ โ””โ”€โ”€ embedded/ +โ”‚ โ”‚ โ””โ”€โ”€ sign-in.spec.ts # IS embedded sign-in tests +โ”‚ โ””โ”€โ”€ thunder/ +โ”‚ โ”œโ”€โ”€ redirect/ +โ”‚ โ”‚ โ”œโ”€โ”€ sign-in.spec.ts # Thunder redirect sign-in tests +โ”‚ โ”‚ โ””โ”€โ”€ user-profile.spec.ts +โ”‚ โ””โ”€โ”€ embedded/ +โ”‚ โ””โ”€โ”€ sign-in.spec.ts # Thunder embedded sign-in tests +โ”œโ”€โ”€ thunder-bootstrap/ +โ”‚ โ””โ”€โ”€ 02-sample-resources.sh # Thunder bootstrap: registers sample app +โ””โ”€โ”€ thunder-config/ + โ””โ”€โ”€ deployment.yaml # Thunder server configuration +``` + +## Test Coverage + +| Test | IS | Thunder | +| ----------------------------------------- | --- | ------- | +| Redirect to IDP login page | Yes | Yes | +| Sign in with valid credentials (redirect) | Yes | Yes | +| Redirect unauthenticated users to IDP | Yes | Yes | +| Sign out and redirect to landing page | Yes | โ€” | +| Block protected routes after sign out | Yes | โ€” | +| Render embedded `` component | Yes | Yes | +| Sign in via embedded component | Yes | Yes | +| Display user profile | Yes | Yes | +| Navigate back from profile | Yes | Yes | + +> Thunder does not support OIDC logout, so sign-out tests are IS-only. + +## How It Works + +1. **`pnpm e2e`** calls `e2e/scripts/run-e2e.sh` which sets `IDP_TARGET` and picks the right Playwright config. +2. Playwright's `webServer` runs `e2e/setup/launch-dev-server.ts` which: + - Waits for the IDP container to be healthy + - Registers an OAuth app via the IDP's management API + - Provisions a test user + - Writes `.env` to the sample app directory (with or without `VITE_ASGARDEO_SIGN_IN_URL` based on mode) + - Starts the Vite dev server +3. Playwright runs the test specs against the sample app. +4. Global teardown cleans up. diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 000000000..9e1356873 --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,82 @@ +services: + # ============================================================================ + # WSO2 Identity Server + # ============================================================================ + wso2is: + image: wso2/wso2is:latest + container_name: asgardeo-e2e-wso2is + ports: + - "9443:9443" + - "9763:9763" + environment: + - JAVA_OPTS=-Xms512m -Xmx1024m + healthcheck: + test: ["CMD", "curl", "-k", "-f", "https://localhost:9443/carbon/admin/login.jsp"] + interval: 15s + timeout: 10s + retries: 20 + start_period: 90s + networks: + - e2e-network + + # ============================================================================ + # Asgardeo Thunder (3-stage setup: db-init -> setup -> server) + # ============================================================================ + + # Stage 1: Initialize the database from the image + thunder-db-init: + image: ghcr.io/asgardeo/thunder:latest + container_name: asgardeo-e2e-thunder-db-init + command: sh -c "cp -r /opt/thunder/repository/database/* /data/" + volumes: + - thunder-db:/data + restart: "no" + networks: + - e2e-network + + # Stage 2: Run setup (creates admin user, default app, flows, etc.) + # Mounts custom 02-sample-resources.sh to register the app with + # redirect_uris pointing to https://localhost:5173 (Vite dev server). + thunder-setup: + image: ghcr.io/asgardeo/thunder:latest + container_name: asgardeo-e2e-thunder-setup + command: ./setup.sh + volumes: + - thunder-db:/opt/thunder/repository/database + - ./thunder-bootstrap/02-sample-resources.sh:/opt/thunder/bootstrap/02-sample-resources.sh:ro + - ./thunder-config/deployment.yaml:/opt/thunder/repository/conf/deployment.yaml:ro + depends_on: + thunder-db-init: + condition: service_completed_successfully + restart: "no" + networks: + - e2e-network + + # Stage 3: Run the Thunder server + thunder: + image: ghcr.io/asgardeo/thunder:latest + container_name: asgardeo-e2e-thunder + depends_on: + thunder-setup: + condition: service_completed_successfully + ports: + - "8090:8090" + volumes: + - thunder-db:/opt/thunder/repository/database + - ./thunder-config/deployment.yaml:/opt/thunder/repository/conf/deployment.yaml:ro + healthcheck: + test: ["CMD", "curl", "-k", "-f", "https://localhost:8090/health/readiness"] + interval: 10s + timeout: 5s + retries: 15 + start_period: 30s + networks: + - e2e-network + +volumes: + thunder-db: + driver: local + +networks: + e2e-network: + driver: bridge diff --git a/e2e/fixtures/base.fixture.ts b/e2e/fixtures/base.fixture.ts new file mode 100644 index 000000000..9afad79e8 --- /dev/null +++ b/e2e/fixtures/base.fixture.ts @@ -0,0 +1,22 @@ +/** + * Base Playwright fixture that extends the default test with shared test data. + */ + +import {test as base} from '@playwright/test'; +import {TEST_USER, SAMPLE_APP} from '../setup/constants'; + +export type E2EFixtures = { + testUser: typeof TEST_USER; + sampleApp: typeof SAMPLE_APP; +}; + +export const test = base.extend({ + testUser: async ({}, use) => { + await use(TEST_USER); + }, + sampleApp: async ({}, use) => { + await use(SAMPLE_APP); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/e2e/helpers/auth-helpers.ts b/e2e/helpers/auth-helpers.ts new file mode 100644 index 000000000..fb11fa9f9 --- /dev/null +++ b/e2e/helpers/auth-helpers.ts @@ -0,0 +1,98 @@ +/** + * Authentication helpers for e2e tests. + * + * This module provides IDP-agnostic public APIs that delegate to + * IDP-specific implementations in helpers/is/ and helpers/thunder/. + */ + +import {type Page} from '@playwright/test'; +import {TEST_USER, getIdpTarget} from '../setup/constants'; +import {THUNDER_CONFIG} from '../setup/thunder/constants'; +import {SELECTORS} from './selectors'; +import {performIsSignIn, handleIsLogoutConsent} from './is/auth-helpers'; +import {performThunderSignIn} from './thunder/auth-helpers'; + +/** + * Returns the credentials to use for sign-in based on the IDP target. + * + * - IS: Uses the provisioned test user (e2e-test-user) + * - Thunder: Uses the pre-created admin user (admin/admin) + */ +function getSignInCredentials(): {username: string; password: string} { + const idpTarget = getIdpTarget(); + + if (idpTarget === 'thunder') { + return { + username: THUNDER_CONFIG.adminUsername, + password: THUNDER_CONFIG.adminPassword, + }; + } + + return { + username: TEST_USER.username, + password: TEST_USER.password, + }; +} + +/** + * Performs the full sign-in flow via redirect-based OAuth2. + * Delegates to the appropriate IDP login page based on the IDP target: + * - IS: WSO2 IS authentication endpoint + * - Thunder: Thunder's Gate login page + */ +export async function performSignIn(page: Page): Promise { + const idpTarget = getIdpTarget(); + const credentials = getSignInCredentials(); + + if (idpTarget === 'thunder') { + await performThunderSignIn(page, credentials); + } else { + await performIsSignIn(page, credentials); + } +} + +/** + * Performs sign-in via the embedded component. + * + * 1. Navigate to /signin โ†’ renders inline (when VITE_ASGARDEO_SIGN_IN_MODE != 'redirect') + * 2. SDK calls signIn({response_mode: 'direct'}) โ†’ IDP returns form fields + * 3. Fill username/password in the SDK-rendered form + * 4. Submit โ†’ SDK posts credentials directly to IDP + * 5. On success, SDK redirects to /dashboard + */ +export async function performEmbeddedSignIn(page: Page): Promise { + const credentials = getSignInCredentials(); + + await page.goto('/signin'); + + // Wait for the component to load and render form fields from the IDP + await page.waitForSelector(SELECTORS.embeddedSignIn.container, {timeout: 30_000}); + await page.waitForSelector(SELECTORS.embeddedSignIn.usernameInput, {timeout: 15_000}); + + await page.locator(SELECTORS.embeddedSignIn.usernameInput).fill(credentials.username); + await page.locator(SELECTORS.embeddedSignIn.passwordInput).fill(credentials.password); + await page.locator(SELECTORS.embeddedSignIn.submitButton).click(); + + // After successful embedded sign-in, SDK sets session and redirects to /dashboard + await page.waitForURL('**/dashboard**', {timeout: 30_000}); + await page.waitForSelector(SELECTORS.dashboard.welcomeHeading, {timeout: 15_000}); +} + +/** + * Performs sign-out by clicking the user dropdown โ†’ "Sign out". + * For IS, also handles the logout consent page ("Are you sure you want to logout?"). + */ +export async function performSignOut(page: Page): Promise { + await page.locator(SELECTORS.header.userDropdownTrigger).click(); + await page.getByText(SELECTORS.header.signOutText).click(); + + const idpTarget = getIdpTarget(); + + if (idpTarget === 'is') { + // IS shows a logout consent page โ€” click "Yes" to confirm + await handleIsLogoutConsent(page); + } + + // Wait for redirect back to the app's landing page (may include ?state=sign_out_success) + await page.waitForURL(/localhost:\d+\/(\?.*)?$/, {timeout: 15_000}); +} diff --git a/e2e/helpers/is/auth-helpers.ts b/e2e/helpers/is/auth-helpers.ts new file mode 100644 index 000000000..e9be2e692 --- /dev/null +++ b/e2e/helpers/is/auth-helpers.ts @@ -0,0 +1,43 @@ +/** + * IS-specific authentication helpers for e2e tests. + */ + +import {type Page} from '@playwright/test'; +import {SAMPLE_APP} from '../../setup/constants'; +import {SELECTORS} from '../selectors'; +import {IS_SELECTORS} from './selectors'; + +/** + * Performs sign-in via the redirect-based flow for WSO2 IS. + * + * 1. Navigate to /signin โ†’ app calls signIn() โ†’ browser redirects to IS authentication endpoint + * 2. Fill username/password on the IS login page + * 3. Submit โ†’ IS redirects back to /callback with auth code + * 4. SDK exchanges code for tokens โ†’ CallbackPage navigates to /dashboard + */ +export async function performIsSignIn(page: Page, credentials: {username: string; password: string}): Promise { + await page.goto(SAMPLE_APP.signInPath); + + // Wait for redirect to IS's authentication endpoint + await page.waitForURL('**/authenticationendpoint/**', {timeout: 30_000}); + + // Wait for the IS login form to render + await page.waitForSelector(IS_SELECTORS.login.usernameInput, {timeout: 15_000}); + + await page.locator(IS_SELECTORS.login.usernameInput).fill(credentials.username); + await page.locator(IS_SELECTORS.login.passwordInput).fill(credentials.password); + await page.locator(IS_SELECTORS.login.signInButton).click(); + + // After successful auth, IS redirects to /callback, SDK exchanges code for tokens, + // then CallbackPage navigates to /dashboard. + await page.waitForURL('**/dashboard**', {timeout: 30_000}); + await page.waitForSelector(SELECTORS.dashboard.welcomeHeading, {timeout: 15_000}); +} + +/** + * Handle the IS logout consent page ("Are you sure you want to logout?"). + */ +export async function handleIsLogoutConsent(page: Page): Promise { + await page.waitForURL('**/oauth2_logout_consent**', {timeout: 15_000}); + await page.getByText(IS_SELECTORS.logout.consentYesButton, {exact: true}).click(); +} diff --git a/e2e/helpers/is/selectors.ts b/e2e/helpers/is/selectors.ts new file mode 100644 index 000000000..a3a5e3c4c --- /dev/null +++ b/e2e/helpers/is/selectors.ts @@ -0,0 +1,16 @@ +/** + * Selectors for WSO2 IS login and logout pages. + */ + +export const IS_SELECTORS = { + /** Selectors for WSO2 IS authentication endpoint (redirect-based flow) */ + login: { + usernameInput: '#usernameUserInput', + passwordInput: '#password', + signInButton: '[data-testid="login-page-continue-login-button"]', + }, + /** Selectors for WSO2 IS logout consent page */ + logout: { + consentYesButton: 'Yes', + }, +} as const; diff --git a/e2e/helpers/selectors.ts b/e2e/helpers/selectors.ts new file mode 100644 index 000000000..811d88651 --- /dev/null +++ b/e2e/helpers/selectors.ts @@ -0,0 +1,27 @@ +/** + * Shared selectors for SDK components used in e2e tests. + * + * IDP-specific selectors live in helpers/is/selectors.ts and helpers/thunder/selectors.ts. + */ + +export const SELECTORS = { + /** Selectors for the SDK's embedded component */ + embeddedSignIn: { + container: '[data-testid="asgardeo-signin"]', + usernameInput: '[data-testid="asgardeo-signin-username"]', + passwordInput: '[data-testid="asgardeo-signin-password"]', + submitButton: '[data-testid="asgardeo-signin-submit"]', + }, + dashboard: { + welcomeHeading: 'h1', + }, + profile: { + heading: 'h1', + backToDashboard: 'Back to dashboard', + }, + header: { + userDropdownTrigger: '[data-testid="asgardeo-user-dropdown-trigger"]', + signInText: 'Sign in', + signOutText: 'Sign out', + }, +} as const; diff --git a/e2e/helpers/thunder/auth-helpers.ts b/e2e/helpers/thunder/auth-helpers.ts new file mode 100644 index 000000000..0bfd91a84 --- /dev/null +++ b/e2e/helpers/thunder/auth-helpers.ts @@ -0,0 +1,38 @@ +/** + * Thunder-specific authentication helpers for e2e tests. + */ + +import {type Page} from '@playwright/test'; +import {SAMPLE_APP} from '../../setup/constants'; +import {SELECTORS} from '../selectors'; +import {THUNDER_SELECTORS} from './selectors'; + +/** + * Performs sign-in via the redirect-based flow for Thunder. + * + * 1. Navigate to /signin โ†’ app calls signIn() โ†’ browser redirects to Thunder's Gate login page + * 2. Fill username/password on the Gate page + * 3. Submit โ†’ Thunder redirects back to /callback with auth code + * 4. SDK exchanges code for tokens โ†’ CallbackPage navigates to /dashboard + */ +export async function performThunderSignIn( + page: Page, + credentials: {username: string; password: string}, +): Promise { + await page.goto(SAMPLE_APP.signInPath); + + // Wait for redirect to Thunder's Gate login page + await page.waitForURL('**/gate/signin**', {timeout: 30_000}); + + // Wait for the Gate login form to render (it's a React SPA) + await page.waitForSelector(THUNDER_SELECTORS.gate.usernameInput, {timeout: 15_000}); + + await page.locator(THUNDER_SELECTORS.gate.usernameInput).fill(credentials.username); + await page.locator(THUNDER_SELECTORS.gate.passwordInput).fill(credentials.password); + await page.getByRole('button', {name: THUNDER_SELECTORS.gate.submitButtonText}).click(); + + // After successful auth, Thunder redirects to /callback, SDK exchanges code for tokens, + // then CallbackPage navigates to /dashboard. + await page.waitForURL('**/dashboard**', {timeout: 30_000}); + await page.waitForSelector(SELECTORS.dashboard.welcomeHeading, {timeout: 15_000}); +} diff --git a/e2e/helpers/thunder/selectors.ts b/e2e/helpers/thunder/selectors.ts new file mode 100644 index 000000000..66ce1b016 --- /dev/null +++ b/e2e/helpers/thunder/selectors.ts @@ -0,0 +1,12 @@ +/** + * Selectors for Thunder's Gate login page. + */ + +export const THUNDER_SELECTORS = { + /** Selectors for Thunder's Gate login page (redirect-based flow) */ + gate: { + usernameInput: '#username', + passwordInput: '#password', + submitButtonText: 'Sign In', + }, +} as const; diff --git a/e2e/playwright-report-embedded/index.html b/e2e/playwright-report-embedded/index.html new file mode 100644 index 000000000..d0a874934 --- /dev/null +++ b/e2e/playwright-report-embedded/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/e2e/playwright.embedded.config.ts b/e2e/playwright.embedded.config.ts new file mode 100644 index 000000000..bf4e2d970 --- /dev/null +++ b/e2e/playwright.embedded.config.ts @@ -0,0 +1,49 @@ +import {defineConfig, devices} from '@playwright/test'; +import path from 'path'; + +const SAMPLE_APP_URL = process.env.SAMPLE_APP_URL ?? 'https://localhost:5173'; +const IDP_TARGET = process.env.IDP_TARGET ?? 'is'; + +export default defineConfig({ + testDir: `./tests/${IDP_TARGET}/embedded`, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [['html', {outputFolder: './playwright-report-embedded'}], ['list']], + + // Note: globalSetup is NOT used. IDP setup + .env writing happens in the + // webServer launch script to ensure .env is written BEFORE Vite starts. + globalTeardown: path.resolve(__dirname, 'setup/global-teardown.ts'), + + use: { + baseURL: SAMPLE_APP_URL, + ignoreHTTPSErrors: true, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + actionTimeout: 15_000, + navigationTimeout: 30_000, + }, + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + launchOptions: { + args: ['--ignore-certificate-errors'], + }, + }, + }, + ], + + webServer: { + command: 'SIGN_IN_MODE=embedded npx tsx e2e/setup/launch-dev-server.ts', + url: SAMPLE_APP_URL, + reuseExistingServer: !process.env.CI, + timeout: 300_000, + ignoreHTTPSErrors: true, + cwd: path.resolve(__dirname, '..'), + }, +}); diff --git a/e2e/playwright.redirect.config.ts b/e2e/playwright.redirect.config.ts new file mode 100644 index 000000000..561d341d4 --- /dev/null +++ b/e2e/playwright.redirect.config.ts @@ -0,0 +1,49 @@ +import {defineConfig, devices} from '@playwright/test'; +import path from 'path'; + +const SAMPLE_APP_URL = process.env.SAMPLE_APP_URL ?? 'https://localhost:5173'; +const IDP_TARGET = process.env.IDP_TARGET ?? 'is'; + +export default defineConfig({ + testDir: `./tests/${IDP_TARGET}/redirect`, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [['html', {outputFolder: './playwright-report'}], ['list']], + + // Note: globalSetup is NOT used. IDP setup + .env writing happens in the + // webServer launch script to ensure .env is written BEFORE Vite starts. + globalTeardown: path.resolve(__dirname, 'setup/global-teardown.ts'), + + use: { + baseURL: SAMPLE_APP_URL, + ignoreHTTPSErrors: true, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + actionTimeout: 15_000, + navigationTimeout: 30_000, + }, + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + launchOptions: { + args: ['--ignore-certificate-errors'], + }, + }, + }, + ], + + webServer: { + command: 'npx tsx e2e/setup/launch-dev-server.ts', + url: SAMPLE_APP_URL, + reuseExistingServer: !process.env.CI, + timeout: 300_000, + ignoreHTTPSErrors: true, + cwd: path.resolve(__dirname, '..'), + }, +}); diff --git a/e2e/scripts/run-e2e.sh b/e2e/scripts/run-e2e.sh new file mode 100755 index 000000000..1b49b14b4 --- /dev/null +++ b/e2e/scripts/run-e2e.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Usage: ./e2e/scripts/run-e2e.sh [--idp is|thunder] [--mode redirect|embedded|all] [--headed] +# +# Defaults: --idp is --mode redirect + +set -euo pipefail + +IDP="is" +MODE="redirect" +EXTRA_ARGS="" + +while [[ $# -gt 0 ]]; do + case $1 in + --idp) IDP="$2"; shift 2 ;; + --mode) MODE="$2"; shift 2 ;; + --headed) EXTRA_ARGS="--headed"; shift ;; + *) EXTRA_ARGS="$EXTRA_ARGS $1"; shift ;; + esac +done + +run_suite() { + local idp=$1 + local mode=$2 + echo "" + echo "=== Running e2e: IDP=$idp MODE=$mode ===" + echo "" + IDP_TARGET="$idp" npx playwright test --config "e2e/playwright.${mode}.config.ts" $EXTRA_ARGS +} + +if [[ "$IDP" == "all" && "$MODE" == "all" ]]; then + for idp in is thunder; do + for mode in redirect embedded; do + run_suite "$idp" "$mode" + done + done +elif [[ "$IDP" == "all" ]]; then + for idp in is thunder; do + run_suite "$idp" "$MODE" + done +elif [[ "$MODE" == "all" ]]; then + for mode in redirect embedded; do + run_suite "$IDP" "$mode" + done +else + run_suite "$IDP" "$MODE" +fi diff --git a/e2e/setup/constants.ts b/e2e/setup/constants.ts new file mode 100644 index 000000000..534de7687 --- /dev/null +++ b/e2e/setup/constants.ts @@ -0,0 +1,33 @@ +/** + * Shared e2e test configuration constants. + * + * IDP-specific configuration lives in setup/is/constants.ts and setup/thunder/constants.ts. + */ + +export const SAMPLE_APP = { + url: process.env.SAMPLE_APP_URL ?? 'https://localhost:5173', + afterSignInPath: '/dashboard', + afterSignOutPath: '/', + signInPath: '/signin', + signUpPath: '/signup', +} as const; + +export const TEST_USER = { + username: 'e2e-test-user', + password: 'E2e@Test1234', + email: 'e2e-test-user@test.local', + firstName: 'E2E', + lastName: 'TestUser', +} as const; + +export type IdpTarget = 'is' | 'thunder'; + +export function getIdpTarget(): IdpTarget { + const target = process.env.IDP_TARGET ?? 'is'; + + if (target !== 'is' && target !== 'thunder') { + throw new Error(`Invalid IDP_TARGET: "${target}". Must be "is" or "thunder".`); + } + + return target; +} diff --git a/e2e/setup/global-teardown.ts b/e2e/setup/global-teardown.ts new file mode 100644 index 000000000..5ec3a63b2 --- /dev/null +++ b/e2e/setup/global-teardown.ts @@ -0,0 +1,9 @@ +/** + * Playwright global teardown. + * + * Docker container lifecycle is managed externally via `pnpm e2e:docker:down`. + */ + +export default async function globalTeardown(): Promise { + console.log('[E2E Global Teardown] Complete.'); +} diff --git a/e2e/setup/http-utils.ts b/e2e/setup/http-utils.ts new file mode 100644 index 000000000..845fe229e --- /dev/null +++ b/e2e/setup/http-utils.ts @@ -0,0 +1,13 @@ +/** + * Shared HTTP utilities for e2e setup scripts. + */ + +import {Agent} from 'undici'; + +export const insecureAgent = new Agent({ + connect: {rejectUnauthorized: false}, +}); + +export function basicAuth(username: string, password: string): string { + return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; +} diff --git a/e2e/setup/is/app-registration.ts b/e2e/setup/is/app-registration.ts new file mode 100644 index 000000000..cb2bff02c --- /dev/null +++ b/e2e/setup/is/app-registration.ts @@ -0,0 +1,234 @@ +/** + * OAuth2/OIDC application registration on WSO2 Identity Server. + */ + +import {SAMPLE_APP} from '../constants'; +import {basicAuth, insecureAgent} from '../http-utils'; +import {IS_CONFIG} from './constants'; + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Register an OAuth2/OIDC application on WSO2 Identity Server via the DCR v1.1 API, + * then configure CORS allowed origins and PKCE via the Application Management API. + * + * DCR alone does not set allowed origins or enable app-native auth, so we need a multi-step process: + * 1. POST /api/identity/oauth2/dcr/v1.1/register โ†’ creates the app, returns clientId + * 2. GET /api/server/v1/applications โ†’ find the app ID by name + * 3. PUT /api/server/v1/applications/{id}/inbound-protocols/oidc โ†’ set allowedOrigins + PKCE + * 4. PATCH /api/server/v1/applications/{id} โ†’ enable app-native authentication API + */ +export async function registerIsApp(): Promise<{clientId: string; clientSecret: string}> { + const {baseUrl, dcrEndpoint, adminUsername, adminPassword} = IS_CONFIG; + const authHeader = basicAuth(adminUsername, adminPassword); + + // Step 1: Register via DCR + const dcrUrl = `${baseUrl}${dcrEndpoint}`; + + console.log(`[E2E] Registering OAuth app on IS via DCR: ${dcrUrl}`); + + const dcrBody = { + client_name: 'asgardeo-e2e-test-app', + grant_types: ['authorization_code', 'refresh_token'], + redirect_uris: [`${SAMPLE_APP.url}${SAMPLE_APP.afterSignInPath}`], + token_type_extension: 'JWT', + application_type: 'web', + ext_public_client: true, + ext_pkce_mandatory: true, + }; + + const dcrResponse = await fetch(dcrUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: authHeader, + }, + body: JSON.stringify(dcrBody), + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + let clientId: string; + let clientSecret: string; + + if (dcrResponse.ok) { + const dcrData = (await dcrResponse.json()) as {client_id: string; client_secret: string}; + + clientId = dcrData.client_id; + clientSecret = dcrData.client_secret; + console.log(`[E2E] IS app registered: clientId=${clientId}`); + } else { + const text = await dcrResponse.text(); + + // Handle "already exists" โ€” look up the existing app + if (dcrResponse.status === 400 && text.includes('already exist')) { + console.log('[E2E] IS app already exists, looking up existing app...'); + const existing = await lookupExistingIsApp(baseUrl, authHeader); + + clientId = existing.clientId; + clientSecret = existing.clientSecret; + } else { + throw new Error(`[E2E] IS DCR failed (${dcrResponse.status}): ${text}`); + } + } + + // Step 2: Find the application ID via Application Management API + const appsUrl = `${baseUrl}/api/server/v1/applications?limit=30`; + const appsResponse = await fetch(appsUrl, { + method: 'GET', + headers: {Authorization: authHeader}, + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + if (!appsResponse.ok) { + console.warn(`[E2E] Could not list IS applications (${appsResponse.status}), skipping CORS config`); + + return {clientId, clientSecret}; + } + + const appsData = (await appsResponse.json()) as { + applications: Array<{id: string; name: string}>; + }; + const app = appsData.applications.find((a) => a.name === 'asgardeo-e2e-test-app'); + + if (!app) { + console.warn('[E2E] Could not find app by name, skipping CORS config'); + + return {clientId, clientSecret}; + } + + console.log(`[E2E] IS app ID: ${app.id}, configuring CORS origins and PKCE...`); + + // Step 3: GET current OIDC config, then PUT with allowedOrigins + PKCE + const oidcUrl = `${baseUrl}/api/server/v1/applications/${app.id}/inbound-protocols/oidc`; + const oidcGetResponse = await fetch(oidcUrl, { + method: 'GET', + headers: {Authorization: authHeader}, + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + if (!oidcGetResponse.ok) { + console.warn(`[E2E] Could not get OIDC config (${oidcGetResponse.status}), skipping CORS config`); + + return {clientId, clientSecret}; + } + + const oidcConfig = (await oidcGetResponse.json()) as Record; + + // Update the config with allowed origins, PKCE, and callback URLs (including post-logout). + // IS requires a single regex pattern for multiple callback URLs. + const afterSignInUrl = `${SAMPLE_APP.url}${SAMPLE_APP.afterSignInPath}`; + const afterSignOutUrl = `${SAMPLE_APP.url}${SAMPLE_APP.afterSignOutPath}`; + const callbackRegex = `regexp=(${escapeRegex(afterSignInUrl)}|${escapeRegex(afterSignOutUrl)})`; + + const updatedConfig = { + ...oidcConfig, + allowedOrigins: [SAMPLE_APP.url], + callbackURLs: [callbackRegex], + publicClient: true, + pkce: { + mandatory: true, + supportPlainTransformAlgorithm: false, + }, + }; + + // Remove read-only fields that can't be sent in PUT + delete updatedConfig.state; + + const oidcPutResponse = await fetch(oidcUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: authHeader, + }, + body: JSON.stringify(updatedConfig), + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + if (!oidcPutResponse.ok) { + const text = await oidcPutResponse.text(); + + console.warn(`[E2E] Could not update OIDC config (${oidcPutResponse.status}): ${text}`); + } else { + console.log(`[E2E] IS app CORS origins and PKCE configured successfully`); + } + + // Step 4: Enable app-native authentication API (required for embedded component) + const appUrl = `${baseUrl}/api/server/v1/applications/${app.id}`; + const patchResponse = await fetch(appUrl, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: authHeader, + }, + body: JSON.stringify({ + advancedConfigurations: { + enableAPIBasedAuthentication: true, + }, + }), + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + if (!patchResponse.ok) { + const text = await patchResponse.text(); + + console.warn(`[E2E] Could not enable app-native auth (${patchResponse.status}): ${text}`); + } else { + console.log(`[E2E] IS app-native authentication enabled`); + } + + return {clientId, clientSecret}; +} + +/** + * Look up an existing OAuth app by name from the IS Application Management API. + */ +async function lookupExistingIsApp( + baseUrl: string, + authHeader: string, +): Promise<{clientId: string; clientSecret: string}> { + const appsUrl = `${baseUrl}/api/server/v1/applications?limit=30`; + const appsResponse = await fetch(appsUrl, { + method: 'GET', + headers: {Authorization: authHeader}, + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + if (!appsResponse.ok) { + throw new Error(`[E2E] Failed to list IS applications (${appsResponse.status})`); + } + + const appsData = (await appsResponse.json()) as { + applications: Array<{id: string; name: string}>; + }; + const app = appsData.applications.find((a) => a.name === 'asgardeo-e2e-test-app'); + + if (!app) { + throw new Error('[E2E] Could not find existing app "asgardeo-e2e-test-app"'); + } + + const oidcUrl = `${baseUrl}/api/server/v1/applications/${app.id}/inbound-protocols/oidc`; + const oidcResponse = await fetch(oidcUrl, { + method: 'GET', + headers: {Authorization: authHeader}, + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + if (!oidcResponse.ok) { + throw new Error(`[E2E] Failed to get OIDC config for existing app (${oidcResponse.status})`); + } + + const oidcData = (await oidcResponse.json()) as {clientId: string; clientSecret: string}; + + console.log(`[E2E] Found existing IS app: clientId=${oidcData.clientId}`); + + return {clientId: oidcData.clientId, clientSecret: oidcData.clientSecret}; +} diff --git a/e2e/setup/is/constants.ts b/e2e/setup/is/constants.ts new file mode 100644 index 000000000..bb9084078 --- /dev/null +++ b/e2e/setup/is/constants.ts @@ -0,0 +1,12 @@ +/** + * WSO2 Identity Server configuration for e2e tests. + */ + +export const IS_CONFIG = { + baseUrl: process.env.IS_BASE_URL ?? 'https://localhost:9443', + dcrEndpoint: '/api/identity/oauth2/dcr/v1.1/register', + scim2UsersEndpoint: '/scim2/Users', + adminUsername: 'admin', + adminPassword: 'admin', + healthCheckPath: '/carbon/admin/login.jsp', +} as const; diff --git a/e2e/setup/is/user-provisioning.ts b/e2e/setup/is/user-provisioning.ts new file mode 100644 index 000000000..782e780af --- /dev/null +++ b/e2e/setup/is/user-provisioning.ts @@ -0,0 +1,50 @@ +/** + * Test user provisioning for WSO2 Identity Server via SCIM2. + */ + +import {TEST_USER} from '../constants'; +import {basicAuth, insecureAgent} from '../http-utils'; +import {IS_CONFIG} from './constants'; + +/** + * Create a test user on WSO2 Identity Server via the SCIM2 Users API. + * + * POST https://localhost:9443/scim2/Users + */ +export async function provisionIsTestUser(): Promise { + const {baseUrl, scim2UsersEndpoint, adminUsername, adminPassword} = IS_CONFIG; + const url = `${baseUrl}${scim2UsersEndpoint}`; + + console.log(`[E2E] Provisioning test user on IS: ${url}`); + + const scimUser = { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], + userName: TEST_USER.username, + password: TEST_USER.password, + name: { + givenName: TEST_USER.firstName, + familyName: TEST_USER.lastName, + }, + emails: [{value: TEST_USER.email, primary: true}], + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/scim+json', + Authorization: basicAuth(adminUsername, adminPassword), + }, + body: JSON.stringify(scimUser), + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + // 201 Created = success, 409 Conflict = user already exists (both acceptable) + if (!response.ok && response.status !== 409) { + const text = await response.text(); + + throw new Error(`[E2E] IS SCIM2 user creation failed (${response.status}): ${text}`); + } + + console.log(`[E2E] IS test user provisioned: ${TEST_USER.username}`); +} diff --git a/e2e/setup/launch-dev-server.ts b/e2e/setup/launch-dev-server.ts new file mode 100644 index 000000000..7f1cd639c --- /dev/null +++ b/e2e/setup/launch-dev-server.ts @@ -0,0 +1,120 @@ +/** + * Launch script: runs IDP setup (writes .env) then starts the Vite dev server. + * + * This ensures the .env is written BEFORE Vite reads it, avoiding the race + * condition between Playwright's webServer and globalSetup. + */ + +import {spawn} from 'child_process'; +import {writeFileSync} from 'fs'; +import path from 'path'; +import {getIdpTarget, SAMPLE_APP} from './constants'; +import {IS_CONFIG} from './is/constants'; +import {THUNDER_CONFIG} from './thunder/constants'; +import {waitForIdp} from './wait-for-idp'; +import {registerIsApp} from './is/app-registration'; +import {getThunderAppClientId} from './thunder/app-registration'; +import {provisionIsTestUser} from './is/user-provisioning'; +import {provisionThunderTestUser} from './thunder/user-provisioning'; + +function writeSampleAppEnv(vars: Record): void { + const envPath = path.resolve(__dirname, '../../samples/teamspace-react/.env'); + const content = Object.entries(vars) + .map(([key, value]) => `${key}='${value}'`) + .join('\n'); + + writeFileSync(envPath, content + '\n'); + console.log(`[E2E] Wrote .env to ${envPath}`); +} + +async function setup(): Promise { + const idpTarget = getIdpTarget(); + const signInMode = process.env.SIGN_IN_MODE ?? 'redirect'; + + console.log(`\n[E2E Setup] IDP target: ${idpTarget}, sign-in mode: ${signInMode}\n`); + + if (idpTarget === 'is') { + const {baseUrl, healthCheckPath} = IS_CONFIG; + + await waitForIdp(`${baseUrl}${healthCheckPath}`); + + const {clientId} = await registerIsApp(); + + await provisionIsTestUser(); + + const envVars: Record = { + VITE_ASGARDEO_BASE_URL: baseUrl, + VITE_ASGARDEO_CLIENT_ID: clientId, + VITE_ASGARDEO_AFTER_SIGN_IN_URL: `${SAMPLE_APP.url}${SAMPLE_APP.afterSignInPath}`, + VITE_ASGARDEO_AFTER_SIGN_OUT_URL: `${SAMPLE_APP.url}${SAMPLE_APP.afterSignOutPath}`, + VITE_ASGARDEO_SIGN_UP_URL: `${SAMPLE_APP.url}${SAMPLE_APP.signUpPath}`, + }; + + // Only set signInUrl for embedded mode โ€” its presence tells the app to render inline + if (signInMode === 'embedded') { + envVars.VITE_ASGARDEO_SIGN_IN_URL = `${SAMPLE_APP.url}${SAMPLE_APP.signInPath}`; + } + + writeSampleAppEnv(envVars); + } else { + const {baseUrl, healthCheckPath} = THUNDER_CONFIG; + + await waitForIdp(`${baseUrl}${healthCheckPath}`); + + const {clientId, applicationId} = await getThunderAppClientId(); + + await provisionThunderTestUser(); + + const envVars: Record = { + VITE_ASGARDEO_BASE_URL: baseUrl, + VITE_ASGARDEO_CLIENT_ID: clientId, + VITE_ASGARDEO_PLATFORM: 'AsgardeoV2', + VITE_ASGARDEO_AFTER_SIGN_IN_URL: `${SAMPLE_APP.url}${SAMPLE_APP.afterSignInPath}`, + VITE_ASGARDEO_AFTER_SIGN_OUT_URL: `${SAMPLE_APP.url}${SAMPLE_APP.afterSignOutPath}`, + VITE_ASGARDEO_SIGN_UP_URL: `${SAMPLE_APP.url}${SAMPLE_APP.signUpPath}`, + }; + + // Only set signInUrl for embedded mode โ€” its presence tells the app to render inline + if (signInMode === 'embedded') { + envVars.VITE_ASGARDEO_SIGN_IN_URL = `${SAMPLE_APP.url}${SAMPLE_APP.signInPath}`; + } + + if (applicationId) { + envVars.VITE_ASGARDEO_APPLICATION_ID = applicationId; + } + + writeSampleAppEnv(envVars); + } + + console.log('\n[E2E Setup] Complete. Starting Vite dev server...\n'); +} + +async function main(): Promise { + await setup(); + + // Start Vite dev server โ€” this replaces the current process + const repoRoot = path.resolve(__dirname, '../..'); + const child = spawn('pnpm', ['--filter', '@asgardeo/teamspace-react', 'dev'], { + cwd: repoRoot, + stdio: 'inherit', + shell: true, + }); + + child.on('error', (err) => { + console.error('[E2E] Failed to start Vite dev server:', err); + process.exit(1); + }); + + child.on('exit', (code) => { + process.exit(code ?? 0); + }); + + // Forward signals to child + process.on('SIGTERM', () => child.kill('SIGTERM')); + process.on('SIGINT', () => child.kill('SIGINT')); +} + +main().catch((err) => { + console.error('[E2E] Setup failed:', err); + process.exit(1); +}); diff --git a/e2e/setup/thunder/app-registration.ts b/e2e/setup/thunder/app-registration.ts new file mode 100644 index 000000000..0f6b0d15c --- /dev/null +++ b/e2e/setup/thunder/app-registration.ts @@ -0,0 +1,97 @@ +/** + * Get the pre-configured Thunder application's client ID and internal application ID. + * + * Thunder's setup.sh bootstrap creates a "React SDK Sample" app with a known + * client_id during the 3-stage Docker init (db-init -> setup -> server). + * We use this pre-created app rather than trying to register a new one, + * since the Thunder management APIs require JWT Bearer auth. + * + * The internal application ID (UUID) is needed for the embedded component + * which calls /flow/execute with {applicationId, flowType}. We retrieve it by + * querying Thunder's SQLite database via docker exec. + */ + +import {execSync} from 'child_process'; +import {SAMPLE_APP} from '../constants'; +import {THUNDER_CONFIG} from './constants'; + +const CONTAINER = 'asgardeo-e2e-thunder'; +const DB_PATH = '/opt/thunder/repository/database/thunderdb.db'; + +function thunderSqlite(sql: string): string { + return execSync(`docker exec ${CONTAINER} sqlite3 ${DB_PATH} "${sql}"`, { + encoding: 'utf-8', + }).trim(); +} + +export async function getThunderAppClientId(): Promise<{clientId: string; applicationId?: string}> { + const clientId = THUNDER_CONFIG.preConfiguredClientId; + + console.log(`[E2E] Using pre-configured Thunder app: clientId=${clientId}`); + + // Retrieve the internal application ID from Thunder's SQLite database. + // The /flow/execute API requires the internal UUID, not the client_id. + let applicationId: string | undefined; + + try { + const result = thunderSqlite("SELECT APP_ID FROM SP_APP WHERE APP_NAME='React SDK Sample'"); + + if (result) { + applicationId = result; + console.log(`[E2E] Thunder application ID: ${applicationId}`); + } + } catch { + console.warn('[E2E] Could not retrieve Thunder application ID from database'); + } + + // Patch the pre-configured app's OAuth config: + // 1. Add the callback URL to redirect_uris (bootstrap only has / and /dashboard) + // 2. Fix the token issuer to match the OIDC discovery issuer (baseUrl, not baseUrl/oauth2/token) + const callbackUrl = `${SAMPLE_APP.url}${SAMPLE_APP.afterSignInPath}`; + const correctIssuer = THUNDER_CONFIG.baseUrl; + + try { + const configJson = thunderSqlite( + `SELECT OAUTH_CONFIG_JSON FROM IDN_OAUTH_CONSUMER_APPS WHERE CONSUMER_KEY='${clientId}'`, + ); + + if (configJson) { + const config = JSON.parse(configJson); + let needsUpdate = false; + + // Add callback URL if missing + const redirectUris: string[] = config.redirect_uris || []; + + if (!redirectUris.includes(callbackUrl)) { + redirectUris.push(callbackUrl); + config.redirect_uris = redirectUris; + needsUpdate = true; + } + + // Fix token issuer to match OIDC discovery (required for ID token validation) + if (config.token?.issuer !== correctIssuer) { + config.token.issuer = correctIssuer; + needsUpdate = true; + } + + if (needsUpdate) { + const updatedJson = JSON.stringify(config); + + // Write JSON to a temp file inside the container to avoid shell escaping issues, + // then use readfile() in the SQLite UPDATE. + execSync( + `docker exec -i ${CONTAINER} sh -c 'cat > /tmp/oauth_config.json'`, + {input: updatedJson, encoding: 'utf-8'}, + ); + thunderSqlite( + `UPDATE IDN_OAUTH_CONSUMER_APPS SET OAUTH_CONFIG_JSON=readfile('/tmp/oauth_config.json') WHERE CONSUMER_KEY='${clientId}'`, + ); + console.log(`[E2E] Thunder app OAuth config updated (redirect_uris, token issuer)`); + } + } + } catch (err) { + console.warn('[E2E] Could not update Thunder app OAuth config:', err); + } + + return {clientId, applicationId}; +} diff --git a/e2e/setup/thunder/constants.ts b/e2e/setup/thunder/constants.ts new file mode 100644 index 000000000..df7acca67 --- /dev/null +++ b/e2e/setup/thunder/constants.ts @@ -0,0 +1,13 @@ +/** + * Asgardeo Thunder configuration for e2e tests. + */ + +export const THUNDER_CONFIG = { + baseUrl: process.env.THUNDER_BASE_URL ?? 'https://localhost:8090', + healthCheckPath: '/health/readiness', + adminUsername: 'admin', + adminPassword: 'admin', + // The React SDK Sample app is created by Thunder's bootstrap script + // (02-sample-resources.sh) with this deterministic client_id. + preConfiguredClientId: 'REACT_SDK_SAMPLE', +} as const; diff --git a/e2e/setup/thunder/user-provisioning.ts b/e2e/setup/thunder/user-provisioning.ts new file mode 100644 index 000000000..64fe4be05 --- /dev/null +++ b/e2e/setup/thunder/user-provisioning.ts @@ -0,0 +1,14 @@ +/** + * Test user provisioning for Asgardeo Thunder. + * + * The admin user (admin/admin) is already created by the bootstrap + * script (01-default-resources.sh) during the 3-stage Docker init. + * + * We use the admin user for e2e testing since creating additional users + * requires JWT Bearer auth which needs a full OAuth flow. + */ + +export async function provisionThunderTestUser(): Promise { + console.log(`[E2E] Thunder test user: using pre-created admin user (admin/admin)`); + console.log(`[E2E] Admin user was created by Thunder bootstrap (01-default-resources.sh)`); +} diff --git a/e2e/setup/wait-for-idp.ts b/e2e/setup/wait-for-idp.ts new file mode 100644 index 000000000..a90893902 --- /dev/null +++ b/e2e/setup/wait-for-idp.ts @@ -0,0 +1,48 @@ +/** + * Health-check polling utility that waits for an IDP to become ready. + */ + +import {insecureAgent} from './http-utils'; + +interface WaitOptions { + /** Maximum time to wait in milliseconds. Default: 180000 (3 minutes). */ + timeoutMs?: number; + /** Polling interval in milliseconds. Default: 5000 (5 seconds). */ + intervalMs?: number; +} + +export async function waitForIdp(healthUrl: string, options: WaitOptions = {}): Promise { + const {timeoutMs = 180_000, intervalMs = 5_000} = options; + const start = Date.now(); + + console.log(`[E2E] Waiting for IDP at ${healthUrl} (timeout: ${timeoutMs / 1000}s)...`); + + while (Date.now() - start < timeoutMs) { + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(healthUrl, { + signal: controller.signal, + // @ts-expect-error -- Node fetch supports dispatcher via undici + dispatcher: insecureAgent, + }); + + clearTimeout(timer); + + if (response.ok || response.status === 302) { + const elapsed = ((Date.now() - start) / 1000).toFixed(1); + + console.log(`[E2E] IDP ready at ${healthUrl} (took ${elapsed}s)`); + + return; + } + } catch { + // IDP not ready yet + } + + await new Promise(resolve => setTimeout(resolve, intervalMs)); + } + + throw new Error(`[E2E] IDP at ${healthUrl} did not become healthy within ${timeoutMs / 1000}s`); +} diff --git a/e2e/tests/is/embedded/sign-in.spec.ts b/e2e/tests/is/embedded/sign-in.spec.ts new file mode 100644 index 000000000..d87417e00 --- /dev/null +++ b/e2e/tests/is/embedded/sign-in.spec.ts @@ -0,0 +1,26 @@ +import {test, expect} from '../../../fixtures/base.fixture'; +import {performEmbeddedSignIn} from '../../../helpers/auth-helpers'; +import {SELECTORS} from '../../../helpers/selectors'; + +test.describe('Embedded Sign-In Flow', () => { + test('should render the embedded component with form fields', async ({page}) => { + await page.goto('/signin'); + + // Wait for the SDK's component to initialize and render form fields + await page.waitForSelector(SELECTORS.embeddedSignIn.container, {timeout: 30_000}); + + await expect(page.locator(SELECTORS.embeddedSignIn.usernameInput)).toBeVisible(); + await expect(page.locator(SELECTORS.embeddedSignIn.passwordInput)).toBeVisible(); + await expect(page.locator(SELECTORS.embeddedSignIn.submitButton)).toBeVisible(); + }); + + test('should successfully sign in with valid credentials via embedded component', async ({page}) => { + await performEmbeddedSignIn(page); + + await expect(page).toHaveURL(/\/dashboard/); + + const heading = page.locator(SELECTORS.dashboard.welcomeHeading); + + await expect(heading).toContainText('Welcome back'); + }); +}); diff --git a/e2e/tests/is/redirect/sign-in.spec.ts b/e2e/tests/is/redirect/sign-in.spec.ts new file mode 100644 index 000000000..5d85ec558 --- /dev/null +++ b/e2e/tests/is/redirect/sign-in.spec.ts @@ -0,0 +1,30 @@ +import {test, expect} from '../../../fixtures/base.fixture'; +import {performSignIn} from '../../../helpers/auth-helpers'; +import {SELECTORS} from '../../../helpers/selectors'; +import {IS_SELECTORS} from '../../../helpers/is/selectors'; + +test.describe('Sign-In Flow', () => { + test('should redirect to the IS login page from /signin', async ({page}) => { + await page.goto('/signin'); + + await page.waitForURL('**/authenticationendpoint/**', {timeout: 30_000}); + await expect(page.locator(IS_SELECTORS.login.usernameInput)).toBeVisible(); + await expect(page.locator(IS_SELECTORS.login.passwordInput)).toBeVisible(); + }); + + test('should successfully sign in with valid credentials and redirect to dashboard', async ({page}) => { + await performSignIn(page); + + await expect(page).toHaveURL(/\/dashboard/); + + const heading = page.locator(SELECTORS.dashboard.welcomeHeading); + + await expect(heading).toContainText('Welcome back'); + }); + + test('should redirect unauthenticated users from /dashboard to the IS login page', async ({page}) => { + await page.goto('/dashboard'); + + await page.waitForURL('**/authenticationendpoint/**', {timeout: 30_000}); + }); +}); diff --git a/e2e/tests/is/redirect/sign-out.spec.ts b/e2e/tests/is/redirect/sign-out.spec.ts new file mode 100644 index 000000000..55b08bbfc --- /dev/null +++ b/e2e/tests/is/redirect/sign-out.spec.ts @@ -0,0 +1,28 @@ +import {test, expect} from '../../../fixtures/base.fixture'; +import {performSignIn, performSignOut} from '../../../helpers/auth-helpers'; +import {SELECTORS} from '../../../helpers/selectors'; + +test.describe('Sign-Out Flow', () => { + test.beforeEach(async ({page}) => { + await performSignIn(page); + }); + + test('should sign out and redirect to the landing page', async ({page}) => { + await expect(page.locator(SELECTORS.dashboard.welcomeHeading)).toContainText('Welcome back'); + + await performSignOut(page); + + await expect(page.getByText(SELECTORS.header.signInText)).toBeVisible({timeout: 10_000}); + }); + + test('should not be able to access protected routes after sign out', async ({page}) => { + await expect(page.locator(SELECTORS.dashboard.welcomeHeading)).toContainText('Welcome back'); + + await performSignOut(page); + + await page.goto('/dashboard'); + + // In redirect mode, ProtectedRoute redirects to /signin which triggers signIn() โ†’ IDP + await page.waitForURL('**/authenticationendpoint/**', {timeout: 30_000}); + }); +}); diff --git a/e2e/tests/is/redirect/user-profile.spec.ts b/e2e/tests/is/redirect/user-profile.spec.ts new file mode 100644 index 000000000..caca67331 --- /dev/null +++ b/e2e/tests/is/redirect/user-profile.spec.ts @@ -0,0 +1,28 @@ +import {test, expect} from '../../../fixtures/base.fixture'; +import {performSignIn} from '../../../helpers/auth-helpers'; +import {SELECTORS} from '../../../helpers/selectors'; + +test.describe('User Profile', () => { + test.beforeEach(async ({page}) => { + await performSignIn(page); + }); + + test('should display the profile page with user information', async ({page}) => { + await page.goto('/profile'); + + await expect(page.locator(SELECTORS.profile.heading)).toContainText('Profile'); + + await page.waitForLoadState('networkidle'); + }); + + test('should navigate back to dashboard from profile', async ({page}) => { + await page.goto('/profile'); + + const backButton = page.getByText(SELECTORS.profile.backToDashboard); + + await expect(backButton).toBeVisible(); + await backButton.click(); + + await expect(page).toHaveURL(/\/dashboard/); + }); +}); diff --git a/e2e/tests/thunder/embedded/sign-in.spec.ts b/e2e/tests/thunder/embedded/sign-in.spec.ts new file mode 100644 index 000000000..d87417e00 --- /dev/null +++ b/e2e/tests/thunder/embedded/sign-in.spec.ts @@ -0,0 +1,26 @@ +import {test, expect} from '../../../fixtures/base.fixture'; +import {performEmbeddedSignIn} from '../../../helpers/auth-helpers'; +import {SELECTORS} from '../../../helpers/selectors'; + +test.describe('Embedded Sign-In Flow', () => { + test('should render the embedded component with form fields', async ({page}) => { + await page.goto('/signin'); + + // Wait for the SDK's component to initialize and render form fields + await page.waitForSelector(SELECTORS.embeddedSignIn.container, {timeout: 30_000}); + + await expect(page.locator(SELECTORS.embeddedSignIn.usernameInput)).toBeVisible(); + await expect(page.locator(SELECTORS.embeddedSignIn.passwordInput)).toBeVisible(); + await expect(page.locator(SELECTORS.embeddedSignIn.submitButton)).toBeVisible(); + }); + + test('should successfully sign in with valid credentials via embedded component', async ({page}) => { + await performEmbeddedSignIn(page); + + await expect(page).toHaveURL(/\/dashboard/); + + const heading = page.locator(SELECTORS.dashboard.welcomeHeading); + + await expect(heading).toContainText('Welcome back'); + }); +}); diff --git a/e2e/tests/thunder/redirect/sign-in.spec.ts b/e2e/tests/thunder/redirect/sign-in.spec.ts new file mode 100644 index 000000000..bdba736ec --- /dev/null +++ b/e2e/tests/thunder/redirect/sign-in.spec.ts @@ -0,0 +1,31 @@ +import {test, expect} from '../../../fixtures/base.fixture'; +import {performSignIn} from '../../../helpers/auth-helpers'; +import {SELECTORS} from '../../../helpers/selectors'; +import {THUNDER_SELECTORS} from '../../../helpers/thunder/selectors'; + +test.describe('Sign-In Flow', () => { + test('should redirect to the Thunder Gate login page from /signin', async ({page}) => { + await page.goto('/signin'); + + await page.waitForURL('**/gate/signin**', {timeout: 30_000}); + await expect(page.locator(THUNDER_SELECTORS.gate.usernameInput)).toBeVisible(); + await expect(page.locator(THUNDER_SELECTORS.gate.passwordInput)).toBeVisible(); + await expect(page.getByRole('button', {name: THUNDER_SELECTORS.gate.submitButtonText})).toBeVisible(); + }); + + test('should successfully sign in with valid credentials and redirect to dashboard', async ({page}) => { + await performSignIn(page); + + await expect(page).toHaveURL(/\/dashboard/); + + const heading = page.locator(SELECTORS.dashboard.welcomeHeading); + + await expect(heading).toContainText('Welcome back'); + }); + + test('should redirect unauthenticated users from /dashboard to the Thunder Gate login page', async ({page}) => { + await page.goto('/dashboard'); + + await page.waitForURL('**/gate/signin**', {timeout: 30_000}); + }); +}); diff --git a/e2e/tests/thunder/redirect/user-profile.spec.ts b/e2e/tests/thunder/redirect/user-profile.spec.ts new file mode 100644 index 000000000..caca67331 --- /dev/null +++ b/e2e/tests/thunder/redirect/user-profile.spec.ts @@ -0,0 +1,28 @@ +import {test, expect} from '../../../fixtures/base.fixture'; +import {performSignIn} from '../../../helpers/auth-helpers'; +import {SELECTORS} from '../../../helpers/selectors'; + +test.describe('User Profile', () => { + test.beforeEach(async ({page}) => { + await performSignIn(page); + }); + + test('should display the profile page with user information', async ({page}) => { + await page.goto('/profile'); + + await expect(page.locator(SELECTORS.profile.heading)).toContainText('Profile'); + + await page.waitForLoadState('networkidle'); + }); + + test('should navigate back to dashboard from profile', async ({page}) => { + await page.goto('/profile'); + + const backButton = page.getByText(SELECTORS.profile.backToDashboard); + + await expect(backButton).toBeVisible(); + await backButton.click(); + + await expect(page).toHaveURL(/\/dashboard/); + }); +}); diff --git a/e2e/thunder-bootstrap/02-sample-resources.sh b/e2e/thunder-bootstrap/02-sample-resources.sh new file mode 100755 index 000000000..28f4100fd --- /dev/null +++ b/e2e/thunder-bootstrap/02-sample-resources.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# ---------------------------------------------------------------------------- +# Custom bootstrap script for e2e tests. +# Creates the React SDK Sample app with redirect_uris pointing to +# the Vite dev server on https://localhost:5173. +# ---------------------------------------------------------------------------- + +set -e + +SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]:-$0}")" +source "${SCRIPT_DIR}/common.sh" + +log_info "Creating e2e test resources..." +echo "" + +# ============================================================================ +# Create Customers Organization Unit +# ============================================================================ + +CUSTOMER_OU_HANDLE="customers" + +log_info "Creating Customers organization unit..." + +read -r -d '' CUSTOMERS_OU_PAYLOAD < { value, placeholder, onBlur, + 'data-testid': `asgardeo-signin-${name}`, }; switch (type) { diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 064e05816..ec0c2b020 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -204,6 +204,7 @@ export const BaseUserDropdown: FC = ({ color="tertiary" variant="text" size="medium" + data-testid="asgardeo-user-dropdown-trigger" {...getReferenceProps()} > = ({ variant="text" size="small" startIcon={item.icon} + data-testid={`asgardeo-user-dropdown-item-${index}`} onMouseEnter={() => setHoveredItemIndex(index)} onMouseLeave={() => setHoveredItemIndex(null)} > diff --git a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx index 639eb005b..ba5fd2c69 100644 --- a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx +++ b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx @@ -209,6 +209,7 @@ const createAuthComponentFromFlow = ( onClick={handleClick} disabled={isLoading || !isFormValid} className={options.buttonClassName} + data-testid="asgardeo-signin-submit" variant={component.variant?.toLowerCase() === 'primary' ? 'solid' : 'outline'} color={component.variant?.toLowerCase() === 'primary' ? 'primary' : 'secondary'} > diff --git a/packages/react/src/components/presentation/auth/SignIn/v1/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/v1/BaseSignIn.tsx index 153c8ad8f..98b4ad50d 100644 --- a/packages/react/src/components/presentation/auth/SignIn/v1/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/v1/BaseSignIn.tsx @@ -979,7 +979,7 @@ const BaseSignInContent: FC = ({ if (!isInitialized && isLoading) { return ( - +
@@ -1007,7 +1007,7 @@ const BaseSignInContent: FC = ({ .filter(authenticator => !HIDDEN_AUTHENTICATORS.includes(authenticator.authenticatorId)); return ( - + {(showTitle || showSubtitle) && ( {showTitle && ( @@ -1127,7 +1127,7 @@ const BaseSignInContent: FC = ({ if (!currentAuthenticator) { return ( - + {error && ( @@ -1149,7 +1149,7 @@ const BaseSignInContent: FC = ({ // Show loading state while passkey authentication is in progress return ( - +
@@ -1166,7 +1166,7 @@ const BaseSignInContent: FC = ({ } return ( - + {flowTitle || t('signin.heading')} diff --git a/packages/react/src/components/presentation/auth/SignIn/v1/options/UsernamePassword.tsx b/packages/react/src/components/presentation/auth/SignIn/v1/options/UsernamePassword.tsx index f15db1cbb..cb249b103 100644 --- a/packages/react/src/components/presentation/auth/SignIn/v1/options/UsernamePassword.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/v1/options/UsernamePassword.tsx @@ -86,6 +86,7 @@ const UsernamePassword: FC = ({ disabled={isLoading} loading={isLoading} className={buttonClassName} + data-testid="asgardeo-signin-submit" style={{marginBottom: `calc(${theme.vars.spacing.unit} * 2)`}} > {t('username.password.buttons.submit.text')} diff --git a/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx index c433f11cd..c2fba98a4 100644 --- a/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx @@ -551,13 +551,13 @@ const BaseSignInContent: FC = ({ messages: flowMessages || [], }; - return
{children(renderProps)}
; + return
{children(renderProps)}
; } // Default UI rendering if (isLoading) { return ( - +
@@ -569,7 +569,7 @@ const BaseSignInContent: FC = ({ if (!components || components.length === 0) { return ( - + {t('errors.signin.components.not.available')} @@ -589,7 +589,7 @@ const BaseSignInContent: FC = ({ ); return ( - + {(showTitle || showSubtitle) && ( {showTitle && ( diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 1d5e1b882..2fcb97e6e 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -255,6 +255,13 @@ const AsgardeoProvider: FC> = ({ return; } + // Don't set loading=false while auth params are in the URL and user isn't signed in yet. + // This prevents ProtectedRoute from redirecting before the sign-in effect processes the auth code. + const currentUrl = new URL(window.location.href); + if (!isSignedInSync && hasAuthParams(currentUrl, afterSignInUrl)) { + return; + } + setIsLoadingSync(asgardeo.isLoading()); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3615a9e19..f9b1d1da0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: '@changesets/cli': specifier: 2.29.8 version: 2.29.8(@types/node@24.0.3) + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@wso2/eslint-plugin': specifier: 'catalog:' version: https://codeload.github.com/brionmario/wso2-ui-configs/tar.gz/a1fc6eb570653c999828aea9f5027cba06af4391#path:packages/eslint-plugin(eslint@8.57.0)(typescript@5.7.2) @@ -67,9 +70,15 @@ importers: prettier: specifier: 2.6.2 version: 2.6.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: 5.7.2 version: 5.7.2 + undici: + specifier: ^7.21.0 + version: 7.21.0 packages/browser: dependencies: @@ -121,7 +130,7 @@ importers: version: 22.15.3 '@vitest/browser': specifier: 3.1.3 - version: 3.1.3(playwright@1.55.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))(vitest@3.1.3) + version: 3.1.3(playwright@1.55.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.1.3) '@wso2/eslint-plugin': specifier: 'catalog:' version: https://codeload.github.com/brionmario/wso2-ui-configs/tar.gz/a1fc6eb570653c999828aea9f5027cba06af4391#path:packages/eslint-plugin(eslint@8.57.0)(typescript@5.7.2) @@ -157,7 +166,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/express: dependencies: @@ -197,7 +206,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/i18n: dependencies: @@ -231,7 +240,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/javascript: dependencies: @@ -268,7 +277,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/nextjs: dependencies: @@ -308,7 +317,7 @@ importers: version: 8.57.0 next: specifier: 15.5.12 - version: 15.5.12(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(sass@1.92.1) + version: 15.5.12(@playwright/test@1.58.2)(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(sass@1.92.1) prettier: specifier: 2.6.2 version: 2.6.2 @@ -323,7 +332,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/node: dependencies: @@ -381,7 +390,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/react: dependencies: @@ -451,7 +460,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@26.1.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@26.1.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/react-router: dependencies: @@ -500,7 +509,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/tanstack-router: dependencies: @@ -552,7 +561,7 @@ importers: version: 5.7.2 vitest: specifier: 3.1.3 - version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages/vue: dependencies: @@ -564,7 +573,7 @@ importers: version: 0.1.3 '@vitejs/plugin-vue': specifier: 5.2.4 - version: 5.2.4(vite@7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0))(vue@3.5.13(typescript@5.1.6)) + version: 5.2.4(vite@7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.1.6)) base64url: specifier: 3.0.1 version: 3.0.1 @@ -604,10 +613,10 @@ importers: version: 20.12.7 '@vitest/coverage-v8': specifier: 3.0.8 - version: 3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0)) + version: 3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/web-worker': specifier: 3.0.8 - version: 3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0)) + version: 3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vue/eslint-config-prettier': specifier: 8.0.0 version: 8.0.0(eslint@8.57.0)(prettier@2.6.2) @@ -658,10 +667,10 @@ importers: version: 5.1.6 vite: specifier: 7.1.12 - version: 7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + version: 7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) vitest: specifier: 3.0.8 - version: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + version: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) vue-tsc: specifier: 2.2.2 version: 2.2.2(typescript@5.1.6) @@ -695,10 +704,10 @@ importers: version: 19.1.5(@types/react@19.1.5) '@vitejs/plugin-basic-ssl': specifier: 2.0.0 - version: 2.0.0(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + version: 2.0.0(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitejs/plugin-react': specifier: 4.4.1 - version: 4.4.1(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + version: 4.4.1(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) eslint: specifier: 9.25.0 version: 9.25.0(jiti@2.6.0) @@ -719,7 +728,7 @@ importers: version: 8.30.1(eslint@9.25.0(jiti@2.6.0))(typescript@5.8.3) vite: specifier: 6.4.1 - version: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) samples/teamspace-react: dependencies: @@ -771,7 +780,7 @@ importers: version: 9.25.0 '@tailwindcss/vite': specifier: 4.1.8 - version: 4.1.8(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + version: 4.1.8(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@types/react': specifier: 19.1.5 version: 19.1.5 @@ -780,10 +789,10 @@ importers: version: 19.1.5(@types/react@19.1.5) '@vitejs/plugin-basic-ssl': specifier: 2.0.0 - version: 2.0.0(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + version: 2.0.0(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitejs/plugin-react': specifier: 4.4.1 - version: 4.4.1(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + version: 4.4.1(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) autoprefixer: specifier: 10.4.16 version: 10.4.16(postcss@8.4.31) @@ -816,7 +825,7 @@ importers: version: 8.30.1(eslint@9.25.0(jiti@2.6.0))(typescript@5.8.3) vite: specifier: 6.4.1 - version: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + version: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) packages: @@ -1964,6 +1973,11 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -4254,6 +4268,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4264,25 +4281,27 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} @@ -5428,8 +5447,8 @@ packages: engines: {node: '>=18'} hasBin: true - playwright-core@1.56.1: - resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} hasBin: true @@ -5438,8 +5457,8 @@ packages: engines: {node: '>=18'} hasBin: true - playwright@1.56.1: - resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} engines: {node: '>=18'} hasBin: true @@ -5852,6 +5871,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} @@ -6588,6 +6610,11 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tw-animate-css@1.3.4: resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==} @@ -6669,6 +6696,10 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici@7.21.0: + resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} + engines: {node: '>=20.18.1'} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -8127,6 +8158,10 @@ snapshots: '@pkgr/core@0.2.4': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@polka/url@1.0.0-next.29': {} '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.5)(react@19.1.4)': @@ -8411,12 +8446,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.8 '@tailwindcss/oxide-win32-x64-msvc': 4.1.8 - '@tailwindcss/vite@4.1.8(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.8(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.8 '@tailwindcss/oxide': 4.1.8 tailwindcss: 4.1.8 - vite: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) '@tanstack/history@1.154.7': {} @@ -8909,36 +8944,36 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-basic-ssl@2.0.0(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))': + '@vitejs/plugin-basic-ssl@2.0.0(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: - vite: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) - '@vitejs/plugin-react@4.4.1(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))': + '@vitejs/plugin-react@4.4.1(vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.2.4(vite@7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0))(vue@3.5.13(typescript@5.1.6))': + '@vitejs/plugin-vue@5.2.4(vite@7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.1.6))': dependencies: - vite: 7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + vite: 7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) vue: 3.5.13(typescript@5.1.6) - '@vitest/browser@3.1.3(playwright@1.55.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))(vitest@3.1.3)': + '@vitest/browser@3.1.3(playwright@1.55.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.1.3)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/utils': 3.1.3 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vitest: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) ws: 8.18.2 optionalDependencies: playwright: 1.55.1 @@ -8948,19 +8983,19 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.1.3(playwright@1.56.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))(vitest@3.1.3)': + '@vitest/browser@3.1.3(playwright@1.58.2)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.1.3)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/utils': 3.1.3 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vitest: 3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) ws: 8.18.2 optionalDependencies: - playwright: 1.56.1 + playwright: 1.58.2 transitivePeerDependencies: - bufferutil - msw @@ -8968,19 +9003,19 @@ snapshots: - vite optional: true - '@vitest/browser@3.1.3(playwright@1.56.1)(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))(vitest@3.1.3)': + '@vitest/browser@3.1.3(playwright@1.58.2)(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.1.3)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/utils': 3.1.3 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vitest: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) ws: 8.18.2 optionalDependencies: - playwright: 1.56.1 + playwright: 1.58.2 transitivePeerDependencies: - bufferutil - msw @@ -8988,7 +9023,7 @@ snapshots: - vite optional: true - '@vitest/coverage-v8@3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0))': + '@vitest/coverage-v8@3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -9002,7 +9037,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + vitest: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -9020,29 +9055,29 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.8(vite@6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0))': + '@vitest/mocker@3.0.8(vite@6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.0.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) - '@vitest/mocker@3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))': + '@vitest/mocker@3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) - '@vitest/mocker@3.1.3(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))': + '@vitest/mocker@3.1.3(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) '@vitest/pretty-format@3.0.8': dependencies: @@ -9094,10 +9129,10 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 - '@vitest/web-worker@3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0))': + '@vitest/web-worker@3.0.8(vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))': dependencies: debug: 4.4.1 - vitest: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + vitest: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -11077,6 +11112,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -11950,7 +11989,7 @@ snapshots: negotiator@1.0.0: {} - next@15.5.12(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(sass@1.92.1): + next@15.5.12(@playwright/test@1.58.2)(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(sass@1.92.1): dependencies: '@next/env': 15.5.12 '@swc/helpers': 0.5.15 @@ -11968,6 +12007,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.12 '@next/swc-win32-arm64-msvc': 15.5.12 '@next/swc-win32-x64-msvc': 15.5.12 + '@playwright/test': 1.58.2 sass: 1.92.1 sharp: 0.34.5 transitivePeerDependencies: @@ -12289,8 +12329,7 @@ snapshots: playwright-core@1.55.1: {} - playwright-core@1.56.1: - optional: true + playwright-core@1.58.2: {} playwright@1.55.1: dependencies: @@ -12298,12 +12337,11 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - playwright@1.56.1: + playwright@1.58.2: dependencies: - playwright-core: 1.56.1 + playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 - optional: true possible-typed-array-names@1.1.0: {} @@ -12710,6 +12748,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} resolve@1.19.0: @@ -13604,6 +13644,13 @@ snapshots: tslib: 1.14.1 typescript: 5.7.2 + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + tw-animate-css@1.3.4: {} type-check@0.4.0: @@ -13689,6 +13736,8 @@ snapshots: undici-types@7.8.0: optional: true + undici@7.21.0: {} + universalify@0.1.2: {} universalify@2.0.1: {} @@ -13725,13 +13774,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0): + vite-node@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -13746,13 +13795,13 @@ snapshots: - tsx - yaml - vite-node@3.1.3(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vite-node@3.1.3(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -13767,13 +13816,13 @@ snapshots: - tsx - yaml - vite-node@3.1.3(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vite-node@3.1.3(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -13788,7 +13837,7 @@ snapshots: - tsx - yaml - vite@6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0): + vite@6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -13804,9 +13853,10 @@ snapshots: sass: 1.75.0 sass-embedded: 1.92.1 terser: 5.39.2 + tsx: 4.21.0 yaml: 2.8.0 - vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -13822,9 +13872,10 @@ snapshots: sass: 1.92.1 sass-embedded: 1.92.1 terser: 5.39.2 + tsx: 4.21.0 yaml: 2.8.0 - vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -13840,9 +13891,10 @@ snapshots: sass: 1.92.1 sass-embedded: 1.92.1 terser: 5.39.2 + tsx: 4.21.0 yaml: 2.8.0 - vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vite@6.4.1(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -13858,9 +13910,10 @@ snapshots: sass: 1.92.1 sass-embedded: 1.92.1 terser: 5.39.2 + tsx: 4.21.0 yaml: 2.8.0 - vite@7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0): + vite@7.1.12(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -13876,12 +13929,13 @@ snapshots: sass: 1.75.0 sass-embedded: 1.92.1 terser: 5.39.2 + tsx: 4.21.0 yaml: 2.8.0 - vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0): + vitest@3.0.8(@types/node@20.12.7)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: '@vitest/expect': 3.0.8 - '@vitest/mocker': 3.0.8(vite@6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0)) + '@vitest/mocker': 3.0.8(vite@6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.3 '@vitest/runner': 3.0.8 '@vitest/snapshot': 3.0.8 @@ -13897,8 +13951,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) - vite-node: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) + vite-node: 3.0.8(@types/node@20.12.7)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.75.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.12.7 @@ -13917,10 +13971,10 @@ snapshots: - tsx - yaml - vitest@3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@26.1.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vitest@3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@26.1.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.3 - '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.3 '@vitest/runner': 3.1.3 '@vitest/snapshot': 3.1.3 @@ -13937,12 +13991,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) - vite-node: 3.1.3(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) + vite-node: 3.1.3(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.3 - '@vitest/browser': 3.1.3(playwright@1.56.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))(vitest@3.1.3) + '@vitest/browser': 3.1.3(playwright@1.58.2)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.1.3) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -13958,10 +14012,10 @@ snapshots: - tsx - yaml - vitest@3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vitest@3.1.3(@types/node@22.15.3)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.3 - '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.3 '@vitest/runner': 3.1.3 '@vitest/snapshot': 3.1.3 @@ -13978,12 +14032,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) - vite-node: 3.1.3(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) + vite-node: 3.1.3(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.3 - '@vitest/browser': 3.1.3(playwright@1.55.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))(vitest@3.1.3) + '@vitest/browser': 3.1.3(playwright@1.55.1)(vite@6.4.1(@types/node@22.15.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.1.3) jsdom: 27.4.0 transitivePeerDependencies: - jiti @@ -13999,10 +14053,10 @@ snapshots: - tsx - yaml - vitest@3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): + vitest@3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@27.4.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.3 - '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.3 '@vitest/runner': 3.1.3 '@vitest/snapshot': 3.1.3 @@ -14019,12 +14073,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) - vite-node: 3.1.3(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + vite: 6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) + vite-node: 3.1.3(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.30 - '@vitest/browser': 3.1.3(playwright@1.56.1)(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0))(vitest@3.1.3) + '@vitest/browser': 3.1.3(playwright@1.58.2)(vite@6.4.1(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.1.3) jsdom: 27.4.0 transitivePeerDependencies: - jiti diff --git a/samples/teamspace-react/src/main.tsx b/samples/teamspace-react/src/main.tsx index 6992792ea..d861b49e2 100644 --- a/samples/teamspace-react/src/main.tsx +++ b/samples/teamspace-react/src/main.tsx @@ -11,8 +11,10 @@ createRoot(document.getElementById('root')!).render( afterSignInUrl={import.meta.env.VITE_ASGARDEO_AFTER_SIGN_IN_URL} afterSignOutUrl={import.meta.env.VITE_ASGARDEO_AFTER_SIGN_OUT_URL} clientId={import.meta.env.VITE_ASGARDEO_CLIENT_ID} + applicationId={import.meta.env.VITE_ASGARDEO_APPLICATION_ID || undefined} signInUrl={import.meta.env.VITE_ASGARDEO_SIGN_IN_URL} signUpUrl={import.meta.env.VITE_ASGARDEO_SIGN_UP_URL} + platform={import.meta.env.VITE_ASGARDEO_PLATFORM || undefined} scopes="openid address email profile user:email read:user internal_organization_create internal_organization_view internal_organization_update internal_organization_delete internal_org_organization_update internal_org_organization_create internal_org_organization_view internal_org_organization_delete" preferences={{ theme: { diff --git a/samples/teamspace-react/src/pages/SignInPage.tsx b/samples/teamspace-react/src/pages/SignInPage.tsx index 9c94645bf..e40888a68 100644 --- a/samples/teamspace-react/src/pages/SignInPage.tsx +++ b/samples/teamspace-react/src/pages/SignInPage.tsx @@ -1,8 +1,27 @@ 'use client'; -import {SignIn} from '@asgardeo/react'; +import {SignIn, useAsgardeo} from '@asgardeo/react'; +import {useEffect, useRef} from 'react'; export default function SignInPage() { + const {signIn, signInUrl, isInitialized} = useAsgardeo(); + const redirectAttempted = useRef(false); + + useEffect(() => { + if (!signInUrl && isInitialized && !redirectAttempted.current) { + redirectAttempted.current = true; + signIn(); + } + }, [signIn, signInUrl, isInitialized]); + + if (!signInUrl) { + return ( +
+

Redirecting to sign in...

+
+ ); + } + return (