// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ATTENTION! // // This file is not named "*_test.dart", and as such will not run when you run // "flutter test". It is only intended to be run as part of the // flutter_gallery_instrumentation_test devicelab test. import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart' show kPrimaryButton; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gallery/gallery/app.dart' show GalleryApp; import 'package:flutter_gallery/gallery/demos.dart'; import 'package:flutter_test/flutter_test.dart'; // Reports success or failure to the native code. const MethodChannel _kTestChannel = MethodChannel('io.flutter.demo.gallery/TestLifecycleListener'); // We don't want to wait for animations to complete before tapping the // back button in the demos with these titles. const List<String> _kUnsynchronizedDemoTitles = <String>[ 'Progress indicators', 'Activity Indicator', 'Video', ]; // These demos can't be backed out of by tapping a button whose // tooltip is 'Back'. const List<String> _kSkippedDemoTitles = <String>[ 'Progress indicators', 'Activity Indicator', 'Video', ]; // There are 3 places where the Gallery demos are traversed. // 1- In widget tests such as dev/integration_tests/flutter_gallery/test/smoke_test.dart // 2- In driver tests such as dev/integration_tests/flutter_gallery/test_driver/transitions_perf_test.dart // 3- In on-device instrumentation tests such as dev/integration_tests/flutter_gallery/test/live_smoketest.dart // // If you change navigation behavior in the Gallery or in the framework, make // sure all 3 are covered. Future<void> main() async { try { // Verify that _kUnsynchronizedDemos and _kSkippedDemos identify // demos that actually exist. final List<String> allDemoTitles = kAllGalleryDemos.map((GalleryDemo demo) => demo.title).toList(); if (!Set<String>.from(allDemoTitles).containsAll(_kUnsynchronizedDemoTitles)) { fail('Unrecognized demo titles in _kUnsynchronizedDemosTitles: $_kUnsynchronizedDemoTitles'); } if (!Set<String>.from(allDemoTitles).containsAll(_kSkippedDemoTitles)) { fail('Unrecognized demo names in _kSkippedDemoTitles: $_kSkippedDemoTitles'); } print('Starting app...'); runApp(const GalleryApp(testMode: true)); final _LiveWidgetController controller = _LiveWidgetController(WidgetsBinding.instance); for (final GalleryDemoCategory category in kAllGalleryDemoCategories) { print('Tapping "${category.name}" section...'); await controller.tap(find.text(category.name)); for (final GalleryDemo demo in kGalleryCategoryToDemos[category]!) { final Finder demoItem = find.text(demo.title); print('Scrolling to "${demo.title}"...'); await controller.scrollIntoView(demoItem, alignment: 0.5); if (_kSkippedDemoTitles.contains(demo.title)) { continue; } for (int i = 0; i < 2; i += 1) { print('Tapping "${demo.title}"...'); await controller.tap(demoItem); // Launch the demo controller.frameSync = !_kUnsynchronizedDemoTitles.contains(demo.title); print('Going back to demo list...'); await controller.tap(backFinder); controller.frameSync = true; } } print('Going back to home screen...'); await controller.tap(find.byTooltip('Back')); } print('Finished successfully!'); _kTestChannel.invokeMethod<void>('success'); } catch (error, stack) { print('Caught error: $error\n$stack'); _kTestChannel.invokeMethod<void>('failure'); } } final Finder backFinder = find.byElementPredicate( (Element element) { final Widget widget = element.widget; if (widget is Tooltip) { return widget.message == 'Back'; } if (widget is CupertinoNavigationBarBackButton) { return true; } return false; }, description: 'Material or Cupertino back button', ); class _LiveWidgetController extends LiveWidgetController { _LiveWidgetController(super.binding); /// With [frameSync] enabled, Flutter Driver will wait to perform an action /// until there are no pending frames in the app under test. bool frameSync = true; /// Waits until at the end of a frame the provided [condition] is [true]. Future<void> _waitUntilFrame(bool Function() condition, [Completer<void>? completer]) { completer ??= Completer<void>(); if (!condition()) { SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { _waitUntilFrame(condition, completer); }); } else { completer.complete(); } return completer.future; } /// Runs `finder` repeatedly until it finds one or more [Element]s. Future<Finder> _waitForElement(Finder finder) async { if (frameSync) { await _waitUntilFrame(() => binding.transientCallbackCount == 0); } await _waitUntilFrame(() => finder.precache()); if (frameSync) { await _waitUntilFrame(() => binding.transientCallbackCount == 0); } return finder; } @override Future<void> tap(Finder finder, { int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true }) async { await super.tap(await _waitForElement(finder), pointer: pointer, buttons: buttons, warnIfMissed: warnIfMissed); } Future<void> scrollIntoView(Finder finder, {required double alignment}) async { final Finder target = await _waitForElement(finder); await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: alignment); } }