blob: df856de4fff83135e11a21b5db2dc76a1aaef9bd [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 'package:uuid/uuid.dart';
import 'browser.dart';
import 'browser_lock.dart';
import 'browser_process.dart';
import 'environment.dart';
import 'safari_installation.dart';
import 'utils.dart';
/// Info about the screen layout for the current version of iOS Safari.
///
/// This is used to properly take screenshots of the browser.
class SafariScreenInfo {
final int heightOfHeader;
final int heightOfFooter;
final double scaleFactor;
SafariScreenInfo(this.heightOfHeader, this.heightOfFooter, this.scaleFactor);
}
/// Provides an environment for the mobile variant of Safari running in an iOS
/// simulator.
class SafariIosEnvironment implements BrowserEnvironment {
late final SafariScreenInfo _screenInfo;
@override
final String name = 'Safari iOS';
@override
Future<Browser> launchBrowserInstance(Uri url, {bool debug = false}) async {
return SafariIos(url, _screenInfo);
}
@override
Runtime get packageTestRuntime => Runtime.safari;
@override
Future<void> prepare() async {
final SafariIosLock lock = browserLock.safariIosLock;
_screenInfo = SafariScreenInfo(
lock.heightOfHeader, lock.heightOfFooter, 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);
await initIosSimulator();
}
@override
Future<void> cleanup() async {}
@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 {
final BrowserProcess _process;
final SafariScreenInfo _screenInfo;
/// Starts a new instance of Safari open to the given [url], which may be a
/// [Uri].
factory SafariIos(Uri url, SafariScreenInfo screenInfo) {
return SafariIos._(BrowserProcess(() 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;
}), screenInfo);
}
SafariIos._(this._process, this._screenInfo);
@override
Future<void> get onExit => _process.onExit;
@override
Future<void> close() => _process.close();
@override
bool get supportsScreenshots => true;
@override
/// 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> captureScreenshot(math.Rectangle<num>? region) async {
final String screenshotTag = const Uuid().v4();
final String filename = 'screenshot$screenshotTag.png';
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 $filename.');
}
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,
_screenInfo.heightOfHeader,
screenshot.width,
screenshot.height -
_screenInfo.heightOfFooter -
_screenInfo.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 * _screenInfo.scaleFactor,
region.top * _screenInfo.scaleFactor,
region.width * _screenInfo.scaleFactor,
region.height * _screenInfo.scaleFactor,
);
}
}