From 223bec409f783b7eecbcec2187e63cc7c2ca9b0c Mon Sep 17 00:00:00 2001 From: koniz-dev Date: Wed, 26 Aug 2026 19:52:13 +0700 Subject: [PATCH 1/3] fix(testing): make the E2E suite green on a fresh clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 koniz-dev/flutter-starter#33 --- integration_test/README.md | 17 ++++- integration_test/app_e2e_test.dart | 50 ++++++++++++--- integration_test/auth_flow_test.dart | 47 +++++++++++--- .../integration_test/app_e2e_test.dart | 64 ++++++++++++++----- .../integration_test/auth_flow_test.dart | 48 ++++++++++++-- .../integration_test/app_e2e_test.dart | 48 +++++++++++--- .../integration_test/auth_flow_test.dart | 47 +++++++++++--- .../integration_test/app_e2e_test.dart | 50 ++++++++++++--- .../integration_test/auth_flow_test.dart | 47 +++++++++++--- 9 files changed, 341 insertions(+), 77 deletions(-) diff --git a/integration_test/README.md b/integration_test/README.md index b9643c0..ed63147 100644 --- a/integration_test/README.md +++ b/integration_test/README.md @@ -29,7 +29,20 @@ patrol test --target integration_test/app_e2e_test.dart Do not rely on translated button labels for critical steps. Use `ValueKey`s from [`lib/core/constants/ui_keys.dart`](../lib/core/constants/ui_keys.dart) (e.g. `e2e_login_submit`, `e2e_home_content`). -`app_e2e_test.dart` covers **auth → home** and nothing else, in the full starter as well as after a strip. It asserts only `e2e_login_submit` and `e2e_home_content`. +Each file here holds **two** tests: + +1. **A smoke test that always runs** — the app boots to a usable login screen. No backend needed. It is not a token test: reaching a rendered login screen exercises the native Patrol harness, app bootstrap, the Riverpod scope, the router, and localization. +2. **The authenticated flow, skipped by default** — everything past login. The sample auth flow POSTs to `BASE_URL`; with no server the login call fails, the app stays on the login screen, and those assertions are unreachable. Rather than ship a suite that is red by construction, they are gated: + +```bash +# skipped (default) — the suite is green on a fresh clone +patrol test --target integration_test/app_e2e_test.dart + +# run the authenticated flow, once you have an API reachable from the device +patrol test --target integration_test/app_e2e_test.dart --dart-define=E2E_BACKEND=true +``` + +A skipped test still appears in the summary (`⏩ Skipped: 1`), so it cannot be quietly forgotten. **There is no tasks coverage, on purpose.** `UiKeys.openTasks`, `UiKeys.tasksFab`, and `UiKeys.addTaskSubmit` are declared but attached to no widget in `lib/`: `HomeScreen` is a deliberately minimal shell with no entry point into the sample `tasks` feature. Patrol matches on the widget tree, so a selector written against an unattached key finds nothing — attach the key first if your fork adds that entry point. `tool/golden/no_feature_flags/` shows the wiring, and its own `app_e2e_test.dart` does drive the full tasks flow. @@ -37,6 +50,8 @@ Do not rely on translated button labels for critical steps. Use `ValueKey`s from ## CI +The workflow fails the job when the summary reports `Total: 0` or a non-zero `Failed:` count. Both were real false-green paths: `patrol test` exits 0 on an empty test set, and piping its output without `pipefail` swallowed a genuine failure. A green run here is now meaningful. + Patrol does **not** run on every PR by default. Use **GitHub Actions → E2E Android (Patrol) → Run workflow** (see [`.github/workflows/e2e-android.yml`](../.github/workflows/e2e-android.yml)). You may need a reachable API if login hits the network. ## More context diff --git a/integration_test/app_e2e_test.dart b/integration_test/app_e2e_test.dart index b72fb3d..ea4c5b8 100644 --- a/integration_test/app_e2e_test.dart +++ b/integration_test/app_e2e_test.dart @@ -3,20 +3,50 @@ import 'package:flutter_starter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('E2E: auth -> home (stable ValueKeys)', ( - $, - ) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); - if ($(#e2e_login_submit).exists) { - await $(TextField).at(0).enterText('test@example.com'); - await $(TextField).at(1).enterText('password123'); - await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); - } + expect($(#e2e_login_submit), findsOneWidget); - expect($(#e2e_home_content), findsOneWidget); + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); }); + + patrolTest( + 'E2E: auth -> home (requires a reachable backend)', + ($) async { + app.main(); + await $.pumpAndSettle(); + + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } + + expect($(#e2e_home_content), findsOneWidget); + }, + skip: !hasBackend, + ); } diff --git a/integration_test/auth_flow_test.dart b/integration_test/auth_flow_test.dart index d9ffc7f..1fcaef4 100644 --- a/integration_test/auth_flow_test.dart +++ b/integration_test/auth_flow_test.dart @@ -3,21 +3,50 @@ import 'package:flutter_starter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('auth flow: enters credentials and reaches home', ($) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); - if ($(#e2e_login_submit).exists) { - expect($(#e2e_login_submit), findsOneWidget); + expect($(#e2e_login_submit), findsOneWidget); - await $(TextField).at(0).enterText('test@example.com'); - await $(TextField).at(1).enterText('password123'); + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); + }); - await $(#e2e_login_submit).tap(); + patrolTest( + 'auth flow: credentials reach home (requires a reachable backend)', + ($) async { + app.main(); await $.pumpAndSettle(); - } - expect($(#e2e_home_content), findsOneWidget); - }); + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } + + expect($(#e2e_home_content), findsOneWidget); + }, + skip: !hasBackend, + ); } diff --git a/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart b/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart index ce7b5a0..3bb981c 100644 --- a/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart +++ b/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart @@ -3,30 +3,60 @@ import 'package:flutter_starter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('E2E: Auth -> open tasks -> create task (uses stable ValueKeys)', ( - $, - ) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); - if ($(#e2e_login_submit).exists) { - await $(TextField).at(0).enterText('test@example.com'); - await $(TextField).at(1).enterText('password123'); - await $(#e2e_login_submit).tap(); + expect($(#e2e_login_submit), findsOneWidget); + + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); + }); + + patrolTest( + 'E2E: auth -> open tasks -> create task (requires a reachable backend)', + ($) async { + app.main(); await $.pumpAndSettle(); - } - await $(#e2e_open_tasks).tap(); - await $.pumpAndSettle(); + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } - await $(#e2e_tasks_fab).tap(); - await $.pumpAndSettle(); + await $(#e2e_open_tasks).tap(); + await $.pumpAndSettle(); - await $(TextField).first.enterText('Patrol Automated Task'); - await $(#e2e_add_task_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_tasks_fab).tap(); + await $.pumpAndSettle(); - expect($('Patrol Automated Task'), findsWidgets); - }); + await $(TextField).first.enterText('Patrol Automated Task'); + await $(#e2e_add_task_submit).tap(); + await $.pumpAndSettle(); + + expect($('Patrol Automated Task'), findsWidgets); + }, + skip: !hasBackend, + ); } diff --git a/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart b/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart index bf0bb93..1fcaef4 100644 --- a/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart +++ b/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart @@ -1,12 +1,52 @@ -// Import main app package +import 'package:flutter/material.dart'; import 'package:flutter_starter/main.dart' as app; +import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('auth flow: enters credentials and navigates to dashboard', ( - $, - ) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); + + expect($(#e2e_login_submit), findsOneWidget); + + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); }); + + patrolTest( + 'auth flow: credentials reach home (requires a reachable backend)', + ($) async { + app.main(); + await $.pumpAndSettle(); + + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } + + expect($(#e2e_home_content), findsOneWidget); + }, + skip: !hasBackend, + ); } diff --git a/tool/golden/no_tasks/integration_test/app_e2e_test.dart b/tool/golden/no_tasks/integration_test/app_e2e_test.dart index 175f6e0..02eba21 100644 --- a/tool/golden/no_tasks/integration_test/app_e2e_test.dart +++ b/tool/golden/no_tasks/integration_test/app_e2e_test.dart @@ -3,18 +3,50 @@ import 'package:flutter_starter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('E2E: Auth -> home (no tasks; stable ValueKeys)', ($) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); - if ($(#e2e_login_submit).exists) { - await $(TextField).at(0).enterText('test@example.com'); - await $(TextField).at(1).enterText('password123'); - await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); - } + expect($(#e2e_login_submit), findsOneWidget); - expect($(#e2e_home_content), findsOneWidget); + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); }); + + patrolTest( + 'E2E: auth -> home, no tasks (requires a reachable backend)', + ($) async { + app.main(); + await $.pumpAndSettle(); + + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } + + expect($(#e2e_home_content), findsOneWidget); + }, + skip: !hasBackend, + ); } diff --git a/tool/golden/no_tasks/integration_test/auth_flow_test.dart b/tool/golden/no_tasks/integration_test/auth_flow_test.dart index d9ffc7f..1fcaef4 100644 --- a/tool/golden/no_tasks/integration_test/auth_flow_test.dart +++ b/tool/golden/no_tasks/integration_test/auth_flow_test.dart @@ -3,21 +3,50 @@ import 'package:flutter_starter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('auth flow: enters credentials and reaches home', ($) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); - if ($(#e2e_login_submit).exists) { - expect($(#e2e_login_submit), findsOneWidget); + expect($(#e2e_login_submit), findsOneWidget); - await $(TextField).at(0).enterText('test@example.com'); - await $(TextField).at(1).enterText('password123'); + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); + }); - await $(#e2e_login_submit).tap(); + patrolTest( + 'auth flow: credentials reach home (requires a reachable backend)', + ($) async { + app.main(); await $.pumpAndSettle(); - } - expect($(#e2e_home_content), findsOneWidget); - }); + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } + + expect($(#e2e_home_content), findsOneWidget); + }, + skip: !hasBackend, + ); } diff --git a/tool/golden/stripped/integration_test/app_e2e_test.dart b/tool/golden/stripped/integration_test/app_e2e_test.dart index 78552ce..565b49a 100644 --- a/tool/golden/stripped/integration_test/app_e2e_test.dart +++ b/tool/golden/stripped/integration_test/app_e2e_test.dart @@ -3,20 +3,50 @@ import 'package:flutter_starter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('E2E: Auth -> home (stripped starter; stable ValueKeys)', ( - $, - ) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); - if ($(#e2e_login_submit).exists) { - await $(TextField).at(0).enterText('test@example.com'); - await $(TextField).at(1).enterText('password123'); - await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); - } + expect($(#e2e_login_submit), findsOneWidget); - expect($(#e2e_home_content), findsOneWidget); + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); }); + + patrolTest( + 'E2E: auth -> home, stripped starter (requires a reachable backend)', + ($) async { + app.main(); + await $.pumpAndSettle(); + + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } + + expect($(#e2e_home_content), findsOneWidget); + }, + skip: !hasBackend, + ); } diff --git a/tool/golden/stripped/integration_test/auth_flow_test.dart b/tool/golden/stripped/integration_test/auth_flow_test.dart index d9ffc7f..1fcaef4 100644 --- a/tool/golden/stripped/integration_test/auth_flow_test.dart +++ b/tool/golden/stripped/integration_test/auth_flow_test.dart @@ -3,21 +3,50 @@ import 'package:flutter_starter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +/// Whether a reachable backend is configured for this run. +/// +/// Pass `--dart-define=E2E_BACKEND=true` when you have an API. The sample auth +/// flow POSTs to `BASE_URL`; with no server the login call fails, the app stays +/// on the login screen, and every assertion past login is unreachable. Those +/// assertions are skipped rather than shipped permanently red - a starter whose +/// E2E suite is red by construction teaches people to ignore it. +const hasBackend = bool.fromEnvironment('E2E_BACKEND'); + +/// Enters the sample credentials and submits the login form. +Future _submitLogin(PatrolIntegrationTester $) async { + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + await $(#e2e_login_submit).tap(); + await $.pumpAndSettle(); +} + void main() { - patrolTest('auth flow: enters credentials and reaches home', ($) async { + // Runs everywhere, including with no backend. Not a token test: reaching a + // rendered login screen exercises the native Patrol harness, app bootstrap, + // the Riverpod scope, the router, and localization. + patrolTest('E2E: app boots to a usable login screen', ($) async { app.main(); await $.pumpAndSettle(); - if ($(#e2e_login_submit).exists) { - expect($(#e2e_login_submit), findsOneWidget); + expect($(#e2e_login_submit), findsOneWidget); - await $(TextField).at(0).enterText('test@example.com'); - await $(TextField).at(1).enterText('password123'); + await $(TextField).at(0).enterText('test@example.com'); + await $(TextField).at(1).enterText('password123'); + expect($('test@example.com'), findsOneWidget); + }); - await $(#e2e_login_submit).tap(); + patrolTest( + 'auth flow: credentials reach home (requires a reachable backend)', + ($) async { + app.main(); await $.pumpAndSettle(); - } - expect($(#e2e_home_content), findsOneWidget); - }); + if ($(#e2e_login_submit).exists) { + await _submitLogin($); + } + + expect($(#e2e_home_content), findsOneWidget); + }, + skip: !hasBackend, + ); } From 36e29986d124afc0d664180555ed44989ad8f8cb Mon Sep 17 00:00:00 2001 From: koniz-dev Date: Wed, 26 Aug 2026 20:35:20 +0700 Subject: [PATCH 2/3] fix(testing): make main() awaitable so E2E can wait for startup 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, 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 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 koniz-dev/flutter-starter#33 --- integration_test/app_e2e_test.dart | 5 +++-- integration_test/auth_flow_test.dart | 5 +++-- lib/main.dart | 6 +++++- .../no_feature_flags/integration_test/app_e2e_test.dart | 5 +++-- .../no_feature_flags/integration_test/auth_flow_test.dart | 5 +++-- tool/golden/no_feature_flags/lib/main.dart | 6 +++++- tool/golden/no_tasks/integration_test/app_e2e_test.dart | 5 +++-- tool/golden/no_tasks/integration_test/auth_flow_test.dart | 5 +++-- tool/golden/no_tasks/lib/main.dart | 6 +++++- tool/golden/stripped/integration_test/app_e2e_test.dart | 5 +++-- tool/golden/stripped/integration_test/auth_flow_test.dart | 5 +++-- tool/golden/stripped/lib/main.dart | 6 +++++- 12 files changed, 44 insertions(+), 20 deletions(-) diff --git a/integration_test/app_e2e_test.dart b/integration_test/app_e2e_test.dart index ea4c5b8..bf4b17d 100644 --- a/integration_test/app_e2e_test.dart +++ b/integration_test/app_e2e_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'E2E: auth -> home (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/integration_test/auth_flow_test.dart b/integration_test/auth_flow_test.dart index 1fcaef4..50917a6 100644 --- a/integration_test/auth_flow_test.dart +++ b/integration_test/auth_flow_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/lib/main.dart b/lib/main.dart index 9137c6b..172eeb7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,11 @@ import 'package:flutter_starter/core/routing/app_router.dart'; import 'package:flutter_starter/l10n/app_localizations.dart'; import 'package:flutter_starter/shared/theme/app_theme.dart'; -void main() async { +// Returns a Future so callers can await startup. `void main() async` would +// discard it: integration tests could not wait for runApp, and any error +// thrown by the awaits below would surface as an unhandled async error +// instead of propagating. +Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Future.wait([EnvConfig.load(), _initializeImageCache()]); diff --git a/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart b/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart index 3bb981c..845d7e1 100644 --- a/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart +++ b/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'E2E: auth -> open tasks -> create task (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart b/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart index 1fcaef4..50917a6 100644 --- a/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart +++ b/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/tool/golden/no_feature_flags/lib/main.dart b/tool/golden/no_feature_flags/lib/main.dart index c35649a..e21d614 100644 --- a/tool/golden/no_feature_flags/lib/main.dart +++ b/tool/golden/no_feature_flags/lib/main.dart @@ -13,7 +13,11 @@ import 'package:flutter_starter/core/routing/app_router.dart'; import 'package:flutter_starter/l10n/app_localizations.dart'; import 'package:flutter_starter/shared/theme/app_theme.dart'; -void main() async { +// Returns a Future so callers can await startup. `void main() async` would +// discard it: integration tests could not wait for runApp, and any error +// thrown by the awaits below would surface as an unhandled async error +// instead of propagating. +Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Future.wait([EnvConfig.load(), _initializeImageCache()]); diff --git a/tool/golden/no_tasks/integration_test/app_e2e_test.dart b/tool/golden/no_tasks/integration_test/app_e2e_test.dart index 02eba21..3717620 100644 --- a/tool/golden/no_tasks/integration_test/app_e2e_test.dart +++ b/tool/golden/no_tasks/integration_test/app_e2e_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'E2E: auth -> home, no tasks (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/tool/golden/no_tasks/integration_test/auth_flow_test.dart b/tool/golden/no_tasks/integration_test/auth_flow_test.dart index 1fcaef4..50917a6 100644 --- a/tool/golden/no_tasks/integration_test/auth_flow_test.dart +++ b/tool/golden/no_tasks/integration_test/auth_flow_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/tool/golden/no_tasks/lib/main.dart b/tool/golden/no_tasks/lib/main.dart index 5387f05..8eac475 100644 --- a/tool/golden/no_tasks/lib/main.dart +++ b/tool/golden/no_tasks/lib/main.dart @@ -14,7 +14,11 @@ import 'package:flutter_starter/features/feature_flags/presentation/providers/fe import 'package:flutter_starter/l10n/app_localizations.dart'; import 'package:flutter_starter/shared/theme/app_theme.dart'; -void main() async { +// Returns a Future so callers can await startup. `void main() async` would +// discard it: integration tests could not wait for runApp, and any error +// thrown by the awaits below would surface as an unhandled async error +// instead of propagating. +Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Future.wait([EnvConfig.load(), _initializeImageCache()]); diff --git a/tool/golden/stripped/integration_test/app_e2e_test.dart b/tool/golden/stripped/integration_test/app_e2e_test.dart index 565b49a..aa50f74 100644 --- a/tool/golden/stripped/integration_test/app_e2e_test.dart +++ b/tool/golden/stripped/integration_test/app_e2e_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'E2E: auth -> home, stripped starter (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/tool/golden/stripped/integration_test/auth_flow_test.dart b/tool/golden/stripped/integration_test/auth_flow_test.dart index 1fcaef4..50917a6 100644 --- a/tool/golden/stripped/integration_test/auth_flow_test.dart +++ b/tool/golden/stripped/integration_test/auth_flow_test.dart @@ -25,9 +25,10 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); + await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); await $(TextField).at(0).enterText('test@example.com'); @@ -38,7 +39,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - app.main(); + await app.main(); await $.pumpAndSettle(); if ($(#e2e_login_submit).exists) { diff --git a/tool/golden/stripped/lib/main.dart b/tool/golden/stripped/lib/main.dart index 9137c6b..172eeb7 100644 --- a/tool/golden/stripped/lib/main.dart +++ b/tool/golden/stripped/lib/main.dart @@ -13,7 +13,11 @@ import 'package:flutter_starter/core/routing/app_router.dart'; import 'package:flutter_starter/l10n/app_localizations.dart'; import 'package:flutter_starter/shared/theme/app_theme.dart'; -void main() async { +// Returns a Future so callers can await startup. `void main() async` would +// discard it: integration tests could not wait for runApp, and any error +// thrown by the awaits below would surface as an unhandled async error +// instead of propagating. +Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Future.wait([EnvConfig.load(), _initializeImageCache()]); From 69513f0f64a23f63ef1d0d7ecf48610527fb030e Mon Sep 17 00:00:00 2001 From: koniz-dev Date: Wed, 26 Aug 2026 21:10:23 +0700 Subject: [PATCH 3/3] fix(testing): drop pumpAndSettle from the E2E tests 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 koniz-dev/flutter-starter#33 --- integration_test/app_e2e_test.dart | 20 ++++++++++---- integration_test/auth_flow_test.dart | 20 ++++++++++---- .../integration_test/app_e2e_test.dart | 26 +++++++++++++------ .../integration_test/auth_flow_test.dart | 20 ++++++++++---- .../integration_test/app_e2e_test.dart | 20 ++++++++++---- .../integration_test/auth_flow_test.dart | 20 ++++++++++---- .../integration_test/app_e2e_test.dart | 20 ++++++++++---- .../integration_test/auth_flow_test.dart | 20 ++++++++++---- 8 files changed, 123 insertions(+), 43 deletions(-) diff --git a/integration_test/app_e2e_test.dart b/integration_test/app_e2e_test.dart index bf4b17d..ab2c6b2 100644 --- a/integration_test/app_e2e_test.dart +++ b/integration_test/app_e2e_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,8 +50,7 @@ void main() { patrolTest( 'E2E: auth -> home (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($); diff --git a/integration_test/auth_flow_test.dart b/integration_test/auth_flow_test.dart index 50917a6..1220af2 100644 --- a/integration_test/auth_flow_test.dart +++ b/integration_test/auth_flow_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,8 +50,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($); diff --git a/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart b/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart index 845d7e1..5accab3 100644 --- a/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart +++ b/tool/golden/no_feature_flags/integration_test/app_e2e_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,22 +50,21 @@ void main() { patrolTest( 'E2E: auth -> open tasks -> create task (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($); } await $(#e2e_open_tasks).tap(); - await $.pumpAndSettle(); + await $(#e2e_tasks_fab).waitUntilVisible(); await $(#e2e_tasks_fab).tap(); - await $.pumpAndSettle(); + await $(#e2e_add_task_submit).waitUntilVisible(); await $(TextField).first.enterText('Patrol Automated Task'); await $(#e2e_add_task_submit).tap(); - await $.pumpAndSettle(); + await $('Patrol Automated Task').waitUntilVisible(); expect($('Patrol Automated Task'), findsWidgets); }, diff --git a/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart b/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart index 50917a6..1220af2 100644 --- a/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart +++ b/tool/golden/no_feature_flags/integration_test/auth_flow_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,8 +50,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($); diff --git a/tool/golden/no_tasks/integration_test/app_e2e_test.dart b/tool/golden/no_tasks/integration_test/app_e2e_test.dart index 3717620..3cb9a32 100644 --- a/tool/golden/no_tasks/integration_test/app_e2e_test.dart +++ b/tool/golden/no_tasks/integration_test/app_e2e_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,8 +50,7 @@ void main() { patrolTest( 'E2E: auth -> home, no tasks (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($); diff --git a/tool/golden/no_tasks/integration_test/auth_flow_test.dart b/tool/golden/no_tasks/integration_test/auth_flow_test.dart index 50917a6..1220af2 100644 --- a/tool/golden/no_tasks/integration_test/auth_flow_test.dart +++ b/tool/golden/no_tasks/integration_test/auth_flow_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,8 +50,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($); diff --git a/tool/golden/stripped/integration_test/app_e2e_test.dart b/tool/golden/stripped/integration_test/app_e2e_test.dart index aa50f74..a5e4edf 100644 --- a/tool/golden/stripped/integration_test/app_e2e_test.dart +++ b/tool/golden/stripped/integration_test/app_e2e_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,8 +50,7 @@ void main() { patrolTest( 'E2E: auth -> home, stripped starter (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($); diff --git a/tool/golden/stripped/integration_test/auth_flow_test.dart b/tool/golden/stripped/integration_test/auth_flow_test.dart index 50917a6..1220af2 100644 --- a/tool/golden/stripped/integration_test/auth_flow_test.dart +++ b/tool/golden/stripped/integration_test/auth_flow_test.dart @@ -12,12 +12,24 @@ import 'package:patrol/patrol.dart'; /// E2E suite is red by construction teaches people to ignore it. const hasBackend = bool.fromEnvironment('E2E_BACKEND'); +/// Boots the app and waits for the first frame. +/// +/// Deliberately does NOT use `pumpAndSettle`. On a real device it times out +/// whenever the tree never reaches an idle frame, which is what happened here: +/// `pumpAndSettle timed out` after 131s with the app running fine. Patrol's +/// `waitUntilVisible` polls instead of demanding quiescence, so it tolerates +/// any ongoing platform or animation activity. +Future _boot(PatrolIntegrationTester $) async { + await app.main(); + await $.pump(const Duration(seconds: 1)); +} + /// Enters the sample credentials and submits the login form. Future _submitLogin(PatrolIntegrationTester $) async { await $(TextField).at(0).enterText('test@example.com'); await $(TextField).at(1).enterText('password123'); await $(#e2e_login_submit).tap(); - await $.pumpAndSettle(); + await $(#e2e_home_content).waitUntilVisible(); } void main() { @@ -25,8 +37,7 @@ void main() { // rendered login screen exercises the native Patrol harness, app bootstrap, // the Riverpod scope, the router, and localization. patrolTest('E2E: app boots to a usable login screen', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); await $(#e2e_login_submit).waitUntilVisible(); expect($(#e2e_login_submit), findsOneWidget); @@ -39,8 +50,7 @@ void main() { patrolTest( 'auth flow: credentials reach home (requires a reachable backend)', ($) async { - await app.main(); - await $.pumpAndSettle(); + await _boot($); if ($(#e2e_login_submit).exists) { await _submitLogin($);