Skip to content
Open
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
135 changes: 135 additions & 0 deletions .github/workflows/ai-device-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: ai-device-tests

on:
workflow_dispatch:
inputs:
suite:
description: "AI device test suite to run"
required: true
default: "trezor-emu"
type: choice
options:
- trezor-emu
simulator_name:
description: "iOS Simulator name"
required: false
default: "iPhone 17"
simulator_os:
description: "iOS Simulator OS version"
required: false
default: "26.2"

env:
TERM: xterm-256color
FORCE_COLOR: 1

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.suite }}
cancel-in-progress: true

jobs:
trezor-emu:
if: inputs.suite == 'trezor-emu'
name: Trezor emulator dashboard
runs-on: [self-hosted, macOS]
timeout-minutes: 90

steps:
- name: Checkout Bitkit iOS
uses: actions/checkout@v6

- name: Checkout bitkit-docker
uses: actions/checkout@v6
with:
repository: synonymdev/bitkit-docker
ref: main
path: bitkit-docker

- name: Install xcbeautify
run: |
if ! command -v xcbeautify >/dev/null 2>&1; then
brew install xcbeautify
fi

- name: System information
run: |
sw_vers
xcodebuild -version
docker version
docker compose version

- name: Resolve Swift packages
run: |
xcodebuild -resolvePackageDependencies -onlyUsePackageVersionsFromResolvedFile | xcbeautify

- name: Start regtest and Trezor emulator
working-directory: bitkit-docker
run: |
docker compose up -d
./scripts/trezor-emulator start
./scripts/trezor-emulator status

- name: Boot simulator
env:
SIMULATOR_NAME: ${{ inputs.simulator_name }}
run: |
xcrun simctl boot "$SIMULATOR_NAME" || true

- name: Run Trezor emulator UI tests
env:
TEST_TREZOR_EMU: "1"
TEST_TREZOR_RESET_STATE: "1"
TREZOR_BRIDGE: "true"
TREZOR_BRIDGE_URL: "http://127.0.0.1:21325"
TREZOR_ELECTRUM_URL: "tcp://127.0.0.1:60001"
E2E: "true"
E2E_BACKEND: "local"
E2E_NETWORK: "regtest"
GEO: "false"
SIMULATOR_NAME: ${{ inputs.simulator_name }}
SIMULATOR_OS: ${{ inputs.simulator_os }}
run: |
mkdir -p TestResults
set -o pipefail
xcodebuild test \
-workspace Bitkit.xcodeproj/project.xcworkspace \
-scheme BitkitAITests \
-configuration Debug \
-destination "platform=iOS Simulator,name=$SIMULATOR_NAME,OS=$SIMULATOR_OS" \
-derivedDataPath DerivedData \
-resultBundlePath TestResults/TrezorBridgeDashboardUITests.xcresult \
SWIFT_ACTIVE_COMPILATION_CONDITIONS='DEBUG E2E_BUILD TEST_TREZOR_EMU' \
-only-testing:BitkitUITests/TrezorBridgeDashboardUITests \
-parallel-testing-enabled NO \
| xcbeautify

- name: Collect diagnostics
if: always()
run: |
mkdir -p ai-device-artifacts/trezor
xcrun simctl io booted screenshot ai-device-artifacts/trezor/simulator.png || true
if [ -d TestResults ]; then
cp -R TestResults ai-device-artifacts/trezor/ || true
fi
(
cd bitkit-docker
./scripts/trezor-emulator status > ../ai-device-artifacts/trezor/trezor-status.json 2>&1 || true
docker compose logs --no-color --tail=500 trezor-user-env-mac > ../ai-device-artifacts/trezor/trezor-user-env.log 2>&1 || true
docker compose logs --no-color > ../ai-device-artifacts/trezor/docker-compose.log 2>&1 || true
curl --silent --show-error -X POST http://127.0.0.1:21325/enumerate > ../ai-device-artifacts/trezor/bridge-enumerate.json 2>&1 || true
)

- name: Upload diagnostics
if: always()
uses: actions/upload-artifact@v6
with:
name: ai-device-tests-trezor-${{ github.run_number }}
path: ai-device-artifacts/trezor
if-no-files-found: warn

- name: Stop emulator services
if: always()
working-directory: bitkit-docker
run: |
./scripts/trezor-emulator stop || true
docker compose down || true
9 changes: 6 additions & 3 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -434,23 +434,24 @@
/* Begin PBXShellScriptBuildPhase section */
96EMBED0012026012000FRAME /* Remove Static Framework Stubs */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/LDKNodeFFI.framework/_CodeSignature/CodeResources",
);
name = "Remove Static Framework Stubs";
outputFileListPaths = (
);
outputPaths = (
"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/.ldk-stubs-removed",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/.ldk-stubs-removed",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Remove static framework stubs from app bundle\\n# LDKNodeFFI is a static library - its code is linked into the main executable.\\n# The empty framework structure causes iOS install errors.\\nFRAMEWORK_PATH=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework\"\\nOUTPUT_SENTINEL=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/.ldk-stubs-removed\"\\n\\nif [ -d \"$FRAMEWORK_PATH\" ]; then\\n echo \"Removing LDKNodeFFI static framework stub...\"\\n rm -rf \"$FRAMEWORK_PATH\"\\n echo \"Done.\"\\nfi\\ntouch \"$OUTPUT_SENTINEL\"\\n";
shellScript = "# Remove static framework stubs from app bundle\n# LDKNodeFFI is a static library - its code is linked into the main executable.\n# The empty framework structure causes iOS install errors.\nFRAMEWORK_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/LDKNodeFFI.framework\"\nOUTPUT_SENTINEL=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/.ldk-stubs-removed\"\n\nif [ -d \"$FRAMEWORK_PATH\" ]; then\n echo \"Removing LDKNodeFFI static framework stub...\"\n rm -rf \"$FRAMEWORK_PATH\"\n if [ -d \"$FRAMEWORK_PATH\" ]; then\n echo \"error: Failed to remove LDKNodeFFI static framework stub\" >&2\n exit 1\n fi\n echo \"Done.\"\nfi\ntouch \"$OUTPUT_SENTINEL\"\n";
};
/* End PBXShellScriptBuildPhase section */

Expand Down Expand Up @@ -696,6 +697,7 @@
DEVELOPMENT_TEAM = KYH47R284B;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Bitkit/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Bitkit;
Expand Down Expand Up @@ -744,6 +746,7 @@
DEVELOPMENT_TEAM = KYH47R284B;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Bitkit/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Bitkit;
Expand Down
91 changes: 91 additions & 0 deletions Bitkit.xcodeproj/xcshareddata/xcschemes/BitkitAITests.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "96FE1F602C2DE6AA006D0C8B"
BuildableName = "Bitkit.app"
BlueprintName = "Bitkit"
ReferencedContainer = "container:Bitkit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "96FE1F7B2C2DE6AC006D0C8B"
BuildableName = "BitkitUITests.xctest"
BlueprintName = "BitkitUITests"
ReferencedContainer = "container:Bitkit.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "96FE1F602C2DE6AA006D0C8B"
BuildableName = "Bitkit.app"
BlueprintName = "Bitkit"
ReferencedContainer = "container:Bitkit.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "96FE1F602C2DE6AA006D0C8B"
BuildableName = "Bitkit.app"
BlueprintName = "Bitkit"
ReferencedContainer = "container:Bitkit.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
13 changes: 11 additions & 2 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ struct AppScene: View {

private var mainContent: some View {
ZStack {
if migrations.isShowingMigrationLoading {
if Env.isTrezorEmulatorTesting {
trezorEmulatorTestContent
} else if migrations.isShowingMigrationLoading {
migrationLoadingContent
} else if showRecoveryScreen {
RecoveryRouter()
Expand All @@ -205,7 +207,7 @@ struct AppScene: View {
walletContent
}

if !removeSplash && !session.skipSplashOnce {
if !Env.isTrezorEmulatorTesting, !removeSplash, !session.skipSplashOnce {
SplashView()
.opacity(hideSplash ? 0 : 1)
}
Expand Down Expand Up @@ -267,6 +269,13 @@ struct AppScene: View {
}
}

private var trezorEmulatorTestContent: some View {
NavigationStack {
TrezorRootView()
}
.accentColor(.white)
}

@ViewBuilder
private var existingWalletContent: some View {
if walletIsInitializing == true {
Expand Down
6 changes: 5 additions & 1 deletion Bitkit/BitkitApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,16 @@ struct BitkitApp: App {

init() {
UIWindow.appearance().overrideUserInterfaceStyle = .dark
if Env.shouldResetTrezorEmulatorState {
TrezorKnownDeviceStorage.removeAll()
TrezorCredentialStorage.deleteAll()
}
_ = ToastWindowManager.shared
}

var body: some Scene {
WindowGroup {
if Env.isUnitTest {
if Env.isUnitTest, !Env.isTrezorEmulatorTesting {
Text("Running tests...")
} else {
ContentView()
Expand Down
8 changes: 8 additions & 0 deletions Bitkit/Components/Trezor/TrezorDeviceRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct TrezorDeviceRow: View {
}
.buttonStyle(.plain)
.disabled(isConnecting)
.accessibilityIdentifier(device.path.hasPrefix("bridge:") ? "TrezorDevice-bridge" : "TrezorDevice-\(device.path)")
}

private var displayName: String {
Expand Down Expand Up @@ -137,6 +138,7 @@ struct KnownDeviceRow: View {
}
.buttonStyle(.plain)
.disabled(isConnecting)
.accessibilityIdentifier("TrezorKnownDeviceConnect-\(accessibilitySuffix)")

// Forget button
Button(action: onForget) {
Expand All @@ -146,10 +148,16 @@ struct KnownDeviceRow: View {
.padding(10)
}
.buttonStyle(.plain)
.accessibilityIdentifier("TrezorForgetDevice-\(accessibilitySuffix)")
}
.padding(16)
.background(Color.white.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 16))
.trezorAccessibilityAnchor("TrezorKnownDevice-\(accessibilitySuffix)")
}

private var accessibilitySuffix: String {
device.path.hasPrefix("bridge:") ? "bridge" : device.id
}
}

Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Components/Trezor/TrezorExpandableSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct TrezorExpandableSection<Content: View>: View {
let title: String
let icon: String
let description: String
var accessibilityIdentifier: String?
@Binding var isExpanded: Bool
@ViewBuilder let content: () -> Content

Expand Down Expand Up @@ -44,6 +45,7 @@ struct TrezorExpandableSection<Content: View>: View {
.animation(.easeInOut(duration: 0.25), value: isExpanded)
}
}
.accessibilityIdentifier(accessibilityIdentifier ?? "TrezorSection-\(title.replacingOccurrences(of: " ", with: ""))")

// Expandable content
if isExpanded {
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Components/Trezor/TrezorPairingCodeInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct TrezorPairingCodeInput: View {
.focused($isFocused)
.frame(width: 1, height: 1)
.opacity(0.01) // Nearly invisible but still functional
.accessibilityIdentifier("TrezorPairingCodeInput")
.onChange(of: code) { newValue in
// Filter to only digits and limit length
let filtered = newValue.filter(\.isNumber)
Expand All @@ -48,6 +49,7 @@ struct TrezorPairingCodeInput: View {
try? await Task.sleep(nanoseconds: 200_000_000)
isFocused = true
}
.accessibilityIdentifier("TrezorPairingCodeDisplay")
}

/// Get digit at specific index, or nil if not entered yet
Expand Down
Loading
Loading