blob: 522cc5742c4777e00dfd8387ccac5536ab7c6c47 [file]
// 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:js_interop';
import 'dart:js_util' as js_util;
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../common/matchers.dart';
const int kPhysicalKeyA = 0x00070004;
const int kLogicalKeyA = 0x00000000061;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
late EngineFlutterWindow myWindow;
final EnginePlatformDispatcher dispatcher = EnginePlatformDispatcher.instance;
setUp(() {
myWindow = EngineFlutterView.implicit(dispatcher, createDomHTMLDivElement());
dispatcher.viewManager.registerView(myWindow);
});
tearDown(() async {
dispatcher.viewManager.unregisterView(myWindow.viewId);
await myWindow.resetHistory();
myWindow.dispose();
});
test('onTextScaleFactorChanged preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback() {
expect(Zone.current, innerZone);
}
myWindow.onTextScaleFactorChanged = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onTextScaleFactorChanged, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnTextScaleFactorChanged();
});
test('onPlatformBrightnessChanged preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback() {
expect(Zone.current, innerZone);
}
myWindow.onPlatformBrightnessChanged = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onPlatformBrightnessChanged, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnPlatformBrightnessChanged();
});
test('onMetricsChanged preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback() {
expect(Zone.current, innerZone);
}
myWindow.onMetricsChanged = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onMetricsChanged, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
});
test('onLocaleChanged preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback() {
expect(Zone.current, innerZone);
}
myWindow.onLocaleChanged = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onLocaleChanged, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnLocaleChanged();
});
test('onBeginFrame preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback(Duration _) {
expect(Zone.current, innerZone);
}
myWindow.onBeginFrame = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onBeginFrame, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnBeginFrame(Duration.zero);
});
test('onReportTimings preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback(List<dynamic> _) {
expect(Zone.current, innerZone);
}
myWindow.onReportTimings = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onReportTimings, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnReportTimings(<ui.FrameTiming>[]);
});
test('onDrawFrame preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback() {
expect(Zone.current, innerZone);
}
myWindow.onDrawFrame = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onDrawFrame, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnDrawFrame();
});
test('onPointerDataPacket preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback(ui.PointerDataPacket _) {
expect(Zone.current, innerZone);
}
myWindow.onPointerDataPacket = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onPointerDataPacket, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnPointerDataPacket(const ui.PointerDataPacket());
});
test('invokeOnKeyData returns normally when onKeyData is null', () {
const ui.KeyData keyData = ui.KeyData(
timeStamp: Duration(milliseconds: 1),
type: ui.KeyEventType.repeat,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: 'a',
synthesized: true,
);
expect(() {
EnginePlatformDispatcher.instance.invokeOnKeyData(keyData, (bool result) {
expect(result, isFalse);
});
}, returnsNormally);
});
test('onKeyData preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
bool onKeyData(ui.KeyData _) {
expect(Zone.current, innerZone);
return false;
}
myWindow.onKeyData = onKeyData;
// Test that the getter returns the exact same onKeyData, e.g. it doesn't
// wrap it.
expect(myWindow.onKeyData, same(onKeyData));
});
const ui.KeyData keyData = ui.KeyData(
timeStamp: Duration(milliseconds: 1),
type: ui.KeyEventType.repeat,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: 'a',
synthesized: true,
);
EnginePlatformDispatcher.instance.invokeOnKeyData(keyData, (bool result) {
expect(result, isFalse);
});
myWindow.onKeyData = null;
});
test('onSemanticsEnabledChanged preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback() {
expect(Zone.current, innerZone);
}
myWindow.onSemanticsEnabledChanged = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onSemanticsEnabledChanged, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnSemanticsEnabledChanged();
});
test('onSemanticsActionEvent preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback(ui.SemanticsActionEvent _) {
expect(Zone.current, innerZone);
}
ui.PlatformDispatcher.instance.onSemanticsActionEvent = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(ui.PlatformDispatcher.instance.onSemanticsActionEvent, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(0, ui.SemanticsAction.tap, null);
});
test('onAccessibilityFeaturesChanged preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback() {
expect(Zone.current, innerZone);
}
myWindow.onAccessibilityFeaturesChanged = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onAccessibilityFeaturesChanged, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnAccessibilityFeaturesChanged();
});
test('onPlatformMessage preserves the zone', () {
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
void callback(String _, ByteData? __, void Function(ByteData?)? ___) {
expect(Zone.current, innerZone);
}
myWindow.onPlatformMessage = callback;
// Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
expect(myWindow.onPlatformMessage, same(callback));
});
EnginePlatformDispatcher.instance.invokeOnPlatformMessage('foo', null, (ByteData? data) {
// Not testing anything here.
});
});
test('sendPlatformMessage preserves the zone', () async {
final Completer<void> completer = Completer<void>();
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
final ByteData inputData = ByteData(4);
inputData.setUint32(0, 42);
myWindow.sendPlatformMessage(
'flutter/debug-echo',
inputData,
(ByteData? outputData) {
expect(Zone.current, innerZone);
completer.complete();
},
);
});
await completer.future;
});
test('sendPlatformMessage responds even when channel is unknown', () async {
bool responded = false;
final ByteData inputData = ByteData(4);
inputData.setUint32(0, 42);
myWindow.sendPlatformMessage(
'flutter/__unknown__channel__',
null,
(ByteData? outputData) {
responded = true;
expect(outputData, isNull);
},
);
await Future<void>.delayed(const Duration(milliseconds: 1));
expect(responded, isTrue);
});
// Emulates the framework sending a request for screen orientation lock.
Future<bool> sendSetPreferredOrientations(List<dynamic> orientations) {
final Completer<bool> completer = Completer<bool>();
final ByteData? inputData = const JSONMethodCodec().encodeMethodCall(MethodCall(
'SystemChrome.setPreferredOrientations',
orientations,
));
myWindow.sendPlatformMessage(
'flutter/platform',
inputData,
(ByteData? outputData) {
const MethodCodec codec = JSONMethodCodec();
completer.complete(codec.decodeEnvelope(outputData!) as bool);
},
);
return completer.future;
}
// Regression test for https://github.com/flutter/flutter/issues/88269
test('sets preferred screen orientation', () async {
final DomScreen? original = domWindow.screen;
final List<String> lockCalls = <String>[];
int unlockCount = 0;
bool simulateError = false;
// The `orientation` property cannot be overridden, so this test overrides the entire `screen`.
js_util.setProperty(domWindow, 'screen', js_util.jsify(<Object?, Object?>{
'orientation': <Object?, Object?>{
'lock': (String lockType) {
lockCalls.add(lockType);
return futureToPromise(() async {
if (simulateError) {
throw Error();
}
return 0.toJS;
}());
}.toJS,
'unlock': () {
unlockCount += 1;
}.toJS,
},
}));
// Sanity-check the test setup.
expect(lockCalls, <String>[]);
expect(unlockCount, 0);
await domWindow.screen!.orientation!.lock('hi');
domWindow.screen!.orientation!.unlock();
expect(lockCalls, <String>['hi']);
expect(unlockCount, 1);
lockCalls.clear();
unlockCount = 0;
expect(await sendSetPreferredOrientations(<dynamic>['DeviceOrientation.portraitUp']), isTrue);
expect(lockCalls, <String>[ScreenOrientation.lockTypePortraitPrimary]);
expect(unlockCount, 0);
lockCalls.clear();
unlockCount = 0;
expect(await sendSetPreferredOrientations(<dynamic>['DeviceOrientation.portraitDown']), isTrue);
expect(lockCalls, <String>[ScreenOrientation.lockTypePortraitSecondary]);
expect(unlockCount, 0);
lockCalls.clear();
unlockCount = 0;
expect(await sendSetPreferredOrientations(<dynamic>['DeviceOrientation.landscapeLeft']), isTrue);
expect(lockCalls, <String>[ScreenOrientation.lockTypeLandscapePrimary]);
expect(unlockCount, 0);
lockCalls.clear();
unlockCount = 0;
expect(await sendSetPreferredOrientations(<dynamic>['DeviceOrientation.landscapeRight']), isTrue);
expect(lockCalls, <String>[ScreenOrientation.lockTypeLandscapeSecondary]);
expect(unlockCount, 0);
lockCalls.clear();
unlockCount = 0;
expect(await sendSetPreferredOrientations(<dynamic>[]), isTrue);
expect(lockCalls, <String>[]);
expect(unlockCount, 1);
lockCalls.clear();
unlockCount = 0;
simulateError = true;
expect(await sendSetPreferredOrientations(<dynamic>['DeviceOrientation.portraitDown']), isFalse);
expect(lockCalls, <String>[ScreenOrientation.lockTypePortraitSecondary]);
expect(unlockCount, 0);
js_util.setProperty(domWindow, 'screen', original);
});
/// Regression test for https://github.com/flutter/flutter/issues/66128.
test("setPreferredOrientation responds even if browser doesn't support api", () async {
final DomScreen? original = domWindow.screen;
// The `orientation` property cannot be overridden, so this test overrides the entire `screen`.
js_util.setProperty(domWindow, 'screen', js_util.jsify(<Object?, Object?>{
'orientation': null,
}));
expect(domWindow.screen!.orientation, isNull);
expect(await sendSetPreferredOrientations(<dynamic>[]), isFalse);
js_util.setProperty(domWindow, 'screen', original);
});
test('SingletonFlutterWindow implements locale, locales, and locale change notifications', () async {
// This will count how many times we notified about locale changes.
int localeChangedCount = 0;
myWindow.onLocaleChanged = () {
localeChangedCount += 1;
};
// We populate the initial list of locales automatically (only test that we
// got some locales; some contributors may be in different locales, so we
// can't test the exact contents).
expect(myWindow.locale, isA<ui.Locale>());
expect(myWindow.locales, isNotEmpty);
// Trigger a change notification (reset locales because the notification
// doesn't actually change the list of languages; the test only observes
// that the list is populated again).
EnginePlatformDispatcher.instance.debugResetLocales();
expect(myWindow.locales, isEmpty);
expect(myWindow.locale, equals(const ui.Locale.fromSubtags()));
expect(localeChangedCount, 0);
domWindow.dispatchEvent(createDomEvent('Event', 'languagechange'));
expect(myWindow.locales, isNotEmpty);
expect(localeChangedCount, 1);
});
test('dispatches browser event on flutter/service_worker channel', () async {
final Completer<void> completer = Completer<void>();
domWindow.addEventListener('flutter-first-frame',
createDomEventListener((DomEvent e) => completer.complete()));
final Zone innerZone = Zone.current.fork();
innerZone.runGuarded(() {
myWindow.sendPlatformMessage(
'flutter/service_worker',
ByteData(0),
(ByteData? outputData) { },
);
});
await expectLater(completer.future, completes);
});
test('sets global html attributes', () {
final DomElement host = createDomHTMLDivElement();
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
expect(host.getAttribute('flt-renderer'), 'html (requested explicitly)');
expect(host.getAttribute('flt-build-mode'), 'debug');
view.dispose();
});
test('in full-page mode, Flutter window replaces viewport meta tags', () {
final DomHTMLMetaElement existingMeta = createDomHTMLMetaElement()
..name = 'viewport'
..content = 'foo=bar';
domDocument.head!.append(existingMeta);
expect(existingMeta.isConnected, isTrue);
final EngineFlutterWindow implicitView = EngineFlutterView.implicit(dispatcher, null);
// The existing viewport meta tag should've been removed.
expect(existingMeta.isConnected, isFalse);
// And a new one should've been added.
final DomHTMLMetaElement? newMeta = domDocument.head!.querySelector('meta[name="viewport"]') as DomHTMLMetaElement?;
expect(newMeta, isNotNull);
newMeta!;
expect(newMeta.getAttribute('flt-viewport'), isNotNull);
expect(newMeta.name, 'viewport');
expect(newMeta.content, contains('width=device-width'));
expect(newMeta.content, contains('initial-scale=1.0'));
expect(newMeta.content, contains('maximum-scale=1.0'));
expect(newMeta.content, contains('user-scalable=no'));
implicitView.dispose();
});
test('auto-view-id', () {
final DomElement host = createDomHTMLDivElement();
final EngineFlutterView implicit1 = EngineFlutterView.implicit(dispatcher, host);
final EngineFlutterView implicit2 = EngineFlutterView.implicit(dispatcher, host);
expect(implicit1.viewId, kImplicitViewId);
expect(implicit2.viewId, kImplicitViewId);
final EngineFlutterView view1 = EngineFlutterView(dispatcher, host);
final EngineFlutterView view2 = EngineFlutterView(dispatcher, host);
final EngineFlutterView view3 = EngineFlutterView(dispatcher, host);
expect(view1.viewId, isNot(kImplicitViewId));
expect(view2.viewId, isNot(kImplicitViewId));
expect(view3.viewId, isNot(kImplicitViewId));
expect(view1.viewId, isNot(view2.viewId));
expect(view2.viewId, isNot(view3.viewId));
expect(view3.viewId, isNot(view1.viewId));
implicit1.dispose();
implicit2.dispose();
view1.dispose();
view2.dispose();
view3.dispose();
});
test('registration', () {
final DomHTMLDivElement host = createDomHTMLDivElement();
final EnginePlatformDispatcher dispatcher = EnginePlatformDispatcher();
expect(dispatcher.viewManager.views, isEmpty);
// Creating the view shouldn't register it.
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
expect(dispatcher.viewManager.views, isEmpty);
dispatcher.viewManager.registerView(view);
expect(dispatcher.viewManager.views, <EngineFlutterView>[view]);
// Disposing the view shouldn't unregister it.
view.dispose();
expect(dispatcher.viewManager.views, <EngineFlutterView>[view]);
dispatcher.dispose();
});
test('dispose', () {
final DomHTMLDivElement host = createDomHTMLDivElement();
final EngineFlutterView view =
EngineFlutterView(EnginePlatformDispatcher.instance, host);
// First, let's make sure the view's root element was inserted into the
// host, and the dimensions provider is active.
expect(view.dom.rootElement.parentElement, host);
expect(view.dimensionsProvider.isClosed, isFalse);
// Now, let's dispose the view and make sure its root element was removed,
// and the dimensions provider is closed.
view.dispose();
expect(view.dom.rootElement.parentElement, isNull);
expect(view.dimensionsProvider.isClosed, isTrue);
// Can't render into a disposed view.
expect(
() => view.render(ui.SceneBuilder().build()),
throwsAssertionError,
);
// Can't update semantics on a disposed view.
expect(
() => view.updateSemantics(ui.SemanticsUpdateBuilder().build()),
throwsAssertionError,
);
});
group('resizing', () {
late DomHTMLDivElement host;
late EngineFlutterView view;
late int metricsChangedCount;
setUp(() async {
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.5);
host = createDomHTMLDivElement();
view = EngineFlutterView(EnginePlatformDispatcher.instance, host);
host.style
..width = '10px'
..height = '10px';
domDocument.body!.append(host);
// Let the DOM settle before starting the test, so we don't get the first
// 10,10 Size in the test. Otherwise, the ResizeObserver may trigger
// unexpectedly after the test has started, and break our "first" result.
await view.onResize.first;
metricsChangedCount = 0;
view.platformDispatcher.onMetricsChanged = () {
metricsChangedCount++;
};
});
tearDown(() {
view.dispose();
host.remove();
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
view.platformDispatcher.onMetricsChanged = null;
});
test('listens to resize', () async {
// Initial size is 10x10, with a 2.5 dpr, is equal to 25x25 physical pixels.
expect(view.physicalSize, const ui.Size(25.0, 25.0));
expect(metricsChangedCount, 0);
// Simulate the browser resizing the host to 20x20.
host.style
..width = '20px'
..height = '20px';
await view.onResize.first;
expect(view.physicalSize, const ui.Size(50.0, 50.0));
expect(metricsChangedCount, 1);
});
test('maintains debugPhysicalSizeOverride', () async {
// Initial size is 10x10, with a 2.5 dpr, is equal to 25x25 physical pixels.
expect(view.physicalSize, const ui.Size(25.0, 25.0));
view.debugPhysicalSizeOverride = const ui.Size(100.0, 100.0);
view.debugForceResize();
expect(view.physicalSize, const ui.Size(100.0, 100.0));
// Resize the host to 20x20.
host.style
..width = '20px'
..height = '20px';
await view.onResize.first;
// The view should maintain the debugPhysicalSizeOverride.
expect(view.physicalSize, const ui.Size(100.0, 100.0));
});
test('can resize host', () async {
// Reset host style, so it tightly wraps the rootElement of the view.
// This style change will trigger a "onResize" event when all the DOM
// operations settle that we must await before taking measurements.
host.style
..display = 'inline-block'
..width = 'auto'
..height = 'auto';
// Resize the host to 20x20 (physical pixels).
view.resize(const ui.Size.square(50));
await view.onResize.first;
// The host tightly wraps the rootElement:
expect(view.physicalSize, const ui.Size(50.0, 50.0));
// Inspect the rootElement directly:
expect(view.dom.rootElement.clientWidth, 50 / view.devicePixelRatio);
expect(view.dom.rootElement.clientHeight, 50 / view.devicePixelRatio);
});
});
group('physicalConstraints', () {
const double dpr = 2.5;
late DomHTMLDivElement host;
late EngineFlutterView view;
setUp(() async {
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(dpr);
host = createDomHTMLDivElement()
..style.width = '640px'
..style.height = '480px';
domDocument.body!.append(host);
});
tearDown(() {
host.remove();
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
});
test('JsViewConstraints are passed and used to compute physicalConstraints', () async {
view = EngineFlutterView(
EnginePlatformDispatcher.instance,
host,
viewConstraints: JsViewConstraints(
minHeight: 320,
maxHeight: double.infinity,
));
// All the metrics until now have been expressed in logical pixels, because
// they're coming from CSS/the browser, which works in logical pixels.
expect(view.physicalConstraints, const ViewConstraints(
minHeight: 320,
// ignore: avoid_redundant_argument_values
maxHeight: double.infinity,
minWidth: 640,
maxWidth: 640,
// However the framework expects physical pixels, so we multiply our expectations
// by the current DPR (2.5)
) * dpr);
});
});
}