Skip to content
This repository was archived by the owner on Sep 3, 2026. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ package io.ton.walletkit.demo.e2e.dapp
import android.content.ClipboardManager
import android.util.Log
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
Expand Down Expand Up @@ -120,19 +122,27 @@ class JsDAppController {
fun openBrowser(url: String = DAPP_URL, injectTonConnect: Boolean = false) {
jsBridge.clearCache() // Clear any cached WebView reference

if (injectTonConnect) {
composeTestRule.onNodeWithContentDescription("Open TonConnect Browser")
.performClick()
} else {
composeTestRule.onNodeWithTag(TestTags.BROWSER_NO_INJECT_BUTTON)
.performClick()
val target = InstrumentationRegistry.getArguments().getString("browserUrl")
?.takeIf { it.isNotBlank() } ?: url

// Modern path: gear -> Investigation -> dApp Browser -> type URL -> open (inject / no-inject).
composeTestRule.onNodeWithTag(TestTags.INVESTIGATION_BUTTON).performClick()
composeTestRule.waitUntil(ELEMENT_TIMEOUT) {
composeTestRule.onAllNodesWithTag(TestTags.INVESTIGATION_BROWSER_ROW).fetchSemanticsNodes().isNotEmpty()
}
composeTestRule.onNodeWithTag(TestTags.INVESTIGATION_BROWSER_ROW).performClick()

composeTestRule.waitForIdle()
composeTestRule.waitUntil(ELEMENT_TIMEOUT) {
composeTestRule.onAllNodesWithTag(TestTags.BROWSER_URL_FIELD).fetchSemanticsNodes().isNotEmpty()
}
composeTestRule.onNodeWithTag(TestTags.BROWSER_URL_FIELD).performTextClearance()
composeTestRule.onNodeWithTag(TestTags.BROWSER_URL_FIELD).performTextInput(target)

// Wait for WebView to load
val buttonTag = if (injectTonConnect) TestTags.BROWSER_INJECT_BUTTON else TestTags.BROWSER_NO_INJECT_BUTTON
composeTestRule.onNodeWithTag(buttonTag).performClick()
composeTestRule.waitForIdle()

Log.d("JsDAppController", "Browser opened, waiting for WebView...")
Log.d("JsDAppController", "Browser opened ($target, inject=$injectTonConnect), waiting for WebView...")
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,28 +83,11 @@ class WalletController(composeTestRule: ComposeTestRule? = null) {
// ===========================================

/**
* Check if we're on the legacy wallet home screen (the only screen the e2e suite knows how
* to drive). The legacy toolbar exposes [TestTags.BROWSER_NO_INJECT_BUTTON]; the modern
* [WalletScreen] does not. Returns true only when the legacy home is visible AND the
* [AddWalletSheet] is not in front of it.
* Whether the wallet home is visible. The home exposes [TestTags.WALLET_BALANCE]; returns true
* only when it is visible AND the [AddWalletSheet] is not in front of it.
*/
fun isOnHomeScreen(): Boolean {
if (!isOnLegacyHome()) return false
return !isAddWalletSheetShowing()
}
fun isOnHomeScreen(): Boolean = isOnModernHome() && !isAddWalletSheetShowing()

private fun isOnLegacyHome(): Boolean = try {
composeTestRule.onNodeWithTag(TestTags.BROWSER_NO_INJECT_BUTTON).assertExists()
true
} catch (e: AssertionError) {
false
}

/**
* The modern [WalletScreen] is the default for new installs. It carries no legacy testTags
* but exposes [TestTags.WALLET_BALANCE] on the balance area, which is also the 5-tap secret
* gesture target that toggles back to the legacy screen.
*/
private fun isOnModernHome(): Boolean = try {
composeTestRule.onNodeWithTag(TestTags.WALLET_BALANCE).assertExists()
true
Expand Down Expand Up @@ -293,24 +276,23 @@ class WalletController(composeTestRule: ComposeTestRule? = null) {
enterMnemonicOnImportScreen(mnemonic)
}
isAddWalletSheetShowing() -> {
Log.d("WalletController", "Legacy AddWalletSheet visible - using paste-all field")
importWalletViaLegacySheet(mnemonic)
Log.d("WalletController", "AddWalletSheet visible - using paste-all field")
importWalletViaAddSheet(mnemonic)
}
else -> {
Log.w("WalletController", "importWallet: no recognised import UI - aborting")
return
}
}

// Wait for wallet home (either modern or legacy) to appear after import succeeds.
// Wait for the wallet home to appear after import succeeds.
composeTestRule.waitForIdle()
try {
composeTestRule.waitUntil(15_000L) { isOnLegacyHome() || isOnModernHome() }
composeTestRule.waitUntil(15_000L) { isOnModernHome() }
Log.d("WalletController", "Wallet loaded successfully in UI")
} catch (e: Exception) {
Log.e("WalletController", "Wallet home still not visible after timeout: ${e.message}")
}
Log.d("WalletController", "After import - legacy: ${isOnLegacyHome()}, modern: ${isOnModernHome()}")
}

/** Modern import flow: paste the full phrase into the first tagged word field, tap Continue. */
Expand All @@ -336,8 +318,8 @@ class WalletController(composeTestRule: ComposeTestRule? = null) {
composeTestRule.waitForIdle()
}

/** Legacy AddWalletSheet flow (only reachable when the dev legacy-screen toggle is on). */
private fun importWalletViaLegacySheet(mnemonic: List<String>) {
/** AddWalletSheet flow: paste the full phrase into the sheet's paste-all field. */
private fun importWalletViaAddSheet(mnemonic: List<String>) {
val mnemonicString = mnemonic.joinToString(" ")

composeTestRule.waitUntil(5_000L) {
Expand Down Expand Up @@ -406,35 +388,16 @@ class WalletController(composeTestRule: ComposeTestRule? = null) {
Log.d("WalletController", "=== setupWallet called with ${mnemonic.size} word mnemonic ===")
composeTestRule.waitForIdle()

// Check if we're already on the legacy home screen (wallet exists from a previous run
// and the dev toggle is already flipped).
// Already on the wallet home — wallet exists from a previous run.
if (isOnHomeScreen()) {
Log.d("WalletController", "Already on legacy home screen - wallet exists, skipping setup")
Log.d("WalletController", "Already on home screen - wallet exists, skipping setup")
return
}

// Check if AddWalletSheet is already showing (password was set previously, dev toggle on)
if (isAddWalletSheetShowing()) {
Log.d("WalletController", "AddWalletSheet already showing - importing wallet directly")
// A pre-wallet surface is showing — import directly.
if (isAddWalletSheetShowing() || isOnOnboardingScreen() || isOnImportScreen()) {
Log.d("WalletController", "Pre-wallet surface visible - importing wallet")
importWallet(mnemonic)
ensureLegacyScreen()
return
}

// Modern first-run lands on the onboarding screen (or the import screen if the user
// already tapped through). Both are valid pre-wallet states.
if (isOnOnboardingScreen() || isOnImportScreen()) {
Log.d("WalletController", "Modern onboarding visible - importing wallet")
importWallet(mnemonic)
ensureLegacyScreen()
return
}

// The post-import / post-unlock app lands on the modern WalletScreen by default.
// Tests target legacy UI, so toggle if needed.
if (isOnModernHome()) {
Log.d("WalletController", "On modern WalletScreen - wallet exists, toggling to legacy")
ensureLegacyScreen()
return
}

Expand All @@ -450,86 +413,24 @@ class WalletController(composeTestRule: ComposeTestRule? = null) {
for (i in 1..10) {
composeTestRule.waitForIdle()

// If on legacy home screen, wallet already exists and toggle was on
if (isOnHomeScreen()) {
Log.d("WalletController", "On legacy home after auth (check $i) - wallet exists, skipping import")
Log.d("WalletController", "On home after auth (check $i) - wallet exists, skipping import")
return
}

// If AddWalletSheet is showing, import wallet
if (isAddWalletSheetShowing()) {
Log.d("WalletController", "AddWalletSheet showing after auth (check $i) - importing wallet")
if (isAddWalletSheetShowing() || isOnOnboardingScreen() || isOnImportScreen()) {
Log.d("WalletController", "Import surface after auth (check $i) - importing wallet")
importWallet(mnemonic)
ensureLegacyScreen()
return
}

// Modern first-run onboarding (or already on the import screen).
if (isOnOnboardingScreen() || isOnImportScreen()) {
Log.d("WalletController", "Onboarding visible after auth (check $i) - importing wallet")
importWallet(mnemonic)
ensureLegacyScreen()
return
}

// Default landing for an existing-wallet app: the modern WalletScreen with the balance
// tile visible. Switch to legacy so the rest of the suite can drive it.
if (isOnModernHome()) {
Log.d("WalletController", "Modern home after auth (check $i) - toggling to legacy")
ensureLegacyScreen()
return
}

Log.d("WalletController", "Waiting for home/addWallet screen (check $i)...")
Log.d("WalletController", "Waiting for home/import surface (check $i)...")
Thread.sleep(200) // Small delay between checks
}

// Fallback: try to import wallet anyway (this may fail)
Log.w("WalletController", "No expected screen detected after 10 checks, attempting import as fallback")
importWallet(mnemonic)
ensureLegacyScreen()
}

/**
* Make sure we end up on the legacy wallet home. The new auth flow drops the user on
* [WalletScreen] by default; e2e tests were written against [LegacyWalletScreen], which is
* reached by the dev-only 5-tap secret on the balance tile. No-op if already on legacy.
*/
@Step("Switch to legacy wallet screen")
fun ensureLegacyScreen() {
composeTestRule.waitForIdle()
if (isOnLegacyHome()) {
Log.d("WalletController", "ensureLegacyScreen: already on legacy home")
return
}

// Wait for the modern home (with the WALLET_BALANCE tile) to be ready before tapping.
try {
composeTestRule.waitUntil(UI_IDLE_TIMEOUT) { isOnModernHome() }
} catch (e: Exception) {
Log.w("WalletController", "ensureLegacyScreen: timed out waiting for modern home: ${e.message}")
}

if (!isOnModernHome()) {
Log.w("WalletController", "ensureLegacyScreen: modern home not visible, skipping toggle")
return
}

// DevToggleTaps requires 5 taps inside a 3-second window. performClick runs on the test
// thread back-to-back so they always land inside that window.
Log.d("WalletController", "ensureLegacyScreen: performing 5 quick taps on WALLET_BALANCE")
repeat(5) {
composeTestRule.onNodeWithTag(TestTags.WALLET_BALANCE).performClick()
}
composeTestRule.waitForIdle()

try {
composeTestRule.waitUntil(UI_IDLE_TIMEOUT) { isOnLegacyHome() }
Log.d("WalletController", "ensureLegacyScreen: legacy home is now visible")
} catch (e: Exception) {
Log.w("WalletController", "ensureLegacyScreen: legacy home did not appear: ${e.message}")
}
composeTestRule.waitForIdle()
}

// ===========================================
Expand All @@ -544,43 +445,34 @@ class WalletController(composeTestRule: ComposeTestRule? = null) {
*/
@Step("Connect by TonConnect URL")
fun connectByUrl(url: String) {
// Wait for wallet home screen with Handle URL button
// Modern path: gear -> Investigation -> TonConnect -> Connect to dApp -> URL dialog.
composeTestRule.onNodeWithTag(TestTags.INVESTIGATION_BUTTON).performClick()
composeTestRule.waitUntil(5000) {
composeTestRule.onAllNodesWithTag(TestTags.HANDLE_URL_BUTTON)
composeTestRule.onAllNodesWithTag(TestTags.INVESTIGATION_TONCONNECT_ROW)
.fetchSemanticsNodes().isNotEmpty()
}
composeTestRule.onNodeWithTag(TestTags.INVESTIGATION_TONCONNECT_ROW).performClick()

// Click "Handle URL" button to open dialog
composeTestRule.onNodeWithTag(TestTags.HANDLE_URL_BUTTON)
.performClick()

composeTestRule.waitForIdle()
composeTestRule.waitUntil(5000) {
composeTestRule.onAllNodesWithTag(TestTags.INVESTIGATION_CONNECT_ROW)
.fetchSemanticsNodes().isNotEmpty()
}
composeTestRule.onNodeWithTag(TestTags.INVESTIGATION_CONNECT_ROW).performClick()

// Wait for the URL input field in the dialog
composeTestRule.waitUntil(3000) {
composeTestRule.onAllNodesWithTag(TestTags.TONCONNECT_URL_FIELD)
.fetchSemanticsNodes().isNotEmpty()
}

// Enter the TonConnect URL
composeTestRule.onNodeWithTag(TestTags.TONCONNECT_URL_FIELD)
.performTextInput(url)

// Click the process button
composeTestRule.onNodeWithTag(TestTags.TONCONNECT_PROCESS_BUTTON)
.performClick()

composeTestRule.onNodeWithTag(TestTags.TONCONNECT_URL_FIELD).performTextInput(url)
composeTestRule.onNodeWithTag(TestTags.TONCONNECT_PROCESS_BUTTON).performClick()
composeTestRule.waitForIdle()
}

/**
* Wait for the (legacy) wallet home screen to be visible. The e2e suite runs against the
* legacy screen, so [setupWallet] ensures we are toggled there before this is called.
*/
/** Wait for the wallet home screen (balance tile) to be visible. */
@Step("Wait for wallet home screen")
fun waitForWalletHome() {
composeTestRule.waitUntil(SHEET_APPEAR_TIMEOUT) {
composeTestRule.onAllNodesWithTag(TestTags.WALLET_ADDRESS)
composeTestRule.onAllNodesWithTag(TestTags.WALLET_BALANCE)
.fetchSemanticsNodes().isNotEmpty()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ import io.ton.walletkit.demo.designsystem.theme.TonTheme
import io.ton.walletkit.demo.presentation.actions.WalletActionsImpl
import io.ton.walletkit.demo.presentation.dev.DevPreferences
import io.ton.walletkit.demo.presentation.state.CreateWalletFlow
import io.ton.walletkit.demo.presentation.state.SheetState
import io.ton.walletkit.demo.presentation.ui.screen.CreatePinScreen
import io.ton.walletkit.demo.presentation.ui.screen.LegacyWalletScreen
import io.ton.walletkit.demo.presentation.ui.screen.UnlockPinScreen
import io.ton.walletkit.demo.presentation.ui.screen.WalletScreen
import io.ton.walletkit.demo.presentation.ui.screen.onboarding.CreateWalletOnboardingScreen
Expand Down Expand Up @@ -132,7 +130,6 @@ private fun AppNavigation(

else -> {
walletKit.value?.let { kit ->
val useLegacyMainScreen by DevPreferences.useLegacyMainScreen.collectAsState()
val createFlow by viewModel.createWalletFlow.collectAsState()
val walletsBootstrapped = state.walletsBootstrapped

Expand All @@ -144,14 +141,10 @@ private fun AppNavigation(
}
}

LaunchedEffect(hasWallet, useLegacyMainScreen, walletsBootstrapped) {
LaunchedEffect(hasWallet, walletsBootstrapped) {
if (!walletsBootstrapped) return@LaunchedEffect
if (hasWallet) return@LaunchedEffect
if (useLegacyMainScreen) {
if (state.sheetState !is SheetState.AddWallet) {
viewModel.openAddWalletSheet()
}
} else if (createFlow is CreateWalletFlow.Idle) {
if (createFlow is CreateWalletFlow.Idle) {
viewModel.showCreateWalletOnboarding()
}
}
Expand All @@ -160,23 +153,13 @@ private fun AppNavigation(
flow = createFlow,
viewModel = viewModel,
fallback = {
if (useLegacyMainScreen) {
LegacyWalletScreen(
state = state,
walletKit = kit,
nftsViewModel = nftsViewModel,
swapViewModel = swapViewModel,
actions = walletActions,
)
} else {
WalletScreen(
state = state,
walletKit = kit,
nftsViewModel = nftsViewModel,
swapViewModel = swapViewModel,
actions = walletActions,
)
}
WalletScreen(
state = state,
walletKit = kit,
nftsViewModel = nftsViewModel,
swapViewModel = swapViewModel,
actions = walletActions,
)
},
)
}
Expand Down
Loading