mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Integration tests (#3477)
* fix android CI * feat: Add integration test harness and new-ui test keys This change: - Adds a BaseRobot with bounded waits to replace hard sleeps in integration tests - Adds AppLauncher, TestConfig and TestWallets as the core test harness - Adds onboarding flows composing the existing onboarding robots - Adds robots for the new dashboard, home page and lightning username page - Adds a tier0 onboarding create suite targeting the new UI - Applies the declared navbar action keys that were never attached to the InkWell - Fixes AlertWithOneAction dropping its buttonKey since the alert redesign - Adds stable ValueKeys to the home page action row, wallet name, top bar and sync bar - Makes the legacy tap and text entry helpers wait for widgets instead of sleeping * fix: Test secrets drift and integration test runner parameterization This change: - Adds missing test wallet seed and receive address entries to the secrets generator - Fixes trailing space typos in two secret key names - Adds bloc_test to pubspec_base.yaml so pubspec regeneration keeps it - Parameterizes the integration test runner with suite dir, tier, platform and device knobs - Fixes the runner aborting on first failure and misaligning durations after retries - Adds adb data reset so Android suites start from a fresh install * feat: Android emulator integration test workflows for dev PRs This change: - Adds a reusable integration test workflow building the app with prebuilt native deps and running the suites on an Android emulator - Adds the PR gate workflow triggering fund-free tier0 suites on pull requests into dev - Fixes the old workflow discarding the test runner exit code so failures now fail the job - Removes the disabled automated integration test workflow on the stale flutter image - Removes the commit-message gated Linux desktop test steps, unit tests stay - Removes the vestigial emulator config from the reusable build workflow * feat: Revive fund-free integration test suites for the new UI This change: - Adds tier0 suites for restore, seed confirmation, wallet switching, receive address, settings navigation and the show keys auth gate - Adds tier1 suites for sync status, swap quotes and transaction history running non-blocking on PRs - Adds robots for the settings, receive, swap and send sheets - Adds auth, wallet switching and restore flows - Applies widget keys to settings rows through their route names, they were declared but never attached - Adds keys to the swap amount fields, send inputs, confirm swiper and history tiles - Adds an integrationTest wrapper containing background async app errors that are not test failures - Makes taps wait out running route transitions before hitting the screen - Adds a watchdog to the test runner so a wedged driver fails bounded instead of hanging - Removes the old UI suites, robots, helpers and the stale widget_test counter template * chore: Add integration testing docs and suite template This change: - Adds docs/INTEGRATION_TESTS.md covering the architecture, local runs, tier rules and the flakiness playbook - Adds a copy-paste suite template teammates start new feature tests from * feat: Manual funds integration test workflow with funded-chain auto-discovery This change: - Adds a funds suite sending a real self transaction on every funded chain - Adds a funds suite creating a real swap and broadcasting its deposit - Discovers funded chains from the funded wallet seeds map, adding a seed to the secret adds the chain - Adds the manual workflow dispatched from the Actions tab with flow, chain and recording inputs - Gates the run behind the funds-tests environment and posts the result to slack - Keeps funded seeds out of PR builds, the checked in seed map is empty * fix: Harden integration test harness from review findings This change: - Fixes body failures being swallowed by the guarded test zone, failing suites hung until the watchdog instead of failing fast - Fixes multi chain CHAINS dispatch values being split apart by the define forwarding - Fixes the swap limits wait timing out on pairs without a provider minimum - Makes unknown chain names in CHAINS fail with a readable message - Keys favorite token rows by index, duplicate token symbols crashed on duplicate keys - Guards the recording encryption so a keyserver flake cannot eat the test result - Removes the dead security and backup robot and corrects the screenshot claim in the docs * chore: Hold two funded seeds per chain in the funded wallets map This change: - Changes the funded wallet seeds map to a list of seeds per chain - Keeps the first seed as the primary for sends and swaps, the second is reserved for cross wallet scenarios * feat: Try each funded wallet per chain until one has a balance This change: - Adds a funds flow restoring a chain's funded wallets in turn and using the first with a spendable balance - Treats a wallet that finishes syncing while still empty as drained so the next seed gets its turn quickly - Fails a chain with a top up message when every funded wallet of that chain is empty * reformat integration test related files * fix: Run the integration test emulator on the kvm capable runner This change: - Moves the test job to the linux-amd64 runner the old emulator workflow targeted, the android builders have no /dev/kvm - Exposes /dev/kvm through a docker device flag instead of a volume, a bind mount materializes a directory when the host node is missing - Fails the kvm check in seconds with a clear error instead of waiting out the emulator boot timeout * fix: Queue the integration test job on the live android runner pool This change: - Moves the job back to the android runner labels, no runner carries the old linux-amd64 label anymore so the job queued forever - Keeps the kvm device flag and check so a host without kvm fails immediately with a clear error * feat: Run the integration test gate on github hosted runners This change: - Moves the test job to ubuntu-latest which has working kvm, the self-hosted android builders do not expose /dev/kvm yet - Runs directly on the vm since the cake container image does not fit hosted disk, flutter is pinned through the flutter action - Adds rust android targets for the breez cargokit build and java 17 for gradle - Keeps the prebuilt native deps from the ghcr docker cache, they pull fine on hosted docker - Adds pub, gradle and flutter caching plus a disk cleanup step - Keeps every input, the runner script, tier1 soft run, recording and artifacts unchanged * fix: Install flutter from the same source as the ci container image This change: - Clones the flutter 3.41.9 tag directly like the container image instead of using the release archive - The archive ships a stricter dart that rejects the checked in mweb ffi bindings, the tag clone compiles them - Caches the cloned sdk between runs * fix: Regenerate the mweb ffi bindings with the workflow dart This change: - Regenerates cw_mweb/lib/generated_bindings.g.dart after the deps step, the docker deps builder generates it with an older dart whose ffigen output the current compiler rejects * fix: Pin ffigen so fresh resolutions generate compliant mweb bindings This change: - Pins ffigen to ^20.1.1 in cw_mweb, the unconstrained dependency resolved to 7.1.0 on fresh checkouts - Old ffigen emits ffi classes without the base or final modifiers the current dart compiler requires * fix: Restore the pinned cw_mweb pubspec before regenerating bindings This change: - Restores cw_mweb/pubspec.yaml after the deps step, the deps image rsync clobbers it with the stale unpinned copy so the ffigen pin never applied - Wipes the rsynced pubspec.lock and .dart_tool so pub resolves the pinned ffigen fresh - Asserts the regenerated bindings carry class modifiers so a bad generation fails in seconds - Skips the libclang install when it is already present, apt spent fourteen minutes on it * fix: Run the emulator test logic from a file, the action splits script lines This change: - Moves the whole emulator script into a file written before the emulator step, the action executes every script line as a separate sh command so multi line logic broke with a syntax error - Passes the action a single line script invoking that file with bash * fix: Give the ci emulator a real phone profile This change: - Sets the avd to the pixel 5 profile, the default 320x640 skin cannot fit the seed word grid so every wallet creation suite failed its seed display checks - Caps tier0 attempts at ten minutes, two failing suites at the thirty minute worst case blew the job timeout * fix: Wait for pages on the ci emulator instead of asserting immediately This change: - Makes the legacy page assertion poll for the page, the wallet keys page arrives after the ci emulator finishes unlocking the wallet so both auth gated suites failed on a race - Raises the tier0 attempt timeout, the first attempt for each suite compiles that target's kernel and was being killed then passing on retry - Raises the job timeout to fit the per suite compiles * fix: Recover from driver attach failures instead of burning the timeout This change: - Detects attempts where the flutter driver never attached to the app and retries them without spending the real retry, restarting adb first - Lowers the ci attempt timeout back to ten minutes, every passing attempt finishes within four so a longer cap only makes an attach failure cost more * fix: Bound the adb recovery and give up when the driver never attaches This change: - Bounds every adb call in the recovery path, adb wait-for-device blocks forever when the emulator is gone and silently ate a whole ci job - Aborts the suite when adb cannot be brought back instead of retrying against a dead device - Stops the run after a few driver attach failures, an environment that never attaches will not fix itself - Dumps logcat after every failed attempt so a failure to start the app leaves evidence behind * fix: Stop the runner failing when every suite passes This change: - Assigns the result arrays empty instead of only declaring them, set -u treats a declared but never assigned array as unbound so the summary died on the first run where nothing failed * fix: Make the ci build cacheable and keep tier results when the runner dies This change: - Splits the flutter and pub caches into restore and save steps, actions/cache only saves when the whole job succeeds so they were never written and every run paid for a cold build - Lets setup-gradle write its cache, it is read only on branches other than the default one - Caps the gradle heap before the test phase, the six gigabyte daemon plus the emulator left the runner too little to stay alive - Runs tier0 and tier1 as separate emulator steps so each uploads its logs before the next can take the runner down - Logs memory and disk around each tier - Documents how ci runs the suites and the two environment quirks that only bite there * fix: token decimals defaulting to zero and breaking amount parsing in solana (cherry picked from commit c220bbb46df9468faf9a8967cb0ef672af7cc664) * fix: Install the test error handler after the app boots and drain benign errors This change: - Installs the test error handler after main() instead of before it, the app replaces FlutterError.onError while booting so the handler installed earlier never ran - Replaces the app's async error handler during tests, it returns at its first await without recording anything so the framework asserted instead of reporting the real error - Drains background errors the app tolerates at runtime from the wait loops, anything unrecognised still fails the test where it happened - Collects the tolerated categories in one place, layout and semantics assertions from debug builds plus the transient network failures exception_handler.dart already ignores * fix: Import FlutterExceptionHandler from foundation so the test build compiles This change: - Imports package:flutter/foundation.dart in the launcher, material.dart only re-exports Brightness and UniqueKey from foundation so FlutterExceptionHandler was not in scope - Drops the material.dart import, every symbol the launcher uses comes from foundation * chore: Drop the funds-tests environment from the funds workflow and docs This change: - Removes the environment input from the funds workflow, we no longer use a funds-tests GitHub environment so passing it would only auto create an unprotected one on the next dispatch - Says plainly in the docs that nothing gates the dispatch, the doc still claimed the run waits for environment approval - Names write_funds_secrets as the one thing keeping funded seeds out of a PR build, that used to be backed by environment scoping too * chore: Trim the integration test comments down to the ones that carry weight This change: - Drops the doc comment sitting on top of every robot primitive, flow and config getter, they restated the method name and nothing else - Collapses the multi paragraph docstrings on the launcher, the error drain and the funded wallet lookup into the one or two lines that actually explain why the code is shaped that way - Removes the file and class header paragraphs, the funded wallets stub keeps a short note because an empty map needs one - Keeps the notes worth having, the chain quirks in the onboarding flow, the route transition tap, the dust thresholds, the void attempt handling in the runner * chore: Fold the components folder into core and clear out the leftover comments This change: - Moves common_test_cases.dart into core, the components folder was down to that one file after common_test_constants.dart went - Drops six methods from it that nothing calls, dragUntilVisible alone was 95 lines of scroll logic with no caller - Removes the comments narrating the line below them across the older robots, "Drag to the left" above a moveBy and "Confirmation for buttons 1-9" above the loop - Deletes the commented out body of takeScreenshots and says instead that it does nothing, all fifteen callers have been no-ops - Notes in the docs that new robots extend BaseRobot while the older ones still carry a CommonTestCases * chore: Clear out pending items * refactor: Move the onboarding robots onto BaseRobot and drop the sleeps This change: - Deletes common_test_cases.dart, the sixteen robots that held a CommonTestCases now extend BaseRobot like the newer ones do - Moves the primitives those robots needed onto BaseRobot with the same behaviour, isSpecificPage, hasType, hasValueKey, hasText, hasTextAtLeastOnce and swipePage - Renames each robot's page check to isDisplayed so every robot answers the same question the same way - Removes the twenty six defaultSleepTime calls, every one of them sat in front of an action that already waits for what it needs - Waits for the view model to move to the next word in seed verification instead of sleeping a second and hoping, and fails with a message when it does not - Settles after opening a tab on the wallet keys page, the assertions there read the tree straight after the tap - Drops the fifteen takeScreenshots calls, the method's body has been commented out the whole time so none of them did anything - Formats the three files dart format was still rewriting * fix: Wait out the navigation the removed sleeps used to cover This change: - Settles after a page mounts in isSpecificPage, the route being replaced stays in the tree while it animates out and assertions on text were matching both screens, seed_confirmation_test failed on finding two "Verify Seed" widgets - Settles after the last pin digit, the gate sends itself away and returning early landed the caller's next navigation on a locked navigator, wallet_switching_test failed on the !_debugLocked assertion Both are places where waiting for a widget to exist is not the same as waiting for the app to be idle, which is what the sleeps were quietly doing. * fix: Recognise every way a flutter drive attach fails and give tier1 a retry This change: - Matches the other wordings flutter drive uses when the driver never attaches, we had only the connect one and missed the isolate initialise, the service disappeared and the driver extension variants - Gives tier1 the same single retry tier0 has, running it with none meant a driver that never attached was reported as a failing test Two of the three tier1 failures on the last run were attaches that never happened, neither of them reached any test code. * fix: Sync status test watched the wrong status class, history scroll had no guard This change: - Accepts SyncronizingSyncStatus in the sync status suite, cw_core spells it without the h and it is a separate class from SyncingSyncStatus, so chains reporting it sat out the full three minute timeout and then failed saying the wallet never synced while printing Synchronizing - Waits for the home scroll view before handing it to scrollUntilVisible, which calls single on the finder every drag and reported a missing scroll view as a bare Bad state: No element - Skips the scroll entirely when the history tiles are already on screen * fix: Test the transaction history the way the screen actually works The suite looked for history tiles on the home page and scrolled for more when it found none, which could never pass. The home page renders the history under a tab, and for a wallet with tokens the assets tab is the one selected first, so the history section was never built. It also shows a three item preview, itemsShort caps it at shortHistoryLength, and everything else sits behind the All button in a modal. This change: - Opens the history tab before looking for tiles, wallets with no assets tab render the history with no tab bar at all and skip that step - Asserts the preview never renders more than the three the view model hands it, instead of scrolling for tiles that were never coming - Opens the All button and checks the full list in the modal, which nothing covered before - Adds keys for the tab items, the All button, the single tab history bar and the modal root * fix: Stop the runner wiping a real wallet install and document what CI does that the docs did not Following app_config.sh and then running the tests locally clears com.cakewallet.cake_wallet between every suite, which is a developer's own wallets. CI never hits this because the workflow renames the app afterwards, a step that only existed in the workflow. This change: - Refuses to clear a package that is not named for testing, with the rename command in the message - Adds that rename to the local prerequisites, next to the reason it matters - Spells out the assets/images vector pass, compile_graphics.sh does not cover that folder and those vec files are neither committed nor ignored, so a fresh checkout fails on the welcome screen - Notes that a second Gradle daemon from Android Studio deadlocks the build * test: Make the tier0 and tier1 assertions check what they claim to An audit of every robot against the screen it drives turned up assertions that pass without telling you anything, the same shape as the transaction history suite looking for tiles on a tab it never opened. This change: - Reads the receive address off the screen instead of the view model, the view model holding the right address says nothing about what the user is told to send to, and reads both the chunked and the plain render because only handling the first comes back empty - Verifies a restored wallet knows the address its seed derives, restoring the wrong wallet from a correct seed used to look the same as success, and this found two stale addresses in the secrets - Replaces the sync bar check, the key it asserted sits on a widget that renders for every status including none, so it was true whenever the home page was up - Requires the All transaction view to render more than the home preview, it could not tell the two apart before - Fails the keys page check when a wallet type matches none of its branches instead of passing having verified nothing * refactor: Remove outdated items from integration tests * test: Wait for the wallet to actually change instead of pumping a fixed half second Tapping a wallet row does nothing when that wallet is already open, and the load runs behind a progress overlay, so a tap that went nowhere looked the same as a successful switch. This change: - Waits for the app store to report the wallet before returning, and fails with a message when it never does * test: Open wallet groups before looking for a wallet by name Wallets that share a seed are listed as children of their group's tile and are not on screen while it is closed, so switching to one would time out looking for a name that is only rendered once the group is opened. Single seed wallets carry their own name as the tile title, which is why this has not come up yet. This change: - Opens the group tiles when the wallet is not already on screen * chore: Keep the receive address widget diff to the key it needed A formatter pass rewrote the whole file, quotes, body style and trailing commas, for what was a one line addition. Restored the original shape so the diff shows only the key. * chore: Drop trailing whitespace left by the comment cleanup * test: Cover the send screen refusing what it cannot send Nothing covered the send screen, which is the one place in the app where getting it wrong costs the user money. The property worth holding is that the swiper, the point of no return, is only ever offered for a transaction the wallet actually built. This change: - Adds a tier0 suite driving an empty form, an address the chain cannot parse, and a well formed address on a wallet that has never held anything, and requires that none of the three ever produce a swiper - Waits for the swiper and requires it not to arrive rather than checking once, the screen passes through syncing, building and failed states on the way and any of them can be the one you catch - Notes in the docs that a merge from dev leaves the generated chain proxies and the assets/images vec files behind, which has broken the local build three times now * test: Add send and swap dry runs and a flows mode that never spends The funds suites were all or nothing: the only way to find out whether a funded wallet still works was to spend from it. Almost everything worth checking happens before the swipe, the wallet finds its balance, prices the fee and builds a transaction, or the provider returns a live quote, and none of that costs anything. This change: - Adds a send dry run that stops at the swiper, reaching it means the transaction was really built - Adds a swap dry run that stops before the swap button, creating a trade registers an order with the provider even though no funds move, so it goes no further than the quote - Adds a dry-run value to FLOWS that runs both and neither of the spending suites, and makes it the default so a mis-click cannot send anything * test: Cover a wallet name that is already taken being refused Two wallets answering to the same name is how someone ends up sending from the wrong one, and nothing checked that the form stops it. This change: - Adds a tier0 suite that creates a wallet, starts a second one and gives it the same name, and requires the form to refuse it and stay put - Splits the walk to the naming form out of createAdditionalWalletFromWalletList so a suite can stop there instead of going through with the wallet * test: Cover a seed the wallet cannot parse restoring nothing Restoring from a seed the wallet does not understand either fails outright or, worse, derives some other wallet the user has no keys for. The restore button is disabled until the words check out and nothing checked that. This change: - Adds a tier0 suite that types twelve words outside the wordlist, presses restore, and requires the form to stay put - Follows it with the real seed in the same run, so the refusal above cannot pass just because the form restores nothing for any seed at all - Splits the walk to the restore form out of restoreFirstWalletFromSeed so a suite can stop there * refactor: Make spending its own switch instead of a value hidden in flows flows says which flow to run, so folding a dry-run value into it made the dropdown answer two questions at once and buried the one that matters. This change: - Adds a spend input, off by default, and gates the broadcasting suites on it - Puts flows back to all, send or swap - Runs the dry runs whenever their flow is picked, they cost nothing either way * chore: cleanup minor issues * test: Cover one seed backing wallets on two chains Wallet groups had no coverage at all, and the part that would break quietly is the seed: a group member that derived its own seed instead of sharing one looks identical from the wallet list. It is also the only way a wallet ends up inside a collapsed group tile, which is the path switchToWallet handles and nothing exercised. This change: - Adds a tier0 suite that creates a solana wallet, adds an ethereum wallet to its seed, and requires both to report the same seed - Switches back to the first afterwards, which only works by opening the group it now sits in - Adds the flow for it, a child wallet skips the seed and verification screens for a page explaining it shares one, so it cannot reuse the normal creation steps - Adds the one key the group page was missing, its next button * test: Cover changing the pin replacing the one that unlocks the wallet Nothing checked that changing the pin actually takes. Watching the setup screen close proves nothing, the only evidence is the new pin opening something the old one guarded. This change: - Adds a tier0 suite that changes the pin and then unlocks the seed and keys page with the new one - Goes through the pin gate on the way in, so it also covers the current pin being required before the change is allowed * test: Cover changing the language, and make settings rows and pickers addressable Writing this turned up two things in the app rather than the test. Only ListItemRegularRow was given its keyValue as a widget key. Toggles, checkboxes, dropdowns and selectors all took the same keyValue and dropped it, so every settings control that is not a plain navigation row could not be found at all. Picker named three item types and gave everything else an empty string, so a picker over plain values, the language list among them, rendered every row with the same key. This change: - Passes keyValue through as a key for the other four row types - Falls back to the item itself for the picker name, so each row is distinct - Adds a tier0 suite that changes the language and requires the row to show the new one * test: Cover switching the node the wallet talks to Which node a wallet is pointed at decides what it sees, and a switch that quietly does not take leaves someone on a node they believe they left. Nothing covered it. This change: - Adds a tier1 suite that reads the current node, switches to another one, confirms, and requires the wallet to be pointed somewhere new - tier1 rather than tier0 because opening the page speed tests every node against the network * test: Cover the address book keeping the address it was given Pasting an address once and trusting it afterwards is the whole point of an address book, so an entry that comes back changed is a way to lose money quietly. Nothing covered it. This change: - Adds a tier0 suite that saves a contact through the form and requires the stored address to match what was typed - Checks the saved record rather than the rows, the list filters what it shows by the currency in play so a contact can be saved and correctly not on screen - Adds keys for the add button, the name field, the currency picker, the address field and save * fix: Key picker rows on a name that does not change with the language Enumerable items fell through to toString, which is translated for some of them, so the same row carried a different key depending on the language in use. * test: Cover the fiat currency setting, transaction details and sending to a contact This change: - Adds a suite for turning the fiat api off and on, which is what decides whether the currency setting is offered at all, then changes the currency through it - Adds a suite that taps a transaction and checks the details that open belong to it - Adds a suite that saves a contact and picks it from the send screen - Adds a suite for restoring a 25 word monero seed, which takes a seed type and a restore height the polyseed path never asks for - Keys the address book button on the send screen so a test can reach it - Lets SUITE_DIR take a single suite file, for iterating on one test - Lists every suite and what it proves in the integration tests doc * test: Cover renaming and deleting a wallet This change: - Adds a suite for renaming a wallet, including the refusal when the name is already held by another wallet - Adds a suite for deleting a wallet, checking it leaves storage and not only the list - Keys the edit button in the wallet list and the name field, the two buttons and both dialogs on the wallet edit page, none of which could be reached from a test before - Fixes the wallets list robot, which looked for a single Wallets title even though the navbar carries the same word, and looked for wallet names across the whole tree even though every dashboard tab stays mounted in an IndexedStack * ci: Report every integration test run to slack This change: - Posts a report to slack on every run, naming each tier's counts, duration and any suite that failed, with the full list of what passed in a reply on the same message - Adds a SUMMARY_FILE knob to the runner, which writes what it counted in a form the report can be built from rather than parsing the run log - Skips with a notice when SLACK_APP_TOKEN or SLACK_TESTS_CHANNEL is missing, and never fails the gate when slack is unreachable - Leaves the funds workflow alone, the new slack_notify input defaults to off * chore: Removing unneeded items * ci: Say which slack scope was missing when a report is rejected A rejection carries the scope it wanted in a field of its own, and dropping it left the warning saying missing_scope without saying what to go and add. * ci: Report funds runs to the tests channel and say when slack refuses them This change: - Sends the funds report to SLACK_TESTS_CHANNEL, so both test workflows land in one place and the apk channel is left to builds - Checks the slack response, the post was going out unchecked so a rejected report looked like a delivered one - Builds the message from the environment instead of interpolating workflow inputs into the shell, a quote in a dispatch input was enough to break the payload * ci: Give the test reports their own slack app Uploading apks and reporting test results are different jobs with different scopes, and the name on a message is how anyone in the channel tells which one sent it. Both test workflows now post through SLACK_TESTS_TOKEN, which only needs chat:write, and the apk uploads keep SLACK_APP_TOKEN and its file scopes. * fix: Restore the swap amount key and say when a suite never ran The AnyPay redesign moved the deposit amount field into SwapAmountBox and it lost the key the swap suite enters an amount through, so that suite could no longer reach it. Both amount fields are keyed now, deposit and receive. This change also: - Records whether a failed suite failed on its own or because the driver never attached, which is what happened to send_validation_test in run 31364715120 where the suite never actually ran - Rebuilds the slack report out of blocks, so the status is a heading, the tiers sit in their own fields, failures name their reason and the run is a button rather than a bare url * perf: End an attempt early when the driver cannot attach flutter drive says it is struggling to attach within about a minute and then, when it is really wedged, sits there until TEST_TIMEOUT kills it. Run 31364715120 lost 20 minutes to two attempts doing exactly that. This change: - Watches the attempt log and ends the attempt VOID_GRACE seconds after the driver first reports trouble, 120 by default, so the retry starts on a fresh app instead of waiting out the remaining eight minutes - Starts that clock at the first complaint rather than at the attempt, since the attempt spends its first minute building and installing - Moves the marker list into one variable, the watchdog and the after the fact check were going to drift apart otherwise * perf: Drop the driver grace to 60s and take the whole drive down with it Measured against run 31364715120: the 19 attempts that attached did so in under a second, while the two that never attached had the warning out at 5 seconds. 60 leaves a wide margin over a healthy attach on a runner that was already running slow, and saves another minute on each dead attempt. Ending the attempt now walks the process tree rather than signalling the wrapper. On linux the drive runs under timeout, and a KILL to that cannot be forwarded, so the drive would have been left holding the device just as the retry went looking for it. * trigger another test run * Triggering another run, its been green so far * fix: Wait for the wallet to be ready before tapping send in the funds suites The send button is disabled until isReadyForSend, which wants a synced wallet, but the funds flow moves on as soon as a balance appears. Every funded chain was therefore tapping a disabled button and then waiting ninety seconds for a confirm sheet that could never open. The robot now waits on the same value the screen gates on, so a wallet that never syncs says so instead of looking like a missing widget. * test: Make a funds send failure say what went wrong and stop it spreading A send that never reaches the confirm sheet was reported as a missing ConfirmSwiper, which says nothing about the cause. It now reports the state of whichever sheet is up, what the wallet holds, and the build error when the chain rejected the transaction. Recovery between chains was a single pop, so the first chain to fail left the app somewhere the next chain's restore could not start from and every chain after it failed too. It now unwinds until the home page is back and stops the suite if it cannot get there. * test: full testing of all the funded tests and fix issues that came up with them * fix: update sync key to fix sync test * fix: out of space error * fix: integration test review issues * fix: merge conflicts and update test suites * fix: false test passes and add funded wallet passphrases chore: remove unused integration test robot methods feat: save integration test screenshots on Android feat: pick the swap pair for the funds dispatch * fix: Keep the runner output on the step log through a file descriptor * fix: timeout because of runner crash * test: handle new seed ui flow and fix hang for runner * fix: emulator teardown not rendering succesfully feat: use currency picker for selecting swap options for funded run selected swap currency * test: add key for compact dot key for sync and link to robot * minor fixes * test: review fixes for integration tests
This commit is contained in:
@@ -1,487 +0,0 @@
|
||||
|
||||
name: Automated Integration Tests
|
||||
|
||||
# Temporarily disabled - uncomment to re-enable
|
||||
on:
|
||||
# push:
|
||||
# pull_request:
|
||||
# branches: [main]
|
||||
workflow_dispatch: # This is to manually trigger if needed
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
jobs:
|
||||
Automated_integration_test:
|
||||
runs-on: linux-amd64
|
||||
timeout-minutes: 90
|
||||
container:
|
||||
image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly
|
||||
env:
|
||||
STORE_PASS: test@cake_wallet
|
||||
KEY_PASS: test@cake_wallet
|
||||
MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
ANDROID_AVD_HOME: /root/.android/avd
|
||||
volumes:
|
||||
- /opt/cw_cache_android/root/.cache:/root/.cache
|
||||
- /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
|
||||
- /opt/cw_cache_android/root/.ccache:/root/.ccache
|
||||
- /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
|
||||
- /opt/cw_cache_android/root/.gradle/:/root/.gradle
|
||||
- /opt/cw_cache_android/root/.android/:/root/.android
|
||||
- /opt/cw_cache_android/root/go/pkg:/root/go/pkg
|
||||
- /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
|
||||
- /dev/kvm:/dev/kvm
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [29]
|
||||
|
||||
steps:
|
||||
- name: Fix github actions messing up $HOME...
|
||||
run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV'
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: configure git
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
git config --global user.email "ci@cakewallet.com"
|
||||
git config --global user.name "CakeWallet CI"
|
||||
- name: Get the full commit message
|
||||
run: |
|
||||
FULL_MESSAGE="$(git log -1 --pretty=%B)"
|
||||
echo "message<<EOF" >> $GITHUB_ENV
|
||||
echo "$FULL_MESSAGE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Add secrets
|
||||
run: |
|
||||
touch cw_bitcoin/lib/.secrets.g.dart
|
||||
touch cw_evm/lib/.secrets.g.dart
|
||||
touch cw_solana/lib/.secrets.g.dart
|
||||
touch cw_core/lib/.secrets.g.dart
|
||||
touch cw_nano/lib/.secrets.g.dart
|
||||
touch cw_tron/lib/.secrets.g.dart
|
||||
|
||||
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
|
||||
echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
|
||||
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
|
||||
|
||||
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
||||
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
||||
|
||||
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
||||
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
||||
|
||||
echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart
|
||||
|
||||
echo "${{ secrets.MAIN_SECRETS_FILE }}" | base64 --decode > lib/.secrets.g.dart
|
||||
|
||||
- name: prepare monero_c and cache
|
||||
run: |
|
||||
export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }')
|
||||
echo MONEROC_HASH=$MONEROC_HASH >> /etc/environment
|
||||
mkdir -p "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c"
|
||||
pushd scripts
|
||||
ln -s "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c"
|
||||
./prepare_moneroc.sh
|
||||
popd
|
||||
pushd scripts/monero_c
|
||||
mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" || true
|
||||
mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" || true
|
||||
mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" || true
|
||||
mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" || true
|
||||
mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" || true
|
||||
mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" || true
|
||||
|
||||
rm -rf "$PWD/contrib/depends/built" "$PWD/monero/contrib/depends/built" "$PWD/wownero/contrib/depends/built"
|
||||
rm -rf "$PWD/contrib/depends/sources" "$PWD/monero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources"
|
||||
mkdir -p contrib/depends || true
|
||||
ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" "$PWD/contrib/depends/built"
|
||||
ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" "$PWD/monero/contrib/depends/built"
|
||||
ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" "$PWD/wownero/contrib/depends/built"
|
||||
ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" "$PWD/contrib/depends/sources"
|
||||
ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" "$PWD/monero/contrib/depends/sources"
|
||||
ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources"
|
||||
popd
|
||||
|
||||
- name: Generate KeyStore
|
||||
run: |
|
||||
pushd /opt/generic_cache
|
||||
if [[ ! -f key.jks ]];
|
||||
then
|
||||
keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS
|
||||
else
|
||||
echo "$PWD/key.jks exist, not generating"
|
||||
fi
|
||||
popd
|
||||
cp /opt/generic_cache/key.jks android/app
|
||||
|
||||
- name: Execute Build and Setup Commands
|
||||
run: |
|
||||
pushd scripts/android
|
||||
source ./app_env.sh cakewallet
|
||||
./app_config.sh
|
||||
popd
|
||||
|
||||
- name: Build monero_c
|
||||
run: |
|
||||
pushd scripts/android/
|
||||
source ./app_env.sh cakewallet
|
||||
./build_monero_all.sh
|
||||
popd
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
|
||||
- name: Build mwebd
|
||||
run: |
|
||||
set -x -e
|
||||
export MWEBD_HASH=$(cat scripts/android/build_mwebd.sh | grep 'git reset --hard' | xargs | awk '{ print $4 }')
|
||||
echo MWEBD_HASH=$MWEBD_HASH >> /etc/environment
|
||||
pushd scripts/android
|
||||
gomobile init;
|
||||
./build_mwebd.sh
|
||||
popd
|
||||
|
||||
- name: Build Decred
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts/android
|
||||
./build_decred.sh
|
||||
popd
|
||||
|
||||
- name: Build generated code
|
||||
run: |
|
||||
flutter --version
|
||||
flutter pub get
|
||||
./model_generator.sh
|
||||
|
||||
- name: Generate key properties
|
||||
run: |
|
||||
dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS
|
||||
|
||||
- name: Generate localization
|
||||
run: |
|
||||
dart run tool/generate_localization.dart
|
||||
|
||||
- name: Rename app
|
||||
run: |
|
||||
sanitized_branch_name=${BRANCH_NAME#origin/} # Remove 'origin/' prefix if it exists
|
||||
sanitized_branch_name=${sanitized_branch_name:0:16} # Take only the first 16 characters
|
||||
sanitized_branch_name=$(echo "$sanitized_branch_name" | tr '[:upper:]' '[:lower:]') # Convert to lowercase
|
||||
sanitized_branch_name=$(echo "$sanitized_branch_name" | sed 's/[^a-z0-9]//g') # Remove all special characters
|
||||
|
||||
echo -e "id=com.cakewallet.test_${sanitized_branch_name}\nname=${BRANCH_NAME}" > android/app.properties
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
flutter build apk --dart-define=hasDevOptions=true --release --split-per-abi
|
||||
|
||||
- name: Rename apk file
|
||||
run: |
|
||||
sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
|
||||
cd build/app/outputs/flutter-apk
|
||||
mkdir test-apk
|
||||
|
||||
cp app-arm64-v8a-release.apk test-apk/${sanitized_branch_name}.apk
|
||||
cp app-x86_64-release.apk test-apk/${sanitized_branch_name}_x86.apk
|
||||
echo "APK files created: test-apk/${sanitized_branch_name}.apk and test-apk/${sanitized_branch_name}_x86.apk"
|
||||
|
||||
- name: Find APK file
|
||||
id: find_apk
|
||||
run: |
|
||||
set -x
|
||||
# Read the sanitized branch name from the app.properties file
|
||||
sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
|
||||
|
||||
echo "Looking for APK file: build/app/outputs/flutter-apk/test-apk/${sanitized_branch_name}.apk"
|
||||
ls -la build/app/outputs/flutter-apk/test-apk/ || echo "test-apk directory not found"
|
||||
apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${sanitized_branch_name}.apk || exit 1)
|
||||
echo "APK_FILE=$apk_file" >> $GITHUB_ENV
|
||||
echo "APK file found: $apk_file"
|
||||
ls -la "$apk_file"
|
||||
|
||||
- name: 🔍 Debug System Environment
|
||||
run: |
|
||||
echo "=== System Information ==="
|
||||
uname -a
|
||||
echo "=== Container Info ==="
|
||||
cat /proc/version || echo "Cannot read /proc/version"
|
||||
echo "=== KVM Device Check ==="
|
||||
ls -la /dev/kvm || echo "KVM device not found"
|
||||
echo "=== udev Status ==="
|
||||
which udevadm || echo "udevadm not found"
|
||||
echo "=== Current User ==="
|
||||
whoami
|
||||
echo "=== Groups ==="
|
||||
groups
|
||||
echo "=== Environment ==="
|
||||
env | grep -E "(HOME|USER|PATH)" | sort
|
||||
|
||||
- name: 🧹 Clean up existing emulators
|
||||
run: |
|
||||
echo "=== Cleaning up existing emulators ==="
|
||||
# Kill any existing emulator processes
|
||||
pkill -f emulator || echo "No emulator processes found"
|
||||
# Kill any existing adb processes
|
||||
pkill -f adb || echo "No adb processes found"
|
||||
# Wait a moment for processes to terminate
|
||||
sleep 2
|
||||
# Start adb server fresh
|
||||
adb start-server || echo "Failed to start adb server"
|
||||
# Kill adb server to ensure clean state
|
||||
adb kill-server || echo "Failed to kill adb server"
|
||||
echo "=== Cleanup complete ==="
|
||||
|
||||
- name: 🔍 Verify AVD Configuration
|
||||
run: |
|
||||
echo "=== AVD Configuration Check ==="
|
||||
|
||||
# Check if AVD directory exists
|
||||
echo "Checking AVD directory..."
|
||||
ls -la ~/.android/avd/ || echo "AVD directory not found"
|
||||
|
||||
# List available AVDs
|
||||
echo "Available AVDs:"
|
||||
emulator -list-avds || echo "Failed to list AVDs"
|
||||
|
||||
# Check Android SDK location
|
||||
echo "Android SDK location:"
|
||||
echo $ANDROID_HOME
|
||||
echo $ANDROID_SDK_ROOT
|
||||
|
||||
# Check emulator binary
|
||||
echo "Emulator binary:"
|
||||
which emulator || echo "Emulator not found in PATH"
|
||||
emulator -version || echo "Failed to get emulator version"
|
||||
|
||||
echo "=== AVD Check Complete ==="
|
||||
|
||||
- name: 🦾 Enable KVM
|
||||
run: |
|
||||
echo "=== KVM Setup and Verification ==="
|
||||
|
||||
# Check if KVM device exists
|
||||
if [ -e /dev/kvm ]; then
|
||||
echo "✅ KVM device found at /dev/kvm"
|
||||
|
||||
# Check current permissions
|
||||
echo "Current KVM permissions:"
|
||||
ls -la /dev/kvm
|
||||
|
||||
# Set proper permissions
|
||||
sudo chmod 666 /dev/kvm || echo "⚠️ Failed to set KVM permissions"
|
||||
|
||||
# Verify permissions were set
|
||||
if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
|
||||
echo "✅ KVM is readable and writable"
|
||||
else
|
||||
echo "⚠️ KVM permissions may not be optimal"
|
||||
fi
|
||||
|
||||
# Test KVM functionality
|
||||
echo "Testing KVM functionality..."
|
||||
if command -v kvm-ok >/dev/null 2>&1; then
|
||||
if kvm-ok 2>/dev/null; then
|
||||
echo "✅ KVM is working properly"
|
||||
else
|
||||
echo "⚠️ KVM check failed, but continuing..."
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ kvm-ok command not available, testing manually..."
|
||||
# Manual KVM test
|
||||
if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
|
||||
echo "✅ KVM device is accessible"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try to create udev rules (may fail in container, that's OK)
|
||||
echo "Setting up udev rules..."
|
||||
sudo mkdir -p /etc/udev/rules.d || echo "⚠️ Could not create udev directory"
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules || echo "⚠️ Could not create udev rule"
|
||||
|
||||
# Try to reload udev rules (will likely fail in container, that's OK)
|
||||
sudo udevadm control --reload-rules || echo "ℹ️ udevadm control failed (expected in container)"
|
||||
sudo udevadm trigger --name-match=kvm || echo "ℹ️ udevadm trigger failed (expected in container)"
|
||||
|
||||
else
|
||||
echo "❌ KVM device not found at /dev/kvm"
|
||||
echo "This will cause the emulator to run in software mode (slower)"
|
||||
fi
|
||||
|
||||
# Check system resources
|
||||
echo "=== System Resources ==="
|
||||
echo "Memory:"
|
||||
free -h
|
||||
echo "Disk space:"
|
||||
df -h
|
||||
echo "CPU cores:"
|
||||
nproc
|
||||
echo "KVM module loaded:"
|
||||
lsmod | grep kvm || echo "KVM module not loaded (may be built-in)"
|
||||
|
||||
echo "=== KVM Setup Complete ==="
|
||||
|
||||
- name: 🦾 Cache gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
|
||||
- name: 🦾 Cache AVD
|
||||
uses: actions/cache@v4
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: avd-${{ matrix.api-level }}
|
||||
|
||||
- name: 🔧 Prepare emulator utilities
|
||||
run: |
|
||||
# Create a helper script for emulator readiness checks
|
||||
cat > /tmp/emulator_ready.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== Emulator Readiness Check ==="
|
||||
|
||||
# Wait for boot completion
|
||||
echo "1. Checking boot completion..."
|
||||
timeout 300 bash -c 'until adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; do sleep 5; echo " Waiting for boot completion..."; done'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Boot completed"
|
||||
else
|
||||
echo "❌ Boot completion timeout"
|
||||
echo "⚠️ Continuing anyway..."
|
||||
fi
|
||||
|
||||
# Wait for input service
|
||||
echo "2. Checking input service..."
|
||||
timeout 60 bash -c 'until adb shell service list 2>/dev/null | grep -q "input"; do sleep 2; echo " Waiting for input service..."; done'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Input service ready"
|
||||
else
|
||||
echo "❌ Input service timeout"
|
||||
echo "⚠️ Continuing anyway..."
|
||||
fi
|
||||
|
||||
# Wait for package manager
|
||||
echo "3. Checking package manager..."
|
||||
timeout 60 bash -c 'until adb shell pm list packages 2>/dev/null >/dev/null; do sleep 2; echo " Waiting for package manager..."; done'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Package manager ready"
|
||||
else
|
||||
echo "❌ Package manager timeout"
|
||||
echo "⚠️ Continuing anyway..."
|
||||
fi
|
||||
|
||||
# Wait for settings service
|
||||
echo "4. Checking settings service..."
|
||||
timeout 60 bash -c 'until adb shell settings get global window_animation_scale 2>/dev/null >/dev/null; do sleep 2; echo " Waiting for settings service..."; done'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Settings service ready"
|
||||
else
|
||||
echo "❌ Settings service timeout"
|
||||
echo "⚠️ Continuing anyway..."
|
||||
fi
|
||||
|
||||
echo "✅ Emulator readiness check completed"
|
||||
EOF
|
||||
|
||||
chmod +x /tmp/emulator_ready.sh
|
||||
|
||||
- name: 🦾 Create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
force-avd-creation: false
|
||||
# arch: ${{ matrix.arch }}
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 1024 -no-snapshot-save
|
||||
working-directory: ${{ github.workspace }}
|
||||
disable-animations: false
|
||||
script: |
|
||||
echo "=== AVD Snapshot Generation ==="
|
||||
|
||||
# Use the helper script to ensure emulator is fully ready
|
||||
/tmp/emulator_ready.sh
|
||||
|
||||
# Manually disable animations with error handling
|
||||
echo "Disabling animations manually..."
|
||||
adb shell settings put global window_animation_scale 0.0 || echo "Failed to disable window animations"
|
||||
adb shell settings put global transition_animation_scale 0.0 || echo "Failed to disable transition animations"
|
||||
adb shell settings put global animator_duration_scale 0.0 || echo "Failed to disable animator duration"
|
||||
|
||||
echo "=== Checking emulator status before termination ==="
|
||||
adb devices || echo "ADB devices check failed"
|
||||
echo "=== AVD snapshot generation complete ==="
|
||||
|
||||
- name: 🚀 Integration tests on Android Emulator
|
||||
timeout-minutes: 60
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 1024 -no-snapshot-save
|
||||
disable-animations: false
|
||||
working-directory: ${{ github.workspace }}
|
||||
script: |
|
||||
echo "=== Pre-test Environment Check ==="
|
||||
pwd
|
||||
ls -la integration_test_runner.sh || echo "integration_test_runner.sh not found"
|
||||
|
||||
echo "=== Emulator Status Check ==="
|
||||
adb devices
|
||||
|
||||
echo "=== Emulator Info ==="
|
||||
adb shell getprop ro.build.version.release || echo "Failed to get Android version"
|
||||
adb shell getprop ro.product.model || echo "Failed to get device model"
|
||||
|
||||
echo "=== Checking Emulator Performance ==="
|
||||
adb shell cat /proc/cpuinfo | grep -i "model name" | head -1 || echo "Could not check CPU info"
|
||||
adb shell cat /proc/meminfo | grep -i "memtotal" || echo "Could not check memory info"
|
||||
|
||||
echo "=== Checking Emulator Process ==="
|
||||
ps aux | grep emulator | grep -v grep || echo "Emulator process not found in ps output"
|
||||
|
||||
echo "=== Making script executable ==="
|
||||
chmod a+rx integration_test_runner.sh
|
||||
|
||||
echo "=== Running integration tests ==="
|
||||
|
||||
echo "Final emulator readiness check..."
|
||||
/tmp/emulator_ready.sh || echo "Emulator readiness check failed, but continuing..."
|
||||
|
||||
echo "Disabling animations for test performance..."
|
||||
adb shell settings put global window_animation_scale 0.0 || echo "Failed to disable window animations"
|
||||
adb shell settings put global transition_animation_scale 0.0 || echo "Failed to disable transition animations"
|
||||
adb shell settings put global animator_duration_scale 0.0 || echo "Failed to disable animator duration"
|
||||
|
||||
echo "Starting integration test runner..."
|
||||
./integration_test_runner.sh
|
||||
TEST_EXIT_CODE=$?
|
||||
|
||||
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||
echo "Integration tests completed successfully"
|
||||
else
|
||||
echo "Integration tests failed with exit code $TEST_EXIT_CODE"
|
||||
fi
|
||||
|
||||
- name: 🧹 Post-test cleanup
|
||||
if: always()
|
||||
run: |
|
||||
echo "=== Post-test cleanup ==="
|
||||
# Kill any remaining emulator processes
|
||||
pkill -f emulator || echo "No emulator processes to kill"
|
||||
# Kill any remaining adb processes
|
||||
pkill -f adb || echo "No adb processes to kill"
|
||||
# Wait for processes to terminate
|
||||
sleep 2
|
||||
# Check for any remaining processes
|
||||
ps aux | grep -E "(emulator|adb)" | grep -v grep || echo "No remaining emulator/adb processes"
|
||||
echo "=== Cleanup complete ==="
|
||||
- name: cleanup
|
||||
run: rm -rf build/app/outputs/flutter-apk/test-apk/
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Funds Integration Tests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
# This test is never triggered automatically as it moves real funds
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
flows:
|
||||
description: "Which flows to run"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- send
|
||||
- swap
|
||||
default: all
|
||||
chains:
|
||||
description: "Chains the send suites run on. auto discovers every funded chain, or a comma separated list of wallet type names"
|
||||
required: true
|
||||
type: string
|
||||
default: auto
|
||||
swap_from:
|
||||
description: "Wallet type the swap deposits from. Empty takes the first funded chain"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
swap_to:
|
||||
description: "Currency the swap receives, for example eth or btc. Empty leaves the swap page's own pairing"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
spend:
|
||||
description: "Actually broadcast and create trades. Off runs the dry runs only"
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
record_screen:
|
||||
description: "Record the emulator screen, uploaded GPG encrypted"
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
concurrency:
|
||||
group: funds-integration-tests
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
funds-tests:
|
||||
uses: ./.github/workflows/reusable-integration-test.yml
|
||||
with:
|
||||
environment: funds
|
||||
ref: ${{ github.sha }}
|
||||
suite_dir: integration_test/funds_suites
|
||||
test_tier: all
|
||||
write_funds_secrets: true
|
||||
record_screen: ${{ inputs.record_screen }}
|
||||
timeout_minutes: 120
|
||||
slack_notify: true
|
||||
extra_dart_defines: "FLOWS=${{ inputs.flows }};CHAINS=${{ inputs.chains }};SPEND=${{ inputs.spend }};SWAP_FROM=${{ inputs.swap_from }};SWAP_RECEIVE=${{ inputs.swap_to }}"
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Integration Tests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: integration-tests-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
android-integration-tests:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false
|
||||
uses: ./.github/workflows/reusable-integration-test.yml
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
suite_dir: integration_test/suites
|
||||
test_tier: tier0
|
||||
slack_notify: true
|
||||
secrets: inherit
|
||||
@@ -24,7 +24,6 @@ jobs:
|
||||
KEY_PASS: test@cake_wallet
|
||||
MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
DESKTOP_FORCE_MOBILE: Y
|
||||
volumes:
|
||||
- /opt/cw_cache_linux/root/.cache:/root/.cache
|
||||
- /opt/cw_cache_linux/root/.ccache:/root/.ccache
|
||||
@@ -61,14 +60,6 @@ jobs:
|
||||
git config --global --add safe.directory '*'
|
||||
git config --global user.email "ci@cakewallet.com"
|
||||
git config --global user.name "CakeWallet CI"
|
||||
|
||||
- name: Get the full commit message
|
||||
run: |
|
||||
FULL_MESSAGE="$(git log -1 --pretty=%B)"
|
||||
echo "message<<EOF" >> $GITHUB_ENV
|
||||
echo "$FULL_MESSAGE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Add secrets
|
||||
run: |
|
||||
touch cw_bitcoin/lib/.secrets.g.dart
|
||||
@@ -143,54 +134,7 @@ jobs:
|
||||
path: ${{ github.workspace }}/build/linux/x64/release/cakewallet_linux.zip
|
||||
name: cakewallet_linux
|
||||
|
||||
- name: Prepare virtual desktop
|
||||
if: ${{ contains(env.message, 'run tests') }}
|
||||
run: |
|
||||
nohup Xvfb :99 -screen 0 720x1280x16 &
|
||||
echo DISPLAY=:99 | sudo tee -a $GITHUB_ENV
|
||||
dbus-daemon --system --fork
|
||||
nohup NetworkManager &
|
||||
nohup ffmpeg -framerate 60 -video_size 720x1280 -f x11grab -i :99 -c:v libx264 -c:a aac /opt/screen_grab.mkv &
|
||||
|
||||
# Note for people adding tests:
|
||||
# - Tests are ran on Linux, with some things being mocked out.
|
||||
# - Screen recording is being provided for the entire length of the test, you can download it in github articats.
|
||||
# - Screen recordeding is encrypted, look at step "Stop screen recording, encrypt and upload", and add your key if you want
|
||||
# Reason for encryption is the fact that we restore the wallet from seed, and we don't want to leak that, while there
|
||||
# isn't much in those wallets anyway, we still wouldn't like to leak it to anyone who is able to access github.
|
||||
|
||||
- name: Test [confirm_seeds_flow_test]
|
||||
if: ${{ contains(env.message, 'run tests') }}
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
xmessage -timeout 30 "confirm_seeds_flow_test" &
|
||||
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
|
||||
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/confirm_seeds_flow_test.dart
|
||||
|
||||
- name: Test [create_wallet_flow_test]
|
||||
if: ${{ contains(env.message, 'run tests') }}
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
xmessage -timeout 30 "create_wallet_flow_test" &
|
||||
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
|
||||
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/create_wallet_flow_test.dart
|
||||
|
||||
# - name: Test [exchange_flow_test]
|
||||
# if: ${{ contains(env.message, 'run tests') }}
|
||||
# timeout-minutes: 20
|
||||
# run: |
|
||||
# xmessage -timeout 30 "exchange_flow_test" &
|
||||
# rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
|
||||
# exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/exchange_flow_test.dart
|
||||
|
||||
- name: Test [restore_wallet_through_seeds_flow_test]
|
||||
if: ${{ contains(env.message, 'run tests') }}
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" &
|
||||
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
|
||||
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart
|
||||
|
||||
# Integration tests moved to integration_tests.yml
|
||||
- name: Test [cw_core]
|
||||
timeout-minutes: 15
|
||||
run: cd cw_core && flutter test
|
||||
@@ -198,32 +142,3 @@ jobs:
|
||||
- name: Test [cw_monero]
|
||||
timeout-minutes: 15
|
||||
run: cd cw_monero && flutter test
|
||||
|
||||
- name: Stop screen recording, encrypt and upload
|
||||
if: always()
|
||||
run: |
|
||||
if [[ ! -f "/opt/screen_grab.mkv" ]];
|
||||
then
|
||||
exit 0;
|
||||
fi
|
||||
killall ffmpeg
|
||||
sleep 5
|
||||
killall -9 ffmpeg || true
|
||||
sleep 5
|
||||
# Feel free to add your own public key if you wish
|
||||
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 6B3199AD9B3D23B8 # konstantin@cakewallet.com
|
||||
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 35C8DBAFB8D9ACAC # cyjan@mrcyjanek.net
|
||||
gpg --trust-model always --encrypt --output /opt/screen_grab.mkv.gpg \
|
||||
--recipient 6B3199AD9B3D23B8 \
|
||||
--recipient 35C8DBAFB8D9ACAC \
|
||||
/opt/screen_grab.mkv
|
||||
rm /opt/screen_grab.mkv
|
||||
mv /opt/screen_grab.mkv.gpg ./screen_grab.mkv.gpg
|
||||
|
||||
- name: Upload Artifact to github
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/screen_grab.mkv.gpg
|
||||
name: tests_screen_grab
|
||||
|
||||
@@ -27,21 +27,15 @@ jobs:
|
||||
KEY_PASS: test@cake_wallet
|
||||
MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
ANDROID_AVD_HOME: /root/.android/avd
|
||||
volumes:
|
||||
- /opt/cw_cache_android/root/.cache:/root/.cache
|
||||
- /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
|
||||
- /opt/cw_cache_android/root/.ccache:/root/.ccache
|
||||
- /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
|
||||
- /opt/cw_cache_android/root/.gradle/:/root/.gradle
|
||||
- /opt/cw_cache_android/root/.android/:/root/.android
|
||||
- /opt/cw_cache_android/root/go/pkg:/root/go/pkg
|
||||
- /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
|
||||
- /dev/kvm:/dev/kvm
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [29]
|
||||
|
||||
steps:
|
||||
- name: Fix github actions messing up $HOME...
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
name: Reusable Integration Test Logic
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
required: true
|
||||
type: string
|
||||
suite_dir:
|
||||
required: false
|
||||
type: string
|
||||
default: "integration_test/suites"
|
||||
test_tier:
|
||||
required: false
|
||||
type: string
|
||||
default: "tier0"
|
||||
extra_dart_defines:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
environment:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
record_screen:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
write_funds_secrets:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
timeout_minutes:
|
||||
required: false
|
||||
type: number
|
||||
default: 240
|
||||
slack_notify:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
timeout-minutes: ${{ inputs.timeout_minutes }}
|
||||
env:
|
||||
STORE_PASS: test@cake_wallet
|
||||
KEY_PASS: test@cake_wallet
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup /usr/local/lib/node_modules /opt/hostedtoolcache/CodeQL
|
||||
sudo rm -rf /usr/share/swift /usr/local/share/powershell /usr/share/miniconda /opt/az /usr/local/graalvm
|
||||
sudo docker image prune -af > /dev/null
|
||||
df -h /
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
if [[ ! -c /dev/kvm ]]; then
|
||||
echo "::error::/dev/kvm is not a character device, this runner has no usable KVM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Restore flutter sdk
|
||||
id: flutter-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ~/flutter
|
||||
key: flutter-sdk-3.41.9-${{ runner.os }}
|
||||
|
||||
- name: Install Flutter
|
||||
run: |
|
||||
if [[ ! -d "$HOME/flutter/bin" ]]; then
|
||||
git clone --branch 3.41.9 --depth 1 https://github.com/flutter/flutter.git "$HOME/flutter"
|
||||
fi
|
||||
echo "$HOME/flutter/bin" >> $GITHUB_PATH
|
||||
export PATH="$PATH:$HOME/flutter/bin"
|
||||
flutter --version
|
||||
|
||||
- name: Save flutter sdk
|
||||
if: steps.flutter-cache.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ~/flutter
|
||||
key: flutter-sdk-3.41.9-${{ runner.os }}
|
||||
|
||||
- name: Setup gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-read-only: false
|
||||
|
||||
- name: Restore pub packages
|
||||
id: pub-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ~/.pub-cache
|
||||
key: pub-cache-${{ runner.os }}-${{ hashFiles('pubspec_base.yaml', 'cw_*/pubspec.yaml') }}
|
||||
restore-keys: pub-cache-${{ runner.os }}-
|
||||
|
||||
- name: Add rust android targets
|
||||
run: |
|
||||
rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Add secrets
|
||||
run: |
|
||||
touch cw_bitcoin/lib/.secrets.g.dart
|
||||
touch cw_evm/lib/.secrets.g.dart
|
||||
touch cw_solana/lib/.secrets.g.dart
|
||||
touch cw_core/lib/.secrets.g.dart
|
||||
touch cw_nano/lib/.secrets.g.dart
|
||||
touch cw_tron/lib/.secrets.g.dart
|
||||
|
||||
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const blinkApiKey = '${{ secrets.BLINK_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
|
||||
echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
|
||||
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
|
||||
|
||||
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
||||
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
||||
|
||||
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
||||
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
||||
|
||||
echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart
|
||||
|
||||
echo "${{ secrets.MAIN_SECRETS_FILE }}" | base64 --decode > lib/.secrets.g.dart
|
||||
- name: Add funded wallet secrets
|
||||
if: ${{ inputs.write_funds_secrets }}
|
||||
run: |
|
||||
echo "${{ secrets.FUNDS_SECRETS_FILE }}" | base64 --decode > integration_test/core/funded_wallets.dart
|
||||
|
||||
- name: Build dependencies utilizing ghcr cache
|
||||
run: |
|
||||
env \
|
||||
CW_DOCKER_REGISTRY=ghcr.io/cake-tech/cake_wallet \
|
||||
CW_DOCKER_USE_CLOUD=true \
|
||||
./scripts/android/docker/build.sh
|
||||
|
||||
- name: Reclaim the space the dependency images took
|
||||
run: |
|
||||
docker system prune -af --volumes > /dev/null
|
||||
df -h /
|
||||
|
||||
- name: Regenerate mweb bindings
|
||||
run: |
|
||||
git checkout -- cw_mweb/pubspec.yaml
|
||||
rm -f cw_mweb/pubspec.lock
|
||||
rm -rf cw_mweb/.dart_tool
|
||||
|
||||
# Hosted runners ship libclang, only install when it is actually missing
|
||||
if ! ldconfig -p | grep -q libclang; then
|
||||
sudo apt-get install -y --no-install-recommends libclang-dev > /dev/null
|
||||
fi
|
||||
|
||||
if ! command -v protoc > /dev/null ||
|
||||
[[ ! -f /usr/include/google/protobuf/descriptor.proto ]]; then
|
||||
sudo apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev > /dev/null
|
||||
fi
|
||||
|
||||
pushd cw_mweb
|
||||
flutter pub get
|
||||
dart run ffigen --config ffigen_config.yaml
|
||||
popd
|
||||
|
||||
# Fail here in seconds rather than minutes into the gradle build
|
||||
if ! grep -q "final class GoSlice" cw_mweb/lib/generated_bindings.g.dart; then
|
||||
echo "::error::mweb bindings regenerated without class modifiers, ffigen resolved too old"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Execute Build and Setup Commands
|
||||
run: |
|
||||
pushd scripts/android
|
||||
source ./app_env.sh cakewallet
|
||||
./app_config.sh
|
||||
popd
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
|
||||
- name: Save pub packages
|
||||
if: steps.pub-cache.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ~/.pub-cache
|
||||
key: pub-cache-${{ runner.os }}-${{ hashFiles('pubspec_base.yaml', 'cw_*/pubspec.yaml') }}
|
||||
|
||||
- name: Build generated code
|
||||
run: |
|
||||
flutter --version
|
||||
./model_generator.sh
|
||||
|
||||
- name: Generate KeyStore
|
||||
run: |
|
||||
keytool -genkey -v -keystore android/app/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS
|
||||
|
||||
- name: Generate key properties
|
||||
run: |
|
||||
dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS
|
||||
|
||||
- name: Generate localization
|
||||
run: |
|
||||
dart run tool/generate_localization.dart
|
||||
|
||||
- name: Compile graphics
|
||||
run: |
|
||||
./compile_graphics.sh
|
||||
|
||||
- name: Compile assets/images graphics
|
||||
run: |
|
||||
while read -r dir; do
|
||||
dart run vector_graphics_compiler --input-dir "$dir" --out-dir "$dir" > /dev/null &
|
||||
done < <(find assets/images -type d)
|
||||
wait
|
||||
|
||||
- name: Rename app
|
||||
run: |
|
||||
sanitized_branch_name=${BRANCH_NAME#origin/} # Remove 'origin/' prefix if it exists
|
||||
sanitized_branch_name=${sanitized_branch_name:0:16} # Take only the first 16 characters
|
||||
sanitized_branch_name=$(echo "$sanitized_branch_name" | tr '[:upper:]' '[:lower:]') # Convert to lowercase
|
||||
sanitized_branch_name=$(echo "$sanitized_branch_name" | sed 's/[^a-z0-9]//g') # Remove all special characters
|
||||
|
||||
echo -e "id=com.cakewallet.test_${sanitized_branch_name}\nname=${BRANCH_NAME}" > android/app.properties
|
||||
|
||||
- name: Prime test build
|
||||
env:
|
||||
SUITE_DIR_INPUT: ${{ inputs.suite_dir }}
|
||||
TEST_TIER_INPUT: ${{ inputs.test_tier }}
|
||||
run: |
|
||||
search_dir="$SUITE_DIR_INPUT"
|
||||
if [[ "$TEST_TIER_INPUT" != "all" ]]; then
|
||||
search_dir="$search_dir/$TEST_TIER_INPUT"
|
||||
fi
|
||||
first_suite=$(find "$search_dir" -name "*_test.dart" -type f | sort | head -1)
|
||||
echo "Priming build with $first_suite"
|
||||
flutter build apk --debug --target-platform android-x64 --target="$first_suite" --dart-define=CI_BUILD=true
|
||||
|
||||
- name: Cap the gradle heap for the test phase
|
||||
run: |
|
||||
sed -i 's/^org.gradle.jvmargs=.*/org.gradle.jvmargs=-Xmx3072M/' android/gradle.properties
|
||||
grep '^org.gradle.jvmargs' android/gradle.properties
|
||||
pkill -f GradleDaemon || echo "No gradle daemon to stop"
|
||||
free -h || true
|
||||
df -h / || true
|
||||
|
||||
- name: Integration tests on Android Emulator
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
env:
|
||||
RECORD_SCREEN: ${{ inputs.record_screen }}
|
||||
ENCRYPT_LOGS: ${{ inputs.write_funds_secrets }}
|
||||
SUITE_DIR_INPUT: ${{ inputs.suite_dir }}
|
||||
EXTRA_DART_DEFINES_INPUT: ${{ inputs.extra_dart_defines }}
|
||||
with:
|
||||
api-level: 29
|
||||
arch: x86_64
|
||||
profile: pixel_5
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 3072 -cores 4 -no-snapshot-save
|
||||
disable-animations: false
|
||||
working-directory: ${{ github.workspace }}
|
||||
script: bash scripts/ci/run_integration_tests.sh ${{ inputs.test_tier }}
|
||||
|
||||
- name: Upload test logs
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: integration-test-logs
|
||||
path: |
|
||||
${{ github.workspace }}/${{ inputs.test_tier }}_run.log
|
||||
${{ github.workspace }}/${{ inputs.test_tier }}_run.log.gpg
|
||||
${{ github.workspace }}/${{ inputs.test_tier }}_logcat.txt
|
||||
${{ github.workspace }}/${{ inputs.test_tier }}_logcat.txt.gpg
|
||||
${{ github.workspace }}/recordings/*.gpg
|
||||
|
||||
- name: Integration tests on Android Emulator (tier1, non-blocking)
|
||||
if: ${{ inputs.test_tier == 'tier0' }}
|
||||
continue-on-error: true
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
env:
|
||||
RECORD_SCREEN: ${{ inputs.record_screen }}
|
||||
ENCRYPT_LOGS: ${{ inputs.write_funds_secrets }}
|
||||
SUITE_DIR_INPUT: ${{ inputs.suite_dir }}
|
||||
EXTRA_DART_DEFINES_INPUT: ${{ inputs.extra_dart_defines }}
|
||||
with:
|
||||
api-level: 29
|
||||
arch: x86_64
|
||||
profile: pixel_5
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 3072 -cores 4 -no-snapshot-save
|
||||
disable-animations: false
|
||||
working-directory: ${{ github.workspace }}
|
||||
script: bash scripts/ci/run_integration_tests.sh tier1
|
||||
|
||||
- name: Upload tier1 logs
|
||||
if: ${{ always() && inputs.test_tier == 'tier0' }}
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: integration-test-logs-tier1
|
||||
path: |
|
||||
${{ github.workspace }}/tier1_run.log
|
||||
${{ github.workspace }}/tier1_run.log.gpg
|
||||
${{ github.workspace }}/tier1_logcat.txt
|
||||
${{ github.workspace }}/tier1_logcat.txt.gpg
|
||||
|
||||
# Superseded runs are cancelled on purpose, reporting those would be noise
|
||||
- name: Report the run to slack
|
||||
if: ${{ !cancelled() && inputs.slack_notify }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
SLACK_TOKEN: ${{ secrets.SLACK_TESTS_TOKEN }}
|
||||
SLACK_CHANNEL: ${{ secrets.SLACK_TESTS_CHANNEL }}
|
||||
BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
DART_DEFINES: ${{ inputs.extra_dart_defines }}
|
||||
run: |
|
||||
if [[ -z "$SLACK_TOKEN" || -z "$SLACK_CHANNEL" ]]; then
|
||||
echo "::notice::No slack token or channel configured, skipping the report"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fmt() {
|
||||
printf '%dm %02ds' $(( $1 / 60 )) $(( $1 % 60 ))
|
||||
}
|
||||
|
||||
field() {
|
||||
awk -F= -v key="$2" '$1 == key { print $2; exit }' "$1"
|
||||
}
|
||||
|
||||
tier_line() {
|
||||
local file="$1" label="$2" note="$3"
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
printf '*%s*\n%s' "$label" "$note"
|
||||
return
|
||||
fi
|
||||
|
||||
printf '*%s*\n%s/%s passed in %s' "$label" \
|
||||
"$(field "$file" passed)" "$(field "$file" total)" \
|
||||
"$(fmt "$(field "$file" duration)")"
|
||||
}
|
||||
|
||||
# For scenarios where the suite never ran because of driver issues,
|
||||
# we can't call it a failing test, so we just log it
|
||||
failure_lines() {
|
||||
local file="$1" label="$2"
|
||||
|
||||
[[ -f "$file" ]] || return 0
|
||||
|
||||
while IFS= read -r entry; do
|
||||
[[ -n "$entry" ]] || continue
|
||||
|
||||
local rest="${entry#*|}"
|
||||
local name="${entry%%|*}"
|
||||
local duration="${rest%%|*}"
|
||||
local reason="${rest#*|}"
|
||||
|
||||
if [[ "$reason" == "void" ]]; then
|
||||
printf '\n• `%s` (%s) _%s, driver never attached, the suite did not run_' \
|
||||
"$name" "$(fmt "$duration")" "$label"
|
||||
else
|
||||
printf '\n• `%s` (%s) _%s_' "$name" "$(fmt "$duration")" "$label"
|
||||
fi
|
||||
done < <(sed -n 's/^fail=//p' "$file")
|
||||
}
|
||||
|
||||
# Adding the results for the funds suites to the report too, so we know if we need to topup
|
||||
chain_lines() {
|
||||
local status_wanted="$1" icon="$2" file
|
||||
local seen=""
|
||||
|
||||
for file in *_summary.txt; do
|
||||
[[ -f "$file" ]] || continue
|
||||
|
||||
while IFS= read -r entry; do
|
||||
[[ -n "$entry" ]] || continue
|
||||
|
||||
local chain="${entry%%|*}"
|
||||
local rest="${entry#*|}"
|
||||
local status="${rest%%|*}"
|
||||
local detail="${rest#*|}"
|
||||
|
||||
[[ "$status" == "$status_wanted" ]] || continue
|
||||
[[ "$seen" == *"[$chain]"* ]] && continue
|
||||
seen="$seen[$chain]"
|
||||
|
||||
printf '\n%s *%s* _%s_' "$icon" "$chain" "$detail"
|
||||
done < <(sed -n 's/^chain=//p' "$file")
|
||||
done
|
||||
}
|
||||
|
||||
pass_names() {
|
||||
local file="$1" label="$2"
|
||||
|
||||
[[ -f "$file" ]] || return 0
|
||||
|
||||
printf '*%s* (%s)' "$label" "$(field "$file" passed)"
|
||||
|
||||
while IFS= read -r entry; do
|
||||
[[ -n "$entry" ]] || continue
|
||||
printf '\n• %s _%s_' "${entry%%|*}" "$(fmt "${entry##*|}")"
|
||||
done < <(sed -n 's/^pass=//p' "$file")
|
||||
}
|
||||
|
||||
tier0_failed=1
|
||||
tier1_failed=0
|
||||
total_passed=0
|
||||
|
||||
if [[ -f all_summary.txt ]]; then
|
||||
tier0_failed=$(field all_summary.txt failed)
|
||||
else
|
||||
[[ -f tier0_summary.txt ]] && tier0_failed=$(field tier0_summary.txt failed)
|
||||
[[ -f tier1_summary.txt ]] && tier1_failed=$(field tier1_summary.txt failed)
|
||||
fi
|
||||
|
||||
for file in tier0_summary.txt tier1_summary.txt; do
|
||||
[[ -f "$file" ]] || continue
|
||||
total_passed=$(( total_passed + $(field "$file" passed) ))
|
||||
done
|
||||
|
||||
if (( tier0_failed > 0 )); then
|
||||
headline=":x: Integration tests failed"
|
||||
elif (( tier1_failed > 0 )); then
|
||||
headline=":warning: Gate passed, tier1 has failures"
|
||||
else
|
||||
headline=":white_check_mark: Integration tests passed"
|
||||
fi
|
||||
|
||||
if (( tier0_failed > 0 )); then
|
||||
tier1_note="_skipped, tier0 failed_"
|
||||
else
|
||||
tier1_note="_did not run_"
|
||||
fi
|
||||
|
||||
where="<$REPO_URL/tree/$BRANCH|$BRANCH>"
|
||||
if [[ -n "$PR_NUMBER" ]]; then
|
||||
where="$where · <$REPO_URL/pull/$PR_NUMBER|#$PR_NUMBER ${PR_TITLE:-}>"
|
||||
fi
|
||||
|
||||
failures=$(printf '%s%s%s' \
|
||||
"$(failure_lines tier0_summary.txt tier0)" \
|
||||
"$(failure_lines tier1_summary.txt tier1)" \
|
||||
"$(failure_lines all_summary.txt suites)")
|
||||
|
||||
if [[ -f all_summary.txt ]]; then
|
||||
primary=$(tier_line all_summary.txt suites '')
|
||||
secondary=""
|
||||
else
|
||||
primary=$(tier_line tier0_summary.txt tier0 '_did not run_')
|
||||
secondary=$(tier_line tier1_summary.txt tier1 "$tier1_note")
|
||||
fi
|
||||
|
||||
chains=$(printf '%s%s%s%s' \
|
||||
"$(chain_lines ok ':white_check_mark:')" \
|
||||
"$(chain_lines empty ':moneybag:')" \
|
||||
"$(chain_lines waiting ':hourglass:')" \
|
||||
"$(chain_lines failed ':x:')")
|
||||
|
||||
needs_funding=$(chain_lines empty ':moneybag:')
|
||||
too_slow=$(chain_lines waiting ':hourglass:')
|
||||
|
||||
blocks=$(jq -n \
|
||||
--arg headline "$headline" \
|
||||
--arg where "$where" \
|
||||
--arg primary "$primary" \
|
||||
--arg secondary "$secondary" \
|
||||
--arg failures "$failures" \
|
||||
--arg chains "$chains" \
|
||||
--arg funding "$needs_funding" \
|
||||
--arg slow "$too_slow" \
|
||||
--arg defines "$(printf '%s' "$DART_DEFINES" | tr ';' ' ')" \
|
||||
--arg commit "${COMMIT:0:8}" \
|
||||
--arg actor "$ACTOR" \
|
||||
--arg repo "$REPO_URL" \
|
||||
--arg run "$RUN_URL" \
|
||||
'[
|
||||
{ type: "header", text: { type: "plain_text", text: $headline, emoji: true } },
|
||||
{ type: "section", text: { type: "mrkdwn", text: $where } },
|
||||
{ type: "section", fields: (
|
||||
[ { type: "mrkdwn", text: $primary } ]
|
||||
+ (if $secondary == "" then [] else
|
||||
[ { type: "mrkdwn", text: $secondary } ] end)
|
||||
) }
|
||||
]
|
||||
+ (if $failures == "" then [] else
|
||||
[ { type: "section", text: { type: "mrkdwn",
|
||||
text: ("*Did not pass*" + $failures) } } ]
|
||||
end)
|
||||
+ (if $chains == "" then [] else
|
||||
[ { type: "divider" },
|
||||
{ type: "section", text: { type: "mrkdwn",
|
||||
text: ("*Chains*" + $chains) } } ]
|
||||
end)
|
||||
+ (if $funding == "" then [] else
|
||||
[ { type: "context", elements: [ { type: "mrkdwn",
|
||||
text: ":moneybag: The chains above could not cover a send. Top those wallets up and re run the funds workflow, nothing there needs debugging :)." } ] } ]
|
||||
end)
|
||||
+ (if $slow == "" then [] else
|
||||
[ { type: "context", elements: [ { type: "mrkdwn",
|
||||
text: ":hourglass: Still rescanning when the clock ran out, so nothing was proven either way and the run is not red for them." } ] } ]
|
||||
end)
|
||||
+ [
|
||||
{ type: "context", elements: [
|
||||
{ type: "mrkdwn",
|
||||
text: ("<" + $repo + "/commit/" + $commit + "|" + $commit + "> · " + $actor
|
||||
+ (if $defines == "" then "" else " · `" + $defines + "`" end)) }
|
||||
] },
|
||||
{ type: "actions", elements: [
|
||||
{ type: "button", url: $run,
|
||||
text: { type: "plain_text", text: "View run", emoji: false } }
|
||||
] }
|
||||
]')
|
||||
|
||||
response=$(curl -sS -X POST https://slack.com/api/chat.postMessage \
|
||||
-H "Authorization: Bearer $SLACK_TOKEN" \
|
||||
-H "Content-type: application/json; charset=utf-8" \
|
||||
--data "$(jq -n --arg channel "$SLACK_CHANNEL" --arg text "$headline" \
|
||||
--argjson blocks "$blocks" \
|
||||
'{channel: $channel, text: $text, blocks: $blocks}')")
|
||||
|
||||
if [[ "$(jq -r '.ok' <<< "$response")" != "true" ]]; then
|
||||
error=$(jq -r '.error // "unknown"' <<< "$response")
|
||||
needed=$(jq -r '.needed // empty' <<< "$response")
|
||||
|
||||
if [[ -n "$needed" ]]; then
|
||||
echo "::warning::Slack rejected the report: $error, it wants the $needed scope"
|
||||
else
|
||||
echo "::warning::Slack rejected the report: $error"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
thread_ts=$(jq -r '.ts // empty' <<< "$response")
|
||||
|
||||
if [[ -z "$thread_ts" ]] || (( total_passed == 0 )); then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# So that the channel is not clutter (and uglyyyy), we put everything that passed in the thread
|
||||
# and the short version is the one that shows up in the channel
|
||||
passed=$(printf '%s\n\n%s' \
|
||||
"$(pass_names tier0_summary.txt tier0)" "$(pass_names tier1_summary.txt tier1)")
|
||||
|
||||
curl -sS -X POST https://slack.com/api/chat.postMessage \
|
||||
-H "Authorization: Bearer $SLACK_TOKEN" \
|
||||
-H "Content-type: application/json; charset=utf-8" \
|
||||
--data "$(jq -n --arg channel "$SLACK_CHANNEL" --arg ts "$thread_ts" --arg text "$passed" \
|
||||
'{channel: $channel, thread_ts: $ts, text: $text}')" > /dev/null
|
||||
|
||||
- name: Post-test cleanup
|
||||
if: always()
|
||||
run: |
|
||||
pkill -f emulator || echo "No emulator processes to kill"
|
||||
pkill -f adb || echo "No adb processes to kill"
|
||||
@@ -0,0 +1,306 @@
|
||||
# Integration tests
|
||||
|
||||
End to end tests that drive the real app on a device. They run on an Android emulator in CI
|
||||
for every pull request into `dev`, and the funds suites run manually before releases.
|
||||
|
||||
## Architecture
|
||||
|
||||
Four layers, each one only talks to the layer below it:
|
||||
|
||||
```
|
||||
suites/ the runnable tests, one file per user journey
|
||||
tier0/ offline capable, no network assertions, the hard PR gate
|
||||
tier1/ needs live network (nodes, swap providers), non blocking on PRs
|
||||
funds_suites/ moves real funds, only ever run by the manual funds workflow
|
||||
flows/ multi screen journeys shared between suites (onboarding, auth, wallet list)
|
||||
robots/ one class per screen, knows that screen's keys and assertions
|
||||
core/ the harness: BaseRobot primitives, AppLauncher, TestConfig, TestWallets
|
||||
```
|
||||
|
||||
New robots extend `BaseRobot`. The older onboarding robots still hold a `CommonTestCases`
|
||||
instead, which is the same idea with a weaker waiting story, migrate one over whenever you
|
||||
touch it.
|
||||
|
||||
Supporting pieces:
|
||||
|
||||
- `test_driver/integration_test.dart` is the standard flutter drive driver.
|
||||
- `integration_test_runner.sh` discovers and runs suites, see Running locally.
|
||||
- `.github/workflows/integration_tests.yml` is the PR gate,
|
||||
`reusable-integration-test.yml` holds the shared build and emulator pipeline.
|
||||
|
||||
## Running locally
|
||||
|
||||
Prerequisites, one time:
|
||||
|
||||
1. A generated `lib/.secrets.g.dart` with the `*TestWalletSeeds` values populated.
|
||||
2. App config for the cakewallet flavor:
|
||||
`cd scripts/android && source ./app_env.sh cakewallet && ./app_config.sh`
|
||||
3. Rename the app, every run wipes its data between suites:
|
||||
|
||||
```
|
||||
printf 'id=com.cakewallet.test_local\nname=local\n' > android/app.properties
|
||||
```
|
||||
|
||||
Step 2 writes the real `com.cakewallet.cake_wallet` id, and clearing that package takes
|
||||
your own wallets with it. CI does this same rename after app_config for the same reason.
|
||||
The runner refuses to clear a package that is not named for testing.
|
||||
4. Compiled vector assets, both passes:
|
||||
|
||||
```
|
||||
./compile_graphics.sh
|
||||
while read -r dir; do
|
||||
dart run vector_graphics_compiler --input-dir "$dir" --out-dir "$dir"
|
||||
done < <(find assets/images -type d)
|
||||
```
|
||||
|
||||
`compile_graphics.sh` does not cover `assets/images`, and those `.svg.vec` files are
|
||||
neither committed nor ignored, so a fresh checkout has none of them. Without this the
|
||||
app throws "Unable to load asset" on the welcome screen and the first suite fails.
|
||||
5. Native libs in `android/app/src/main/jniLibs/` for monero, wownero and zano
|
||||
(prebuilts from the pinned monero_c release).
|
||||
6. `~/.cargo/bin` on PATH so gradle can build the breez rust crates, and `protoc` installed
|
||||
(`brew install protobuf`), the spark crate generates its protobuf bindings at build time.
|
||||
|
||||
If the build sits at 0% CPU for minutes, close Android Studio or stop its Gradle daemon,
|
||||
two daemons on this project deadlock on the same locks.
|
||||
|
||||
Re-run steps 2 to 4 after every merge from dev, then `flutter pub get` at the root and the
|
||||
codegen for `lib/`. The chain proxies in `lib/<chain>/<chain>.dart` are generated and
|
||||
gitignored, so a merge that adds a call like `bitcoin!.hasSelectedLightning` leaves them
|
||||
behind and the build fails on a method that looks like it should exist. Step 2 also
|
||||
rewrites `android/app.properties` with the real app id, so redo step 3 alongside it, and it
|
||||
finishes by rewriting `pubspec.yaml` after its own last pub get, which is why the root pub
|
||||
get has to come after it. Skipping step 4 shows up as "Unable to load asset" on whichever
|
||||
screen dev added an icon to, one suite in, rather than at build time.
|
||||
|
||||
Run one suite against a booted emulator:
|
||||
|
||||
```
|
||||
flutter drive --driver=test_driver/integration_test.dart \
|
||||
--target=integration_test/suites/tier0/onboarding_create_test.dart \
|
||||
--dart-define=CI_BUILD=true -d <device-id>
|
||||
```
|
||||
|
||||
Run a whole tier through the runner:
|
||||
|
||||
```
|
||||
TEST_TIER=tier0 FLUTTER_DEVICE=<device-id> ./integration_test_runner.sh
|
||||
```
|
||||
|
||||
`SUITE_DIR` also takes a single file, which is what you want while iterating on one suite:
|
||||
|
||||
```bash
|
||||
SUITE_DIR=integration_test/suites/tier0/fiat_currency_test.dart ./integration_test_runner.sh
|
||||
```
|
||||
|
||||
Runner knobs (environment variables): `SUITE_DIR`, `TEST_TIER` (tier0, tier1, all),
|
||||
`PLATFORM` (android, linux, macos, auto), `FLUTTER_DEVICE`, `RETRY_COUNT`,
|
||||
`EXTRA_DART_DEFINES`, `REMOVE_DATA_DIRECTORY=N` to keep app data between suites,
|
||||
`REMOVE_DESKTOP_DATA=Y` to let a desktop run wipe the wallet directories it shares with the
|
||||
installed app, `VOID_GRACE` for how long a driver that cannot attach is given to recover.
|
||||
|
||||
Test knobs (dart defines): `TEST_WALLET_TYPES=all` runs every available wallet type,
|
||||
a comma separated list of type names runs just those, unset runs the representative set
|
||||
(solana, ethereum, bitcoin, monero).
|
||||
|
||||
## Which tier does my test go in
|
||||
|
||||
- Creates wallets, navigates screens, checks local state: **tier0**.
|
||||
- Needs a node, a provider API or any live network response: **tier1**.
|
||||
- Signs and broadcasts a real transaction: **funds_suites**, never anywhere else.
|
||||
|
||||
tier0 blocks merges. tier1 runs on every PR but reports without blocking until it has a
|
||||
stable history. funds_suites run only from the manual funds workflow.
|
||||
|
||||
## What is covered today
|
||||
|
||||
Twenty seven suites. Add a line here when you add one.
|
||||
|
||||
### tier0, no network needed
|
||||
|
||||
| Suite | What it proves |
|
||||
| --- | --- |
|
||||
| `onboarding_create_test` | A wallet can be created for every configured type, and the app lands on the dashboard with that wallet open |
|
||||
| `onboarding_restore_test` | A wallet restores from seed for every configured type, and the restored wallet reports the seed it was given |
|
||||
| `invalid_seed_test` | A seed the wallet cannot parse leaves the user on the restore screen with nothing created |
|
||||
| `duplicate_wallet_name_test` | A name already taken is refused instead of quietly overwriting the wallet holding it |
|
||||
| `wallet_group_test` | A wallet added to an existing seed shares that seed, and both wallets stay reachable through the group |
|
||||
| `wallet_rename_test` | A wallet can be renamed, and cannot be renamed onto a name another wallet already holds |
|
||||
| `wallet_delete_test` | Deleting a wallet takes it off the list and out of storage, and leaves the others alone |
|
||||
| `receive_address_test` | The receive sheet shows an address the opened wallet owns, not a leftover from the previous one |
|
||||
| `seed_confirmation_test` | The seed and keys the app displays are the ones the opened wallet holds |
|
||||
| `show_keys_auth_test` | The seed and keys page turns away a wrong pin and opens for the right one |
|
||||
| `change_pin_test` | A changed pin is the one that opens what the old pin used to guard |
|
||||
| `send_validation_test` | The send screen refuses an empty address, a malformed one, an empty amount and more than the balance |
|
||||
| `address_book_test` | A saved contact keeps the address it was given |
|
||||
| `send_from_address_book_test` | A contact picked on the send screen fills in the address that was saved for it |
|
||||
| `settings_nav_test` | Every settings row opens its page and backs out of it |
|
||||
| `language_test` | Changing the language changes what the settings screen shows |
|
||||
| `fiat_currency_test` | Turning the fiat api off hides the currency setting, turning it back on returns it and the currency can be changed |
|
||||
|
||||
### tier1, needs a node or a provider
|
||||
|
||||
| Suite | What it proves |
|
||||
| --- | --- |
|
||||
| `sync_status_test` | A restored wallet reaches a node and starts syncing rather than sitting at connecting |
|
||||
| `node_switching_test` | Confirming another node repoints the wallet at it |
|
||||
| `transaction_history_test` | A wallet with known transactions renders them, both in the home preview and the full list |
|
||||
| `transaction_details_test` | Tapping a transaction opens the details of that transaction and not another one |
|
||||
| `swap_quote_test` | The swap sheet reaches a provider and returns a real quote, no trade is created |
|
||||
| `monero_legacy_seed_test` | A 25 word monero seed restores through the legacy path, which takes a seed type and a restore height the polyseed path never asks for |
|
||||
|
||||
### funds_suites, manual workflow only
|
||||
|
||||
| Suite | What it proves |
|
||||
| --- | --- |
|
||||
| `send_dry_run_test` | Every funded chain builds and signs a transaction, then throws it away without broadcasting |
|
||||
| `send_funds_test` | A real self transaction is broadcast on every funded chain |
|
||||
| `swap_dry_run_test` | A real deposit is priced end to end, stopping before the trade is created |
|
||||
| `swap_funds_test` | A swap is created and its deposit broadcast |
|
||||
|
||||
The two dry runs need funded seeds because a quote and a signed transaction both depend on a
|
||||
real balance, but neither spends anything. `SPEND=true` is what lets the two that do spend
|
||||
past their guard.
|
||||
|
||||
## Adding a test for your feature
|
||||
|
||||
1. Give the widgets your test touches stable keys, inline in the widget:
|
||||
`key: ValueKey('<page>_<element>_key')`. Keys go on the widget the test taps or reads,
|
||||
never on AnimatedSwitcher children where the key is the animation identity.
|
||||
2. Create or extend the robot for the screen in `robots/`. One robot per screen, it
|
||||
extends `BaseRobot` and implements `isDisplayed()`. Robots own every finder for their
|
||||
screen, suites never call `find` directly.
|
||||
3. If the journey spans several screens, add a flow to `flows/` composing the robots.
|
||||
4. Copy `integration_test/templates/example_feature_test.dart.example` into the right
|
||||
tier directory, rename it to `<feature>_test.dart` and fill it in. The runner only
|
||||
picks up files ending in `_test.dart`.
|
||||
5. Run it twice locally against an emulator before pushing.
|
||||
6. Add it to the table above.
|
||||
|
||||
## Harness rules
|
||||
|
||||
- Suites use `integrationTest` from `core/app_launcher.dart`, not `testWidgets`. It contains
|
||||
background async errors from app networking that would otherwise fail the test, awaited
|
||||
assertion failures still fail the test normally.
|
||||
- No `sleep` or fixed delays, use `pumpUntilFound` and `pumpUntil` with a timeout.
|
||||
- No `pumpAndSettle` in new code, screens with endless animations never settle,
|
||||
use the bounded `settle()` from BaseRobot.
|
||||
- No `find.text` on localized strings, keys only. Wallet names and addresses are data,
|
||||
matching on those is fine.
|
||||
- `.secrets.g.dart` is only imported by `core/test_wallets.dart`.
|
||||
- Every suite starts from a fresh install, the runner wipes app data between suites.
|
||||
- Reading a view model through the page widget for a state assertion is fine, driving
|
||||
the app through view models instead of the UI is not.
|
||||
|
||||
## Secrets
|
||||
|
||||
Test wallet seeds and receive addresses live in `lib/.secrets.g.dart` under names like
|
||||
`solanaTestWalletSeeds`, defined in `tool/utils/secret_key.dart` and reachable through
|
||||
`TestWallets`. CI injects the real values from the `MAIN_SECRETS_FILE` repository secret.
|
||||
|
||||
Adding a new test secret means adding it in `tool/utils/secret_key.dart`, mapping it in
|
||||
`TestWallets` and asking the team lead to add the value to the CI secret.
|
||||
|
||||
The slack report uses two more repository secrets, `SLACK_TESTS_TOKEN` and
|
||||
`SLACK_TESTS_CHANNEL`, which is a channel id rather than a webhook. Both the PR gate and
|
||||
the funds workflow report through them, so every test result lands in one channel.
|
||||
|
||||
That is a separate slack app from the one behind `SLACK_APP_TOKEN`, which uploads apks to
|
||||
the builds channel. They are split so each token carries only what it needs, `chat:write`
|
||||
for this one and the file scopes for that one, and so the name on a message says which of
|
||||
the two sent it. A post coming back `missing_scope` means the app is short the
|
||||
`chat:write` scope or was never reinstalled after it was added.
|
||||
|
||||
## Funds suites
|
||||
|
||||
`funds_suites/` restore funded wallets and move real funds: a small self send on every
|
||||
funded chain, and one real swap with its deposit broadcast. They only run from the manual
|
||||
`Funds Integration Tests` workflow, which the team lead dispatches from the Actions tab
|
||||
before a release. The job runs in the `funds` environment, so add required reviewers to that
|
||||
environment under Settings, Environments, or a dispatch still starts without an approval.
|
||||
|
||||
Funded seeds live in the `FUNDS_SECRETS_FILE` secret, a base64 encoded replacement for
|
||||
`integration_test/core/funded_wallets.dart` mapping wallet type names to that chain's
|
||||
funded wallets, at least two per chain. A wallet with a passphrase carries it and the restore
|
||||
flow ticks the passphrase box and enters it. A wallet without one leaves it out:
|
||||
|
||||
```dart
|
||||
import "funded_wallet.dart";
|
||||
|
||||
const Map<String, List<FundedWallet>> fundedWallets = {
|
||||
"solana": [
|
||||
FundedWallet(seed: "first wallet seed words ...", passphrase: "first passphrase"),
|
||||
FundedWallet(seed: "second wallet seed words ...", passphrase: "second passphrase"),
|
||||
],
|
||||
"ethereum": [
|
||||
FundedWallet(seed: "seed words ..."),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
A wrong passphrase restores an empty derivation, so it shows up the same way a drained
|
||||
wallet does, as "no spendable balance".
|
||||
|
||||
The suites restore a chain's funded wallets one by one and use the first that shows a
|
||||
spendable balance, a wallet that finishes syncing while still empty counts as drained.
|
||||
A chain where every funded wallet is empty fails with a message asking for a top up.
|
||||
|
||||
The checked in default is an empty map and must stay empty. Only the funds workflow turns
|
||||
on `write_funds_secrets`, and that input is the one thing keeping funded seeds out of a PR
|
||||
build now that no environment scopes the secret. In auto mode the suites discover every
|
||||
chain present in the map, so funding a new chain only means adding its entry to the
|
||||
secret. The dispatch inputs narrow a run: `flows` picks send, swap or both, `chains` lists
|
||||
the chains the send suites run on, `swap_from` picks the wallet type the swap deposits from
|
||||
(empty takes the first funded chain) and `swap_to` the currency it receives (empty leaves
|
||||
the swap page's own pairing, which is XMR for everything but a monero wallet). Whatever
|
||||
`swap_to` names must have a fixture address in `TestWallets.receiveAddressFor`, that is
|
||||
where the payout goes. Per chain send amounts are tuned in `TestConfig._fundsSendAmounts`.
|
||||
The swap suite enters an amount just above the provider minimum, the deposit wallet's
|
||||
balance has to cover that minimum plus fees.
|
||||
|
||||
## How CI runs them
|
||||
|
||||
The PR gate runs on GitHub hosted runners, the self-hosted android builders have no
|
||||
usable KVM so the emulator cannot start there. Moving back is a one line change to
|
||||
`runs-on` in `reusable-integration-test.yml` once a KVM capable builder exists.
|
||||
|
||||
tier0 and tier1 each get their own emulator step and upload their logs separately, so a
|
||||
runner that dies during tier1 cannot take the gate's own results with it. The gradle heap
|
||||
is capped before the test phase because the daemon and the emulator together were
|
||||
exhausting the runner.
|
||||
|
||||
Every run posts to slack. The message names each tier's counts, its duration and any suite
|
||||
that failed, and the full list of what passed goes in a reply on the same message so the
|
||||
channel keeps the short version. A suite that failed because the driver never attached is
|
||||
labelled as such rather than reported as a failing test, since in that case the suite never
|
||||
ran at all. It needs `SLACK_TESTS_TOKEN` and `SLACK_TESTS_CHANNEL`, and
|
||||
without either the step logs a notice and skips rather than failing the gate. Cancelled
|
||||
runs stay quiet, since superseded runs are cancelled on purpose. The report is built from
|
||||
the `SUMMARY_FILE` each tier writes, so anything the runner counts is available to it.
|
||||
|
||||
Two environment quirks are worth knowing when a build fails in CI but not locally: the
|
||||
deps docker image regenerates the mweb ffi bindings with an ffigen too old for the current
|
||||
compiler and its rsync overwrites `cw_mweb/pubspec.yaml`, so the workflow restores that
|
||||
file and regenerates the bindings itself. And `assets/images` svgs need compiling to
|
||||
`.vec`, which `compile_graphics.sh` does not cover.
|
||||
|
||||
## Flakiness playbook
|
||||
|
||||
- A timeout in `pumpUntilFound` throws with the finder description. Diagnose through the
|
||||
`integration-test-logs` artifact in CI, it holds the run log and a final logcat dump.
|
||||
The timeout also takes a screenshot, it lands in `build/integration_test_screenshots/`
|
||||
on the machine that ran the suite, named after the robot and the helper that timed out.
|
||||
CI does not upload them.
|
||||
- The app's own error handling ignores missing `.svg.vec` assets by design, the harness
|
||||
filters those too. Any other FlutterError fails the test, that is intentional.
|
||||
- Monero and wownero load native libraries on first open, give their steps generous
|
||||
timeouts rather than retries.
|
||||
- A driver that cannot attach is watched rather than waited out. Once flutter drive says
|
||||
it is struggling, the attempt gets `VOID_GRACE` seconds (60 by default) to come back and
|
||||
is then ended so the retry can start, instead of sitting there for the rest of
|
||||
`TEST_TIMEOUT`. Run 31364715120 lost 20 minutes to two attempts that did exactly that.
|
||||
60 is well clear of a healthy attach, which took under a second on all 19 attempts that
|
||||
worked in that same run, against 5 seconds for the warning to appear on the two that
|
||||
did not.
|
||||
- A suite that fails once in CI retries once with wiped app data. A red gate means the
|
||||
same suite failed twice in a row.
|
||||
@@ -1,260 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class CommonTestCases {
|
||||
WidgetTester tester;
|
||||
CommonTestCases(this.tester);
|
||||
|
||||
Future<void> isSpecificPage<T>() async {
|
||||
await tester.pumpAndSettle();
|
||||
hasType<T>();
|
||||
}
|
||||
|
||||
Future<void> tapItemByKey(
|
||||
String key, {
|
||||
bool shouldPumpAndSettle = true,
|
||||
int pumpDuration = 100,
|
||||
}) async {
|
||||
await tester.pump(Duration(milliseconds: 500));
|
||||
|
||||
final widgetFinder = find.byKey(ValueKey(key));
|
||||
|
||||
expect(tester.any(widgetFinder), true, reason: 'Widget with key "$key" should be visible');
|
||||
|
||||
final widget = widgetFinder.first;
|
||||
await tester.tap(widget, warnIfMissed: false);
|
||||
shouldPumpAndSettle
|
||||
? await tester.pumpAndSettle(Duration(milliseconds: pumpDuration))
|
||||
: await tester.pump();
|
||||
}
|
||||
|
||||
Future<void> tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async {
|
||||
await tester.tap(finder);
|
||||
shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump();
|
||||
}
|
||||
|
||||
void hasText(String text, {bool hasWidget = true}) {
|
||||
final textWidget = find.text(text);
|
||||
expect(textWidget, hasWidget ? findsOneWidget : findsNothing);
|
||||
}
|
||||
|
||||
void hasTextAtLestOnce(String text, {bool hasWidget = true}) {
|
||||
final textWidget = find.text(text);
|
||||
expect(textWidget, hasWidget ? findsAny : findsNothing);
|
||||
}
|
||||
|
||||
void hasType<T>() {
|
||||
final typeWidget = find.byType(T);
|
||||
expect(typeWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
bool isKeyPresent(String key) {
|
||||
final typeWidget = find.byKey(ValueKey(key));
|
||||
return typeWidget.tryEvaluate();
|
||||
}
|
||||
|
||||
void hasValueKey(String key) {
|
||||
final typeWidget = find.byKey(ValueKey(key));
|
||||
expect(typeWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> swipePage({bool swipeRight = true}) async {
|
||||
await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> swipeByPageKey({required String key, bool swipeRight = true}) async {
|
||||
await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> goBack() async {
|
||||
tester.printToConsole('Routing back to previous screen');
|
||||
final NavigatorState navigator = tester.state(find.byType(Navigator));
|
||||
navigator.pop();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> startGesture(String key, Offset gestureOffset) async {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final hasKey = isKeyPresent(key);
|
||||
|
||||
tester.printToConsole("Has gestureKey: $hasKey");
|
||||
|
||||
if (!hasKey) return;
|
||||
|
||||
final gesture = await tester.startGesture(tester.getCenter(find.byKey(ValueKey(key))));
|
||||
|
||||
// Drag to the left
|
||||
await gesture.moveBy(gestureOffset);
|
||||
|
||||
// End the gesture
|
||||
await gesture.up();
|
||||
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
Future<void> dragUntilVisible(
|
||||
String childKey,
|
||||
String parentKey, {
|
||||
int maxScrolls = 100,
|
||||
double scrollStep = 50.0,
|
||||
int maxReverseScrolls = 50,
|
||||
}) async {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final itemFinder = find.byKey(ValueKey(childKey));
|
||||
final listFinder = find.byKey(ValueKey(parentKey));
|
||||
|
||||
// Check if the widget is already in the widget tree
|
||||
if (tester.any(itemFinder)) {
|
||||
// Widget is already built and in the tree
|
||||
tester.printToConsole('Child is already present');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the Scrollable associated with the Parent Ad
|
||||
final scrollableFinder = find.descendant(
|
||||
of: listFinder,
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
|
||||
// Ensure that the Scrollable is found
|
||||
expect(
|
||||
scrollableFinder,
|
||||
findsOneWidget,
|
||||
reason: 'Scrollable descendant of the Parent Widget not found.',
|
||||
);
|
||||
|
||||
// Get the initial scroll position
|
||||
final scrollableState = tester.state<ScrollableState>(scrollableFinder);
|
||||
double previousScrollPosition = scrollableState.position.pixels;
|
||||
bool scrollDown = true;
|
||||
bool reversedDirection = false;
|
||||
bool found = false;
|
||||
|
||||
int reverseScrollCount = 0;
|
||||
|
||||
for (int scrolls = 0; scrolls < maxScrolls; scrolls++) {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Check if the widget is visible
|
||||
if (tester.any(itemFinder)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Log current state for debugging
|
||||
tester.printToConsole('Scrolling ${scrollDown ? 'down' : 'up'}, attempt $scrolls');
|
||||
|
||||
// Stop if reverse scroll limit is exceeded
|
||||
if (!scrollDown && reverseScrollCount >= maxReverseScrolls) {
|
||||
tester.printToConsole('Maximum reverse scrolls reached. Widget not found.');
|
||||
break;
|
||||
}
|
||||
|
||||
// Perform scrolling in the current direction
|
||||
await tester.drag(
|
||||
scrollableFinder,
|
||||
scrollDown ? Offset(0, -scrollStep) : Offset(0, scrollStep),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Update the scroll position after the drag
|
||||
final currentScrollPosition = scrollableState.position.pixels;
|
||||
|
||||
if (currentScrollPosition == previousScrollPosition) {
|
||||
// Cannot scroll further in the current direction
|
||||
if (reversedDirection) {
|
||||
// We've already tried both directions
|
||||
tester.printToConsole('Reached the scroll limit in both directions. Widget not found.');
|
||||
break;
|
||||
} else {
|
||||
// Reverse the scroll direction and reset reverse scroll count
|
||||
scrollDown = !scrollDown;
|
||||
reversedDirection = true;
|
||||
reverseScrollCount = 0;
|
||||
tester.printToConsole('Reached the end, reversing direction');
|
||||
}
|
||||
} else {
|
||||
// Update scroll position and reverse scroll count, incrementing only for reverse scrolling
|
||||
previousScrollPosition = currentScrollPosition;
|
||||
if (!scrollDown) reverseScrollCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
tester.printToConsole('Widget not found after $maxScrolls scrolls.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> scrollItemIntoView(
|
||||
String itemKeyId,
|
||||
double scrollPixels,
|
||||
String scrollableFinderKey,
|
||||
) async {
|
||||
final Finder itemFinder = find.byKey(ValueKey(itemKeyId));
|
||||
|
||||
final scrollableFinder = find.descendant(
|
||||
of: find.byKey(ValueKey(scrollableFinderKey)),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
|
||||
// Check if the item is already visible
|
||||
if (tester.any(itemFinder)) {
|
||||
tester.printToConsole('Item $itemKeyId is already visible');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if scrollable exists
|
||||
if (!tester.any(scrollableFinder)) {
|
||||
tester.printToConsole('Scrollable not found for $itemKeyId');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await tester.scrollUntilVisible(
|
||||
itemFinder,
|
||||
scrollPixels,
|
||||
scrollable: scrollableFinder,
|
||||
maxScrolls: 10,
|
||||
);
|
||||
|
||||
// Wait for the scroll to complete
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 500));
|
||||
|
||||
// Verify the item is now visible
|
||||
if (!tester.any(itemFinder)) {
|
||||
tester.printToConsole('Item $itemKeyId not found after scrolling');
|
||||
}
|
||||
} catch (e) {
|
||||
tester.printToConsole('Could not scroll to $itemKeyId: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enterText(String text, String editableTextKey) async {
|
||||
final editableTextWidget = find.byKey(ValueKey((editableTextKey)));
|
||||
|
||||
await tester.enterText(editableTextWidget, text);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void findWidgetViaDescendant({
|
||||
required FinderBase<Element> of,
|
||||
required FinderBase<Element> matching,
|
||||
}) {
|
||||
final textWidget = find.descendant(of: of, matching: matching);
|
||||
|
||||
expect(textWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> defaultSleepTime({int seconds = 2}) async =>
|
||||
await Future.delayed(Duration(seconds: seconds));
|
||||
|
||||
Future<void> takeScreenshots(String screenshotName) async {
|
||||
// Pausing this for now
|
||||
// await (tester.binding as IntegrationTestWidgetsFlutterBinding).takeScreenshot(screenshotName);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
class CommonTestConstants {
|
||||
static final pin = [0, 8, 0, 1];
|
||||
static final String sendTestAmount = '0.00008';
|
||||
static final String sendTestFiatAmount = '1.00';
|
||||
static final String exchangeTestAmount = '0.01';
|
||||
static final WalletType testWalletType = WalletType.solana;
|
||||
static final String testWalletName = 'Integrated Testing Wallet';
|
||||
static final CryptoCurrency sendTestReceiveCurrency = CryptoCurrency.sol;
|
||||
static final CryptoCurrency exchangeTestReceiveCurrency = CryptoCurrency.usdtSol;
|
||||
static final CryptoCurrency exchangeTestDepositCurrency = CryptoCurrency.sol;
|
||||
static final String testWalletAddress = '7TSTXnQZxQDA4JoNqs4DqVqE7oNWk2kmHkMT6DD6vw2S';
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/entities/seed_type.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_utils.dart';
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:cake_wallet/main.dart' as app;
|
||||
|
||||
import '../robots/create_pin_welcome_page_robot.dart';
|
||||
import '../robots/new_wallet_page_robot.dart';
|
||||
import '../robots/new_wallet_type_page_robot.dart';
|
||||
import '../robots/pre_seed_page_robot.dart';
|
||||
import '../robots/restore_from_seed_or_key_robot.dart';
|
||||
import '../robots/restore_options_page_robot.dart';
|
||||
import '../robots/seed_verification_page_robot.dart';
|
||||
import '../robots/setup_pin_code_robot.dart';
|
||||
import '../robots/wallet_group_description_page_robot.dart';
|
||||
import '../robots/wallet_list_page_robot.dart';
|
||||
import '../robots/wallet_seed_page_robot.dart';
|
||||
import '../robots/welcome_page_robot.dart';
|
||||
import 'common_test_cases.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
import 'common_test_constants.dart';
|
||||
|
||||
class CommonTestFlows {
|
||||
CommonTestFlows(this._tester)
|
||||
: _commonTestCases = CommonTestCases(_tester),
|
||||
_welcomePageRobot = WelcomePageRobot(_tester),
|
||||
_preSeedPageRobot = PreSeedPageRobot(_tester),
|
||||
_setupPinCodeRobot = SetupPinCodeRobot(_tester),
|
||||
_newWalletPageRobot = NewWalletPageRobot(_tester),
|
||||
_walletSeedPageRobot = WalletSeedPageRobot(_tester),
|
||||
_walletListPageRobot = WalletListPageRobot(_tester),
|
||||
_newWalletTypePageRobot = NewWalletTypePageRobot(_tester),
|
||||
_restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester),
|
||||
_seedVerificationPageRobot = SeedVerificationPageRobot(_tester),
|
||||
_createPinWelcomePageRobot = CreatePinWelcomePageRobot(_tester),
|
||||
_restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester),
|
||||
_walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(_tester);
|
||||
|
||||
final WidgetTester _tester;
|
||||
final CommonTestCases _commonTestCases;
|
||||
|
||||
final WelcomePageRobot _welcomePageRobot;
|
||||
final PreSeedPageRobot _preSeedPageRobot;
|
||||
final SetupPinCodeRobot _setupPinCodeRobot;
|
||||
final NewWalletPageRobot _newWalletPageRobot;
|
||||
final WalletSeedPageRobot _walletSeedPageRobot;
|
||||
final WalletListPageRobot _walletListPageRobot;
|
||||
final NewWalletTypePageRobot _newWalletTypePageRobot;
|
||||
final RestoreOptionsPageRobot _restoreOptionsPageRobot;
|
||||
final CreatePinWelcomePageRobot _createPinWelcomePageRobot;
|
||||
final SeedVerificationPageRobot _seedVerificationPageRobot;
|
||||
final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
|
||||
final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot;
|
||||
|
||||
//* ========== Handles flow to start the app afresh and accept disclaimer =============
|
||||
Future<void> startAppFlow(Key key) async {
|
||||
await app.main(topLevelKey: key);
|
||||
|
||||
await _tester.pump(Duration(seconds: 2));
|
||||
|
||||
// // --------- Disclaimer Page ------------
|
||||
// // Tap checkbox to accept disclaimer
|
||||
// await _disclaimerPageRobot.tapDisclaimerCheckbox();
|
||||
|
||||
// // Tap accept button
|
||||
// await _disclaimerPageRobot.tapAcceptButton();
|
||||
}
|
||||
|
||||
//* ========== Handles flow from welcome to creating a new wallet ===============
|
||||
Future<void> welcomePageToCreateNewWalletFlow(
|
||||
WalletType walletTypeToCreate,
|
||||
List<int> walletPin,
|
||||
) async {
|
||||
await _welcomeToCreateWalletPath(walletTypeToCreate, walletPin);
|
||||
|
||||
await _generateNewWalletDetails();
|
||||
|
||||
await _confirmPreSeedInfo();
|
||||
|
||||
await _confirmWalletDetails();
|
||||
|
||||
await _verifyWalletSeed();
|
||||
}
|
||||
|
||||
//* ========== Handles flow from welcome to restoring wallet from seeds ===============
|
||||
Future<void> welcomePageToRestoreWalletThroughSeedsFlow(
|
||||
WalletType walletTypeToRestore,
|
||||
String walletSeed,
|
||||
List<int> walletPin,
|
||||
) async {
|
||||
await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin);
|
||||
await _restoreFromSeeds(walletTypeToRestore, walletSeed);
|
||||
}
|
||||
|
||||
//* ========== Handles flow from welcome to restoring wallet from keys ===============
|
||||
Future<void> welcomePageToRestoreWalletThroughKeysFlow(
|
||||
WalletType walletTypeToRestore,
|
||||
List<int> walletPin,
|
||||
) async {
|
||||
await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin);
|
||||
await _restoreFromKeys();
|
||||
}
|
||||
|
||||
void confirmAllAvailableWalletTypeIconsDisplayCorrectly() {
|
||||
for (var walletType in availableWalletTypes) {
|
||||
final imageUrl = walletTypeToCryptoCurrency(walletType).iconPath;
|
||||
|
||||
final walletIconFinder = find.image(
|
||||
Image.asset(
|
||||
imageUrl!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
).image,
|
||||
);
|
||||
|
||||
expect(walletIconFinder, findsAny);
|
||||
}
|
||||
}
|
||||
|
||||
//* ========== Handles creating new wallet flow from wallet list/menu ===============
|
||||
Future<void> createNewWalletFromWalletMenu(WalletType walletTypeToCreate) async {
|
||||
_tester.printToConsole('Creating ${walletTypeToCreate.name} Wallet');
|
||||
|
||||
await _tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
await _walletListPageRobot.navigateToCreateNewWalletPage();
|
||||
await _commonTestCases.defaultSleepTime();
|
||||
|
||||
await _selectWalletTypeForWallet(walletTypeToCreate);
|
||||
await _commonTestCases.defaultSleepTime();
|
||||
|
||||
// ---- Wallet Group/New Seed Implementation Comes here
|
||||
await _walletGroupDescriptionPageFlow(true, walletTypeToCreate);
|
||||
|
||||
await _generateNewWalletDetails();
|
||||
|
||||
await _confirmPreSeedInfo();
|
||||
|
||||
await _confirmWalletDetails();
|
||||
|
||||
await _verifyWalletSeed();
|
||||
|
||||
await _commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> _walletGroupDescriptionPageFlow(bool isNewSeed, WalletType walletType) async {
|
||||
if (!isBIP39Wallet(walletType)) return;
|
||||
|
||||
await _walletGroupDescriptionPageRobot.isWalletGroupDescriptionPage();
|
||||
|
||||
if (isNewSeed) {
|
||||
await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage();
|
||||
} else {
|
||||
await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup();
|
||||
}
|
||||
}
|
||||
|
||||
//* ========== Handles restore wallet flow from wallet list/menu ===============
|
||||
Future<void> restoreWalletFromWalletMenu(WalletType walletType, String walletSeed) async {
|
||||
_tester.printToConsole('Restoring ${walletType.name} Wallet');
|
||||
await _walletListPageRobot.navigateToRestoreWalletOptionsPage();
|
||||
await _commonTestCases.defaultSleepTime();
|
||||
|
||||
if (!Platform.isLinux) await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
|
||||
if (!Platform.isLinux) await _commonTestCases.defaultSleepTime();
|
||||
|
||||
await _selectWalletTypeForWallet(walletType);
|
||||
await _commonTestCases.defaultSleepTime();
|
||||
|
||||
await _restoreFromSeeds(walletType, walletSeed);
|
||||
await _commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
//* ========== Handles setting up pin code for wallet on first install ===============
|
||||
Future<void> setupPinCodeForWallet(List<int> pin) async {
|
||||
if (Platform.isLinux) return;
|
||||
// ----------- SetupPinCode Page -------------
|
||||
// Confirm initial defaults - Widgets to be displayed etc
|
||||
await _setupPinCodeRobot.isSetupPinCodePage();
|
||||
|
||||
await _setupPinCodeRobot.enterPinCode(pin);
|
||||
await _setupPinCodeRobot.enterPinCode(pin);
|
||||
await _setupPinCodeRobot.tapSuccessButton();
|
||||
}
|
||||
|
||||
Future<void> _welcomeToCreateWalletPath(
|
||||
WalletType walletTypeToCreate,
|
||||
List<int> pin,
|
||||
) async {
|
||||
await _createPinWelcomePageRobot.tapSetAPinButton();
|
||||
|
||||
await setupPinCodeForWallet(pin);
|
||||
|
||||
await _welcomePageRobot.navigateToCreateNewWalletPage();
|
||||
|
||||
await _selectWalletTypeForWallet(walletTypeToCreate);
|
||||
|
||||
if (_welcomePageRobot.hasNewSingleSeedButton()) {
|
||||
await _welcomePageRobot.tapNewSingleSeed();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _welcomeToRestoreFromSeedsOrKeysPath(
|
||||
WalletType walletTypeToRestore,
|
||||
List<int> pin,
|
||||
) async {
|
||||
await _createPinWelcomePageRobot.tapSetAPinButton();
|
||||
|
||||
await setupPinCodeForWallet(pin);
|
||||
|
||||
await _welcomePageRobot.navigateToRestoreWalletPage();
|
||||
|
||||
if (!Platform.isLinux) await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
|
||||
|
||||
await _selectWalletTypeForWallet(walletTypeToRestore);
|
||||
}
|
||||
|
||||
//* ============ Handles New Wallet Type Page ==================
|
||||
Future<void> _selectWalletTypeForWallet(WalletType type) async {
|
||||
// ----------- NewWalletType Page -------------
|
||||
_tester.printToConsole('Selecting wallet type: ${type.name}');
|
||||
|
||||
await _tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type);
|
||||
|
||||
// Tapping the wallet type navigates directly (no separate Next button).
|
||||
_tester.printToConsole('Tapping wallet type: ${type.name}');
|
||||
await _newWalletTypePageRobot.selectWalletType(type);
|
||||
|
||||
await _tester.pumpAndSettle(Duration(milliseconds: 2000));
|
||||
|
||||
_tester.printToConsole('Wallet type selection completed for: ${type.name}');
|
||||
}
|
||||
|
||||
//* ============ Handles New Wallet Page ==================
|
||||
Future<void> _generateNewWalletDetails() async {
|
||||
_tester.printToConsole('Attempting to find NewWalletPage...');
|
||||
|
||||
await _tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
await _newWalletPageRobot.isNewWalletPage();
|
||||
_tester.printToConsole('NewWalletPage found successfully');
|
||||
|
||||
await _newWalletPageRobot.generateWalletName();
|
||||
|
||||
if (Platform.isLinux) {
|
||||
// manual pin input
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.enterPasswordForWalletRestore(CommonTestConstants.pin.join(""));
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.enterPasswordRepeatForWalletRestore(CommonTestConstants.pin.join(""));
|
||||
}
|
||||
|
||||
await _newWalletPageRobot.onNextButtonPressed();
|
||||
}
|
||||
|
||||
//* ============ Handles Pre Seed Page =====================
|
||||
Future<void> _confirmPreSeedInfo() async {
|
||||
await _preSeedPageRobot.isPreSeedPage();
|
||||
|
||||
await _preSeedPageRobot.onConfirmButtonPressed();
|
||||
}
|
||||
|
||||
//* ============ Handles Wallet Seed Page ==================
|
||||
Future<void> _confirmWalletDetails() async {
|
||||
await _walletSeedPageRobot.isWalletSeedPage();
|
||||
|
||||
_walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly();
|
||||
|
||||
_walletSeedPageRobot.confirmWalletSeedReminderDisplays();
|
||||
|
||||
// await _walletSeedPageRobot.onCopySeedsButtonPressed();
|
||||
|
||||
await _walletSeedPageRobot.onVerifySeedButtonPressed();
|
||||
}
|
||||
|
||||
//* ============ Handles Wallet Seed Verification Page ==================
|
||||
|
||||
Future<void> _verifyWalletSeed() async {
|
||||
await _seedVerificationPageRobot.isSeedVerificationPage();
|
||||
|
||||
_seedVerificationPageRobot.hasTitle();
|
||||
|
||||
await _seedVerificationPageRobot.verifyWalletSeeds();
|
||||
}
|
||||
|
||||
//* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Seeds Action
|
||||
Future<void> _restoreFromSeeds(WalletType type, String walletSeed) async {
|
||||
// ----------- RestoreFromSeedOrKeys Page -------------
|
||||
|
||||
await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions();
|
||||
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(walletSeed);
|
||||
|
||||
final numberOfWords = walletSeed.split(' ').length;
|
||||
|
||||
if (numberOfWords == 25 && (type == WalletType.monero)) {
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy);
|
||||
|
||||
// Using a constant value of 2831400 for the blockheight as its the restore blockheight for our testing wallet
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.enterBlockHeightForWalletRestore(secrets.moneroTestWalletBlockHeight);
|
||||
}
|
||||
|
||||
if (Platform.isLinux) {
|
||||
// manual pin input
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.enterPasswordForWalletRestore(CommonTestConstants.pin.join(""));
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.enterPasswordRepeatForWalletRestore(CommonTestConstants.pin.join(""));
|
||||
}
|
||||
|
||||
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
|
||||
}
|
||||
|
||||
//* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Keys Action
|
||||
Future<void> _restoreFromKeys() async {
|
||||
await _commonTestCases.swipePage();
|
||||
await _commonTestCases.defaultSleepTime();
|
||||
|
||||
await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions(
|
||||
isSeedFormEntry: false,
|
||||
);
|
||||
|
||||
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore('');
|
||||
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
|
||||
}
|
||||
|
||||
//* ====== Utility Function to get test wallet seeds for each wallet type ========
|
||||
String getWalletSeedsByWalletType(WalletType walletType) {
|
||||
switch (walletType) {
|
||||
case WalletType.monero:
|
||||
return secrets.moneroTestWalletSeeds;
|
||||
case WalletType.bitcoin:
|
||||
return secrets.bitcoinTestWalletSeeds;
|
||||
case WalletType.ethereum:
|
||||
return secrets.ethereumTestWalletSeeds;
|
||||
case WalletType.litecoin:
|
||||
return secrets.litecoinTestWalletSeeds;
|
||||
case WalletType.bitcoinCash:
|
||||
return secrets.bitcoinCashTestWalletSeeds;
|
||||
case WalletType.polygon:
|
||||
return secrets.polygonTestWalletSeeds;
|
||||
case WalletType.solana:
|
||||
return secrets.solanaTestWalletSeeds;
|
||||
case WalletType.base:
|
||||
return secrets.baseTestWalletSeeds;
|
||||
case WalletType.arbitrum:
|
||||
return secrets.arbitrumTestWalletSeeds;
|
||||
case WalletType.bsc:
|
||||
return secrets.bscTestWalletSeeds;
|
||||
case WalletType.tron:
|
||||
return secrets.tronTestWalletSeeds;
|
||||
case WalletType.nano:
|
||||
return secrets.nanoTestWalletSeeds;
|
||||
case WalletType.wownero:
|
||||
return secrets.wowneroTestWalletSeeds;
|
||||
case WalletType.zano:
|
||||
return secrets.zanoTestWalletSeeds;
|
||||
case WalletType.decred:
|
||||
return secrets.decredTestWalletSeeds;
|
||||
case WalletType.dogecoin:
|
||||
return secrets.dogeTestWalletSeeds;
|
||||
case WalletType.zcash:
|
||||
return secrets.zcashTestWalletSeeds;
|
||||
case WalletType.none:
|
||||
case WalletType.haven:
|
||||
case WalletType.banano:
|
||||
throw Exception("Unable to get seeds for ${walletType}");
|
||||
}
|
||||
}
|
||||
|
||||
//* ====== Utility Function to get test receive address for each wallet type ========
|
||||
String getReceiveAddressByWalletType(WalletType walletType) {
|
||||
switch (walletType) {
|
||||
case WalletType.monero:
|
||||
return secrets.moneroTestWalletReceiveAddress;
|
||||
case WalletType.bitcoin:
|
||||
return secrets.bitcoinTestWalletReceiveAddress;
|
||||
case WalletType.ethereum:
|
||||
return secrets.ethereumTestWalletReceiveAddress;
|
||||
case WalletType.litecoin:
|
||||
return secrets.litecoinTestWalletReceiveAddress;
|
||||
case WalletType.bitcoinCash:
|
||||
return secrets.bitcoinCashTestWalletReceiveAddress;
|
||||
case WalletType.polygon:
|
||||
return secrets.polygonTestWalletReceiveAddress;
|
||||
case WalletType.base:
|
||||
return secrets.baseTestWalletReceiveAddress;
|
||||
case WalletType.arbitrum:
|
||||
return secrets.arbitrumTestWalletReceiveAddress;
|
||||
case WalletType.bsc:
|
||||
return secrets.bscTestWalletReceiveAddress;
|
||||
case WalletType.solana:
|
||||
return secrets.solanaTestWalletReceiveAddress;
|
||||
case WalletType.tron:
|
||||
return secrets.tronTestWalletReceiveAddress;
|
||||
case WalletType.nano:
|
||||
return secrets.nanoTestWalletReceiveAddress;
|
||||
case WalletType.wownero:
|
||||
return secrets.wowneroTestWalletReceiveAddress;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:cake_wallet/main.dart" as app;
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "benign_errors.dart";
|
||||
|
||||
void integrationTest(String description, Future<void> Function(WidgetTester tester) body) {
|
||||
testWidgets(description, (tester) async {
|
||||
final completer = Completer<void>();
|
||||
|
||||
final run = runZonedGuarded(() async {
|
||||
try {
|
||||
await body(tester);
|
||||
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete();
|
||||
}
|
||||
} catch (error, stack) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(error, stack);
|
||||
}
|
||||
}
|
||||
}, (error, stack) {
|
||||
final kind = benignErrorKind(error.toString());
|
||||
|
||||
if (kind != null) {
|
||||
debugPrint("Ignoring benign $kind background error: $error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (completer.isCompleted) {
|
||||
debugPrint("Background error after the test finished: $error");
|
||||
return;
|
||||
}
|
||||
|
||||
completer.completeError(error, stack);
|
||||
});
|
||||
|
||||
unawaited(run);
|
||||
|
||||
await completer.future;
|
||||
});
|
||||
}
|
||||
|
||||
class AppLauncher {
|
||||
AppLauncher(this.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
|
||||
Future<void> launchApp({required String testKey}) async {
|
||||
final bindingHandler = FlutterError.onError;
|
||||
|
||||
await app.main(topLevelKey: ValueKey(testKey));
|
||||
|
||||
_installTestErrorHandler(bindingHandler);
|
||||
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
}
|
||||
|
||||
void _installTestErrorHandler(FlutterExceptionHandler? bindingHandler) {
|
||||
FlutterError.onError = (details) {
|
||||
final reportedByTestFramework = details.library == "Flutter test framework";
|
||||
final kind = reportedByTestFramework ? null : benignErrorKind(details.exceptionAsString());
|
||||
|
||||
if (kind != null) {
|
||||
debugPrint("Ignoring benign $kind error: ${details.exceptionAsString()}");
|
||||
return;
|
||||
}
|
||||
|
||||
bindingHandler?.call(details);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "benign_errors.dart";
|
||||
|
||||
abstract class BaseRobot {
|
||||
BaseRobot(this.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
|
||||
Future<void> isDisplayed();
|
||||
|
||||
void drainBackgroundErrors() {
|
||||
final Object? pending = tester.takeException();
|
||||
|
||||
if (pending == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final kind = benignErrorKind(pending.toString());
|
||||
|
||||
if (kind != null) {
|
||||
tester.printToConsole("Ignoring benign $kind background error: $pending");
|
||||
return;
|
||||
}
|
||||
|
||||
throw TestFailure("Unhandled background error: $pending");
|
||||
}
|
||||
|
||||
Future<void> _pumpStep(Duration step) async {
|
||||
await tester.pump(step);
|
||||
|
||||
drainBackgroundErrors();
|
||||
}
|
||||
|
||||
Future<void> pumpUntilFound(
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 30),
|
||||
Duration step = const Duration(milliseconds: 100),
|
||||
}) async {
|
||||
final endTime = DateTime.now().add(timeout);
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
await _pumpStep(step);
|
||||
|
||||
if (tester.any(finder)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await takeScreenshot("${runtimeType}_pump_until_found_timeout");
|
||||
throw TestFailure("Widget not found after ${timeout.inSeconds}s: $finder");
|
||||
}
|
||||
|
||||
Future<void> pumpUntilGone(
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 30),
|
||||
Duration step = const Duration(milliseconds: 100),
|
||||
}) async {
|
||||
final endTime = DateTime.now().add(timeout);
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
await _pumpStep(step);
|
||||
|
||||
if (!tester.any(finder)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await takeScreenshot("${runtimeType}_pump_until_gone_timeout");
|
||||
throw TestFailure("Widget still present after ${timeout.inSeconds}s: $finder");
|
||||
}
|
||||
|
||||
Future<void> tapByKey(String key, {Duration timeout = const Duration(seconds: 30)}) async {
|
||||
final finder = find.byKey(ValueKey(key));
|
||||
|
||||
await pumpUntilFound(finder, timeout: timeout);
|
||||
|
||||
// A tap during a route transition lands on the animating overlay instead of the widget
|
||||
await settle(max: const Duration(seconds: 2));
|
||||
|
||||
await tester.tap(finder.first, warnIfMissed: false);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> tapWhenVisible(
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 30),
|
||||
}) async {
|
||||
await pumpUntilFound(finder, timeout: timeout);
|
||||
|
||||
await settle(max: const Duration(seconds: 2));
|
||||
|
||||
await tester.tap(finder.first, warnIfMissed: false);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> enterTextByKey(String key, String text) async {
|
||||
final finder = find.byKey(ValueKey(key));
|
||||
|
||||
await pumpUntilFound(finder);
|
||||
|
||||
await tester.enterText(finder.first, text);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<bool> pumpUntil(
|
||||
bool Function() condition, {
|
||||
Duration timeout = const Duration(seconds: 30),
|
||||
Duration step = const Duration(milliseconds: 100),
|
||||
}) async {
|
||||
final endTime = DateTime.now().add(timeout);
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
await _pumpStep(step);
|
||||
|
||||
if (condition()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> goBack() async {
|
||||
final navigator = tester.state<NavigatorState>(find.byType(Navigator).last);
|
||||
navigator.pop();
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
Future<void> dismissModal() async {
|
||||
final navigator = tester.state<NavigatorState>(find.byType(Navigator).first);
|
||||
navigator.pop();
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
// We make this bounded instead of pumpAndSettle
|
||||
Future<void> settle({Duration max = const Duration(seconds: 3)}) async {
|
||||
final endTime = DateTime.now().add(max);
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
await _pumpStep(const Duration(milliseconds: 100));
|
||||
|
||||
if (!tester.binding.hasScheduledFrame) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> isSpecificPage<T>({Duration timeout = const Duration(seconds: 30)}) async {
|
||||
await pumpUntilFound(find.byType(T), timeout: timeout);
|
||||
|
||||
await settle();
|
||||
|
||||
hasType<T>();
|
||||
}
|
||||
|
||||
bool isKeyPresent(String key) => tester.any(find.byKey(ValueKey(key)));
|
||||
|
||||
String describeScreen() {
|
||||
final names = tester.allWidgets
|
||||
.map((widget) => widget.runtimeType.toString())
|
||||
.where((name) => name.endsWith("Page") || name.endsWith("Sheet") || name.endsWith("Screen"))
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
return names.join(", ");
|
||||
}
|
||||
|
||||
void hasType<T>() {
|
||||
expect(find.byType(T), findsOneWidget);
|
||||
}
|
||||
|
||||
void hasText(String text) {
|
||||
expect(find.text(text), findsOneWidget);
|
||||
}
|
||||
|
||||
void hasTextAtLeastOnce(String text) {
|
||||
expect(find.text(text), findsAny);
|
||||
}
|
||||
|
||||
String? textByKey(String key) {
|
||||
final finder = find.byKey(ValueKey(key));
|
||||
|
||||
if (!tester.any(finder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tester.widget<Text>(finder.first).data;
|
||||
}
|
||||
|
||||
static bool _surfaceConverted = false;
|
||||
|
||||
Future<void> takeScreenshot(String name) async {
|
||||
try {
|
||||
final binding = tester.binding;
|
||||
|
||||
if (binding is! IntegrationTestWidgetsFlutterBinding) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_surfaceConverted) {
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
_surfaceConverted = true;
|
||||
}
|
||||
|
||||
var finished = false;
|
||||
|
||||
unawaited(
|
||||
binding.takeScreenshot("${name}_${DateTime.now().millisecondsSinceEpoch}").then(
|
||||
(_) => finished = true,
|
||||
onError: (Object error) {
|
||||
finished = true;
|
||||
tester.printToConsole("Screenshot $name failed: $error");
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final deadline = DateTime.now().add(const Duration(seconds: 10));
|
||||
|
||||
while (!finished && DateTime.now().isBefore(deadline)) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
if (!finished) {
|
||||
tester.printToConsole("Screenshot $name gave up after 10s");
|
||||
}
|
||||
} catch (e) {
|
||||
tester.printToConsole("Screenshot $name failed: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
String? benignErrorKind(String message) {
|
||||
if (_toleratedAssertions.any(message.contains)) {
|
||||
return "assertion";
|
||||
}
|
||||
|
||||
if (_toleratedAssets.any(message.contains)) {
|
||||
return "asset";
|
||||
}
|
||||
|
||||
if (_toleratedNetworkNoise.any(message.contains)) {
|
||||
return "network";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Debug build assertions. The semantics ones are real accessibility bugs that already
|
||||
// exist on dev, take them off this list once they are fixed.
|
||||
const List<String> _toleratedAssertions = [
|
||||
"overflowed by",
|
||||
"minValue, and maxValue must be valid numbers",
|
||||
"node.parent?._dirty",
|
||||
"RenderBox was not laid out",
|
||||
];
|
||||
|
||||
// CakeImageWidget reports these before falling back to the svg
|
||||
const List<String> _toleratedAssets = [
|
||||
"Unable to load asset",
|
||||
"NetworkImageLoadException",
|
||||
"Failed to load network image",
|
||||
];
|
||||
|
||||
// The same failures exception_handler.dart ignores
|
||||
const List<String> _toleratedNetworkNoise = [
|
||||
"SocketException",
|
||||
"HttpException",
|
||||
"ClientException",
|
||||
"HandshakeException",
|
||||
"TimeoutException",
|
||||
"Failed host lookup",
|
||||
"Connection closed",
|
||||
"Connection reset by peer",
|
||||
"Connection refused",
|
||||
"Connection timed out",
|
||||
"Operation timed out",
|
||||
"Network is unreachable",
|
||||
"No route to host",
|
||||
"Broken pipe",
|
||||
"Software caused connection abort",
|
||||
"Connection closed before full header",
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
class FundedWallet {
|
||||
const FundedWallet({required this.seed, this.passphrase = ""});
|
||||
|
||||
final String seed;
|
||||
final String passphrase;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import "funded_wallet.dart";
|
||||
|
||||
// Empty on purpose and it has to stay empty in git. The manual funds workflow overwrites
|
||||
// this file from the FUNDS_SECRETS_FILE secret, keyed by WalletType name.
|
||||
const Map<String, List<FundedWallet>> fundedWallets = {};
|
||||
@@ -0,0 +1,46 @@
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
class FundsOutcome {
|
||||
static const marker = "FUNDS_RESULT";
|
||||
|
||||
static void ok(WidgetTester tester, String chain, String detail) =>
|
||||
_write(tester, chain, "ok", detail);
|
||||
|
||||
static void empty(WidgetTester tester, String chain, String detail) =>
|
||||
_write(tester, chain, "empty", detail);
|
||||
|
||||
static void waiting(WidgetTester tester, String chain, String detail) =>
|
||||
_write(tester, chain, "waiting", detail);
|
||||
|
||||
static void failed(WidgetTester tester, String chain, String detail) =>
|
||||
_write(tester, chain, statusFor(detail), detail);
|
||||
|
||||
static String statusFor(String detail) {
|
||||
if (_isWalletBalanceLow(detail)) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return _isChainTooSlowToSync(detail) ? "waiting" : "failed";
|
||||
}
|
||||
|
||||
static bool needsFurtherReview(String detail) => statusFor(detail) == "failed";
|
||||
|
||||
static bool _isChainTooSlowToSync(String detail) =>
|
||||
detail.toLowerCase().contains("never became ready to send");
|
||||
|
||||
// Low wallet balance is not a failure, we just need to add fundsss
|
||||
static bool _isWalletBalanceLow(String detail) {
|
||||
final wording = detail.toLowerCase();
|
||||
|
||||
return wording.contains("not enough") ||
|
||||
wording.contains("wrong balance") ||
|
||||
wording.contains("insufficient") ||
|
||||
wording.contains("no spendable");
|
||||
}
|
||||
|
||||
static void _write(WidgetTester tester, String chain, String status, String detail) {
|
||||
final clean = detail.replaceAll("\n", " ").replaceAll("|", "/").trim();
|
||||
|
||||
tester.printToConsole("$marker|$chain|$status|$clean");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import "package:cake_wallet/wallet_types.g.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
|
||||
import "test_wallets.dart";
|
||||
|
||||
class TestConfig {
|
||||
static final List<int> pin = [0, 8, 0, 1];
|
||||
|
||||
static const bool isCIBuild = bool.fromEnvironment("CI_BUILD");
|
||||
|
||||
static const String _walletTypesOverride = String.fromEnvironment("TEST_WALLET_TYPES");
|
||||
|
||||
// Solana and ethereum cover the cheap key-derivation chains,
|
||||
// bitcoin covers the electrum family and monero covers the native-library chains.
|
||||
static final List<WalletType> _representativeWalletTypes = [
|
||||
WalletType.solana,
|
||||
WalletType.ethereum,
|
||||
WalletType.bitcoin,
|
||||
WalletType.monero,
|
||||
];
|
||||
|
||||
static List<WalletType> get walletTypesUnderTest {
|
||||
if (_walletTypesOverride == "all") {
|
||||
return availableWalletTypes;
|
||||
}
|
||||
|
||||
if (_walletTypesOverride.isNotEmpty) {
|
||||
return _walletTypesOverride
|
||||
.split(",")
|
||||
.map((name) => WalletType.values.byName(name.trim()))
|
||||
.where(availableWalletTypes.contains)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return _representativeWalletTypes.where(availableWalletTypes.contains).toList();
|
||||
}
|
||||
|
||||
static const String _fundsFlows = String.fromEnvironment("FLOWS", defaultValue: "all");
|
||||
|
||||
static const String _spend = String.fromEnvironment("SPEND", defaultValue: "false");
|
||||
|
||||
static const String _swapFrom = String.fromEnvironment("SWAP_FROM");
|
||||
|
||||
static const String swapReceive = String.fromEnvironment("SWAP_RECEIVE");
|
||||
|
||||
static WalletType swapDepositType(List<WalletType> funded) =>
|
||||
_swapFrom.isEmpty ? funded.first : _walletTypeByName(_swapFrom, "SWAP_FROM");
|
||||
|
||||
static bool shouldRunFundsFlow(String flow) => _fundsFlows == "all" || _fundsFlows == flow;
|
||||
|
||||
static bool shouldDryRun(String flow) => shouldRunFundsFlow(flow);
|
||||
|
||||
static bool shouldSpend(String flow) => _spend == "true" && shouldRunFundsFlow(flow);
|
||||
|
||||
static const String _fundedChainsOverride = String.fromEnvironment(
|
||||
"CHAINS",
|
||||
defaultValue: "auto",
|
||||
);
|
||||
|
||||
static List<WalletType> get fundedWalletTypesUnderTest {
|
||||
if (_fundedChainsOverride == "auto") {
|
||||
return TestWallets.fundedWalletTypes;
|
||||
}
|
||||
|
||||
return _fundedChainsOverride
|
||||
.split(",")
|
||||
.map((name) => _walletTypeByName(name, "CHAINS"))
|
||||
.where((type) => TestWallets.fundedWalletsFor(type).isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static const Map<String, String> _fundsSendAmounts = {
|
||||
"solana": "0.0001",
|
||||
"ethereum": "0.00001",
|
||||
"polygon": "0.01",
|
||||
"base": "0.00001",
|
||||
"arbitrum": "0.00001",
|
||||
"bsc": "0.000001",
|
||||
"tron": "1",
|
||||
"bitcoin": "0.00001",
|
||||
"litecoin": "0.001",
|
||||
"bitcoinCash": "0.0002",
|
||||
"dogecoin": "2",
|
||||
"monero": "0.0001",
|
||||
"wownero": "0.1",
|
||||
"zcash": "0.0001",
|
||||
"nano": "0.000001",
|
||||
"decred": "0.001",
|
||||
"zano": "0.001",
|
||||
};
|
||||
|
||||
static String fundsSendAmountFor(WalletType type) => _fundsSendAmounts[type.name] ?? "0.0001";
|
||||
|
||||
// Making this two hours to accommodate monero that takes a longgg time syncing
|
||||
// also, this would only affect the funded tests which need actual funds to test
|
||||
static const Duration walletSyncBudget = Duration(hours: 2);
|
||||
|
||||
static WalletType _walletTypeByName(String name, String input) {
|
||||
final trimmed = name.trim();
|
||||
|
||||
final type = WalletType.values.where((value) => value.name == trimmed).toList();
|
||||
|
||||
if (type.isEmpty) {
|
||||
throw ArgumentError(
|
||||
'Unknown wallet type "$trimmed" in $input, '
|
||||
"valid names: ${WalletType.values.map((value) => value.name).join(", ")}",
|
||||
);
|
||||
}
|
||||
|
||||
return type.first;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import "package:cake_wallet/.secrets.g.dart" as secrets;
|
||||
import "package:cake_wallet/wallet_types.g.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
|
||||
import "funded_wallet.dart";
|
||||
import "funded_wallets.dart";
|
||||
|
||||
class TestWallets {
|
||||
static List<WalletType> get fundedWalletTypes =>
|
||||
availableWalletTypes.where((type) => fundedWalletsFor(type).isNotEmpty).toList();
|
||||
|
||||
static List<FundedWallet> fundedWalletsFor(WalletType type) =>
|
||||
(fundedWallets[type.name] ?? []).where((wallet) => wallet.seed.trim().isNotEmpty).toList();
|
||||
|
||||
static String seedFor(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return secrets.moneroTestWalletSeeds;
|
||||
case WalletType.bitcoin:
|
||||
return secrets.bitcoinTestWalletSeeds;
|
||||
case WalletType.ethereum:
|
||||
return secrets.ethereumTestWalletSeeds;
|
||||
case WalletType.litecoin:
|
||||
return secrets.litecoinTestWalletSeeds;
|
||||
case WalletType.bitcoinCash:
|
||||
return secrets.bitcoinCashTestWalletSeeds;
|
||||
case WalletType.polygon:
|
||||
return secrets.polygonTestWalletSeeds;
|
||||
case WalletType.solana:
|
||||
return secrets.solanaTestWalletSeeds;
|
||||
case WalletType.base:
|
||||
return secrets.baseTestWalletSeeds;
|
||||
case WalletType.arbitrum:
|
||||
return secrets.arbitrumTestWalletSeeds;
|
||||
case WalletType.bsc:
|
||||
return secrets.bscTestWalletSeeds;
|
||||
case WalletType.tron:
|
||||
return secrets.tronTestWalletSeeds;
|
||||
case WalletType.nano:
|
||||
return secrets.nanoTestWalletSeeds;
|
||||
case WalletType.wownero:
|
||||
return secrets.wowneroTestWalletSeeds;
|
||||
case WalletType.zano:
|
||||
return secrets.zanoTestWalletSeeds;
|
||||
case WalletType.decred:
|
||||
return secrets.decredTestWalletSeeds;
|
||||
case WalletType.dogecoin:
|
||||
return secrets.dogeTestWalletSeeds;
|
||||
case WalletType.zcash:
|
||||
return secrets.zcashTestWalletSeeds;
|
||||
case WalletType.none:
|
||||
case WalletType.haven:
|
||||
case WalletType.banano:
|
||||
throw Exception("No test wallet seed available for ${type.name}");
|
||||
}
|
||||
}
|
||||
|
||||
static String receiveAddressFor(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return secrets.moneroTestWalletReceiveAddress;
|
||||
case WalletType.bitcoin:
|
||||
return secrets.bitcoinTestWalletReceiveAddress;
|
||||
case WalletType.ethereum:
|
||||
return secrets.ethereumTestWalletReceiveAddress;
|
||||
case WalletType.litecoin:
|
||||
return secrets.litecoinTestWalletReceiveAddress;
|
||||
case WalletType.bitcoinCash:
|
||||
return secrets.bitcoinCashTestWalletReceiveAddress;
|
||||
case WalletType.polygon:
|
||||
return secrets.polygonTestWalletReceiveAddress;
|
||||
case WalletType.base:
|
||||
return secrets.baseTestWalletReceiveAddress;
|
||||
case WalletType.arbitrum:
|
||||
return secrets.arbitrumTestWalletReceiveAddress;
|
||||
case WalletType.bsc:
|
||||
return secrets.bscTestWalletReceiveAddress;
|
||||
case WalletType.solana:
|
||||
return secrets.solanaTestWalletReceiveAddress;
|
||||
case WalletType.tron:
|
||||
return secrets.tronTestWalletReceiveAddress;
|
||||
case WalletType.nano:
|
||||
return secrets.nanoTestWalletReceiveAddress;
|
||||
case WalletType.wownero:
|
||||
return secrets.wowneroTestWalletReceiveAddress;
|
||||
case WalletType.zano:
|
||||
case WalletType.decred:
|
||||
case WalletType.dogecoin:
|
||||
case WalletType.zcash:
|
||||
case WalletType.none:
|
||||
case WalletType.haven:
|
||||
case WalletType.banano:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static String get moneroLegacySeed => secrets.moneroLegacyTestWalletSeeds;
|
||||
|
||||
static String get moneroRestoreBlockHeight => secrets.moneroTestWalletBlockHeight;
|
||||
|
||||
static String get zcashRestoreHeight => secrets.zcashTestWalletRestoreHeight;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/test_config.dart";
|
||||
import "../robots/auth_page_robot.dart";
|
||||
|
||||
class AuthFlows {
|
||||
AuthFlows(this.tester) : _authPageRobot = AuthPageRobot(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final AuthPageRobot _authPageRobot;
|
||||
|
||||
Future<void> authenticateWithPin({List<int>? pin, bool required = true}) async {
|
||||
final prompted = await _waitForPinWidget(
|
||||
timeout: required ? const Duration(seconds: 30) : const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
if (!prompted) {
|
||||
expect(required, false, reason: "The pin was never asked for");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await _authPageRobot.enterPinCode(pin ?? TestConfig.pin);
|
||||
}
|
||||
|
||||
bool isPinWidgetShown() => _authPageRobot.onAuthPage();
|
||||
|
||||
Future<bool> _waitForPinWidget({Duration timeout = const Duration(seconds: 30)}) async {
|
||||
final endTime = DateTime.now().add(timeout);
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
if (_authPageRobot.onAuthPage()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/sync_status.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/test_config.dart";
|
||||
import "../core/test_wallets.dart";
|
||||
import "../robots/home_page_robot.dart";
|
||||
import "../robots/new_dashboard_robot.dart";
|
||||
import "../robots/new_send_page_robot.dart";
|
||||
import "onboarding_flows.dart";
|
||||
|
||||
class FundsFlows {
|
||||
FundsFlows(this.tester)
|
||||
: _onboardingFlows = OnboardingFlows(tester),
|
||||
_dashboardRobot = NewDashboardRobot(tester),
|
||||
_homePageRobot = HomePageRobot(tester),
|
||||
_sendRobot = NewSendPageRobot(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final OnboardingFlows _onboardingFlows;
|
||||
final NewDashboardRobot _dashboardRobot;
|
||||
final HomePageRobot _homePageRobot;
|
||||
final NewSendPageRobot _sendRobot;
|
||||
|
||||
bool _hasRestoredAnyWallet = false;
|
||||
|
||||
Future<bool> openFundedWallet(WalletType type) async {
|
||||
final wallets = TestWallets.fundedWalletsFor(type);
|
||||
|
||||
for (int index = 0; index < wallets.length; index++) {
|
||||
final wallet = wallets[index];
|
||||
|
||||
tester.printToConsole("Trying funded wallet ${index + 1}/${wallets.length} for ${type.name}");
|
||||
|
||||
if (_hasRestoredAnyWallet) {
|
||||
await _dashboardRobot.openWalletsTab();
|
||||
|
||||
final isWalletListReady = await _dashboardRobot.pumpUntil(
|
||||
() =>
|
||||
tester.any(find.byKey(const ValueKey("wallet_list_page_restore_wallet_button_key"))),
|
||||
timeout: const Duration(seconds: 60),
|
||||
);
|
||||
|
||||
expect(
|
||||
isWalletListReady,
|
||||
true,
|
||||
reason: "The wallet list restore button didn't show before ${type.name}, "
|
||||
"Currently have: ${_dashboardRobot.describeScreen()} on screen",
|
||||
);
|
||||
|
||||
await _onboardingFlows.restoreAdditionalWalletFromWalletList(
|
||||
type,
|
||||
seed: wallet.seed,
|
||||
passphrase: wallet.passphrase,
|
||||
);
|
||||
} else {
|
||||
await _onboardingFlows.restoreFirstWalletFromSeed(
|
||||
type,
|
||||
seed: wallet.seed,
|
||||
passphrase: wallet.passphrase,
|
||||
);
|
||||
}
|
||||
|
||||
_hasRestoredAnyWallet = true;
|
||||
|
||||
await _waitForDashboard(type);
|
||||
await _homePageRobot.isDisplayed();
|
||||
|
||||
if (await _hasSpendableBalance()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
tester.printToConsole("Wallet ${index + 1} for ${type.name} has no spendable balance");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> returnToHome({int maxPops = 6}) async {
|
||||
for (int attempt = 0; attempt <= maxPops; attempt++) {
|
||||
if (await _homePageRobot.pumpUntil(
|
||||
() => _isHomeInFront,
|
||||
timeout: const Duration(seconds: 3),
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await _homePageRobot.dismissModal();
|
||||
} catch (e) {
|
||||
tester.printToConsole("Nothing left to pop while returning home: $e");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _isHomeInFront;
|
||||
}
|
||||
|
||||
Future<void> _waitForDashboard(WalletType type) async {
|
||||
final reached = await _dashboardRobot.pumpUntil(
|
||||
() => tester.any(find.byKey(const ValueKey("new_dashboard_page_key"))),
|
||||
timeout: TestConfig.walletSyncBudget,
|
||||
);
|
||||
|
||||
if (reached) {
|
||||
return;
|
||||
}
|
||||
|
||||
final status = getIt.get<AppStore>().wallet?.syncStatus;
|
||||
final stillSyncing = status is SyncingSyncStatus || status is SyncronizingSyncStatus;
|
||||
|
||||
throw TestFailure(
|
||||
stillSyncing
|
||||
? "${type.name} never became ready to send, it is still syncing (${status.runtimeType})"
|
||||
: "${type.name} never reached the dashboard, its status was ${status.runtimeType}",
|
||||
);
|
||||
}
|
||||
|
||||
bool get _isHomeInFront => _homePageRobot.isInFront && !_sendRobot.isSendFlowOpen;
|
||||
|
||||
Future<bool> _hasSpendableBalance({Duration timeout = TestConfig.walletSyncBudget}) async {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final endTime = DateTime.now().add(timeout);
|
||||
DateTime? syncedSince;
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
await tester.pump(const Duration(milliseconds: 250));
|
||||
|
||||
final wallet = appStore.wallet;
|
||||
|
||||
if (wallet == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wallet.balance.values.any((balance) => balance.available.sign > 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wallet.syncStatus is SyncedSyncStatus) {
|
||||
syncedSince ??= DateTime.now();
|
||||
|
||||
if (DateTime.now().difference(syncedSince) > const Duration(seconds: 15)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
syncedSince = null;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:cake_wallet/entities/seed_type.dart";
|
||||
import "package:cake_wallet/reactions/wallet_utils.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/test_config.dart";
|
||||
import "../core/test_wallets.dart";
|
||||
import "../robots/create_pin_welcome_page_robot.dart";
|
||||
import "../robots/lightning_username_page_robot.dart";
|
||||
import "../robots/new_wallet_page_robot.dart";
|
||||
import "../robots/new_wallet_type_page_robot.dart";
|
||||
import "../robots/pre_seed_page_robot.dart";
|
||||
import "../robots/restore_from_seed_or_key_robot.dart";
|
||||
import "../robots/restore_options_page_robot.dart";
|
||||
import "../robots/seed_verification_page_robot.dart";
|
||||
import "../robots/setup_pin_code_robot.dart";
|
||||
import "../robots/wallet_group_description_page_robot.dart";
|
||||
import "../robots/wallet_group_existing_seed_page_robot.dart";
|
||||
import "../robots/wallet_groups_display_page_robot.dart";
|
||||
import "../robots/wallet_list_page_robot.dart";
|
||||
import "../robots/wallet_seed_page_robot.dart";
|
||||
import "../robots/welcome_page_robot.dart";
|
||||
|
||||
class OnboardingFlows {
|
||||
OnboardingFlows(this.tester)
|
||||
: _welcomePageRobot = WelcomePageRobot(tester),
|
||||
_preSeedPageRobot = PreSeedPageRobot(tester),
|
||||
_setupPinCodeRobot = SetupPinCodeRobot(tester),
|
||||
_newWalletPageRobot = NewWalletPageRobot(tester),
|
||||
_walletSeedPageRobot = WalletSeedPageRobot(tester),
|
||||
_walletListPageRobot = WalletListPageRobot(tester),
|
||||
_newWalletTypePageRobot = NewWalletTypePageRobot(tester),
|
||||
_seedVerificationPageRobot = SeedVerificationPageRobot(tester),
|
||||
_createPinWelcomePageRobot = CreatePinWelcomePageRobot(tester),
|
||||
_restoreOptionsPageRobot = RestoreOptionsPageRobot(tester),
|
||||
_restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(tester),
|
||||
_lightningUsernamePageRobot = LightningUsernamePageRobot(tester),
|
||||
_walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(tester),
|
||||
_walletGroupsDisplayPageRobot = WalletGroupsDisplayPageRobot(tester),
|
||||
_walletGroupExistingSeedPageRobot = WalletGroupExistingSeedPageRobot(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
|
||||
final WelcomePageRobot _welcomePageRobot;
|
||||
final PreSeedPageRobot _preSeedPageRobot;
|
||||
final SetupPinCodeRobot _setupPinCodeRobot;
|
||||
final NewWalletPageRobot _newWalletPageRobot;
|
||||
final WalletSeedPageRobot _walletSeedPageRobot;
|
||||
final WalletListPageRobot _walletListPageRobot;
|
||||
final NewWalletTypePageRobot _newWalletTypePageRobot;
|
||||
final SeedVerificationPageRobot _seedVerificationPageRobot;
|
||||
final CreatePinWelcomePageRobot _createPinWelcomePageRobot;
|
||||
final RestoreOptionsPageRobot _restoreOptionsPageRobot;
|
||||
final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
|
||||
final LightningUsernamePageRobot _lightningUsernamePageRobot;
|
||||
final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot;
|
||||
final WalletGroupsDisplayPageRobot _walletGroupsDisplayPageRobot;
|
||||
final WalletGroupExistingSeedPageRobot _walletGroupExistingSeedPageRobot;
|
||||
|
||||
Future<void> createFirstWallet(WalletType type, {List<int>? pin}) async {
|
||||
await _createPinWelcomePageRobot.tapSetAPinButton();
|
||||
|
||||
await setupPinCode(pin ?? TestConfig.pin);
|
||||
|
||||
await _welcomePageRobot.navigateToCreateNewWalletPage();
|
||||
|
||||
await _selectWalletType(type);
|
||||
|
||||
if (_welcomePageRobot.hasNewSingleSeedButton()) {
|
||||
await _welcomePageRobot.tapNewSingleSeed();
|
||||
}
|
||||
|
||||
await _completeWalletCreationSteps(type);
|
||||
}
|
||||
|
||||
Future<void> createAdditionalWalletFromWalletList(WalletType type) async {
|
||||
tester.printToConsole("Creating ${type.name} wallet");
|
||||
|
||||
await startCreatingWalletFromWalletList(type);
|
||||
|
||||
await _completeWalletCreationSteps(type);
|
||||
}
|
||||
|
||||
Future<void> createWalletInGroupOf(WalletType type, String existingWalletName) async {
|
||||
tester.printToConsole("Adding a ${type.name} wallet to the seed of $existingWalletName");
|
||||
|
||||
await _walletListPageRobot.navigateToCreateNewWalletPage();
|
||||
|
||||
await _selectWalletType(type);
|
||||
|
||||
await _walletGroupDescriptionPageRobot.isDisplayed();
|
||||
await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup();
|
||||
|
||||
await _walletGroupsDisplayPageRobot.isDisplayed();
|
||||
await _walletGroupsDisplayPageRobot.selectWallet(existingWalletName);
|
||||
await _walletGroupsDisplayPageRobot.continueWithSelectedSeed();
|
||||
|
||||
await _newWalletPageRobot.isDisplayed();
|
||||
await _newWalletPageRobot.generateWalletName();
|
||||
await _newWalletPageRobot.onNextButtonPressed();
|
||||
|
||||
await _walletGroupExistingSeedPageRobot.isDisplayed();
|
||||
await _walletGroupExistingSeedPageRobot.openWallet();
|
||||
}
|
||||
|
||||
Future<void> startCreatingWalletFromWalletList(WalletType type) async {
|
||||
await _walletListPageRobot.navigateToCreateNewWalletPage();
|
||||
|
||||
await _selectWalletType(type);
|
||||
|
||||
if (isBIP39Wallet(type)) {
|
||||
await _walletGroupDescriptionPageRobot.isDisplayed();
|
||||
await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage();
|
||||
}
|
||||
|
||||
await _newWalletPageRobot.isDisplayed();
|
||||
}
|
||||
|
||||
Future<void> restoreFirstWalletFromSeed(
|
||||
WalletType type, {
|
||||
String? seed,
|
||||
String passphrase = "",
|
||||
List<int>? pin,
|
||||
}) async {
|
||||
await startRestoringFirstWallet(type, pin: pin);
|
||||
|
||||
await _restoreFromSeed(type, seed ?? TestWallets.seedFor(type), passphrase);
|
||||
}
|
||||
|
||||
Future<void> startRestoringFirstWallet(WalletType type, {List<int>? pin}) async {
|
||||
await _createPinWelcomePageRobot.tapSetAPinButton();
|
||||
|
||||
await setupPinCode(pin ?? TestConfig.pin);
|
||||
|
||||
await _welcomePageRobot.navigateToRestoreWalletPage();
|
||||
|
||||
// Desktop skips the restore options page and goes straight to the type list.
|
||||
if (!Platform.isLinux) {
|
||||
await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
|
||||
}
|
||||
|
||||
await _selectWalletType(type);
|
||||
|
||||
await _restoreFromSeedOrKeysPageRobot.isDisplayed();
|
||||
}
|
||||
|
||||
Future<void> restoreAdditionalWalletFromWalletList(
|
||||
WalletType type, {
|
||||
String? seed,
|
||||
String passphrase = "",
|
||||
}) async {
|
||||
tester.printToConsole("Restoring ${type.name} wallet");
|
||||
|
||||
await _walletListPageRobot.navigateToRestoreWalletOptionsPage();
|
||||
|
||||
if (!Platform.isLinux) {
|
||||
await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
|
||||
}
|
||||
|
||||
await _selectWalletType(type);
|
||||
|
||||
await _restoreFromSeed(type, seed ?? TestWallets.seedFor(type), passphrase);
|
||||
}
|
||||
|
||||
Future<void> _restoreFromSeed(WalletType type, String seed, String passphrase) async {
|
||||
await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions();
|
||||
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(seed);
|
||||
|
||||
if (passphrase.isNotEmpty) {
|
||||
await _restoreFromSeedOrKeysPageRobot.enablePassphrase();
|
||||
}
|
||||
|
||||
// 25 word monero seeds are legacy seeds and need their restore block height entered.
|
||||
if (seed.split(" ").length == 25 && type == WalletType.monero) {
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy);
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.enterBlockHeightForWalletRestore(TestWallets.moneroRestoreBlockHeight);
|
||||
}
|
||||
|
||||
if (type == WalletType.zcash && TestWallets.zcashRestoreHeight.isNotEmpty) {
|
||||
await _restoreFromSeedOrKeysPageRobot
|
||||
.enterBlockHeightForWalletRestore(TestWallets.zcashRestoreHeight);
|
||||
}
|
||||
|
||||
if (Platform.isLinux) {
|
||||
final password = TestConfig.pin.join("");
|
||||
await _restoreFromSeedOrKeysPageRobot.enterPasswordForWalletRestore(password);
|
||||
await _restoreFromSeedOrKeysPageRobot.enterPasswordRepeatForWalletRestore(password);
|
||||
}
|
||||
|
||||
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
|
||||
|
||||
if (passphrase.isNotEmpty) {
|
||||
await _restoreFromSeedOrKeysPageRobot.enterPassphrase(passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setupPinCode(List<int> pin) async {
|
||||
// Desktop builds take a wallet password on the new wallet form instead of a pin.
|
||||
if (Platform.isLinux) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _setupPinCodeRobot.isDisplayed();
|
||||
|
||||
await _setupPinCodeRobot.enterPinCode(pin);
|
||||
await _setupPinCodeRobot.enterPinCode(pin);
|
||||
await _setupPinCodeRobot.tapSuccessButton();
|
||||
}
|
||||
|
||||
Future<void> _selectWalletType(WalletType type) async {
|
||||
await _newWalletTypePageRobot.isDisplayed();
|
||||
|
||||
await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type);
|
||||
await _newWalletTypePageRobot.selectWalletType(type);
|
||||
}
|
||||
|
||||
Future<void> _completeWalletCreationSteps(WalletType type) async {
|
||||
await _generateNewWalletDetails();
|
||||
|
||||
await _preSeedPageRobot.isDisplayed();
|
||||
await _preSeedPageRobot.onConfirmButtonPressed();
|
||||
|
||||
await _confirmWalletSeedDetails();
|
||||
|
||||
await _verifyWalletSeed();
|
||||
|
||||
if (type == WalletType.bitcoin) {
|
||||
await _lightningUsernamePageRobot.isDisplayed();
|
||||
await _lightningUsernamePageRobot.tapSkip();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _generateNewWalletDetails() async {
|
||||
await _newWalletPageRobot.isDisplayed();
|
||||
|
||||
await _newWalletPageRobot.generateWalletName();
|
||||
|
||||
if (Platform.isLinux) {
|
||||
final password = TestConfig.pin.join("");
|
||||
await _restoreFromSeedOrKeysPageRobot.enterPasswordForWalletRestore(password);
|
||||
await _restoreFromSeedOrKeysPageRobot.enterPasswordRepeatForWalletRestore(password);
|
||||
}
|
||||
|
||||
await _newWalletPageRobot.onNextButtonPressed();
|
||||
}
|
||||
|
||||
Future<void> _confirmWalletSeedDetails() async {
|
||||
await _walletSeedPageRobot.isDisplayed();
|
||||
|
||||
_walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly();
|
||||
_walletSeedPageRobot.confirmWalletSeedReminderDisplays();
|
||||
|
||||
await _walletSeedPageRobot.onVerifySeedButtonPressed();
|
||||
}
|
||||
|
||||
Future<void> _verifyWalletSeed() async {
|
||||
await _seedVerificationPageRobot.isDisplayed();
|
||||
|
||||
_seedVerificationPageRobot.hasTitle();
|
||||
|
||||
await _seedVerificationPageRobot.verifyWalletSeeds();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../robots/new_dashboard_robot.dart";
|
||||
import "auth_flows.dart";
|
||||
|
||||
class WalletFlows {
|
||||
WalletFlows(this.tester)
|
||||
: _authFlows = AuthFlows(tester),
|
||||
_dashboardRobot = NewDashboardRobot(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final AuthFlows _authFlows;
|
||||
final NewDashboardRobot _dashboardRobot;
|
||||
|
||||
Future<void> _expandWalletGroups({int maxGroups = 10}) async {
|
||||
for (int index = 0; index < maxGroups; index++) {
|
||||
final tile = find.byKey(ValueKey("group_wallets_expansion_tile_widget_$index"));
|
||||
|
||||
if (!tester.any(tile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.tap(tile, warnIfMissed: false);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> switchToWallet(String name) async {
|
||||
await _dashboardRobot.openWalletsTab();
|
||||
|
||||
final walletFinder = find.text(name);
|
||||
|
||||
// Wallets sharing a seed are listed under their group's name, so the wallet itself is
|
||||
// not on screen until that group is opened.
|
||||
if (!tester.any(walletFinder)) {
|
||||
await _expandWalletGroups();
|
||||
}
|
||||
|
||||
await _dashboardRobot.pumpUntilFound(walletFinder);
|
||||
|
||||
await tester.tap(walletFinder.first, warnIfMissed: false);
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
await _authFlows.authenticateWithPin(required: false);
|
||||
|
||||
// The row does nothing at all when that wallet is already the open one, and the load
|
||||
// itself runs behind a progress overlay, so the store is the only thing that tells us
|
||||
// the switch happened.
|
||||
final switched = await _dashboardRobot.pumpUntil(
|
||||
() => getIt.get<AppStore>().wallet?.name == name,
|
||||
timeout: const Duration(minutes: 2),
|
||||
);
|
||||
|
||||
expect(switched, true, reason: "Wallet never became $name after tapping its row");
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'components/common_test_constants.dart';
|
||||
import 'components/common_test_flows.dart';
|
||||
import 'robots/auth_page_robot.dart';
|
||||
import 'robots/dashboard_page_robot.dart';
|
||||
import 'robots/exchange_confirm_page_robot.dart';
|
||||
import 'robots/exchange_page_robot.dart';
|
||||
import 'robots/exchange_trade_page_robot.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
DashboardPageRobot dashboardPageRobot;
|
||||
ExchangePageRobot exchangePageRobot;
|
||||
ExchangeConfirmPageRobot exchangeConfirmPageRobot;
|
||||
AuthPageRobot authPageRobot;
|
||||
ExchangeTradePageRobot exchangeTradePageRobot;
|
||||
CommonTestFlows commonTestFlows;
|
||||
|
||||
group('Startup Test', () {
|
||||
testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL',
|
||||
(tester) async {
|
||||
authPageRobot = AuthPageRobot(tester);
|
||||
exchangePageRobot = ExchangePageRobot(tester);
|
||||
dashboardPageRobot = DashboardPageRobot(tester);
|
||||
exchangeTradePageRobot = ExchangeTradePageRobot(tester);
|
||||
exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
|
||||
commonTestFlows = CommonTestFlows(tester);
|
||||
|
||||
await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key'));
|
||||
|
||||
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
|
||||
CommonTestConstants.testWalletType,
|
||||
secrets.solanaTestWalletSeeds,
|
||||
CommonTestConstants.pin,
|
||||
);
|
||||
|
||||
// ----------- RestoreFromSeedOrKeys Page -------------
|
||||
await dashboardPageRobot.navigateToExchangePage();
|
||||
|
||||
// ----------- Exchange Page -------------
|
||||
await exchangePageRobot.isExchangePage();
|
||||
exchangePageRobot.hasResetButton();
|
||||
await exchangePageRobot.displayBothExchangeCards();
|
||||
exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards();
|
||||
exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards();
|
||||
|
||||
await exchangePageRobot
|
||||
.selectDepositCurrency(CommonTestConstants.exchangeTestDepositCurrency);
|
||||
await exchangePageRobot
|
||||
.selectReceiveCurrency(CommonTestConstants.exchangeTestReceiveCurrency);
|
||||
|
||||
await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
|
||||
await exchangePageRobot.enterDepositRefundAddress(
|
||||
depositAddress: CommonTestConstants.testWalletAddress);
|
||||
|
||||
await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
|
||||
|
||||
await exchangePageRobot.onExchangeButtonPressed();
|
||||
|
||||
await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
|
||||
|
||||
final onAuthPage = authPageRobot.onAuthPage();
|
||||
if (onAuthPage) {
|
||||
await authPageRobot.enterPinCode(CommonTestConstants.pin);
|
||||
}
|
||||
|
||||
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
|
||||
if (onAuthPageDesktop) {
|
||||
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
|
||||
}
|
||||
|
||||
// ----------- Exchange Confirm Page -------------
|
||||
await exchangeConfirmPageRobot.isExchangeConfirmPage();
|
||||
|
||||
exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly();
|
||||
await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly();
|
||||
await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
|
||||
|
||||
// ----------- Exchange Trade Page -------------
|
||||
await exchangeTradePageRobot.isExchangeTradePage();
|
||||
exchangeTradePageRobot.hasInformationDialog();
|
||||
await exchangeTradePageRobot.onGotItButtonPressed();
|
||||
|
||||
await exchangeTradePageRobot.onSendFromCakeButtonPressed();
|
||||
|
||||
await exchangeTradePageRobot.handleConfirmSendResult();
|
||||
|
||||
await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../core/app_launcher.dart";
|
||||
import "../core/funds_outcome.dart";
|
||||
import "../core/test_config.dart";
|
||||
import "../flows/auth_flows.dart";
|
||||
import "../flows/funds_flows.dart";
|
||||
import "../robots/home_page_robot.dart";
|
||||
import "../robots/new_send_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Every funded chain can build a transaction without sending it", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final authFlows = AuthFlows(tester);
|
||||
final fundsFlows = FundsFlows(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final sendRobot = NewSendPageRobot(tester);
|
||||
|
||||
if (!TestConfig.shouldDryRun("send")) {
|
||||
tester.printToConsole("FLOWS excludes send, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
|
||||
|
||||
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
|
||||
|
||||
await appLauncher.launchApp(testKey: "send_dry_run_test_app_key");
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final failures = <String>[];
|
||||
|
||||
for (final type in walletTypes) {
|
||||
tester.printToConsole("Send dry run starting for ${type.name}");
|
||||
|
||||
try {
|
||||
final opened = await fundsFlows.openFundedWallet(type);
|
||||
|
||||
if (!opened) {
|
||||
tester.printToConsole("${type.name} has nothing spendable, top it up");
|
||||
FundsOutcome.empty(tester, type.name, "every funded wallet is empty");
|
||||
continue;
|
||||
}
|
||||
|
||||
expect(appStore.wallet?.type, type);
|
||||
|
||||
final ownAddress = appStore.wallet!.walletAddresses.address;
|
||||
|
||||
await homePageRobot.openSendSheet();
|
||||
await sendRobot.isDisplayed();
|
||||
|
||||
await sendRobot.enterAddress(ownAddress);
|
||||
await sendRobot.enterAmount(TestConfig.fundsSendAmountFor(type));
|
||||
await sendRobot.tapSendButtonWhenReady();
|
||||
|
||||
await authFlows.authenticateWithPin(required: false);
|
||||
|
||||
await sendRobot.confirmTransactionBuilt();
|
||||
|
||||
await sendRobot.confirmSendButtonReplacesSwiper();
|
||||
|
||||
tester.printToConsole("${type.name} built a transaction, leaving it unsent");
|
||||
FundsOutcome.ok(tester, type.name, "built and signed, not broadcast");
|
||||
|
||||
expect(
|
||||
await fundsFlows.returnToHome(),
|
||||
true,
|
||||
reason: "${type.name} finished but the send flow would not close",
|
||||
);
|
||||
} catch (e) {
|
||||
if (FundsOutcome.needsFurtherReview(e.toString())) {
|
||||
failures.add("${type.name}: $e");
|
||||
} else {
|
||||
tester.printToConsole("${type.name} needs attention rather than a fix: $e");
|
||||
}
|
||||
|
||||
FundsOutcome.failed(tester, type.name, e.toString());
|
||||
|
||||
if (!await fundsFlows.returnToHome()) {
|
||||
// The app is hanging or found itself somewhere we don't know,
|
||||
// no need to continue running anything else
|
||||
for (final skipped in walletTypes.skip(walletTypes.indexOf(type) + 1)) {
|
||||
FundsOutcome.waiting(
|
||||
tester,
|
||||
skipped.name,
|
||||
"never ran, ${type.name} left the app somewhere it could not come back from",
|
||||
);
|
||||
}
|
||||
|
||||
if (FundsOutcome.needsFurtherReview(e.toString())) {
|
||||
failures.add("${type.name}: could not get back to the home page, "
|
||||
"the chains after this one never ran");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(failures, isEmpty, reason: "Failed chains:\n${failures.join("\n")}");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../core/app_launcher.dart";
|
||||
import "../core/funds_outcome.dart";
|
||||
import "../core/test_config.dart";
|
||||
import "../flows/auth_flows.dart";
|
||||
import "../flows/funds_flows.dart";
|
||||
import "../robots/home_page_robot.dart";
|
||||
import "../robots/new_send_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// tests everything up to the point of swiping
|
||||
integrationTest("Send a real self transaction on every funded chain", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final fundsFlows = FundsFlows(tester);
|
||||
final authFlows = AuthFlows(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final sendRobot = NewSendPageRobot(tester);
|
||||
|
||||
if (!TestConfig.shouldSpend("send")) {
|
||||
tester.printToConsole("FLOWS excludes send, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
|
||||
|
||||
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
|
||||
|
||||
await appLauncher.launchApp(testKey: "send_funds_test_app_key");
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final failures = <String>[];
|
||||
|
||||
for (final type in walletTypes) {
|
||||
tester.printToConsole("Funds send starting for ${type.name}");
|
||||
|
||||
try {
|
||||
final opened = await fundsFlows.openFundedWallet(type);
|
||||
|
||||
if (!opened) {
|
||||
tester.printToConsole("${type.name} has nothing spendable, top it up");
|
||||
FundsOutcome.empty(tester, type.name, "every funded wallet is empty");
|
||||
continue;
|
||||
}
|
||||
|
||||
expect(appStore.wallet?.type, type);
|
||||
|
||||
// Making it a self send so the funds remain inside the funded wallet, only fees are spent.
|
||||
final ownAddress = appStore.wallet!.walletAddresses.address;
|
||||
|
||||
await homePageRobot.openSendSheet();
|
||||
await sendRobot.isDisplayed();
|
||||
|
||||
await sendRobot.enterAddress(ownAddress);
|
||||
await sendRobot.enterAmount(TestConfig.fundsSendAmountFor(type));
|
||||
await sendRobot.tapSendButtonWhenReady();
|
||||
|
||||
await authFlows.authenticateWithPin(required: false);
|
||||
|
||||
await sendRobot.swipeToConfirm();
|
||||
await sendRobot.confirmTransactionCommitted();
|
||||
|
||||
// The send page sits under the confirm sheet, so one pop leaves it up and the next
|
||||
// chain would try to restore from behind it.
|
||||
expect(
|
||||
await fundsFlows.returnToHome(),
|
||||
true,
|
||||
reason: "${type.name} broadcast but the send flow would not close",
|
||||
);
|
||||
|
||||
tester.printToConsole("FUNDS_SEND_OK: ${type.name}");
|
||||
FundsOutcome.ok(tester, type.name, "self send broadcast");
|
||||
} catch (e) {
|
||||
if (FundsOutcome.needsFurtherReview(e.toString())) {
|
||||
failures.add("${type.name}: $e");
|
||||
} else {
|
||||
tester.printToConsole("${type.name} needs attention rather than a fix: $e");
|
||||
}
|
||||
|
||||
FundsOutcome.failed(tester, type.name, e.toString());
|
||||
tester.printToConsole("FUNDS_SEND_FAIL: ${type.name}: $e");
|
||||
|
||||
if (!await fundsFlows.returnToHome()) {
|
||||
for (final skipped in walletTypes.skip(walletTypes.indexOf(type) + 1)) {
|
||||
FundsOutcome.waiting(
|
||||
tester,
|
||||
skipped.name,
|
||||
"never ran, ${type.name} left the app somewhere it could not come back from",
|
||||
);
|
||||
}
|
||||
|
||||
if (FundsOutcome.needsFurtherReview(e.toString())) {
|
||||
failures.add("${type.name}: could not get back to the home page, "
|
||||
"the chains after this one never ran");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(failures, isEmpty, reason: "Failed chains:\n${failures.join("\n")}");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../core/app_launcher.dart";
|
||||
import "../core/funds_outcome.dart";
|
||||
import "../core/test_config.dart";
|
||||
import "../flows/funds_flows.dart";
|
||||
import "../robots/home_page_robot.dart";
|
||||
import "../robots/new_swap_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// tests everything up to the point of creating the trade
|
||||
integrationTest("Swap prices a real deposit without creating a trade", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final fundsFlows = FundsFlows(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final swapRobot = NewSwapPageRobot(tester);
|
||||
|
||||
if (!TestConfig.shouldDryRun("swap")) {
|
||||
tester.printToConsole("FLOWS excludes swap, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
|
||||
|
||||
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
|
||||
|
||||
await appLauncher.launchApp(testKey: "swap_dry_run_test_app_key");
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
|
||||
final type = TestConfig.swapDepositType(walletTypes);
|
||||
|
||||
expect(
|
||||
walletTypes,
|
||||
contains(type),
|
||||
reason: "SWAP_FROM=${type.name} is not among the funded wallets under test",
|
||||
);
|
||||
|
||||
final opened = await fundsFlows.openFundedWallet(type);
|
||||
|
||||
expect(opened, true, reason: "Every funded wallet for ${type.name} came up empty");
|
||||
expect(appStore.wallet?.type, type);
|
||||
|
||||
await homePageRobot.openSwapSheet();
|
||||
await swapRobot.isDisplayed();
|
||||
|
||||
if (TestConfig.swapReceive.isNotEmpty) {
|
||||
await swapRobot.chooseReceiveCurrency(CryptoCurrency.fromString(TestConfig.swapReceive));
|
||||
}
|
||||
|
||||
await swapRobot.enterMinimumViableDepositAmount();
|
||||
await swapRobot.enterTestReceiveAddress();
|
||||
|
||||
await swapRobot.confirmQuoteReceived();
|
||||
await swapRobot.confirmSwapButtonEnabled();
|
||||
|
||||
tester.printToConsole("${type.name} got a quote and the swap button enabled, no trade created");
|
||||
FundsOutcome.ok(tester, type.name, "quoted, swap button enabled, no trade created");
|
||||
|
||||
await swapRobot.dismissModal();
|
||||
await homePageRobot.isDisplayed();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../core/app_launcher.dart";
|
||||
import "../core/funds_outcome.dart";
|
||||
import "../core/test_config.dart";
|
||||
import "../core/test_wallets.dart";
|
||||
import "../flows/auth_flows.dart";
|
||||
import "../flows/funds_flows.dart";
|
||||
import "../robots/home_page_robot.dart";
|
||||
import "../robots/new_send_page_robot.dart";
|
||||
import "../robots/new_swap_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Create a real swap and broadcast its deposit", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final fundsFlows = FundsFlows(tester);
|
||||
final authFlows = AuthFlows(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final sendRobot = NewSendPageRobot(tester);
|
||||
final swapRobot = NewSwapPageRobot(tester);
|
||||
|
||||
if (!TestConfig.shouldSpend("swap")) {
|
||||
tester.printToConsole("FLOWS excludes swap, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
|
||||
|
||||
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
|
||||
|
||||
final type = TestConfig.swapDepositType(walletTypes);
|
||||
|
||||
expect(
|
||||
walletTypes,
|
||||
contains(type),
|
||||
reason: "SWAP_FROM=${type.name} is not among the funded wallets under test",
|
||||
);
|
||||
|
||||
await appLauncher.launchApp(testKey: "swap_funds_test_app_key");
|
||||
|
||||
final opened = await fundsFlows.openFundedWallet(type);
|
||||
|
||||
expect(
|
||||
opened,
|
||||
true,
|
||||
reason: "${type.name}: none of the ${TestWallets.fundedWalletsFor(type).length} funded "
|
||||
"wallets has a spendable balance, top them up before the next run",
|
||||
);
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
expect(appStore.wallet?.type, type);
|
||||
|
||||
await homePageRobot.openSwapSheet();
|
||||
await swapRobot.isDisplayed();
|
||||
|
||||
if (TestConfig.swapReceive.isNotEmpty) {
|
||||
await swapRobot.chooseReceiveCurrency(CryptoCurrency.fromString(TestConfig.swapReceive));
|
||||
}
|
||||
|
||||
await swapRobot.enterMinimumViableDepositAmount();
|
||||
final payoutAddress = await swapRobot.enterTestReceiveAddress();
|
||||
await swapRobot.confirmQuoteReceived();
|
||||
await swapRobot.confirmSwapButtonEnabled();
|
||||
|
||||
await swapRobot.tapSwapButton();
|
||||
|
||||
await authFlows.authenticateWithPin(required: false);
|
||||
|
||||
await swapRobot.confirmTradeCreated();
|
||||
swapRobot.confirmPayoutAddress(payoutAddress);
|
||||
|
||||
await sendRobot.swipeToConfirm();
|
||||
await swapRobot.confirmDepositCommitted();
|
||||
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
tester.printToConsole("FUNDS_SWAP_OK: ${type.name}");
|
||||
FundsOutcome.ok(tester, type.name, "trade created and deposit broadcast");
|
||||
});
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/core/secure_storage.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/authentication_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/store/wallet_list_store.dart';
|
||||
import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockAppStore extends Mock implements AppStore {}
|
||||
|
||||
class MockAuthService extends Mock implements AuthService {}
|
||||
|
||||
class MockSettingsStore extends Mock implements SettingsStore {}
|
||||
|
||||
class MockAuthenticationStore extends Mock implements AuthenticationStore {}
|
||||
|
||||
class MockWalletListStore extends Mock implements WalletListStore {}
|
||||
|
||||
class MockLinkViewModel extends Mock implements LinkViewModel {}
|
||||
|
||||
class MockHiveInterface extends Mock implements HiveInterface {}
|
||||
|
||||
class MockHiveBox extends Mock implements Box<dynamic> {}
|
||||
|
||||
class MockSecureStorage extends Mock implements SecureStorage {}
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/core/secure_storage.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/authentication_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/store/wallet_list_store.dart';
|
||||
import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'mocks.dart';
|
||||
|
||||
class TestHelpers {
|
||||
static void setup() {
|
||||
// Fallback values can also be declared here
|
||||
registerDependencies();
|
||||
}
|
||||
|
||||
static void registerDependencies() {
|
||||
getAndRegisterAppStore();
|
||||
getAndRegisterAuthService();
|
||||
getAndRegisterSettingsStore();
|
||||
getAndRegisterAuthenticationStore();
|
||||
getAndRegisterWalletListStore();
|
||||
|
||||
getAndRegisterLinkViewModel();
|
||||
getAndRegisterSecureStorage();
|
||||
getAndRegisterHiveInterface();
|
||||
}
|
||||
|
||||
static MockSettingsStore getAndRegisterSettingsStore() {
|
||||
_removeRegistrationIfExists<SettingsStore>();
|
||||
final service = MockSettingsStore();
|
||||
getIt.registerSingleton<SettingsStore>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static MockAppStore getAndRegisterAppStore() {
|
||||
_removeRegistrationIfExists<AppStore>();
|
||||
final service = MockAppStore();
|
||||
final settingsStore = getAndRegisterSettingsStore();
|
||||
|
||||
when(() => service.settingsStore).thenAnswer((invocation) => settingsStore);
|
||||
getIt.registerSingleton<AppStore>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static MockAuthService getAndRegisterAuthService() {
|
||||
_removeRegistrationIfExists<AuthService>();
|
||||
final service = MockAuthService();
|
||||
getIt.registerSingleton<AuthService>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static MockAuthenticationStore getAndRegisterAuthenticationStore() {
|
||||
_removeRegistrationIfExists<AuthenticationStore>();
|
||||
final service = MockAuthenticationStore();
|
||||
when(() => service.state).thenReturn(AuthenticationState.uninitialized);
|
||||
getIt.registerSingleton<AuthenticationStore>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static MockWalletListStore getAndRegisterWalletListStore() {
|
||||
_removeRegistrationIfExists<WalletListStore>();
|
||||
final service = MockWalletListStore();
|
||||
getIt.registerSingleton<WalletListStore>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static MockLinkViewModel getAndRegisterLinkViewModel() {
|
||||
_removeRegistrationIfExists<LinkViewModel>();
|
||||
final service = MockLinkViewModel();
|
||||
getIt.registerSingleton<LinkViewModel>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static MockHiveInterface getAndRegisterHiveInterface() {
|
||||
_removeRegistrationIfExists<HiveInterface>();
|
||||
final service = MockHiveInterface();
|
||||
getIt.registerSingleton<HiveInterface>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static MockSecureStorage getAndRegisterSecureStorage() {
|
||||
_removeRegistrationIfExists<SecureStorage>();
|
||||
final service = MockSecureStorage();
|
||||
getIt.registerSingleton<SecureStorage>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
static void _removeRegistrationIfExists<T extends Object>() {
|
||||
if (getIt.isRegistered<T>()) {
|
||||
getIt.unregister<T>();
|
||||
}
|
||||
}
|
||||
|
||||
static void tearDown() => getIt.reset();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
null
|
||||
@@ -1,36 +1,20 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/auth/auth_page.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import 'pin_code_widget_robot.dart';
|
||||
import "pin_code_widget_robot.dart";
|
||||
|
||||
class AuthPageRobot extends PinCodeWidgetRobot {
|
||||
AuthPageRobot(this.tester)
|
||||
: commonTestCases = CommonTestCases(tester),
|
||||
super(tester);
|
||||
AuthPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<AuthPage>();
|
||||
}
|
||||
|
||||
bool onAuthPage() {
|
||||
final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key'));
|
||||
final hasPinButtons = find.byKey(const ValueKey("pin_code_button_3_key"));
|
||||
final hasPin = hasPinButtons.tryEvaluate();
|
||||
return hasPin;
|
||||
}
|
||||
|
||||
bool onAuthPageDesktop() {
|
||||
final hasWalletPasswordInput = find.byKey(ValueKey('enter_wallet_password'));
|
||||
return hasWalletPasswordInput.tryEvaluate();
|
||||
}
|
||||
|
||||
Future<void> isAuthPage() async {
|
||||
await commonTestCases.isSpecificPage<AuthPage>();
|
||||
await commonTestCases.takeScreenshots('auth_page');
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
commonTestCases.hasText(S.current.setup_pin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import "package:cake_wallet/entities/fiat_api_mode.dart";
|
||||
import "package:cake_wallet/src/screens/settings/connection_sync_page.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class ConnectionSyncPageRobot extends BaseRobot {
|
||||
ConnectionSyncPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<ConnectionSyncPage>();
|
||||
}
|
||||
|
||||
Future<void> setFiatApiMode(FiatApiMode mode) async {
|
||||
await tapByKey("fiat_api");
|
||||
|
||||
final unselected = find.byKey(ValueKey("picker_items_index_${mode.title}_button_key"));
|
||||
final selected =
|
||||
find.byKey(ValueKey("picker_items_index_${mode.title}_selected_item_button_key"));
|
||||
|
||||
await pumpUntil(() => tester.any(unselected) || tester.any(selected));
|
||||
|
||||
await tester.tap(tester.any(unselected) ? unselected : selected);
|
||||
|
||||
await settle();
|
||||
}
|
||||
|
||||
void hasFiatApiMode(FiatApiMode mode) {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey("fiat_api")),
|
||||
matching: find.text(mode.title),
|
||||
),
|
||||
findsOneWidget,
|
||||
reason: "The fiat api row does not show ${mode.title}",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import "package:cake_wallet/src/screens/contact/contact_list_page.dart";
|
||||
import "package:cake_wallet/src/screens/contact/contact_page.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class ContactRobot extends BaseRobot {
|
||||
ContactRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await pumpUntilFound(find.byType(ContactListPage));
|
||||
}
|
||||
|
||||
Future<void> openAddContact() async {
|
||||
await tapByKey("contact_list_page_add_contact_button_key");
|
||||
|
||||
await pumpUntilFound(find.byType(ContactPage));
|
||||
}
|
||||
|
||||
Future<void> enterName(String name) async {
|
||||
await enterTextByKey("contact_page_name_textfield_key", name);
|
||||
}
|
||||
|
||||
Future<void> chooseCurrency(String currencyName) async {
|
||||
await tapByKey("contact_page_currency_picker_button_key");
|
||||
|
||||
await tapByKey("picker_items_index_${currencyName}_button_key");
|
||||
|
||||
await settle();
|
||||
}
|
||||
|
||||
Future<void> enterAddress(String address) async {
|
||||
final input = find.byKey(const ValueKey("contact_page_address_input_key"));
|
||||
|
||||
await pumpUntilFound(input);
|
||||
|
||||
final field = find.descendant(of: input, matching: find.byType(EditableText));
|
||||
|
||||
await tester.enterText(field.first, address);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
await tapByKey("contact_page_save_button_key");
|
||||
|
||||
await pumpUntilGone(find.byType(ContactPage));
|
||||
|
||||
await settle();
|
||||
}
|
||||
|
||||
void expectContactSaved(String name, String address) {
|
||||
final page = tester.widget<ContactListPage>(find.byType(ContactListPage));
|
||||
|
||||
final saved = page.contactListViewModel.contacts.where((c) => c.name == name).toList();
|
||||
|
||||
expect(saved, isNotEmpty, reason: "The contact $name was never saved");
|
||||
|
||||
expect(
|
||||
saved.first.address,
|
||||
address,
|
||||
reason: "The address book stored a different address than it was given",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,16 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/welcome/create_pin_welcome_page.dart';
|
||||
import 'package:cake_wallet/wallet_type_utils.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/welcome/create_pin_welcome_page.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class CreatePinWelcomePageRobot {
|
||||
CreatePinWelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class CreatePinWelcomePageRobot extends BaseRobot {
|
||||
CreatePinWelcomePageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isCreatePinWelcomePage() async {
|
||||
await commonTestCases.isSpecificPage<CreatePinWelcomePage>();
|
||||
await commonTestCases.takeScreenshots('create_pin_welcome_page');
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
String title;
|
||||
if (isMoneroOnly) {
|
||||
title = S.current.monero_com;
|
||||
}
|
||||
|
||||
title = S.current.cake_wallet;
|
||||
|
||||
commonTestCases.hasText(title);
|
||||
}
|
||||
|
||||
void hasDescription() {
|
||||
String description;
|
||||
if (isMoneroOnly) {
|
||||
description = S.current.monero_com_wallet_text;
|
||||
}
|
||||
|
||||
description = S.current.new_first_wallet_text;
|
||||
|
||||
commonTestCases.hasText(description);
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<CreatePinWelcomePage>();
|
||||
}
|
||||
|
||||
Future<void> tapSetAPinButton() async {
|
||||
await commonTestCases.tapItemByKey('create_pin_welcome_page_create_a_pin_button_key');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("create_pin_welcome_page_create_a_pin_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class DashboardMenuWidgetRobot {
|
||||
DashboardMenuWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> hasMenuWidget() async {
|
||||
await commonTestCases.takeScreenshots('menu_widget_page');
|
||||
commonTestCases.hasType<MenuWidget>();
|
||||
}
|
||||
|
||||
void displaysTheCorrectWalletNameAndSubName() {
|
||||
final menuWidgetState = tester.state<MenuWidgetState>(find.byType(MenuWidget));
|
||||
|
||||
final walletName = menuWidgetState.widget.dashboardViewModel.name;
|
||||
commonTestCases.hasText(walletName);
|
||||
|
||||
final walletSubName = menuWidgetState.widget.dashboardViewModel.subname;
|
||||
if (walletSubName.isNotEmpty) {
|
||||
commonTestCases.hasText(walletSubName);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> navigateToWalletMenu() async {
|
||||
await commonTestCases.tapItemByKey('dashboard_page_menu_widget_wallet_menu_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> navigateToSecurityAndBackupPage() async {
|
||||
await commonTestCases.tapItemByKey(
|
||||
'dashboard_page_menu_widget_security_and_backup_button_key',
|
||||
);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/pages/balance/crypto_balance_widget.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import 'dashboard_menu_widget_robot.dart';
|
||||
|
||||
class DashboardPageRobot {
|
||||
DashboardPageRobot(this.tester)
|
||||
: commonTestCases = CommonTestCases(tester),
|
||||
dashboardMenuWidgetRobot = DashboardMenuWidgetRobot(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final DashboardMenuWidgetRobot dashboardMenuWidgetRobot;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isDashboardPage() async {
|
||||
await commonTestCases.isSpecificPage<DashboardPage>();
|
||||
await commonTestCases.takeScreenshots('dashboard_page');
|
||||
}
|
||||
|
||||
Future<void> confirmWalletTypeIsDisplayedCorrectly(
|
||||
WalletType type, {
|
||||
bool isHaven = false,
|
||||
}) async {
|
||||
final cryptoBalanceWidget =
|
||||
tester.widget<CryptoBalanceWidget>(find.byType(CryptoBalanceWidget));
|
||||
final hasAccounts = cryptoBalanceWidget.dashboardViewModel.balanceViewModel.hasAccounts;
|
||||
|
||||
if (hasAccounts) {
|
||||
final walletName = cryptoBalanceWidget.dashboardViewModel.name;
|
||||
commonTestCases.hasText(walletName);
|
||||
} else {
|
||||
final walletName = walletTypeToString(type);
|
||||
final assetName = isHaven ? '$walletName Assets' : walletName;
|
||||
commonTestCases.hasText(assetName);
|
||||
}
|
||||
await commonTestCases.defaultSleepTime(seconds: 5);
|
||||
}
|
||||
|
||||
void confirmServiceUpdateButtonDisplays() {
|
||||
commonTestCases.hasValueKey('dashboard_page_services_update_button_key');
|
||||
}
|
||||
|
||||
void confirmSyncIndicatorButtonDisplays() {
|
||||
commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key');
|
||||
}
|
||||
|
||||
void confirmMenuButtonDisplays() {
|
||||
commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key');
|
||||
}
|
||||
|
||||
Future<void> confirmRightCryptoAssetTitleDisplaysPerPageView(
|
||||
WalletType type, {
|
||||
bool isHaven = false,
|
||||
}) async {
|
||||
//Balance Page
|
||||
await confirmWalletTypeIsDisplayedCorrectly(type, isHaven: isHaven);
|
||||
|
||||
// Swipe to Cake features Page
|
||||
await swipeDashboardTab(false);
|
||||
commonTestCases.hasText('Cake ${S.current.features}');
|
||||
|
||||
// Swipe back to balance
|
||||
await swipeDashboardTab(true);
|
||||
|
||||
// Swipe to Transactions Page
|
||||
await swipeDashboardTab(true);
|
||||
commonTestCases.hasText(S.current.transactions);
|
||||
|
||||
// Swipe back to balance
|
||||
await swipeDashboardTab(false);
|
||||
await commonTestCases.defaultSleepTime(seconds: 3);
|
||||
}
|
||||
|
||||
Future<void> swipeDashboardTab(bool swipeRight) async {
|
||||
await commonTestCases.swipeByPageKey(
|
||||
key: 'dashboard_page_view_key',
|
||||
swipeRight: swipeRight,
|
||||
);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> openDrawerMenu() async {
|
||||
await commonTestCases.tapItemByKey('dashboard_page_wallet_menu_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> navigateToBuyPage() async {
|
||||
await commonTestCases.tapItemByKey('dashboard_page_buy_action_button_key');
|
||||
}
|
||||
|
||||
Future<void> navigateToWalletsListPage({bool isDrawer = false}) async {
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
await commonTestCases.tapItemByKey(
|
||||
isDrawer
|
||||
? 'dashboard_page_menu_widget_wallet_menu_button_key'
|
||||
: 'dashboard_page__wallet_list_button_key',
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 2000));
|
||||
}
|
||||
|
||||
Future<void> navigateToSendPage() async {
|
||||
await commonTestCases.tapItemByKey('dashboard_page_send_action_button_key');
|
||||
}
|
||||
|
||||
Future<void> navigateToSellPage() async {
|
||||
await commonTestCases.tapItemByKey('dashboard_page_sell_action_button_key');
|
||||
}
|
||||
|
||||
Future<void> navigateToReceivePage() async {
|
||||
await commonTestCases.tapItemByKey('dashboard_page_receive_action_button_key');
|
||||
}
|
||||
|
||||
Future<void> navigateToExchangePage() async {
|
||||
await commonTestCases.tapItemByKey('dashboard_page_swap_action_button_key');
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class DisclaimerPageRobot {
|
||||
DisclaimerPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isDisclaimerPage() async {
|
||||
await commonTestCases.isSpecificPage<DisclaimerPage>();
|
||||
await commonTestCases.takeScreenshots('disclaimer_page');
|
||||
}
|
||||
|
||||
void hasCheckIcon(bool hasBeenTapped) {
|
||||
// The checked Icon should not be available initially, until user taps the checkbox
|
||||
final checkIcon = find.byKey(ValueKey('disclaimer_check_icon_key'));
|
||||
expect(checkIcon, hasBeenTapped ? findsOneWidget : findsNothing);
|
||||
}
|
||||
|
||||
void hasDisclaimerCheckbox() {
|
||||
final checkBox = find.byKey(ValueKey('disclaimer_check_key'));
|
||||
expect(checkBox, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> tapDisclaimerCheckbox() async {
|
||||
await commonTestCases.tapItemByKey('disclaimer_check_key');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> tapAcceptButton() async {
|
||||
await commonTestCases.tapItemByKey('disclaimer_accept_button_key');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import "package:cake_wallet/entities/fiat_currency.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart";
|
||||
import "package:cake_wallet/src/screens/settings/display_settings_page.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class DisplaySettingsPageRobot extends BaseRobot {
|
||||
DisplaySettingsPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<DisplaySettingsPage>();
|
||||
}
|
||||
|
||||
Future<void> openLanguagePicker() async {
|
||||
await tapByKey("display_settings_language");
|
||||
}
|
||||
|
||||
Future<void> chooseLanguage(String code) async {
|
||||
await tapByKey("picker_items_index_${code}_button_key");
|
||||
|
||||
await settle();
|
||||
}
|
||||
|
||||
bool hasFiatCurrencyRow() =>
|
||||
tester.any(find.byKey(const ValueKey("display_settings_fiat_currency")));
|
||||
|
||||
Future<void> openFiatCurrencyPicker() async {
|
||||
await tapByKey("display_settings_fiat_currency");
|
||||
|
||||
await pumpUntilFound(find.byType(FiatCurrencyPickerSheet));
|
||||
}
|
||||
|
||||
Future<void> chooseFiatCurrency(FiatCurrency currency) async {
|
||||
final searchField = find.descendant(
|
||||
of: find.byType(CurrencyPickerSearchField),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
|
||||
await tester.enterText(searchField, currency.fullName);
|
||||
|
||||
final row = find.byWidgetPredicate(
|
||||
(widget) => widget is FiatCurrencyRow && widget.currency == currency,
|
||||
);
|
||||
|
||||
await pumpUntilFound(row);
|
||||
|
||||
await tester.tap(row.first);
|
||||
|
||||
await pumpUntilGone(find.byType(FiatCurrencyPickerSheet));
|
||||
}
|
||||
|
||||
void expectFiatCurrencyShown(FiatCurrency currency) {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey("display_settings_fiat_currency")),
|
||||
matching: find.text(currency.title),
|
||||
),
|
||||
findsOneWidget,
|
||||
reason: "The currency row still does not show ${currency.title}",
|
||||
);
|
||||
}
|
||||
|
||||
void expectLanguageShown(String displayName) {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey("display_settings_language")),
|
||||
matching: find.text(displayName),
|
||||
),
|
||||
findsOneWidget,
|
||||
reason: "The language row still does not show $displayName",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class ExchangeConfirmPageRobot {
|
||||
ExchangeConfirmPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isExchangeConfirmPage() async {
|
||||
await commonTestCases.isSpecificPage<ExchangeConfirmPage>();
|
||||
await commonTestCases.takeScreenshots('exchange_confirm_page');
|
||||
}
|
||||
|
||||
void confirmComponentsOfTradeDisplayProperly() {
|
||||
final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
|
||||
final trade = exchangeConfirmPage.trade;
|
||||
|
||||
commonTestCases.hasText(trade.id);
|
||||
commonTestCases.hasText('${trade.provider.title} ${S.current.trade_id}');
|
||||
|
||||
commonTestCases.hasValueKey('exchange_confirm_page_saved_id_button_key');
|
||||
commonTestCases.hasValueKey('exchange_confirm_page_copy_to_clipboard_button_key');
|
||||
}
|
||||
|
||||
Future<void> confirmCopyTradeIdToClipBoardWorksProperly() async {
|
||||
final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
|
||||
final trade = exchangeConfirmPage.trade;
|
||||
|
||||
await commonTestCases.tapItemByKey('exchange_confirm_page_copy_to_clipboard_button_key');
|
||||
|
||||
ClipboardData? clipboardData = await Clipboard.getData('text/plain');
|
||||
|
||||
expect(clipboardData?.text, trade.id);
|
||||
}
|
||||
|
||||
Future<void> onSavedTradeIdButtonPressed() async {
|
||||
await tester.pumpAndSettle();
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await commonTestCases.tapItemByKey('exchange_confirm_page_saved_id_button_key');
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import '../components/common_test_constants.dart';
|
||||
import 'auth_page_robot.dart';
|
||||
|
||||
class ExchangePageRobot {
|
||||
ExchangePageRobot(this.tester)
|
||||
: commonTestCases = CommonTestCases(tester),
|
||||
authPageRobot = AuthPageRobot(tester);
|
||||
|
||||
WidgetTester tester;
|
||||
AuthPageRobot authPageRobot;
|
||||
CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isExchangePage() async {
|
||||
await commonTestCases.isSpecificPage<ExchangePage>();
|
||||
await commonTestCases.takeScreenshots('exchange_page');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
void hasResetButton() {
|
||||
commonTestCases.hasText(S.current.reset);
|
||||
}
|
||||
|
||||
void displaysPresentProviderPicker() {
|
||||
commonTestCases.hasType<PresentProviderPicker>();
|
||||
}
|
||||
|
||||
Future<void> displayBothExchangeCards() async {
|
||||
final ExchangePage exchangeCard = tester.widget<ExchangePage>(
|
||||
find.byType(ExchangePage),
|
||||
);
|
||||
|
||||
final depositKey = exchangeCard.depositKey;
|
||||
final receiveKey = exchangeCard.receiveKey;
|
||||
|
||||
final depositExchangeCard = find.byKey(depositKey);
|
||||
expect(depositExchangeCard, findsOneWidget);
|
||||
|
||||
final receiveExchangeCard = find.byKey(receiveKey);
|
||||
expect(receiveExchangeCard, findsOneWidget);
|
||||
}
|
||||
|
||||
void confirmRightComponentsDisplayOnDepositExchangeCards() {
|
||||
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
|
||||
final exchangeViewModel = exchangePage.exchangeViewModel;
|
||||
final depositCardPrefix = 'deposit_exchange_card';
|
||||
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_title_key');
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_currency_picker_button_key');
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_text_key');
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_amount_textfield_key');
|
||||
|
||||
exchangePage.depositKey.currentState!.changeLimits(min: '0.1');
|
||||
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_min_limit_text_key');
|
||||
|
||||
final initialCurrency = exchangeViewModel.depositCurrency;
|
||||
if (initialCurrency.tag != null) {
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_tag_text_key');
|
||||
}
|
||||
|
||||
if (exchangeViewModel.hasAllAmount) {
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_send_all_button_key');
|
||||
}
|
||||
|
||||
if (exchangeViewModel.isMoneroWallet) {
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_address_book_button_key');
|
||||
}
|
||||
|
||||
if (exchangeViewModel.isDepositAddressEnabled) {
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_editable_address_textfield_key');
|
||||
} else {
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_non_editable_address_textfield_key');
|
||||
commonTestCases.hasValueKey('${depositCardPrefix}_copy_refund_address_button_key');
|
||||
}
|
||||
|
||||
// commonTestCases.hasValueKey('${depositCardPrefix}_max_limit_text_key');
|
||||
}
|
||||
|
||||
void confirmRightComponentsDisplayOnReceiveExchangeCards() {
|
||||
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
|
||||
final exchangeViewModel = exchangePage.exchangeViewModel;
|
||||
final receiveCardPrefix = 'receive_exchange_card';
|
||||
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_title_key');
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_currency_picker_button_key');
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_text_key');
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_amount_textfield_key');
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_min_limit_text_key');
|
||||
|
||||
final initialCurrency = exchangeViewModel.receiveCurrency;
|
||||
if (initialCurrency.tag != null) {
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_tag_text_key');
|
||||
}
|
||||
|
||||
if (exchangeViewModel.hasAllAmount) {
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_send_all_button_key');
|
||||
}
|
||||
|
||||
if (exchangeViewModel.isMoneroWallet) {
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_address_book_button_key');
|
||||
}
|
||||
|
||||
commonTestCases.hasValueKey('${receiveCardPrefix}_editable_address_textfield_key');
|
||||
}
|
||||
|
||||
Future<void> selectDepositCurrency(CryptoCurrency depositCurrency) async {
|
||||
final depositPrefix = 'deposit_exchange_card';
|
||||
final currencyPickerKey = '${depositPrefix}_currency_picker_button_key';
|
||||
final currencyPickerDialogKey = '${depositPrefix}_currency_picker_dialog_button_key';
|
||||
|
||||
await commonTestCases.tapItemByKey(currencyPickerKey);
|
||||
commonTestCases.hasValueKey(currencyPickerDialogKey);
|
||||
|
||||
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
|
||||
final exchangeViewModel = exchangePage.exchangeViewModel;
|
||||
|
||||
if (depositCurrency == exchangeViewModel.depositCurrency) {
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await commonTestCases
|
||||
.tapItemByKey('picker_items_index_${depositCurrency.name}_selected_item_button_key');
|
||||
return;
|
||||
}
|
||||
|
||||
await commonTestCases.enterText(depositCurrency.name, 'search_bar_widget_key');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key');
|
||||
}
|
||||
|
||||
Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
|
||||
final receivePrefix = 'receive_exchange_card';
|
||||
final currencyPickerKey = '${receivePrefix}_currency_picker_button_key';
|
||||
final currencyPickerDialogKey = '${receivePrefix}_currency_picker_dialog_button_key';
|
||||
|
||||
await commonTestCases.tapItemByKey(currencyPickerKey);
|
||||
commonTestCases.hasValueKey(currencyPickerDialogKey);
|
||||
|
||||
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
|
||||
final exchangeViewModel = exchangePage.exchangeViewModel;
|
||||
|
||||
if (receiveCurrency == exchangeViewModel.receiveCurrency) {
|
||||
await commonTestCases
|
||||
.tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
|
||||
return;
|
||||
}
|
||||
|
||||
await commonTestCases.enterText(receiveCurrency.name, 'search_bar_widget_key');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
|
||||
}
|
||||
|
||||
Future<void> enterDepositAmount(String amount) async {
|
||||
await commonTestCases.enterText(amount, 'deposit_exchange_card_amount_textfield_key');
|
||||
}
|
||||
|
||||
Future<void> enterDepositRefundAddress({String? depositAddress}) async {
|
||||
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
|
||||
final exchangeViewModel = exchangePage.exchangeViewModel;
|
||||
|
||||
if (exchangeViewModel.isDepositAddressEnabled && depositAddress != null) {
|
||||
await commonTestCases.enterText(
|
||||
depositAddress, 'deposit_exchange_card_editable_address_textfield_key');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enterReceiveAddress(String receiveAddress) async {
|
||||
await commonTestCases.enterText(
|
||||
receiveAddress,
|
||||
'receive_exchange_card_editable_address_textfield_key',
|
||||
);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onExchangeButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('exchange_page_exchange_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
bool hasMaxLimitError() {
|
||||
final maxErrorText = find.text(S.current.error_text_input_above_maximum_limit);
|
||||
|
||||
bool hasMaxError = maxErrorText.tryEvaluate();
|
||||
|
||||
return hasMaxError;
|
||||
}
|
||||
|
||||
bool hasMinLimitError() {
|
||||
final minErrorText = find.text(S.current.error_text_input_below_minimum_limit);
|
||||
|
||||
bool hasMinError = minErrorText.tryEvaluate();
|
||||
|
||||
return hasMinError;
|
||||
}
|
||||
|
||||
bool hasTradeCreationFailureError() {
|
||||
final tradeCreationFailureDialogButton =
|
||||
find.byKey(ValueKey('exchange_page_trade_creation_failure_dialog_button_key'));
|
||||
|
||||
bool hasTradeCreationFailure = tradeCreationFailureDialogButton.tryEvaluate();
|
||||
tester.printToConsole('Trade not created error: $hasTradeCreationFailure');
|
||||
return hasTradeCreationFailure;
|
||||
}
|
||||
|
||||
Future<void> onTradeCreationFailureDialogButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('exchange_page_trade_creation_failure_dialog_button_key');
|
||||
}
|
||||
|
||||
/// Handling Trade Failure Errors or errors shown through the Failure Dialog.
|
||||
///
|
||||
/// Simulating the user's flow and response when this error comes up.
|
||||
/// Examples are:
|
||||
/// - No provider can handle this trade error,
|
||||
/// - Trade amount below limit error.
|
||||
Future<void> _handleTradeCreationFailureErrors() async {
|
||||
tester.printToConsole('Inside trade creation failure handle');
|
||||
bool isTradeCreationFailure = false;
|
||||
|
||||
isTradeCreationFailure = hasTradeCreationFailureError();
|
||||
|
||||
int maxRetries = 3;
|
||||
int retries = 0;
|
||||
|
||||
while (isTradeCreationFailure && retries < maxRetries) {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await onTradeCreationFailureDialogButtonPressed();
|
||||
|
||||
await commonTestCases.defaultSleepTime(seconds: 5);
|
||||
|
||||
await onExchangeButtonPressed();
|
||||
|
||||
await _handleAuthPage();
|
||||
|
||||
isTradeCreationFailure = hasTradeCreationFailureError();
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the min limit error.
|
||||
///
|
||||
/// Simulates the user's flow and response when it comes up.
|
||||
///
|
||||
/// Has a max retry of 20 times.
|
||||
Future<void> _handleMinLimitError(String initialAmount) async {
|
||||
double amount;
|
||||
|
||||
amount = double.parse(initialAmount);
|
||||
|
||||
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
|
||||
final exchangeViewModel = exchangePage.exchangeViewModel;
|
||||
|
||||
amount = (exchangeViewModel.limits.min ?? 0.0) + 1.0;
|
||||
|
||||
await enterDepositAmount(amount.toString());
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await onExchangeButtonPressed();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Handles the max limit error.
|
||||
///
|
||||
/// Simulates the user's flow and response when it comes up.
|
||||
///
|
||||
/// Has a max retry of 20 times.
|
||||
Future<void> _handleMaxLimitError(String initialAmount) async {
|
||||
double amount;
|
||||
|
||||
amount = double.parse(initialAmount);
|
||||
|
||||
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
|
||||
final exchangeViewModel = exchangePage.exchangeViewModel;
|
||||
|
||||
amount = (exchangeViewModel.limits.max ?? 0.0) - 1.0;
|
||||
|
||||
await enterDepositAmount(amount.toString());
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await onExchangeButtonPressed();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> handleErrors(String initialAmount) async {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
bool isMinLimitError = hasMinLimitError();
|
||||
|
||||
if (isMinLimitError) {
|
||||
tester.printToConsole('Has min limit error');
|
||||
await _handleMinLimitError(initialAmount);
|
||||
}
|
||||
|
||||
bool isMaxLimitError = hasMaxLimitError();
|
||||
|
||||
if (isMaxLimitError) {
|
||||
await _handleMaxLimitError(initialAmount);
|
||||
}
|
||||
|
||||
tester.printToConsole('No limits error, proceeding with flow');
|
||||
|
||||
await _handleAuthPage();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await _handleTradeCreationFailureErrors();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> _handleAuthPage() async {
|
||||
final onAuthPage = authPageRobot.onAuthPage();
|
||||
if (onAuthPage) {
|
||||
await authPageRobot.enterPinCode(CommonTestConstants.pin);
|
||||
}
|
||||
|
||||
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
|
||||
if (onAuthPageDesktop) {
|
||||
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class ExchangeTradeExternalSendPageRobot {
|
||||
ExchangeTradeExternalSendPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isExchangeTradeExternalSendPage() async {
|
||||
await commonTestCases.isSpecificPage<ExchangeTradeExternalSendPage>();
|
||||
await commonTestCases.takeScreenshots('exchange_trade_external_send_page');
|
||||
}
|
||||
|
||||
Future<void> verifySendDetailsItemsDisplayProperly() async {
|
||||
final widget =
|
||||
tester.widget<ExchangeTradeExternalSendPage>(find.byType(ExchangeTradeExternalSendPage));
|
||||
final exchangeTradeViewModel = widget.exchangeTradeViewModel;
|
||||
final items = exchangeTradeViewModel.items.where((item) => item.isExternalSendDetail).toList();
|
||||
|
||||
for (var item in items) {
|
||||
commonTestCases.hasValueKey('exchange_trade_external_send_page_send_item_${item.title}_key');
|
||||
tester.printToConsole('${item.title} present on screen');
|
||||
}
|
||||
|
||||
commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onContinueButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('exchange_trade_external_send_page_continue_button_key');
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class ExchangeTradePageRobot {
|
||||
ExchangeTradePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isExchangeTradePage() async {
|
||||
await commonTestCases.isSpecificPage<ExchangeTradePage>();
|
||||
await commonTestCases.takeScreenshots('exchange_trade_page');
|
||||
}
|
||||
|
||||
void hasInformationDialog() {
|
||||
commonTestCases.hasValueKey('information_page_dialog_key');
|
||||
}
|
||||
|
||||
Future<void> onGotItButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('information_page_got_it_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onSendFromExternalButtonPressed() async {
|
||||
tester.printToConsole('Routing to send from external details page');
|
||||
|
||||
await commonTestCases.tapItemByKey(
|
||||
'exchange_trade_page_send_from_external_button_key',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onSendFromCakeButtonPressed() async {
|
||||
tester.printToConsole('Now sending from cake');
|
||||
|
||||
await commonTestCases.tapItemByKey(
|
||||
'exchange_trade_page_send_from_cake_button_key',
|
||||
shouldPumpAndSettle: false,
|
||||
);
|
||||
|
||||
final Completer<void> completer = Completer<void>();
|
||||
|
||||
// Loop to wait for the async operation to complete
|
||||
while (true) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
final ExchangeTradeState state = tester.state(find.byType(ExchangeTradeForm));
|
||||
final execState = state.widget.exchangeTradeViewModel.sendViewModel.state;
|
||||
|
||||
bool isDone = execState is ExecutedSuccessfullyState;
|
||||
bool isFailed = execState is FailureState;
|
||||
|
||||
tester.printToConsole('isDone: $isDone');
|
||||
tester.printToConsole('isFailed: $isFailed');
|
||||
|
||||
if (isDone || isFailed) {
|
||||
tester.printToConsole(
|
||||
isDone ? 'Completer is done' : 'Completer is done though operation failed');
|
||||
completer.complete();
|
||||
await tester.pump();
|
||||
break;
|
||||
} else {
|
||||
tester.printToConsole('Completer is not done');
|
||||
await tester.pump();
|
||||
}
|
||||
}
|
||||
|
||||
await expectLater(completer.future, completes);
|
||||
|
||||
tester.printToConsole('Done confirming sending');
|
||||
|
||||
await commonTestCases.defaultSleepTime(seconds: 4);
|
||||
}
|
||||
|
||||
Future<void> onSendButtonOnConfirmSendingDialogPressed() async {
|
||||
tester.printToConsole('Send Button on Confirm Dialog Triggered');
|
||||
await commonTestCases.defaultSleepTime(seconds: 4);
|
||||
|
||||
final sendText = find.text(S.current.send);
|
||||
bool hasText = sendText.tryEvaluate();
|
||||
|
||||
if (hasText) {
|
||||
await commonTestCases.tapItemByFinder(sendText);
|
||||
|
||||
await commonTestCases.defaultSleepTime(seconds: 4);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onCancelButtonOnConfirmSendingDialogPressed() async {
|
||||
tester.printToConsole('Cancel Button on Confirm Dialog Triggered');
|
||||
|
||||
await commonTestCases.tapItemByKey(
|
||||
'exchange_trade_page_confirm_sending_dialog_cancel_button_key',
|
||||
);
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onSendFailureDialogButtonPressed() async {
|
||||
await commonTestCases.defaultSleepTime(seconds: 6);
|
||||
|
||||
tester.printToConsole('Send Button Failure Dialog Triggered');
|
||||
|
||||
await commonTestCases.tapItemByKey('exchange_trade_page_send_failure_dialog_button_key');
|
||||
}
|
||||
|
||||
Future<bool> hasErrorWhileSending() async {
|
||||
await tester.pump();
|
||||
|
||||
tester.printToConsole('Checking if there is an error');
|
||||
|
||||
final errorDialog = find.byKey(
|
||||
ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
|
||||
);
|
||||
|
||||
bool hasError = errorDialog.tryEvaluate();
|
||||
|
||||
tester.printToConsole('Has error: $hasError');
|
||||
|
||||
return hasError;
|
||||
}
|
||||
|
||||
Future<void> handleConfirmSendResult() async {
|
||||
bool hasError = false;
|
||||
|
||||
hasError = await hasErrorWhileSending();
|
||||
|
||||
int maxRetries = 20;
|
||||
int retries = 0;
|
||||
|
||||
while (hasError && retries < maxRetries) {
|
||||
tester.printToConsole('hasErrorInLoop: $hasError');
|
||||
await tester.pump();
|
||||
|
||||
await onSendFailureDialogButtonPressed();
|
||||
tester.printToConsole('Failure button tapped');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await onSendFromCakeButtonPressed();
|
||||
tester.printToConsole('Confirm sending button tapped');
|
||||
|
||||
hasError = await hasErrorWhileSending();
|
||||
|
||||
retries++;
|
||||
}
|
||||
|
||||
if (!hasError) {
|
||||
tester.printToConsole('No error, proceeding with flow');
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import "package:cake_wallet/new-ui/pages/home_page.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart";
|
||||
import "package:cw_core/sync_status.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class HomePageRobot extends BaseRobot {
|
||||
HomePageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
final shown = await pumpUntil(() => isInFront);
|
||||
|
||||
expect(shown, true, reason: "The home tab never came to the front");
|
||||
}
|
||||
|
||||
bool get isInFront {
|
||||
final stackFinder = find.byType(IndexedStack);
|
||||
|
||||
if (!tester.any(stackFinder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final stack = tester.widget<IndexedStack>(stackFinder.first);
|
||||
final index = stack.children.indexWhere((child) => child is NewHomePage);
|
||||
|
||||
return index >= 0 && stack.index == index;
|
||||
}
|
||||
|
||||
Future<void> hasWalletName(String name) async {
|
||||
final nameFinder = find.byKey(const ValueKey("home_page_wallet_name_text_key"));
|
||||
final syncBarFinder = find.byType(SyncBar);
|
||||
|
||||
final shown = await pumpUntil(() => tester.any(nameFinder) || tester.any(syncBarFinder));
|
||||
|
||||
expect(shown, true, reason: "Top bar showed neither the wallet name nor the sync bar");
|
||||
|
||||
if (!tester.any(nameFinder)) {
|
||||
tester.printToConsole("Sync bar is in place of the wallet name, not checking it on screen");
|
||||
return;
|
||||
}
|
||||
|
||||
expect(textByKey("home_page_wallet_name_text_key"), name);
|
||||
}
|
||||
|
||||
Future<void> openSendSheet() async {
|
||||
await tapByKey("home_page_send_button_key");
|
||||
}
|
||||
|
||||
Future<void> openReceiveSheet() async {
|
||||
await tapByKey("home_page_receive_button_key");
|
||||
}
|
||||
|
||||
Future<void> openSwapSheet() async {
|
||||
await tapByKey("home_page_swap_button_key");
|
||||
}
|
||||
|
||||
Future<void> openSettingsSheet() async {
|
||||
await tapByKey("home_page_settings_button_key");
|
||||
}
|
||||
|
||||
Future<void> openHistoryTab() async {
|
||||
final tabFinder = find.byKey(const ValueKey("line_tab_switcher_1_key"));
|
||||
|
||||
if (!tester.any(tabFinder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tapWhenVisible(tabFinder);
|
||||
|
||||
await settle();
|
||||
}
|
||||
|
||||
Future<void> confirmTransactionHistoryVisible({
|
||||
Duration timeout = const Duration(minutes: 3),
|
||||
}) async {
|
||||
final hasTransactions = await pumpUntil(
|
||||
() => _dashboardTransactionCount() > 0,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
expect(
|
||||
hasTransactions,
|
||||
true,
|
||||
reason: "No transactions arrived within ${timeout.inSeconds}s",
|
||||
);
|
||||
|
||||
await openHistoryTab();
|
||||
|
||||
await pumpUntilFound(find.byType(HistoryTile));
|
||||
|
||||
expect(
|
||||
tester.widgetList(find.byType(HistoryTile)).length,
|
||||
lessThanOrEqualTo(3),
|
||||
reason: "The home page preview should never render more than the short history",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> openAllTransactions() async {
|
||||
final actionButton = find.byKey(const ValueKey("assets_history_action_button_key"));
|
||||
final historyBar = find.byKey(const ValueKey("history_top_bar_key"));
|
||||
|
||||
await tapWhenVisible(tester.any(actionButton) ? actionButton : historyBar);
|
||||
|
||||
await pumpUntilFound(find.byKey(const ValueKey("history_modal_key")));
|
||||
}
|
||||
|
||||
Future<void> confirmAllTransactionsVisible() async {
|
||||
await pumpUntilFound(_allViewTiles);
|
||||
|
||||
final rendered = tester.widgetList(_allViewTiles).length;
|
||||
|
||||
if (_dashboardTransactionCount() > 3) {
|
||||
expect(
|
||||
rendered,
|
||||
greaterThan(3),
|
||||
reason: "The All view rendered $rendered tiles, no more than the home preview does",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
expect(_allViewTiles, findsWidgets);
|
||||
}
|
||||
|
||||
String firstTransactionIdInAllView() {
|
||||
final tile = tester.widgetList<HistoryTile>(_allViewTiles).first;
|
||||
final key = tile.key! as ValueKey<String>;
|
||||
|
||||
return key.value.substring("home_page_transaction_".length, key.value.length - "_key".length);
|
||||
}
|
||||
|
||||
Future<void> openFirstTransactionDetails() async {
|
||||
await pumpUntilFound(_allViewTiles);
|
||||
|
||||
await tester.tap(_allViewTiles.first);
|
||||
|
||||
await pumpUntilFound(find.byType(TransactionDetailsModal));
|
||||
|
||||
await settle();
|
||||
}
|
||||
|
||||
String openedTransactionId() {
|
||||
final modal = tester.widget<TransactionDetailsModal>(find.byType(TransactionDetailsModal));
|
||||
|
||||
return modal.transactionDetailsViewModel.transactionInfo.id;
|
||||
}
|
||||
|
||||
void hasTransactionIdRow() {
|
||||
expect(
|
||||
find.byKey(const ValueKey("standard_list_item_transaction_details_id_key")),
|
||||
findsOneWidget,
|
||||
reason: "The details opened without showing the transaction id",
|
||||
);
|
||||
}
|
||||
|
||||
// The home preview stays mounted underneath the modal, so its tiles have to be left out.
|
||||
Finder get _allViewTiles => find.descendant(
|
||||
of: find.byKey(const ValueKey("history_modal_key")),
|
||||
matching: find.byType(HistoryTile),
|
||||
);
|
||||
|
||||
Future<void> confirmSyncIndicatorShown({Duration timeout = const Duration(minutes: 3)}) async {
|
||||
final syncBar = find.byType(SyncBar);
|
||||
final tick = find.descendant(of: syncBar, matching: find.byIcon(Icons.check));
|
||||
|
||||
SyncStatus? lastStatus;
|
||||
var sawIndicator = false;
|
||||
|
||||
final synced = await pumpUntil(
|
||||
() {
|
||||
lastStatus = _dashboardStatus();
|
||||
|
||||
if (lastStatus == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final keyed = find.descendant(
|
||||
of: syncBar,
|
||||
matching: find.byKey(ValueKey(lastStatus.runtimeType)),
|
||||
);
|
||||
|
||||
sawIndicator = sawIndicator || tester.any(keyed) || tester.any(tick);
|
||||
|
||||
return lastStatus is SyncedSyncStatus;
|
||||
},
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
expect(
|
||||
synced,
|
||||
true,
|
||||
reason: "The wallet never synced within ${timeout.inMinutes}m, "
|
||||
"it reported ${lastStatus.runtimeType}",
|
||||
);
|
||||
|
||||
expect(
|
||||
sawIndicator,
|
||||
true,
|
||||
reason: "The sync bar never showed a status while the wallet synced",
|
||||
);
|
||||
}
|
||||
|
||||
SyncStatus? _dashboardStatus() {
|
||||
final finder = find.byType(NewHomePage);
|
||||
|
||||
if (!tester.any(finder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tester.widget<NewHomePage>(finder.first).dashboardViewModel.status;
|
||||
}
|
||||
|
||||
int _dashboardTransactionCount() {
|
||||
final finder = find.byType(NewHomePage);
|
||||
|
||||
if (!tester.any(finder)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return tester.widget<NewHomePage>(finder.first).dashboardViewModel.transactions.length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class LightningUsernamePageRobot extends BaseRobot {
|
||||
LightningUsernamePageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await pumpUntilFound(find.byKey(const ValueKey("lightning_username_page_skip_button_key")));
|
||||
}
|
||||
|
||||
Future<void> tapSkip() async {
|
||||
await tapByKey("lightning_username_page_skip_button_key");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart";
|
||||
import "package:cake_wallet/src/screens/settings/manage_nodes_page.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class ManageNodesPageRobot extends BaseRobot {
|
||||
ManageNodesPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<ManageNodesPage>();
|
||||
}
|
||||
|
||||
String currentNodeUri() {
|
||||
final page = tester.widget<ManageNodesPage>(find.byType(ManageNodesPage));
|
||||
|
||||
return page.nodeListViewModel.currentNode.uriRaw;
|
||||
}
|
||||
|
||||
Future<void> switchToAnotherNode() async {
|
||||
final rows = find.byType(NodeListRow);
|
||||
|
||||
await pumpUntilFound(rows.first);
|
||||
|
||||
expect(
|
||||
tester.widgetList(rows).length,
|
||||
greaterThan(1),
|
||||
reason: "Only the current node is listed, there is nothing to switch to",
|
||||
);
|
||||
|
||||
await tapWhenVisible(rows.at(1));
|
||||
|
||||
await tapWhenVisible(find.text(S.current.change));
|
||||
|
||||
await settle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class NewDashboardRobot extends BaseRobot {
|
||||
NewDashboardRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed({Duration timeout = const Duration(seconds: 30)}) async {
|
||||
await pumpUntilFound(find.byKey(const ValueKey("new_dashboard_page_key")), timeout: timeout);
|
||||
}
|
||||
|
||||
Future<void> openHomeTab() async {
|
||||
await tapByKey("dashboard_page_home_action_button_key");
|
||||
}
|
||||
|
||||
Future<void> openWalletsTab() async {
|
||||
await tapByKey("dashboard_page_wallets_action_button_key");
|
||||
}
|
||||
|
||||
Future<void> openContactsTab() async {
|
||||
await tapByKey("dashboard_page_contacts_action_button_key");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import "package:cake_wallet/new-ui/pages/receive_page.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class NewReceivePageRobot extends BaseRobot {
|
||||
NewReceivePageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await pumpUntilFound(find.byType(NewReceivePage));
|
||||
}
|
||||
|
||||
Future<void> confirmAddressMatches(String expected) async {
|
||||
final wanted = _normalise(expected);
|
||||
|
||||
await pumpUntilFound(find.byKey(const ValueKey("receive_page_address_key")));
|
||||
|
||||
final matched = await pumpUntil(() => _renderedAddress() == wanted);
|
||||
|
||||
expect(
|
||||
matched,
|
||||
true,
|
||||
reason: "Receive page should show $wanted but showed ${_renderedAddress()}",
|
||||
);
|
||||
}
|
||||
|
||||
String _normalise(String address) =>
|
||||
address.replaceAll("bitcoincash:", "").replaceAll(RegExp(r"\s"), "");
|
||||
|
||||
String? _renderedAddress() {
|
||||
final root = find.byKey(const ValueKey("receive_page_address_key"));
|
||||
|
||||
if (!tester.any(root)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final rich = find.descendant(of: root, matching: find.byType(RichText));
|
||||
|
||||
if (tester.any(rich)) {
|
||||
final text = _normalise(tester.widget<RichText>(rich.first).text.toPlainText());
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
final plain = find.descendant(of: root, matching: find.byType(Text));
|
||||
|
||||
if (tester.any(plain)) {
|
||||
return _normalise(tester.widget<Text>(plain.first).data ?? "");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import "package:cake_wallet/core/execution_state.dart";
|
||||
import "package:cake_wallet/new-ui/pages/send_page.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/confirm_swiper.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart";
|
||||
import "package:cake_wallet/view_model/send/send_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
import "../core/test_config.dart";
|
||||
|
||||
class NewSendPageRobot extends BaseRobot {
|
||||
NewSendPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await pumpUntilFound(find.byType(NewSendPage));
|
||||
}
|
||||
|
||||
Future<void> enterAddress(String address) async {
|
||||
await _enterTextInInput("send_page_address_input_key", address);
|
||||
}
|
||||
|
||||
Future<void> enterAmount(String amount) async {
|
||||
await _enterTextInInput("send_page_amount_input_key", amount);
|
||||
}
|
||||
|
||||
String enteredAddress() => _textInInput("send_page_address_input_key");
|
||||
|
||||
Future<void> pickContactFromAddressBook(String contactName) async {
|
||||
await tapByKey("send_page_address_book_button_key");
|
||||
|
||||
final tabs = find.byType(Tab);
|
||||
|
||||
await pumpUntilFound(tabs);
|
||||
|
||||
await tester.tap(tabs.at(1));
|
||||
|
||||
await settle();
|
||||
|
||||
final contactFinder = find.byKey(ValueKey(contactName));
|
||||
|
||||
await pumpUntilFound(contactFinder);
|
||||
|
||||
await tester.tap(contactFinder.first);
|
||||
|
||||
await pumpUntilGone(contactFinder);
|
||||
}
|
||||
|
||||
Future<void> tapSendButton() async {
|
||||
await tapByKey("send_page_send_button_key");
|
||||
}
|
||||
|
||||
Future<void> tapSendButtonWhenReady({Duration timeout = TestConfig.walletSyncBudget}) async {
|
||||
final ready = await pumpUntil(() => _model()?.isReadyForSend ?? false, timeout: timeout);
|
||||
|
||||
expect(
|
||||
ready,
|
||||
true,
|
||||
reason: "The wallet never became ready to send within ${timeout.inMinutes}m, "
|
||||
"so the send button stayed disabled",
|
||||
);
|
||||
|
||||
await tapSendButton();
|
||||
}
|
||||
|
||||
bool get isSendFlowOpen =>
|
||||
tester.any(find.byType(NewSendPage)) || tester.any(find.byType(SendConfirmSheet));
|
||||
|
||||
Future<void> swipeToConfirm({Duration timeout = const Duration(seconds: 90)}) async {
|
||||
final finder = find.byKey(const ValueKey("send_page_confirm_swiper_key"));
|
||||
|
||||
await pumpUntilFound(finder, timeout: timeout);
|
||||
|
||||
await settle(max: const Duration(seconds: 2));
|
||||
|
||||
final width = tester.view.physicalSize.width / tester.view.devicePixelRatio;
|
||||
await tester.drag(finder.first, Offset(width, 0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
Future<void> confirmSendButtonReplacesSwiper() async {
|
||||
final swiper = find.byKey(const ValueKey("send_page_confirm_swiper_key"));
|
||||
|
||||
await pumpUntilFound(swiper);
|
||||
|
||||
tester.binding.platformDispatcher.accessibilityFeaturesTestValue =
|
||||
const FakeAccessibilityFeatures(accessibleNavigation: true);
|
||||
addTearDown(tester.binding.platformDispatcher.clearAccessibilityFeaturesTestValue);
|
||||
|
||||
await settle(max: const Duration(seconds: 2));
|
||||
|
||||
final button = find.descendant(of: swiper, matching: find.byType(NewPrimaryButton));
|
||||
|
||||
expect(
|
||||
tester.any(button),
|
||||
true,
|
||||
reason: "Accessible navigation is on but the sheet still only offers the swiper",
|
||||
);
|
||||
|
||||
expect(
|
||||
tester.widget<NewPrimaryButton>(button.first).onPressed,
|
||||
isNotNull,
|
||||
reason: "The send button a screen reader gets is disabled",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> confirmTransactionBuilt({Duration timeout = const Duration(minutes: 3)}) async {
|
||||
await pumpUntil(
|
||||
() => tester.any(find.byType(ConfirmSwiper)) || _model()?.state is FailureState,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
if (tester.any(find.byType(ConfirmSwiper))) {
|
||||
return;
|
||||
}
|
||||
|
||||
fail("No confirm sheet after ${timeout.inSeconds}s. ${_describeSendState()}");
|
||||
}
|
||||
|
||||
Future<void> confirmTransactionCommitted({
|
||||
Duration timeout = const Duration(minutes: 3),
|
||||
}) async {
|
||||
await pumpUntilFound(find.byType(TransactionCommittedScreenActionButton), timeout: timeout);
|
||||
}
|
||||
|
||||
Future<void> expectNoTransactionBuilt({
|
||||
Duration window = const Duration(seconds: 15),
|
||||
}) async {
|
||||
final offered = await pumpUntil(
|
||||
() => tester.any(find.byType(ConfirmSwiper)),
|
||||
timeout: window,
|
||||
);
|
||||
|
||||
expect(
|
||||
offered,
|
||||
false,
|
||||
reason:
|
||||
"The send screen showed the swiper, wanting to broadcast a transaction it should have refused",
|
||||
);
|
||||
}
|
||||
|
||||
SendViewModel? _model() {
|
||||
final sheet = find.byType(SendConfirmSheet);
|
||||
|
||||
if (tester.any(sheet)) {
|
||||
return tester.widget<SendConfirmSheet>(sheet.first).sendViewModel;
|
||||
}
|
||||
|
||||
final page = find.byType(NewSendPage);
|
||||
|
||||
if (tester.any(page)) {
|
||||
return tester.widget<NewSendPage>(page.first).sendViewModel;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String _describeSendState() {
|
||||
final model = _model();
|
||||
|
||||
final available = model?.wallet.balance.values
|
||||
.map((balance) => balance.available)
|
||||
.where((amount) => amount.sign > 0)
|
||||
.join(", ");
|
||||
|
||||
final errorFinder = find.byType(TransactionErrorActions);
|
||||
final error = tester.any(errorFinder)
|
||||
? tester.widget<TransactionErrorActions>(errorFinder.first).errorText
|
||||
: model?.state is FailureState
|
||||
? (model!.state as FailureState).error
|
||||
: null;
|
||||
|
||||
return "state=${model?.state.runtimeType}, "
|
||||
"wallet holds ${available == null || available.isEmpty ? "nothing" : available}"
|
||||
"${error == null ? "" : ", the chain said: $error"}";
|
||||
}
|
||||
|
||||
String _textInInput(String inputKey) {
|
||||
final inputFinder = find.byKey(ValueKey(inputKey));
|
||||
|
||||
if (!tester.any(inputFinder)) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
final field = find.descendant(of: inputFinder, matching: find.byType(EditableText));
|
||||
|
||||
return tester.widget<EditableText>(field.first).controller.text;
|
||||
}
|
||||
|
||||
Future<void> _enterTextInInput(String inputKey, String text) async {
|
||||
final inputFinder = find.byKey(ValueKey(inputKey));
|
||||
|
||||
await pumpUntilFound(inputFinder);
|
||||
|
||||
final fieldFinder = find.descendant(of: inputFinder, matching: find.byType(EditableText));
|
||||
|
||||
await tester.enterText(fieldFinder.first, text);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import "package:cake_wallet/new-ui/pages/settings_page.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class NewSettingsPageRobot extends BaseRobot {
|
||||
NewSettingsPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await pumpUntilFound(find.byType(SettingsMainPage));
|
||||
}
|
||||
|
||||
Future<void> openRow(String route) async {
|
||||
await scrollUntilRowVisible(route);
|
||||
await tapByKey(route);
|
||||
}
|
||||
|
||||
Future<void> scrollUntilRowVisible(String route) async {
|
||||
final rowFinder = find.byKey(ValueKey(route));
|
||||
|
||||
if (tester.any(rowFinder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final scrollableFinder = find.descendant(
|
||||
of: find.byType(SettingsMainPage),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
rowFinder,
|
||||
200,
|
||||
scrollable: scrollableFinder.first,
|
||||
maxScrolls: 30,
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> confirmLeafPageDisplayed(Type pageType) async {
|
||||
await pumpUntilFound(find.byType(pageType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import "package:cake_wallet/core/execution_state.dart";
|
||||
import "package:cake_wallet/exchange/limits_state.dart";
|
||||
import "package:cake_wallet/new-ui/pages/swap_page.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/swap_page/swap_amount_box.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/view_model/send/send_view_model_state.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
import "../core/test_config.dart";
|
||||
import "../core/test_wallets.dart";
|
||||
|
||||
class NewSwapPageRobot extends BaseRobot {
|
||||
NewSwapPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await pumpUntilFound(find.byType(NewSwapPage));
|
||||
}
|
||||
|
||||
Future<void> enterDepositAmount(String amount) async {
|
||||
await enterTextByKey("swap_page_deposit_amount_field_key", amount);
|
||||
}
|
||||
|
||||
Future<void> chooseReceiveCurrency(CryptoCurrency currency) async {
|
||||
await tapByKey("swap_amount_box_receive_currency_button_key");
|
||||
|
||||
final search = find.descendant(
|
||||
of: find.byType(CurrencyPickerSearchField),
|
||||
matching: find.byType(EditableText),
|
||||
);
|
||||
|
||||
await pumpUntilFound(search);
|
||||
|
||||
await tester.enterText(search.first, currency.title);
|
||||
await settle(max: const Duration(seconds: 2));
|
||||
|
||||
await tapWhenVisible(
|
||||
find.byWidgetPredicate(
|
||||
(widget) => widget is CurrencyPickerRow && _isSameCurrency(widget.currency, currency),
|
||||
),
|
||||
);
|
||||
|
||||
final chosen = await pumpUntil(
|
||||
() => _isSameCurrency(
|
||||
tester.widget<NewSwapPage>(find.byType(NewSwapPage)).exchangeViewModel.receiveCurrency,
|
||||
currency,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
chosen,
|
||||
true,
|
||||
reason: "The picker did not switch the receive currency to ${currency.title}",
|
||||
);
|
||||
}
|
||||
|
||||
static bool _isSameCurrency(CryptoCurrency one, CryptoCurrency other) =>
|
||||
one.title.toLowerCase() == other.title.toLowerCase() &&
|
||||
(one.tag ?? "").toLowerCase() == (other.tag ?? "").toLowerCase();
|
||||
|
||||
Future<void> confirmQuoteReceived({Duration timeout = const Duration(seconds: 90)}) async {
|
||||
final received = await pumpUntil(() => _bestRate() > 0, timeout: timeout);
|
||||
|
||||
expect(received, true, reason: "No provider quote arrived within ${timeout.inSeconds}s");
|
||||
}
|
||||
|
||||
Future<void> enterMinimumViableDepositAmount() async {
|
||||
final limitsLoaded = await pumpUntil(
|
||||
() => _limitsState() is LimitsLoadedSuccessfully,
|
||||
timeout: const Duration(seconds: 90),
|
||||
);
|
||||
|
||||
expect(limitsLoaded, true, reason: "Provider limits never loaded");
|
||||
|
||||
final min = _limitsMin() ?? 0;
|
||||
|
||||
// 5 percent over the minimum, so a small rate move does not invalidate the trade.
|
||||
final amount = min > 0 ? (min * 1.05).toStringAsFixed(8) : "0.01";
|
||||
await enterDepositAmount(amount);
|
||||
}
|
||||
|
||||
Future<String> enterTestReceiveAddress() async {
|
||||
final exchangeViewModel =
|
||||
tester.widget<NewSwapPage>(find.byType(NewSwapPage)).exchangeViewModel;
|
||||
final receiveType = cryptoCurrencyOrTokenToWalletType(exchangeViewModel.receiveCurrency);
|
||||
final address = receiveType == null ? "" : TestWallets.receiveAddressFor(receiveType);
|
||||
|
||||
expect(
|
||||
address.isNotEmpty,
|
||||
true,
|
||||
reason: "No test receive address for ${exchangeViewModel.receiveCurrency.title}",
|
||||
);
|
||||
|
||||
final receiveCard = find.byWidgetPredicate(
|
||||
(widget) => widget is SwapAmountBox && widget.isReceiverCard,
|
||||
);
|
||||
|
||||
tester.state<SwapAmountBoxState>(receiveCard).addressController.text = address;
|
||||
await settle();
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
Future<void> confirmSwapButtonEnabled({Duration timeout = TestConfig.walletSyncBudget}) async {
|
||||
final button = find.byKey(const ValueKey("exchange_page_exchange_button_key"));
|
||||
|
||||
final enabled = await pumpUntil(
|
||||
() => tester.any(button) && !tester.widget<LoadingPrimaryButton>(button).isDisabled,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
expect(
|
||||
enabled,
|
||||
true,
|
||||
reason: "The swap button stayed disabled with an amount, a quote and a receive address",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tapSwapButton() async {
|
||||
await tapByKey("exchange_page_exchange_button_key");
|
||||
}
|
||||
|
||||
Future<void> confirmTradeCreated({Duration timeout = const Duration(minutes: 2)}) async {
|
||||
await pumpUntilFound(find.byType(SwapConfirmSheet), timeout: timeout);
|
||||
}
|
||||
|
||||
void confirmPayoutAddress(String expected) {
|
||||
final sheet = tester.widget<SwapConfirmSheet>(find.byType(SwapConfirmSheet));
|
||||
final payout = sheet.exchangeTradeViewModel.trade.payoutAddress ?? "";
|
||||
final isHex = expected.startsWith("0x");
|
||||
|
||||
expect(
|
||||
isHex ? payout.toLowerCase() : payout,
|
||||
isHex ? expected.toLowerCase() : expected,
|
||||
reason: "The trade pays out somewhere other than the test wallet",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> confirmDepositCommitted({Duration timeout = const Duration(minutes: 3)}) async {
|
||||
ExecutionState? lastState;
|
||||
|
||||
final committed = await pumpUntil(
|
||||
() {
|
||||
final sheet = find.byType(SwapConfirmSheet);
|
||||
|
||||
if (!tester.any(sheet)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
lastState =
|
||||
tester.widget<SwapConfirmSheet>(sheet).exchangeTradeViewModel.sendViewModel.state;
|
||||
|
||||
return lastState is TransactionCommitted;
|
||||
},
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
expect(
|
||||
committed,
|
||||
true,
|
||||
reason: "The deposit never committed within ${timeout.inMinutes}m, "
|
||||
"the sheet reported ${lastState.runtimeType}",
|
||||
);
|
||||
|
||||
expect(
|
||||
lastState,
|
||||
isA<TransactionCommitted>(),
|
||||
reason: "The confirm sheet closed before the deposit committed, "
|
||||
"it last reported ${lastState.runtimeType}",
|
||||
);
|
||||
|
||||
await pumpUntilGone(find.byType(NewSwapPage));
|
||||
}
|
||||
|
||||
double? _limitsMin() {
|
||||
final finder = find.byType(NewSwapPage);
|
||||
|
||||
if (!tester.any(finder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.limits.min;
|
||||
}
|
||||
|
||||
LimitsState? _limitsState() {
|
||||
final finder = find.byType(NewSwapPage);
|
||||
|
||||
if (!tester.any(finder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.limitsState;
|
||||
}
|
||||
|
||||
double _bestRate() {
|
||||
final finder = find.byType(NewSwapPage);
|
||||
|
||||
if (!tester.any(finder)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.bestRate;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,32 @@
|
||||
import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class NewWalletPageRobot {
|
||||
NewWalletPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class NewWalletPageRobot extends BaseRobot {
|
||||
NewWalletPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isNewWalletPage() async {
|
||||
await commonTestCases.isSpecificPage<NewWalletPage>();
|
||||
await commonTestCases.takeScreenshots('new_wallet_page');
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<NewWalletPage>();
|
||||
}
|
||||
|
||||
Future<void> enterWalletName(String walletName) async {
|
||||
await commonTestCases.enterText(
|
||||
walletName,
|
||||
'new_wallet_page_wallet_name_textformfield_key',
|
||||
);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await enterTextByKey("new_wallet_page_wallet_name_textformfield_key", walletName);
|
||||
}
|
||||
|
||||
Future<void> generateWalletName() async {
|
||||
await commonTestCases.tapItemByKey(
|
||||
'new_wallet_page_wallet_name_textformfield_generate_name_button_key',
|
||||
);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("new_wallet_page_wallet_name_textformfield_generate_name_button_key");
|
||||
}
|
||||
|
||||
Future<void> expectNameRejected() async {
|
||||
await pumpUntilFound(find.byType(AlertWithOneAction));
|
||||
|
||||
hasType<NewWalletPage>();
|
||||
}
|
||||
|
||||
Future<void> onNextButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('new_wallet_page_confirm_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("new_wallet_page_confirm_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,29 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class NewWalletTypePageRobot {
|
||||
NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class NewWalletTypePageRobot extends BaseRobot {
|
||||
NewWalletTypePageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isNewWalletTypePage() async {
|
||||
await commonTestCases.isSpecificPage<NewWalletTypePage>();
|
||||
await commonTestCases.takeScreenshots('new_wallet_type_page');
|
||||
}
|
||||
|
||||
void displaysCorrectTitle(bool isCreate) {
|
||||
commonTestCases.hasText(
|
||||
isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet,
|
||||
);
|
||||
}
|
||||
|
||||
void hasWalletTypeForm() {
|
||||
commonTestCases.hasType<WalletTypeForm>();
|
||||
}
|
||||
|
||||
void displaysCorrectImage(ThemeType type) {
|
||||
final walletTypeImage = Image.asset('assets/images/wallet_type.png').image;
|
||||
final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png').image;
|
||||
|
||||
find.image(
|
||||
type == ThemeType.dark ? walletTypeImage : walletTypeLightImage,
|
||||
);
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<NewWalletTypePage>();
|
||||
}
|
||||
|
||||
Future<void> findParticularWalletTypeInScrollableList(WalletType type) async {
|
||||
await tester.pump(Duration(milliseconds: 1000));
|
||||
|
||||
final scrollableWidget = find.descendant(
|
||||
of: find.byKey(Key('new_wallet_type_scrollable_key')),
|
||||
of: find.byKey(const Key("new_wallet_type_scrollable_key")),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
|
||||
final targetWidget = find.byKey(ValueKey('new_wallet_type_${type.name}_button_key'));
|
||||
final targetWidget = find.byKey(ValueKey("new_wallet_type_${type.name}_button_key"));
|
||||
|
||||
tester.printToConsole('Attempting to scroll to wallet type ${type.name}');
|
||||
tester.printToConsole("Attempting to scroll to wallet type ${type.name}");
|
||||
|
||||
await tester.pump(Duration(milliseconds: 500));
|
||||
await pumpUntilFound(scrollableWidget.first);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
targetWidget,
|
||||
@@ -58,12 +32,14 @@ class NewWalletTypePageRobot {
|
||||
maxScrolls: 20,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
await settle();
|
||||
|
||||
expect(tester.any(targetWidget), true,
|
||||
reason: 'Wallet type ${type.name} should be visible after scrolling');
|
||||
expect(
|
||||
tester.any(targetWidget),
|
||||
true,
|
||||
reason: "Wallet type ${type.name} should be visible after scrolling",
|
||||
);
|
||||
|
||||
// Additional check to ensure the widget is actually on screen
|
||||
final widgetRect = tester.getRect(targetWidget);
|
||||
final screenSize = tester.view.physicalSize;
|
||||
final screenRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
|
||||
@@ -71,15 +47,13 @@ class NewWalletTypePageRobot {
|
||||
expect(
|
||||
screenRect.overlaps(widgetRect),
|
||||
true,
|
||||
reason: 'Wallet type ${type.name} should be within screen bounds',
|
||||
reason: "Wallet type ${type.name} should be within screen bounds",
|
||||
);
|
||||
|
||||
tester.printToConsole('Wallet type ${type.name} is now visible and tappable');
|
||||
tester.printToConsole("Wallet type ${type.name} is now visible and tappable");
|
||||
}
|
||||
|
||||
// Tapping a wallet type now navigates directly (the page no longer has a
|
||||
// separate "Next" button — selection happens on tap).
|
||||
Future<void> selectWalletType(WalletType type) async {
|
||||
await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key');
|
||||
await tapByKey("new_wallet_type_${type.name}_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,24 @@
|
||||
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/pin_code/pin_code_widget.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class PinCodeWidgetRobot {
|
||||
PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class PinCodeWidgetRobot extends BaseRobot {
|
||||
PinCodeWidgetRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
@override
|
||||
Future<void> isDisplayed() async => hasPinCodeWidget();
|
||||
|
||||
void hasPinCodeWidget() {
|
||||
final pinCodeWidget = find.bySubtype<PinCodeWidget>();
|
||||
expect(pinCodeWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
void hasNumberButtonsVisible() {
|
||||
// Confirmation for buttons 1-9
|
||||
for (var i = 1; i < 10; i++) {
|
||||
commonTestCases.hasValueKey('pin_code_button_${i}_key');
|
||||
Future<void> enterPinCode(List<int> pinCode) async {
|
||||
for (final pin in pinCode) {
|
||||
await tapByKey("pin_code_button_${pin}_key");
|
||||
}
|
||||
|
||||
// Confirmation for 0 button
|
||||
commonTestCases.hasValueKey('pin_code_button_0_key');
|
||||
}
|
||||
|
||||
Future<void> enterPassword(String password, {int pumpDuration = 100}) async {
|
||||
await commonTestCases.enterText(
|
||||
password,
|
||||
'enter_wallet_password',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await commonTestCases.tapItemByKey(
|
||||
'unlock',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> enterPinCode(List<int> pinCode, {int pumpDuration = 100}) async {
|
||||
for (int pin in pinCode) {
|
||||
await commonTestCases.tapItemByKey(
|
||||
'pin_code_button_${pin}_key',
|
||||
pumpDuration: pumpDuration,
|
||||
);
|
||||
}
|
||||
|
||||
await commonTestCases.takeScreenshots('pin_code_widget');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await settle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart";
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class PreSeedPageRobot {
|
||||
PreSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class PreSeedPageRobot extends BaseRobot {
|
||||
PreSeedPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isPreSeedPage() async {
|
||||
await commonTestCases.isSpecificPage<PreSeedPage>();
|
||||
await commonTestCases.takeScreenshots('pre_seed_page');
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<PreSeedPage>();
|
||||
}
|
||||
|
||||
// The button is only built once all three boxes are ticked
|
||||
Future<void> onConfirmButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey("pre_seed_page_only_way_checkbox_key");
|
||||
await commonTestCases.tapItemByKey("pre_seed_page_write_down_checkbox_key");
|
||||
await commonTestCases.tapItemByKey("pre_seed_page_never_share_checkbox_key");
|
||||
await commonTestCases.tapItemByKey('pre_seed_page_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("pre_seed_page_only_way_checkbox_key");
|
||||
await tapByKey("pre_seed_page_write_down_checkbox_key");
|
||||
await tapByKey("pre_seed_page_never_share_checkbox_key");
|
||||
await tapByKey("pre_seed_page_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,101 @@
|
||||
import 'package:cake_wallet/entities/seed_type.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/entities/seed_type.dart";
|
||||
import "package:cake_wallet/src/screens/restore/wallet_restore_page.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/src/widgets/validable_annotated_editable_text.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class RestoreFromSeedOrKeysPageRobot {
|
||||
RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class RestoreFromSeedOrKeysPageRobot extends BaseRobot {
|
||||
RestoreFromSeedOrKeysPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isRestoreFromSeedKeyPage() async {
|
||||
await commonTestCases.isSpecificPage<WalletRestorePage>();
|
||||
await commonTestCases.takeScreenshots('wallet_restore_page');
|
||||
}
|
||||
|
||||
Future<void> confirmViewComponentsDisplayProperlyPerPageView() async {
|
||||
commonTestCases.hasText(S.current.wallet_name);
|
||||
commonTestCases.hasText(S.current.enter_seed_phrase);
|
||||
commonTestCases.hasText(S.current.restore_title_from_seed);
|
||||
|
||||
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key');
|
||||
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key');
|
||||
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
|
||||
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key');
|
||||
|
||||
commonTestCases.hasText(S.current.private_key, hasWidget: false);
|
||||
commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false);
|
||||
|
||||
await commonTestCases.swipePage();
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
commonTestCases.hasText(S.current.wallet_name);
|
||||
commonTestCases.hasText(S.current.private_key);
|
||||
commonTestCases.hasText(S.current.restore_title_from_keys);
|
||||
|
||||
commonTestCases.hasText(S.current.enter_seed_phrase, hasWidget: false);
|
||||
commonTestCases.hasText(S.current.restore_title_from_seed, hasWidget: false);
|
||||
|
||||
await commonTestCases.swipePage(swipeRight: false);
|
||||
}
|
||||
|
||||
void confirmRestoreButtonDisplays() {
|
||||
commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key');
|
||||
}
|
||||
|
||||
void confirmAdvancedSettingButtonDisplays() {
|
||||
commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key');
|
||||
}
|
||||
|
||||
Future<void> enterWalletNameText(String walletName, {bool isSeedFormEntry = true}) async {
|
||||
await commonTestCases.enterText(
|
||||
walletName,
|
||||
'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_textfield_key',
|
||||
);
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WalletRestorePage>();
|
||||
}
|
||||
|
||||
Future<void> selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async {
|
||||
await commonTestCases.tapItemByKey(
|
||||
await tapByKey(
|
||||
'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_refresh_button_key',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> enterSeedPhraseForWalletRestore(String text) async {
|
||||
ValidatableAnnotatedEditableTextState seedTextState =
|
||||
await tester.state(find.byType(ValidatableAnnotatedEditableText));
|
||||
final ValidatableAnnotatedEditableTextState seedTextState =
|
||||
tester.state(find.byType(ValidatableAnnotatedEditableText));
|
||||
|
||||
seedTextState.widget.controller.text = text;
|
||||
await tester.pumpAndSettle();
|
||||
await settle();
|
||||
}
|
||||
|
||||
Future<void> enterPasswordForWalletRestore(String text) async {
|
||||
await commonTestCases.enterText(
|
||||
text,
|
||||
'password',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await enterTextByKey("password", text);
|
||||
await settle();
|
||||
}
|
||||
|
||||
Future<void> enterPasswordRepeatForWalletRestore(String text) async {
|
||||
await commonTestCases.enterText(
|
||||
text,
|
||||
'repeat_wallet_password',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await enterTextByKey("repeat_wallet_password", text);
|
||||
await settle();
|
||||
}
|
||||
|
||||
Future<void> enterBlockHeightForWalletRestore(String blockHeight) async {
|
||||
await commonTestCases.enterText(
|
||||
blockHeight,
|
||||
'wallet_restore_from_seed_blockheight_textfield_key',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await enterTextByKey("wallet_restore_from_seed_blockheight_textfield_key", blockHeight);
|
||||
await settle();
|
||||
}
|
||||
|
||||
Future<void> chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType selectedType) async {
|
||||
await commonTestCases.tapItemByKey('wallet_restore_from_seed_seedtype_picker_button_key');
|
||||
await tapByKey("wallet_restore_from_seed_seedtype_picker_button_key");
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await commonTestCases.tapItemByKey('picker_items_index_${selectedType.title}_button_key');
|
||||
await tapByKey("picker_items_index_${selectedType.title}_button_key");
|
||||
}
|
||||
|
||||
Future<void> onPasteSeedPhraseButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
|
||||
}
|
||||
|
||||
Future<void> enterPrivateKeyForWalletRestore(String privateKey) async {
|
||||
await commonTestCases.enterText(
|
||||
privateKey,
|
||||
'wallet_restore_from_key_private_key_textfield_key',
|
||||
Future<void> expectRestoreRefused({Duration window = const Duration(seconds: 10)}) async {
|
||||
final button = tester.widget<LoadingPrimaryButton>(
|
||||
find.byKey(const ValueKey("wallet_restore_seed_or_key_restore_button_key")),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
button.isDisabled,
|
||||
true,
|
||||
reason: "The restore button accepted a seed the wallet cannot parse",
|
||||
);
|
||||
|
||||
final left = await pumpUntil(
|
||||
() => !tester.any(find.byType(WalletRestorePage)),
|
||||
timeout: window,
|
||||
);
|
||||
|
||||
expect(left, false, reason: "A seed the wallet cannot parse still restored something");
|
||||
}
|
||||
|
||||
Future<void> onRestoreWalletButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("wallet_restore_seed_or_key_restore_button_key");
|
||||
}
|
||||
|
||||
Future<void> enablePassphrase() async {
|
||||
await tapByKey("wallet_restore_has_passphrase_checkbox_key");
|
||||
await settle();
|
||||
|
||||
final page = tester.widget<WalletRestorePage>(find.byType(WalletRestorePage));
|
||||
|
||||
expect(
|
||||
page.walletRestoreViewModel.hasPassphrase,
|
||||
true,
|
||||
reason: "The passphrase box did not tick, the wallet would restore without it",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> enterPassphrase(String passphrase) async {
|
||||
await enterTextByKey(
|
||||
"add_passphrase_bottom_sheet_widget_passphrase_textfield_key",
|
||||
passphrase,
|
||||
);
|
||||
await enterTextByKey(
|
||||
"add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key",
|
||||
passphrase,
|
||||
);
|
||||
|
||||
await tapByKey("add_passphrase_bottom_sheet_widget_restore_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,16 @@
|
||||
import 'package:cake_wallet/src/screens/restore/restore_options_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/restore/restore_options_page.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class RestoreOptionsPageRobot {
|
||||
RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class RestoreOptionsPageRobot extends BaseRobot {
|
||||
RestoreOptionsPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isRestoreOptionsPage() async {
|
||||
await commonTestCases.isSpecificPage<RestoreOptionsPage>();
|
||||
await commonTestCases.takeScreenshots('restore_options');
|
||||
}
|
||||
|
||||
void hasRestoreOptionsButton() {
|
||||
commonTestCases.hasValueKey('restore_options_from_seeds_or_keys_button_key');
|
||||
commonTestCases.hasValueKey('restore_options_from_backup_button_key');
|
||||
commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key');
|
||||
commonTestCases.hasValueKey('restore_options_from_qr_button_key');
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<RestoreOptionsPage>();
|
||||
}
|
||||
|
||||
Future<void> navigateToRestoreFromSeedsOrKeysPage() async {
|
||||
await commonTestCases.tapItemByKey('restore_options_from_seeds_or_keys_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> navigateToRestoreFromBackupPage() async {
|
||||
await commonTestCases.tapItemByKey('restore_options_from_backup_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> navigateToRestoreFromHardwareWalletPage() async {
|
||||
await commonTestCases.tapItemByKey('restore_options_from_hardware_wallet_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> backAndVerify() async {
|
||||
await commonTestCases.goBack();
|
||||
await isRestoreOptionsPage();
|
||||
await tapByKey("restore_options_from_seeds_or_keys_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class SecurityAndBackupPageRobot {
|
||||
SecurityAndBackupPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isSecurityAndBackupPage() async {
|
||||
await commonTestCases.isSpecificPage<SecurityBackupPage>();
|
||||
await commonTestCases.takeScreenshots('security_backup_page');
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
commonTestCases.hasText(S.current.security_and_backup);
|
||||
}
|
||||
|
||||
Future<void> navigateToShowKeysPage() async {
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key');
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import "package:cake_wallet/src/screens/settings/security_backup_page.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class SecurityBackupPageRobot extends BaseRobot {
|
||||
SecurityBackupPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<SecurityBackupPage>();
|
||||
}
|
||||
|
||||
Future<void> openChangePin() async {
|
||||
await tapByKey("security_backup_page_change_pin_button_key");
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class SeedVerificationPageRobot {
|
||||
SeedVerificationPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class SeedVerificationPageRobot extends BaseRobot {
|
||||
SeedVerificationPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isSeedVerificationPage() async {
|
||||
await commonTestCases.isSpecificPage<SeedVerificationPage>();
|
||||
await commonTestCases.takeScreenshots('seed_verification_page');
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<SeedVerificationPage>();
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
commonTestCases.hasText(S.current.verify_seed);
|
||||
hasText(S.current.verify_seed);
|
||||
}
|
||||
|
||||
Future<void> verifyWalletSeeds() async {
|
||||
@@ -28,18 +25,23 @@ class SeedVerificationPageRobot {
|
||||
while (!walletSeedViewModel.isVerificationComplete &&
|
||||
walletSeedViewModel.verificationWordCount != 0) {
|
||||
final currentCorrectWord = walletSeedViewModel.currentCorrectWord;
|
||||
final currentStep = walletSeedViewModel.currentStepIndex;
|
||||
|
||||
commonTestCases.hasTextAtLestOnce(currentCorrectWord);
|
||||
hasTextAtLeastOnce(currentCorrectWord);
|
||||
|
||||
await commonTestCases.tapItemByKey(
|
||||
'seed_verification_option_${currentCorrectWord}_button_key',
|
||||
await tapByKey("seed_verification_option_${currentCorrectWord}_button_key");
|
||||
|
||||
final hasMovedOn = await pumpUntil(
|
||||
() =>
|
||||
walletSeedViewModel.isVerificationComplete ||
|
||||
walletSeedViewModel.currentStepIndex != currentStep,
|
||||
);
|
||||
|
||||
await commonTestCases.defaultSleepTime(seconds: 1);
|
||||
if (!hasMovedOn) {
|
||||
throw TestFailure("Seed verification never moved past step $currentStep");
|
||||
}
|
||||
}
|
||||
|
||||
await commonTestCases.tapItemByKey('wallet_seed_page_open_wallet_button_key');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("wallet_seed_page_open_wallet_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,716 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/send/send_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import '../components/common_test_constants.dart';
|
||||
import 'auth_page_robot.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
|
||||
|
||||
class SendPageRobot {
|
||||
SendPageRobot({required this.tester})
|
||||
: commonTestCases = CommonTestCases(tester),
|
||||
authPageRobot = AuthPageRobot(tester);
|
||||
|
||||
WidgetTester tester;
|
||||
CommonTestCases commonTestCases;
|
||||
AuthPageRobot authPageRobot;
|
||||
|
||||
Future<void> isSendPage() async {
|
||||
await commonTestCases.isSpecificPage<SendPage>();
|
||||
await commonTestCases.takeScreenshots('send_page');
|
||||
}
|
||||
|
||||
Future<void> waitForSendPage() async {
|
||||
tester.printToConsole('Waiting for SendPage to be available');
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final maxDuration = Duration(seconds: 30);
|
||||
|
||||
while (stopwatch.elapsed < maxDuration) {
|
||||
await tester.pump(Duration(milliseconds: 500));
|
||||
|
||||
// Check if we're still on auth page
|
||||
if (authPageRobot.onAuthPage()) {
|
||||
tester.printToConsole('Still on auth page, waiting...');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if SendPage is available
|
||||
final sendPageFinder = find.byType(SendPage);
|
||||
if (sendPageFinder.tryEvaluate()) {
|
||||
tester.printToConsole('SendPage found!');
|
||||
return;
|
||||
}
|
||||
|
||||
tester.printToConsole('SendPage not found yet, waiting...');
|
||||
}
|
||||
|
||||
throw Exception('SendPage not found after ${maxDuration.inSeconds} seconds');
|
||||
}
|
||||
|
||||
Future<void> checkIfSendPageIsVisible() async {
|
||||
tester.printToConsole('Confirming SendPage is visible');
|
||||
|
||||
if (authPageRobot.onAuthPage()) {
|
||||
tester.printToConsole('Auth page is currently visible');
|
||||
await _handleAuthPage();
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
await waitForSendPage();
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
commonTestCases.hasText(S.current.send);
|
||||
}
|
||||
|
||||
void confirmViewComponentsDisplayProperly() {
|
||||
SendPage sendPage = tester.widget(find.byType(SendPage));
|
||||
final sendViewModel = sendPage.sendViewModel;
|
||||
|
||||
commonTestCases.hasValueKey('send_page_address_textfield_key');
|
||||
commonTestCases.hasValueKey('send_page_note_textfield_key');
|
||||
commonTestCases.hasValueKey('send_page_amount_textfield_key');
|
||||
commonTestCases.hasValueKey('send_page_add_template_button_key');
|
||||
|
||||
if (sendViewModel.hasMultipleTokens) {
|
||||
commonTestCases.hasValueKey('send_page_currency_picker_button_key');
|
||||
}
|
||||
|
||||
if (!sendViewModel.isBatchSending) {
|
||||
commonTestCases.hasValueKey('send_page_send_all_button_key');
|
||||
}
|
||||
|
||||
if (!sendViewModel.isFiatDisabled) {
|
||||
commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key');
|
||||
}
|
||||
|
||||
if (sendViewModel.feesViewModel.hasFees) {
|
||||
commonTestCases.hasValueKey('send_page_select_fee_priority_button_key');
|
||||
}
|
||||
|
||||
if (sendViewModel.hasCoinControl) {
|
||||
commonTestCases.hasValueKey('send_page_unspent_coin_button_key');
|
||||
}
|
||||
|
||||
if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) {
|
||||
commonTestCases.hasValueKey('send_page_add_receiver_button_key');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
|
||||
final currencyPickerKey = 'send_page_currency_picker_button_key';
|
||||
final currencyPickerDialogKey = 'send_page_currency_picker_dialog_button_key';
|
||||
|
||||
await commonTestCases.tapItemByKey(currencyPickerKey);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
// Check if picker dialog is present
|
||||
if (!commonTestCases.isKeyPresent(currencyPickerDialogKey)) {
|
||||
tester.printToConsole('Currency picker dialog not found, may already be selected');
|
||||
return;
|
||||
}
|
||||
|
||||
SendPage sendPage = tester.widget(find.byType(SendPage));
|
||||
final sendViewModel = sendPage.sendViewModel;
|
||||
|
||||
if (receiveCurrency == sendViewModel.selectedCryptoCurrency) {
|
||||
await commonTestCases
|
||||
.tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
|
||||
return;
|
||||
}
|
||||
|
||||
await commonTestCases.enterText(receiveCurrency.title, 'search_bar_widget_key');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.fullName}_button_key');
|
||||
}
|
||||
|
||||
Future<void> enterReceiveAddress(String receiveAddress) async {
|
||||
await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> enterSendAmount(String amount, {bool isFiat = false}) async {
|
||||
await commonTestCases.enterText(
|
||||
amount,
|
||||
isFiat ? 'send_page_fiat_amount_textfield_key' : 'send_page_amount_textfield_key',
|
||||
);
|
||||
}
|
||||
|
||||
String _getTextFromField(ValueKey<String> key) {
|
||||
final field = find.byKey(key);
|
||||
if (!field.tryEvaluate()) return '';
|
||||
final baseTextFormField = tester.widget<BaseTextFormField>(field);
|
||||
return baseTextFormField.controller?.text ?? '';
|
||||
}
|
||||
|
||||
/// Validates wallet balance for $1 send by checking against the converted crypto amount
|
||||
/// Returns true if wallet has sufficient balance, false otherwise
|
||||
Future<bool> validateWalletBalanceForOneDollarSend() async {
|
||||
SendPage sendPage = tester.widget(find.byType(SendPage));
|
||||
final sendViewModel = sendPage.sendViewModel;
|
||||
|
||||
final balance = await sendViewModel.sendingBalance;
|
||||
|
||||
await setupOneDollarSend();
|
||||
|
||||
// Get the crypto amount that was set for the $1 send
|
||||
String cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
|
||||
if (cryptoAmount.isEmpty || cryptoAmount == '0' || cryptoAmount == '0.0') {
|
||||
cryptoAmount = '0.001'; // fallback amount
|
||||
}
|
||||
|
||||
final amount = double.tryParse(cryptoAmount) ?? 0.0;
|
||||
|
||||
tester.printToConsole(
|
||||
'Wallet balance: $balance, sending amount for \$${CommonTestConstants.sendTestFiatAmount}: $cryptoAmount',
|
||||
);
|
||||
|
||||
if (balance.isEmpty || double.tryParse(balance) == null) {
|
||||
tester.printToConsole('Invalid wallet balance: $balance');
|
||||
return false;
|
||||
}
|
||||
|
||||
final balanceValue = double.parse(balance);
|
||||
if (balanceValue < amount) {
|
||||
tester.printToConsole(
|
||||
'Insufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send: $balanceValue < $amount',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
tester.printToConsole(
|
||||
'Wallet has sufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> selectTransactionPriority({TransactionPriority? priority}) async {
|
||||
SendPage sendPage = tester.widget(find.byType(SendPage));
|
||||
final sendViewModel = sendPage.sendViewModel;
|
||||
|
||||
if (!sendViewModel.feesViewModel.hasFees || priority == null) return;
|
||||
|
||||
final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key';
|
||||
await commonTestCases.tapItemByKey(transactionPriorityPickerKey);
|
||||
|
||||
if (priority == sendViewModel.feesViewModel.transactionPriority) {
|
||||
await commonTestCases
|
||||
.tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key');
|
||||
return;
|
||||
}
|
||||
|
||||
await commonTestCases.dragUntilVisible(
|
||||
'picker_items_index_${priority.title}_button_key',
|
||||
'picker_scrollbar_key',
|
||||
);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await commonTestCases.tapItemByKey('picker_items_index_${priority.title}_button_key');
|
||||
}
|
||||
|
||||
Future<void> onSendButtonPressed() async {
|
||||
tester.printToConsole('Pressing send');
|
||||
|
||||
await checkIfSendPageIsVisible();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
final sendPage = tester.widget<SendPage>(find.byType(SendPage));
|
||||
|
||||
while (true) {
|
||||
bool isReadyForSend = sendPage.sendViewModel.isReadyForSend;
|
||||
await tester.pump();
|
||||
if (isReadyForSend) {
|
||||
tester.printToConsole('Is ready for send');
|
||||
break;
|
||||
} else {
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tester.pumpAndSettle();
|
||||
tester.printToConsole('not yet ready for send');
|
||||
}
|
||||
}
|
||||
await commonTestCases.tapItemByKey(
|
||||
'send_page_send_button_key',
|
||||
shouldPumpAndSettle: false,
|
||||
);
|
||||
|
||||
await _waitForSendTransactionCompletion();
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> _waitForSendTransactionCompletion() async {
|
||||
await tester.pump();
|
||||
final Completer<void> completer = Completer<void>();
|
||||
|
||||
// Loop to wait for the async operation to complete
|
||||
while (true) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
tester.printToConsole('Before _handleAuth');
|
||||
|
||||
await _handleAuthPage();
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
tester.printToConsole('After _handleAuth');
|
||||
|
||||
await tester.pump();
|
||||
|
||||
if (authPageRobot.onAuthPage()) {
|
||||
tester.printToConsole('Still on auth page, continuing to wait');
|
||||
continue;
|
||||
}
|
||||
|
||||
final sendPageFinder = find.byType(SendPage);
|
||||
if (!sendPageFinder.tryEvaluate()) {
|
||||
tester.printToConsole('SendPage not found yet, continuing to wait');
|
||||
continue;
|
||||
}
|
||||
|
||||
final sendPage = tester.widget<SendPage>(sendPageFinder);
|
||||
final state = sendPage.sendViewModel.state;
|
||||
|
||||
await tester.pump();
|
||||
|
||||
bool isDone = state is ExecutedSuccessfullyState || state is TransactionCommitted;
|
||||
bool isFailed = state is FailureState;
|
||||
|
||||
tester.printToConsole('isDone: $isDone');
|
||||
tester.printToConsole('isFailed: $isFailed');
|
||||
|
||||
if (isDone || isFailed) {
|
||||
tester.printToConsole(
|
||||
isDone ? 'Completer is done' : 'Completer is done though operation failed',
|
||||
);
|
||||
completer.complete();
|
||||
await tester.pump();
|
||||
break;
|
||||
} else {
|
||||
tester.printToConsole('Completer is not done');
|
||||
await tester.pump();
|
||||
}
|
||||
}
|
||||
|
||||
await expectLater(completer.future, completes);
|
||||
|
||||
tester.printToConsole('Done confirming sending operation');
|
||||
}
|
||||
|
||||
Future<void> _handleAuthPage() async {
|
||||
tester.printToConsole('Inside _handleAuth');
|
||||
|
||||
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
|
||||
if (onAuthPageDesktop) {
|
||||
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
|
||||
await commonTestCases.defaultSleepTime();
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.pump();
|
||||
tester.printToConsole('starting auth checks');
|
||||
|
||||
final authPage = authPageRobot.onAuthPage();
|
||||
|
||||
tester.printToConsole('hasAuth:$authPage');
|
||||
|
||||
if (authPage) {
|
||||
await tester.pump();
|
||||
tester.printToConsole('Starting inner _handleAuth loop checks');
|
||||
|
||||
try {
|
||||
await authPageRobot.enterPinCode(CommonTestConstants.pin, pumpDuration: 500);
|
||||
tester.printToConsole('Auth done');
|
||||
|
||||
await tester.pump(Duration(seconds: 3));
|
||||
|
||||
tester.printToConsole('Auth pump done');
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
} catch (e) {
|
||||
tester.printToConsole('Auth failed, retrying: $e');
|
||||
await tester.pump();
|
||||
|
||||
await commonTestCases.defaultSleepTime(seconds: 1);
|
||||
await _handleAuthPage();
|
||||
}
|
||||
} else {
|
||||
tester.printToConsole('No auth page detected, proceeding');
|
||||
}
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
//* ------ On Sending Failure ------------
|
||||
Future<bool> hasErrorWhileSending() async {
|
||||
await tester.pump();
|
||||
|
||||
tester.printToConsole('Checking if there is an error');
|
||||
|
||||
final errorDialog = find.byKey(ValueKey('send_page_send_failure_dialog_button_key'));
|
||||
|
||||
bool hasError = errorDialog.tryEvaluate();
|
||||
|
||||
tester.printToConsole('Has error: $hasError');
|
||||
|
||||
return hasError;
|
||||
}
|
||||
|
||||
Future<void> onSendFailureDialogButtonPressed() async {
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
tester.printToConsole('Send Button Failure Dialog Triggered');
|
||||
|
||||
await commonTestCases.tapItemByKey('send_page_send_failure_dialog_button_key');
|
||||
}
|
||||
|
||||
//* ------ On Sending Success ------------
|
||||
Future<void> onSendSliderOnConfirmSendingBottomSheetDragged() async {
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tester.pump();
|
||||
|
||||
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
|
||||
tester.printToConsole('Found confirm sending bottom sheet, starting slider drag');
|
||||
|
||||
final accessibleButton = find.byType(PrimaryButton);
|
||||
if (accessibleButton.tryEvaluate()) {
|
||||
tester.printToConsole('Found accessible navigation button, tapping it');
|
||||
await tester.tap(accessibleButton);
|
||||
await tester.pumpAndSettle();
|
||||
} else {
|
||||
await _performSendSliderDrag();
|
||||
}
|
||||
|
||||
tester.printToConsole('Slider/button action completed, waiting for transaction completion');
|
||||
|
||||
// Check if bottom sheet is dismissed
|
||||
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
|
||||
tester.printToConsole('Bottom sheet still present, waiting a bit more for dismissal');
|
||||
await commonTestCases.defaultSleepTime(seconds: 3);
|
||||
|
||||
// If still present, try one more manual dismissal attempt
|
||||
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
|
||||
tester.printToConsole('Bottom sheet still present, attempting final manual dismissal');
|
||||
await tester.tapAt(Offset(200, 200)); // Tap outside
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for transaction completion
|
||||
await _waitForCommitTransactionCompletion();
|
||||
await commonTestCases.defaultSleepTime(seconds: 2);
|
||||
} else {
|
||||
tester.printToConsole('Confirm sending bottom sheet not found, waiting and retrying');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tester.pump();
|
||||
await onSendSliderOnConfirmSendingBottomSheetDragged();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performSendSliderDrag() async {
|
||||
final sliderFinder = find.byKey(const ValueKey('standard_slide_button_widget_slider_key'));
|
||||
expect(sliderFinder, findsOneWidget, reason: 'Slider should be found');
|
||||
|
||||
// Find the StandardSlideButton widget to get the main container
|
||||
final slideButtonFinder = find.byType(StandardSlideButton);
|
||||
expect(slideButtonFinder, findsOneWidget, reason: 'StandardSlideButton should be found');
|
||||
|
||||
// Get the main container bounds (the outer container, not the slider container)
|
||||
final mainContainerRect = tester.getRect(slideButtonFinder);
|
||||
final containerWidth = mainContainerRect.width;
|
||||
|
||||
final sideMargin = 4.0;
|
||||
final sliderWidth = 42.0;
|
||||
final effectiveMaxWidth = containerWidth - 2 * sideMargin;
|
||||
final threshold = effectiveMaxWidth - sliderWidth - 10;
|
||||
|
||||
// Add a small buffer to ensure we exceed the threshold
|
||||
final dragDistance = threshold + 20;
|
||||
|
||||
tester.printToConsole(
|
||||
'Main container width: $containerWidth, Threshold: $threshold, Drag distance: $dragDistance',
|
||||
);
|
||||
|
||||
// Start the drag
|
||||
await tester.drag(sliderFinder, Offset(dragDistance, 0));
|
||||
|
||||
// Wait for the drag to complete and trigger onHorizontalDragEnd
|
||||
await tester.pump(Duration(milliseconds: 100));
|
||||
|
||||
// Release the drag (this should trigger onHorizontalDragEnd)
|
||||
await tester.pump(Duration(seconds: 2));
|
||||
|
||||
tester.printToConsole('Drag completed, waiting for callback');
|
||||
|
||||
// Wait for the slide completion callback to trigger and bottom sheet to dismiss
|
||||
await _waitForBottomSheetDismissal();
|
||||
}
|
||||
|
||||
Future<void> _waitForBottomSheetDismissal() async {
|
||||
tester.printToConsole('Waiting for bottom sheet dismissal');
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final maxDuration = Duration(seconds: 10);
|
||||
|
||||
while (stopwatch.elapsed < maxDuration) {
|
||||
await tester.pump(Duration(milliseconds: 500));
|
||||
|
||||
// Check if the confirm bottom sheet is still present
|
||||
if (!commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
|
||||
tester.printToConsole('Bottom sheet dismissed successfully!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if transaction has started (this indicates slider was successful)
|
||||
if (_isTransactionStarted()) {
|
||||
tester.printToConsole(
|
||||
'Transaction started, slider was successful even if bottom sheet is still visible');
|
||||
return;
|
||||
}
|
||||
|
||||
tester.printToConsole('Bottom sheet still present, waiting');
|
||||
}
|
||||
|
||||
tester.printToConsole('Bottom sheet dismissal timeout reached, trying manual dismissal');
|
||||
|
||||
// If the bottom sheet is still present after timeout, try to manually dismiss it
|
||||
// This might happen if the slider drag didn't trigger the onSlideComplete properly
|
||||
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
|
||||
tester.printToConsole('Attempting manual bottom sheet dismissal');
|
||||
|
||||
// Try to find and tap a close button or back button
|
||||
final closeButton = find.byIcon(Icons.close);
|
||||
if (closeButton.tryEvaluate()) {
|
||||
await tester.tap(closeButton);
|
||||
await tester.pumpAndSettle();
|
||||
tester.printToConsole('Manual close button tapped');
|
||||
} else {
|
||||
// Try to pop the bottom sheet by tapping outside or using back gesture
|
||||
await tester.tapAt(Offset(100, 100)); // Tap outside the bottom sheet
|
||||
await tester.pumpAndSettle();
|
||||
tester.printToConsole('Tapped outside bottom sheet');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isTransactionStarted() {
|
||||
try {
|
||||
// Check if SendPage is available and transaction state has changed
|
||||
final sendPageFinder = find.byType(SendPage);
|
||||
if (!sendPageFinder.tryEvaluate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final sendPage = tester.widget<SendPage>(sendPageFinder);
|
||||
final state = sendPage.sendViewModel.state;
|
||||
|
||||
// Check if we're in a transaction-related state
|
||||
return state is TransactionCommitting ||
|
||||
state is IsExecutingState ||
|
||||
state is TransactionCommitted ||
|
||||
state is ExecutedSuccessfullyState;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _waitForCommitTransactionCompletion() async {
|
||||
tester.printToConsole('Starting to wait for transaction completion');
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final maxDuration = Duration(seconds: 60);
|
||||
|
||||
while (stopwatch.elapsed < maxDuration) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
// Check if the confirm bottom sheet is gone (transaction started)
|
||||
if (!commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
|
||||
tester.printToConsole('Confirm bottom sheet disappeared, transaction may be processing');
|
||||
}
|
||||
|
||||
// Check if we've navigated back to the dashboard (for cases of successful transaction)
|
||||
final dashboardFinder = find.byType(DashboardPage);
|
||||
if (dashboardFinder.tryEvaluate()) {
|
||||
tester.printToConsole('Dashboard detected, transaction completed successfully!');
|
||||
await tester.pump();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if SendPage is still available (transaction still processing)
|
||||
final sendPageFinder = find.byType(SendPage);
|
||||
if (sendPageFinder.tryEvaluate()) {
|
||||
final sendPage = tester.widget<SendPage>(sendPageFinder);
|
||||
final state = sendPage.sendViewModel.state;
|
||||
|
||||
bool isDone = state is ExecutedSuccessfullyState || state is TransactionCommitted;
|
||||
bool isFailed = state is FailureState;
|
||||
|
||||
tester.printToConsole('Transaction state: $state');
|
||||
tester.printToConsole('isDone: $isDone');
|
||||
tester.printToConsole('isFailed: $isFailed');
|
||||
|
||||
if (isDone) {
|
||||
tester.printToConsole('Transaction committed successfully!');
|
||||
await tester.pump();
|
||||
return;
|
||||
} else if (isFailed) {
|
||||
tester.printToConsole('Transaction failed: $state');
|
||||
await tester.pump();
|
||||
return;
|
||||
} else {
|
||||
tester.printToConsole('Transaction still processing');
|
||||
await tester.pump();
|
||||
}
|
||||
} else {
|
||||
// SendPage not found, check if we're on dashboard or still processing
|
||||
if (dashboardFinder.tryEvaluate()) {
|
||||
tester.printToConsole('Dashboard detected - transaction completed successfully!');
|
||||
await tester.pump();
|
||||
return;
|
||||
} else {
|
||||
tester.printToConsole(
|
||||
'SendPage not found, but dashboard not yet visible - continuing to wait...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stopwatch.elapsed >= maxDuration) {
|
||||
tester.printToConsole('Transaction completion timeout reached');
|
||||
}
|
||||
|
||||
tester.printToConsole('Done waiting for transaction completion');
|
||||
}
|
||||
|
||||
//* ---- Handle Transaction Success Flow -----
|
||||
Future<void> handleTransactionSuccessFlow() async {
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
// Wait for any success dialogs to appear
|
||||
await tester.pump(Duration(seconds: 2));
|
||||
|
||||
// Check for contact addition dialog first (if new contact address exists)
|
||||
final contactDialog = find.byKey(ValueKey('send_page_add_contact_bottom_sheet_yes_button_key'));
|
||||
if (contactDialog.tryEvaluate()) {
|
||||
tester.printToConsole('Found contact addition dialog, selecting Yes');
|
||||
|
||||
// Check if the button is actually visible and tappable
|
||||
final buttonRect = tester.getRect(contactDialog);
|
||||
final screenSize = tester.view.physicalSize / tester.view.devicePixelRatio;
|
||||
|
||||
if (buttonRect.bottom <= screenSize.height && buttonRect.top >= 0) {
|
||||
await commonTestCases.tapItemByKey('send_page_add_contact_bottom_sheet_yes_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
} else {
|
||||
tester.printToConsole('Contact dialog button is off-screen, skipping');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for the main success dialog
|
||||
final successDialog = find.byKey(ValueKey('send_page_sent_dialog_ok_button_key'));
|
||||
if (successDialog.tryEvaluate()) {
|
||||
tester.printToConsole('Found transaction success dialog, closing it');
|
||||
await commonTestCases.tapItemByKey('send_page_sent_dialog_ok_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
}
|
||||
|
||||
//* ---- Fiat/Crypto Amount Validation -----
|
||||
Future<void> testFiatAmountEntry() async {
|
||||
tester.printToConsole('Testing fiat amount entry...');
|
||||
|
||||
await enterSendAmount('');
|
||||
await enterSendAmount('', isFiat: true);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await enterSendAmount(CommonTestConstants.sendTestFiatAmount, isFiat: true);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
// Wait for conversion to complete
|
||||
await tester.pump(Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Next we get the crypto amount value and validate it's not 0
|
||||
final cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
|
||||
|
||||
tester.printToConsole(
|
||||
'Crypto amount after entering \$${CommonTestConstants.sendTestFiatAmount}: $cryptoAmount',
|
||||
);
|
||||
|
||||
if (cryptoAmount.isNotEmpty && cryptoAmount != '0' && cryptoAmount != '0.0') {
|
||||
tester.printToConsole('Fiat to crypto conversion working - crypto amount: $cryptoAmount');
|
||||
} else {
|
||||
tester.printToConsole(
|
||||
'Fiat to crypto conversion may not be working - crypto amount: $cryptoAmount',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> testCryptoAmountEntry() async {
|
||||
tester.printToConsole('Testing crypto amount entry...');
|
||||
|
||||
await enterSendAmount('');
|
||||
await enterSendAmount('', isFiat: true);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
String cryptoAmount = '0.001';
|
||||
|
||||
await enterSendAmount(cryptoAmount);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
// Wait for conversion to complete
|
||||
await tester.pump(Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Get the fiat amount value and validate it's not 0
|
||||
final fiatAmount = _getTextFromField(ValueKey('send_page_fiat_amount_textfield_key'));
|
||||
|
||||
tester.printToConsole('Fiat amount after entering $cryptoAmount: $fiatAmount');
|
||||
|
||||
if (fiatAmount.isNotEmpty && fiatAmount != '0' && fiatAmount != '0.0') {
|
||||
tester.printToConsole('Crypto to fiat conversion working, fiat amount: $fiatAmount');
|
||||
} else {
|
||||
tester.printToConsole(
|
||||
'Crypto to fiat conversion may not be working - fiat amount: $fiatAmount');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setupOneDollarSend() async {
|
||||
// Clear existing amounts
|
||||
await enterSendAmount('');
|
||||
await enterSendAmount('', isFiat: true);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
await enterSendAmount(CommonTestConstants.sendTestFiatAmount, isFiat: true);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
// Wait for conversion to complete
|
||||
await tester.pump(Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Get the converted crypto amount
|
||||
String cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
|
||||
if (cryptoAmount.isEmpty || cryptoAmount == '0' || cryptoAmount == '0.0') {
|
||||
cryptoAmount = '0.001'; // fallback amount
|
||||
}
|
||||
|
||||
tester.printToConsole(
|
||||
'Sending $cryptoAmount (equivalent to \$${CommonTestConstants.sendTestFiatAmount}) to test wallet',
|
||||
);
|
||||
|
||||
// Update the amount field with the converted value
|
||||
await enterSendAmount(cryptoAmount);
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,16 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import 'pin_code_widget_robot.dart';
|
||||
import "pin_code_widget_robot.dart";
|
||||
|
||||
class SetupPinCodeRobot extends PinCodeWidgetRobot {
|
||||
SetupPinCodeRobot(this.tester)
|
||||
: commonTestCases = CommonTestCases(tester),
|
||||
super(tester);
|
||||
SetupPinCodeRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isSetupPinCodePage() async {
|
||||
await commonTestCases.isSpecificPage<SetupPinCodePage>();
|
||||
await commonTestCases.takeScreenshots('setup_pin_code_page');
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
commonTestCases.hasText(S.current.setup_pin);
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<SetupPinCodePage>();
|
||||
}
|
||||
|
||||
Future<void> tapSuccessButton() async {
|
||||
await commonTestCases.tapItemByKey('setup_pin_code_success_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("setup_pin_code_success_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import "package:cake_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class ShowKeysDisclaimerPageRobot extends BaseRobot {
|
||||
ShowKeysDisclaimerPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<ShowKeysDisclaimerPage>();
|
||||
}
|
||||
|
||||
Future<void> tapShowKeys() async {
|
||||
await tapByKey("show_keys_disclaimer_page_button_key");
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class TransactionSuccessInfoRobot {
|
||||
TransactionSuccessInfoRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isTransactionSuccessInfoPage() async {
|
||||
await commonTestCases.isSpecificPage<TransactionSuccessPage>();
|
||||
await commonTestCases.takeScreenshots('transaction_success_info_page');
|
||||
}
|
||||
|
||||
Future<void> onConfirmButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('transaction_success_info_page_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
|
||||
class TransactionsPageRobot {
|
||||
TransactionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isTransactionsPage() async {
|
||||
await commonTestCases.isSpecificPage<TransactionsPage>();
|
||||
await commonTestCases.takeScreenshots('transactions_page');
|
||||
}
|
||||
|
||||
Future<void> confirmTransactionsPageConstantsDisplayProperly() async {
|
||||
await commonTestCases.defaultSleepTime();
|
||||
|
||||
final transactionsPage = tester.widget<TransactionsPage>(find.byType(TransactionsPage));
|
||||
final dashboardViewModel = transactionsPage.dashboardViewModel;
|
||||
if (dashboardViewModel.status is SyncingSyncStatus) {
|
||||
commonTestCases.hasValueKey('transactions_page_syncing_alert_card_key');
|
||||
commonTestCases.hasText(S.current.syncing_wallet_alert_title);
|
||||
commonTestCases.hasText(S.current.syncing_wallet_alert_content);
|
||||
}
|
||||
|
||||
commonTestCases.hasValueKey('transactions_page_header_row_key');
|
||||
commonTestCases.hasText(S.current.transactions);
|
||||
commonTestCases.hasValueKey('transactions_page_header_row_transaction_filter_button_key');
|
||||
}
|
||||
|
||||
Future<void> confirmTransactionHistoryListDisplaysCorrectly(bool hasTxHistoryWhileSyncing) async {
|
||||
try {
|
||||
final transactionsPage = tester.widget<TransactionsPage>(find.byType(TransactionsPage));
|
||||
final dashboardViewModel = transactionsPage.dashboardViewModel;
|
||||
|
||||
await _waitForListToBeReady();
|
||||
|
||||
if (!hasTxHistoryWhileSyncing) {
|
||||
await _waitForSyncToComplete(dashboardViewModel);
|
||||
} else {
|
||||
await tester.pump(Duration(seconds: 2));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
await _performComprehensiveItemCheck(dashboardViewModel);
|
||||
} catch (e) {
|
||||
tester.printToConsole('Error in transaction history check: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _waitForSyncToComplete(DashboardViewModel dashboardViewModel) async {
|
||||
const maxWaitTime = Duration(minutes: 2);
|
||||
final endTime = DateTime.now().add(maxWaitTime);
|
||||
|
||||
tester.printToConsole('Waiting for wallet to sync...');
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
await tester.pump(Duration(seconds: 2));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
if (dashboardViewModel.status is SyncedSyncStatus) {
|
||||
tester.printToConsole('Wallet synced successfully');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have now have transaction items available
|
||||
if (dashboardViewModel.items.isNotEmpty) {
|
||||
tester.printToConsole('Items available while syncing, proceeding with test');
|
||||
return;
|
||||
}
|
||||
|
||||
tester.printToConsole('Sync status: ${dashboardViewModel.status.runtimeType}');
|
||||
}
|
||||
|
||||
tester.printToConsole('Warning: Wallet did not sync within expected time, proceeding anyway');
|
||||
}
|
||||
|
||||
Future<void> _performComprehensiveItemCheck(DashboardViewModel dashboardViewModel) async {
|
||||
try {
|
||||
await _waitForItemsToLoad(dashboardViewModel);
|
||||
|
||||
final itemsToProcess = dashboardViewModel.items.where((item) {
|
||||
if (item is DateSectionItem) return false;
|
||||
if (item is TransactionListItem) {
|
||||
return !(item.hasTokens && item.assetOfTransaction == null);
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
if (itemsToProcess.isEmpty) {
|
||||
tester.printToConsole('No transaction items to process - checking for placeholder');
|
||||
_verifyPlaceholder();
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a temporary limit to prevent the test from taking too long
|
||||
final maxItemsToProcess = 100;
|
||||
final itemsToCheck = itemsToProcess.take(maxItemsToProcess).toList();
|
||||
|
||||
tester.printToConsole(
|
||||
'Processing ${itemsToCheck.length} items out of ${itemsToProcess.length} total items',
|
||||
);
|
||||
|
||||
await _processVisibleItems(itemsToCheck, dashboardViewModel);
|
||||
|
||||
// Try to scroll and process more items if needed
|
||||
await _processItemsWithScrolling(itemsToCheck, dashboardViewModel);
|
||||
} catch (e) {
|
||||
tester.printToConsole('Error in comprehensive item check: $e');
|
||||
try {
|
||||
_verifyPlaceholder();
|
||||
} catch (placeholderError) {
|
||||
tester.printToConsole('Could not verify placeholder either: $placeholderError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _waitForItemsToLoad(DashboardViewModel dashboardViewModel) async {
|
||||
const maxWaitTime = Duration(seconds: 30);
|
||||
final endTime = DateTime.now().add(maxWaitTime);
|
||||
|
||||
while (DateTime.now().isBefore(endTime)) {
|
||||
if (dashboardViewModel.items.isNotEmpty) {
|
||||
tester.printToConsole('Items loaded: ${dashboardViewModel.items.length}');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if placeholder is shown
|
||||
if (tester.any(find.byKey(ValueKey('transactions_page_placeholder_transactions_text_key')))) {
|
||||
tester.printToConsole('Placeholder is shown - no items to load');
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.pump(Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
tester.printToConsole('Warning: No items loaded and no placeholder shown within expected time');
|
||||
}
|
||||
|
||||
Future<void> _processVisibleItems(
|
||||
List<dynamic> items,
|
||||
DashboardViewModel dashboardViewModel,
|
||||
) async {
|
||||
try {
|
||||
int processedCount = 0;
|
||||
|
||||
for (var item in items) {
|
||||
try {
|
||||
final keyId = (item.key as ValueKey<String>).value;
|
||||
|
||||
// Check if item is already visible
|
||||
if (tester.any(find.byKey(ValueKey(keyId)))) {
|
||||
tester.printToConsole('Processing visible item: $keyId');
|
||||
await _verifyItemDisplay(item, dashboardViewModel);
|
||||
processedCount++;
|
||||
}
|
||||
} catch (itemError) {
|
||||
tester.printToConsole('Error processing item: $itemError');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
tester.printToConsole('Processed $processedCount visible items\n');
|
||||
} catch (e) {
|
||||
tester.printToConsole('Error in visible items processing: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processItemsWithScrolling(
|
||||
List<dynamic> items,
|
||||
DashboardViewModel dashboardViewModel,
|
||||
) async {
|
||||
try {
|
||||
final scrollableFinder = find.descendant(
|
||||
of: find.byKey(ValueKey('transactions_page_list_view_builder_key')),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
|
||||
if (!tester.any(scrollableFinder)) {
|
||||
tester.printToConsole('No scrollable found, skipping scroll processing');
|
||||
return;
|
||||
}
|
||||
|
||||
int processedCount = 0;
|
||||
const maxScrollAttempts = 10;
|
||||
|
||||
for (var item in items) {
|
||||
try {
|
||||
final keyId = (item.key as ValueKey<String>).value;
|
||||
|
||||
// Skip if already processed
|
||||
if (tester.any(find.byKey(ValueKey(keyId)))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to scroll to the item
|
||||
bool found = false;
|
||||
for (int attempt = 0; attempt < maxScrollAttempts; attempt++) {
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 200));
|
||||
|
||||
if (tester.any(find.byKey(ValueKey(keyId)))) {
|
||||
tester.printToConsole('Found item after scrolling: $keyId');
|
||||
await _verifyItemDisplay(item, dashboardViewModel);
|
||||
processedCount++;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Perform scroll
|
||||
try {
|
||||
await tester.drag(scrollableFinder, Offset(0, -100));
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 300));
|
||||
} catch (scrollError) {
|
||||
tester.printToConsole('Scroll failed: $scrollError');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
tester.printToConsole('Could not find item after scrolling: $keyId');
|
||||
}
|
||||
} catch (itemError) {
|
||||
tester.printToConsole('Error processing item with scrolling: $itemError');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
tester.printToConsole('Processed $processedCount items with scrolling');
|
||||
} catch (e) {
|
||||
tester.printToConsole('Error in scroll processing: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _verifyPlaceholder() {
|
||||
commonTestCases.hasValueKey('transactions_page_placeholder_transactions_text_key');
|
||||
commonTestCases.hasText(S.current.placeholder_transactions);
|
||||
}
|
||||
|
||||
Future<void> _waitForListToBeReady() async {
|
||||
const maxWaitAttempts = 10;
|
||||
int attempts = 0;
|
||||
|
||||
while (attempts < maxWaitAttempts) {
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 500));
|
||||
|
||||
// Check if the list view is present
|
||||
final listViewFinder = find.byKey(ValueKey('transactions_page_list_view_builder_key'));
|
||||
if (tester.any(listViewFinder)) {
|
||||
tester.printToConsole('List view is ready');
|
||||
return;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
tester.printToConsole('Waiting for list view to be ready, attempt $attempts\n');
|
||||
}
|
||||
|
||||
tester.printToConsole('List view did not become ready within expected time');
|
||||
}
|
||||
|
||||
Future<void> _verifyItemDisplay(dynamic item, DashboardViewModel dashboardViewModel) async {
|
||||
// Execute the proper check depending on item type.
|
||||
switch (item.runtimeType) {
|
||||
case TransactionListItem:
|
||||
final transactionItem = item as TransactionListItem;
|
||||
tester.printToConsole(transactionItem.formattedTitle);
|
||||
tester.printToConsole(transactionItem.formattedFiatAmount);
|
||||
tester.printToConsole('\n');
|
||||
await _verifyTransactionListItemDisplay(transactionItem, dashboardViewModel);
|
||||
break;
|
||||
|
||||
case AnonpayTransactionListItem:
|
||||
await _verifyAnonpayTransactionListItemDisplay(item as AnonpayTransactionListItem);
|
||||
break;
|
||||
|
||||
case TradeListItem:
|
||||
await _verifyTradeListItemDisplay(item as TradeListItem);
|
||||
break;
|
||||
|
||||
case OrderListItem:
|
||||
await _verifyOrderListItemDisplay(item as OrderListItem);
|
||||
break;
|
||||
|
||||
default:
|
||||
tester.printToConsole('Unhandled item type: ${item.runtimeType}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyTransactionListItemDisplay(
|
||||
TransactionListItem item,
|
||||
DashboardViewModel dashboardViewModel,
|
||||
) async {
|
||||
final keyId =
|
||||
'${dashboardViewModel.type.name}_transaction_history_item_${item.transaction.id}_key';
|
||||
|
||||
if (!tester.any(find.byKey(ValueKey(keyId)))) {
|
||||
tester.printToConsole(
|
||||
'Could not find transaction item with key: $keyId for transaction ${item.transaction.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.hasTokens && item.assetOfTransaction == null) return;
|
||||
|
||||
try {
|
||||
//* ==============Confirm it has the right key for this item ========
|
||||
commonTestCases.hasValueKey(keyId);
|
||||
|
||||
//* ======Confirm it displays the properly formatted amount==========
|
||||
commonTestCases.findWidgetViaDescendant(
|
||||
of: find.byKey(ValueKey(keyId)),
|
||||
matching: find.text(item.formattedCryptoAmount),
|
||||
);
|
||||
|
||||
//* ======Confirm it displays the properly formatted date============
|
||||
final formattedDate = DateFormat('HH:mm').format(item.transaction.date);
|
||||
commonTestCases.findWidgetViaDescendant(
|
||||
of: find.byKey(ValueKey(keyId)),
|
||||
matching: find.text(formattedDate),
|
||||
);
|
||||
|
||||
//* ======Confirm it displays the properly formatted fiat amount=====
|
||||
final formattedFiatAmount =
|
||||
dashboardViewModel.balanceViewModel.isFiatDisabled ? '' : item.formattedFiatAmount;
|
||||
if (formattedFiatAmount.isNotEmpty) {
|
||||
commonTestCases.findWidgetViaDescendant(
|
||||
of: find.byKey(ValueKey(keyId)),
|
||||
matching: find.text(formattedFiatAmount),
|
||||
);
|
||||
}
|
||||
|
||||
//* ======Confirm it displays the right image based on the transaction direction=====
|
||||
final imageToUse = item.transaction.direction == TransactionDirection.incoming
|
||||
? 'assets/images/down_arrow.png'
|
||||
: 'assets/images/up_arrow.png';
|
||||
|
||||
find.widgetWithImage(Container, AssetImage(imageToUse));
|
||||
} catch (e) {
|
||||
tester.printToConsole('Error verifying transaction item ${item.transaction.id}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyAnonpayTransactionListItemDisplay(AnonpayTransactionListItem item) async {
|
||||
final keyId = 'anonpay_invoice_transaction_list_item_${item.transaction.invoiceId}_key';
|
||||
|
||||
//* ==============Confirm it has the right key for this item ========
|
||||
commonTestCases.hasValueKey(keyId);
|
||||
|
||||
//* ==============Confirm it displays the correct provider =========================
|
||||
commonTestCases.hasText(item.transaction.provider);
|
||||
|
||||
//* ===========Confirm it displays the properly formatted amount with currency ========
|
||||
final currency = item.transaction.fiatAmount != null
|
||||
? item.transaction.fiatEquiv ?? ''
|
||||
: CryptoCurrency.fromFullName(item.transaction.coinTo).name.toUpperCase();
|
||||
|
||||
final amount =
|
||||
item.transaction.fiatAmount?.toString() ?? (item.transaction.amountTo?.toString() ?? '');
|
||||
|
||||
final amountCurrencyText = amount + ' ' + currency;
|
||||
|
||||
commonTestCases.hasText(amountCurrencyText);
|
||||
|
||||
//* ======Confirm it displays the properly formatted date=================
|
||||
final formattedDate = DateFormat('HH:mm').format(item.transaction.createdAt);
|
||||
commonTestCases.hasText(formattedDate);
|
||||
|
||||
//* ===============Confirm it displays the right image====================
|
||||
find.widgetWithImage(ClipRRect, AssetImage('assets/images/trocador.png'));
|
||||
}
|
||||
|
||||
Future<void> _verifyTradeListItemDisplay(TradeListItem item) async {
|
||||
final keyId = 'trade_list_item_${item.trade.id}_key';
|
||||
final from = item.trade.from?.toString() ?? '';
|
||||
final to = item.trade.to?.toString() ?? '';
|
||||
|
||||
//* ==============Confirm it has the right key for this item ========
|
||||
commonTestCases.hasValueKey(keyId);
|
||||
|
||||
//* ==============Confirm it displays the correct provider =========================
|
||||
final conversionFlow = '$from → $to';
|
||||
|
||||
commonTestCases.hasText(conversionFlow);
|
||||
|
||||
//* ===========Confirm it displays the properly formatted amount with its crypto tag ========
|
||||
|
||||
final amountCryptoText = item.tradeFormattedAmount + ' ' + from;
|
||||
|
||||
commonTestCases.hasText(amountCryptoText);
|
||||
|
||||
//* ======Confirm it displays the properly formatted date=================
|
||||
final createdAtFormattedDate =
|
||||
item.trade.createdAt != null ? DateFormat('HH:mm').format(item.trade.createdAt!) : null;
|
||||
|
||||
if (createdAtFormattedDate != null) {
|
||||
commonTestCases.hasText(createdAtFormattedDate);
|
||||
}
|
||||
|
||||
//* ===============Confirm it displays the right image====================
|
||||
commonTestCases.hasValueKey(item.trade.provider.image);
|
||||
}
|
||||
|
||||
Future<void> _verifyOrderListItemDisplay(OrderListItem item) async {
|
||||
final keyId = 'order_list_item_${item.order.id}_key';
|
||||
|
||||
//* ==============Confirm it has the right key for this item ========
|
||||
commonTestCases.hasValueKey(keyId);
|
||||
|
||||
//* ==============Confirm it displays the correct provider =========================
|
||||
final orderFlow = '${item.order.from!} → ${item.order.to}';
|
||||
|
||||
commonTestCases.hasText(orderFlow);
|
||||
|
||||
//* ===========Confirm it displays the properly formatted amount with its crypto tag ========
|
||||
|
||||
final amountCryptoText = item.orderFormattedAmount + ' ' + item.order.to!;
|
||||
|
||||
commonTestCases.hasText(amountCryptoText);
|
||||
|
||||
//* ======Confirm it displays the properly formatted date=================
|
||||
final createdAtFormattedDate = DateFormat('HH:mm').format(item.order.createdAt);
|
||||
|
||||
commonTestCases.hasText(createdAtFormattedDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import "package:cake_wallet/src/screens/wallet/wallet_edit_page.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WalletEditPageRobot extends BaseRobot {
|
||||
WalletEditPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WalletEditPage>();
|
||||
}
|
||||
|
||||
Future<void> enterName(String name) async {
|
||||
final input = find.byKey(const ValueKey("wallet_edit_page_name_input_key"));
|
||||
|
||||
await pumpUntilFound(input);
|
||||
|
||||
final field = find.descendant(of: input, matching: find.byType(EditableText));
|
||||
|
||||
await tester.enterText(field.first, name);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
await tapByKey("wallet_edit_page_save_button_key");
|
||||
}
|
||||
|
||||
Future<void> tapDelete() async {
|
||||
await tapByKey("wallet_edit_page_delete_button_key");
|
||||
}
|
||||
|
||||
Future<void> confirmDelete() async {
|
||||
await tapByKey("wallet_edit_page_confirm_delete_button_key");
|
||||
}
|
||||
|
||||
Future<void> expectNameTakenRefusal() async {
|
||||
await pumpUntilFound(find.byKey(const ValueKey("wallet_edit_page_name_taken_ok_button_key")));
|
||||
|
||||
hasType<WalletEditPage>();
|
||||
}
|
||||
|
||||
Future<void> dismissNameTakenRefusal() async {
|
||||
await tapByKey("wallet_edit_page_name_taken_ok_button_key");
|
||||
|
||||
await settle();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,25 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WalletGroupDescriptionPageRobot {
|
||||
WalletGroupDescriptionPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class WalletGroupDescriptionPageRobot extends BaseRobot {
|
||||
WalletGroupDescriptionPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isWalletGroupDescriptionPage() async {
|
||||
await commonTestCases.isSpecificPage<WalletGroupDescriptionPage>();
|
||||
await commonTestCases.takeScreenshots('wallet_group_description_page');
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WalletGroupDescriptionPage>();
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
commonTestCases.hasText(S.current.wallet_group);
|
||||
}
|
||||
|
||||
bool hasNewSingleSeedButton() {
|
||||
return commonTestCases.isKeyPresent('wallet_group_description_page_create_new_seed_button_key');
|
||||
}
|
||||
bool hasNewSingleSeedButton() =>
|
||||
isKeyPresent("wallet_group_description_page_create_new_seed_button_key");
|
||||
|
||||
Future<void> navigateToCreateNewSeedPage() async {
|
||||
if (hasNewSingleSeedButton()) {
|
||||
await commonTestCases.tapItemByKey(
|
||||
'wallet_group_description_page_create_new_seed_button_key',
|
||||
);
|
||||
await tapByKey("wallet_group_description_page_create_new_seed_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> navigateToChooseWalletGroup() async {
|
||||
await commonTestCases.tapItemByKey(
|
||||
'wallet_group_description_page_choose_wallet_group_button_key',
|
||||
);
|
||||
await tapByKey("wallet_group_description_page_choose_wallet_group_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import "package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WalletGroupExistingSeedPageRobot extends BaseRobot {
|
||||
WalletGroupExistingSeedPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WalletGroupExistingSeedDescriptionPage>();
|
||||
}
|
||||
|
||||
Future<void> openWallet() async {
|
||||
await tapByKey("wallet_group_existing_seed_description_page_open_wallet_button_key");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import "package:cake_wallet/src/screens/new_wallet/wallet_group_display_page.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WalletGroupsDisplayPageRobot extends BaseRobot {
|
||||
WalletGroupsDisplayPageRobot(super.tester);
|
||||
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WalletGroupsDisplayPage>();
|
||||
}
|
||||
|
||||
Future<void> selectWallet(String name) async {
|
||||
await tapWhenVisible(find.text(name));
|
||||
}
|
||||
|
||||
Future<void> continueWithSelectedSeed() async {
|
||||
await tapByKey("wallet_group_display_page_next_button_key");
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,38 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cw_core/monero_wallet_keys.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_monero/monero_wallet.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:polyseed/polyseed.dart';
|
||||
import "package:cake_wallet/reactions/wallet_connect.dart";
|
||||
import "package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/monero_wallet_keys.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:cw_monero/monero_wallet.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:polyseed/polyseed.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WalletKeysAndSeedPageRobot {
|
||||
WalletKeysAndSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class WalletKeysAndSeedPageRobot extends BaseRobot {
|
||||
WalletKeysAndSeedPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
final CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isWalletKeysAndSeedPage() async {
|
||||
await commonTestCases.isSpecificPage<WalletKeysPage>();
|
||||
await commonTestCases.takeScreenshots('wallet_keys_page');
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WalletKeysPage>();
|
||||
}
|
||||
|
||||
void hasTitle() {
|
||||
final walletKeysPage = tester.widget<WalletKeysPage>(find.byType(WalletKeysPage));
|
||||
final walletKeysViewModel = walletKeysPage.walletKeysViewModel;
|
||||
commonTestCases.hasText(walletKeysViewModel.title);
|
||||
}
|
||||
|
||||
void hasShareWarning() {
|
||||
commonTestCases.hasText(S.current.do_not_share_warning_text.toUpperCase());
|
||||
}
|
||||
int _verifiedCredentials = 0;
|
||||
|
||||
Future<void> confirmWalletCredentials(WalletType walletType) async {
|
||||
_verifiedCredentials = 0;
|
||||
|
||||
final walletKeysPage = tester.widget<WalletKeysPage>(find.byType(WalletKeysPage));
|
||||
final walletKeysViewModel = walletKeysPage.walletKeysViewModel;
|
||||
|
||||
final appStore = walletKeysViewModel.appStore;
|
||||
final walletName = walletType.name;
|
||||
bool hasSeed = appStore.wallet!.seed != null;
|
||||
bool hasHexSeed = appStore.wallet!.hexSeed != null;
|
||||
bool hasPrivateKey = appStore.wallet!.privateKey != null;
|
||||
final hasSeed = appStore.wallet!.seed != null;
|
||||
final hasHexSeed = appStore.wallet!.hexSeed != null;
|
||||
final hasPrivateKey = appStore.wallet!.privateKey != null;
|
||||
|
||||
if (walletType == WalletType.monero) {
|
||||
final moneroWallet = appStore.wallet as MoneroWalletBase;
|
||||
final moneroWallet = appStore.wallet! as MoneroWalletBase;
|
||||
final lang = PolyseedLang.getByPhrase(moneroWallet.seed);
|
||||
final legacySeed = moneroWallet.seedLegacy(lang.nameEnglish);
|
||||
|
||||
@@ -57,51 +47,89 @@ class WalletKeysAndSeedPageRobot {
|
||||
if (walletType == WalletType.bitcoin ||
|
||||
walletType == WalletType.litecoin ||
|
||||
walletType == WalletType.bitcoinCash) {
|
||||
final seedWords = appStore.wallet!.seed!.split(" ");
|
||||
for (var seedWord in seedWords) {
|
||||
commonTestCases.hasTextAtLestOnce(seedWord);
|
||||
}
|
||||
tester.printToConsole('$walletName wallet has seeds properly displayed');
|
||||
_showsSeedWords(appStore.wallet!.seed!);
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has seeds properly displayed");
|
||||
}
|
||||
|
||||
if (isEVMCompatibleChain(walletType) ||
|
||||
walletType == WalletType.solana ||
|
||||
walletType == WalletType.tron) {
|
||||
if (hasSeed) {
|
||||
final seedWords = appStore.wallet!.seed!.split(" ");
|
||||
for (var seedWord in seedWords) {
|
||||
commonTestCases.hasTextAtLestOnce(seedWord);
|
||||
}
|
||||
tester.printToConsole('$walletName wallet has seeds properly displayed');
|
||||
_showsSeedWords(appStore.wallet!.seed!);
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has seeds properly displayed");
|
||||
}
|
||||
if (hasPrivateKey) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
|
||||
commonTestCases.hasText(appStore.wallet!.privateKey!);
|
||||
tester.printToConsole('$walletName wallet has private key properly displayed');
|
||||
await _openKeysTab();
|
||||
_showsCredentialOnce(appStore.wallet!.privateKey!, "private key");
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has private key properly displayed");
|
||||
}
|
||||
}
|
||||
|
||||
if (walletType == WalletType.nano || walletType == WalletType.banano) {
|
||||
if (hasSeed) {
|
||||
final seedWords = appStore.wallet!.seed!.split(" ");
|
||||
for (var seedWord in seedWords) {
|
||||
commonTestCases.hasTextAtLestOnce(seedWord);
|
||||
}
|
||||
tester.printToConsole('$walletName wallet has seeds properly displayed');
|
||||
_showsSeedWords(appStore.wallet!.seed!);
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has seeds properly displayed");
|
||||
}
|
||||
if (hasHexSeed) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
|
||||
commonTestCases.hasText(appStore.wallet!.hexSeed!);
|
||||
tester.printToConsole('$walletName wallet has hexSeed properly displayed');
|
||||
await _openKeysTab();
|
||||
_showsCredentialOnce(appStore.wallet!.hexSeed!, "hex seed");
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has hexSeed properly displayed");
|
||||
}
|
||||
if (hasPrivateKey) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
|
||||
commonTestCases.hasText(appStore.wallet!.privateKey!);
|
||||
tester.printToConsole('$walletName wallet has private key properly displayed');
|
||||
await _openKeysTab();
|
||||
_showsCredentialOnce(appStore.wallet!.privateKey!, "private key");
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has private key properly displayed");
|
||||
}
|
||||
}
|
||||
|
||||
await commonTestCases.defaultSleepTime(seconds: 5);
|
||||
_expectSomethingWasVerified(walletType);
|
||||
}
|
||||
|
||||
void _expectSomethingWasVerified(WalletType walletType) {
|
||||
expect(
|
||||
_verifiedCredentials,
|
||||
greaterThan(0),
|
||||
reason: "Nothing was verified for ${walletType.name}, this suite covers no credential "
|
||||
"for that type so it cannot tell a working keys page from a broken one",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openKeysTab() async {
|
||||
await tapByKey("wallet_keys_page_keys");
|
||||
|
||||
await settle();
|
||||
}
|
||||
|
||||
void _showsCredential(String value, String label) {
|
||||
expect(
|
||||
tester.any(find.text(value, skipOffstage: false)),
|
||||
true,
|
||||
reason: "The keys page did not show the wallet's $label",
|
||||
);
|
||||
}
|
||||
|
||||
void _showsCredentialOnce(String value, String label) {
|
||||
final rows = tester.widgetList(find.text(value, skipOffstage: false)).length;
|
||||
|
||||
expect(
|
||||
rows,
|
||||
1,
|
||||
reason: "The keys page showed $rows rows for the wallet's $label, expected one",
|
||||
);
|
||||
}
|
||||
|
||||
void _showsSeedWords(String seed) {
|
||||
final words = seed.split(" ");
|
||||
|
||||
for (var index = 0; index < words.length; index++) {
|
||||
_showsCredential(words[index], "seed word ${index + 1}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmMoneroWalletCredentials(
|
||||
@@ -111,68 +139,42 @@ class WalletKeysAndSeedPageRobot {
|
||||
String legacySeed,
|
||||
) async {
|
||||
final keys = appStore.wallet!.keys as MoneroWalletKeys;
|
||||
|
||||
final hasPublicSpendKey = commonTestCases.isKeyPresent(
|
||||
'${walletName}_wallet_public_spend_key_item_key',
|
||||
);
|
||||
final hasPrivateSpendKey = commonTestCases.isKeyPresent(
|
||||
'${walletName}_wallet_private_spend_key_item_key',
|
||||
);
|
||||
final hasPublicViewKey = commonTestCases.isKeyPresent(
|
||||
'${walletName}_wallet_public_view_key_item_key',
|
||||
);
|
||||
final hasPrivateViewKey = commonTestCases.isKeyPresent(
|
||||
'${walletName}_wallet_private_view_key_item_key',
|
||||
);
|
||||
final hasSeeds = seed.isNotEmpty;
|
||||
final hasSeedLegacy = Polyseed.isValidSeed(seed);
|
||||
|
||||
if (hasPublicSpendKey) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
|
||||
commonTestCases.hasText(keys.publicSpendKey);
|
||||
tester.printToConsole('$walletName wallet has public spend key properly displayed');
|
||||
}
|
||||
if (hasPrivateSpendKey) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
|
||||
commonTestCases.hasText(keys.privateSpendKey);
|
||||
tester.printToConsole('$walletName wallet has private spend key properly displayed');
|
||||
}
|
||||
if (hasPublicViewKey) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
|
||||
commonTestCases.hasText(keys.publicViewKey);
|
||||
tester.printToConsole('$walletName wallet has public view key properly displayed');
|
||||
}
|
||||
if (hasPrivateViewKey) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
|
||||
commonTestCases.hasText(keys.privateViewKey);
|
||||
tester.printToConsole('$walletName wallet has private view key properly displayed');
|
||||
}
|
||||
await _openKeysTab();
|
||||
|
||||
_showsCredentialOnce(keys.publicSpendKey, "public spend key");
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has public spend key properly displayed");
|
||||
|
||||
_showsCredentialOnce(keys.privateSpendKey, "private spend key");
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has private spend key properly displayed");
|
||||
|
||||
_showsCredentialOnce(keys.publicViewKey, "public view key");
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has public view key properly displayed");
|
||||
|
||||
_showsCredentialOnce(keys.privateViewKey, "private view key");
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has private view key properly displayed");
|
||||
|
||||
if (hasSeeds) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_seed');
|
||||
final seedWords = seed.split(" ");
|
||||
for (var seedWord in seedWords) {
|
||||
commonTestCases.hasTextAtLestOnce(seedWord);
|
||||
}
|
||||
tester.printToConsole('$walletName wallet has seeds properly displayed');
|
||||
await tapByKey("wallet_keys_page_seed");
|
||||
await settle();
|
||||
|
||||
_showsSeedWords(seed);
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has seeds properly displayed");
|
||||
}
|
||||
if (hasSeedLegacy) {
|
||||
await commonTestCases.tapItemByKey('wallet_keys_page_seed_legacy');
|
||||
final seedWords = legacySeed.split(" ");
|
||||
for (var seedWord in seedWords) {
|
||||
commonTestCases.hasTextAtLestOnce(seedWord);
|
||||
}
|
||||
tester.printToConsole('$walletName wallet has legacy seeds properly displayed');
|
||||
await tapByKey("wallet_keys_page_seed_legacy");
|
||||
await settle();
|
||||
|
||||
_showsSeedWords(legacySeed);
|
||||
_verifiedCredentials++;
|
||||
tester.printToConsole("$walletName wallet has legacy seeds properly displayed");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> backToDashboard() async {
|
||||
tester.printToConsole('Going back to dashboard from credentials page');
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
await commonTestCases.goBack();
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
await commonTestCases.goBack();
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 2000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,48 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WalletListPageRobot {
|
||||
WalletListPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class WalletListPageRobot extends BaseRobot {
|
||||
WalletListPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
final shown = await pumpUntil(_isShowing);
|
||||
|
||||
Future<void> isWalletListPage() async {
|
||||
await commonTestCases.isSpecificPage<WalletListPageRobot>();
|
||||
await commonTestCases.takeScreenshots('wallet_list_page');
|
||||
expect(shown, true, reason: "The wallets tab never came to the front");
|
||||
}
|
||||
|
||||
void displaysCorrectTitle() {
|
||||
commonTestCases.hasText(S.current.wallets);
|
||||
Future<void> openEditFor(String walletName) async {
|
||||
await tapByKey("wallet_list_edit_${walletName}_button_key");
|
||||
}
|
||||
|
||||
bool hasWallet(String walletName) => tester.any(
|
||||
find.descendant(
|
||||
of: find.byType(WalletListPage),
|
||||
matching: find.text(walletName),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> navigateToCreateNewWalletPage() async {
|
||||
await commonTestCases.tapItemByKey('wallet_list_page_create_new_wallet_button_key');
|
||||
await tapByKey("wallet_list_page_create_new_wallet_button_key");
|
||||
}
|
||||
|
||||
Future<void> navigateToRestoreWalletOptionsPage() async {
|
||||
await commonTestCases.tapItemByKey('wallet_list_page_restore_wallet_button_key');
|
||||
await tapByKey("wallet_list_page_restore_wallet_button_key");
|
||||
}
|
||||
|
||||
bool _isShowing() {
|
||||
final stackFinder = find.byType(IndexedStack);
|
||||
|
||||
if (!tester.any(stackFinder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final stack = tester.widget<IndexedStack>(stackFinder.first);
|
||||
final index = stack.children.indexWhere((child) => child is WalletListPage);
|
||||
|
||||
return index >= 0 && stack.index == index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/src/screens/seed/wallet_seed_page.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WalletSeedPageRobot {
|
||||
WalletSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class WalletSeedPageRobot extends BaseRobot {
|
||||
WalletSeedPageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isWalletSeedPage() async {
|
||||
await commonTestCases.isSpecificPage<WalletSeedPage>();
|
||||
await commonTestCases.takeScreenshots('wallet_seed_page');
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WalletSeedPage>();
|
||||
}
|
||||
|
||||
Future<void> onVerifySeedButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_seed_page_verify_seed_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onSaveSeedButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onConfirmButtonOnSeedAlertDialogPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_confirm_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onBackButtonOnSeedAlertDialogPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_back_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("wallet_seed_page_verify_seed_button_key");
|
||||
}
|
||||
|
||||
void confirmWalletDetailsDisplayCorrectly() {
|
||||
@@ -42,23 +23,18 @@ class WalletSeedPageRobot {
|
||||
|
||||
final walletName = walletSeedViewModel.name;
|
||||
final walletSeeds = walletSeedViewModel.seedSplit;
|
||||
commonTestCases.hasText(walletName);
|
||||
for (var seed in walletSeeds) {
|
||||
commonTestCases.hasTextAtLestOnce(seed);
|
||||
hasText(walletName);
|
||||
|
||||
for (var index = 0; index < walletSeeds.length; index++) {
|
||||
expect(
|
||||
tester.any(find.text(walletSeeds[index])),
|
||||
true,
|
||||
reason: "The seed page did not show seed word ${index + 1}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void confirmWalletSeedReminderDisplays() {
|
||||
commonTestCases.hasText(S.current.cake_seeds_save_disclaimer);
|
||||
}
|
||||
|
||||
Future<void> onSaveSeedsButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> onCopySeedsButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_seed_page_copy_seeds_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
hasText(S.current.cake_seeds_save_disclaimer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,28 @@
|
||||
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:cake_wallet/src/screens/welcome/welcome_page.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import '../components/common_test_cases.dart';
|
||||
import "../core/base_robot.dart";
|
||||
|
||||
class WelcomePageRobot {
|
||||
WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
|
||||
class WelcomePageRobot extends BaseRobot {
|
||||
WelcomePageRobot(super.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
late CommonTestCases commonTestCases;
|
||||
|
||||
Future<void> isWelcomePage() async {
|
||||
await commonTestCases.isSpecificPage<WelcomePage>();
|
||||
await commonTestCases.takeScreenshots('welcome_page');
|
||||
}
|
||||
|
||||
void confirmActionButtonsDisplay() {
|
||||
final createNewWalletButton = find.byKey(ValueKey('welcome_page_create_new_wallet_button_key'));
|
||||
|
||||
final restoreWalletButton = find.byKey(ValueKey('welcome_page_restore_wallet_button_key'));
|
||||
|
||||
expect(createNewWalletButton, findsOneWidget);
|
||||
expect(restoreWalletButton, findsOneWidget);
|
||||
@override
|
||||
Future<void> isDisplayed() async {
|
||||
await isSpecificPage<WelcomePage>();
|
||||
}
|
||||
|
||||
Future<void> navigateToCreateNewWalletPage() async {
|
||||
await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("welcome_page_create_new_wallet_button_key");
|
||||
}
|
||||
|
||||
bool hasNewSingleSeedButton() {
|
||||
return commonTestCases.isKeyPresent('wallet_group_description_page_create_new_seed_button_key');
|
||||
}
|
||||
bool hasNewSingleSeedButton() =>
|
||||
isKeyPresent("wallet_group_description_page_create_new_seed_button_key");
|
||||
|
||||
Future<void> tapNewSingleSeed() async {
|
||||
await commonTestCases.tapItemByKey('wallet_group_description_page_create_new_seed_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
await tapByKey("wallet_group_description_page_create_new_seed_button_key");
|
||||
}
|
||||
|
||||
Future<void> navigateToRestoreWalletPage() async {
|
||||
await commonTestCases.tapItemByKey('welcome_page_restore_wallet_button_key');
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
|
||||
Future<void> backAndVerify() async {
|
||||
await commonTestCases.goBack();
|
||||
await isWelcomePage();
|
||||
await tapByKey("welcome_page_restore_wallet_button_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_wallets.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/contact_robot.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("A saved contact keeps the address it was given", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final contactRobot = ContactRobot(tester);
|
||||
|
||||
const contactName = "Integration Test Contact";
|
||||
final contactAddress = TestWallets.receiveAddressFor(WalletType.solana);
|
||||
|
||||
await appLauncher.launchApp(testKey: "address_book_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await dashboardRobot.openContactsTab();
|
||||
await contactRobot.isDisplayed();
|
||||
|
||||
await contactRobot.openAddContact();
|
||||
|
||||
await contactRobot.enterName(contactName);
|
||||
await contactRobot.chooseCurrency("sol");
|
||||
await contactRobot.enterAddress(contactAddress);
|
||||
|
||||
await contactRobot.save();
|
||||
|
||||
await contactRobot.isDisplayed();
|
||||
|
||||
contactRobot.expectContactSaved(contactName, contactAddress);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_config.dart";
|
||||
import "../../flows/auth_flows.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_settings_page_robot.dart";
|
||||
import "../../robots/security_backup_page_robot.dart";
|
||||
import "../../robots/setup_pin_code_robot.dart";
|
||||
import "../../robots/show_keys_disclaimer_page_robot.dart";
|
||||
import "../../robots/wallet_keys_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Changing the pin replaces the one that unlocks the wallet", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final authFlows = AuthFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final settingsRobot = NewSettingsPageRobot(tester);
|
||||
final securityRobot = SecurityBackupPageRobot(tester);
|
||||
final setupPinRobot = SetupPinCodeRobot(tester);
|
||||
final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
|
||||
final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
|
||||
|
||||
final newPin = [1, 2, 3, 4];
|
||||
|
||||
expect(newPin, isNot(TestConfig.pin), reason: "The new pin has to differ from the old one");
|
||||
|
||||
await appLauncher.launchApp(testKey: "change_pin_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSettingsSheet();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.securityBackupPage);
|
||||
await securityRobot.isDisplayed();
|
||||
|
||||
await securityRobot.openChangePin();
|
||||
await authFlows.authenticateWithPin();
|
||||
|
||||
await setupPinRobot.isDisplayed();
|
||||
await setupPinRobot.enterPinCode(newPin);
|
||||
await setupPinRobot.enterPinCode(newPin);
|
||||
await setupPinRobot.tapSuccessButton();
|
||||
|
||||
await securityRobot.isDisplayed();
|
||||
|
||||
await securityRobot.goBack();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.showKeysDisclaimer);
|
||||
await disclaimerRobot.isDisplayed();
|
||||
await disclaimerRobot.tapShowKeys();
|
||||
await authFlows.authenticateWithPin(pin: newPin);
|
||||
|
||||
await walletKeysRobot.isDisplayed();
|
||||
|
||||
expect(find.byType(WalletKeysPage), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_wallet_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("A wallet name already in use is refused", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final newWalletPageRobot = NewWalletPageRobot(tester);
|
||||
|
||||
const walletType = WalletType.solana;
|
||||
|
||||
await appLauncher.launchApp(testKey: "duplicate_wallet_name_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(walletType);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final existingName = getIt.get<AppStore>().wallet!.name;
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
|
||||
await onboardingFlows.startCreatingWalletFromWalletList(walletType);
|
||||
|
||||
await newWalletPageRobot.enterWalletName(existingName);
|
||||
await newWalletPageRobot.onNextButtonPressed();
|
||||
|
||||
await newWalletPageRobot.expectNameRejected();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/entities/fiat_api_mode.dart";
|
||||
import "package:cake_wallet/entities/fiat_currency.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/connection_sync_page_robot.dart";
|
||||
import "../../robots/display_settings_page_robot.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_settings_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Turning the fiat api off hides the currency setting, turning it on returns it",
|
||||
(tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final settingsRobot = NewSettingsPageRobot(tester);
|
||||
final connectionRobot = ConnectionSyncPageRobot(tester);
|
||||
final displayRobot = DisplaySettingsPageRobot(tester);
|
||||
|
||||
const wanted = FiatCurrency.eur;
|
||||
|
||||
await appLauncher.launchApp(testKey: "fiat_currency_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final settingsStore = getIt.get<SettingsStore>();
|
||||
|
||||
expect(
|
||||
settingsStore.fiatCurrency,
|
||||
isNot(wanted),
|
||||
reason: "The wallet already starts on $wanted, so changing to it would prove nothing",
|
||||
);
|
||||
|
||||
await homePageRobot.openSettingsSheet();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
if (settingsStore.fiatApiMode != FiatApiMode.disabled) {
|
||||
await settingsRobot.openRow(Routes.connectionSync);
|
||||
await connectionRobot.isDisplayed();
|
||||
|
||||
await connectionRobot.setFiatApiMode(FiatApiMode.disabled);
|
||||
connectionRobot.hasFiatApiMode(FiatApiMode.disabled);
|
||||
|
||||
expect(settingsStore.fiatApiMode, FiatApiMode.disabled);
|
||||
|
||||
await connectionRobot.goBack();
|
||||
await settingsRobot.isDisplayed();
|
||||
}
|
||||
|
||||
await settingsRobot.openRow(Routes.displaySettingsPage);
|
||||
await displayRobot.isDisplayed();
|
||||
|
||||
expect(
|
||||
displayRobot.hasFiatCurrencyRow(),
|
||||
false,
|
||||
reason: "The currency setting is still showing while the fiat api is off",
|
||||
);
|
||||
|
||||
await displayRobot.goBack();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.connectionSync);
|
||||
await connectionRobot.isDisplayed();
|
||||
|
||||
await connectionRobot.setFiatApiMode(FiatApiMode.enabled);
|
||||
connectionRobot.hasFiatApiMode(FiatApiMode.enabled);
|
||||
|
||||
expect(settingsStore.fiatApiMode, FiatApiMode.enabled);
|
||||
|
||||
await connectionRobot.goBack();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.displaySettingsPage);
|
||||
await displayRobot.isDisplayed();
|
||||
|
||||
expect(
|
||||
displayRobot.hasFiatCurrencyRow(),
|
||||
true,
|
||||
reason: "The currency setting did not come back after the fiat api was turned on",
|
||||
);
|
||||
|
||||
await displayRobot.openFiatCurrencyPicker();
|
||||
await displayRobot.chooseFiatCurrency(wanted);
|
||||
|
||||
displayRobot.expectFiatCurrencyShown(wanted);
|
||||
|
||||
expect(
|
||||
settingsStore.fiatCurrency,
|
||||
wanted,
|
||||
reason: "The row shows ${wanted.title} but the setting behind it did not change",
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_wallets.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/restore_from_seed_or_key_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("A seed the wallet cannot parse restores nothing", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final restoreRobot = RestoreFromSeedOrKeysPageRobot(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
const walletType = WalletType.solana;
|
||||
|
||||
await appLauncher.launchApp(testKey: "invalid_seed_test_app_key");
|
||||
|
||||
await onboardingFlows.startRestoringFirstWallet(walletType);
|
||||
|
||||
// Twelve words that are not in the wordlist. Restoring from this would either fail
|
||||
// outright or, worse it derives some other wallet the user has no keys for.
|
||||
await restoreRobot.enterSeedPhraseForWalletRestore(
|
||||
"alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima",
|
||||
);
|
||||
|
||||
await restoreRobot.onRestoreWalletButtonPressed();
|
||||
|
||||
await restoreRobot.expectRestoreRefused();
|
||||
|
||||
// The same form with the real seed goes through. Without this the test above would pass
|
||||
// just as happily if the form were broken and restored nothing for any seed at all.
|
||||
await restoreRobot.selectWalletNameFromAvailableOptions();
|
||||
await restoreRobot.enterSeedPhraseForWalletRestore(TestWallets.seedFor(walletType));
|
||||
await restoreRobot.onRestoreWalletButtonPressed();
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
expect(getIt.get<AppStore>().wallet?.type, walletType);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/display_settings_page_robot.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_settings_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Changing the language changes what the settings screen shows", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final settingsRobot = NewSettingsPageRobot(tester);
|
||||
final displayRobot = DisplaySettingsPageRobot(tester);
|
||||
|
||||
// Not the default, so seeing it afterwards means the change took rather than the screen
|
||||
// having always said so.
|
||||
const languageCode = "de";
|
||||
const languageName = "Deutsch (German)";
|
||||
|
||||
await appLauncher.launchApp(testKey: "language_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSettingsSheet();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.displaySettingsPage);
|
||||
await displayRobot.isDisplayed();
|
||||
|
||||
await displayRobot.openLanguagePicker();
|
||||
await displayRobot.chooseLanguage(languageCode);
|
||||
|
||||
displayRobot.expectLanguageShown(languageName);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_config.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Create a wallet for each configured wallet type", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "onboarding_create_test_app_key");
|
||||
|
||||
final walletTypes = TestConfig.walletTypesUnderTest;
|
||||
final firstType = walletTypes.first;
|
||||
|
||||
tester.printToConsole("Creating first wallet: ${firstType.name}");
|
||||
|
||||
await onboardingFlows.createFirstWallet(firstType);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
expect(appStore.wallet?.type, firstType);
|
||||
|
||||
await homePageRobot.hasWalletName(appStore.wallet!.name);
|
||||
|
||||
for (final type in walletTypes.skip(1)) {
|
||||
tester.printToConsole("Creating additional wallet: ${type.name}");
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
|
||||
await onboardingFlows.createAdditionalWalletFromWalletList(type);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
expect(appStore.wallet?.type, type);
|
||||
|
||||
await homePageRobot.hasWalletName(appStore.wallet!.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_config.dart";
|
||||
import "../../core/test_wallets.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Restore a wallet from seed for each configured wallet type", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "onboarding_restore_test_app_key");
|
||||
|
||||
final walletTypes = TestConfig.walletTypesUnderTest;
|
||||
final firstType = walletTypes.first;
|
||||
|
||||
tester.printToConsole("Restoring first wallet: ${firstType.name}");
|
||||
|
||||
await onboardingFlows.restoreFirstWalletFromSeed(firstType);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
expect(appStore.wallet?.type, firstType);
|
||||
|
||||
await homePageRobot.hasWalletName(appStore.wallet!.name);
|
||||
|
||||
await _expectRestoredTheRightWallet(homePageRobot, appStore, firstType);
|
||||
|
||||
for (final type in walletTypes.skip(1)) {
|
||||
tester.printToConsole("Restoring additional wallet: ${type.name}");
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
|
||||
await onboardingFlows.restoreAdditionalWalletFromWalletList(type);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
expect(appStore.wallet?.type, type);
|
||||
|
||||
await homePageRobot.hasWalletName(appStore.wallet!.name);
|
||||
|
||||
await _expectRestoredTheRightWallet(homePageRobot, appStore, type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _expectRestoredTheRightWallet(
|
||||
HomePageRobot robot,
|
||||
AppStore appStore,
|
||||
WalletType type,
|
||||
) async {
|
||||
final expectedAddress = TestWallets.receiveAddressFor(type);
|
||||
|
||||
if (expectedAddress.isEmpty) {
|
||||
robot.tester.printToConsole("No known address for ${type.name}, derivation not verified");
|
||||
return;
|
||||
}
|
||||
|
||||
final settled = await robot.pumpUntil(
|
||||
() {
|
||||
final addresses = appStore.wallet?.walletAddresses;
|
||||
|
||||
if (addresses == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return addresses.address == expectedAddress ||
|
||||
addresses.addressesMap.containsKey(expectedAddress) ||
|
||||
addresses.allAddressesMap.containsKey(expectedAddress);
|
||||
},
|
||||
timeout: const Duration(seconds: 60),
|
||||
);
|
||||
|
||||
expect(
|
||||
settled,
|
||||
true,
|
||||
reason: "${type.name} restored from a known seed derived "
|
||||
"${appStore.wallet?.walletAddresses.address} and does not know $expectedAddress",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_receive_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Receive sheet shows the opened wallet's own address", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final receiveRobot = NewReceivePageRobot(tester);
|
||||
|
||||
const walletType = WalletType.solana;
|
||||
|
||||
await appLauncher.launchApp(testKey: "receive_address_test_app_key");
|
||||
|
||||
await onboardingFlows.restoreFirstWalletFromSeed(walletType);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final expectedAddress = getIt.get<AppStore>().wallet!.walletAddresses.address;
|
||||
expect(expectedAddress.isNotEmpty, true);
|
||||
|
||||
await homePageRobot.openReceiveSheet();
|
||||
|
||||
await receiveRobot.isDisplayed();
|
||||
await receiveRobot.confirmAddressMatches(expectedAddress);
|
||||
|
||||
await receiveRobot.dismissModal();
|
||||
await homePageRobot.isDisplayed();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/auth_flows.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_settings_page_robot.dart";
|
||||
import "../../robots/show_keys_disclaimer_page_robot.dart";
|
||||
import "../../robots/wallet_keys_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Displayed seed and keys match the opened wallet", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final authFlows = AuthFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final settingsRobot = NewSettingsPageRobot(tester);
|
||||
final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
|
||||
final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
|
||||
|
||||
// These three cover the different key display paths on the wallet keys page.
|
||||
final walletTypes = [WalletType.solana, WalletType.bitcoin, WalletType.monero];
|
||||
|
||||
await appLauncher.launchApp(testKey: "seed_confirmation_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(walletTypes.first);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
|
||||
for (final type in walletTypes) {
|
||||
if (type != walletTypes.first) {
|
||||
await dashboardRobot.openWalletsTab();
|
||||
await onboardingFlows.createAdditionalWalletFromWalletList(type);
|
||||
await homePageRobot.isDisplayed();
|
||||
}
|
||||
|
||||
expect(appStore.wallet?.type, type);
|
||||
|
||||
tester.printToConsole("Confirming displayed credentials for ${type.name}");
|
||||
|
||||
await homePageRobot.openSettingsSheet();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.showKeysDisclaimer);
|
||||
await disclaimerRobot.isDisplayed();
|
||||
await disclaimerRobot.tapShowKeys();
|
||||
await authFlows.authenticateWithPin();
|
||||
|
||||
await walletKeysRobot.isDisplayed();
|
||||
await walletKeysRobot.confirmWalletCredentials(type);
|
||||
|
||||
await settingsRobot.goBack();
|
||||
await settingsRobot.dismissModal();
|
||||
|
||||
await homePageRobot.isDisplayed();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_wallets.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/contact_robot.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_send_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("A contact picked on the send screen fills in its own address", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final contactRobot = ContactRobot(tester);
|
||||
final sendRobot = NewSendPageRobot(tester);
|
||||
|
||||
const contactName = "Send Book Contact";
|
||||
final contactAddress = TestWallets.receiveAddressFor(WalletType.solana);
|
||||
|
||||
await appLauncher.launchApp(testKey: "send_from_address_book_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await dashboardRobot.openContactsTab();
|
||||
await contactRobot.isDisplayed();
|
||||
|
||||
await contactRobot.openAddContact();
|
||||
await contactRobot.enterName(contactName);
|
||||
await contactRobot.chooseCurrency("sol");
|
||||
await contactRobot.enterAddress(contactAddress);
|
||||
await contactRobot.save();
|
||||
|
||||
await contactRobot.isDisplayed();
|
||||
|
||||
await dashboardRobot.openHomeTab();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSendSheet();
|
||||
await sendRobot.isDisplayed();
|
||||
|
||||
await sendRobot.pickContactFromAddressBook(contactName);
|
||||
|
||||
expect(
|
||||
sendRobot.enteredAddress(),
|
||||
contactAddress,
|
||||
reason: "The send screen was filled with an address the contact does not hold",
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_wallets.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_send_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Send screen refuses what it cannot send", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final sendRobot = NewSendPageRobot(tester);
|
||||
|
||||
const walletType = WalletType.solana;
|
||||
|
||||
await appLauncher.launchApp(testKey: "send_validation_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(walletType);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSendSheet();
|
||||
await sendRobot.isDisplayed();
|
||||
|
||||
await sendRobot.tapSendButtonWhenReady(timeout: const Duration(minutes: 5));
|
||||
await sendRobot.expectNoTransactionBuilt();
|
||||
|
||||
// test that no transaction is built when the address is wrong or cannot be parsed
|
||||
await sendRobot.enterAddress("not-a-real-address");
|
||||
await sendRobot.enterAmount("0.0001");
|
||||
await sendRobot.tapSendButton();
|
||||
|
||||
await sendRobot.expectNoTransactionBuilt();
|
||||
|
||||
// test that no transaction is built even if the address is valid but no balance
|
||||
await sendRobot.enterAddress(TestWallets.receiveAddressFor(walletType));
|
||||
await sendRobot.enterAmount("0.0001");
|
||||
await sendRobot.tapSendButton();
|
||||
|
||||
await sendRobot.expectNoTransactionBuilt();
|
||||
|
||||
await sendRobot.dismissModal();
|
||||
await homePageRobot.isDisplayed();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import "package:cake_wallet/new-ui/pages/about_page.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/settings/connection_sync_page.dart";
|
||||
import "package:cake_wallet/src/screens/settings/display_settings_page.dart";
|
||||
import "package:cake_wallet/src/screens/settings/manage_nodes_page.dart";
|
||||
import "package:cake_wallet/src/screens/settings/other_settings_page.dart";
|
||||
import "package:cake_wallet/src/screens/settings/privacy_page.dart";
|
||||
import "package:cake_wallet/src/screens/settings/security_backup_page.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_settings_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Every settings row opens its page and backs out", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final settingsRobot = NewSettingsPageRobot(tester);
|
||||
|
||||
final rowsUnderTest = <String, Type>{
|
||||
Routes.manageNodes: ManageNodesPage,
|
||||
Routes.privacyPage: PrivacyPage,
|
||||
Routes.otherSettingsPage: OtherSettingsPage,
|
||||
Routes.connectionSync: ConnectionSyncPage,
|
||||
Routes.displaySettingsPage: DisplaySettingsPage,
|
||||
Routes.securityBackupPage: SecurityBackupPage,
|
||||
Routes.aboutPage: AboutPage,
|
||||
};
|
||||
|
||||
await appLauncher.launchApp(testKey: "settings_nav_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSettingsSheet();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
for (final entry in rowsUnderTest.entries) {
|
||||
tester.printToConsole("Opening settings row ${entry.key}");
|
||||
|
||||
await settingsRobot.openRow(entry.key);
|
||||
await settingsRobot.confirmLeafPageDisplayed(entry.value);
|
||||
|
||||
await settingsRobot.goBack();
|
||||
await settingsRobot.isDisplayed();
|
||||
}
|
||||
|
||||
await settingsRobot.dismissModal();
|
||||
await homePageRobot.isDisplayed();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/auth_flows.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_settings_page_robot.dart";
|
||||
import "../../robots/show_keys_disclaimer_page_robot.dart";
|
||||
import "../../robots/wallet_keys_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Seed and keys page rejects a wrong pin and accepts the right one",
|
||||
(tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final authFlows = AuthFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final settingsRobot = NewSettingsPageRobot(tester);
|
||||
final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
|
||||
final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
|
||||
|
||||
final wrongPin = [9, 9, 9, 9];
|
||||
|
||||
await appLauncher.launchApp(testKey: "show_keys_auth_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSettingsSheet();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.showKeysDisclaimer);
|
||||
await disclaimerRobot.isDisplayed();
|
||||
await disclaimerRobot.tapShowKeys();
|
||||
|
||||
await authFlows.authenticateWithPin(pin: wrongPin);
|
||||
|
||||
// A wrong pin must keep the keys page locked away.
|
||||
await settingsRobot.settle(max: const Duration(seconds: 3));
|
||||
expect(find.byType(WalletKeysPage), findsNothing);
|
||||
expect(authFlows.isPinWidgetShown(), true);
|
||||
|
||||
await authFlows.authenticateWithPin();
|
||||
|
||||
await walletKeysRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.goBack();
|
||||
await settingsRobot.dismissModal();
|
||||
await homePageRobot.isDisplayed();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/auth_flows.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/wallet_edit_page_robot.dart";
|
||||
import "../../robots/wallet_list_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Deleting a wallet takes it off the list and out of storage", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final authFlows = AuthFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final walletListRobot = WalletListPageRobot(tester);
|
||||
final walletEditRobot = WalletEditPageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "wallet_delete_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final deletedWallet = appStore.wallet!.name;
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
await onboardingFlows.createAdditionalWalletFromWalletList(WalletType.ethereum);
|
||||
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final keptWallet = appStore.wallet!.name;
|
||||
|
||||
expect(keptWallet, isNot(deletedWallet), reason: "The second wallet never became the open one");
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
await walletListRobot.isDisplayed();
|
||||
|
||||
await walletListRobot.openEditFor(deletedWallet);
|
||||
await walletEditRobot.isDisplayed();
|
||||
|
||||
await walletEditRobot.tapDelete();
|
||||
await authFlows.authenticateWithPin(required: false);
|
||||
|
||||
await walletEditRobot.confirmDelete();
|
||||
|
||||
await walletListRobot.isDisplayed();
|
||||
|
||||
final removed = await walletListRobot.pumpUntil(
|
||||
() => !walletListRobot.hasWallet(deletedWallet),
|
||||
timeout: const Duration(minutes: 1),
|
||||
);
|
||||
|
||||
expect(removed, true, reason: "$deletedWallet is still on the list after being deleted");
|
||||
|
||||
expect(
|
||||
walletListRobot.hasWallet(keptWallet),
|
||||
true,
|
||||
reason: "Deleting $deletedWallet took $keptWallet with it",
|
||||
);
|
||||
|
||||
final stored = await WalletInfo.getAll();
|
||||
final names = stored.map((wallet) => wallet.name).toList();
|
||||
|
||||
expect(names, isNot(contains(deletedWallet)));
|
||||
expect(names, contains(keptWallet));
|
||||
|
||||
expect(
|
||||
appStore.wallet?.name,
|
||||
keptWallet,
|
||||
reason: "Deleting another wallet changed which one is open",
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../flows/wallet_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("A wallet added to an existing seed shares it and both stay reachable",
|
||||
(tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final walletFlows = WalletFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
const firstType = WalletType.solana;
|
||||
const secondType = WalletType.ethereum;
|
||||
|
||||
await appLauncher.launchApp(testKey: "wallet_group_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(firstType);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final firstWalletName = appStore.wallet!.name;
|
||||
final sharedSeed = appStore.wallet!.seed;
|
||||
|
||||
expect(sharedSeed, isNotNull, reason: "The first wallet has no seed to share");
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
|
||||
await onboardingFlows.createWalletInGroupOf(secondType, firstWalletName);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final secondWalletName = appStore.wallet!.name;
|
||||
|
||||
expect(
|
||||
secondWalletName,
|
||||
isNot(firstWalletName),
|
||||
reason: "Adding to a group opened the wallet that was already there",
|
||||
);
|
||||
|
||||
expect(appStore.wallet?.type, secondType);
|
||||
|
||||
expect(
|
||||
appStore.wallet!.seed,
|
||||
sharedSeed,
|
||||
reason: "The wallet added to the group derived its own seed instead of sharing one",
|
||||
);
|
||||
|
||||
await walletFlows.switchToWallet(firstWalletName);
|
||||
|
||||
await homePageRobot.isDisplayed();
|
||||
await homePageRobot.hasWalletName(firstWalletName);
|
||||
|
||||
expect(appStore.wallet?.seed, sharedSeed);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/wallet_edit_page_robot.dart";
|
||||
import "../../robots/wallet_list_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("A wallet can be renamed, but not onto a name already in use", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final walletListRobot = WalletListPageRobot(tester);
|
||||
final walletEditRobot = WalletEditPageRobot(tester);
|
||||
|
||||
const renamedTo = "Renamed Test Wallet";
|
||||
|
||||
await appLauncher.launchApp(testKey: "wallet_rename_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final renamedWallet = appStore.wallet!.name;
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
await onboardingFlows.createAdditionalWalletFromWalletList(WalletType.ethereum);
|
||||
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final openWallet = appStore.wallet!.name;
|
||||
|
||||
expect(openWallet, isNot(renamedWallet), reason: "The second wallet never became the open one");
|
||||
|
||||
await dashboardRobot.openWalletsTab();
|
||||
await walletListRobot.isDisplayed();
|
||||
|
||||
await walletListRobot.openEditFor(renamedWallet);
|
||||
await walletEditRobot.isDisplayed();
|
||||
|
||||
await walletEditRobot.enterName(openWallet);
|
||||
await walletEditRobot.save();
|
||||
|
||||
await walletEditRobot.expectNameTakenRefusal();
|
||||
await walletEditRobot.dismissNameTakenRefusal();
|
||||
|
||||
await walletEditRobot.enterName(renamedTo);
|
||||
await walletEditRobot.save();
|
||||
|
||||
await walletListRobot.isDisplayed();
|
||||
|
||||
final renamed = await walletListRobot.pumpUntil(() => walletListRobot.hasWallet(renamedTo));
|
||||
|
||||
expect(renamed, true, reason: "The list never showed $renamedTo after the rename was saved");
|
||||
|
||||
expect(
|
||||
walletListRobot.hasWallet(renamedWallet),
|
||||
false,
|
||||
reason: "The list still shows $renamedWallet next to the name it was renamed to",
|
||||
);
|
||||
|
||||
final stored = await WalletInfo.getAll();
|
||||
final names = stored.map((wallet) => wallet.name).toList();
|
||||
|
||||
expect(names, contains(renamedTo));
|
||||
expect(names, isNot(contains(renamedWallet)));
|
||||
|
||||
expect(
|
||||
appStore.wallet?.name,
|
||||
openWallet,
|
||||
reason: "Renaming another wallet changed which one is open",
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../core/test_wallets.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("A 25 word monero seed restores through the legacy path", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
final legacySeed = TestWallets.moneroLegacySeed;
|
||||
|
||||
expect(
|
||||
legacySeed.split(" ").length,
|
||||
25,
|
||||
reason: "The legacy seed secret is not a 25 word seed, this suite would restore a polyseed",
|
||||
);
|
||||
|
||||
await appLauncher.launchApp(testKey: "monero_legacy_seed_test_app_key");
|
||||
|
||||
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.monero, seed: legacySeed);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
|
||||
expect(appStore.wallet?.type, WalletType.monero);
|
||||
|
||||
expect(
|
||||
appStore.wallet?.seed?.split(" ").length,
|
||||
25,
|
||||
reason: "The wallet came back with a seed of a different length than the one restored",
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/manage_nodes_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_settings_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Switching nodes changes which one the wallet is pointed at", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final settingsRobot = NewSettingsPageRobot(tester);
|
||||
final nodesRobot = ManageNodesPageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "node_switching_test_app_key");
|
||||
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSettingsSheet();
|
||||
await settingsRobot.isDisplayed();
|
||||
|
||||
await settingsRobot.openRow(Routes.manageNodes);
|
||||
await nodesRobot.isDisplayed();
|
||||
|
||||
final initialNodeUri = nodesRobot.currentNodeUri();
|
||||
|
||||
expect(initialNodeUri, isNotEmpty, reason: "The wallet started with no node at all");
|
||||
|
||||
await nodesRobot.switchToAnotherNode();
|
||||
|
||||
expect(
|
||||
nodesRobot.currentNodeUri(),
|
||||
isNot(initialNodeUri),
|
||||
reason: "The node was confirmed but the wallet is still pointed at $initialNodeUri",
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
import "../../robots/new_swap_page_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Swap sheet fetches a provider quote, no trade is created", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
final swapRobot = NewSwapPageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "swap_quote_test_app_key");
|
||||
|
||||
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.openSwapSheet();
|
||||
await swapRobot.isDisplayed();
|
||||
|
||||
await swapRobot.enterDepositAmount("1");
|
||||
|
||||
await swapRobot.confirmQuoteReceived();
|
||||
|
||||
await swapRobot.dismissModal();
|
||||
await homePageRobot.isDisplayed();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Restored wallet connects to a node and starts syncing", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "sync_status_test_app_key");
|
||||
|
||||
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.bitcoin);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.confirmSyncIndicatorShown();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Tapping a transaction opens the details of that transaction", (tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "transaction_details_test_app_key");
|
||||
|
||||
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
await homePageRobot.confirmTransactionHistoryVisible();
|
||||
|
||||
await homePageRobot.openAllTransactions();
|
||||
await homePageRobot.confirmAllTransactionsVisible();
|
||||
|
||||
final tapped = homePageRobot.firstTransactionIdInAllView();
|
||||
|
||||
expect(tapped, isNotEmpty, reason: "The tile carries no transaction id to check against");
|
||||
|
||||
await homePageRobot.openFirstTransactionDetails();
|
||||
|
||||
// A list that hands the wrong transaction to the details screen shows one person's
|
||||
// amounts under another's id, and nothing on the screen would say so.
|
||||
expect(
|
||||
homePageRobot.openedTransactionId(),
|
||||
tapped,
|
||||
reason: "The details opened for a different transaction than the one tapped",
|
||||
);
|
||||
|
||||
homePageRobot.hasTransactionIdRow();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("Transaction history renders for a wallet with known transactions",
|
||||
(tester) async {
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
await appLauncher.launchApp(testKey: "transaction_history_test_app_key");
|
||||
|
||||
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
expect(getIt.get<AppStore>().wallet?.type, WalletType.solana);
|
||||
|
||||
await homePageRobot.confirmTransactionHistoryVisible();
|
||||
|
||||
await homePageRobot.openAllTransactions();
|
||||
|
||||
await homePageRobot.confirmAllTransactionsVisible();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copy this file into integration_test/suites/tier0/ or tier1/, rename it to
|
||||
// <feature>_test.dart and replace the example steps with your journey.
|
||||
// The runner only picks up files ending in _test.dart, this template never runs.
|
||||
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:integration_test/integration_test.dart";
|
||||
|
||||
import "../../core/app_launcher.dart";
|
||||
import "../../flows/onboarding_flows.dart";
|
||||
import "../../robots/home_page_robot.dart";
|
||||
import "../../robots/new_dashboard_robot.dart";
|
||||
|
||||
void main() {
|
||||
// Required first line in every suite, integrationTest contains background app errors
|
||||
// that testWidgets would turn into failures.
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
integrationTest("One sentence describing the journey under test", (tester) async {
|
||||
// Robots and flows take the tester, create the ones your journey needs.
|
||||
final appLauncher = AppLauncher(tester);
|
||||
final onboardingFlows = OnboardingFlows(tester);
|
||||
final dashboardRobot = NewDashboardRobot(tester);
|
||||
final homePageRobot = HomePageRobot(tester);
|
||||
|
||||
// Boots the real app, the key names the suite in failure output.
|
||||
await appLauncher.launchApp(testKey: "example_feature_test_app_key");
|
||||
|
||||
// Every suite starts from a fresh install, so get to a working wallet first.
|
||||
// Use restoreFirstWalletFromSeed instead when your feature needs a known wallet.
|
||||
await onboardingFlows.createFirstWallet(WalletType.solana);
|
||||
|
||||
await dashboardRobot.isDisplayed();
|
||||
await homePageRobot.isDisplayed();
|
||||
|
||||
// Drive your feature through its robot from here. Robots own the finders and
|
||||
// assertions for their screen, the suite only sequences them:
|
||||
//
|
||||
// await homePageRobot.openSettingsSheet();
|
||||
// await mySettingsRobot.isDisplayed();
|
||||
// await mySettingsRobot.doTheThing();
|
||||
//
|
||||
// Rules of thumb:
|
||||
// - Wait with pumpUntilFound and pumpUntil, never with sleeps or pumpAndSettle.
|
||||
// - Tap and read widgets by ValueKey, add keys to lib/ widgets when they are missing.
|
||||
// - Assert app state through getIt (AppStore) or the page widget's view model.
|
||||
// - A widget being in the tree does not mean it is on screen. Every dashboard tab
|
||||
// stays mounted in an IndexedStack, so find.byType on a tab passes from any other
|
||||
// tab. Check what is in front, the way WalletListPageRobot does.
|
||||
// - A button being findable does not mean it is enabled. Tapping a disabled button
|
||||
// does nothing at all and reads later as a missing widget somewhere else, so wait
|
||||
// on whatever the screen itself gates on.
|
||||
// - Add the finished suite to the table in docs/INTEGRATION_TESTS.md.
|
||||
});
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../components/common_test_constants.dart';
|
||||
import '../components/common_test_flows.dart';
|
||||
import '../robots/auth_page_robot.dart';
|
||||
import '../robots/dashboard_page_robot.dart';
|
||||
import '../robots/security_and_backup_page_robot.dart';
|
||||
import '../robots/wallet_keys_robot.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
AuthPageRobot authPageRobot;
|
||||
CommonTestFlows commonTestFlows;
|
||||
DashboardPageRobot dashboardPageRobot;
|
||||
WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot;
|
||||
SecurityAndBackupPageRobot securityAndBackupPageRobot;
|
||||
|
||||
testWidgets(
|
||||
'Confirm if the seeds display properly',
|
||||
(tester) async {
|
||||
// Store the original FlutterError.onError handler
|
||||
final originalOnError = FlutterError.onError;
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
debugPrint('FlutterError caught: ${details.exception}');
|
||||
};
|
||||
|
||||
try {
|
||||
authPageRobot = AuthPageRobot(tester);
|
||||
commonTestFlows = CommonTestFlows(tester);
|
||||
dashboardPageRobot = DashboardPageRobot(tester);
|
||||
walletKeysAndSeedPageRobot = WalletKeysAndSeedPageRobot(tester);
|
||||
securityAndBackupPageRobot = SecurityAndBackupPageRobot(tester);
|
||||
|
||||
await commonTestFlows.startAppFlow(
|
||||
ValueKey('confirm_creds_display_correctly_flow_app_key'),
|
||||
);
|
||||
|
||||
await commonTestFlows.welcomePageToCreateNewWalletFlow(
|
||||
WalletType.solana,
|
||||
CommonTestConstants.pin,
|
||||
);
|
||||
|
||||
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
|
||||
|
||||
await _confirmSeedsFlowForWalletType(
|
||||
WalletType.solana,
|
||||
authPageRobot,
|
||||
dashboardPageRobot,
|
||||
securityAndBackupPageRobot,
|
||||
walletKeysAndSeedPageRobot,
|
||||
tester,
|
||||
);
|
||||
|
||||
// Do the same for other available wallet types
|
||||
for (var walletType in availableWalletTypes) {
|
||||
if (walletType == WalletType.solana) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await dashboardPageRobot.navigateToWalletsListPage(isDrawer: true);
|
||||
|
||||
await commonTestFlows.createNewWalletFromWalletMenu(walletType);
|
||||
|
||||
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
|
||||
|
||||
await _confirmSeedsFlowForWalletType(
|
||||
walletType,
|
||||
authPageRobot,
|
||||
dashboardPageRobot,
|
||||
securityAndBackupPageRobot,
|
||||
walletKeysAndSeedPageRobot,
|
||||
tester,
|
||||
);
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(seconds: 15));
|
||||
} finally {
|
||||
FlutterError.onError = originalOnError;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmSeedsFlowForWalletType(
|
||||
WalletType walletType,
|
||||
AuthPageRobot authPageRobot,
|
||||
DashboardPageRobot dashboardPageRobot,
|
||||
SecurityAndBackupPageRobot securityAndBackupPageRobot,
|
||||
WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot,
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await dashboardPageRobot.openDrawerMenu();
|
||||
await dashboardPageRobot.dashboardMenuWidgetRobot.navigateToSecurityAndBackupPage();
|
||||
|
||||
await securityAndBackupPageRobot.navigateToShowKeysPage();
|
||||
|
||||
final onAuthPage = authPageRobot.onAuthPage();
|
||||
if (onAuthPage) {
|
||||
await authPageRobot.enterPinCode(CommonTestConstants.pin);
|
||||
}
|
||||
|
||||
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
|
||||
if (onAuthPageDesktop) {
|
||||
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await walletKeysAndSeedPageRobot.isWalletKeysAndSeedPage();
|
||||
walletKeysAndSeedPageRobot.hasTitle();
|
||||
walletKeysAndSeedPageRobot.hasShareWarning();
|
||||
|
||||
await walletKeysAndSeedPageRobot.confirmWalletCredentials(walletType);
|
||||
|
||||
await walletKeysAndSeedPageRobot.backToDashboard();
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../components/common_test_constants.dart';
|
||||
import '../components/common_test_flows.dart';
|
||||
import '../robots/dashboard_page_robot.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
CommonTestFlows commonTestFlows;
|
||||
DashboardPageRobot dashboardPageRobot;
|
||||
|
||||
testWidgets(
|
||||
'Create Wallet Flow',
|
||||
(tester) async {
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
debugPrint('FlutterError caught: ${details.exception}');
|
||||
};
|
||||
commonTestFlows = CommonTestFlows(tester);
|
||||
dashboardPageRobot = DashboardPageRobot(tester);
|
||||
|
||||
// Start the app
|
||||
await commonTestFlows.startAppFlow(
|
||||
ValueKey('create_wallets_through_seeds_test_app_key'),
|
||||
);
|
||||
|
||||
await commonTestFlows.welcomePageToCreateNewWalletFlow(
|
||||
WalletType.solana,
|
||||
CommonTestConstants.pin,
|
||||
);
|
||||
|
||||
// Confirm it actually restores a solana wallet
|
||||
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
|
||||
|
||||
// Do the same for other available wallet types
|
||||
for (var walletType in availableWalletTypes) {
|
||||
if (walletType == WalletType.solana) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await dashboardPageRobot.navigateToWalletsListPage();
|
||||
|
||||
await commonTestFlows.createNewWalletFromWalletMenu(walletType);
|
||||
|
||||
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
|
||||
}
|
||||
|
||||
// Goes to the wallet menu and provides a confirmation that all the wallets were correctly restored
|
||||
await dashboardPageRobot.navigateToWalletsListPage();
|
||||
|
||||
commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly();
|
||||
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../components/common_test_constants.dart';
|
||||
import '../components/common_test_flows.dart';
|
||||
import '../robots/dashboard_page_robot.dart';
|
||||
import '../robots/exchange_confirm_page_robot.dart';
|
||||
import '../robots/exchange_page_robot.dart';
|
||||
import '../robots/exchange_trade_external_send_page_robot.dart';
|
||||
import '../robots/exchange_trade_page_robot.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
CommonTestFlows commonTestFlows;
|
||||
ExchangePageRobot exchangePageRobot;
|
||||
DashboardPageRobot dashboardPageRobot;
|
||||
ExchangeTradePageRobot exchangeTradePageRobot;
|
||||
ExchangeConfirmPageRobot exchangeConfirmPageRobot;
|
||||
ExchangeTradeExternalSendPageRobot exchangeTradeExternalSendPageRobot;
|
||||
|
||||
testWidgets('Exchange flow', (tester) async {
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
debugPrint('FlutterError caught: ${details.exception}');
|
||||
};
|
||||
|
||||
commonTestFlows = CommonTestFlows(tester);
|
||||
exchangePageRobot = ExchangePageRobot(tester);
|
||||
dashboardPageRobot = DashboardPageRobot(tester);
|
||||
exchangeTradePageRobot = ExchangeTradePageRobot(tester);
|
||||
exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
|
||||
exchangeTradeExternalSendPageRobot = ExchangeTradeExternalSendPageRobot(tester);
|
||||
|
||||
await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key'));
|
||||
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
|
||||
CommonTestConstants.testWalletType,
|
||||
secrets.solanaTestWalletSeeds,
|
||||
CommonTestConstants.pin,
|
||||
);
|
||||
await dashboardPageRobot.navigateToExchangePage();
|
||||
|
||||
// ----------- Exchange Page -------------
|
||||
await exchangePageRobot.selectDepositCurrency(CommonTestConstants.exchangeTestDepositCurrency);
|
||||
await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.exchangeTestReceiveCurrency);
|
||||
|
||||
await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
|
||||
await exchangePageRobot.enterDepositRefundAddress(
|
||||
depositAddress: CommonTestConstants.testWalletAddress,
|
||||
);
|
||||
await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
|
||||
await exchangePageRobot.onExchangeButtonPressed();
|
||||
await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
|
||||
|
||||
await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
|
||||
|
||||
await exchangeTradePageRobot.onGotItButtonPressed();
|
||||
await exchangeTradePageRobot.onSendFromExternalButtonPressed();
|
||||
|
||||
await exchangeTradeExternalSendPageRobot.isExchangeTradeExternalSendPage();
|
||||
await exchangeTradeExternalSendPageRobot.verifySendDetailsItemsDisplayProperly();
|
||||
await exchangeTradeExternalSendPageRobot.onContinueButtonPressed();
|
||||
});
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../components/common_test_constants.dart';
|
||||
import '../components/common_test_flows.dart';
|
||||
import '../robots/dashboard_page_robot.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
CommonTestFlows commonTestFlows;
|
||||
DashboardPageRobot dashboardPageRobot;
|
||||
|
||||
testWidgets(
|
||||
'Restoring Wallets Through Seeds',
|
||||
(tester) async {
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
debugPrint('FlutterError caught: ${details.exception}');
|
||||
};
|
||||
|
||||
commonTestFlows = CommonTestFlows(tester);
|
||||
dashboardPageRobot = DashboardPageRobot(tester);
|
||||
|
||||
// Start the app
|
||||
await commonTestFlows.startAppFlow(
|
||||
ValueKey('restore_wallets_through_seeds_test_app_key'),
|
||||
);
|
||||
|
||||
// Restore the first wallet type: Solana
|
||||
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
|
||||
WalletType.solana,
|
||||
secrets.solanaTestWalletSeeds,
|
||||
CommonTestConstants.pin,
|
||||
);
|
||||
|
||||
// Confirm it actually restores a solana wallet
|
||||
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
|
||||
|
||||
// Do the same for other available wallet types
|
||||
for (var walletType in availableWalletTypes) {
|
||||
if (walletType == WalletType.solana) {
|
||||
continue;
|
||||
}
|
||||
final seed = commonTestFlows.getWalletSeedsByWalletType(walletType);
|
||||
if (seed.isEmpty) {
|
||||
tester.printToConsole("----------------------------");
|
||||
tester.printToConsole("- Skipped wallet: ${walletType}");
|
||||
tester.printToConsole("- Make sure to add seed to secrets");
|
||||
tester.printToConsole("----------------------------");
|
||||
continue;
|
||||
}
|
||||
|
||||
await dashboardPageRobot.navigateToWalletsListPage();
|
||||
|
||||
await commonTestFlows.restoreWalletFromWalletMenu(
|
||||
walletType,
|
||||
seed,
|
||||
);
|
||||
|
||||
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
|
||||
}
|
||||
|
||||
// Goes to the wallet menu and provides a visual confirmation that all the wallets were correctly restored
|
||||
await dashboardPageRobot.navigateToWalletsListPage();
|
||||
|
||||
commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly();
|
||||
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
},
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user