From 4656a84ae7fd93d587beb8dad523df3b6ec0ab2c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 8 Sep 2026 05:25:27 -0700 Subject: [PATCH] ci(e2e): replace maestro with touchpress in the expo native e2e The seven expo-native flows now run on the Playwright runner through touchpress, which drives the same simulator and emulator through agent-device. Each Maestro flow becomes one spec and each subflow a function, so runFlow's implicit environment is replaced by arguments. The warmup flow is gone: touchpress's readyWhen gate holds the worker's launch until the bundle renders, which is what the warmup bought. The per-flow bash runner is gone too, replaced by the runner's own retries, per-test timeouts and HTML report. Clearing app state, the platform back gesture and typing into a focused field have no touchpress equivalent, so device-control.ts reaches the agent-device client directly on the session touchpress already opened. --- .changeset/expo-native-e2e-touchpress.md | 2 + .github/workflows/expo-native-build.yml | 110 +++---- integration/playwright.config.ts | 3 + integration/playwright.expo-native.config.ts | 71 +++++ integration/tests/expo-native/.gitignore | 3 +- integration/tests/expo-native/README.md | 64 ++++ .../authview-detach-reattach.spec.ts | 32 ++ integration/tests/expo-native/config.yaml | 15 - .../embedded-profile-host-back.spec.ts | 37 +++ integration/tests/expo-native/flows.ts | 160 ++++++++++ .../flows/authview-detach-reattach.yaml | 46 --- .../flows/embedded-profile-host-back.yaml | 58 ---- .../google-sign-in-missing-credentials.yaml | 9 - .../session-persists-across-restart.yaml | 22 -- .../tests/expo-native/flows/sign-in.yaml | 13 - .../expo-native/flows/subflows/_warmup.yaml | 12 - .../flows/subflows/assert-signed-in.yaml | 9 - .../flows/subflows/assert-signed-out.yaml | 6 - .../expo-native/flows/subflows/open-app.yaml | 17 -- .../subflows/sign-in-email-password.yaml | 126 -------- .../user-button-sign-out-re-sign-in.yaml | 36 --- .../flows/user-profile-custom-pages.yaml | 45 --- ...google-sign-in-missing-credentials.spec.ts | 9 + .../tests/expo-native/preflight.setup.ts | 15 + .../tests/expo-native/run-android-flows.sh | 28 -- integration/tests/expo-native/run-flows.sh | 114 ------- .../session-persists-across-restart.spec.ts | 17 ++ integration/tests/expo-native/sign-in.spec.ts | 12 + .../user-button-sign-out-re-sign-in.spec.ts | 30 ++ .../user-profile-custom-pages.spec.ts | 31 ++ package.json | 6 +- packages/testing/package.json | 2 +- pnpm-lock.yaml | 279 +++++++++++++++--- pnpm-workspace.yaml | 6 + 34 files changed, 786 insertions(+), 659 deletions(-) create mode 100644 .changeset/expo-native-e2e-touchpress.md create mode 100644 integration/playwright.expo-native.config.ts create mode 100644 integration/tests/expo-native/README.md create mode 100644 integration/tests/expo-native/authview-detach-reattach.spec.ts delete mode 100644 integration/tests/expo-native/config.yaml create mode 100644 integration/tests/expo-native/embedded-profile-host-back.spec.ts create mode 100644 integration/tests/expo-native/flows.ts delete mode 100644 integration/tests/expo-native/flows/authview-detach-reattach.yaml delete mode 100644 integration/tests/expo-native/flows/embedded-profile-host-back.yaml delete mode 100644 integration/tests/expo-native/flows/google-sign-in-missing-credentials.yaml delete mode 100644 integration/tests/expo-native/flows/session-persists-across-restart.yaml delete mode 100644 integration/tests/expo-native/flows/sign-in.yaml delete mode 100644 integration/tests/expo-native/flows/subflows/_warmup.yaml delete mode 100644 integration/tests/expo-native/flows/subflows/assert-signed-in.yaml delete mode 100644 integration/tests/expo-native/flows/subflows/assert-signed-out.yaml delete mode 100644 integration/tests/expo-native/flows/subflows/open-app.yaml delete mode 100644 integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml delete mode 100644 integration/tests/expo-native/flows/user-button-sign-out-re-sign-in.yaml delete mode 100644 integration/tests/expo-native/flows/user-profile-custom-pages.yaml create mode 100644 integration/tests/expo-native/google-sign-in-missing-credentials.spec.ts create mode 100644 integration/tests/expo-native/preflight.setup.ts delete mode 100755 integration/tests/expo-native/run-android-flows.sh delete mode 100755 integration/tests/expo-native/run-flows.sh create mode 100644 integration/tests/expo-native/session-persists-across-restart.spec.ts create mode 100644 integration/tests/expo-native/sign-in.spec.ts create mode 100644 integration/tests/expo-native/user-button-sign-out-re-sign-in.spec.ts create mode 100644 integration/tests/expo-native/user-profile-custom-pages.spec.ts diff --git a/.changeset/expo-native-e2e-touchpress.md b/.changeset/expo-native-e2e-touchpress.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-native-e2e-touchpress.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/expo-native-build.yml b/.github/workflows/expo-native-build.yml index ce6a95119b6..404fe883311 100644 --- a/.github/workflows/expo-native-build.yml +++ b/.github/workflows/expo-native-build.yml @@ -7,6 +7,7 @@ on: - main paths: - '.github/workflows/expo-native-build.yml' + - 'integration/playwright.expo-native.config.ts' - 'integration/templates/expo-native/**' - 'integration/tests/expo-native/**' - 'packages/expo/**' @@ -27,7 +28,9 @@ env: SDK_PACK_DIR: /tmp/clerk-expo-pack E2E_INSTANCE_NAME: clerkstage-with-native-components BAPI_URL: https://api.clerkstage.dev - MAESTRO_VERSION: '2.8.0' + # Only keys the runner cache. Keep it equal to the agent-device version + # pinned in the root package.json, or the cache silently stops hitting. + AGENT_DEVICE_VERSION: '0.20.10' jobs: native-build: @@ -103,16 +106,18 @@ jobs: # Bump the version when native build commands change. key: expo-native-build-v1-${{ runner.os }}-${{ matrix.expo-sdk }}-${{ matrix.platform }}-${{ matrix.run-e2e == true && 'e2e' || 'build' }}-${{ env.E2E_INSTANCE_NAME }}-${{ steps.native-build-key.outputs.hash }} - - if: steps.native-build-cache.outputs.cache-hit != 'true' + - if: steps.native-build-cache.outputs.cache-hit != 'true' || matrix.run-e2e == true uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 - - if: steps.native-build-cache.outputs.cache-hit != 'true' + - if: steps.native-build-cache.outputs.cache-hit != 'true' || matrix.run-e2e == true uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24.15.0 cache: pnpm + # An e2e job installs even on a native-build cache hit: the suite runs on + # Playwright and touchpress out of the workspace's own node_modules. - name: Install monorepo dependencies - if: steps.native-build-cache.outputs.cache-hit != 'true' + if: steps.native-build-cache.outputs.cache-hit != 'true' || matrix.run-e2e == true run: pnpm install --frozen-lockfile - name: Build and pack Clerk packages @@ -137,8 +142,8 @@ jobs: GOOGLE_SIGNIN_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-google-signin-*.tgz)" pnpm add "$SDK_TARBALL" "$GOOGLE_SIGNIN_TARBALL" -w # expo-dev-client makes even release builds boot into the dev - # launcher (unreachable Metro in CI), which stalls every Maestro - # flow on a blank screen. Skip it on e2e jobs only. + # launcher (unreachable Metro in CI), which stalls every e2e spec on + # a blank screen. Skip it on e2e jobs only. DEV_CLIENT="expo-dev-client" if [ "$RUN_E2E" = "true" ]; then DEV_CLIENT=""; fi pnpm expo install expo-auth-session expo-constants expo-crypto $DEV_CLIENT expo-secure-store expo-web-browser @@ -223,32 +228,21 @@ jobs: path: ${{ steps.native-build-key.outputs.artifact }} key: ${{ steps.native-build-cache.outputs.cache-primary-key }} - - name: Cache maestro CLI - if: steps.keys.outputs.pk != '' + - name: Resolve Xcode version + id: xcode + if: matrix.platform == 'ios' && steps.keys.outputs.pk != '' + run: echo "version=$(xcodebuild -version | head -1 | tr ' ' '-')" >> "$GITHUB_OUTPUT" + + # agent-device builds an XCTest runner the first time it drives a + # simulator, which costs minutes. Exact key on purpose: agent-device + # advises against restore-key fallbacks here, and prepare recovers a + # stale one anyway. + - name: Cache agent-device Apple runner + if: matrix.platform == 'ios' && steps.keys.outputs.pk != '' uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: - path: ~/.maestro - key: maestro-${{ runner.os }}-${{ env.MAESTRO_VERSION }} - - - name: Install maestro CLI - if: steps.keys.outputs.pk != '' - run: | - set -o pipefail - if [ -x "$HOME/.maestro/bin/maestro" ]; then - echo "Using cached Maestro" - else - installed=0 - for i in 1 2 3; do - if curl -fLs --retry 3 --retry-delay 5 "https://get.maestro.mobile.dev" | MAESTRO_VERSION="$MAESTRO_VERSION" bash; then - if [ -x "$HOME/.maestro/bin/maestro" ]; then installed=1; break; fi - fi - echo "Maestro install attempt $i failed (or binary missing); retrying" - sleep 5 - done - [ "$installed" = 1 ] || { echo "::error::Maestro install failed after 3 attempts"; exit 1; } - fi - echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" - "$HOME/.maestro/bin/maestro" --version + path: ~/.agent-device/apple-runner/derived + key: agent-device-runner-${{ runner.os }}-${{ env.AGENT_DEVICE_VERSION }}-${{ steps.xcode.outputs.version }} - name: Boot iOS simulator id: sim @@ -286,7 +280,7 @@ jobs: echo "user_id=$user_id" } >> "$GITHUB_OUTPUT" - - name: Run iOS e2e (Maestro) + - name: Run iOS e2e id: run_e2e_ios if: matrix.platform == 'ios' && steps.user.outputs.user_id != '' working-directory: ${{ env.FIXTURE_DIR }} @@ -305,22 +299,28 @@ jobs: xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCorrection -bool NO || true xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.Prediction -bool NO || true # The one-time keyboard tutorial sheets carry their own Continue - # button, which can hijack taps on the AuthView's Continue. + # button, which makes the AuthView's Continue ambiguous. for key in DidShowContinuousPathIntroduction DidShowGestureKeyboardIntroduction KeyboardDidShowProductivityTutorial UIKeyboardDidShowInternationalInfoIntroduction; do xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.preferences "$key" -bool YES || true done xcrun simctl install "$SIM_UDID" ios/build/Build/Products/Release-iphonesimulator/ClerkExpoNativeBuildFixture.app # Stream the app's console output into the debug artifact so a hang has # actionable evidence (keychain/network errors) instead of just screenshots. - mkdir -p "$RUNNER_TEMP/maestro-debug" + mkdir -p "$RUNNER_TEMP/e2e-debug" xcrun simctl spawn "$SIM_UDID" log stream --style compact \ --predicate 'processImagePath CONTAINS "ClerkExpoNativeBuildFixture"' \ - > "$RUNNER_TEMP/maestro-debug/sim-console.log" 2>&1 & + > "$RUNNER_TEMP/e2e-debug/sim-console.log" 2>&1 & LOG_PID=$! - cd "$GITHUB_WORKSPACE/$E2E_DIR" + cd "$GITHUB_WORKSPACE" + # On a native-build cache hit no xcodebuild has run, so without this + # the first snapshot pays for the cold XCTest runner build and the + # launch times out. This is the only thing the repo still needs + # agent-device itself for; the specs reach the device through + # touchpress. Pinned to touchpress's own version, because a CLI at a + # different version replaces the daemon and drops its sessions. + ./node_modules/.bin/agent-device prepare ios-runner --platform ios --udid "$SIM_UDID" --timeout 600000 rc=0 - MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-flows.sh \ - xcrun simctl terminate "$SIM_UDID" com.clerk.exponativebuildfixture || rc=$? + pnpm test:integration:expo-native --project=ios || rc=$? kill "$LOG_PID" 2>/dev/null || true exit "$rc" @@ -353,7 +353,7 @@ jobs: disable-animations: false script: echo "Generated AVD snapshot for caching." - - name: Run Android e2e (Maestro) + - name: Run Android e2e id: run_e2e_android if: matrix.platform == 'android' && steps.user.outputs.user_id != '' uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2 @@ -370,9 +370,11 @@ jobs: # The action runs each script line in a separate sh -c; the folded # scalar (>-) plus && keeps everything in one shell invocation. script: >- - cd integration/tests/expo-native && - MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-android-flows.sh - "$GITHUB_WORKSPACE/$FIXTURE_DIR/android/app/build/outputs/apk/release/app-release.apk" + mkdir -p "$RUNNER_TEMP/e2e-debug" && + adb install -r "$GITHUB_WORKSPACE/$FIXTURE_DIR/android/app/build/outputs/apk/release/app-release.apk" && + adb logcat -c && + (adb logcat -v threadtime > "$RUNNER_TEMP/e2e-debug/android-logcat.log" 2>&1 &) && + pnpm test:integration:expo-native --project=android # Runs before the artifact upload so the credentials are already dead # by the time the debug files become downloadable. @@ -385,26 +387,30 @@ jobs: curl -fsS -X DELETE "$BAPI_URL/v1/users/$USER_ID" \ -H "Authorization: Bearer $CLERK_SECRET_KEY" || true - # Test reports record flow env (and typed input) in plaintext; - # add-mask only covers step logs, not artifact contents. - - name: Scrub test credentials from Maestro debug output + # A driver error can quote the text it was handed, and add-mask only + # covers step logs, not artifact contents. + - name: Scrub test credentials from the e2e report if: always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure') env: CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} run: | - [ -d "$RUNNER_TEMP/maestro-debug" ] || exit 0 [ -n "$CLERK_TEST_PASSWORD" ] || exit 0 + mkdir -p "$RUNNER_TEMP/e2e-debug" + for dir in "$E2E_DIR/playwright-report" "$E2E_DIR/test-results"; do + [ -d "$dir" ] || continue + cp -R "$dir" "$RUNNER_TEMP/e2e-debug/" + done # shellcheck disable=SC2016 - find "$RUNNER_TEMP/maestro-debug" -type f \ - \( -name '*.html' -o -name '*.json' -o -name '*.log' -o -name '*.txt' -o -name '*.xml' -o -name '*.yaml' \) \ + find "$RUNNER_TEMP/e2e-debug" -type f \ + \( -name '*.html' -o -name '*.json' -o -name '*.jsonl' -o -name '*.md' -o -name '*.log' -o -name '*.txt' -o -name '*.xml' -o -name '*.yaml' -o -name '*.zip' \) \ -exec perl -pi -e 's/\Q$ENV{CLERK_TEST_PASSWORD}\E/[REDACTED]/g' {} + - - name: Upload Maestro artifacts on e2e failure + - name: Upload e2e artifacts on failure if: always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure') uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: maestro-${{ matrix.platform }} - path: ${{ runner.temp }}/maestro-debug + name: expo-native-e2e-${{ matrix.platform }} + path: ${{ runner.temp }}/e2e-debug retention-days: 7 - name: Report e2e outcome @@ -415,7 +421,7 @@ jobs: run: | outcome="$IOS_OUTCOME" [ "$outcome" = "skipped" ] && outcome="$ANDROID_OUTCOME" - echo "## Maestro e2e (${{ matrix.platform }}): $outcome" >> "$GITHUB_STEP_SUMMARY" + echo "## Expo native e2e (${{ matrix.platform }}): $outcome" >> "$GITHUB_STEP_SUMMARY" if [ "$outcome" = "failure" ]; then - echo "::error::Maestro e2e failed. See the maestro-${{ matrix.platform }} artifact." + echo "::error::Expo native e2e failed. See the expo-native-e2e-${{ matrix.platform }} artifact." fi diff --git a/integration/playwright.config.ts b/integration/playwright.config.ts index 0b09ed58937..a3506409af4 100644 --- a/integration/playwright.config.ts +++ b/integration/playwright.config.ts @@ -9,6 +9,9 @@ config({ path: path.resolve(import.meta.dirname, '.env.local') }); export const common: PlaywrightTestConfig = { testDir: './tests', + // Device specs, run by playwright.expo-native.config.ts against a booted + // simulator or emulator. Nothing here can drive one. + testIgnore: '**/expo-native/**', snapshotDir: './tests/snapshots', fullyParallel: true, forbidOnly: !!process.env.CI, diff --git a/integration/playwright.expo-native.config.ts b/integration/playwright.expo-native.config.ts new file mode 100644 index 00000000000..53e4bd5a35f --- /dev/null +++ b/integration/playwright.expo-native.config.ts @@ -0,0 +1,71 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ + +import { defineConfig } from '@playwright/test'; +import type { TouchpressOptions } from 'touchpress'; + +const deviceName = process.env.E2E_DEVICE_NAME; + +export default defineConfig({ + testDir: './tests/expo-native', + forbidOnly: !!process.env.CI, + // A spec that needs its retry every run is a bug, not a flake. One absorbs + // emulator and simulator noise, and no more. + retries: process.env.CI ? 1 : 0, + // One device, so one worker. + workers: 1, + // The heaviest spec signs in twice through the native AuthView. + timeout: 300_000, + expect: { timeout: 15_000 }, + reporter: process.env.CI + ? [['list'], ['html', { open: 'never', outputFolder: './tests/expo-native/playwright-report' }]] + : 'list', + outputDir: './tests/expo-native/test-results', + + use: { + app: 'com.clerk.exponativebuildfixture', + // Matches 'signed in' and 'signed out' but not 'loading', so the gate holds + // until clerk-js has finished initialising rather than until the element + // merely exists. That is what the old warmup flow bought: on a cold CI + // emulator clerk-js can take past a minute, and waiting for it here charges + // the time to launchTimeout instead of to the first assertion that runs. + readyWhen: { text: 'signed' }, + actionTimeout: 20_000, + // Every spec clears state and relaunches itself, and that has to happen + // inside the test rather than in the fixture that would precede it. + // The per-test relaunch would only add a launch nothing reads. + relaunch: 'per-worker', + // CI boots a fresh device per job; a claim left by a killed run must not + // fail the next one. + onDeviceInUse: 'reclaim', + // A cold Release build on a CI emulator can take a minute to first paint. + launchTimeout: 120_000, + // Unset means the one booted device, which is what both CI jobs give us and + // what avoids agent-device's AVD-name-with-spaces translation entirely. + deviceName, + }, + + projects: [ + { + name: 'setup-ios', + testMatch: /preflight\.setup\.ts/, + use: { platform: 'ios' }, + }, + { + name: 'setup-android', + testMatch: /preflight\.setup\.ts/, + use: { platform: 'android' }, + }, + { + name: 'ios', + dependencies: ['setup-ios'], + testIgnore: /preflight\.setup\.ts/, + use: { platform: 'ios' }, + }, + { + name: 'android', + dependencies: ['setup-android'], + testIgnore: /preflight\.setup\.ts/, + use: { platform: 'android' }, + }, + ], +}); diff --git a/integration/tests/expo-native/.gitignore b/integration/tests/expo-native/.gitignore index e33609d251c..aaa9103e44a 100644 --- a/integration/tests/expo-native/.gitignore +++ b/integration/tests/expo-native/.gitignore @@ -1 +1,2 @@ -*.png +test-results/ +playwright-report/ diff --git a/integration/tests/expo-native/README.md b/integration/tests/expo-native/README.md new file mode 100644 index 00000000000..0f3f7219b50 --- /dev/null +++ b/integration/tests/expo-native/README.md @@ -0,0 +1,64 @@ +# Expo native e2e + +Drives the `@clerk/expo` native components (`AuthView`, `UserButton`, +`UserProfileView`) on a real simulator or emulator, through +[touchpress](https://github.com/wobsoriano/touchpress) on the Playwright runner. +Every spec asserts across the native-to-JS bridge: a sign-in completed by the +native SDK has to show up in `useAuth()`, and a JS sign-out has to reach the +native side. + +The app under test is the fixture in `integration/templates/expo-native`. + +## Running it + +You need a booted device, the fixture installed on it, and a test user. + +```sh +# iOS +xcrun simctl install booted \ + integration/templates/expo-native/ios/build/Build/Products/Release-iphonesimulator/ClerkExpoNativeBuildFixture.app + +# Android +adb install -r integration/templates/expo-native/android/app/build/outputs/apk/release/app-release.apk +``` + +```sh +export CLERK_TEST_EMAIL=... CLERK_TEST_PASSWORD=... +pnpm test:integration:expo-native --project=ios +pnpm test:integration:expo-native --project=android +``` + +One booted device per platform. The config names no device, so it takes the one +that is booted; set `E2E_DEVICE_NAME` if you keep several running. `preflight` +fails in about a second when nothing is booted, rather than once per spec. + +## Writing a spec + +`flows.ts` holds what every spec shares: `openApp`, `signInWithEmailPassword`, +`expectSignedIn`, `expectSignedOut`, and `tapControl`. Import `test` and `expect` +from there rather than from `touchpress`, so the spec gets the `control` fixture. + +Selectors are English text and accessibility labels, because clerk-android ships +no test identifiers and both native SDKs localize every string. The devices have +to run the `en` locale. + +Two rules the tree enforces, both explained where they are implemented: + +- Match whole strings on anything near an icon or a heading. `getByText` is a + substring match, and `'Security'` also finds the image labelled + `icon-security`. +- Tap Clerk's own buttons through `tapControl`, which is a whole-string text tap. + Jetpack Compose puts the label on a container beside an unlabeled button, and + touchpress retargets onto it. The report shows that as `retarget to @eNN`. + +Clearing state, going back, and typing into a focused field are `device` +methods. `clearState` relaunches and waits for the ready gate itself, and +`clearKeychain` is separate because the simulator keychain belongs to every app +on it, so `openApp` calls it first. + +## When a spec fails + +The failure message carries the locator, what was expected, the names closest to +it on screen, and the whole accessibility tree. Read that before changing a +selector, and never add a sleep. CI uploads the HTML report, which carries a +screenshot and a tree listing per failed test, as `expo-native-e2e-`. diff --git a/integration/tests/expo-native/authview-detach-reattach.spec.ts b/integration/tests/expo-native/authview-detach-reattach.spec.ts new file mode 100644 index 00000000000..914003ec935 --- /dev/null +++ b/integration/tests/expo-native/authview-detach-reattach.spec.ts @@ -0,0 +1,32 @@ +import { expect, expectSignedIn, expectSignedOut, openApp, signInWithEmailPassword, test } from './flows'; + +// Dismiss the native AuthView mid-flow and reopen it: the native view must +// survive detach/reattach and still complete a sign-in afterwards. Dismissal is +// the platform-native gesture: Android's system back button (doubling as the +// back-dismisses-AuthView regression), iOS's sheet Close control (clerk-ios +// exposes the stable clerk.dismissButton identifier). +// Also asserts the custom `logo` React element is rehosted into the native logo +// slot, both on first mount and after reattach. +test('AuthView survives detach and reattach', async ({ device, platform }) => { + await openApp(device); + await device.getByTestId('open-auth-view-button').tap(); + await expect(device.getByText(/Welcome! Sign in to continue\.?/)).toBeVisible({ timeout: 25_000 }); + // The rehosted logo is sized on the first React Native layout pass, so it can + // lag the welcome copy by a frame or two. + await expect(device.getByText('E2E Custom Logo')).toBeVisible({ timeout: 10_000 }); + + if (platform === 'android') { + await device.goBack(); + } else { + await device.getByTestId('clerk.dismissButton').tap(); + } + + await expect(device.getByTestId('open-auth-view-button')).toBeVisible({ timeout: 15_000 }); + await device.getByTestId('open-auth-view-button').tap(); + await expect(device.getByText('E2E Custom Logo')).toBeVisible({ timeout: 15_000 }); + + await signInWithEmailPassword(device, platform); + await expectSignedIn(device); + await device.getByTestId('sign-out-button').tap(); + await expectSignedOut(device); +}); diff --git a/integration/tests/expo-native/config.yaml b/integration/tests/expo-native/config.yaml deleted file mode 100644 index 94e3659b694..00000000000 --- a/integration/tests/expo-native/config.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Maestro workspace config: https://docs.maestro.dev/reference/workspace-configuration -# Top-level files under flows/ are cross-platform tests; flows/subflows/ holds -# runFlow-only pieces that discovery never runs standalone. Selectors use -# English text/labels: clerk-android ships no testTag/resource-ids (clerk-ios -# does expose accessibility identifiers like clerk.auth.start.identifier, but -# shared flows can't rely on iOS-only ids), and both native SDKs localize all -# strings, so devices must run the en locale. -appId: com.clerk.exponativebuildfixture -flows: - - 'flows/*.yaml' -platform: - ios: - disableAnimations: true - android: - disableAnimations: true diff --git a/integration/tests/expo-native/embedded-profile-host-back.spec.ts b/integration/tests/expo-native/embedded-profile-host-back.spec.ts new file mode 100644 index 00000000000..b5b8a18def6 --- /dev/null +++ b/integration/tests/expo-native/embedded-profile-host-back.spec.ts @@ -0,0 +1,37 @@ +import { expect, expectSignedIn, expectSignedOut, openApp, signInWithEmailPassword, tapControl, test } from './flows'; + +// Embedded UserProfileView (onHostBack): internal navigation stays native, and +// the host-supplied root back button closes the screen from JS. +test('Embedded UserProfileView host back round trip', async ({ device, platform }) => { + await openApp(device); + await device.getByTestId('open-auth-view-button').tap(); + await signInWithEmailPassword(device, platform); + await expectSignedIn(device); + await device.getByTestId('open-embedded-profile-button').tap(); + + // The internal push differs per platform: clerk-ios pushes a Security screen + // from the profile root, clerk-android uses tabs at the root and pushes the + // Manage account screen instead. The double-Back contract below is the same: + // both back buttons read 'Back' but only one exists at a time, so the first + // tap pops Clerk's internal stack and the second is the host chevron firing + // onHostBack. + const ios = platform === 'ios'; + const root = ios ? 'Security' : 'Edit profile'; + const row = ios ? 'Security' : 'Manage account'; + // A section heading the pushed screen has and the profile root does not. + const pushed = ios ? 'PASSWORD' : 'EMAIL ADDRESSES'; + + // Exact throughout: these rows sit next to an icon whose label repeats the row + // name ('Security' is a substring of 'icon-security'), and the pushed screen + // repeats its heading in the controls underneath it. + await expect(device.getByText(root, { exact: true })).toBeVisible({ timeout: 20_000 }); + await tapControl(device, row); + await expect(device.getByText(pushed, { exact: true })).toBeVisible({ timeout: 15_000 }); + await tapControl(device, 'Back'); + await expect(device.getByText(root, { exact: true })).toBeVisible({ timeout: 15_000 }); + + await tapControl(device, 'Back'); + await expect(device.getByTestId('open-embedded-profile-button')).toBeVisible({ timeout: 15_000 }); + await device.getByTestId('sign-out-button').tap(); + await expectSignedOut(device); +}); diff --git a/integration/tests/expo-native/flows.ts b/integration/tests/expo-native/flows.ts new file mode 100644 index 00000000000..ab4d14c56ce --- /dev/null +++ b/integration/tests/expo-native/flows.ts @@ -0,0 +1,160 @@ +/** + * The pieces every spec shares. + * + * clerk-android ships no testTag or resource-ids and both native SDKs localize + * every string, so shared selectors are English text and accessibility labels + * and the devices have to run the en locale. clerk-ios does expose accessibility + * identifiers, and the two fields below use them because a SwiftUI text field is + * otherwise unreachable until something focuses it. + */ + +import type { Device, Locator, Platform } from 'touchpress'; +import { expect, test } from 'touchpress'; + +export { expect, test }; + +function credentials(): { email: string; password: string } { + const email = process.env.CLERK_TEST_EMAIL; + const password = process.env.CLERK_TEST_PASSWORD; + if (!email || !password) { + throw new Error('CLERK_TEST_EMAIL and CLERK_TEST_PASSWORD are required'); + } + return { email, password }; +} + +/** Launch from a clean state and land signed out. */ +export async function openApp(device: Device): Promise { + await device.clearKeychain(); + await device.clearState(); + await expectSignedOut(device, 45_000); +} + +/** + * The JS layer observed the native sign-in, which is the bridge doing its job. + * + * The budget spans a sign-in round trip, the native-to-JS sync and the sheet + * dismissing. Until it dismisses, `auth-state` is not in the visible tree at + * all, so a short budget here reads as 'no node matched' rather than as the + * slow network it is. + */ +export async function expectSignedIn(device: Device, timeout = 60_000): Promise { + await expect(device.getByTestId('auth-state')).toHaveText('signed in', { exact: true, timeout }); + await expect(device.getByTestId('user-id')).toBeVisible(); +} + +export async function expectSignedOut(device: Device, timeout = 20_000): Promise { + await expect(device.getByTestId('auth-state')).toHaveText('signed out', { exact: true, timeout }); +} + +/** Enter email and password into the native AuthView and submit. */ +export async function signInWithEmailPassword(device: Device, platform: Platform | undefined): Promise { + const { email, password } = credentials(); + + await expect(device.getByText(/Welcome! Sign in to continue\.?/)).toBeVisible({ timeout: 25_000 }); + // The AuthView can render its welcome copy a beat before the email field. + // The placeholder varies by instance config (email-only vs email+username). + const identifierPlaceholder = device.getByText(/Enter your email( or username)?/); + await expect(identifierPlaceholder).toBeVisible({ timeout: 25_000 }); + + // On iOS the field itself is missing from the accessibility tree until it is + // focused: SwiftUI animates it to near-zero opacity rather than removing it, + // and an element that faint is left out. The placeholder is what carries the + // identifier meanwhile, so tapping it is what makes the field appear. + await identifierPlaceholder.tap(); + await identifierField(device, platform).fill(email); + await expect(device.getByText(email)).toBeVisible({ timeout: 10_000 }); + await tapControl(device, 'Continue'); + + // Which first factor comes next depends on the instance config and on the + // SDK: clerk-ios can go email-code-first where clerk-android goes + // password-first. Accept either screen. + await expect(device.getByText(/Enter your password|Check your email/)).toBeVisible({ timeout: 15_000 }); + + // Email-link-first instances cannot be automated, so switch strategy. Never + // taken on the CI instance, and kept for the dev instances that do offer it. + if (await isVisible(device.getByText('Open email app'))) { + await tapControl(device, 'Use another method'); + await tapControl(device, 'Sign in with your password'); + } + await submitEmailCode(device); + + if (await isVisible(device.getByText('Enter your password'))) { + await device.getByText('Enter your password').tap(); + await passwordField(device, platform).fill(password, { secret: true }); + await tapControl(device, 'Continue'); + // The screen stays stable while the request is in flight, so the settle + // after the tap can return before the next screen exists. Either outcome + // ends the wait, and neither one failing is itself a failure: the spec's + // own assertion is what decides. + await expect(device.getByText(/Check your email|signed in/)) + .toBeVisible({ timeout: 15_000 }) + .catch(() => undefined); + } + // Some instances ask for the email code after the password instead. + await submitEmailCode(device); + + // Android's Google Password Manager offers to save the password. + await dismissIfPresent(device.getByText(/Google Password Manager/), device.getByText(/Not now|Never/)); + // The iOS system Save Password sheet overlays the app. + await dismissIfPresent( + device.getByText(/Save Password|Strong Password|Use Strong Password|AutoFill Passwords/), + device.getByText(/Not Now|Never for This Website|Don.t Save/), + ); +} + +/** + * The documented test code for a `+clerk_test@` address. + * + * @see https://clerk.com/docs/testing/test-emails-and-phones + */ +async function submitEmailCode(device: Device): Promise { + if (!(await isVisible(device.getByText('Check your email')))) { + return; + } + // The code boxes carry no selector of their own. + await device.keyboard.type('424242'); +} + +/** + * clerk-ios puts its identifier on the HStack wrapping the field, and a SwiftUI + * identifier propagates to every element inside it, so the role is what + * separates the field from the label and the reveal button sharing that id. + * clerk-android exposes no identifiers at all, and each of these screens has + * exactly one EditText. + */ +function identifierField(device: Device, platform: Platform | undefined): Locator { + return platform === 'ios' + ? device.locator({ testId: { kind: 'exact', value: 'clerk.auth.start.identifier' }, role: 'text-field' }) + : device.getByRole('text-field'); +} + +function passwordField(device: Device, platform: Platform | undefined): Locator { + return platform === 'ios' + ? device.locator({ testId: { kind: 'exact', value: 'clerk.auth.signIn.password' }, role: 'secure-text-field' }) + : device.getByRole('text-field'); +} + +/** + * Tap a button in Clerk's own native UI by the string written on it. + * + * Whole-string: the Android sign-in screen alone offers 'Continue to Acme', + * 'Continue with Apple', 'Continue with GitHub' and 'Continue with Google' next + * to the button a substring match is looking for. + */ +export async function tapControl(device: Device, name: string): Promise { + await device.getByText(name, { exact: true }).tap(); +} + +/** Reads one screen and does not retry, so only use it to branch on. */ +async function isVisible(locator: Locator): Promise { + return (await locator.count()) > 0; +} + +async function dismissIfPresent(prompt: Locator, dismiss: Locator): Promise { + if (!(await isVisible(prompt))) { + return; + } + if (await isVisible(dismiss)) { + await dismiss.tap(); + } +} diff --git a/integration/tests/expo-native/flows/authview-detach-reattach.yaml b/integration/tests/expo-native/flows/authview-detach-reattach.yaml deleted file mode 100644 index ecb1c592d98..00000000000 --- a/integration/tests/expo-native/flows/authview-detach-reattach.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Dismiss the native AuthView mid-flow and reopen it: the native view must -# survive detach/reattach and still complete a sign-in afterwards. Dismissal -# is the platform-native gesture: Android's system back button (doubling as -# the back-dismisses-AuthView regression), iOS's sheet Close control -# (clerk-ios exposes the stable clerk.dismissButton identifier). -# Also asserts the custom `logo` React element is rehosted into the native -# logo slot with a nonzero size, both on first mount and after reattach. -appId: com.clerk.exponativebuildfixture -name: AuthView survives detach and reattach ---- -- runFlow: subflows/open-app.yaml -- tapOn: - id: 'open-auth-view-button' -- extendedWaitUntil: - visible: 'Welcome! Sign in to continue\.?' - timeout: 25000 -# Retrying wait (not assertVisible): the rehosted logo is sized on the first -# RN layout pass, so it can lag the welcome copy by a frame or two. -- extendedWaitUntil: - visible: 'E2E Custom Logo' - timeout: 10000 -- runFlow: - when: - platform: Android - commands: - - back -- runFlow: - when: - platform: iOS - commands: - - tapOn: - id: 'clerk.dismissButton' -- extendedWaitUntil: - visible: - id: 'open-auth-view-button' - timeout: 15000 -- tapOn: - id: 'open-auth-view-button' -- extendedWaitUntil: - visible: 'E2E Custom Logo' - timeout: 15000 -- runFlow: subflows/sign-in-email-password.yaml -- runFlow: subflows/assert-signed-in.yaml -- tapOn: - id: 'sign-out-button' -- runFlow: subflows/assert-signed-out.yaml diff --git a/integration/tests/expo-native/flows/embedded-profile-host-back.yaml b/integration/tests/expo-native/flows/embedded-profile-host-back.yaml deleted file mode 100644 index 360a99e8ed7..00000000000 --- a/integration/tests/expo-native/flows/embedded-profile-host-back.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# Embedded UserProfileView (onHostBack): internal navigation stays native, -# and the host-supplied root back button closes the screen from JS. -appId: com.clerk.exponativebuildfixture -name: Embedded UserProfileView host back round trip ---- -- runFlow: subflows/open-app.yaml -- tapOn: - id: 'open-auth-view-button' -- runFlow: subflows/sign-in-email-password.yaml -- runFlow: subflows/assert-signed-in.yaml -- tapOn: - id: 'open-embedded-profile-button' -# The internal push differs per platform: clerk-ios pushes a Security screen -# from the profile root, clerk-android uses tabs at the root and pushes the -# Manage account screen instead. The double-Back contract below is the same: -# both back buttons read 'Back' but only one exists at a time, so the first -# tap pops Clerk's internal stack and the second is the host chevron firing -# onHostBack. -- runFlow: - when: - platform: iOS - commands: - - extendedWaitUntil: - visible: 'Security' - timeout: 20000 - - tapOn: - text: 'Security' - - extendedWaitUntil: - visible: 'Password|Passkeys|Two-step verification|Active devices' - timeout: 15000 - - tapOn: 'Back' - - extendedWaitUntil: - visible: 'Security' - timeout: 15000 -- runFlow: - when: - platform: Android - commands: - - extendedWaitUntil: - visible: 'Edit profile' - timeout: 20000 - - tapOn: - text: 'Manage account' - - extendedWaitUntil: - visible: 'EMAIL ADDRESSES|Add email address' - timeout: 15000 - - tapOn: 'Back' - - extendedWaitUntil: - visible: 'Edit profile' - timeout: 15000 -- tapOn: 'Back' -- extendedWaitUntil: - visible: - id: 'open-embedded-profile-button' - timeout: 15000 -- tapOn: - id: 'sign-out-button' -- runFlow: subflows/assert-signed-out.yaml diff --git a/integration/tests/expo-native/flows/google-sign-in-missing-credentials.yaml b/integration/tests/expo-native/flows/google-sign-in-missing-credentials.yaml deleted file mode 100644 index 417319758f3..00000000000 --- a/integration/tests/expo-native/flows/google-sign-in-missing-credentials.yaml +++ /dev/null @@ -1,9 +0,0 @@ -appId: com.clerk.exponativebuildfixture -name: useSignInWithGoogle surfaces the missing-credentials error ---- -- runFlow: subflows/open-app.yaml -- tapOn: - id: 'google-sign-in-button' -- extendedWaitUntil: - visible: '.*Google Sign-In credentials not found.*' - timeout: 15000 diff --git a/integration/tests/expo-native/flows/session-persists-across-restart.yaml b/integration/tests/expo-native/flows/session-persists-across-restart.yaml deleted file mode 100644 index 95d8c900f6d..00000000000 --- a/integration/tests/expo-native/flows/session-persists-across-restart.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Sign in via the native AuthView, restart WITHOUT clearing state, and assert -# the session is restored from secure-store with no re-auth (bridge + token -# cache persistence). -appId: com.clerk.exponativebuildfixture -name: Session persists across app restart ---- -- runFlow: subflows/open-app.yaml -- tapOn: - id: 'open-auth-view-button' -- runFlow: subflows/sign-in-email-password.yaml -- runFlow: subflows/assert-signed-in.yaml -- stopApp -- launchApp -- extendedWaitUntil: - visible: 'signed in' - timeout: 45000 -- assertVisible: - id: 'user-id' -# Leave the app signed out for whichever flow runs next. -- tapOn: - id: 'sign-out-button' -- runFlow: subflows/assert-signed-out.yaml diff --git a/integration/tests/expo-native/flows/sign-in.yaml b/integration/tests/expo-native/flows/sign-in.yaml deleted file mode 100644 index 80645825691..00000000000 --- a/integration/tests/expo-native/flows/sign-in.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Native AuthView email+password sign-in, asserting the native->JS session -# sync, then JS-side sign-out asserting the reverse direction. -appId: com.clerk.exponativebuildfixture -name: Native AuthView sign-in syncs to JS ---- -- runFlow: subflows/open-app.yaml -- tapOn: - id: 'open-auth-view-button' -- runFlow: subflows/sign-in-email-password.yaml -- runFlow: subflows/assert-signed-in.yaml -- tapOn: - id: 'sign-out-button' -- runFlow: subflows/assert-signed-out.yaml diff --git a/integration/tests/expo-native/flows/subflows/_warmup.yaml b/integration/tests/expo-native/flows/subflows/_warmup.yaml deleted file mode 100644 index bde6f57e9f3..00000000000 --- a/integration/tests/expo-native/flows/subflows/_warmup.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Cold-boot warmup, not a test. Lets the JS bundle parse and the a11y tree -# populate once, so the first real flow doesn't eat the cold-start cost and -# flake its in-flow waits. Excluded from the per-flow loop (lives in subflows/). -appId: com.clerk.exponativebuildfixture ---- -- launchApp: - clearState: true - clearKeychain: true -- extendedWaitUntil: - visible: - id: 'auth-state' - timeout: 90000 diff --git a/integration/tests/expo-native/flows/subflows/assert-signed-in.yaml b/integration/tests/expo-native/flows/subflows/assert-signed-in.yaml deleted file mode 100644 index 65bcc55159d..00000000000 --- a/integration/tests/expo-native/flows/subflows/assert-signed-in.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Subflow: assert the JS layer observed the native sign-in (bridge sync). -# extendedWaitUntil retries until timeout; assertVisible does not. -appId: com.clerk.exponativebuildfixture ---- -- extendedWaitUntil: - visible: 'signed in' - timeout: 30000 -- assertVisible: - id: 'user-id' diff --git a/integration/tests/expo-native/flows/subflows/assert-signed-out.yaml b/integration/tests/expo-native/flows/subflows/assert-signed-out.yaml deleted file mode 100644 index 72986f7cae9..00000000000 --- a/integration/tests/expo-native/flows/subflows/assert-signed-out.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Subflow: assert the JS layer is signed out. -appId: com.clerk.exponativebuildfixture ---- -- extendedWaitUntil: - visible: 'signed out' - timeout: 20000 diff --git a/integration/tests/expo-native/flows/subflows/open-app.yaml b/integration/tests/expo-native/flows/subflows/open-app.yaml deleted file mode 100644 index 681f2d289c4..00000000000 --- a/integration/tests/expo-native/flows/subflows/open-app.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Subflow: launch from a clean state and land signed out. -appId: com.clerk.exponativebuildfixture ---- -- launchApp: - clearState: true - clearKeychain: true -- extendedWaitUntil: - visible: - id: 'auth-state' - timeout: 30000 -# Text-only matching from here on: iOS failed combined id+text filters on RN -# Text elements in CI while id-only and text-only both match. Also wait out -# clerk-js init ('loading') with a retrying wait; assertVisible never retries. -- extendedWaitUntil: - visible: 'signed (in|out)' - timeout: 45000 -- assertVisible: 'signed out' diff --git a/integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml b/integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml deleted file mode 100644 index 704f8b26f07..00000000000 --- a/integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml +++ /dev/null @@ -1,126 +0,0 @@ -# Subflow: enter email + password into the native AuthView and submit. -# Requires CLERK_TEST_EMAIL and CLERK_TEST_PASSWORD env vars. -appId: com.clerk.exponativebuildfixture ---- -- extendedWaitUntil: - visible: 'Welcome! Sign in to continue\.?' - timeout: 25000 -# The AuthView sometimes renders its welcome text a beat before the email -# field; don't tap until the field is actually present. The placeholder -# varies by instance config (email-only vs email+username). -- extendedWaitUntil: - visible: 'Enter your email( or username)?' - timeout: 25000 -# Clear any pre-populated identifier (Clerk persists the last-used one in -# secure-store, surviving clearState). On iOS eraseText is cursor-dependent, -# so Select-all via the edit menu when it shows (the menu is unreliable on a -# remounted AuthView). On Android eraseText clears the whole field, and the -# long-press can open Gboard's features panel over the screen instead of the -# edit menu, so skip the menu dance there. The retry gates on reaching the -# password step, so a mangled identifier gets a clean second attempt. -- retry: - maxRetries: 2 - commands: - - tapOn: - text: 'Enter your email( or username)?' - - waitForAnimationToEnd: - timeout: 2000 - # A remounted AuthView prefills the last-used identifier, which is the - # same email this flow signs in with, and clearing a prefilled field is - # cursor-position dependent on Android. Only clear and retype when the - # field does not already hold the right value. - - runFlow: - when: - notVisible: ${CLERK_TEST_EMAIL} - commands: - - runFlow: - when: - platform: iOS - commands: - - longPressOn: - text: 'Enter your email( or username)?' - - runFlow: - when: - visible: 'Select All' - commands: - - tapOn: - text: 'Select All' - - eraseText: 100 - - inputText: ${CLERK_TEST_EMAIL} - # Wait for the typed value to land before tapping: the tap can race the - # recomposition that enables the button, and this also catches a mangled - # identifier inside the retry instead of at the next screen's timeout. - - extendedWaitUntil: - visible: ${CLERK_TEST_EMAIL} - timeout: 5000 - - waitForAnimationToEnd: - timeout: 2000 - - tapOn: - text: 'Continue' - index: 0 - # Which first factor comes next depends on instance config AND SDK: - # clerk-ios can go email-code-first where clerk-android goes - # password-first. Accept either screen. - - extendedWaitUntil: - visible: 'Enter your password|Check your email' - timeout: 15000 -# Email-code-first: for +clerk_test@ emails the documented test code is -# 424242: https://clerk.com/docs/testing/test-emails-and-phones -- runFlow: - when: - visible: 'Check your email' - commands: - - inputText: '424242' - - waitForAnimationToEnd: - timeout: 5000 -- runFlow: - when: - visible: 'Enter your password' - commands: - - tapOn: - text: 'Enter your password' - - eraseText: 50 - - inputText: ${CLERK_TEST_PASSWORD} - # The password is masked so its value can't be asserted; settle for the - # screen going stable so the tap doesn't race the enabling recomposition. - - waitForAnimationToEnd: - timeout: 2000 - - tapOn: - text: 'Continue' - index: 0 - - waitForAnimationToEnd: - timeout: 5000 - # The screen stays stable while the sign-in request is in flight, so the - # animation wait alone can return before the next screen exists. Wait for - # either outcome so the conditional below sees the real screen. - - extendedWaitUntil: - visible: 'Check your email|signed in' - timeout: 15000 - optional: true -# Some instances ask for the email code after the password instead. -- runFlow: - when: - visible: 'Check your email' - commands: - - inputText: '424242' - - waitForAnimationToEnd: - timeout: 5000 -# Android Google Password Manager may prompt to save the password. -- runFlow: - when: - visible: '.*Google Password Manager.*' - commands: - - tapOn: - text: 'Not now|Never' - - waitForAnimationToEnd: - timeout: 2000 -# iOS system Save Password / iCloud Keychain prompt overlays the app. -- runFlow: - when: - visible: 'Save Password|Strong Password|Use Strong Password|AutoFill Passwords' - commands: - - tapOn: - text: 'Not Now|Never for This Website|Don.t Save' - optional: true - - waitForAnimationToEnd: - timeout: 2000 diff --git a/integration/tests/expo-native/flows/user-button-sign-out-re-sign-in.yaml b/integration/tests/expo-native/flows/user-button-sign-out-re-sign-in.yaml deleted file mode 100644 index eb1a2a12cb7..00000000000 --- a/integration/tests/expo-native/flows/user-button-sign-out-re-sign-in.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Native UserButton -> account sheet -> native sign-out, asserting the JS -# layer observes it (the reverse bridge direction from sign-in.yaml). -# clerk-android exposes no test identifiers (no testTag / resource-ids), so -# shared selectors are English text / accessibility labels. -# -# Ends with a second sign-in in the SAME app process (no restart, no -# clearState): a past regression had the second native sign-in complete -# natively while the JS SDK never observed it. Every other flow cold-launches -# with clearState, so this is the only place that path is exercised. -appId: com.clerk.exponativebuildfixture -name: UserButton native sign-out, then same-process re-sign-in ---- -- runFlow: subflows/open-app.yaml -- tapOn: - id: 'open-auth-view-button' -- runFlow: subflows/sign-in-email-password.yaml -- runFlow: subflows/assert-signed-in.yaml -# iOS labels the trigger "Open account"; Android "Open user profile". -- tapOn: - text: 'Open (account|user profile)' -- extendedWaitUntil: - visible: 'Manage account' - timeout: 15000 -- assertVisible: 'Sign out' -- tapOn: - text: 'Sign out' -- runFlow: subflows/assert-signed-out.yaml -# Second sign-in without restarting the app: the remounted AuthView must -# work and the JS layer must observe the new session. -- tapOn: - id: 'open-auth-view-button' -- runFlow: subflows/sign-in-email-password.yaml -- runFlow: subflows/assert-signed-in.yaml -- tapOn: - id: 'sign-out-button' -- runFlow: subflows/assert-signed-out.yaml diff --git a/integration/tests/expo-native/flows/user-profile-custom-pages.yaml b/integration/tests/expo-native/flows/user-profile-custom-pages.yaml deleted file mode 100644 index 0b408084b51..00000000000 --- a/integration/tests/expo-native/flows/user-profile-custom-pages.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# A custom page's React Native content is mounted as a child of the native host -# and rehosted into the destination its row pushes, then survives the trip back. -appId: com.clerk.exponativebuildfixture -name: UserProfileView renders a custom page ---- -- runFlow: subflows/open-app.yaml -- tapOn: - id: 'open-auth-view-button' -- runFlow: subflows/sign-in-email-password.yaml -- runFlow: subflows/assert-signed-in.yaml -- tapOn: - id: 'open-embedded-profile-button' -# Retrying wait: the row list lands a frame after the native profile paints. -- extendedWaitUntil: - visible: 'E2E Custom Page' - timeout: 20000 -- tapOn: - text: 'E2E Custom Page' -- extendedWaitUntil: - visible: 'Rehosted RN body' - timeout: 15000 -# The Android destination is a bare AndroidView with no back chrome, unlike the -# iOS page which clerk-ios pushes onto its own NavigationStack. -- runFlow: - when: - platform: iOS - commands: - - tapOn: 'Back' -- runFlow: - when: - platform: Android - commands: - - back -- extendedWaitUntil: - visible: 'E2E Custom Page' - timeout: 15000 -# The host chevron firing onHostBack, which reads 'Back' on both platforms. -- tapOn: 'Back' -- extendedWaitUntil: - visible: - id: 'open-embedded-profile-button' - timeout: 15000 -- tapOn: - id: 'sign-out-button' -- runFlow: subflows/assert-signed-out.yaml diff --git a/integration/tests/expo-native/google-sign-in-missing-credentials.spec.ts b/integration/tests/expo-native/google-sign-in-missing-credentials.spec.ts new file mode 100644 index 00000000000..27459aec627 --- /dev/null +++ b/integration/tests/expo-native/google-sign-in-missing-credentials.spec.ts @@ -0,0 +1,9 @@ +import { expect, openApp, test } from './flows'; + +test('useSignInWithGoogle surfaces the missing-credentials error', async ({ device }) => { + await openApp(device); + await device.getByTestId('google-sign-in-button').tap(); + await expect(device.getByTestId('google-result')).toHaveText('Google Sign-In credentials not found', { + timeout: 15_000, + }); +}); diff --git a/integration/tests/expo-native/preflight.setup.ts b/integration/tests/expo-native/preflight.setup.ts new file mode 100644 index 00000000000..21e1de0f46f --- /dev/null +++ b/integration/tests/expo-native/preflight.setup.ts @@ -0,0 +1,15 @@ +import { preflight, setupTest } from 'touchpress'; + +/** + * Fails once, in about a second, when the project's device is not booted, + * instead of once per spec at the end of the launch timeout. touchpress's own + * `test` cannot run this, because its automatic `device` fixture would open the + * session preflight exists to check for. + */ +setupTest('the project names a booted device', async ({ platform, app, readyWhen, deviceName, sessionPrefix }) => { + const report = await preflight({ platform, app, readyWhen, deviceName, sessionPrefix }); + if (report.ok) { + return; + } + throw new Error(report.problems.join('\n')); +}); diff --git a/integration/tests/expo-native/run-android-flows.sh b/integration/tests/expo-native/run-android-flows.sh deleted file mode 100755 index 29a8e165b84..00000000000 --- a/integration/tests/expo-native/run-android-flows.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" - -apk_path=${1:?APK path is required} -command -v adb >/dev/null 2>&1 || { - echo 'adb is required' - exit 1 -} - -adb install -r "$apk_path" - -logcat_pid= -stop_logcat() { - [ -n "$logcat_pid" ] || return 0 - kill "$logcat_pid" >/dev/null 2>&1 || true - wait "$logcat_pid" 2>/dev/null || true -} -trap stop_logcat EXIT - -if [ -n "${MAESTRO_DEBUG_OUTPUT:-}" ]; then - mkdir -p "$MAESTRO_DEBUG_OUTPUT" - adb logcat -c || true - adb logcat -v threadtime > "$MAESTRO_DEBUG_OUTPUT/android-logcat.log" 2>&1 & - logcat_pid=$! -fi - -./run-flows.sh adb shell am force-stop com.clerk.exponativebuildfixture diff --git a/integration/tests/expo-native/run-flows.sh b/integration/tests/expo-native/run-flows.sh deleted file mode 100755 index b3eb7b7f64c..00000000000 --- a/integration/tests/expo-native/run-flows.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# Runs every top-level Maestro flow (flows/*.yaml; flows/subflows/ are -# runFlow-only pieces) as one CLI invocation per flow, so a hang or crash in -# one flow can't poison the rest, with one clean-state retry per flow. -# Whole-flow retry can mask app instability (the Maestro docs discourage it), -# so it is capped at a single retry purely to absorb emulator/simulator flake. -# -# Usage: ./run-flows.sh [force-stop command...] -# CI iOS: ./run-flows.sh xcrun simctl terminate "$SIM_UDID" com.clerk.exponativebuildfixture -# CI Android: ./run-flows.sh adb shell am force-stop com.clerk.exponativebuildfixture -# Local: ./run-flows.sh -# -# Required env: CLERK_TEST_EMAIL, CLERK_TEST_PASSWORD -# Optional env: MAESTRO_DEBUG_OUTPUT (directory for CI debug artifacts) -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" - -# Runs the official maestro CLI. maestro-runner was tried and reverted: its -# drivers mangle typed text and resolve text selectors by substring, so -# tapOn 'Continue' hits the 'Continue to ' title instead of the button. -command -v maestro >/dev/null 2>&1 || { - echo "maestro is required: https://docs.maestro.dev" - exit 1 -} - -: "${CLERK_TEST_EMAIL:?CLERK_TEST_EMAIL is required}" -: "${CLERK_TEST_PASSWORD:?CLERK_TEST_PASSWORD is required}" - -force_stop() { if [ "$#" -gt 0 ]; then "$@" >/dev/null 2>&1 || true; fi; } - -run_flow() { - local output_name=$1 - shift - local output_root=${MAESTRO_DEBUG_OUTPUT:-${TMPDIR:-/tmp}/clerk-expo-maestro-runner} - - maestro test \ - --debug-output "$output_root/$output_name" \ - --flatten-debug-output \ - "$@" -} - -record_result() { - local flow=$1 - local result=$2 - local attempts=$3 - local duration=$4 - - echo "Flow $flow: $result after $attempts attempt(s) in ${duration}s" - if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - printf '| `%s` | %s | %s | %ss |\n' "$flow" "$result" "$attempts" "$duration" >> "$GITHUB_STEP_SUMMARY" - fi -} - -if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - { - echo '### Maestro flow timings' - echo '| Flow | Result | Attempts | Duration |' - echo '| --- | --- | ---: | ---: |' - } >> "$GITHUB_STEP_SUMMARY" -fi - -# Warm up the JS bundle and accessibility tree before running the flows. -warmup_started=$SECONDS -warmup_result=failed -for warmup_attempt in 1 2; do - if run_flow "warmup-attempt-$warmup_attempt" flows/subflows/_warmup.yaml; then - warmup_result=passed - break - fi - force_stop "$@" - if [ "$warmup_attempt" -eq 1 ]; then - echo "::warning::Warmup failed attempt 1, retrying after 10s..." - sleep 10 - fi -done -warmup_duration=$((SECONDS - warmup_started)) -record_result "_warmup" "$warmup_result" "$warmup_attempt" "$warmup_duration" -if [ "$warmup_result" != passed ]; then - echo "::error::Warmup failed after 2 attempts; aborting Maestro flows" - exit 1 -fi - -# Force-stop so the first launchApp clearState doesn't race the warm process. -force_stop "$@" - -# Every flows/*.yaml is a cross-platform test (platform differences live in -# per-step `when: platform:` conditionals); flows/subflows/ are runFlow-only. -status=0 -for flow in flows/*.yaml; do - [ -e "$flow" ] || continue - flow_started=$SECONDS - flow_result=failed - for attempt in 1 2; do - if run_flow "${flow##*/}-attempt-$attempt" \ - -e CLERK_TEST_EMAIL="$CLERK_TEST_EMAIL" \ - -e CLERK_TEST_PASSWORD="$CLERK_TEST_PASSWORD" \ - "$flow"; then - flow_result=passed - break - fi - if [ "$attempt" -eq 2 ]; then - echo "::error::Flow $flow failed after 2 attempts" - status=1 - force_stop "$@" - break - fi - echo "::warning::Flow $flow failed attempt $attempt, retrying after 10s..." - force_stop "$@" - sleep 10 - done - flow_duration=$((SECONDS - flow_started)) - record_result "$flow" "$flow_result" "$attempt" "$flow_duration" -done -exit $status diff --git a/integration/tests/expo-native/session-persists-across-restart.spec.ts b/integration/tests/expo-native/session-persists-across-restart.spec.ts new file mode 100644 index 00000000000..3a7e0cb6746 --- /dev/null +++ b/integration/tests/expo-native/session-persists-across-restart.spec.ts @@ -0,0 +1,17 @@ +import { expectSignedIn, expectSignedOut, openApp, signInWithEmailPassword, test } from './flows'; + +// Sign in via the native AuthView, restart WITHOUT clearing state, and assert +// the session is restored from secure-store with no re-auth (bridge + token +// cache persistence). +test('Session persists across app restart', async ({ device, platform }) => { + await openApp(device); + await device.getByTestId('open-auth-view-button').tap(); + await signInWithEmailPassword(device, platform); + await expectSignedIn(device); + + await device.relaunch(); + await expectSignedIn(device, 45_000); + + await device.getByTestId('sign-out-button').tap(); + await expectSignedOut(device); +}); diff --git a/integration/tests/expo-native/sign-in.spec.ts b/integration/tests/expo-native/sign-in.spec.ts new file mode 100644 index 00000000000..94d5ed45b63 --- /dev/null +++ b/integration/tests/expo-native/sign-in.spec.ts @@ -0,0 +1,12 @@ +import { expectSignedIn, expectSignedOut, openApp, signInWithEmailPassword, test } from './flows'; + +// Native AuthView email+password sign-in, asserting the native->JS session +// sync, then JS-side sign-out asserting the reverse direction. +test('Native AuthView sign-in syncs to JS', async ({ device, platform }) => { + await openApp(device); + await device.getByTestId('open-auth-view-button').tap(); + await signInWithEmailPassword(device, platform); + await expectSignedIn(device); + await device.getByTestId('sign-out-button').tap(); + await expectSignedOut(device); +}); diff --git a/integration/tests/expo-native/user-button-sign-out-re-sign-in.spec.ts b/integration/tests/expo-native/user-button-sign-out-re-sign-in.spec.ts new file mode 100644 index 00000000000..64e5466e03c --- /dev/null +++ b/integration/tests/expo-native/user-button-sign-out-re-sign-in.spec.ts @@ -0,0 +1,30 @@ +import { expect, expectSignedIn, expectSignedOut, openApp, signInWithEmailPassword, tapControl, test } from './flows'; + +// Native UserButton -> account sheet -> native sign-out, asserting the JS layer +// observes it (the reverse bridge direction from sign-in.spec.ts). +// +// Ends with a second sign-in in the SAME app process (no restart, no clean +// state): a past regression had the second native sign-in complete natively +// while the JS SDK never observed it. Every other spec starts from a cleared +// state, so this is the only place that path is exercised. +test('UserButton native sign-out, then same-process re-sign-in', async ({ device, platform }) => { + await openApp(device); + await device.getByTestId('open-auth-view-button').tap(); + await signInWithEmailPassword(device, platform); + await expectSignedIn(device); + + // iOS labels the trigger 'Open account'; Android 'Open user profile'. + await device.getByText(/Open (account|user profile)/).tap(); + await expect(device.getByText('Manage account')).toBeVisible({ timeout: 15_000 }); + await expect(device.getByText('Sign out')).toBeVisible(); + await tapControl(device, 'Sign out'); + await expectSignedOut(device); + + // Second sign-in without restarting the app: the remounted AuthView must work + // and the JS layer must observe the new session. + await device.getByTestId('open-auth-view-button').tap(); + await signInWithEmailPassword(device, platform); + await expectSignedIn(device); + await device.getByTestId('sign-out-button').tap(); + await expectSignedOut(device); +}); diff --git a/integration/tests/expo-native/user-profile-custom-pages.spec.ts b/integration/tests/expo-native/user-profile-custom-pages.spec.ts new file mode 100644 index 00000000000..5af7f3e37e3 --- /dev/null +++ b/integration/tests/expo-native/user-profile-custom-pages.spec.ts @@ -0,0 +1,31 @@ +import { expect, expectSignedIn, expectSignedOut, openApp, signInWithEmailPassword, tapControl, test } from './flows'; + +// A custom page's React Native content is mounted as a child of the native host +// and rehosted into the destination its row pushes, then survives the trip back. +test('UserProfileView renders a custom page', async ({ device, platform }) => { + await openApp(device); + await device.getByTestId('open-auth-view-button').tap(); + await signInWithEmailPassword(device, platform); + await expectSignedIn(device); + await device.getByTestId('open-embedded-profile-button').tap(); + + // The row list lands a frame after the native profile paints. + await expect(device.getByText('E2E Custom Page')).toBeVisible({ timeout: 20_000 }); + await tapControl(device, 'E2E Custom Page'); + await expect(device.getByText('Rehosted RN body')).toBeVisible({ timeout: 15_000 }); + + // The Android destination is a bare AndroidView with no back chrome, unlike + // the iOS page which clerk-ios pushes onto its own NavigationStack. + if (platform === 'android') { + await device.goBack(); + } else { + await tapControl(device, 'Back'); + } + await expect(device.getByText('E2E Custom Page')).toBeVisible({ timeout: 15_000 }); + + // The host chevron firing onHostBack, which reads 'Back' on both platforms. + await tapControl(device, 'Back'); + await expect(device.getByTestId('open-embedded-profile-button')).toBeVisible({ timeout: 15_000 }); + await device.getByTestId('sign-out-button').tap(); + await expectSignedOut(device); +}); diff --git a/package.json b/package.json index 77cccd05ecb..3086e81c255 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test:integration:custom": "pnpm test:integration:base --grep @custom", "test:integration:deployment:nextjs": "pnpm playwright test --config integration/playwright.deployments.config.ts", "test:integration:electron": "pnpm test:integration:base --grep @electron", - "test:integration:expo-native": "integration/tests/expo-native/run-flows.sh", + "test:integration:expo-native": "pnpm playwright test --config integration/playwright.expo-native.config.ts", "test:integration:expo-web:disabled": "E2E_APP_ID=expo.expo-web pnpm test:integration:base --grep @expo-web", "test:integration:express": "E2E_APP_ID=express.* pnpm test:integration:base --grep @express", "test:integration:fastify": "E2E_APP_ID=fastify.* pnpm test:integration:base --grep @fastify", @@ -87,7 +87,7 @@ "@eslint/js": "9.31.0", "@faker-js/faker": "^9.9.0", "@octokit/rest": "^20.1.2", - "@playwright/test": "^1.62.1", + "@playwright/test": "catalog:repo", "@stylexjs/eslint-plugin": "0.19.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", @@ -99,6 +99,7 @@ "@types/react-dom": "catalog:react", "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "5.0.0", + "agent-device": "0.20.10", "chalk": "4.1.2", "citty": "^0.2.2", "conventional-changelog-conventionalcommits": "^4.6.3", @@ -142,6 +143,7 @@ "rolldown": "catalog:repo", "statuses": "^1.5.0", "tinyglobby": "^0.2.17", + "touchpress": "0.2.0", "tree-kill": "^1.2.2", "tsdown": "catalog:repo", "tsup": "catalog:repo", diff --git a/packages/testing/package.json b/packages/testing/package.json index 1657761ac04..a2d802c6dc9 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -82,7 +82,7 @@ "dotenv": "17.2.2" }, "devDependencies": { - "@playwright/test": "^1.62.1", + "@playwright/test": "catalog:repo", "cypress": "^14.5.4" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14c47378629..92af30a23de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ catalogs: '@floating-ui/react': specifier: 0.27.20 version: 0.27.20 + '@playwright/test': + specifier: 1.63.0 + version: 1.63.0 '@swc/helpers': specifier: 0.5.21 version: 0.5.21 @@ -155,8 +158,8 @@ importers: specifier: ^20.1.2 version: 20.1.2 '@playwright/test': - specifier: ^1.62.1 - version: 1.62.1 + specifier: catalog:repo + version: 1.63.0 '@stylexjs/eslint-plugin': specifier: 0.19.0 version: 0.19.0 @@ -190,6 +193,9 @@ importers: '@vitest/coverage-v8': specifier: 5.0.0 version: 5.0.0(vitest@5.0.0) + agent-device: + specifier: 0.20.10 + version: 0.20.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) chalk: specifier: 4.1.2 version: 4.1.2 @@ -319,6 +325,9 @@ importers: tinyglobby: specifier: ^0.2.17 version: 0.2.17 + touchpress: + specifier: 0.2.0 + version: 0.2.0(@playwright/test@1.63.0)(bufferutil@4.1.0)(utf-8-validate@5.0.10) tree-kill: specifier: ^1.2.2 version: 1.2.2 @@ -854,7 +863,7 @@ importers: version: 2.13.6(@types/node@25.6.0)(typescript@6.0.3) next: specifier: '>=15.5.18' - version: 15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -888,7 +897,7 @@ importers: version: 2.1.0 next: specifier: 15.5.21 - version: 15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) packages/nuxt: dependencies: @@ -1075,7 +1084,7 @@ importers: version: 1.17.0(react@18.3.1) next: specifier: ^15.0.0 - version: 15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1194,8 +1203,8 @@ importers: version: 17.2.2 devDependencies: '@playwright/test': - specifier: ^1.62.1 - version: 1.62.1 + specifier: catalog:repo + version: 1.63.0 cypress: specifier: ^14.5.4 version: 14.5.4 @@ -3876,6 +3885,9 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@limrun/api@0.24.5': + resolution: {integrity: sha512-bgIs+KcP9xITj1v2gMe8wJrvqPakl1PlXtvxlO6Akci8ijU7V27RWQAq7JBaA89AzwUb5EvLc+nmAMMiKl/g1A==} + '@loaderkit/resolve@1.0.4': resolution: {integrity: sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==} @@ -4738,8 +4750,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.62.1': - resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} engines: {node: '>=20'} hasBin: true @@ -7016,6 +7028,16 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agent-device@0.20.10: + resolution: {integrity: sha512-baj3tQQ1JH/FbJ3x3FVmSn4nctOlFmdYsiz4LNZh3rEOj/8g3hkIq07lcUii07lMSQ7hCiY1m8+SyWPAX1ykow==} + engines: {node: '>=22.12'} + hasBin: true + peerDependencies: + ai: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + ai: + optional: true + agent-install@0.0.5: resolution: {integrity: sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==} hasBin: true @@ -7352,8 +7374,8 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} - b4a@1.7.3: - resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} peerDependencies: react-native-b4a: '*' peerDependenciesMeta: @@ -7427,6 +7449,35 @@ packages: bare-abort-controller: optional: true + bare-fs@4.8.1: + resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.2: + resolution: {integrity: sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==} + + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.4: + resolution: {integrity: sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==} + base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} @@ -9147,6 +9198,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-client@1.2.0: + resolution: {integrity: sha512-kDI75RSzO3TwyG/K9w1ap8XwqSPcwi6jaMkNulfVeZmSeUM49U8kUzk1s+vKNt0tGrXgK47i+620Yasn1ccFiw==} + engines: {node: '>=18.0.0'} + eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -9620,11 +9675,6 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -10206,8 +10256,8 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - ipaddr.js@2.2.0: - resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + ipaddr.js@2.5.0: + resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} engines: {node: '>= 10'} iron-webcrypto@1.2.1: @@ -12394,6 +12444,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pixelmatch@7.2.0: + resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==} + hasBin: true + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -12441,13 +12495,13 @@ packages: engines: {node: '>=18'} hasBin: true - playwright-core@1.62.1: - resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} engines: {node: '>=20'} hasBin: true - playwright@1.62.1: - resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} engines: {node: '>=20'} hasBin: true @@ -12463,6 +12517,10 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + portfinder@1.0.38: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} @@ -12856,6 +12914,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + publint@0.3.18: resolution: {integrity: sha512-JRJFeBTrfx4qLwEuGFPk+haJOJN97KnPuK01yj+4k/Wj5BgoOK5uNsivporiqBjk2JDaslg7qJOhGRnpltGeog==} engines: {node: '>=18'} @@ -13808,8 +13870,8 @@ packages: stream-json@1.9.1: resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} - streamx@2.23.0: - resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -14065,13 +14127,16 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tar-stream@3.1.7: - resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar-stream@3.2.1: + resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} tar@7.5.21: resolution: {integrity: sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==} engines: {node: '>=18'} + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -14196,6 +14261,16 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + touchpress@0.2.0: + resolution: {integrity: sha512-6s3LF0IHsEX094yti9NtBrN+bhdM/mjJsUXc5n5L9rrvgQLO6mCFy+mvkwVvqYFOhR9XkmnkLLbIyRFtobXYGQ==} + engines: {node: '>=22.12'} + peerDependencies: + '@playwright/test': '>=1.63' + ai: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + ai: + optional: true + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -14494,6 +14569,14 @@ packages: resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} engines: {node: '>=18.17'} + undici@7.24.7: + resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} + engines: {node: '>=20.18.1'} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + undici@8.10.0: resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} engines: {node: '>=22.19.0'} @@ -15474,6 +15557,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -18015,7 +18102,7 @@ snapshots: '@fastify/proxy-addr@5.1.0': dependencies: '@fastify/forwarded': 3.0.1 - ipaddr.js: 2.2.0 + ipaddr.js: 2.5.0 '@floating-ui/core@1.8.0': dependencies: @@ -18501,6 +18588,19 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@limrun/api@0.24.5(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + dependencies: + eventsource-client: 1.2.0 + https-proxy-agent: 7.0.6 + ignore: 7.0.6 + proxy-from-env: 2.1.0 + undici: 7.24.7 + ws: 8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@loaderkit/resolve@1.0.4': dependencies: '@braidai/lang': 1.1.2 @@ -19249,6 +19349,7 @@ snapshots: - '@vitejs/devtools-kit' - aws4fetch - bare-abort-controller + - bare-buffer - better-sqlite3 - bun-types-no-globals - db0 @@ -19597,9 +19698,9 @@ snapshots: '@pkgr/core@0.2.9': optional: true - '@playwright/test@1.62.1': + '@playwright/test@1.63.0': dependencies: - playwright: 1.62.1 + playwright: 1.63.0 '@polka/url@1.0.0-next.29': {} @@ -22521,6 +22622,22 @@ snapshots: agent-base@7.1.4: {} + agent-device@0.20.10(bufferutil@4.1.0)(utf-8-validate@5.0.10): + dependencies: + '@limrun/api': 0.24.5(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ipaddr.js: 2.5.0 + tar-stream: 3.2.1 + undici: 7.29.0 + yaml: 2.9.0 + yauzl: 3.4.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + agent-install@0.0.5: dependencies: '@iarna/toml': 2.2.5 @@ -22669,10 +22786,11 @@ snapshots: buffer-crc32: 1.0.0 readable-stream: 4.7.0 readdir-glob: 1.1.3 - tar-stream: 3.1.7 + tar-stream: 3.2.1 zip-stream: 6.0.1 transitivePeerDependencies: - bare-abort-controller + - bare-buffer - react-native-b4a are-docs-informative@0.0.2: {} @@ -22963,7 +23081,7 @@ snapshots: axobject-query@4.1.0: {} - b4a@1.7.3: {} + b4a@1.8.1: {} babel-dead-code-elimination@1.0.12: dependencies: @@ -23096,6 +23214,33 @@ snapshots: bare-events@2.8.2: {} + bare-fs@4.8.1: + dependencies: + bare-events: 2.8.2 + bare-path: 3.1.2 + bare-stream: 2.13.4(bare-events@2.8.2) + bare-url: 2.5.4 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.2: {} + + bare-stream@2.13.4(bare-events@2.8.2): + dependencies: + b4a: 1.8.1 + streamx: 2.28.1 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.5.4: + dependencies: + bare-path: 3.1.2 + base-64@1.0.0: {} base-x@3.0.11: @@ -25063,6 +25208,10 @@ snapshots: events@3.3.0: {} + eventsource-client@1.2.0: + dependencies: + eventsource-parser: 3.0.8 + eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -25763,9 +25912,6 @@ snapshots: fs.realpath@1.0.0: {} - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -26462,7 +26608,7 @@ snapshots: ipaddr.js@1.9.1: {} - ipaddr.js@2.2.0: {} + ipaddr.js@2.5.0: {} iron-webcrypto@1.2.1: {} @@ -28492,7 +28638,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.5.21(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 15.5.21 '@swc/helpers': 0.5.15 @@ -28511,7 +28657,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 15.5.21 '@next/swc-win32-x64-msvc': 15.5.21 '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.62.1 + '@playwright/test': 1.63.0 babel-plugin-react-compiler: 1.0.0 sharp: 0.34.5 transitivePeerDependencies: @@ -28613,6 +28759,7 @@ snapshots: - '@vercel/kv' - aws4fetch - bare-abort-controller + - bare-buffer - better-sqlite3 - bun-types-no-globals - drizzle-orm @@ -28853,6 +29000,7 @@ snapshots: - '@vue/compiler-sfc' - aws4fetch - bare-abort-controller + - bare-buffer - better-sqlite3 - bufferutil - bun-types-no-globals @@ -29375,6 +29523,10 @@ snapshots: pirates@4.0.7: {} + pixelmatch@7.2.0: + dependencies: + pngjs: 7.0.0 + pkce-challenge@5.0.1: {} pkg-dir@3.0.0: @@ -29416,13 +29568,11 @@ snapshots: pkglab-linux-arm64: 0.17.1 pkglab-linux-x64: 0.17.1 - playwright-core@1.62.1: {} + playwright-core@1.63.0: {} - playwright@1.62.1: + playwright@1.63.0: dependencies: - playwright-core: 1.62.1 - optionalDependencies: - fsevents: 2.3.2 + playwright-core: 1.63.0 plist@3.1.0: dependencies: @@ -29434,6 +29584,8 @@ snapshots: pngjs@5.0.0: {} + pngjs@7.0.0: {} + portfinder@1.0.38: dependencies: async: 3.2.6 @@ -29743,6 +29895,8 @@ snapshots: proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + publint@0.3.18: dependencies: '@publint/pack': 0.1.4 @@ -31027,7 +31181,7 @@ snapshots: dependencies: stream-chain: 2.2.5 - streamx@2.23.0: + streamx@2.28.1: dependencies: events-universal: 1.0.1 fast-fifo: 1.3.2 @@ -31330,13 +31484,15 @@ snapshots: tapable@2.3.3: {} - tar-stream@3.1.7: + tar-stream@3.2.1: dependencies: - b4a: 1.7.3 + b4a: 1.8.1 + bare-fs: 4.8.1 fast-fifo: 1.3.2 - streamx: 2.23.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller + - bare-buffer - react-native-b4a tar@7.5.21: @@ -31347,6 +31503,13 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + teex@1.0.1: + dependencies: + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + term-size@2.2.1: {} terminal-link@2.1.1: @@ -31385,7 +31548,7 @@ snapshots: text-decoder@1.2.3: dependencies: - b4a: 1.7.3 + b4a: 1.8.1 transitivePeerDependencies: - react-native-b4a @@ -31456,6 +31619,20 @@ snapshots: totalist@3.0.1: {} + touchpress@0.2.0(@playwright/test@1.63.0)(bufferutil@4.1.0)(utf-8-validate@5.0.10): + dependencies: + '@playwright/test': 1.63.0 + agent-device: 0.20.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) + pixelmatch: 7.2.0 + pngjs: 7.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -31747,6 +31924,10 @@ snapshots: undici@6.22.0: {} + undici@7.24.7: {} + + undici@7.29.0: {} + undici@8.10.0: {} unenv@2.0.0-rc.24: @@ -32812,6 +32993,10 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yauzl@3.4.0: + dependencies: + pend: 1.2.0 + yocto-queue@0.1.0: {} yocto-queue@1.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 334176d0815..dfee4acf040 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -15,6 +15,9 @@ catalogs: react-dom: 18.3.1 repo: '@floating-ui/react': 0.27.20 + # Root and @clerk/testing must agree. Two copies in one process is a hard + # "Requiring @playwright/test second time" failure in the integration suite. + '@playwright/test': 1.63.0 '@swc/helpers': 0.5.21 '@tanstack/query-core': ^5.101.4 core-js: 3.47.0 @@ -45,6 +48,9 @@ minimumReleaseAgeExclude: - '@clerk/*' - 'pkglab' - 'pkglab-*' + # Drives the expo-native device e2e. Published by a Clerk maintainer and + # still on 0.x, so releases land faster than the 48h window allows. + - 'touchpress' trustPolicy: no-downgrade trustPolicyExclude: