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:
David Adegoke
2026-09-23 20:11:37 +03:00
committed by GitHub
parent 28d540d5f1
commit dfa5165766
137 changed files with 6024 additions and 4772 deletions
@@ -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
+28
View File
@@ -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
+1 -86
View File
@@ -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
-6
View File
@@ -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"
+306
View File
@@ -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 '';
}
}
}
+75
View File
@@ -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);
};
}
}
+238
View File
@@ -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");
}
}
}
+51
View File
@@ -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",
];
+6
View File
@@ -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 = {};
+46
View File
@@ -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");
}
}
+112
View File
@@ -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;
}
}
+102
View File
@@ -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;
}
+41
View File
@@ -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;
}
}
+155
View File
@@ -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();
}
}
+59
View File
@@ -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");
}
}
-96
View File
@@ -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");
});
}
-27
View File
@@ -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
+10 -26
View File
@@ -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");
}
}
+114 -112
View File
@@ -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);
}
}
+13 -35
View File
@@ -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