I'm building a budgeting app called Budget Lover. It is a personal project, written in Flutter with Firebase behind it, and it tries to answer one question: how much can I spend today? At its heart is a single "Safe to Spend" number, worked out from the balance, the bills that are coming and the person's typical spending. It speaks 15 languages, and it isn't released yet: so far it lives on an emulator and on my own phone.

A few days ago I installed the latest build on that phone, ran it in Greek, and spent six minutes tapping through it while taking screenshots and recording the screen. The screenshots and the recording showed five bugs. The test suite, more than 1,100 tests, had passed earlier that day.

Two of the app's six tabs: Insights, where the "Safe to Spend" number lives, and Savings.

This isn't an argument against automated tests. Every bug below now has one. It's a record of what my tests were blind to, because I suspect many apps share the same blind spots.

What the tests covered

The suite includes layout tests that open every screen and bottom sheet in all 15 languages, at a normal and an enlarged text size and on a small 320 dp screen, and fail on any overflow. That net had already caught plenty of long German and Greek labels. It did not catch these.

1. A word that didn't fit

The Home screen has four quick actions with a short caption under each. In English the captions are short. In Greek, "Add savings" is Προσθήκη αποταμίευσης, and its last word is wider than a quarter of the screen, so the app showed "αποταμίευσ…".

My layout tests fail on overflow, but a caption cut short with an ellipsis is not an overflow: Flutter did exactly what I had asked it to do. The caption now shrinks until its longest word fits, and a new test checks whether any caption was truncated, in every language.

The same four quick actions in four languages. The long Greek and German captions now fit.

2. English inside a Greek screen

The same screenshot showed a sentence in English on the trip card. In the app, the English text is the key to its translation, and a script scans the code for those keys and complains about any that a language table is missing. This sentence was picked by a condition (isEmpty ? 'a' : 'b'), which the script can't see.

There are now two defences: a second scanner for exactly that pattern, which found eight texts hiding the same way, and a test that opens every screen in Greek and fails if any text is missing from the table.

3. A dialog nobody opened

On Sundays the app offers a short weekly review in a dialog with an icon and a title. In Greek the title, Κυριακάτικη ανασκόπηση, ran out of the dialog on the right. No test had ever opened that dialog, because it only appears on Sundays. It is now part of the layout tests like every other sheet, and in Greek it overflowed by 96 pixels the first time it was asked.

The lesson: whatever appears conditionally (on Sundays, for one kind of account, on first launch) needs a deliberate way to be reached from a test.

4. The screen that kept reloading

This one was my own doing, and only the recording showed it. Every time the keyboard opened on the sign-in screen, the loading screen flashed over the form and the text I had typed disappeared.

I had just added a soft cross-fade between the loading, sign-in and app screens. The gate behind them asked Firebase "who is signed in?" inside its build method, which creates a new stream on every rebuild, and a new stream starts out as "waiting", which the app shows as the loading screen. The keyboard animation triggered a rebuild on every frame, and the new cross-fade made each of those moments visible.

// Before: a new stream every time the screen rebuilds StreamBuilder<User?>(stream: FirebaseAuth.instance.authStateChanges(), ...) // After: asked once, kept for the life of the screen late final _auth = FirebaseAuth.instance.authStateChanges(); ... StreamBuilder<User?>(stream: _auth, ...) 

The more useful change was the test. It types into the form, moves the keyboard frame by frame, rebuilds the parent, and checks that the loading screen never appears and the text stays. Then I put the old code back for a minute to make sure the test fails against it. It did.

The sign-in screen in English and Greek, in the app's brand colours.

5. A login that only failed in the real build

Not a language problem, but the same lesson. The Google sign-in button failed on my phone with an error about a missing server client id. The Google plugin looks that value up in a string resource by name, and the release build's resource shrinker removes strings that look unused. My debug builds carry the string; the release build did not. The app now passes the id explicitly, and a test checks it against the Firebase configuration file.

And a sixth, while writing this

While taking the screenshots for this article I found one more. Arabic reads right to left, so the first tab sits on the right of the tab strip. Its highlight stayed on the left, under the wrong tab. It was a one-word fix (a direction-aware alignment instead of a fixed one), and a test now checks the highlight under every tab in both directions.

One screen in six of the 15 languages, including Arabic, which reads right to left.

What I changed in how I work

  • Tests mirror what a user sees. Every screen, every language, three screen and text-size combinations.
  • Every bug leaves a test behind, and where I can, I check that the test fails on the old code before I trust it.
  • A real-device pass stays in the loop, and it is a recording, not just screenshots. The flicker was invisible in a still image.
  • Translated text is product code. The English string is the key, and a script refuses any language table with a missing key or mismatched placeholders.

Questions I'm still thinking about

Translation quality is the part I can judge least. I read Greek and English; for the other thirteen languages the checks tell me the text is complete and that it fits, not that it sounds natural. If you speak one of them, I'd be curious whether the wording feels like a native app or like a translation.

The number on that orange card is the heart of the app, and I keep wondering what makes a person trust a figure like that. If you've ever tried to budget, what would you want to see behind it?

And on the engineering side: when did a real device last prove your test suite wrong, and what did you add afterwards?

The app is still in testing, so none of these decisions is final.

Disclosure: I drafted this article with the help of an AI assistant and edited it myself.