mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Current implementation runs timers and microtask callbacks in the root zone. That assumes that the top-level `scheduleMicrotask` or `Timer` constructors have been used, which have so far wrapped the callback with `runCallbackGuarded` before calling the zone implementation. That means that doing `zone.scheduleMicrotask` directly would not ensure that the microtask was run in the correct zone. If a `run` handler throws, it wouldn't be caught. This change makes the `realAsyncZone` do whatever the root zone would do if its `ZoneDelegate` got called with the intended zone and arguments. That should be consistent with the current behavior, and be compatible with incoming bug-fixes to the platform `Zone` behavior. Prepares Flutter for landing https://dart-review.googlesource.com/c/sdk/+/406961 which is currently blocked (so this indirectly fixes https://github.com/dart-lang/sdk/issues/59913). There are no new tests, the goal is that all existing tests keep running, and that they keep doing so when the Dart CL lands. Currently that CL only breaks one test, the `dev/automated_tests/test_smoke_test/fail_test_on_exception_after_test.dart` test which threw the error-after-test in the root zone instead of the test zone. This change fixes that.
878 lines
28 KiB
Dart
878 lines
28 KiB
Dart
// Copyright 2014 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';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:matcher/expect.dart' as matcher;
|
|
import 'package:matcher/src/expect/async_matcher.dart';
|
|
|
|
import 'multi_view_testing.dart';
|
|
|
|
void main() {
|
|
group('expectLater', () {
|
|
testWidgets('completes when matcher completes', (WidgetTester tester) async {
|
|
final Completer<void> completer = Completer<void>();
|
|
final Future<void> future = expectLater(null, FakeMatcher(completer));
|
|
String? result;
|
|
future.then<void>((void value) {
|
|
result = '123';
|
|
});
|
|
matcher.expect(result, isNull);
|
|
completer.complete();
|
|
matcher.expect(result, isNull);
|
|
await future;
|
|
await tester.pump();
|
|
matcher.expect(result, '123');
|
|
});
|
|
|
|
testWidgets('respects the skip flag', (WidgetTester tester) async {
|
|
final Completer<void> completer = Completer<void>();
|
|
final Future<void> future = expectLater(
|
|
null,
|
|
FakeMatcher(completer),
|
|
skip: 'testing skip',
|
|
); // [intended] API testing
|
|
bool completed = false;
|
|
future.then<void>((_) {
|
|
completed = true;
|
|
});
|
|
matcher.expect(completed, isFalse);
|
|
await future;
|
|
matcher.expect(completed, isTrue);
|
|
});
|
|
});
|
|
|
|
group('group retry flag allows test to run multiple times', () {
|
|
bool retried = false;
|
|
group('the group with retry flag', () {
|
|
testWidgets('the test inside it', (WidgetTester tester) async {
|
|
addTearDown(() => retried = true);
|
|
if (!retried) {
|
|
debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
|
|
}
|
|
expect(retried, isTrue);
|
|
});
|
|
}, retry: 1);
|
|
});
|
|
|
|
group('testWidget retry flag allows test to run multiple times', () {
|
|
bool retried = false;
|
|
testWidgets('the test with retry flag', (WidgetTester tester) async {
|
|
addTearDown(() => retried = true);
|
|
if (!retried) {
|
|
debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
|
|
}
|
|
expect(retried, isTrue);
|
|
}, retry: 1);
|
|
});
|
|
|
|
group('respects the group skip flag', () {
|
|
testWidgets('should be skipped', (WidgetTester tester) async {
|
|
expect(false, true);
|
|
});
|
|
}, skip: true); // [intended] API testing
|
|
|
|
group('pumping', () {
|
|
testWidgets('pumping', (WidgetTester tester) async {
|
|
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
|
|
int count;
|
|
|
|
final AnimationController test = AnimationController(
|
|
duration: const Duration(milliseconds: 5100),
|
|
vsync: tester,
|
|
);
|
|
count = await tester.pumpAndSettle(const Duration(seconds: 1));
|
|
expect(count, 1); // it always pumps at least one frame
|
|
|
|
test.forward(from: 0.0);
|
|
count = await tester.pumpAndSettle(const Duration(seconds: 1));
|
|
// 1 frame at t=0, starting the animation
|
|
// 1 frame at t=1
|
|
// 1 frame at t=2
|
|
// 1 frame at t=3
|
|
// 1 frame at t=4
|
|
// 1 frame at t=5
|
|
// 1 frame at t=6, ending the animation
|
|
expect(count, 7);
|
|
|
|
test.forward(from: 0.0);
|
|
await tester.pump(); // starts the animation
|
|
count = await tester.pumpAndSettle(const Duration(seconds: 1));
|
|
expect(count, 6);
|
|
|
|
test.forward(from: 0.0);
|
|
await tester.pump(); // starts the animation
|
|
await tester.pump(); // has no effect
|
|
count = await tester.pumpAndSettle(const Duration(seconds: 1));
|
|
expect(count, 6);
|
|
});
|
|
|
|
testWidgets('pumpFrames', (WidgetTester tester) async {
|
|
final List<int> logPaints = <int>[];
|
|
int? initial;
|
|
|
|
final Widget target = _AlwaysAnimating(
|
|
onPaint: () {
|
|
final int current = SchedulerBinding.instance.currentFrameTimeStamp.inMicroseconds;
|
|
initial ??= current;
|
|
logPaints.add(current - initial!);
|
|
},
|
|
);
|
|
|
|
await tester.pumpFrames(target, const Duration(milliseconds: 55));
|
|
|
|
// `pumpframes` defaults to 16 milliseconds and 683 microseconds per pump,
|
|
// so we expect 4 pumps of 16683 microseconds each in the 55ms duration.
|
|
expect(logPaints, <int>[0, 16683, 33366, 50049]);
|
|
logPaints.clear();
|
|
|
|
await tester.pumpFrames(
|
|
target,
|
|
const Duration(milliseconds: 30),
|
|
const Duration(milliseconds: 10),
|
|
);
|
|
|
|
// Since `pumpFrames` was given a 10ms interval per pump, we expect the
|
|
// results to continue from 50049 with 10000 microseconds per pump over
|
|
// the 30ms duration.
|
|
expect(logPaints, <int>[60049, 70049, 80049]);
|
|
});
|
|
});
|
|
group('pageBack', () {
|
|
testWidgets('fails when there are no back buttons', (WidgetTester tester) async {
|
|
await tester.pumpWidget(Container());
|
|
|
|
expect(expectAsync0(tester.pageBack), throwsA(isA<TestFailure>()));
|
|
});
|
|
|
|
testWidgets('successfully taps material back buttons', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
return ElevatedButton(
|
|
child: const Text('Next'),
|
|
onPressed: () {
|
|
Navigator.push<void>(
|
|
context,
|
|
MaterialPageRoute<void>(
|
|
builder: (BuildContext context) {
|
|
return Scaffold(appBar: AppBar(title: const Text('Page 2')));
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Next'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
|
|
await tester.pageBack();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
|
|
expect(find.text('Next'), findsOneWidget);
|
|
expect(find.text('Page 2'), findsNothing);
|
|
});
|
|
|
|
testWidgets('successfully taps cupertino back buttons', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
return CupertinoButton(
|
|
child: const Text('Next'),
|
|
onPressed: () {
|
|
Navigator.push<void>(
|
|
context,
|
|
CupertinoPageRoute<void>(
|
|
builder: (BuildContext context) {
|
|
return CupertinoPageScaffold(
|
|
navigationBar: const CupertinoNavigationBar(middle: Text('Page 2')),
|
|
child: Container(),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Next'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
|
|
await tester.pageBack();
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Next'), findsOneWidget);
|
|
expect(find.text('Page 2'), findsNothing);
|
|
});
|
|
});
|
|
|
|
testWidgets('hasRunningAnimations control test', (WidgetTester tester) async {
|
|
final AnimationController controller = AnimationController(
|
|
duration: const Duration(seconds: 1),
|
|
vsync: const TestVSync(),
|
|
);
|
|
expect(tester.hasRunningAnimations, isFalse);
|
|
controller.forward();
|
|
expect(tester.hasRunningAnimations, isTrue);
|
|
controller.stop();
|
|
expect(tester.hasRunningAnimations, isFalse);
|
|
controller.forward();
|
|
expect(tester.hasRunningAnimations, isTrue);
|
|
await tester.pumpAndSettle();
|
|
expect(tester.hasRunningAnimations, isFalse);
|
|
});
|
|
|
|
testWidgets('pumpAndSettle control test', (WidgetTester tester) async {
|
|
final AnimationController controller = AnimationController(
|
|
duration: const Duration(minutes: 525600),
|
|
vsync: const TestVSync(),
|
|
);
|
|
expect(await tester.pumpAndSettle(), 1);
|
|
controller.forward();
|
|
try {
|
|
await tester.pumpAndSettle();
|
|
expect(true, isFalse);
|
|
} catch (e) {
|
|
expect(e, isFlutterError);
|
|
}
|
|
controller.stop();
|
|
expect(await tester.pumpAndSettle(), 1);
|
|
controller.duration = const Duration(seconds: 1);
|
|
controller.forward();
|
|
expect(
|
|
await tester.pumpAndSettle(const Duration(milliseconds: 300)),
|
|
5,
|
|
); // 0, 300, 600, 900, 1200ms
|
|
});
|
|
|
|
testWidgets('Input event array', (WidgetTester tester) async {
|
|
final List<String> logs = <String>[];
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Listener(
|
|
onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'),
|
|
onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'),
|
|
onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'),
|
|
child: const Text('test'),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset location = tester.getCenter(find.text('test'));
|
|
final List<PointerEventRecord> records = <PointerEventRecord>[
|
|
PointerEventRecord(Duration.zero, <PointerEvent>[
|
|
// Typically PointerAddedEvent is not used in testers, but for records
|
|
// captured on a device it is usually what start a gesture.
|
|
PointerAddedEvent(position: location),
|
|
PointerDownEvent(position: location, buttons: kSecondaryMouseButton, pointer: 1),
|
|
]),
|
|
...<PointerEventRecord>[
|
|
for (
|
|
Duration t = const Duration(milliseconds: 5);
|
|
t < const Duration(milliseconds: 80);
|
|
t += const Duration(milliseconds: 16)
|
|
)
|
|
PointerEventRecord(t, <PointerEvent>[
|
|
PointerMoveEvent(
|
|
timeStamp: t - const Duration(milliseconds: 1),
|
|
position: location,
|
|
buttons: kSecondaryMouseButton,
|
|
pointer: 1,
|
|
),
|
|
]),
|
|
],
|
|
PointerEventRecord(const Duration(milliseconds: 80), <PointerEvent>[
|
|
PointerUpEvent(
|
|
timeStamp: const Duration(milliseconds: 79),
|
|
position: location,
|
|
buttons: kSecondaryMouseButton,
|
|
pointer: 1,
|
|
),
|
|
]),
|
|
];
|
|
final List<Duration> timeDiffs = await tester.handlePointerEventRecord(records);
|
|
expect(timeDiffs.length, records.length);
|
|
for (final Duration diff in timeDiffs) {
|
|
expect(diff, Duration.zero);
|
|
}
|
|
|
|
const String b = '$kSecondaryMouseButton';
|
|
expect(logs.first, 'down $b');
|
|
for (int i = 1; i < logs.length - 1; i++) {
|
|
expect(logs[i], 'move $b');
|
|
}
|
|
expect(logs.last, 'up $b');
|
|
});
|
|
|
|
group('runAsync', () {
|
|
testWidgets('works with no async calls', (WidgetTester tester) async {
|
|
String? value;
|
|
await tester.runAsync(() async {
|
|
value = '123';
|
|
});
|
|
expect(value, '123');
|
|
});
|
|
|
|
testWidgets('works with real async calls', (WidgetTester tester) async {
|
|
final StringBuffer buf = StringBuffer('1');
|
|
await tester.runAsync(() async {
|
|
buf.write('2');
|
|
//ignore: avoid_slow_async_io
|
|
await Directory.current.stat();
|
|
buf.write('3');
|
|
});
|
|
buf.write('4');
|
|
expect(buf.toString(), '1234');
|
|
});
|
|
|
|
testWidgets('propagates return values', (WidgetTester tester) async {
|
|
final String? value = await tester.runAsync<String>(() async {
|
|
return '123';
|
|
});
|
|
expect(value, '123');
|
|
});
|
|
|
|
testWidgets('reports errors via framework', (WidgetTester tester) async {
|
|
final String? value = await tester.runAsync<String>(() async {
|
|
throw ArgumentError();
|
|
});
|
|
expect(value, isNull);
|
|
expect(tester.takeException(), isArgumentError);
|
|
});
|
|
|
|
testWidgets('disallows re-entry', (WidgetTester tester) async {
|
|
final Completer<void> completer = Completer<void>();
|
|
tester.runAsync<void>(() => completer.future);
|
|
expect(() => tester.runAsync(() async {}), throwsA(isA<TestFailure>()));
|
|
completer.complete();
|
|
});
|
|
|
|
testWidgets('maintains existing zone values', (WidgetTester tester) async {
|
|
final Object key = Object();
|
|
await runZoned<Future<void>>(() {
|
|
expect(Zone.current[key], 'abczed');
|
|
return tester.runAsync<void>(() async {
|
|
expect(Zone.current[key], 'abczed');
|
|
});
|
|
}, zoneValues: <dynamic, dynamic>{key: 'abczed'});
|
|
});
|
|
|
|
testWidgets('control test (return value)', (WidgetTester tester) async {
|
|
final String? result = await tester.binding.runAsync<String>(() async => 'Judy Turner');
|
|
expect(result, 'Judy Turner');
|
|
});
|
|
|
|
testWidgets('async throw', (WidgetTester tester) async {
|
|
final String? result = await tester.binding.runAsync<Never>(
|
|
() async => throw Exception('Lois Dilettente'),
|
|
);
|
|
expect(result, isNull);
|
|
expect(tester.takeException(), isNotNull);
|
|
});
|
|
|
|
testWidgets('sync throw', (WidgetTester tester) async {
|
|
final String? result = await tester.binding.runAsync<Never>(
|
|
() => throw Exception('Butch Barton'),
|
|
);
|
|
expect(result, isNull);
|
|
expect(tester.takeException(), isNotNull);
|
|
});
|
|
|
|
testWidgets('runs in original zone', (WidgetTester tester) async {
|
|
final Zone testZone = Zone.current;
|
|
Zone? runAsyncZone;
|
|
Zone? timerZone;
|
|
Zone? periodicTimerZone;
|
|
Zone? microtaskZone;
|
|
|
|
Zone? innerZone;
|
|
Zone? innerTimerZone;
|
|
Zone? innerPeriodicTimerZone;
|
|
Zone? innerMicrotaskZone;
|
|
|
|
await tester.binding.runAsync<void>(() async {
|
|
final Zone currentZone = Zone.current;
|
|
runAsyncZone = currentZone;
|
|
|
|
// Complete a future when all callbacks have completed.
|
|
int pendingCallbacks = 6;
|
|
final Completer<void> callbacksDone = Completer<void>();
|
|
void onCallback() {
|
|
if (--pendingCallbacks == 0) {
|
|
testZone.run(() {
|
|
callbacksDone.complete(null);
|
|
});
|
|
}
|
|
}
|
|
|
|
// On the runAsync zone itself.
|
|
currentZone.createTimer(Duration.zero, () {
|
|
timerZone = Zone.current;
|
|
onCallback();
|
|
});
|
|
currentZone.createPeriodicTimer(Duration.zero, (Timer timer) {
|
|
timer.cancel();
|
|
periodicTimerZone = Zone.current;
|
|
onCallback();
|
|
});
|
|
currentZone.scheduleMicrotask(() {
|
|
microtaskZone = Zone.current;
|
|
onCallback();
|
|
});
|
|
|
|
// On a nested user-created zone.
|
|
final Zone inner = runZoned(() => Zone.current);
|
|
innerZone = inner;
|
|
inner.createTimer(Duration.zero, () {
|
|
innerTimerZone = Zone.current;
|
|
onCallback();
|
|
});
|
|
inner.createPeriodicTimer(Duration.zero, (Timer timer) {
|
|
timer.cancel();
|
|
innerPeriodicTimerZone = Zone.current;
|
|
onCallback();
|
|
});
|
|
inner.scheduleMicrotask(() {
|
|
innerMicrotaskZone = Zone.current;
|
|
onCallback();
|
|
});
|
|
|
|
await callbacksDone.future;
|
|
});
|
|
expect(runAsyncZone, isNotNull);
|
|
expect(timerZone, same(runAsyncZone));
|
|
expect(periodicTimerZone, same(runAsyncZone));
|
|
expect(microtaskZone, same(runAsyncZone));
|
|
|
|
expect(innerZone, isNotNull);
|
|
expect(innerTimerZone, same(innerZone));
|
|
expect(innerPeriodicTimerZone, same(innerZone));
|
|
expect(innerMicrotaskZone, same(innerZone));
|
|
|
|
expect(runAsyncZone, isNot(same(testZone)));
|
|
expect(runAsyncZone, isNot(same(innerZone)));
|
|
expect(innerZone, isNot(same(testZone)));
|
|
});
|
|
});
|
|
|
|
group('showKeyboard', () {
|
|
testWidgets('can be called twice', (WidgetTester tester) async {
|
|
await tester.pumpWidget(MaterialApp(home: Material(child: Center(child: TextFormField()))));
|
|
await tester.showKeyboard(find.byType(TextField));
|
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
|
await tester.pump();
|
|
await tester.showKeyboard(find.byType(TextField));
|
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
|
await tester.pump();
|
|
await tester.showKeyboard(find.byType(TextField));
|
|
await tester.showKeyboard(find.byType(TextField));
|
|
await tester.pump();
|
|
});
|
|
|
|
testWidgets(
|
|
'can focus on offstage text input field if finder says not to skip offstage nodes',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(home: Material(child: Offstage(child: TextFormField()))),
|
|
);
|
|
await tester.showKeyboard(find.byType(TextField, skipOffstage: false));
|
|
},
|
|
);
|
|
});
|
|
|
|
testWidgets('verifyTickersWereDisposed control test', (WidgetTester tester) async {
|
|
late FlutterError error;
|
|
final Ticker ticker = tester.createTicker((Duration duration) {});
|
|
ticker.start();
|
|
try {
|
|
tester.verifyTickersWereDisposed('');
|
|
} on FlutterError catch (e) {
|
|
error = e;
|
|
} finally {
|
|
expect(error, isNotNull);
|
|
expect(error.diagnostics.length, 4);
|
|
expect(error.diagnostics[2].level, DiagnosticLevel.hint);
|
|
expect(
|
|
error.diagnostics[2].toStringDeep(),
|
|
'Tickers used by AnimationControllers should be disposed by\n'
|
|
'calling dispose() on the AnimationController itself. Otherwise,\n'
|
|
'the ticker will leak.\n',
|
|
);
|
|
expect(error.diagnostics.last, isA<DiagnosticsProperty<Ticker>>());
|
|
expect(error.diagnostics.last.value, ticker);
|
|
expect(
|
|
error.toStringDeep(),
|
|
startsWith(
|
|
'FlutterError\n'
|
|
' A Ticker was active .\n'
|
|
' All Tickers must be disposed.\n'
|
|
' Tickers used by AnimationControllers should be disposed by\n'
|
|
' calling dispose() on the AnimationController itself. Otherwise,\n'
|
|
' the ticker will leak.\n'
|
|
' The offending ticker was:\n'
|
|
' _TestTicker()\n',
|
|
),
|
|
);
|
|
}
|
|
ticker.stop();
|
|
});
|
|
|
|
group('testWidgets variants work', () {
|
|
int numberOfVariationsRun = 0;
|
|
|
|
testWidgets(
|
|
'variant tests run all values provided',
|
|
(WidgetTester tester) async {
|
|
if (debugDefaultTargetPlatformOverride == null) {
|
|
expect(numberOfVariationsRun, equals(TargetPlatform.values.length));
|
|
} else {
|
|
numberOfVariationsRun += 1;
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant(TargetPlatform.values.toSet()),
|
|
);
|
|
|
|
testWidgets(
|
|
'variant tests have descriptions with details',
|
|
(WidgetTester tester) async {
|
|
if (debugDefaultTargetPlatformOverride == null) {
|
|
expect(tester.testDescription, equals('variant tests have descriptions with details'));
|
|
} else {
|
|
expect(
|
|
tester.testDescription,
|
|
equals(
|
|
'variant tests have descriptions with details (variant: $debugDefaultTargetPlatformOverride)',
|
|
),
|
|
);
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant(TargetPlatform.values.toSet()),
|
|
);
|
|
});
|
|
|
|
group('TargetPlatformVariant', () {
|
|
int numberOfVariationsRun = 0;
|
|
TargetPlatform? origTargetPlatform;
|
|
|
|
setUpAll(() {
|
|
origTargetPlatform = debugDefaultTargetPlatformOverride;
|
|
});
|
|
|
|
tearDownAll(() {
|
|
expect(debugDefaultTargetPlatformOverride, equals(origTargetPlatform));
|
|
});
|
|
|
|
testWidgets(
|
|
'TargetPlatformVariant.only tests given value',
|
|
(WidgetTester tester) async {
|
|
expect(debugDefaultTargetPlatformOverride, equals(TargetPlatform.iOS));
|
|
expect(defaultTargetPlatform, equals(TargetPlatform.iOS));
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
group('all', () {
|
|
testWidgets('TargetPlatformVariant.all tests run all variants', (WidgetTester tester) async {
|
|
if (debugDefaultTargetPlatformOverride == null) {
|
|
expect(numberOfVariationsRun, equals(TargetPlatform.values.length));
|
|
} else {
|
|
numberOfVariationsRun += 1;
|
|
}
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
const Set<TargetPlatform> excludePlatforms = <TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.linux,
|
|
};
|
|
testWidgets(
|
|
'TargetPlatformVariant.all, excluding runs an all variants except those provided in excluding',
|
|
(WidgetTester tester) async {
|
|
if (debugDefaultTargetPlatformOverride == null) {
|
|
expect(
|
|
numberOfVariationsRun,
|
|
equals(TargetPlatform.values.length - excludePlatforms.length),
|
|
);
|
|
expect(
|
|
excludePlatforms,
|
|
isNot(contains(debugDefaultTargetPlatformOverride)),
|
|
reason: 'this test should not run on any platform in excludePlatforms',
|
|
);
|
|
} else {
|
|
numberOfVariationsRun += 1;
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant.all(excluding: excludePlatforms),
|
|
);
|
|
});
|
|
|
|
testWidgets('TargetPlatformVariant.desktop + mobile contains all TargetPlatform values', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final TargetPlatformVariant all = TargetPlatformVariant.all();
|
|
final TargetPlatformVariant desktop = TargetPlatformVariant.all();
|
|
final TargetPlatformVariant mobile = TargetPlatformVariant.all();
|
|
expect(desktop.values.union(mobile.values), equals(all.values));
|
|
});
|
|
});
|
|
|
|
group('Pending timer', () {
|
|
late TestExceptionReporter currentExceptionReporter;
|
|
setUp(() {
|
|
currentExceptionReporter = reportTestException;
|
|
});
|
|
|
|
tearDown(() {
|
|
reportTestException = currentExceptionReporter;
|
|
});
|
|
|
|
test('Throws assertion message without code', () async {
|
|
late FlutterErrorDetails flutterErrorDetails;
|
|
reportTestException = (FlutterErrorDetails details, String testDescription) {
|
|
flutterErrorDetails = details;
|
|
};
|
|
|
|
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
|
|
debugPrint('DISREGARD NEXT PENDING TIMER LIST, IT IS EXPECTED');
|
|
await binding.runTest(() async {
|
|
final Timer timer = Timer(const Duration(seconds: 1), () {});
|
|
expect(timer.isActive, true);
|
|
}, () {});
|
|
|
|
expect(flutterErrorDetails.exception, isA<AssertionError>());
|
|
expect(
|
|
(flutterErrorDetails.exception as AssertionError).message,
|
|
'A Timer is still pending even after the widget tree was disposed.',
|
|
);
|
|
expect(binding.inTest, true);
|
|
binding.postTest();
|
|
});
|
|
});
|
|
|
|
group('Accessibility announcements testing API', () {
|
|
testWidgets('Returns the list of announcements', (WidgetTester tester) async {
|
|
// Make sure the handler is properly set
|
|
expect(
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler(
|
|
SystemChannels.accessibility.name,
|
|
null,
|
|
),
|
|
isFalse,
|
|
);
|
|
|
|
await SemanticsService.announce('announcement 1', TextDirection.ltr);
|
|
await SemanticsService.announce(
|
|
'announcement 2',
|
|
TextDirection.rtl,
|
|
assertiveness: Assertiveness.assertive,
|
|
);
|
|
await SemanticsService.announce('announcement 3', TextDirection.rtl);
|
|
|
|
final List<CapturedAccessibilityAnnouncement> list = tester.takeAnnouncements();
|
|
expect(list, hasLength(3));
|
|
final CapturedAccessibilityAnnouncement first = list[0];
|
|
expect(first.message, 'announcement 1');
|
|
expect(first.textDirection, TextDirection.ltr);
|
|
|
|
final CapturedAccessibilityAnnouncement second = list[1];
|
|
expect(second.message, 'announcement 2');
|
|
expect(second.textDirection, TextDirection.rtl);
|
|
expect(second.assertiveness, Assertiveness.assertive);
|
|
|
|
final CapturedAccessibilityAnnouncement third = list[2];
|
|
expect(third.message, 'announcement 3');
|
|
expect(third.textDirection, TextDirection.rtl);
|
|
expect(third.assertiveness, Assertiveness.polite);
|
|
|
|
final List<CapturedAccessibilityAnnouncement> emptyList = tester.takeAnnouncements();
|
|
expect(emptyList, <CapturedAccessibilityAnnouncement>[]);
|
|
});
|
|
|
|
test('New test API is not breaking existing tests', () async {
|
|
final List<Map<dynamic, dynamic>> log = <Map<dynamic, dynamic>>[];
|
|
|
|
Future<dynamic> handleMessage(dynamic mockMessage) async {
|
|
final Map<dynamic, dynamic> message = mockMessage as Map<dynamic, dynamic>;
|
|
log.add(message);
|
|
}
|
|
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage);
|
|
|
|
await SemanticsService.announce(
|
|
'announcement 1',
|
|
TextDirection.rtl,
|
|
assertiveness: Assertiveness.assertive,
|
|
);
|
|
expect(
|
|
log,
|
|
equals(<Map<String, dynamic>>[
|
|
<String, dynamic>{
|
|
'type': 'announce',
|
|
'data': <String, dynamic>{
|
|
'message': 'announcement 1',
|
|
'textDirection': 0,
|
|
'assertiveness': 1,
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
|
|
// Remove the handler
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
|
|
});
|
|
|
|
tearDown(() {
|
|
// Make sure that the handler is removed in [TestWidgetsFlutterBinding.postTest]
|
|
expect(
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler(
|
|
SystemChannels.accessibility.name,
|
|
null,
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
});
|
|
|
|
testWidgets('wrapWithView: false does not include View', (WidgetTester tester) async {
|
|
FlutterView? flutterView;
|
|
View? view;
|
|
int builderCount = 0;
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
builderCount++;
|
|
flutterView = View.maybeOf(context);
|
|
view = context.findAncestorWidgetOfExactType<View>();
|
|
return const ViewCollection(views: <Widget>[]);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(builderCount, 1);
|
|
expect(view, isNull);
|
|
expect(flutterView, isNull);
|
|
expect(find.byType(View), findsNothing);
|
|
});
|
|
|
|
testWidgets('passing a view to pumpWidget with wrapWithView: true throws', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(View(view: FakeView(tester.view), child: const SizedBox.shrink()));
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError e) => e.message,
|
|
'message',
|
|
contains('consider setting the "wrapWithView" parameter of that method to false'),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('can pass a View to pumpWidget when wrapWithView: false', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
View(view: tester.view, child: const SizedBox.shrink()),
|
|
);
|
|
expect(find.byType(View), findsOne);
|
|
});
|
|
}
|
|
|
|
class FakeMatcher extends AsyncMatcher {
|
|
FakeMatcher(this.completer);
|
|
|
|
final Completer<void> completer;
|
|
|
|
@override
|
|
Future<String?> matchAsync(dynamic object) {
|
|
return completer.future.then<String?>((void value) {
|
|
return object?.toString();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('--fake--');
|
|
}
|
|
|
|
class _AlwaysAnimating extends StatefulWidget {
|
|
const _AlwaysAnimating({required this.onPaint});
|
|
|
|
final VoidCallback onPaint;
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _AlwaysAnimatingState();
|
|
}
|
|
|
|
class _AlwaysAnimatingState extends State<_AlwaysAnimating> with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(duration: const Duration(milliseconds: 100), vsync: this);
|
|
_controller.repeat();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _controller.view,
|
|
builder: (BuildContext context, Widget? child) {
|
|
return CustomPaint(painter: _AlwaysRepaint(widget.onPaint));
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AlwaysRepaint extends CustomPainter {
|
|
_AlwaysRepaint(this.onPaint);
|
|
|
|
final VoidCallback onPaint;
|
|
|
|
@override
|
|
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
onPaint();
|
|
}
|
|
}
|