Skip to content

Unable to complete any iOS UI tests that lasts longer than ~6 minutes #145143

Open

Description

@martinverhey

Steps to reproduce

Add the following to pubspec.yaml:

  integration_test:
    sdk: flutter

Setup project for UI testing:
https://github.com/flutter/flutter/tree/main/packages/integration_test#ios-device-testing

Add a UI test that takes longer than 6 minutes to run. I provided a code sample for this.

TEST CASE:
Use Xcode to run the UI tests on a physical device.

The test will run for a while but exit after a certain time before completion. In my test cases it always happened around 5 minutes and 43 seconds.

My tests consist of a simple loop of pressing a button and increasing the count with await Future.delayed(const Duration(seconds: 1)); within the loop.
But our production tests show the same result. Unrelated to what tests run and in what order it always interrupts around that time mark.

Notes:

  • It works on older Xcode versions (14.2 for example)
  • It works when running through Flutter with debug or run from vscode
  • It works on a simulator from Xcode or Flutter in vscode
  • It works on Xcode 15 (or later) while using Swift for a simple UI test with a similar setup.
  • It does not work on Xcode 15 (or later) from a Flutter project on a physical device
    That's why I think it's Flutter related.

Expected results

Test completes after 600 prints.

Actual results

Test fails after ~330 prints (somewhere before 6 minutes)

Code sample

Code sample

main.dart (default generated file, comments removed)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Add custom UI test file widget_test.dart in /integration_test with the following content:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:bug/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(const MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Verify that our counter has incremented.
    for (var i = 0; i < 600; i++) {
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();
      debugPrint("${i + 1}");

      await Future.delayed(const Duration(seconds: 1));

      expect(find.text((i + 1).toString()), findsOneWidget);
    }
  });
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs

This is the output in text from .xcresult

Runner (3702) encountered an error
Test runner never began executing tests after launching

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.19.3, on macOS 14.2.1 23C71 darwin-arm64, locale en-NL)
    • Flutter version 3.19.3 on channel stable at /Users/myname/Developer/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ba39319843 (7 days ago), 2024-03-07 15:22:21 -0600
    • Engine revision 2e4ba9c6fb
    • Dart version 3.3.1
    • DevTools version 2.31.1

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
    • Android SDK at /Users/myname/Library/Android/sdk
    • Platform android-34, build-tools 33.0.2
    • ANDROID_HOME = /Users/myname/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.15.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)

[✓] VS Code (version 1.87.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.84.0

[✓] Connected device (4 available)
    • MartiniPhone Pro (mobile) • _hidden_            • ios            • iOS 17.4 21E219
    • iPhone 15 Pro (mobile)    • 97C25645-A5DD-4BAC-8646-2AFED3D23A94 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-17-4 (simulator)
    • macOS (desktop)           • macos                                • darwin-arm64   • macOS 14.2.1 23C71 darwin-arm64
    • Chrome (web)              • chrome                               • web-javascript • Google Chrome 122.0.6261.129

[✓] Network resources
    • All expected network resources are available.

• No issues found!

Activity

  1. added
    in triagePresently being triaged by the triage team
    on Mar 15, 2024
  2. huycozy commented on Mar 15, 2024

    @huycozy
    Member

    I can also reproduce this issue. The test runs on my physical device iPhone 7, iOS 15.8, but it unexpectedly quits when the counter (from sample code) is around 300.

    Also, there is an output file named: Hang (Spindump).txt from XCode; its content is only a text No samples:
    Screenshot 2024-03-15 at 14 34 51

    Hang (Spindump).txt

    The complete sample project: reproduce_ios_integration_test.zip

    flutter doctor -v (stable and master)
    [✓] Flutter (Channel stable, 3.19.3, on macOS 14.1 23B74 darwin-x64, locale en-VN)
        • Flutter version 3.19.3 on channel stable at /Users/huynq/Documents/GitHub/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision ba39319843 (6 hours ago), 2024-03-07 15:22:21 -0600
        • Engine revision 2e4ba9c6fb
        • Dart version 3.3.1
        • DevTools version 2.31.1
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
        • Android SDK at /Users/huynq/Library/Android/sdk
        • Platform android-34, build-tools 34.0.0
        • ANDROID_HOME = /Users/huynq/Library/Android/sdk
        • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 15.2)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 15C500b
        • CocoaPods version 1.15.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 2023.2)
        • Android Studio at /Applications/Android Studio.app/Contents
        • Flutter plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/6351-dart
        • android-studio-dir = /Applications/Android Studio.app/
        • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
    
    [✓] VS Code (version 1.87.0)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.84.0
    
    [✓] Connected device (3 available)
        • iPhone (mobile) • d9a94afe2b649fef56ba0bfeb052f0f2a7dae95e • ios            • iOS 15.8 19H370
        • macOS (desktop) • macos                                    • darwin-x64     • macOS 14.1 23B74 darwin-x64
        • Chrome (web)    • chrome                                   • web-javascript • Google Chrome 122.0.6261.112
    
    [✓] Network resources
        • All expected network resources are available.
    
    • No issues found!
    [!] Flutter (Channel master, 3.21.0-7.0.pre.46, on macOS 14.1 23B74 darwin-x64, locale en-VN)
        • Flutter version 3.21.0-7.0.pre.46 on channel master at /Users/huynq/Documents/GitHub/flutter_master
        ! Warning: `flutter` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/flutter, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path.
        ! Warning: `dart` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/dart, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path.
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 9b276be7fe (2 hours ago), 2024-03-14 21:46:36 -0400
        • Engine revision 995d8903e0
        • Dart version 3.4.0 (build 3.4.0-234.0.dev)
        • DevTools version 2.33.1
        • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
        • Android SDK at /Users/huynq/Library/Android/sdk
        • Platform android-34, build-tools 34.0.0
        • ANDROID_HOME = /Users/huynq/Library/Android/sdk
        • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 15.2)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 15C500b
        • CocoaPods version 1.15.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 2023.2)
        • Android Studio at /Applications/Android Studio.app/Contents
        • Flutter plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/6351-dart
        • android-studio-dir = /Applications/Android Studio.app/
        • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
    
    [✓] VS Code (version 1.87.2)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.84.0
    
    [✓] Connected device (4 available)
        • RMX2001 (mobile) • EUYTFEUSQSRGDA6D          • android-arm64  • Android 11 (API 30)
        • iPad (mobile)    • 00008103-000A1464346A201E • ios            • iOS 17.2 21C62
        • macOS (desktop)  • macos                     • darwin-x64     • macOS 14.1 23B74 darwin-x64
        • Chrome (web)     • chrome                    • web-javascript • Google Chrome 122.0.6261.129
    
    [✓] Network resources
        • All expected network resources are available.
    
    ! Doctor found issues in 1 category.
  3. added
    platform-iosiOS applications specifically
    frameworkflutter/packages/flutter repository. See also f: labels.
    f: integration_testThe flutter/packages/integration_test plugin
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-iosOwned by iOS platform team
    and removed
    in triagePresently being triaged by the triage team
    on Mar 15, 2024
  4. added
    P2Important issues not at the top of the work list
    triaged-iosTriaged by iOS platform team
    on Mar 20, 2024
  5. Yokoi-K commented on Mar 21, 2024

    @Yokoi-K

    I'm running into the exact same problem with my app.
    It crashes around the 6 minute mark.

    flutter doctor
    ❯ flutter doctor
    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel stable, 3.19.3, on macOS 14.3.1 23D60 darwin-arm64, locale ja-JP)
    [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
    [✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2022.1)
    [✓] VS Code (version 1.85.2)
    
  6. Keeeeen commented on Mar 21, 2024

    @Keeeeen

    I encountered the same issue with Xcode 15.2.
    It consistently crashes within 4 to 8 minutes.
    Is there any solution available?

    flutter doctor -v
    [✓] Flutter (Channel stable, 3.19.3, on macOS 14.4 23E214 darwin-arm64, locale ja-JP)
        • Flutter version 3.19.3 on channel stable at /Users/xxxxx/.asdf/installs/flutter/3.19.3-stable
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision ba39319843 (2 weeks ago), 2024-03-07 15:22:21 -0600
        • Engine revision 2e4ba9c6fb
        • Dart version 3.3.1
        • DevTools version 2.31.1
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
        • Android SDK at /Users/xxxxx/Library/Android/sdk
        • Platform android-34, build-tools 34.0.0
        • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 15.2)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 15C500b
        • CocoaPods version 1.15.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 2023.1)
        • Android Studio at /Applications/Android Studio.app/Contents
        • Flutter plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/6351-dart
        • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
    
    [✓] VS Code (version 1.87.2)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.84.0
    
    [✓] Connected device (3 available)
        • iPhone 15 Pro Max (mobile) • FFB3B1C0-F0C1-4FB5-AB84-5BF7E22A9DF1 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-17-2
          (simulator)
        • macOS (desktop)            • macos                                • darwin-arm64   • macOS 14.4 23E214 darwin-arm64
        • Chrome (web)               • chrome                               • web-javascript • Google Chrome 122.0.6261.129
        ! Error: Browsing on the local area network for This is a pen. Ensure the device is unlocked and attached with a cable or associated with the
          same local area network as this Mac.
          The device must be opted into Developer Mode to connect wirelessly. (code -27)
        ! Error: Browsing on the local area network for Y ios se3. Ensure the device is unlocked and attached with a cable or associated with the
          same local area network as this Mac.
          The device must be opted into Developer Mode to connect wirelessly. (code -27)
    
    [✓] Network resources
        • All expected network resources are available.
    
    • No issues found!
    
  7. ostk0069 commented on Mar 21, 2024

    @ostk0069

    this reproduces also in my environment.
    Maybe any updates in xctest for xcode 15 cause the issue?

  8. 7 remaining items

  9. ivard commented on Oct 22, 2024

    @ivard

    I experience the same problem. I tested it using Flutter 3.24.3. It doesn't make a difference whether I use XCode 15.4 or XCode 16.0. After ~6 minutes the tests stop without any warning or error message.

  10. ivard commented on Oct 25, 2024

    @ivard

    When the tests are pre-empted the last error message is Runner (528) encountered an error (Test runner never began executing tests after launching). It seems like the test framework thinks the test never started.

    In the XCode 15 changelog the following is mentioned:

    Fixed: For tests that are executed without parallelization, a watchdog timer is now in place that covers the period during which XCTestCase instances are constructed (i.e. the designated initializer -initWithInvocation: is called). Hangs and long delays that occur during this period cause the test harness to abort testing early and attach a spindump to the result bundle for further investigation. If you have expensive work that occurs during a test case initializer, this work should be moved to -setUp or otherwise refactored to occur during the test method itself. (104390801)

    My best guess would be that the bug is related to this change.

  11. Keeeeen commented on Nov 6, 2024

    @Keeeeen

    I'm having trouble updating the version of Xcode. Do you have any suggestions or workarounds?

  12. FujiKinaga commented on Nov 6, 2024

    @FujiKinaga

    If you are using flutterfire, XCode 15 or above is starting to become mandatory.
    This is a pretty serious state of affairs.

  13. khanhandpad commented on Nov 27, 2024

    @khanhandpad

    same here, should someone help us to resolve this, thanks

  14. Grodien commented on Mar 4, 2025

    @Grodien

    Same issue here, anyone found a working solution yet?

  15. bahrimootaz commented on Apr 17, 2025

    @bahrimootaz

    Same here.

    ➜  flutter doctor -v
    [✓] Flutter (Channel stable, 3.22.2, on macOS 15.3.2 24D81 darwin-arm64, locale en-DE)
        • Flutter version 3.22.2 on channel stable at ...
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 761747bfc5 (11 months ago), 2024-06-05 22:15:13 +0200
        • Engine revision edd8546116
        • Dart version 3.4.3
        • DevTools version 2.34.3
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
        • Android SDK at ....
        • Platform android-34, build-tools 32.1.0-rc1
        • ANDROID_HOME = .....
        • Java binary at: ...../java
        • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 16.2)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 16C5032a
        • CocoaPods version 1.15.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 2023.2)
        • Android Studio at /Applications/Android Studio.app/Contents
        • Flutter plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/6351-dart
        • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
    
    [✗] Cannot determine if IntelliJ is installed
        ✗ Directory listing failed
    
    [✓] VS Code (version 1.99.2)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.102.0
    
    [✓] Connected device (4 available)
        • ..
    
    [✓] Network resources
        • All expected network resources are available.
    
    ! Doctor found issues in 1 category.

    Test-Runner-2025.04.17_14-33-39-+0200.xcresult.zip

  16. MSRPRO commented on Jun 27, 2025

    @MSRPRO

    Same here with Xcode version 16.4 and Flutter version 3.29.3. Who should we tag to get this issue the attention it needs? Our iOS integration tests have been on hold for quite some time now, and we need to re-enable them ASAP.

  17. userException commented on Nov 19, 2025

    @userException

    Making this thread alive again. Still having the same issue with even with Xcode 16.3/26 + Flutter 3.35.7.

    Breaking down the tests into smaller chunks is not an option especially for a test cases where we do happy path of a full user journey, like signup, login, do some stuff, logout. It definitely is longer than 5 minutes and breaking this into smaller test cases beats the purpose of testing a happy path in one go without relaunches/re-logins.

    Please consider this high priority for production use cases using Flutter.

  18. abhishekbedi1432 commented on Dec 4, 2025

    @abhishekbedi1432

    We are facing similar issue in flutter release 3.34.0

  19. JKennedy24 commented on Jan 6, 2026

    @JKennedy24

    Has anyone tried with Xcode 26?

  20. smallTrogdor commented on Mar 19, 2026

    @smallTrogdor

    With the information from @ivard and by looking at the source code, I suspect the issue to be that during XCTestCase construction, the thread is blocked by the runLoop started here until all dart tests have been reported. Before Xcode 15, this was never a problem.

    Can the Flutter team confirm this and maybe move this to P1 consider this in the upcoming Swift migration? This issue preventing any integration tests lasting longer than 6 minutes on iOS has been open for > two years - @jmagman ? Thank you for all the previous work done!

    Can reproduce this on Flutter 3.41.4 and Xcode 26.3.

    EDIT
    After looking a bit more into past and current issues concerning this plugin, especially these ones #88013 and #71873 the picture became a bit clearer.
    I therefore updated my above statement and hope for a consideration of this issue when it is migrated to a Swift implementation.

  21. Grodien commented on Jun 8, 2026

    @Grodien

    For everyone still looking for a workaround. You can replace the RunnerTests.m implementation with the following code:

    @import XCTest;
    @import integration_test;
    @import ObjectiveC.runtime;
    @import UIKit;
    
    // Custom implementation replacing INTEGRATION_TEST_IOS_RUNNER macro.
    //
    // The default macro runs all Dart tests inside +testInvocations (the XCTestCase
    // construction phase). Xcode 15+ added a watchdog timer that kills tests still
    // in construction after ~6 minutes, causing long-running integration tests to
    // be terminated with "Test runner never began executing tests after launching".
    // See: https://github.com/flutter/flutter/issues/145143
    //
    // This implementation moves test execution into an actual test method, which is
    // not subject to the construction-phase watchdog. The trade-off is that all Dart
    // tests are reported as a single XCTest case instead of individual cases.
    
    @interface RunnerTests : XCTestCase
    @end
    
    @implementation RunnerTests
    
    - (void)testIntegrationTest {
      FLTIntegrationTestRunner *integrationTestRunner = [[FLTIntegrationTestRunner alloc] init];
    
      __block BOOL allTestsPassed = YES;
      __block NSMutableArray<NSString *> *failures = [[NSMutableArray alloc] init];
    
      [integrationTestRunner testIntegrationTestWithResults:^(SEL testSelector, BOOL success, NSString *failureMessage) {
        if (!success) {
          allTestsPassed = NO;
          NSString *name = NSStringFromSelector(testSelector);
          [failures addObject:[NSString stringWithFormat:@"%@: %@", name, failureMessage ?: @"(no message)"]];
        }
      }];
    
      NSDictionary<NSString *, UIImage *> *capturedScreenshotsByName = integrationTestRunner.capturedScreenshotsByName;
      [capturedScreenshotsByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, UIImage *screenshot, BOOL *stop) {
        XCTAttachment *attachment = [XCTAttachment attachmentWithImage:screenshot];
        attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
        if (name != nil) {
          attachment.name = name;
        }
        [self addAttachment:attachment];
      }];
    
      if (!allTestsPassed) {
        XCTFail(@"Flutter integration test failures:\n%@", [failures componentsJoinedByString:@"\n"]);
      }
    }
    
    @end
    
  22. Joaccko0 commented on Oct 1, 2026

    @Joaccko0

    Workaround: survive the construction-phase watchdog without losing per-test XCTest reporting

    Tested against a real, production CI test suite (6 modules, 35 testWidgets() tests, ~20 minutes of actual device run time) on BrowserStack App Automate.

    Confirming the root cause at the source level
    Reading packages/integration_test/ios/integration_test/Sources/integration_test/FLTIntegrationTestRunner.m:

    (void)testIntegrationTestWithResults:(FLTIntegrationTestResults)testResult {
      while (!integrationTestPlugin.testResults) {
        [NSRunLoop.currentRunLoop runUntilDate:...]; // polls every 1s
      }
      [integrationTestPlugin.testResults enumerateKeysAndObjectsUsingBlock:^(...) {
        testResult(NSSelectorFromString(testSelectorName), ...);
      }];
    }
    

    Two things this confirms:

    1. The Dart side does not stream results. IntegrationTestWidgetsFlutterBinding only invokes the native channel method allTestsFinished once, from its own tearDownAll, after every Dart test has already finished. testIntegrationTestWithResults: just polls until that single message has arrived, then replays the now-complete dictionary.
    2. This entire wait-and-replay happens inside +testInvocations, which is exactly the XCTestCase construction phase the Xcode 15+ watchdog quoted earlier in this issue applies to. There's nothing Flutter-specific about the 6-minute cutoff — any +testInvocations override that blocks that long will hit it, Flutter or not.

    So the fix has to move the blocking wait out of +testInvocations — which is also exactly what @Grodien's workaround does. The only reason it collapses to one result is that +testInvocations has no way to know in advance how many tests exist, or what to call them, without running them first.

    The idea: know the test names ahead of time
    IntegrationTestPlugin.testResults is a plain NSDictionary<NSString*, NSString*> keyed by the test's description string. That description is knowable statically — it's just the literal first argument to testWidgets(...) in your .dart source. Two things worth calling out:

    • It's only the testWidgets() string — not prefixed by the enclosing group(). flutter_test/lib/src/widget_tester.dart builds combinedDescription from the testWidgets() argument only and passes that straight to binding.runTest(description: combinedDescription). The group name only gets prepended by the console reporter for display; it's never part of the key results is stored under.
    • If two tests anywhere in the run share the exact same description string, Dart's own results[description] ??= _success will silently conflate them — this is a pre-existing property of integration_test, not something this workaround introduces. The generator below treats this as a hard error rather than letting it fail silently.

    Given that, the fix is:

    1. Build time: statically extract every testWidgets() description from your test sources, in source order, with simple comment-stripping (so disabled tests aren't counted).
    2. +testInvocations: return N invocations immediately, built from that static list — no Dart tests run yet, so this returns in milliseconds regardless of how long the actual suite will take.
    3. First invocation actually executed (this happens during normal test execution, which the construction-phase watchdog does not cover): do the same poll-until-testResults-exists wait, once, and cache the dictionary.
    4. Every invocation (the first one included) looks up its own known description in the cached dictionary and asserts pass/fail for real, with the original failure detail attached.

    Generator script

    Ruby, run at build time against your test source files, before the Xcode build. Needs only the standard library (kept it compatible back to the Ruby ~2.6 that ships on macOS, since that's commonly what's available on a bare CI runner without rbenv/rvm — Array#tally, 2.7+, bit me once here; group_by/select work everywhere).

    #!/usr/bin/env ruby
    # frozen_string_literal: true
    #
    # Usage: generate_runner_tests.rb <file1.dart> [file2.dart ...] > RunnerTests.m
    
    ENVIRONMENT_NAME = 'QA' # substitute any ${...}-interpolated env name your group()/testWidgets() strings use
    
    def strip_comments(source)
      source = source.gsub(%r{/\*.*?\*/}m, '')
      source.gsub(%r{//[^\n]*}, '')
    end
    
    def extract_descriptions(path)
      source = strip_comments(File.read(path, encoding: 'UTF-8'))
      descriptions = []
      current_group = nil
    
      # group()/testWidgets() may split the string onto the next line after the
      # opening paren -- \s*\n? accounts for that.
      source.scan(/(group|testWidgets)\s*\(\s*\n?\s*'((?:[^'\\]|\\.)*)'/m) do |kind, desc|
        desc = desc.gsub('${TestEnvConfig.name}', ENVIRONMENT_NAME)
        if kind == 'group'
          current_group = desc
        else
          raise "testWidgets() outside of a group() in #{path}: #{desc}" if current_group.nil?
    
          # IMPORTANT: do NOT prefix with current_group here. The real key in
          # IntegrationTestPlugin.testResults is only the testWidgets() string.
          descriptions << desc
        end
      end
      descriptions
    end
    
    def objc_escape(str)
      str.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
    end
    
    # Readable selector for XCTest/BrowserStack (e.g. "test00_Verify_widget_abc123").
    # A selector can't contain spaces or ":" (the ":" changes method arity), so
    # "_" is the closest legal approximation of word separation -- casing is left
    # exactly as it appears in the source. This name is ours -- it does not need
    # to match testCaseNameFromDartTestName:, because we never go through
    # FLTIntegrationTestRunner. It just needs to be a valid, unique Objective-C
    # identifier; the zero-padded index guarantees both even if two descriptions
    # sanitize to the same text.
    def selector_name(index, description, pad_width)
      words = description.split(/[^a-zA-Z0-9]+/).reject(&:empty?)
      readable = words.join('_')
      readable = readable[0, 80]
      "test#{index.to_s.rjust(pad_width, '0')}_#{readable}"
    end
    
    manifest = ARGV.flat_map { |path| extract_descriptions(path) }
    
    if manifest.empty?
      warn 'No testWidgets() found -- check the input paths.'
      exit 1
    end
    
    duplicates = manifest.group_by { |d| d }.select { |_, group| group.size > 1 }.keys
    unless duplicates.empty?
      warn 'Exact duplicate testWidgets() descriptions found across input files:'
      duplicates.each { |d| warn "  - #{d}" }
      warn 'IntegrationTestPlugin.testResults stores them under the SAME key (results[description] ??= ...),'
      warn 'so there is no way to tell which result belongs to which static test. Rename one of them.'
      exit 1
    end
    
    pad_width = manifest.size.to_s.length
    selectors = manifest.each_with_index.map { |desc, i| selector_name(i, desc, pad_width) }
    
    entries = manifest.map { |desc| "    @\"#{objc_escape(desc)}\"," }.join("\n")
    selector_entries = selectors.map { |sel| "    @\"#{sel}\"," }.join("\n")
    
    template = <<~OBJC
      // GENERATED by generate_runner_tests.rb -- DO NOT EDIT BY HAND.
      @import XCTest;
      @import integration_test;
      @import ObjectiveC.runtime;
    
      static NSArray<NSString *> *kExpectedTestDescriptions;
      static NSArray<NSString *> *kGeneratedSelectorNames;
      static NSDictionary<NSString *, NSString *> *kCachedResults;
    
      __attribute__((constructor))
      static void InitializeExpectedTestDescriptions(void) {
        kExpectedTestDescriptions = @[
      #{entries}
        ];
        kGeneratedSelectorNames = @[
      #{selector_entries}
        ];
      }
    
      @interface RunnerTestsManifest : NSObject
      + (void)runTestAtIndex:(NSUInteger)index onTestCase:(XCTestCase *)testCase;
      @end
    
      @implementation RunnerTestsManifest
    
      + (void)runTestAtIndex:(NSUInteger)index onTestCase:(XCTestCase *)testCase {
        if (kCachedResults == nil) {
          IntegrationTestPlugin *plugin = [IntegrationTestPlugin instance];
          // Same polling FLTIntegrationTestRunner.testIntegrationTestWithResults:
          // does, but reading testResults directly -- so the key stays the raw
          // Dart description, without going through testCaseNameFromDartTestName:.
          while (!plugin.testResults) {
            [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
          }
          kCachedResults = [plugin.testResults copy];
    
          if (kCachedResults.count != kExpectedTestDescriptions.count) {
            NSLog(@"[RunnerTestsManifest] WARNING: static manifest expected %lu tests "
                  @"but the run reported %lu -- the manifest is stale relative to the real .dart sources.",
                  (unsigned long)kExpectedTestDescriptions.count, (unsigned long)kCachedResults.count);
          }
        }
    
        NSString *description = kExpectedTestDescriptions[index];
        NSString *result = kCachedResults[description];
    
        if (result == nil) {
          XCTFail(@"No result found for static test #%lu (\\"%@\\"). "
                  @"The run may have aborted before reaching this test, or the "
                  @"manifest is out of date relative to the real .dart source.",
                  (unsigned long)index, description);
          return;
        }
    
        XCTAssertTrue([result isEqualToString:@"success"], @"%@", result);
      }
    
      @end
    
      @interface RunnerTests : XCTestCase
      @end
    
      @implementation RunnerTests
    
      + (NSArray<NSInvocation *> *)testInvocations {
        NSMutableArray<NSInvocation *> *invocations = [NSMutableArray array];
        for (NSUInteger i = 0; i < kExpectedTestDescriptions.count; i++) {
          SEL selector = NSSelectorFromString(kGeneratedSelectorNames[i]);
          IMP implementation = imp_implementationWithBlock(^(id _self) {
            [RunnerTestsManifest runTestAtIndex:i onTestCase:_self];
          });
          class_addMethod(self, selector, implementation, "v@:");
    
          NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
          NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
          invocation.selector = selector;
          [invocations addObject:invocation];
        }
        return invocations;
      }
    
      @end
    OBJC
    
    puts template
    

    Run it wherever you currently write RunnerTests.m (I generate it in the same CI step that previously did echo 'INTEGRATION_TEST_IOS_RUNNER(RunnerTests)' > RunnerTests.m):

    ruby generate_runner_tests.rb \
      integration_test/modulos/login/tests/login_test.dart \
      integration_test/modulos/transferencias/tests/transferencias_test.dart \
      ... \
      > ios/RunnerTests/RunnerTests.m
    

    What it produces - Illustrative shape:

    kExpectedTestDescriptions = @[
      @"increments the counter and verifies the display",
      @"shows an error when the amount exceeds the balance",
      // ... one entry per testWidgets(), in source order
    ];
    kGeneratedSelectorNames = @[
      @"test00_increments_the_counter_and_verifies_the_display",
      @"test01_shows_an_error_when_the_amount_exceeds_the_balance",
      // ...
    ];
    

    Each of those becomes its own XCTest invocation, so BrowserStack (or plain xcresult) shows them as separate rows with real names, same as it already does for Android.

    Validation

    Ran this against our actual CI suite — 6 feature modules combined into a single app session, 35 testWidgets() tests, via BrowserStack App Automate.

    • Wall-clock device run time: 20m41s and 20m07s respectively — both more than 3x past the ~6 minute mark that kills the stock INTEGRATION_TEST_IOS_RUNNER macro on Xcode 15+.
    • No Test runner never began executing tests after launching, no spindump requested by testmanagerd.
    • kCachedResults.count == kExpectedTestDescriptions.count (35 == 35) — the static manifest's own built-in consistency check never fired, confirming the build-time-extracted list matched the real run exactly.
    • BrowserStack reported all 35 as distinct, individually named entries.

    I added two canary tests (one designed to always pass, one designed to always fail) to rule out the possibility that the mechanism only looks correct because nothing had failed yet:

    flutter: 20:03 +49: Manifest Canary Tests CANARY-FAIL: este test siempre debe fallar a proposito [E]
      The test description was: CANARY-FAIL: este test siempre debe fallar a proposito
    flutter: 20:03 +50 -1: Some tests failed.
    
    • Consistency check still held (37 == 37, with the 2 canary tests added to the 35 real ones).
    • Confirmed Dart-side: results[description] correctly held the real failure detail (not "success") for the intentionally-failing test, which is exactly what kCachedResults[description] reads and what XCTAssertTrue(..., @"%@", result) surfaces as the XCTest failure message.

    Confirmed visually on the BrowserStack dashboard for this exact run:
    test35_CANARY_PASS_... shows green, test36_CANARY_FAIL_...a_proposito shows red and distinct from it, both with their full readable names next to every other real test in the suite (test26_WAL_991... through test34_WAL_1258_..., all green). So both the per-test naming and the pass/fail distinction hold end to end, from the Dart expect() down to the BrowserStack UI.

    Limitations

    • Requires a build-time step that can read your .dart sources. The extraction above is regex-based and specific to our codebase's conventions (single-quoted strings, one level of group() nesting); a more general implementation should probably use package:analyzer to actually parse the AST instead of regexing Dart source, especially for codebases with nested group()s, nested strings or const/interpolated descriptions.
    • Relies on exact string equality between the statically-extracted description and what IntegrationTestPlugin.testResults reports at runtime. Anything that makes those differ (e.g. a description built from a runtime value rather than a literal) breaks the matching — the generator refuses to run on exact-duplicate descriptions for the same reason, but it can't catch a description that only becomes non-unique at runtime.
    • Still bypasses FLTIntegrationTestRunner/testCaseNameFromDartTestName: entirely and reads IntegrationTestPlugin.testResults directly — this is a public property, but it's one layer below the API surface this package otherwise exposes, so it's worth the Flutter team's eyes on whether that's an acceptable thing to rely on vs. something worth exposing properly if this approach (or something like it) is useful for the eventual Swift rewrite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listf: integration_testThe flutter/packages/integration_test pluginfound in release: 3.19Found to occur in 3.19found in release: 3.21Found to occur in 3.21frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-iosiOS applications specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions