Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ jobs:
name: Shared checks
uses: ./.github/workflows/shared-checks.yml
with:
run_integration_tests: false
run_integration_tests: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.head_ref == 'sean/Explore-E2E-tests' }}
secrets: inherit
25 changes: 25 additions & 0 deletions .github/workflows/e2e-branch-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: E2E Branch Checks

on:
workflow_dispatch:
inputs:
e2e_key_name:
description: Named .keys.json publishable key used by E2EHost tests
required: true
type: choice
default: with-email-codes
options:
- with-email-codes
- with-session-tasks-setup-mfa

permissions:
contents: read

jobs:
checks:
name: Shared checks with E2E
uses: ./.github/workflows/shared-checks.yml
with:
run_integration_tests: true
e2e_key_name: ${{ inputs.e2e_key_name }}
secrets: inherit
98 changes: 97 additions & 1 deletion .github/workflows/shared-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ on:
required: false
type: boolean
default: false
e2e_key_name:
description: Named .keys.json publishable key used by E2EHost tests
required: false
type: string
default: with-email-codes
secrets:
CLERK_TEST_KEYS_JSON:
required: false
Expand Down Expand Up @@ -115,9 +120,100 @@ jobs:
- name: Run ClerkKitUI tests
run: make test-ui

test-e2e:
name: Run E2EHost tests on iOS
needs: format-and-lint
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Verify macOS and Xcode version
run: |
sw_vers
xcodebuild -version

- name: Cache Swift dependencies
uses: actions/cache@v4
with:
path: .build
key: swiftpm-e2e-${{ hashFiles('Package.resolved') }}
restore-keys: swiftpm-e2e-

- name: Skip E2E tests
if: ${{ !inputs.run_integration_tests }}
run: echo "E2EHost tests run only when run_integration_tests is true."

- name: Setup E2E test keys
if: inputs.run_integration_tests
env:
CLERK_TEST_KEYS_JSON: ${{ secrets.CLERK_TEST_KEYS_JSON }}
run: |
set +x
if [ -z "$CLERK_TEST_KEYS_JSON" ]; then
echo "❌ Error: CLERK_TEST_KEYS_JSON secret is not set"
echo " Please configure CLERK_TEST_KEYS_JSON secret in repository settings"
exit 1
fi

printf '%s\n' "$CLERK_TEST_KEYS_JSON" > .keys.json
chmod 600 .keys.json
echo "✅ Created .keys.json from CLERK_TEST_KEYS_JSON secret"

- name: Validate E2E test keys
if: inputs.run_integration_tests
env:
CLERK_E2E_KEY_NAME: ${{ inputs.e2e_key_name }}
run: |
set -euo pipefail

required_keys="$(printf '%s\n' \
"with-email-codes" \
"with-session-tasks-setup-mfa" \
"$CLERK_E2E_KEY_NAME" \
| sort -u)"

missing=0
while IFS= read -r key_name; do
if ! jq -e --arg key_name "$key_name" '.[$key_name].pk | strings | select(length > 0)' .keys.json > /dev/null; then
echo "::error::Missing $key_name.pk in CLERK_TEST_KEYS_JSON."
missing=1
fi
done <<< "$required_keys"

if [ "$missing" -ne 0 ]; then
echo "Add the missing key to 1Password, then sync GitHub with:"
echo " make sync-test-keys-to-github"
exit 1
fi

echo "✅ Required E2E test keys are configured"

- name: Run E2EHost tests
if: inputs.run_integration_tests
env:
CLERK_E2E_KEY_NAME: ${{ inputs.e2e_key_name }}
run: |
set -euo pipefail
make test-e2e

- name: Upload E2EHost result bundle
if: failure()
uses: actions/upload-artifact@v4
with:
name: E2EHost.xcresult
path: build/reports/E2EHost.xcresult
if-no-files-found: ignore

- name: Cleanup E2E test keys
if: always() && inputs.run_integration_tests
run: |
rm -f .keys.json
echo "✅ Cleaned up .keys.json"

build:
name: Build ${{ matrix.platform }}
needs: [test, test-ui]
needs: [test, test-ui, test-e2e]
runs-on: macos-latest
strategy:
matrix:
Expand Down
44 changes: 43 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ After running `make setup`, you're ready to start developing!
- `make check` - Run both format-check and lint (for CI)
- `make test` - Run `ClerkKitTests` on macOS
- `make test-ui` - Run `ClerkKitUITests` on iOS Simulator
- `make test-e2e` - Run E2EHost tests on iOS Simulator
- `make test-integration` - Run only integration tests (requires `.keys.json` file; Clerk employees only)
- `make fetch-test-keys` - Fetch integration test keys from 1Password (optional, for Clerk employees only; auto-installs CLI if needed)

Expand Down Expand Up @@ -120,7 +121,7 @@ SwiftLint checks for:

## Testing

This project uses **Swift Testing** (not XCTest) for all tests. Tests are organized into two categories:
This project uses **Swift Testing** for package unit and integration tests, and **XCTest/XCUITest** for app-level E2E UI automation. Tests are organized into three categories:

### Unit and UI Tests

Expand Down Expand Up @@ -191,6 +192,47 @@ Each test method must call `configureClerkForIntegrationTesting(keyName:)` at th
- Integration tests may be slower than unit tests due to real network calls
- Some tests may be flaky due to network conditions - consider retrying

### E2EHost Tests

E2E tests live in `Examples/E2EHost/` and run a dedicated SwiftUI test host app on an iOS Simulator with XCUITest. The host app exists only for release-gating E2E coverage, keeping product-facing examples such as Quickstart free of test-only controls and launch configuration. By default, these tests use the same Clerk test instance as the `with-email-codes` integration tests.

**Running E2E tests (Clerk employees only):**
```bash
make fetch-test-keys
make test-e2e
```

If CI is missing a named test key, add it to the 1Password item, then sync the GitHub Actions snapshot:
```bash
make sync-test-keys-to-github
```

You can also provide a key directly:
```bash
CLERK_E2E_PUBLISHABLE_KEY=pk_test_... make test-e2e
```

To run against a different named test instance from `.keys.json`:
```bash
CLERK_E2E_KEY_NAME=with-session-tasks-setup-mfa make test-e2e
```
If omitted, `CLERK_E2E_KEY_NAME` defaults to `with-email-codes`.
Session-task examples include `with-session-tasks`, `with-session-tasks-reset-password`, and `with-session-tasks-setup-mfa`.

To choose a specific simulator:
```bash
IOS_SIMULATOR_DESTINATION='platform=iOS Simulator,name=iPhone 16' make test-e2e
```

**Requirements:**
- Network access
- Valid publishable key in `.keys.json` for `CLERK_E2E_KEY_NAME` or `CLERK_E2E_PUBLISHABLE_KEY`
- iOS Simulator available through `xcrun simctl`

The test runner writes its result bundle to `build/reports/E2EHost.xcresult`. In CI, this bundle is uploaded on failure. AI tools may help draft page objects, test flows, and accessibility ID changes, but generated tests must use the approved accessibility identifiers and be reviewed like production code. Maestro can be useful for exploratory mobile QA, but XCUITest is the release-gating E2E layer.

E2E cleanup uses the normal host-level delete-account control when possible. If a failure leaves the test inside an auth sheet or pending session-task screen, teardown relaunches `E2EHost` with the same keychain service and an E2E-only cleanup-on-launch flag so the app can delete the restored user without exposing a visible cleanup button.

## Releasing (Maintainers)

SDK releases can be published through the **Release SDK** GitHub Actions workflow:
Expand Down
73 changes: 73 additions & 0 deletions Examples/E2EHost/E2EHost/E2EConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// E2EConfiguration.swift
// E2EHost
//

import ClerkKit
import ClerkKitUI
import Foundation

struct E2EConfiguration {
let publishableKey: String
let authMode: AuthView.Mode
let keychainService: String?
let cleanupOnLaunch: Bool

init(
publishableKey: String,
authMode: AuthView.Mode,
keychainService: String?,
cleanupOnLaunch: Bool
) {
self.publishableKey = publishableKey
self.authMode = authMode
self.keychainService = keychainService
self.cleanupOnLaunch = cleanupOnLaunch
}

init(processInfo: ProcessInfo = .processInfo) {
let environment = processInfo.environment

publishableKey = Self.normalized(environment["CLERK_PUBLISHABLE_KEY"])
?? Self.normalized(environment["CLERK_E2E_PUBLISHABLE_KEY"])
?? ""
authMode = Self.authMode(from: environment["CLERK_E2E_AUTH_MODE"])
keychainService = Self.normalized(environment["CLERK_E2E_KEYCHAIN_SERVICE"])
cleanupOnLaunch = environment["CLERK_E2E_CLEANUP_ON_LAUNCH"] == "1"
}

var clerkOptions: Clerk.Options {
guard let keychainService else {
return Clerk.Options()
}

return Clerk.Options(
keychainConfig: .init(service: keychainService)
)
}

private static func authMode(from value: String?) -> AuthView.Mode {
guard let value = normalized(value), let authMode = AuthView.Mode(rawValue: value) else {
return .signInOrUp
}

return authMode
}

private static func normalized(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}

return value
}
}

extension E2EConfiguration {
static let mock = E2EConfiguration(
publishableKey: "",
authMode: .signInOrUp,
keychainService: nil,
cleanupOnLaunch: false
)
}
28 changes: 28 additions & 0 deletions Examples/E2EHost/E2EHost/E2EHostApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// E2EHostApp.swift
// E2EHost
//

import ClerkKit
import ClerkKitUI
import SwiftUI

@main
struct E2EHostApp: App {
private let configuration = E2EConfiguration()

init() {
Clerk.configure(
publishableKey: configuration.publishableKey,
options: configuration.clerkOptions
)
}

var body: some Scene {
WindowGroup {
E2EHostView(configuration: configuration)
.prefetchClerkImages()
.environment(Clerk.shared)
}
}
}
Loading
Loading