blob: 0dd41c08c8b9ecd6cdc54927af754e0b525908e5 [file] [log] [blame]
// Copyright 2013 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.
import 'dart:async';
import 'dart:io' as io;
import 'dart:math' as math;
import 'package:image/image.dart';
import 'package:path/path.dart' as path;
import 'package:test_api/src/backend/runtime.dart';
import 'browser.dart';
import 'browser_lock.dart';
import 'environment.dart';
import 'safari_installation.dart';
import 'utils.dart';
/// Provides an environment for the mobile variant of Safari running in an iOS
/// simulator.
class SafariIosEnvironment implements BrowserEnvironment {
@override
Browser launchBrowserInstance(Uri url, {bool debug = false}) {
return SafariIos(url);
}
@override
Runtime get packageTestRuntime => Runtime.safari;
@override
Future<void> prepare() async {
await initIosSimulator();
}
@override
ScreenshotManager? getScreenshotManager() {
return SafariIosScreenshotManager();
}
@override
String get packageTestConfigurationYamlFile => 'dart_test_safari.yaml';
}
/// Runs an instance of Safari for iOS (i.e. mobile Safari).
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is
/// constructed, and is killed when [close] is called.
///
/// Any errors starting or running the process are reported through [onExit].
class SafariIos extends Browser {
@override
final String name = 'Safari iOS';
/// Starts a new instance of Safari open to the given [url], which may be a
/// [Uri].
factory SafariIos(Uri url) {
return SafariIos._(() async {
// iOS-Safari
// Uses `xcrun simctl`. It is a command line utility to control the
// Simulator. For more details on interacting with the simulator:
// https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/iOS_Simulator_Guide/InteractingwiththeiOSSimulator/InteractingwiththeiOSSimulator.html
final io.Process process = await io.Process.start('xcrun', <String>[
'simctl',
'openurl', // Opens the url on Safari installed on the simulator.
'booted', // The simulator is already booted.
url.toString(),
]);
return process;
});
}
SafariIos._(Future<io.Process> Function() startBrowser) : super(startBrowser);
}
/// [ScreenshotManager] implementation for Safari.
///
/// This manager will only be created/used for macOS.
class SafariIosScreenshotManager extends ScreenshotManager {
@override
String get filenameSuffix => '.iOS_Safari';
SafariIosScreenshotManager() {
final SafariIosLock lock = browserLock.safariIosLock;
_heightOfHeader = lock.heightOfHeader;
_heightOfFooter = lock.heightOfFooter;
_scaleFactor = lock.scaleFactor;
/// Create the directory to use for taking screenshots, if it does not
/// exists.
if (!environment.webUiSimulatorScreenshotsDirectory.existsSync()) {
environment.webUiSimulatorScreenshotsDirectory.createSync();
}
// Temporary directories are deleted in the clenaup phase of after `felt`
// runs the tests.
temporaryDirectories.add(environment.webUiSimulatorScreenshotsDirectory);
}
/// This scale factor is used to enlarge/shrink the screenshot region
/// sent from the tests.
/// For more details see [_scaleScreenshotRegion(region)].
late final double _scaleFactor;
/// Height of the part to crop from the top of the image.
///
/// `xcrun simctl` command takes the screenshot of the entire simulator. We
/// are cropping top bit from screenshot, otherwise due to the clock on top of
/// the screen, the screenshot will differ between each run.
/// Note that this gap can change per phone and per iOS version. For more
/// details refer to `browser_lock.yaml` file.
late final int _heightOfHeader;
/// Height of the part to crop from the bottom of the image.
///
/// This area is the footer navigation bar of the phone, it is not the area
/// used by tests (which is inside the browser).
late final int _heightOfFooter;
/// Used as a suffix for the temporary file names used for screenshots.
int _fileNameCounter = 0;
/// Capture a screenshot of entire simulator.
///
/// Example screenshot with dimensions: W x H.
///
/// <---------- W ------------->
/// _____________________________
/// | Phone Top bar (clock etc.) | Ʌ
/// |_____________________________| |
/// | Broswer search bar | |
/// |_____________________________| |
/// | Web page content | |
/// | | |
/// | |
/// | | H
/// | |
/// | | |
/// | | |
/// | | |
/// | | |
/// |_____________________________| |
/// | Phone footer bar | |
/// |_____________________________| V
///
/// After taking the screenshot, the image is cropped as heigh as
/// [_heightOfHeader] and [_heightOfFooter] from the top and bottom parts
/// consecutively. Hence web content has the dimensions:
///
/// W x (H - [_heightOfHeader] - [_heightOfFooter])
///
/// [region] is used to decide which part of the web content will be used in
/// test image. It includes starting coordinate x,y as well as height and
/// width of the area to capture.
///
/// Uses simulator tool `xcrun simctl`'s 'screenshot' command.
@override
Future<Image> capture(math.Rectangle<num>? region) async {
final String filename = 'screenshot$_fileNameCounter.png';
_fileNameCounter++;
await iosSimulator.takeScreenshot(
filename, environment.webUiSimulatorScreenshotsDirectory,
);
final io.File file = io.File(path.join(
environment.webUiSimulatorScreenshotsDirectory.path, filename));
List<int> imageBytes;
if (!file.existsSync()) {
throw Exception('Failed to read the screenshot '
'screenshot$_fileNameCounter.png.');
}
imageBytes = await file.readAsBytes();
file.deleteSync();
final Image screenshot = decodePng(imageBytes)!;
// Create an image with no footer and header. The _heightOfHeader,
// _heightOfFooter values are already in real coordinates therefore
// they don't need to be scaled.
final Image content = copyCrop(
screenshot,
0,
_heightOfHeader,
screenshot.width,
screenshot.height - _heightOfFooter - _heightOfHeader,
);
if (region == null) {
return content;
} else {
final math.Rectangle<num> scaledRegion = _scaleScreenshotRegion(region);
return copyCrop(
content,
scaledRegion.left.toInt(),
scaledRegion.top.toInt(),
scaledRegion.width.toInt(),
scaledRegion.height.toInt(),
);
}
}
/// Perform a linear transform on the screenshot region to convert its
/// dimensions from linear coordinated to coordinated on the phone screen.
/// This uniform/isotropic scaling is done using [_scaleFactor].
math.Rectangle<num> _scaleScreenshotRegion(math.Rectangle<num> region) {
return math.Rectangle<num>(
region.left * _scaleFactor,
region.top * _scaleFactor,
region.width * _scaleFactor,
region.height * _scaleFactor,
);
}
}