blob: 46074f4da5998a3d592fe7aa7994ecdad49f69ae [file] [log] [blame]
// 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:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
group(CustomPainter, () {
setUp(() {
debugResetSemanticsIdCounter();
_PainterWithSemantics.shouldRebuildSemanticsCallCount = 0;
_PainterWithSemantics.buildSemanticsCallCount = 0;
_PainterWithSemantics.semanticsBuilderCallCount = 0;
});
_defineTests();
});
}
void _defineTests() {
testWidgets('builds no semantics by default', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(CustomPaint(
painter: _PainterWithoutSemantics(),
));
expect(semanticsTester, hasSemantics(
TestSemantics.root(),
));
semanticsTester.dispose();
});
testWidgets('provides foreground semantics', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(CustomPaint(
foregroundPainter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
label: 'foreground',
textDirection: TextDirection.rtl,
),
),
),
));
expect(semanticsTester, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
id: 2,
label: 'foreground',
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
),
],
),
],
),
));
semanticsTester.dispose();
});
testWidgets('provides background semantics', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
label: 'background',
textDirection: TextDirection.rtl,
),
),
),
));
expect(semanticsTester, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
id: 2,
label: 'background',
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
),
],
),
],
),
));
semanticsTester.dispose();
});
testWidgets('combines background, child and foreground semantics', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
label: 'background',
textDirection: TextDirection.rtl,
),
),
),
foregroundPainter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
label: 'foreground',
textDirection: TextDirection.rtl,
),
),
),
child: Semantics(
container: true,
child: const Text('Hello', textDirection: TextDirection.ltr),
),
));
expect(semanticsTester, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
id: 3,
label: 'background',
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
),
TestSemantics(
id: 2,
label: 'Hello',
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
),
TestSemantics(
id: 4,
label: 'foreground',
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
),
],
),
],
),
));
semanticsTester.dispose();
});
testWidgets('applies $SemanticsProperties', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
key: ValueKey<int>(1),
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: SemanticsProperties(
checked: false,
selected: false,
button: false,
label: 'label-before',
value: 'value-before',
increasedValue: 'increase-before',
decreasedValue: 'decrease-before',
hint: 'hint-before',
textDirection: TextDirection.rtl,
),
),
),
));
expect(semanticsTester, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
rect: const Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
id: 2,
flags: 1,
label: 'label-before',
value: 'value-before',
increasedValue: 'increase-before',
decreasedValue: 'decrease-before',
hint: 'hint-before',
textDirection: TextDirection.rtl,
),
],
),
],
),
));
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: CustomPainterSemantics(
key: const ValueKey<int>(1),
rect: const Rect.fromLTRB(5.0, 6.0, 7.0, 8.0),
properties: SemanticsProperties(
checked: true,
selected: true,
button: true,
label: 'label-after',
value: 'value-after',
increasedValue: 'increase-after',
decreasedValue: 'decrease-after',
hint: 'hint-after',
textDirection: TextDirection.ltr,
onScrollDown: () { },
onLongPress: () { },
onDecrease: () { },
onIncrease: () { },
onScrollLeft: () { },
onScrollRight: () { },
onScrollUp: () { },
onTap: () { },
),
),
),
));
expect(semanticsTester, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
rect: const Rect.fromLTRB(5.0, 6.0, 7.0, 8.0),
actions: 255,
id: 2,
flags: 15,
label: 'label-after',
value: 'value-after',
increasedValue: 'increase-after',
decreasedValue: 'decrease-after',
hint: 'hint-after',
textDirection: TextDirection.ltr,
),
],
),
],
),
));
semanticsTester.dispose();
});
testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async {
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
key: ValueKey<int>(1),
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: SemanticsProperties(
checked: false,
selected: false,
button: false,
label: 'label-before',
value: 'value-before',
increasedValue: 'increase-before',
decreasedValue: 'decrease-before',
hint: 'hint-before',
textDirection: TextDirection.rtl,
),
),
),
));
// Start with semantics off.
expect(tester.binding.pipelineOwner.semanticsOwner, isNull);
// Semantics on
SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpAndSettle();
expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);
// Semantics off
semantics.dispose();
await tester.pumpAndSettle();
expect(tester.binding.pipelineOwner.semanticsOwner, isNull);
// Semantics on
semantics = SemanticsTester(tester);
await tester.pumpAndSettle();
expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);
semantics.dispose();
}, semanticsEnabled: false);
testWidgets('Supports all actions', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final List<SemanticsAction> performedActions = <SemanticsAction>[];
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: CustomPainterSemantics(
key: const ValueKey<int>(1),
rect: const Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: SemanticsProperties(
onDismiss: () => performedActions.add(SemanticsAction.dismiss),
onTap: () => performedActions.add(SemanticsAction.tap),
onLongPress: () => performedActions.add(SemanticsAction.longPress),
onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
onScrollRight: () => performedActions.add(SemanticsAction.scrollRight),
onScrollUp: () => performedActions.add(SemanticsAction.scrollUp),
onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
onIncrease: () => performedActions.add(SemanticsAction.increase),
onDecrease: () => performedActions.add(SemanticsAction.decrease),
onCopy: () => performedActions.add(SemanticsAction.copy),
onCut: () => performedActions.add(SemanticsAction.cut),
onPaste: () => performedActions.add(SemanticsAction.paste),
onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
onMoveCursorForwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByWord),
onMoveCursorBackwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByWord),
onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
onSetText: (String text) => performedActions.add(SemanticsAction.setText),
onDidGainAccessibilityFocus: () => performedActions.add(SemanticsAction.didGainAccessibilityFocus),
onDidLoseAccessibilityFocus: () => performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
),
),
),
));
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.customAction) // customAction is not user-exposed.
..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
const int expectedId = 2;
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: allActions.fold<int>(0, (int previous, SemanticsAction action) => previous | action.index),
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
// Do the actions work?
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
int expectedLength = 1;
for (final SemanticsAction action in allActions) {
switch (action) {
case SemanticsAction.moveCursorBackwardByCharacter:
case SemanticsAction.moveCursorForwardByCharacter:
case SemanticsAction.moveCursorBackwardByWord:
case SemanticsAction.moveCursorForwardByWord:
semanticsOwner.performAction(expectedId, action, true);
break;
case SemanticsAction.setSelection:
semanticsOwner.performAction(expectedId, action, <String, int>{
'base': 4,
'extent': 5,
});
break;
case SemanticsAction.setText:
semanticsOwner.performAction(expectedId, action, 'text');
break;
case SemanticsAction.copy:
case SemanticsAction.customAction:
case SemanticsAction.cut:
case SemanticsAction.decrease:
case SemanticsAction.didGainAccessibilityFocus:
case SemanticsAction.didLoseAccessibilityFocus:
case SemanticsAction.dismiss:
case SemanticsAction.increase:
case SemanticsAction.longPress:
case SemanticsAction.paste:
case SemanticsAction.scrollDown:
case SemanticsAction.scrollLeft:
case SemanticsAction.scrollRight:
case SemanticsAction.scrollUp:
case SemanticsAction.showOnScreen:
case SemanticsAction.tap:
semanticsOwner.performAction(expectedId, action);
break;
}
expect(performedActions.length, expectedLength);
expect(performedActions.last, action);
expectedLength += 1;
}
semantics.dispose();
});
testWidgets('Supports all flags', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
// checked state and toggled state are mutually exclusive.
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
key: ValueKey<int>(1),
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: SemanticsProperties(
enabled: true,
checked: true,
selected: true,
hidden: true,
button: true,
slider: true,
keyboardKey: true,
link: true,
textField: true,
readOnly: true,
focused: true,
focusable: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
multiline: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
toggled: true,
),
),
),
));
List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
// therefore it has to be removed.
flags.remove(SemanticsFlag.hasImplicitScrolling);
TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
TestSemantics.rootChild(
id: 2,
rect: TestSemantics.fullScreen,
flags: flags,
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
await tester.pumpWidget(CustomPaint(
painter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
key: ValueKey<int>(1),
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: SemanticsProperties(
enabled: true,
checked: true,
toggled: true,
selected: true,
hidden: true,
button: true,
slider: true,
keyboardKey: true,
link: true,
textField: true,
readOnly: true,
focused: true,
focusable: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
multiline: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
),
),
),
));
flags = SemanticsFlag.values.values.toList();
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
// therefore it has to be removed.
flags.remove(SemanticsFlag.hasImplicitScrolling);
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
TestSemantics.rootChild(
id: 2,
rect: TestSemantics.fullScreen,
flags: flags,
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
semantics.dispose();
});
group('diffing', () {
testWidgets('complains about duplicate keys', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(CustomPaint(
painter: _SemanticsDiffTest(<String>[
'a-k',
'a-k',
]),
));
expect(tester.takeException(), isFlutterError);
semanticsTester.dispose();
});
_testDiff('adds one item to an empty list', (_DiffTester tester) async {
await tester.diff(
from: <String>[],
to: <String>['a'],
);
});
_testDiff('removes the last item from the list', (_DiffTester tester) async {
await tester.diff(
from: <String>['a'],
to: <String>[],
);
});
_testDiff('appends one item at the end of a non-empty list', (_DiffTester tester) async {
await tester.diff(
from: <String>['a'],
to: <String>['a', 'b'],
);
});
_testDiff('prepends one item at the beginning of a non-empty list', (_DiffTester tester) async {
await tester.diff(
from: <String>['b'],
to: <String>['a', 'b'],
);
});
_testDiff('inserts one item in the middle of a list', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'c-k',
],
to: <String>[
'a-k',
'b-k',
'c-k',
],
);
});
_testDiff('removes one item from the middle of a list', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'b-k',
'c-k',
],
to: <String>[
'a-k',
'c-k',
],
);
});
_testDiff('swaps two items', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'b-k',
],
to: <String>[
'b-k',
'a-k',
],
);
});
_testDiff('finds and moved one keyed item', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'b',
'c',
],
to: <String>[
'b',
'c',
'a-k',
],
);
});
});
testWidgets('rebuilds semantics upon resize', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
final _PainterWithSemantics painter = _PainterWithSemantics(
semantics: const CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
label: 'background',
textDirection: TextDirection.rtl,
),
),
);
final CustomPaint paint = CustomPaint(painter: painter);
await tester.pumpWidget(SizedBox(
height: 20.0,
width: 20.0,
child: paint,
));
expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
await tester.pumpWidget(SizedBox(
height: 20.0,
width: 20.0,
child: paint,
));
expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
await tester.pumpWidget(SizedBox(
height: 40.0,
width: 40.0,
child: paint,
));
expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
expect(_PainterWithSemantics.buildSemanticsCallCount, 2);
expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
semanticsTester.dispose();
});
testWidgets('does not rebuild when shouldRebuildSemantics is false', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
const CustomPainterSemantics testSemantics = CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
label: 'background',
textDirection: TextDirection.rtl,
),
);
await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
semantics: testSemantics,
)));
expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
semantics: testSemantics,
)));
expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 1);
expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
const CustomPainterSemantics testSemantics2 = CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
label: 'background',
textDirection: TextDirection.rtl,
),
);
await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
semantics: testSemantics2,
)));
expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 2);
expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
semanticsTester.dispose();
});
}
void _testDiff(String description, Future<void> Function(_DiffTester tester) testFunction) {
testWidgets(description, (WidgetTester tester) async {
await testFunction(_DiffTester(tester));
});
}
class _DiffTester {
_DiffTester(this.tester);
final WidgetTester tester;
/// Creates an initial semantics list using the `from` list, then updates the
/// list to the `to` list. This causes [RenderCustomPaint] to diff the two
/// lists and apply the changes. This method asserts the changes were
/// applied correctly, specifically:
///
/// - checks that initial and final configurations are in the desired states.
/// - checks that keyed nodes have stable IDs.
Future<void> diff({ required List<String> from, required List<String> to }) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
TestSemantics createExpectations(List<String> labels) {
return TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
for (final String label in labels)
TestSemantics(
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
label: label,
),
],
),
],
);
}
await tester.pumpWidget(CustomPaint(
painter: _SemanticsDiffTest(from),
));
expect(semanticsTester, hasSemantics(createExpectations(from), ignoreId: true));
SemanticsNode root = RendererBinding.instance!.renderView.debugSemantics!;
final Map<Key, int> idAssignments = <Key, int>{};
root.visitChildren((SemanticsNode firstChild) {
firstChild.visitChildren((SemanticsNode node) {
if (node.key != null) {
idAssignments[node.key!] = node.id;
}
return true;
});
return true;
});
await tester.pumpWidget(CustomPaint(
painter: _SemanticsDiffTest(to),
));
await tester.pumpAndSettle();
expect(semanticsTester, hasSemantics(createExpectations(to), ignoreId: true));
root = RendererBinding.instance!.renderView.debugSemantics!;
root.visitChildren((SemanticsNode firstChild) {
firstChild.visitChildren((SemanticsNode node) {
if (node.key != null && idAssignments[node.key] != null) {
expect(idAssignments[node.key], node.id, reason:
'Node with key ${node.key} was previously assigned ID ${idAssignments[node.key]}. '
'After diffing the child list, its ID changed to ${node.id}. IDs must be stable.',
);
}
return true;
});
return true;
});
semanticsTester.dispose();
}
}
class _SemanticsDiffTest extends CustomPainter {
_SemanticsDiffTest(this.data);
final List<String> data;
@override
void paint(Canvas canvas, Size size) {
// We don't test painting.
}
@override
SemanticsBuilderCallback get semanticsBuilder => buildSemantics;
List<CustomPainterSemantics> buildSemantics(Size size) {
final List<CustomPainterSemantics> semantics = <CustomPainterSemantics>[];
for (final String label in data) {
Key? key;
if (label.endsWith('-k')) {
key = ValueKey<String>(label);
}
semantics.add(
CustomPainterSemantics(
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
key: key,
properties: SemanticsProperties(
label: label,
textDirection: TextDirection.rtl,
),
),
);
}
return semantics;
}
@override
bool shouldRepaint(_SemanticsDiffTest oldPainter) => true;
}
class _PainterWithSemantics extends CustomPainter {
_PainterWithSemantics({ required this.semantics });
final CustomPainterSemantics semantics;
static int semanticsBuilderCallCount = 0;
static int buildSemanticsCallCount = 0;
static int shouldRebuildSemanticsCallCount = 0;
@override
void paint(Canvas canvas, Size size) {
// We don't test painting.
}
@override
SemanticsBuilderCallback get semanticsBuilder {
semanticsBuilderCallCount += 1;
return buildSemantics;
}
List<CustomPainterSemantics> buildSemantics(Size size) {
buildSemanticsCallCount += 1;
return <CustomPainterSemantics>[semantics];
}
@override
bool shouldRepaint(_PainterWithSemantics oldPainter) {
return true;
}
@override
bool shouldRebuildSemantics(_PainterWithSemantics oldPainter) {
shouldRebuildSemanticsCallCount += 1;
return !identical(oldPainter.semantics, semantics);
}
}
class _PainterWithoutSemantics extends CustomPainter {
_PainterWithoutSemantics();
@override
void paint(Canvas canvas, Size size) {
// We don't test painting.
}
@override
bool shouldRepaint(_PainterWithSemantics oldPainter) => true;
}