Skip to content
Merged
141 changes: 64 additions & 77 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ on:
workflow_dispatch:
inputs:
e2e_branch:
description: "Branch of synonymdev/bitkit-e2e-tests to use"
description: "Branch of synonymdev/bitkit-e2e-tests to use (main | default-feature-branch | custom branch name)"
required: false
default: "ios-preparation"
# push:
# branches: [master]
# pull_request:
# branches: [master]
default: "default-feature-branch"
pull_request:

env:
TERM: xterm-256color
Expand All @@ -22,6 +19,7 @@ concurrency:

jobs:
build:
if: github.event.pull_request.draft == false
runs-on: [self-hosted, macOS]

steps:
Expand All @@ -37,36 +35,6 @@ jobs:
echo "Xcode Version:"
xcodebuild -version

- name: Setup iOS Simulator
run: |
# Set simulator name
SIMULATOR_NAME="iPhone 17"
echo "SIMULATOR_NAME=$SIMULATOR_NAME" >> $GITHUB_ENV

# Boot the iPhone 17 simulator if not already running
if ! xcrun simctl list devices | grep "iPhone 17" | grep -q "Booted"; then
echo "Booting $SIMULATOR_NAME..."
xcrun simctl boot "$SIMULATOR_NAME"

# Wait for simulator to boot
for i in {1..30}; do
if xcrun simctl list devices | grep "$SIMULATOR_NAME" | grep -q "Booted"; then
echo "$SIMULATOR_NAME is booted!"
break
fi
echo "Waiting for $SIMULATOR_NAME boot... ($i/30)"
sleep 5
done
else
echo "$SIMULATOR_NAME is already booted!"
fi

# Wait for simulator to be fully ready
sleep 15

# Launch simulator app
open -a Simulator

- name: Clean build environment
run: |
# Clean any existing build artifacts
Expand All @@ -85,18 +53,24 @@ jobs:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CHATWOOT_API: ${{ secrets.CHATWOOT_API }}
SIMULATOR_NAME: "iPhone 17"
OS_VERSION: "latest"
E2E: true
run: |
echo "=== Building iOS app ==="

# Ensure iOS Simulator platform is available
xcodebuild -downloadPlatform iOS || echo "iOS platform already installed"

# Build for iOS Simulator
echo "Using simulator: $SIMULATOR_NAME (iOS $OS_VERSION)"

if xcodebuild -showsdks | grep -q "iOS Simulator"; then
echo "✅ iOS Simulator platform already installed"
else
echo "⚙️ iOS Simulator platform not found — downloading..."
xcodebuild -downloadPlatform iOS
fi

xcodebuild -workspace Bitkit.xcodeproj/project.xcworkspace \
-scheme Bitkit \
-configuration Debug \
-destination "platform=iOS Simulator,name=$SIMULATOR_NAME" \
-destination "platform=iOS Simulator,name=$SIMULATOR_NAME,OS=$OS_VERSION" \
-derivedDataPath DerivedData \
-allowProvisioningUpdates \
build
Expand All @@ -113,9 +87,17 @@ jobs:
name: bitkit-e2e-ios_${{ github.run_number }}
path: e2e-app/

e2e-branch:
if: github.event.pull_request.draft == false
uses: synonymdev/bitkit-e2e-tests/.github/workflows/determine-e2e-branch.yml@main
with:
app_branch: ${{ github.head_ref || github.ref_name }}
e2e_branch_input: ${{ github.event.inputs.e2e_branch || 'default-feature-branch' }}

e2e-tests:
if: github.event.pull_request.draft == false
runs-on: [self-hosted, macOS]
needs: build
needs: [build, e2e-branch]

strategy:
fail-fast: false
Expand All @@ -132,15 +114,15 @@ jobs:
steps:
- name: Show selected E2E branch
env:
E2E_BRANCH: ${{ github.event.inputs.e2e_branch || 'ios-preparation' }}
E2E_BRANCH: ${{ needs.e2e-branch.outputs.branch }}
run: echo $E2E_BRANCH

- name: Clone E2E tests
uses: actions/checkout@v4
with:
repository: synonymdev/bitkit-e2e-tests
path: bitkit-e2e-tests
ref: ${{ github.event.inputs.e2e_branch || 'ios-preparation' }}
ref: ${{ needs.e2e-branch.outputs.branch }}

- name: Download iOS app
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -200,42 +182,47 @@ jobs:
done
echo "Electrum server is ready!"

- name: Setup iOS Simulator
run: |
# Boot iOS Simulator
xcrun simctl boot "iPhone 17" || true
xcrun simctl bootstatus "iPhone 17" -b

# Install the app
xcrun simctl install "iPhone 17" bitkit-e2e-tests/aut/bitkit.app
- name: Clear previous E2E artifacts
working-directory: bitkit-e2e-tests
run: |
rm -rf artifacts/
rm -rf /tmp/lock/

- name: Run E2E Tests (${{ matrix.shard.name }})
- name: Clear iOS Simulator environment
run: |
cd bitkit-e2e-tests

# Setup logging
LOGDIR="./artifacts"
mkdir -p "$LOGDIR"
LOGFILE="$LOGDIR/simulator.log"

# Start simulator logging
xcrun simctl spawn "iPhone 17" log stream --predicate 'process == "Bitkit"' --style compact > "$LOGFILE" &
LOG_PID=$!

# Setup port forwarding for regtest and LND
xcrun simctl spawn "iPhone 17" launchctl load -w /System/Library/LaunchDaemons/com.apple.usbmuxd.plist || true

# Cleanup function
cleanup() {
kill "$LOG_PID" 2>/dev/null || true
wait "$LOG_PID" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

# Pass everything through to WDIO/Mocha
npm run e2e:ios -- "$@"
echo "🔧 Shutting down all running iOS simulators..."
xcrun simctl shutdown all || true
xcrun simctl erase all || true
echo "🔧 Disabling iOS Simulator notifications..."
defaults write com.apple.iphonesimulator DisableAllNotifications -bool true

- name: Run E2E Tests 1 (${{ matrix.shard.name }})
continue-on-error: true
id: test1
working-directory: bitkit-e2e-tests
run: ./ci_run_ios.sh --mochaOpts.grep "${{ matrix.shard.grep }}"
env:
RECORD_VIDEO: true
ATTEMPT: 1

- name: Run E2E Tests 2 (${{ matrix.shard.name }})
continue-on-error: true
if: steps.test1.outcome != 'success'
id: test2
working-directory: bitkit-e2e-tests
run: ./ci_run_ios.sh --mochaOpts.grep "${{ matrix.shard.grep }}"
env:
RECORD_VIDEO: true
ATTEMPT: 2

- name: Run E2E Tests 3 (${{ matrix.shard.name }})
if: steps.test1.outcome != 'success' && steps.test2.outcome != 'success'
id: test3
working-directory: bitkit-e2e-tests
run: ./ci_run_ios.sh --mochaOpts.grep "${{ matrix.shard.grep }}"
env:
RECORD_VIDEO: true
ATTEMPT: 3

- name: Upload E2E Artifacts (${{ matrix.shard.name }})
if: failure()
Expand Down
14 changes: 14 additions & 0 deletions Bitkit/Components/DrawerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable {
return true
}
}

var accessibilityIdentifier: String {
switch self {
case .wallet: return "DrawerWallet"
case .activity: return "DrawerActivity"
case .contacts: return "DrawerContacts"
case .profile: return "DrawerProfile"
case .widgets: return "DrawerWidgets"
case .shop: return "DrawerShop"
case .settings: return "DrawerSettings"
case .appStatus: return "DrawerAppStatus"
}
}
}

struct DrawerView: View {
Expand Down Expand Up @@ -95,6 +108,7 @@ struct DrawerView: View {
}) {
menuItemContent(item: item)
}
.accessibilityIdentifier(item.accessibilityIdentifier)
}

Spacer()
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Components/EmptyStateView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ struct EmptyStateView: View {
.frame(width: 44, height: 44) // Increase hit area
}
.offset(x: 16, y: -16)
.accessibilityIdentifier("WalletOnboardingClose")

Spacer()
}
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Components/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct Header: View {
.frame(width: 24, height: 24)
.frame(width: 32, height: 32)
}
.accessibilityIdentifier("HeaderMenu")
}
}
.frame(height: 48)
Expand Down
29 changes: 29 additions & 0 deletions Bitkit/Components/Home/Suggestions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,33 @@ let cards: [SuggestionCardData] = [
),
]

extension SuggestionCardData {
var accessibilityId: String {
switch action {
case .backup:
return "back_up"
case .buyBitcoin:
return "buy"
case .invite:
return "invite"
case .profile:
return "profile"
case .quickpay:
return "quick_pay"
case .notifications:
return "notifications"
case .secure:
return "secure"
case .shop:
return "shop"
case .support:
return "support"
case .transferToSpending:
return "lightning"
}
}
}

struct Suggestions: View {
@EnvironmentObject var app: AppViewModel
@EnvironmentObject var navigation: NavigationViewModel
Expand Down Expand Up @@ -167,7 +194,9 @@ struct Suggestions: View {
data: card,
onDismiss: { dismissCard(card) }
)
.accessibilityIdentifier("Suggestion-\(card.accessibilityId)")
}
.accessibilityIdentifier("Suggestions")
.id("suggestions-\(filteredCards.count)-\(suggestionsManager.dismissedIds.count)")
.frame(height: cardSize)
.padding(.bottom, 16)
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Components/Home/SuggestionsCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ struct SuggestionCard: View {
.padding(8)
}
.padding(8)
.accessibilityIdentifier("SuggestionDismiss")
.accessibility(label: Text("Dismiss \(data.title)"))
}
}
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Components/NavigationBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ struct NavigationBar: View {
}
.frame(width: 32, height: 32)
.offset(x: 6)
.accessibilityIdentifier("HeaderMenu")
} else {
Spacer()
.frame(width: 24, height: 24)
Expand Down
4 changes: 4 additions & 0 deletions Bitkit/Components/QR.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ struct QR: View {
cachedContent = newContent
cachedImage = generateQRCode(from: newContent)
}
.accessibilityElement(children: .ignore)
.accessibilityIdentifier("QRCode")
.accessibilityLabel(content)
.accessibilityValue(content)
}

func generateQRCode(from string: String) -> UIImage {
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Components/SeedTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct SeedTextField: View {
.onSubmit {
focusedField = isLastField ? nil : index + 1
}
.accessibilityIdentifier("Word-\(index)")
}
.frame(minHeight: 46)
.padding(.horizontal, 16)
Expand Down
9 changes: 9 additions & 0 deletions Bitkit/Views/Backup/BackupMnemonic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ struct BackupMnemonicView: View {
: "Make sure no one can see your screen. <accent>Never share your recovery phrase</accent> with anyone, as it may result in loss of funds."
}

private var mnemonicAccessibilityLabel: String {
mnemonic.joined(separator: " ")
}

var body: some View {
VStack(alignment: .leading, spacing: 0) {
SheetHeader(title: t("security__mnemonic_your"))
Expand Down Expand Up @@ -50,6 +54,10 @@ struct BackupMnemonicView: View {
.background(Color.gray6)
.blur(radius: showMnemonic ? 0 : 5)
.privacySensitive()
.accessibilityElement(children: .ignore)
.accessibilityIdentifier("SeedContainer")
.accessibilityLabel(mnemonicAccessibilityLabel)
.accessibilityHidden(!showMnemonic)

if !showMnemonic {
CustomButton(
Expand All @@ -59,6 +67,7 @@ struct BackupMnemonicView: View {
showMnemonic = true
}
.frame(maxWidth: 180)
.accessibilityIdentifier("TapToReveal")
}
}
.cornerRadius(16)
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Views/Onboarding/CreateWalletView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ struct CreateWalletView: View {
app.toast(error)
}
}
.accessibilityIdentifier("NewWallet")

CustomButton(
title: t("onboarding__restore"),
variant: .secondary,
destination: MultipleWalletsView()
)
.accessibilityIdentifier("RestoreWallet")
}
}
.padding(.horizontal, 32)
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Views/Onboarding/CreateWalletWithPassphraseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ struct CreateWalletWithPassphraseView: View {
TextField(t("onboarding__passphrase"), text: $bip39Passphrase)
.focused($isTextFieldFocused)
.padding(.bottom, 28)
.accessibilityIdentifier("PassphraseInput")

CustomButton(title: t("onboarding__create_new_wallet"), isDisabled: !isValidPassphrase) {
createWallet()
}
.buttonBottomPadding(isFocused: isTextFieldFocused)
.accessibilityIdentifier("CreateNewWallet")
}
.frame(minHeight: geometry.size.height)
.padding(.horizontal, 16)
Expand Down
Loading
Loading