Skip to content

fix(testing): make the E2E suite green on a fresh clone - #36

Merged
koniz-dev merged 3 commits into
mainfrom
fix/e2e-green-without-backend
Aug 26, 2026
Merged

fix(testing): make the E2E suite green on a fresh clone#36
koniz-dev merged 3 commits into
mainfrom
fix/e2e-green-without-backend

Conversation

@koniz-dev

Copy link
Copy Markdown
Owner

With the native harness working (#30), the shipped test failed for real:

📝 Total: 1   ✅ Successful: 0   ❌ Failed: 1
Expected: exactly one matching candidate
[<'e2e_home_content'>]: []      app_e2e_test.dart:20

It taps login and asserts e2e_home_content, but the sample auth flow POSTs to
BASE_URL and no server exists in CI or on a fresh clone. Every fork inherited
a red run.
A starter whose E2E suite is red by construction teaches people to
ignore it - which is exactly how the four-month-red main in #11 went unnoticed.

Each E2E file now holds two tests

1. Smoke, always runs, no backend. The app boots to a usable login screen and
the form accepts input. Not a token test - reaching a rendered login screen
exercises the native Patrol harness, app bootstrap, the Riverpod scope, the
router, and localization. If any of those break, this goes red.

2. The authenticated flow, skipped by default.

const hasBackend = bool.fromEnvironment('E2E_BACKEND');

patrolTest('E2E: auth -> home (requires a reachable backend)', ($) async {
  ...
}, skip: !hasBackend);
patrol test --target integration_test/app_e2e_test.dart                              # skipped
patrol test --target integration_test/app_e2e_test.dart --dart-define=E2E_BACKEND=true  # runs

Skipping rather than deleting keeps it visible as ⏩ Skipped: 1 in the summary,
so the gap is legible instead of forgotten.

Golden tree kept in lockstep

All six tool/golden/{stripped,no_tasks,no_feature_flags}/integration_test/
counterparts got the same treatment, per the CLAUDE.md rule that changing a file
with a tool/golden/* counterpart means updating the counterpart too.
no_feature_flags keeps its richer auth -> open tasks -> create task flow, behind
the same gate.

clearPackageData = true (added in #30) makes the smoke test deterministic: each
run starts with no persisted session, so the app reliably routes to login.

Verification of criteria 1 and 2 is a dispatched E2E run, reported on the issue.

Refs #33

With the native harness working (#30), the shipped test failed for real: it
taps login and asserts e2e_home_content, but the sample auth flow POSTs to
BASE_URL and no server exists in CI or on a fresh clone. Every fork inherited
a red run. A starter whose E2E suite is red by construction teaches people to
ignore it - which is how the four-month-red main in #11 happened.

Each E2E file now holds two tests:

- A smoke test that always runs: the app boots to a usable login screen and
  the form accepts input. Backend-free, but not a token test - reaching a
  rendered login screen exercises the native Patrol harness, app bootstrap,
  the Riverpod scope, the router, and localization.
- The authenticated flow, gated on `skip: !hasBackend` where
  `hasBackend = bool.fromEnvironment('E2E_BACKEND')`. Teams with an API run
  `patrol test --dart-define=E2E_BACKEND=true`.

Skipping keeps the flow visible: it reports as `⏩ Skipped: 1` rather than
disappearing, so nobody forgets it exists.

Applied to both root files and all six tool/golden/*/integration_test/
counterparts, so strip-smoke stays consistent per the CLAUDE.md rule.
no_feature_flags keeps its richer auth -> tasks -> create-task flow behind the
same gate.

Refs #33
The first attempt still failed, and the reason was more interesting than the
symptom. The smoke test reported "Found 0 widgets with key", so I checked
whether the login button renders conditionally - it does not - and then found
the real cause:

  void main() async { ... }

`main()` returns **void**, not Future<void>, so the future produced by its four
startup awaits (EnvConfig.load, storage init, migrations, saved locale) is
unreachable. `app.main()` in a test could not be awaited at all: pumpAndSettle
settled an EMPTY widget tree and the assertion ran before runApp was called.
The original test hid this behind `if ($(#e2e_login_submit).exists)`, so it
looked like an auth failure when the app had not started.

Changed to `Future<void> main() async`, which is the correct signature for an
async entrypoint and is what Flutter supports. Two benefits beyond the tests:
an error thrown by those awaits now propagates instead of becoming an
unhandled async error.

The tests now `await app.main()` and additionally poll with
`$(#e2e_login_submit).waitUntilVisible()` rather than trusting a single
pumpAndSettle on a cold emulator.

Applied to lib/main.dart and all three tool/golden/*/lib/main.dart
counterparts, plus all eight integration_test files.

Refs #33
The re-run got further and failed differently, which named the real cause:

  pumpAndSettle timed out
  at app_e2e_test.dart:29  (the await $.pumpAndSettle() after await app.main())

The smoke test ran 131s rather than 13s, so awaiting main() worked - the app
boots. `pumpAndSettle` demands an idle frame, and on a real device this tree
never reaches one, so it throws before any assertion runs.

Ruled out by reading, not guessed: `isLoading` defaults to false so no spinner
renders on login, lib/ contains no AnimationController and no Timer.periodic
outside Debouncer, and the auth notifier's build() returns a const state
without a network call.

So the tests no longer use pumpAndSettle at all. `waitUntilVisible` polls
instead of demanding quiescence, which is the primitive Patrol provides for
exactly this, and each tap now waits on its next target rather than on global
idleness. Applied to both root files and all six golden counterparts.

Why the tree never settles is a separate question worth answering - it will
bite anyone writing a widget test against the full app - and is filed
separately rather than guessed at here.

Refs #33
@koniz-dev
koniz-dev merged commit 40839dc into main Aug 26, 2026
3 checks passed
@koniz-dev
koniz-dev deleted the fix/e2e-green-without-backend branch August 26, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant