diff --git a/.gitignore b/.gitignore index 0fa6b675c..942f9f701 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# temporary output for screenshot generation +/screenshots/ diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index d5d8ca9cb..b75060b6e 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -201,6 +201,8 @@ class _AppDrawerState extends State { return typeBookmarks .where((filter) => !hiddenFilters.contains(filter)) .map((filter) => CollectionNavTile( + // key is expected by test driver + key: Key('drawer-type-${filter?.key}'), leading: DrawerFilterIcon(filter: filter), title: DrawerFilterTitle(filter: filter), filter: filter, @@ -257,6 +259,8 @@ class _AppDrawerState extends State { } return PageNavTile( + // key is expected by test driver + key: Key('drawer-page-$route'), trailing: trailing, routeName: route, pageBuilder: pageBuilder ?? (_) => const SizedBox(), diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart index 61c702bf7..d83ca6989 100644 --- a/lib/widgets/settings/language/language.dart +++ b/lib/widgets/settings/language/language.dart @@ -28,6 +28,8 @@ class LanguageSection extends StatelessWidget { final currentUnitSystem = context.select((s) => s.unitSystem); return AvesExpansionTile( + // key is expected by test driver + key: const Key('section-language'), // use a fixed value instead of the title to identify this expansion tile // so that the tile state is kept when the language is modified value: 'language', diff --git a/lib/widgets/settings/language/locale.dart b/lib/widgets/settings/language/locale.dart index 631890ad2..7c42dd2af 100644 --- a/lib/widgets/settings/language/locale.dart +++ b/lib/widgets/settings/language/locale.dart @@ -18,6 +18,8 @@ class LocaleTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( + // key is expected by test driver + key: const Key('tile-language'), title: Text(context.l10n.settingsLanguage), subtitle: Selector( selector: (context, s) => settings.locale, diff --git a/pubspec.yaml b/pubspec.yaml index 49396db87..969eda812 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,11 +112,11 @@ flutter: ################################################################################ # Test driver -# run (any device): -# % flutter drive --flavor play -t test_driver/driver_play.dart --profile +# capture shaders (profile mode, real device only): +# % flutter drive --flavor play -t test_driver/driver_shaders.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json -# capture shaders in profile mode (real device only): -# % flutter drive --flavor play -t test_driver/driver_play.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json +# generate screenshots (profile mode, specific collection): +# % flutter drive --flavor play -t test_driver/driver_screenshots.dart --profile ################################################################################ # Adaptations diff --git a/screenshots/en-settings.png b/screenshots/en-settings.png new file mode 100644 index 000000000..ceadac18c Binary files /dev/null and b/screenshots/en-settings.png differ diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart new file mode 100644 index 000000000..af963e965 --- /dev/null +++ b/test_driver/driver_screenshots.dart @@ -0,0 +1,44 @@ +import 'package:aves/main_play.dart' as app; +import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/enums.dart'; +import 'package:aves/widgets/filter_grids/countries_page.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + enableFlutterDriverExtension(); + + // something like `configure().then((_) => app.main());` does not behave as expected + // and starts the app without waiting for `configure` to complete + configureAndLaunch(); +} + +Future configureAndLaunch() async { + await settings.init(monitorPlatformSettings: false); + settings + // app + ..hasAcceptedTerms = true + ..isInstalledAppAccessAllowed = true + ..isErrorReportingAllowed = false + ..keepScreenOn = KeepScreenOn.always + ..homePage = HomePageSetting.collection + ..setTileExtent(CountryListPage.routeName, 112) + ..setTileLayout(CountryListPage.routeName, TileLayout.grid) + // viewer + ..showOverlayOnOpening = true + ..showOverlayMinimap = false + ..showOverlayInfo = true + ..showOverlayShootingDetails = false + ..enableOverlayBlurEffect = true + ..viewerUseCutout = true + // info + ..infoMapStyle = EntryMapStyle.stamenWatercolor + ..infoMapZoom = 11 + ..coordinateFormat = CoordinateFormat.dms + ..unitSystem = UnitSystem.metric; + + // TODO TLAD covers.set(LocationFilter(LocationLevel.country, location), contentId) + + app.main(); +} diff --git a/test_driver/driver_screenshots_test.dart b/test_driver/driver_screenshots_test.dart new file mode 100644 index 000000000..a9353868e --- /dev/null +++ b/test_driver/driver_screenshots_test.dart @@ -0,0 +1,173 @@ +// ignore_for_file: avoid_print +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +import 'utils/adb_utils.dart'; +import 'utils/driver_extension.dart'; + +late FlutterDriver driver; + +void main() { + group('[Aves app]', () { + setUpAll(() async { + await Directory(directory).create(); + + await Future.forEach( + [ + 'deckers.thibault.aves.debug', + 'deckers.thibault.aves.profile', + ], + (package) => grantPermissions(package, [ + 'android.permission.READ_EXTERNAL_STORAGE', + 'android.permission.ACCESS_MEDIA_LOCATION', + ])); + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + unawaited(driver.close()); + }); + + [ + 'de', + 'en', + // TODO TLAD other locales + ].forEach((v) async { + setLanguage(v); + collection(); + viewer(); + info(); + stats(); + countries(); + }); + }, timeout: const Timeout(Duration(seconds: 30))); +} + +const directory = 'screenshots'; +String screenshotLocale = ''; + +Future takeScreenshot(FlutterDriver driver, String name) async { + final pixels = await driver.screenshot(); + final file = File('$directory/$screenshotLocale-$name.png'); + await file.writeAsBytes(pixels); + print('* saved screenshot to ${file.path}'); +} + +void setLanguage(String locale) { + test('set language', () async { + await driver.tapKeyAndWait('appbar-leading-button'); + await driver.tapKeyAndWait('drawer-settings-button'); + await driver.tapKeyAndWait('section-language'); + await driver.tapKeyAndWait('tile-language'); + await driver.tapKeyAndWait(locale); + screenshotLocale = locale; + + await pressDeviceBackButton(); + await driver.waitUntilNoTransientCallbacks(); + }); +} + +void collection() { + test('1. Collection', () async { + // TODO TLAD hidden filters: reverse of TagFilter('aves-screenshot-collection') + + await driver.tapKeyAndWait('appbar-leading-button'); + await driver.tapKeyAndWait('drawer-type-null'); + + await takeScreenshot(driver, '1-collection'); + }); +} + +void viewer() { + test('2. Viewer', () async { + const query = 'Singapore 087 Zoo - Douc langur'; + + await driver.tapKeyAndWait('menu-searchCollection'); + await driver.tap(find.byType('TextField')); + await driver.enterText(query); + final queryChip = find.byValueKey('query-$query'); + await driver.waitFor(queryChip); + await driver.tap(queryChip); + await driver.waitUntilNoTransientCallbacks(); + + // delay to avoid flaky descendant resolution + await Future.delayed(const Duration(seconds: 2)); + await driver.tap(find.descendant( + of: find.byValueKey('collection-grid'), + matching: find.byType('MetaData'), + firstMatchOnly: true, + )); + await driver.waitUntilNoTransientCallbacks(); + await Future.delayed(const Duration(seconds: 2)); + + final imageView = find.byValueKey('image_view'); + await driver.doubleTap(imageView); + await Future.delayed(const Duration(seconds: 1)); + + await takeScreenshot(driver, '2-viewer'); + }); +} + +void info() { + test('3. Info (basic), 4. Info (metadata)', () async { + final verticalPageView = find.byValueKey('vertical-pageview'); + + await driver.scroll(verticalPageView, 0, -600, const Duration(milliseconds: 400)); + await Future.delayed(const Duration(seconds: 2)); + + await takeScreenshot(driver, '3-info-basic'); + + await driver.scroll(verticalPageView, 0, -800, const Duration(milliseconds: 600)); + await Future.delayed(const Duration(seconds: 1)); + + final gpsTile = find.descendant( + of: find.byValueKey('tilecard-GPS'), + matching: find.byType('ListTile'), + ); + await driver.tap(gpsTile); + await driver.waitUntilNoTransientCallbacks(); + + await takeScreenshot(driver, '3-info-metadata'); + + await pressDeviceBackButton(); + await driver.waitUntilNoTransientCallbacks(); + + await pressDeviceBackButton(); + await driver.waitUntilNoTransientCallbacks(); + }); +} + +void stats() { + test('5. Stats', () async { + // TODO TLAD hidden filters: PathFilter('/storage/emulated/0/Pictures/Dev') + + await driver.tapKeyAndWait('appbar-leading-button'); + await driver.tapKeyAndWait('drawer-type-null'); + + await driver.tapKeyAndWait('appbar-menu-button'); + await driver.tapKeyAndWait('menu-stats'); + + await takeScreenshot(driver, '5-stats'); + + await pressDeviceBackButton(); + await driver.waitUntilNoTransientCallbacks(); + }); +} + +void countries() { + test('6. Countries', () async { + // TODO TLAD hidden filters: reverse of TagFilter('aves-screenshot-collection') + // TODO TLAD OR 1) set country covers, 2) hidden filters: PathFilter('/storage/emulated/0/Pictures/Dev') + + await driver.tapKeyAndWait('appbar-leading-button'); + await driver.tapKeyAndWait('drawer-page-/countries'); + + await takeScreenshot(driver, '6-countries'); + + await pressDeviceBackButton(); + await driver.waitUntilNoTransientCallbacks(); + }); +} diff --git a/test_driver/driver_play.dart b/test_driver/driver_shaders.dart similarity index 97% rename from test_driver/driver_play.dart rename to test_driver/driver_shaders.dart index e4a357117..11a1be7b8 100644 --- a/test_driver/driver_play.dart +++ b/test_driver/driver_shaders.dart @@ -31,6 +31,7 @@ Future configureAndLaunch() async { ..keepScreenOn = KeepScreenOn.always ..hasAcceptedTerms = false ..isErrorReportingAllowed = false + ..isInstalledAppAccessAllowed = true ..locale = const Locale('en') ..homePage = HomePageSetting.collection ..infoMapStyle = EntryMapStyle.googleNormal diff --git a/test_driver/driver_play_test.dart b/test_driver/driver_shaders_test.dart similarity index 94% rename from test_driver/driver_play_test.dart rename to test_driver/driver_shaders_test.dart index 2ea5f80f5..674d35ae0 100644 --- a/test_driver/driver_play_test.dart +++ b/test_driver/driver_shaders_test.dart @@ -12,22 +12,19 @@ import 'utils/driver_extension.dart'; late FlutterDriver driver; -extension ExtraFlutterDriver on FlutterDriver { - Future tapKeyAndWait(String key) async { - await driver.tap(find.byValueKey(key)); - await driver.waitUntilNoTransientCallbacks(); - } -} - void main() { group('[Aves app]', () { setUpAll(() async { await copyContent(sourcePicturesDir, targetPicturesDir); - await grantPermissions('deckers.thibault.aves.debug', [ - 'android.permission.READ_EXTERNAL_STORAGE', - 'android.permission.WRITE_EXTERNAL_STORAGE', - 'android.permission.ACCESS_MEDIA_LOCATION', - ]); + await Future.forEach( + [ + 'deckers.thibault.aves.debug', + 'deckers.thibault.aves.profile', + ], + (package) => grantPermissions(package, [ + 'android.permission.READ_EXTERNAL_STORAGE', + 'android.permission.ACCESS_MEDIA_LOCATION', + ])); driver = await FlutterDriver.connect(); }); diff --git a/test_driver/utils/driver_extension.dart b/test_driver/utils/driver_extension.dart index 3d70e7569..6be8b9c7f 100644 --- a/test_driver/utils/driver_extension.dart +++ b/test_driver/utils/driver_extension.dart @@ -8,4 +8,9 @@ extension ExtraFlutterDriver on FlutterDriver { await Future.delayed(doubleTapDelay); await tap(finder, timeout: timeout); } + + Future tapKeyAndWait(String key) async { + await tap(find.byValueKey(key)); + await waitUntilNoTransientCallbacks(); + } }