blob: c79617e69c025976354d09e9cd56a65a1d50ba65 [file]
// 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 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
// Registers a tear down that calls [TestWidgetsFlutterBinding.postTest] if a
// test that drives the binding directly fails before calling it. Otherwise the
// binding would stay in a test and every later test in the file would fail.
//
// Call this before registering tear downs that reset state, so that (since tear
// downs run in reverse order) the state is reset before `postTest` runs.
void _addPostTestTearDown(TestWidgetsFlutterBinding binding) {
addTearDown(() {
if (binding.inTest) {
binding.postTest();
}
});
}
// Generates test cases for a debug variable or test setting.
//
// The `name` argument is the identifier of the variable or setting being tested.
//
// The `mutateAndGetReset` callback modifies the variable or setting and
// returns a [VoidCallback] that restores it to its original value.
//
// The `expectedErrorMessage` is the message expected on the [FlutterError]
// reported by [TestWidgetsFlutterBinding.postTest] when the variable is left
// modified.
void _testDebugVariable({
required String name,
required VoidCallback Function(TestWidgetsFlutterBinding binding) mutateAndGetReset,
required String expectedErrorMessage,
}) {
group('$name:', () {
// Verifies that the variable can be mutated in [testWidgets] and cleanly
// reset via [addTearDown] without triggering invariant failures.
testWidgets('can be reset with addTearDown', (WidgetTester tester) async {
final VoidCallback reset = mutateAndGetReset(tester.binding);
addTearDown(reset);
});
// Verifies that when mutated during a test without being reset,
// [TestWidgetsFlutterBinding.postTest] catches the invariant failure, reports
// it via [reportTestException] with [expectedErrorMessage], and finishes
// cleaning up the binding state without throwing.
test('direct runTest reports exception in postTest when not reset', () async {
FlutterErrorDetails? reportedError;
final TestExceptionReporter oldReporter = reportTestException;
reportTestException = (FlutterErrorDetails details, String testDescription) {
reportedError = details;
};
addTearDown(() {
reportTestException = oldReporter;
});
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
_addPostTestTearDown(binding);
VoidCallback? reset;
addTearDown(() => reset?.call());
await binding.runTest(() async {
reset = mutateAndGetReset(binding);
}, () {});
expect(() => binding.postTest(), returnsNormally);
expect(reportedError, isNotNull);
expect((reportedError!.exception as FlutterError).message, expectedErrorMessage);
expect(binding.inTest, isFalse);
});
});
}
void main() {
const foundationErrorMessage =
'The value of a foundation debug variable was changed by the test.';
_testDebugVariable(
name: 'debugDefaultTargetPlatformOverride',
mutateAndGetReset: (TestWidgetsFlutterBinding binding) {
final TargetPlatform? original = debugDefaultTargetPlatformOverride;
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
return () => debugDefaultTargetPlatformOverride = original;
},
expectedErrorMessage: foundationErrorMessage,
);
_testDebugVariable(
name: 'debugDoublePrecision',
mutateAndGetReset: (TestWidgetsFlutterBinding binding) {
final int? original = debugDoublePrecision;
debugDoublePrecision = 3;
return () => debugDoublePrecision = original;
},
expectedErrorMessage: foundationErrorMessage,
);
_testDebugVariable(
name: 'debugBrightnessOverride',
mutateAndGetReset: (TestWidgetsFlutterBinding binding) {
final Brightness? original = debugBrightnessOverride;
debugBrightnessOverride = Brightness.dark;
return () => debugBrightnessOverride = original;
},
expectedErrorMessage: foundationErrorMessage,
);
_testDebugVariable(
name: 'autoUpdateGoldenFiles',
mutateAndGetReset: (TestWidgetsFlutterBinding binding) {
final bool original = autoUpdateGoldenFiles;
autoUpdateGoldenFiles = !original;
return () => autoUpdateGoldenFiles = original;
},
expectedErrorMessage: 'The value of autoUpdateGoldenFiles was changed by the test.',
);
_testDebugVariable(
name: 'ErrorWidget.builder',
mutateAndGetReset: (TestWidgetsFlutterBinding binding) {
final ErrorWidgetBuilder original = ErrorWidget.builder;
ErrorWidget.builder = (FlutterErrorDetails details) => Container();
return () => ErrorWidget.builder = original;
},
expectedErrorMessage: 'The value of ErrorWidget.builder was changed by the test.',
);
_testDebugVariable(
name: 'shouldPropagateDevicePointerEvents',
mutateAndGetReset: (TestWidgetsFlutterBinding binding) {
final bool original = binding.shouldPropagateDevicePointerEvents;
binding.shouldPropagateDevicePointerEvents = !original;
return () => binding.shouldPropagateDevicePointerEvents = original;
},
expectedErrorMessage:
'The value of shouldPropagateDevicePointerEvents was changed by the test.',
);
_testDebugVariable(
name: 'reportTestException',
mutateAndGetReset: (TestWidgetsFlutterBinding binding) {
final TestExceptionReporter original = reportTestException;
reportTestException = (FlutterErrorDetails details, String testDescription) {};
return () => reportTestException = original;
},
expectedErrorMessage: 'The value of reportTestException was changed by the test.',
);
// Verifies that when a test body fails, [TestWidgetsFlutterBinding._runTestBody]
// leaves the widget tree intact for inspection (does not unmount it to
// `_postTestMessage`) and [TestWidgetsFlutterBinding.postTest] skips invariant
// verification to avoid spurious secondary errors.
test('direct runTest with failed test body skips invariant check and preserves widget tree in postTest', () async {
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
final TestExceptionReporter oldReporter = reportTestException;
reportTestException = (FlutterErrorDetails details, String testDescription) {};
addTearDown(() {
reportTestException = oldReporter;
});
_addPostTestTearDown(binding);
addTearDown(() {
debugDefaultTargetPlatformOverride = null;
});
await binding.runTest(() async {
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
runApp(const Placeholder());
throw Exception('test failed');
}, () {});
// postTest should NOT throw invariant error because the test body failed.
expect(() => binding.postTest(), returnsNormally);
expect(binding.inTest, isFalse);
// The widget tree from the test is preserved for inspection rather than
// being unmounted and replaced with _postTestMessage.
expect(find.byType(Placeholder), findsOneWidget);
});
// Verifies that asynchronous operations executed in [addTearDown] callbacks
// (which may schedule microtasks) do not cause invariant failures in
// [TestWidgetsFlutterBinding.postTest].
testWidgets(
'addTearDown can execute asynchronous operations without triggering invariant errors',
(WidgetTester tester) async {
addTearDown(() async {
await Future<void>.value();
});
},
);
// Verifies that invariant checks that report failures via
// [FlutterError.reportError] instead of throwing (such as
// [SchedulerBinding.debugAssertNoTransientCallbacks]) are captured and
// reported with the test description in [TestWidgetsFlutterBinding.postTest].
test('postTest reports invariant failures logged via FlutterError.reportError', () async {
FlutterErrorDetails? reportedError;
String? reportedDescription;
final TestExceptionReporter oldReporter = reportTestException;
reportTestException = (FlutterErrorDetails details, String testDescription) {
reportedError = details;
reportedDescription = testDescription;
};
addTearDown(() {
reportTestException = oldReporter;
});
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
_addPostTestTearDown(binding);
// A Ticker reschedules its frame callback on every tick, so it survives the
// frames pumped while unmounting the tree. A one-shot frame callback would
// be consumed by those frames instead.
Ticker? ticker;
addTearDown(() => ticker?.dispose());
await binding.runTest(
() async {
ticker = Ticker((Duration _) {})..start();
},
() {},
description: 'leaking ticker test',
);
expect(() => binding.postTest(), returnsNormally);
expect(reportedError, isNotNull);
expect(
reportedError!.exception,
'An animation is still running even after the widget tree was disposed.',
);
expect(reportedDescription, 'leaking ticker test');
expect(binding.inTest, isFalse);
});
}