diff --git a/README.md b/README.md index 563b9760..4287efec 100644 --- a/README.md +++ b/README.md @@ -145,23 +145,18 @@ _Screenshots_ uses a configuration file to configure a run. The default config filename is `screenshots.yaml`: ````yaml # A list of screen capture tests +# Note: flutter driver expects a pair of files eg, main1.dart and main1_test.dart tests: - test_driver/main1.dart - test_driver/main2.dart -# Note: flutter driver expects a pair of files for testing -# For example: -# main1.dart is the test app (that calls your app) -# main1_test.dart is the matching test that flutter driver -# expects to find. - # Interim location of screenshots from tests staging: /tmp/screenshots # A list of locales supported by the app locales: - - de-DE - en-US + - de-DE # A map of devices to emulate devices: @@ -169,6 +164,7 @@ devices: iPhone XS Max: frame: false iPad Pro (12.9-inch) (3rd generation): + orientation: LandscapeRight android: Nexus 6P: @@ -182,9 +178,13 @@ Individual devices can be configured in `screenshots.yaml` by specifying per dev | Parameter | Values | Required | Description | | --- | --- | --- | --- | |frame|true/false|optional|Controls whether screenshots generated on the device should be placed in a frame. Overrides the global frame setting (see example `screenshots.yaml` above). +|orientation|Portrait \| LandscapeRight \| PortraitUpsideDown \| LandscapeLeft|optional|Controls orientation of device during test. Currently disables framing resulting in a raw screenshot. Ignored for real devices. + Note: images generated for those devices where framing is disabled are probably not suitable for upload, but can be used for local review. +Note: orientation on iOS simulators is implemented using an AppleScript script which requires granting permission on first use. + # Record/Compare Mode _Screenshots_ can be used to monitor any unexpected changes to the UI by comparing the new screenshots to previously recorded screenshots. Any differences will be highlighted in a 'diff' image for review. diff --git a/example/screenshots.yaml b/example/screenshots.yaml index e3a933e9..29f9312b 100644 --- a/example/screenshots.yaml +++ b/example/screenshots.yaml @@ -8,17 +8,19 @@ staging: /tmp/screenshots # A list of locales supported in app locales: - - fr-CA - en-US + - fr-CA # A list of devices to run tests on devices: ios: iPhone XS Max: - iPad Pro (12.9-inch) (2nd generation): frame: false + iPad Pro (12.9-inch) (2nd generation): + orientation: LandscapeRight android: Nexus 6P: + orientation: LandscapeRight # Frame screenshots frame: true diff --git a/example/screenshots_CI.yaml b/example/screenshots_CI.yaml index 8345aa4e..3d90c347 100644 --- a/example/screenshots_CI.yaml +++ b/example/screenshots_CI.yaml @@ -8,8 +8,8 @@ staging: /tmp/screenshots # A list of locales supported in app locales: - - fr-CA - en-US + - fr-CA # A list of devices to run tests on devices: diff --git a/lib/resources/script/sim_orientation.scpt b/lib/resources/script/sim_orientation.scpt new file mode 100644 index 00000000..9106074d --- /dev/null +++ b/lib/resources/script/sim_orientation.scpt @@ -0,0 +1,31 @@ +on run argv + my do_submenu("Simulator", "Hardware", "Orientation", item 1 of argv) + return item 1 of argv +end run + +on do_submenu(app_name, menu_name, menu_item, submenu_item) + try + -- bring the target application to the front + tell application app_name + activate + end tell + tell application "System Events" + tell process app_name + tell menu bar 1 + tell menu bar item menu_name + tell menu menu_name + tell menu item menu_item + tell menu menu_item + click menu item submenu_item + end tell + end tell + end tell + end tell + end tell + end tell + end tell + return true + on error error_message + return false + end try +end do_submenu diff --git a/lib/src/archive.dart b/lib/src/archive.dart index ac8aa95a..60e2ab2a 100644 --- a/lib/src/archive.dart +++ b/lib/src/archive.dart @@ -5,12 +5,9 @@ import 'utils.dart' as utils; class Archive { static final _timeStamp = getTimestamp(); - final _stagingTestDir; final archiveDirPrefix; - Archive(String stagingDir, String archiveDir) - : _stagingTestDir = '$stagingDir/$kTestScreenshotsDir', - archiveDirPrefix = '$archiveDir/$_timeStamp'; + Archive(String archiveDir) : archiveDirPrefix = '$archiveDir/$_timeStamp'; String dstDir(DeviceType deviceType, String locale) => '$archiveDirPrefix/${utils.getStringFromEnum(deviceType)}/$locale'; diff --git a/lib/src/config.dart b/lib/src/config.dart index 3e5cc792..76669b97 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:meta/meta.dart'; import 'image_processor.dart'; +import 'orientation.dart'; import 'screens.dart'; import 'package:yaml/yaml.dart'; import 'utils.dart' as utils; @@ -40,7 +41,7 @@ class Config { /// (called by screenshots) @visibleForTesting Future storeEnv(Screens screens, String emulatorName, String locale, - String deviceType) async { + String deviceType, String orientation) async { // store env for later use by tests final screenProps = screens.screenProps(emulatorName); final screenSize = screenProps == null ? null : screenProps['size']; @@ -49,6 +50,7 @@ class Config { 'locale': locale, 'device_name': emulatorName, 'device_type': deviceType, + 'orientation': orientation }; await _envStore.writeAsString(json.encode(currentEnv)); } @@ -62,6 +64,33 @@ class Config { @visibleForTesting // config is exported in library Future validate( Screens screens, List allDevices, List allEmulators) async { + // validate params + final deviceNames = utils.getAllConfiguredDeviceNames(configInfo); + for (final devName in deviceNames) { + final deviceInfo = findDeviceInfo(configInfo, devName); + if (deviceInfo != null) { + final orientation = deviceInfo['orientation']; + if (orientation != null && !isValidOrientation(orientation)) { + stderr.writeln( + 'Invalid value for \'orientation\' for device \'$devName\': $orientation'); + stderr.writeln('Valid values:'); + for (final orientation in Orientation.values) { + stderr.writeln(' ${utils.getStringFromEnum(orientation)}'); + } + exit(1); + } + final frame = deviceInfo['frame']; + if (frame != null && !isValidFrame(frame)) { + stderr.writeln( + 'Invalid value for \'frame\' for device \'$devName\': $frame'); + stderr.writeln('Valid values:'); + stderr.writeln(' true'); + stderr.writeln(' false'); + exit(1); + } + } + } + final isDeviceAttached = (device) => device != null; final isEmulatorInstalled = (emulator) => emulator != null; @@ -209,3 +238,27 @@ class Config { simulators.forEach((simulator, _) => stdout.write(' $simulator\n')); } } + +bool isValidOrientation(String orientation) { + return Orientation.values.firstWhere( + (o) => utils.getStringFromEnum(o) == orientation, + orElse: () => null) != + null; +} + +bool isValidFrame(dynamic frame) { + return frame != null && (frame == true || frame == false); +} + +/// Find device info in config for device name. +Map findDeviceInfo(Map configInfo, String deviceName) { + Map deviceInfo; + configInfo['devices'].forEach((deviceType, devices) { + if (devices != null) { + devices.forEach((_deviceName, _deviceInfo) { + if (_deviceName == deviceName) deviceInfo = _deviceInfo; + }); + } + }); + return deviceInfo; +} diff --git a/lib/src/daemon_client.dart b/lib/src/daemon_client.dart index ea802ff9..cb3426fa 100644 --- a/lib/src/daemon_client.dart +++ b/lib/src/daemon_client.dart @@ -199,3 +199,17 @@ List iosDevices() { return device; }).toList(); } + +/// Wait for emulator or simulator to start +Future waitForEmulatorToStart( + DaemonClient daemonClient, String deviceId) async { + bool started = false; + while (!started) { + final devices = await daemonClient.devices; + final device = devices.firstWhere( + (device) => device['id'] == deviceId && device['emulator'], + orElse: () => null); + started = device != null; + await Future.delayed(Duration(milliseconds: 1000)); + } +} diff --git a/lib/src/fastlane.dart b/lib/src/fastlane.dart index 73ac9410..376e2b3d 100644 --- a/lib/src/fastlane.dart +++ b/lib/src/fastlane.dart @@ -50,6 +50,7 @@ Future _clearFastlaneDir(Screens screens, String deviceName, String locale, /// Generate fastlane dir path for ios or android. String getDirPath( DeviceType deviceType, String locale, String androidModelType) { + locale = locale.replaceAll('_', '-'); // in case canonicalized const androidPrefix = 'android/fastlane/metadata/android'; const iosPrefix = 'ios/fastlane/screenshots'; String dirPath; @@ -82,4 +83,4 @@ void deleteMatchingFiles(String dirPath, RegExp pattern) { } else { Directory(dirPath).createSync(recursive: true); } -} \ No newline at end of file +} diff --git a/lib/src/image_processor.dart b/lib/src/image_processor.dart index b0093a5b..e84854f7 100644 --- a/lib/src/image_processor.dart +++ b/lib/src/image_processor.dart @@ -40,32 +40,36 @@ class ImageProcessor { if (screenProps == null) { print('Warning: \'$deviceName\' images will not be processed'); } else { - final Map screenResources = screenProps['resources']; - final staging = _config['staging']; + // add frame if required + if (isFrameRequired(_config, deviceType, deviceName)) { + final Map screenResources = screenProps['resources']; + final staging = _config['staging']; // print('screenResources=$screenResources'); - print('Processing screenshots from test...'); + print('Processing screenshots from test...'); - // unpack images for screen from package to local staging area - await resources.unpackImages(screenResources, staging); + // unpack images for screen from package to local staging area + await resources.unpackImages(screenResources, staging); - // add status and nav bar and frame for each screenshot - final screenshots = Directory('$staging/$kTestScreenshotsDir').listSync(); - for (final screenshotPath in screenshots) { - // add status bar for each screenshot + // add status and nav bar and frame for each screenshot + final screenshots = + Directory('$staging/$kTestScreenshotsDir').listSync(); + for (final screenshotPath in screenshots) { + // add status bar for each screenshot // print('overlaying status bar over screenshot at $screenshotPath'); - await overlay(_config, screenResources, screenshotPath.path); + await overlay(_config, screenResources, screenshotPath.path); - if (deviceType == DeviceType.android) { - // add nav bar for each screenshot + if (deviceType == DeviceType.android) { + // add nav bar for each screenshot // print('appending navigation bar to screenshot at $screenshotPath'); - await append(_config, screenResources, screenshotPath.path); - } + await append(_config, screenResources, screenshotPath.path); + } - // add frame if required - if (isFrameRequired(_config, deviceType, deviceName)) { // print('placing $screenshotPath in frame'); - await frame(_config, screenProps, screenshotPath.path, deviceType, runMode); + await frame( + _config, screenProps, screenshotPath.path, deviceType, runMode); } + } else { + print('Warning: framing is not enabled'); } } @@ -190,9 +194,12 @@ class ImageProcessor { bool isFrameRequired = config['frame']; if (device != null) { final isDeviceFrameRequired = device['frame']; - if (isDeviceFrameRequired != null) { - isFrameRequired = isDeviceFrameRequired; - } + // device frame over-rides global frame + isDeviceFrameRequired != null + ? isFrameRequired = isDeviceFrameRequired + : null; + // orientation over-rides global and device frame setting + device['orientation'] != null ? isFrameRequired = false : null; } return isFrameRequired; } @@ -211,7 +218,7 @@ class ImageProcessor { // set the default background color String backgroundColor; - (deviceType == DeviceType.ios && runMode!=RunMode.archive) + (deviceType == DeviceType.ios && runMode != RunMode.archive) ? backgroundColor = kDefaultIosBackground : backgroundColor = kDefaultAndroidBackground; diff --git a/lib/src/orientation.dart b/lib/src/orientation.dart new file mode 100644 index 00000000..f28504e7 --- /dev/null +++ b/lib/src/orientation.dart @@ -0,0 +1,57 @@ +import 'globals.dart'; +import 'utils.dart' as utils; +import 'run.dart' as run; + +enum Orientation { Portrait, LandscapeRight, PortraitUpsideDown, LandscapeLeft } + +/// Change orientation of a running emulator or simulator. +/// (No known way of supporting real devices.) +void changeDeviceOrientation(DeviceType deviceType, Orientation orientation, + {String deviceId, String scriptDir}) { + final androidOrientations = { + 'Portrait': '0', + 'LandscapeRight': '1', + 'PortraitUpsideDown': '2', + 'LandscapeLeft': '3' + }; + final iosOrientations = { + 'Portrait': 'Portrait', + 'LandscapeRight': 'Landscape Right', + 'PortraitUpsideDown': 'Portrait Upside Down', + 'LandscapeLeft': 'Landscape Left' + }; + const sim_orientation_script = 'sim_orientation.scpt'; + final _orientation = utils.getStringFromEnum(orientation); + print('Setting orientation to $_orientation'); + switch (deviceType) { + case DeviceType.android: + run.cmd('adb', [ + '-s', + deviceId, + 'shell', + 'settings', + 'put', + 'system', + 'user_rotation', + androidOrientations[_orientation] + ]); + break; + case DeviceType.ios: + // requires permission when run for first time + run.cmd( + 'osascript', + ['$scriptDir/$sim_orientation_script', iosOrientations[_orientation]], + '.', + true); + break; + } +} + +Orientation getOrientationEnum(String orientation) { + final _orientation = + utils.getEnumFromString(Orientation.values, orientation); + _orientation == null + ? throw 'Error: orientation \'$orientation\' not found' + : null; + return _orientation; +} diff --git a/lib/src/resources.dart b/lib/src/resources.dart index 33dcef38..a064c25c 100644 --- a/lib/src/resources.dart +++ b/lib/src/resources.dart @@ -31,6 +31,10 @@ Future unpackScripts(String dstDir) async { 'resources/script/simulator-controller', dstDir, ); + await unpackScript( + 'resources/script/sim_orientation.scpt', + dstDir, + ); } /// Read script from resources and install in staging area. diff --git a/lib/src/run.dart b/lib/src/run.dart index eda0b08f..1f7acc2b 100644 --- a/lib/src/run.dart +++ b/lib/src/run.dart @@ -1,12 +1,15 @@ import 'dart:async'; import 'dart:io'; +import 'package:intl/intl.dart'; + import 'archive.dart'; import 'config.dart'; import 'daemon_client.dart'; import 'fastlane.dart' as fastlane; import 'globals.dart'; import 'image_processor.dart'; +import 'orientation.dart' as orient; import 'resources.dart' as resources; import 'screens.dart'; import 'utils.dart' as utils; @@ -48,12 +51,11 @@ Future run( final stagingDir = configInfo['staging']; await Directory(stagingDir + '/$kTestScreenshotsDir').create(recursive: true); await resources.unpackScripts(stagingDir); - final archiveDir = configInfo['archive']; - Archive archive = Archive(stagingDir, archiveDir); - if (archiveDir == null) { - await fastlane.clearFastlaneDirs(configInfo, screens, runMode); - } else { + Archive archive = Archive(configInfo['archive']); + if (runMode == RunMode.archive) { print('Archiving screenshots to ${archive.archiveDirPrefix}...'); + } else { + await fastlane.clearFastlaneDirs(configInfo, screens, runMode); } // run integration tests in each real device (or emulator/simulator) for // each locale and process screenshots @@ -68,14 +70,14 @@ Future run( print(' $recordingDir/ios/fastlane/screenshots'); print(' $recordingDir/android/fastlane/metadata/android'); } else { - if (archiveDir == null) { + if (runMode == RunMode.archive) { + print(' ${archive.archiveDirPrefix}'); + } else { print(' ios/fastlane/screenshots'); print(' android/fastlane/metadata/android'); print('for upload to both Apple and Google consoles.'); print('\nFor uploading and other automation options see:'); print(' https://pub.dartlang.org/packages/fledge'); - } else { - print(' ${archive.archiveDirPrefix}'); } } print('\nscreenshots completed successfully.'); @@ -144,37 +146,63 @@ Future runTestsOnAll(DaemonClient daemonClient, List devices, List emulators, utils.getIosSimulators(), configDeviceName); deviceId = simulator['udid']; // check if current device is pending a locale change - if (locales[0] == utils.getIosSimulatorLocale(deviceId)) { + if (Intl.canonicalizedLocale(locales[0]) == + Intl.canonicalizedLocale(utils.getIosSimulatorLocale(deviceId))) { print('Starting $configDeviceName...'); - startSimulator(deviceId); + await startSimulator(daemonClient, deviceId); } else { pendingIosLocaleChangeAtStart = true; - print('Not starting $configDeviceName due to pending locale change'); +// print( +// 'Postponing \'$configDeviceName\' startup due to pending locale change'); } } } - assert(deviceId != null); + deviceId == null + ? throw 'Error: device \'$configDeviceName\' not found' + : null; final deviceType = getDeviceType(configInfo, configDeviceName); // if device is real ios or android, cannot change locale if (device != null && !device['emulator']) { - final defaultLocale = 'en-US'; // todo: need actual local + final defaultLocale = 'en_US'; // todo: need actual locale of real device print('Warning: the locale of a real device cannot be changed.'); - await runProcessTests(config, screens, configDeviceName, defaultLocale, - deviceType, testPaths, deviceId, imageProcessor, runMode, archive); + await runProcessTests( + config, + screens, + configDeviceName, + defaultLocale, + deviceType, + testPaths, + deviceId, + imageProcessor, + runMode, + archive, + 'unknown'); } else { - // Check for a running android device or emulator + // Function to check for a running android device or emulator bool isRunningAndroidDeviceOrEmulator(Map device, Map emulator) { return (device != null && device['platform'] != 'ios') || (device == null && emulator != null); } - // save original locale for reverting later if necessary + // save original android locale for reverting later if necessary String origAndroidLocale; if (isRunningAndroidDeviceOrEmulator(device, emulator)) { origAndroidLocale = utils.getAndroidDeviceLocale(deviceId); } + // Function to check for a running ios device or simulator. + bool isRunningIosDeviceOrSimulator(Map device, Map emulator) { + return (device != null && device['platform'] == 'ios') || + (device == null && simulator != null); + } + + // save original ios locale for reverting later if necessary + String origIosLocale; + if (isRunningIosDeviceOrSimulator(device, emulator)) { + origIosLocale = utils.getIosSimulatorLocale(deviceId); + } + for (final locale in locales) { // set locale if android device or emulator if (isRunningAndroidDeviceOrEmulator(device, emulator)) { @@ -182,38 +210,90 @@ Future runTestsOnAll(DaemonClient daemonClient, List devices, List emulators, } // set locale if ios simulator if ((device != null && - device['platform'] == 'ios' && - device['emulator'])) { - // an already running simulator + device['platform'] == 'ios' && + device['emulator']) || + (device == null && + simulator != null && + !pendingIosLocaleChangeAtStart)) { + // an already running simulator or a started simulator + final localeChanged = await setSimulatorLocale( + deviceId, configDeviceName, locale, stagingDir, daemonClient); + if (localeChanged) { + // restart simulator + print('Restarting \'$configDeviceName\' due to locale change...'); + await shutdownSimulator(deviceId); + await startSimulator(daemonClient, deviceId); + } + } + if (pendingIosLocaleChangeAtStart) { + // a non-running simulator await setSimulatorLocale( - deviceId, configDeviceName, locale, stagingDir); - } else { - if (device == null && simulator != null) { - if (pendingIosLocaleChangeAtStart) { - // a non-running simulator - await setSimulatorLocale( - deviceId, configDeviceName, locale, stagingDir, - running: false); - pendingIosLocaleChangeAtStart = false; - } else { - // a running simulator - await setSimulatorLocale( - deviceId, configDeviceName, locale, stagingDir); - } + deviceId, configDeviceName, locale, stagingDir, daemonClient); + print('Starting $configDeviceName...'); + await startSimulator(daemonClient, deviceId); + pendingIosLocaleChangeAtStart = false; + } + + // Change orientation if required + final deviceInfo = configInfo['devices'] + [utils.getStringFromEnum(deviceType)][configDeviceName]; + String deviceOrientation; + deviceInfo != null + ? deviceOrientation = deviceInfo['orientation'] + : null; + if (deviceOrientation != null) { + final orientation = orient.getOrientationEnum(deviceOrientation); + final currentDevice = + utils.getDeviceFromId(await daemonClient.devices, deviceId); + currentDevice == null + ? throw 'Error: device \'$configDeviceName\' not found in flutter daemon.' + : null; + switch (deviceType) { + case DeviceType.android: + if (currentDevice['emulator']) { + orient.changeDeviceOrientation(deviceType, orientation, + deviceId: deviceId); + } else { + print( + 'Warning: cannot change orientation of a real android device.'); + } + break; + case DeviceType.ios: + if (currentDevice['emulator']) { + orient.changeDeviceOrientation(deviceType, orientation, + scriptDir: '$stagingDir/resources/script'); + } else { + print( + 'Warning: cannot change orientation of a real iOS device.'); + } + break; } } + // run tests and process images - await runProcessTests(config, screens, configDeviceName, locale, - deviceType, testPaths, deviceId, imageProcessor, runMode, archive); + await runProcessTests( + config, + screens, + configDeviceName, + locale, + deviceType, + testPaths, + deviceId, + imageProcessor, + runMode, + archive, + deviceOrientation); } + // if an emulator was started, revert locale if necessary and shut it down if (emulator != null) { await setEmulatorLocale(deviceId, origAndroidLocale, configDeviceName); await shutdownAndroidEmulator(daemonClient, deviceId); } if (simulator != null) { - // todo: revert locale - shutdownSimulator(deviceId); + await setSimulatorLocale(deviceId, configDeviceName, origIosLocale, + stagingDir, daemonClient); + await shutdownSimulator(deviceId); } } } @@ -229,11 +309,12 @@ Future runProcessTests( String deviceId, ImageProcessor imageProcessor, RunMode runMode, - Archive archive) async { + Archive archive, + String orientation) async { // store env for later use by tests // ignore: invalid_use_of_visible_for_testing_member - await config.storeEnv( - screens, configDeviceName, locale, utils.getStringFromEnum(deviceType)); + await config.storeEnv(screens, configDeviceName, locale, + utils.getStringFromEnum(deviceType), orientation); for (final testPath in testPaths) { print('Running $testPath on \'$configDeviceName\' in locale $locale...'); await utils.streamCmd('flutter', ['-d', deviceId, 'drive', testPath]); @@ -243,12 +324,15 @@ Future runProcessTests( } } -void shutdownSimulator(String deviceId) { +Future shutdownSimulator(String deviceId) async { cmd('xcrun', ['simctl', 'shutdown', deviceId]); + // shutdown apparently needs time when restarting + await Future.delayed(Duration(milliseconds: 2000)); } -void startSimulator(String deviceId) { +Future startSimulator(DaemonClient daemonClient, String deviceId) async { cmd('xcrun', ['simctl', 'boot', deviceId]); + await waitForEmulatorToStart(daemonClient, deviceId); } /// Start android emulator and return device id. @@ -289,36 +373,41 @@ Map _findDevice(List devices, List emulators, String deviceName) { return device; } -/// Set the locale for a running simulator. -Future setSimulatorLocale( - String deviceId, String deviceName, String testLocale, stagingDir, - {bool running = true}) async { +/// Set the simulator locale. +/// (Startup managed elsewhere) +/// Returns true of locale changed. +Future setSimulatorLocale(String deviceId, String deviceName, + String testLocale, String stagingDir, DaemonClient daemonClient) async { // a running simulator final deviceLocale = utils.getIosSimulatorLocale(deviceId); -// print('simulator locale=$deviceLocale'); - if (testLocale != deviceLocale) { - if (running) shutdownSimulator(deviceId); + print('\'$deviceName\' locale: $deviceLocale, test locale: $testLocale'); + bool localeChanged = false; + if (Intl.canonicalizedLocale(testLocale) != + Intl.canonicalizedLocale(deviceLocale)) { print( 'Changing locale from $deviceLocale to $testLocale on \'$deviceName\'...'); await _changeSimulatorLocale(stagingDir, deviceId, testLocale); - print('Starting $deviceName...'); - startSimulator(deviceId); + localeChanged = true; } + return localeChanged; } /// Set the locale of a running emulator. Future setEmulatorLocale(String deviceId, testLocale, deviceName) async { final deviceLocale = utils.getAndroidDeviceLocale(deviceId); - print('emulator locale=$deviceLocale'); + print('\'$deviceName\' locale: $deviceLocale, test locale: $testLocale'); if (deviceLocale != null && deviceLocale != '' && - deviceLocale != testLocale) { + Intl.canonicalizedLocale(deviceLocale) != + Intl.canonicalizedLocale(testLocale)) { // daemonClient.verbose = true; print( 'Changing locale from $deviceLocale to $testLocale on \'$deviceName\'...'); changeAndroidLocale(deviceId, deviceLocale, testLocale); // daemonClient.verbose = false; await utils.waitAndroidLocaleChange(deviceId, testLocale); + // allow additional time before orientation change + await Future.delayed(Duration(milliseconds: 5000)); } } @@ -334,7 +423,7 @@ void changeAndroidLocale( stdout.write( ' https://stackoverflow.com/questions/43923996/adb-root-is-not-working-on-emulator/45668555#45668555 for details.\n'); } - // adb shell "setprop persist.sys.locale fr-CA; setprop ctl.restart zygote" + // adb shell "setprop persist.sys.locale fr_CA; setprop ctl.restart zygote" cmd('adb', [ '-s', deviceId, diff --git a/lib/src/utils.dart b/lib/src/utils.dart index ccf5d452..71ef58eb 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -170,17 +170,17 @@ T getEnumFromString(List values, String value) { String getAndroidDeviceLocale(String deviceId) { // ro.product.locale is available on first boot but does not update, // persist.sys.locale is empty on first boot but updates with locale changes - String deviceLocale = run + String locale = run .cmd('adb', ['-s', deviceId, 'shell', 'getprop persist.sys.locale'], '.', true) .trim(); - if (deviceLocale.isEmpty) { - deviceLocale = run + if (locale.isEmpty) { + locale = run .cmd('adb', ['-s', deviceId, 'shell', 'getprop ro.product.locale'], '.', true) .trim(); } - return deviceLocale; + return locale; } /// Returns locale of simulator with udid [udId]. @@ -271,7 +271,7 @@ List getAllConfiguredDeviceNames(Map configInfo) { return deviceNames; } -/// Get device from deviceName. +/// Get device for deviceName from list of devices. Map getDevice(List devices, String deviceName) { return devices.firstWhere( (device) => device['model'] == null @@ -280,6 +280,12 @@ Map getDevice(List devices, String deviceName) { orElse: () => null); } +/// Get device for deviceId from list of devices. +Map getDeviceFromId(List devices, String deviceId) { + return devices.firstWhere((device) => device['id'] == deviceId, + orElse: () => null); +} + /// Wait for message to appear in sys log and return first matching line Future waitSysLogMsg( String deviceId, RegExp regExp, String locale) async { diff --git a/pubspec.yaml b/pubspec.yaml index 7c8eb32b..0118d985 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: platform: ^2.2.0 process: ^3.0.9 meta: ^1.1.6 + intl: ^0.15.8 dev_dependencies: test: ^1.5.1+1 diff --git a/test/env_test.dart b/test/env_test.dart index 1d17eab5..dbb68dc8 100644 --- a/test/env_test.dart +++ b/test/env_test.dart @@ -7,17 +7,19 @@ void main() { final Config config = Config(configPath: 'test/screenshots_test.yaml'); final screens = await Screens(); await screens.init(); + final orientation='orientation'; final env = { 'screen_size': '1440x2560', - 'locale': 'en-US', + 'locale': 'en_US', 'device_name': 'Nexus 6P', 'device_type': 'android', + 'orientation': orientation }; // called by screenshots before test await config.storeEnv( - screens, env['device_name'], env['locale'], env['device_type']); + screens, env['device_name'], env['locale'], env['device_type'], orientation ); // called by test final Config testConfig = Config(configPath: 'test/screenshots_test.yaml'); diff --git a/test/screenshots_test.dart b/test/screenshots_test.dart index 112a9cdd..0c111a6d 100644 --- a/test/screenshots_test.dart +++ b/test/screenshots_test.dart @@ -7,6 +7,8 @@ import 'package:screenshots/src/config.dart'; import 'package:screenshots/src/daemon_client.dart'; import 'package:screenshots/src/globals.dart'; import 'package:screenshots/src/image_processor.dart'; +import 'package:screenshots/src/orientation.dart' as orient; +import 'package:screenshots/src/orientation.dart'; import 'package:screenshots/src/screens.dart'; import 'package:screenshots/src/resources.dart' as resources; import 'package:screenshots/src/run.dart' as run; @@ -202,15 +204,6 @@ void main() { '/tmp/screenshots/$kTestScreenshotsDir', 'my_prefix'); }); - test('config guide', () async { - final Screens screens = Screens(); - await screens.init(); - final Config config = Config(configPath: 'test/screenshots_test.yaml'); - final daemonClient = DaemonClient(); - await daemonClient.start; - config.generateConfigGuide(screens, await daemonClient.devices); - }); - test('rooted emulator', () async { final emulatorId = 'Nexus_5X_API_27'; final stagingDir = '/tmp/tmp'; @@ -265,8 +258,8 @@ void main() { test('change android locale', () async { final deviceName = 'Nexus 6P'; final emulatorId = 'Nexus_6P_API_28'; - final origLocale = 'en-US'; - final newLocale = 'fr-CA'; + final origLocale = 'en_US'; + final newLocale = 'fr_CA'; final daemonClient = DaemonClient(); await daemonClient.start; daemonClient.verbose = true; @@ -286,10 +279,13 @@ void main() { final simulatorName = 'iPhone X'; final simulatorInfo = utils.getHighestIosSimulator(utils.getIosSimulators(), simulatorName); - // note: daemonClient should get an 'add.device' event after simulator startup final deviceId = simulatorInfo['udid']; - run.startSimulator(deviceId); - run.shutdownSimulator(deviceId); + final daemonClient = DaemonClient(); + daemonClient.verbose = true; + await daemonClient.start; + await run.startSimulator(daemonClient, deviceId); + await run.shutdownSimulator(deviceId); + await daemonClient.stop; }); test('start emulator on travis', () async { @@ -310,26 +306,12 @@ void main() { ProcessStartMode.detached); }); - // reproduce https://github.com/flutter/flutter/issues/27785 - // on android (hangs during test) - // tested on android emulator in default locale (en-US) and it worked - // tested on android emulator in automatically changed to locale fr-CA and it hangs - // tested on android emulator booted in locale fr-CA and it hangs -// [trace] FlutterDriver: Isolate found with number: 939713595 -// [trace] FlutterDriver: Isolate is paused at start. -// [trace] FlutterDriver: Attempting to resume isolate -// [trace] FlutterDriver: Waiting for service extension -// [info ] FlutterDriver: Connected to Flutter application. -// 00:04 +0: end-to-end test tap on the floating action button; verify counter -// [warning] FlutterDriver: waitFor message is taking a long time to complete... -// hangs test('change locale on android and test', () async { final emulatorId = 'Nexus_6P_API_28'; final deviceName = 'any device name'; final stagingDir = '/tmp/tmp'; - final origLocale = 'en-US'; - final newLocale = 'en-US'; // succeeds -// final newLocale = 'fr-CA'; // fails + final origLocale = 'en_US'; + final newLocale = 'fr_CA'; final testAppDir = 'example'; final testAppSrcPath = 'test_driver/main.dart'; @@ -338,6 +320,7 @@ void main() { final daemonClient = DaemonClient(); await daemonClient.start; + // start emulator final deviceId = await daemonClient.launchEmulator(emulatorId); @@ -347,17 +330,17 @@ void main() { // run test await utils.streamCmd('flutter', ['drive', testAppSrcPath], testAppDir); - // stop emulator + // restore orig locale await run.setEmulatorLocale(deviceId, origLocale, deviceName); + + // stop emulator expect(await run.shutdownAndroidEmulator(daemonClient, deviceId), deviceId); - }, - timeout: - Timeout(Duration(seconds: 90))); // increase time to get stacktrace + }, timeout: Timeout(Duration(seconds: 90))); test('get android device locale', () async { final emulatorId = 'Nexus_6P_API_28'; final stagingDir = '/tmp/tmp'; - final locale = 'en-US'; + final locale = 'en_US'; await resources.unpackScripts(stagingDir); final daemonClient = DaemonClient(); @@ -369,49 +352,46 @@ void main() { expect(deviceLocale, locale); }); - // reproduce https://github.com/flutter/flutter/issues/27785 - // on ios - // tested on ios device in default locale (en-US) and it worked - // tested on ios device in manually changed to locale fr-CA and it hangs - // tested on ios simulator in default locale (en-US) and it worked - // tested on ios simulator in automatically changed to locale fr-CA and it hangs test('change locale on iOS and test', () async { final simulatorName = 'iPhone X'; final stagingDir = '/tmp/tmp'; - final locale = 'en-US'; // default locale (works) -// final locale = 'fr-CA'; // fails + final origLocale = 'en_US'; + final locale = 'fr_CA'; final testAppDir = 'example'; final testAppSrcPath = 'test_driver/main.dart'; // unpack resources await resources.unpackScripts(stagingDir); + final daemonClient = DaemonClient(); + await daemonClient.start; + // change locale final simulatorInfo = utils.getHighestIosSimulator(utils.getIosSimulators(), simulatorName); final deviceId = simulatorInfo['udid']; - await run.setSimulatorLocale(deviceId, simulatorName, locale, stagingDir, - running: false); + await run.setSimulatorLocale( + deviceId, simulatorName, locale, stagingDir, daemonClient); // start simulator -// final daemonClient = DaemonClient(); -// await daemonClient.start; - run.startSimulator(deviceId); + await run.startSimulator(daemonClient, deviceId); // run test await utils.streamCmd( 'flutter', ['-d', deviceId, 'drive', testAppSrcPath], testAppDir); // stop simulator - run.shutdownSimulator(deviceId); - }, - // increase time to get stacktrace - timeout: Timeout(Duration(minutes: 2))); + await run.shutdownSimulator(deviceId); + + // restore orig locale + await run.setSimulatorLocale( + deviceId, simulatorName, origLocale, stagingDir, daemonClient); + }, timeout: Timeout(Duration(seconds: 90))); test('get ios simulator locale', () async { final udId = '03D4FC12-3927-4C8B-A226-17DE34AE9C18'; var locale = utils.getIosSimulatorLocale(udId); - expect(locale, 'en-US'); + expect(locale, 'en_US'); }); test('get avd from a running emulator', () async { @@ -519,7 +499,7 @@ devices: }); test('scan syslog for string', () async { - final toLocale = 'en-US'; + final toLocale = 'en_US'; // final expected = // 'ContactsProvider: Locale has changed from [fr_CA] to [en_US]'; // final expected = RegExp('Locale has changed from'); @@ -536,7 +516,7 @@ devices: }); test('reg exp', () { - final locale = 'fr-CA'; + final locale = 'fr_CA'; final line = 'ContactsProvider: Locale has changed from [en_US] to [${locale.replaceFirst('-', '_')}]'; // final regExp = RegExp( @@ -666,4 +646,116 @@ devices: print('adbPath=$_adbPath'); }); }); + + group('manage device orientation', () { + test('find ios simulator orientation', () async { + final udId = '03D4FC12-3927-4C8B-A226-17DE34AE9C18'; + final env = Platform.environment; + final preferencesDir = + '${env['HOME']}/Library/Developer/CoreSimulator/Devices/$udId/data/Library/Preferences'; + await Directory(preferencesDir).listSync().forEach((fsEntity) { + // print contents + final filePath = fsEntity.path; + print('filePath=$filePath'); + try { + final contents = run.cmd('plutil', + ['-convert', 'xml1', '-r', '-o', '-', filePath], '.', true); + print('contents=$contents'); + } catch (e) { + print('error: $e'); + } + }); + }); + + test('set ios simulator orientation', () async { + final scriptDir = 'lib/resources/script'; + final simulatorName = 'iPhone 7 Plus'; + final simulatorInfo = + utils.getHighestIosSimulator(utils.getIosSimulators(), simulatorName); + final deviceId = simulatorInfo['udid']; + final daemonClient = DaemonClient(); + daemonClient.verbose = true; + await daemonClient.start; + await run.startSimulator(daemonClient, deviceId); + await Future.delayed(Duration(milliseconds: 5000)); // finish booting + orient.changeDeviceOrientation( + DeviceType.ios, orient.Orientation.LandscapeRight, + scriptDir: scriptDir); + await Future.delayed(Duration(milliseconds: 3000)); + orient.changeDeviceOrientation( + DeviceType.ios, orient.Orientation.Portrait, + scriptDir: scriptDir); + await Future.delayed(Duration(milliseconds: 1000)); + await run.shutdownSimulator(deviceId); + await daemonClient.stop; + }); + + test('set android emulator orientation', () async { + final emulatorId = 'Nexus_6P_API_28'; + final daemonClient = DaemonClient(); + await daemonClient.start; + final deviceId = await daemonClient.launchEmulator(emulatorId); + orient.changeDeviceOrientation( + DeviceType.android, orient.Orientation.LandscapeRight, + deviceId: deviceId); + await Future.delayed(Duration(milliseconds: 3000)); + orient.changeDeviceOrientation( + DeviceType.android, orient.Orientation.Portrait, + deviceId: deviceId); + await Future.delayed(Duration(milliseconds: 3000)); + expect( + await run.shutdownAndroidEmulator(daemonClient, deviceId), deviceId); + }); + }); + + group('config validate', () { + test('config guide', () async { + final Screens screens = Screens(); + await screens.init(); + final Config config = Config(configPath: 'test/screenshots_test.yaml'); + final daemonClient = DaemonClient(); + await daemonClient.start; + config.generateConfigGuide(screens, await daemonClient.devices); + }); + + test('validate device params', () { + final deviceName = 'ios device 1'; + final orientation = 'Portrait'; + final frame = true; + final params = ''' + devices: + ios: + $deviceName: + orientation: $orientation + frame: $frame + ios device 2: + android: + android device 1: + android device 2: + fuschia: + '''; + final configInfo = loadYaml(params); + final deviceNames = utils.getAllConfiguredDeviceNames(configInfo); + for (final devName in deviceNames) { + final deviceInfo = findDeviceInfo(configInfo, devName); + print('devName=$devName'); + print('deviceInfo=$deviceInfo'); + if (deviceInfo != null) { + expect(deviceInfo['orientation'], orientation); + expect(isValidOrientation(orientation), isTrue); + expect(isValidOrientation('bad orientation'), isFalse); + expect(deviceInfo['frame'], frame); + expect(isValidFrame(frame), isTrue); + expect(isValidFrame('bad frame'), isFalse); + } + } + }); + + test('valid values for params', () { + print(Orientation.values); + for (final orientation in Orientation.values) { + print('${utils.getStringFromEnum(orientation)}'); + } + }); + }); } diff --git a/test/screenshots_yaml_test.dart b/test/screenshots_yaml_test.dart index de9a172d..e719aed6 100644 --- a/test/screenshots_yaml_test.dart +++ b/test/screenshots_yaml_test.dart @@ -19,9 +19,9 @@ staging: /tmp/screenshots # A list of locales supported in app locales: -# - fr-CA - - en-US -# - de-DE +# - fr_CA + - en_US +# - de_DE # A list of devices to emulate devices: @@ -49,7 +49,7 @@ void main() { test('config info for app from string', () { final expected = { 'tests': ['example/test_driver/main.dart'], - 'locales': ['en-US'], + 'locales': ['en_US'], 'frame': true, 'devices': { 'android': {'Nexus 5X': null}, @@ -68,7 +68,7 @@ void main() { test('config info for app from file', () { final expected = { 'tests': ['example/test_driver/main.dart'], - 'locales': ['en-US'], + 'locales': ['en_US'], 'frame': true, 'devices': { 'android': {'Nexus 5X': null},