flutter/packages/flutter_test/test/matchers_test.dart
Bartek Pacia 948523b80c
Add accessibility identifier to SemanticsProperties (#138331)
This PR adds `String? identifier` to `Semantics` and `SemanticsProperties`. The `identifier` will be exposed on Android as `resource-id` and on iOS as `accessibilityIdentifier`.

Mainly targeted at #17988

Initial Engine PR with Android support: https://github.com/flutter/engine/pull/47961
iOS Engine PR: https://github.com/flutter/engine/pull/48858

### Migration

This change breaks the SemanticsUpdateBuilder API which is on the Framework<-->Engine border. For more details see [engine PR](https://github.com/flutter/engine/pull/47961).

Steps:
part 1: [engine] add `SemanticsUpdateBuilderNew` https://github.com/flutter/engine/pull/47961
**part 2: [flutter] use `SemanticsUpdateBuilderNew`**  <-- we are here
part 3: [engine] update `SemanticsUpdateBuilder` to be the same as `SemanticsUpdateBuilderNew`*
part 4: [flutter] use (now updated) `SemanticsUpdateBuilder` again.
part 5: [engine] remove `SemanticsBuilderNew`
2023-12-11 18:03:07 +00:00

1510 lines
49 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.
// flutter_ignore_for_file: golden_tag (see analyze.dart)
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix3;
/// Class that makes it easy to mock common toStringDeep behavior.
class _MockToStringDeep {
_MockToStringDeep(String str) : _lines = <String>[] {
final List<String> lines = str.split('\n');
for (int i = 0; i < lines.length - 1; ++i) {
_lines.add('${lines[i]}\n');
}
// If the last line is empty, that really just means that the previous
// line was terminated with a line break.
if (lines.isNotEmpty && lines.last.isNotEmpty) {
_lines.add(lines.last);
}
}
_MockToStringDeep.fromLines(this._lines);
/// Lines in the message to display when [toStringDeep] is called.
/// For correct toStringDeep behavior, each line should be terminated with a
/// line break.
final List<String> _lines;
String toStringDeep({ String prefixLineOne = '', String prefixOtherLines = '' }) {
final StringBuffer sb = StringBuffer();
if (_lines.isNotEmpty) {
sb.write('$prefixLineOne${_lines.first}');
}
for (int i = 1; i < _lines.length; ++i) {
sb.write('$prefixOtherLines${_lines[i]}');
}
return sb.toString();
}
@override
String toString() => toStringDeep();
}
void main() {
test('hasOneLineDescription', () {
expect('Hello', hasOneLineDescription);
expect('Hello\nHello', isNot(hasOneLineDescription));
expect(' Hello', isNot(hasOneLineDescription));
expect('Hello ', isNot(hasOneLineDescription));
expect(Object(), isNot(hasOneLineDescription));
});
test('hasAGoodToStringDeep', () {
expect(_MockToStringDeep('Hello\n World\n'), hasAGoodToStringDeep);
// Not terminated with a line break.
expect(_MockToStringDeep('Hello\n World'), isNot(hasAGoodToStringDeep));
// Trailing whitespace on last line.
expect(_MockToStringDeep('Hello\n World \n'),
isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('Hello\n World\t\n'),
isNot(hasAGoodToStringDeep));
// Leading whitespace on line 1.
expect(_MockToStringDeep(' Hello\n World \n'),
isNot(hasAGoodToStringDeep));
// Single line.
expect(_MockToStringDeep('Hello World'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('Hello World\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('Hello: World\nFoo: bar\n'),
hasAGoodToStringDeep);
expect(_MockToStringDeep('Hello: World\nFoo: 42\n'),
hasAGoodToStringDeep);
// Contains default Object.toString().
expect(_MockToStringDeep('Hello: World\nFoo: ${Object()}\n'),
isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n'), hasAGoodToStringDeep);
expect(_MockToStringDeep('A\n├─B\n╘══════\n'), hasAGoodToStringDeep);
// Last line is all whitespace or vertical line art.
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep('A\n├─B\n ││\n'), isNot(hasAGoodToStringDeep));
expect(_MockToStringDeep(
'A\n'
'├─B\n'
'\n'
'└─C\n'), hasAGoodToStringDeep);
// Last line is all whitespace or vertical line art.
expect(_MockToStringDeep(
'A\n'
'├─B\n'
'\n'), isNot(hasAGoodToStringDeep));
expect(
_MockToStringDeep.fromLines(<String>[
'Paragraph#00000\n',
' │ size: (400x200)\n',
' ╘═╦══ text ═══\n',
' ║ TextSpan:\n',
' ║ "I polished up that handle so carefullee\n',
' ║ That now I am the Ruler of the Queen\'s Navee!"\n',
' ╚═══════════\n',
]),
hasAGoodToStringDeep,
);
// Text span
expect(
_MockToStringDeep.fromLines(<String>[
'Paragraph#00000\n',
' │ size: (400x200)\n',
' ╘═╦══ text ═══\n',
' ║ TextSpan:\n',
' ║ "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen\'s Navee!"\n',
' ╚═══════════\n',
]),
isNot(hasAGoodToStringDeep),
);
});
test('equalsIgnoringHashCodes', () {
expect('Foo#34219', equalsIgnoringHashCodes('Foo#00000'));
expect('Foo#34219', equalsIgnoringHashCodes('Foo#12345'));
expect('Foo#34219', equalsIgnoringHashCodes('Foo#abcdf'));
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo')));
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#')));
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#0')));
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#00')));
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#00000 ')));
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#000000')));
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#123456')));
expect('Foo#34219:', equalsIgnoringHashCodes('Foo#00000:'));
expect('Foo#34219:', isNot(equalsIgnoringHashCodes('Foo#00000')));
expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#00000'));
expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#12345'));
expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#abcdf'));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo')));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#')));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#0')));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#00')));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#00000 ')));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#000000')));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#123456')));
expect('FOO#A3b4D', equalsIgnoringHashCodes('FOO#00000'));
expect('FOO#A3b4J', isNot(equalsIgnoringHashCodes('FOO#00000')));
expect('Foo#12345(Bar#9110f)',
equalsIgnoringHashCodes('Foo#00000(Bar#00000)'));
expect('Foo#12345(Bar#9110f)',
isNot(equalsIgnoringHashCodes('Foo#00000(Bar#)')));
expect('Foo', isNot(equalsIgnoringHashCodes('Foo#00000')));
expect('Foo#', isNot(equalsIgnoringHashCodes('Foo#00000')));
expect('Foo#3421', isNot(equalsIgnoringHashCodes('Foo#00000')));
expect('Foo#342193', isNot(equalsIgnoringHashCodes('Foo#00000')));
expect(<String>['Foo#a3b4d'], equalsIgnoringHashCodes(<String>['Foo#12345']));
expect(
<String>['Foo#a3b4d', 'Foo#12345'],
equalsIgnoringHashCodes(<String>['Foo#00000', 'Foo#00000']),
);
expect(
<String>['Foo#a3b4d', 'Bar#12345'],
equalsIgnoringHashCodes(<String>['Foo#00000', 'Bar#00000']),
);
expect(
<String>['Foo#a3b4d', 'Bar#12345'],
isNot(equalsIgnoringHashCodes(<String>['Bar#00000', 'Foo#00000'])),
);
expect(<String>['Foo#a3b4d'], isNot(equalsIgnoringHashCodes(<String>['Foo'])));
expect(
<String>['Foo#a3b4d'],
isNot(equalsIgnoringHashCodes(<String>['Foo#00000', 'Bar#00000'])),
);
});
test('moreOrLessEquals', () {
expect(0.0, moreOrLessEquals(1e-11));
expect(1e-11, moreOrLessEquals(0.0));
expect(-1e-11, moreOrLessEquals(0.0));
expect(0.0, isNot(moreOrLessEquals(1e11)));
expect(1e11, isNot(moreOrLessEquals(0.0)));
expect(-1e11, isNot(moreOrLessEquals(0.0)));
expect(0.0, isNot(moreOrLessEquals(1.0)));
expect(1.0, isNot(moreOrLessEquals(0.0)));
expect(-1.0, isNot(moreOrLessEquals(0.0)));
expect(1e-11, moreOrLessEquals(-1e-11));
expect(-1e-11, moreOrLessEquals(1e-11));
expect(11.0, isNot(moreOrLessEquals(-11.0, epsilon: 1.0)));
expect(-11.0, isNot(moreOrLessEquals(11.0, epsilon: 1.0)));
expect(11.0, moreOrLessEquals(-11.0, epsilon: 100.0));
expect(-11.0, moreOrLessEquals(11.0, epsilon: 100.0));
expect(0, moreOrLessEquals(0.0));
expect(0.0, moreOrLessEquals(0));
});
test('matrixMoreOrLessEquals', () {
expect(
Matrix4.rotationZ(math.pi),
matrixMoreOrLessEquals(Matrix4.fromList(<double>[
-1, 0, 0, 0,
0, -1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]))
);
expect(
Matrix4.rotationZ(math.pi),
matrixMoreOrLessEquals(Matrix4.fromList(<double>[
-2, 0, 0, 0,
0, -2, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]), epsilon: 2)
);
expect(
Matrix4.rotationZ(math.pi),
isNot(matrixMoreOrLessEquals(Matrix4.fromList(<double>[
-2, 0, 0, 0,
0, -2, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
])))
);
});
test('matrix3MoreOrLessEquals', () {
expect(
Matrix3.rotationZ(math.pi),
matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
-1, 0, 0,
0, -1, 0,
0, 0, 1,
]))
);
expect(
Matrix3.rotationZ(math.pi),
matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
-2, 0, 0,
0, -2, 0,
0, 0, 1,
]), epsilon: 2)
);
expect(
Matrix3.rotationZ(math.pi),
isNot(matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
-2, 0, 0,
0, -2, 0,
0, 0, 1,
])))
);
});
test('rectMoreOrLessEquals', () {
expect(
const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
rectMoreOrLessEquals(const Rect.fromLTRB(0.0, 0.0, 10.0, 10.00000000001)),
);
expect(
const Rect.fromLTRB(11.0, 11.0, 20.0, 20.0),
isNot(rectMoreOrLessEquals(const Rect.fromLTRB(-11.0, -11.0, 20.0, 20.0), epsilon: 1.0)),
);
expect(
const Rect.fromLTRB(11.0, 11.0, 20.0, 20.0),
rectMoreOrLessEquals(const Rect.fromLTRB(-11.0, -11.0, 20.0, 20.0), epsilon: 100.0),
);
});
test('within', () {
expect(0.0, within<double>(distance: 0.1, from: 0.05));
expect(0.0, isNot(within<double>(distance: 0.1, from: 0.2)));
expect(0, within<int>(distance: 1, from: 1));
expect(0, isNot(within<int>(distance: 1, from: 2)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x01000000)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00010000)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00000100)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00000001)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x01010101)));
expect(const Color(0x00000000), isNot(within<Color>(distance: 1, from: const Color(0x02000000))));
expect(const Offset(1.0, 0.0), within(distance: 1.0, from: Offset.zero));
expect(const Offset(1.0, 0.0), isNot(within(distance: 1.0, from: const Offset(-1.0, 0.0))));
expect(const Rect.fromLTRB(0.0, 1.0, 2.0, 3.0), within<Rect>(distance: 4.0, from: const Rect.fromLTRB(1.0, 3.0, 5.0, 7.0)));
expect(const Rect.fromLTRB(0.0, 1.0, 2.0, 3.0), isNot(within<Rect>(distance: 3.9, from: const Rect.fromLTRB(1.0, 3.0, 5.0, 7.0))));
expect(const Size(1.0, 1.0), within<Size>(distance: 1.415, from: const Size(2.0, 2.0)));
expect(const Size(1.0, 1.0), isNot(within<Size>(distance: 1.414, from: const Size(2.0, 2.0))));
expect(
() => within<bool>(distance: 1, from: false),
throwsArgumentError,
);
expect(
() => within<int>(distance: 1, from: 2, distanceFunction: (int a, int b) => -1).matches(1, <dynamic, dynamic>{}),
throwsArgumentError,
);
});
test('isSameColorAs', () {
expect(
const Color(0x87654321),
isSameColorAs(const _CustomColor(0x87654321)),
);
expect(
const _CustomColor(0x87654321),
isSameColorAs(const Color(0x87654321)),
);
expect(
const Color(0x12345678),
isNot(isSameColorAs(const _CustomColor(0x87654321))),
);
expect(
const _CustomColor(0x87654321),
isNot(isSameColorAs(const Color(0x12345678))),
);
expect(
const _CustomColor(0xFF123456),
isSameColorAs(const _CustomColor(0xFF123456, isEqual: false)),
);
});
group('coversSameAreaAs', () {
test('empty Paths', () {
expect(
Path(),
coversSameAreaAs(
Path(),
areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
),
);
});
test('mismatch', () {
final Path rectPath = Path()
..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
expect(
Path(),
isNot(coversSameAreaAs(
rectPath,
areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
)),
);
});
test('mismatch out of examined area', () {
final Path rectPath = Path()
..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
rectPath.addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
expect(
Path(),
coversSameAreaAs(
rectPath,
areaToCompare: const Rect.fromLTRB(0.0, 0.0, 4.0, 4.0),
),
);
});
test('differently constructed rects match', () {
final Path rectPath = Path()
..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
final Path linePath = Path()
..moveTo(5.0, 5.0)
..lineTo(5.0, 6.0)
..lineTo(6.0, 6.0)
..lineTo(6.0, 5.0)
..close();
expect(
linePath,
coversSameAreaAs(
rectPath,
areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
),
);
});
test('partially overlapping paths', () {
final Path rectPath = Path()
..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
final Path linePath = Path()
..moveTo(5.0, 5.0)
..lineTo(5.0, 6.0)
..lineTo(6.0, 6.0)
..lineTo(6.0, 5.5)
..close();
expect(
linePath,
isNot(coversSameAreaAs(
rectPath,
areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
)),
);
});
});
group('matchesGoldenFile', () {
late _FakeComparator comparator;
Widget boilerplate(Widget child) {
return Directionality(
textDirection: TextDirection.ltr,
child: child,
);
}
setUp(() {
comparator = _FakeComparator();
goldenFileComparator = comparator;
});
group('matches', () {
testWidgets('if comparator succeeds', (WidgetTester tester) async {
await tester.pumpWidget(boilerplate(const Text('hello')));
final Finder finder = find.byType(Text);
await expectLater(finder, matchesGoldenFile('foo.png'));
expect(comparator.invocation, _ComparatorInvocation.compare);
expect(comparator.imageBytes, hasLength(greaterThan(0)));
expect(comparator.golden, Uri.parse('foo.png'));
});
testWidgets('list of integers', (WidgetTester tester) async {
await expectLater(<int>[1, 2], matchesGoldenFile('foo.png'));
expect(comparator.invocation, _ComparatorInvocation.compare);
expect(comparator.imageBytes, equals(<int>[1, 2]));
expect(comparator.golden, Uri.parse('foo.png'));
});
testWidgets('future list of integers', (WidgetTester tester) async {
await expectLater(Future<List<int>>.value(<int>[1, 2]), matchesGoldenFile('foo.png'));
expect(comparator.invocation, _ComparatorInvocation.compare);
expect(comparator.imageBytes, equals(<int>[1, 2]));
expect(comparator.golden, Uri.parse('foo.png'));
});
testWidgets('future nullable list of integers',
(WidgetTester tester) async {
await expectLater(Future<List<int>?>.value(<int>[1, 2]), matchesGoldenFile('foo.png'));
expect(comparator.invocation, _ComparatorInvocation.compare);
expect(comparator.imageBytes, equals(<int>[1, 2]));
expect(comparator.golden, Uri.parse('foo.png'));
});
});
group('does not match', () {
testWidgets('if comparator returns false', (WidgetTester tester) async {
comparator.behavior = _ComparatorBehavior.returnFalse;
await tester.pumpWidget(boilerplate(const Text('hello')));
final Finder finder = find.byType(Text);
await expectLater(
() => expectLater(finder, matchesGoldenFile('foo.png')),
throwsA(isA<TestFailure>().having(
(TestFailure error) => error.message,
'message',
contains('does not match'),
)),
);
expect(comparator.invocation, _ComparatorInvocation.compare);
});
testWidgets('if comparator throws', (WidgetTester tester) async {
comparator.behavior = _ComparatorBehavior.throwTestFailure;
await tester.pumpWidget(boilerplate(const Text('hello')));
final Finder finder = find.byType(Text);
await expectLater(
() => expectLater(finder, matchesGoldenFile('foo.png')),
throwsA(isA<TestFailure>().having(
(TestFailure error) => error.message,
'message',
contains('fake message'),
)),
);
expect(comparator.invocation, _ComparatorInvocation.compare);
});
testWidgets('if finder finds no widgets', (WidgetTester tester) async {
await tester.pumpWidget(boilerplate(Container()));
final Finder finder = find.byType(Text);
await expectLater(
() => expectLater(finder, matchesGoldenFile('foo.png')),
throwsA(isA<TestFailure>().having(
(TestFailure error) => error.message,
'message',
contains('no widget was found'),
)),
);
expect(comparator.invocation, isNull);
});
testWidgets('if finder finds multiple widgets', (WidgetTester tester) async {
await tester.pumpWidget(boilerplate(const Column(
children: <Widget>[Text('hello'), Text('world')],
)));
final Finder finder = find.byType(Text);
await expectLater(
() => expectLater(finder, matchesGoldenFile('foo.png')),
throwsA(isA<TestFailure>().having(
(TestFailure error) => error.message,
'message',
contains('too many widgets'),
)),
);
expect(comparator.invocation, isNull);
});
});
testWidgets('calls update on comparator if autoUpdateGoldenFiles is true', (WidgetTester tester) async {
autoUpdateGoldenFiles = true;
await tester.pumpWidget(boilerplate(const Text('hello')));
final Finder finder = find.byType(Text);
await expectLater(finder, matchesGoldenFile('foo.png'));
expect(comparator.invocation, _ComparatorInvocation.update);
expect(comparator.imageBytes, hasLength(greaterThan(0)));
expect(comparator.golden, Uri.parse('foo.png'));
autoUpdateGoldenFiles = false;
});
});
group('matchesSemanticsData', () {
testWidgets('matches SemanticsData', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
const Key key = Key('semantics');
await tester.pumpWidget(Semantics(
key: key,
namesRoute: true,
header: true,
button: true,
link: true,
onTap: () { },
onLongPress: () { },
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
onTapHint: 'scan',
onLongPressHint: 'fill',
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () { },
const CustomSemanticsAction(label: 'bar'): () { },
},
));
expect(tester.getSemantics(find.byKey(key)),
matchesSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar'),
],
),
);
// Doesn't match custom actions
expect(tester.getSemantics(find.byKey(key)),
isNot(matchesSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'barz'),
],
)),
);
// Doesn't match wrong hints
expect(tester.getSemantics(find.byKey(key)),
isNot(matchesSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scans',
onLongPressHint: 'fills',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar'),
],
)),
);
handle.dispose();
});
testWidgets('Can match all semantics flags and actions', (WidgetTester tester) async {
int actions = 0;
int flags = 0;
const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
for (final SemanticsAction action in SemanticsAction.values) {
actions |= action.index;
}
for (final SemanticsFlag flag in SemanticsFlag.values) {
flags |= flag.index;
}
final SemanticsData data = SemanticsData(
flags: flags,
actions: actions,
identifier: 'i',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
attributedDecreasedValue: AttributedString('d'),
attributedHint: AttributedString('e'),
tooltip: 'f',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
platformViewId: 105,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
currentValueLength: 10,
maxValueLength: 15,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
expect(node, matchesSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
size: const Size(10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
/* Flags */
hasCheckedState: true,
isChecked: true,
isCheckStateMixed: true,
isSelected: true,
isButton: true,
isSlider: true,
isKeyboardKey: true,
isLink: true,
isTextField: true,
isReadOnly: true,
hasEnabledState: true,
isFocused: true,
isFocusable: true,
isEnabled: true,
isInMutuallyExclusiveGroup: true,
isHeader: true,
isObscured: true,
isMultiline: true,
namesRoute: true,
scopesRoute: true,
isHidden: true,
isImage: true,
isLiveRegion: true,
hasToggledState: true,
isToggled: true,
hasImplicitScrolling: true,
hasExpandedState: true,
isExpanded: true,
/* Actions */
hasTapAction: true,
hasLongPressAction: true,
hasScrollLeftAction: true,
hasScrollRightAction: true,
hasScrollUpAction: true,
hasScrollDownAction: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
hasShowOnScreenAction: true,
hasMoveCursorForwardByCharacterAction: true,
hasMoveCursorBackwardByCharacterAction: true,
hasMoveCursorForwardByWordAction: true,
hasMoveCursorBackwardByWordAction: true,
hasSetTextAction: true,
hasSetSelectionAction: true,
hasCopyAction: true,
hasCutAction: true,
hasPasteAction: true,
hasDidGainAccessibilityFocusAction: true,
hasDidLoseAccessibilityFocusAction: true,
hasDismissAction: true,
customActions: <CustomSemanticsAction>[action],
));
});
testWidgets('Can match child semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
const Key key = Key('a');
await tester.pumpWidget(Semantics(
key: key,
label: 'Foo',
container: true,
explicitChildNodes: true,
textDirection: TextDirection.ltr,
child: Semantics(
label: 'Bar',
textDirection: TextDirection.ltr,
),
));
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(node, matchesSemantics(
label: 'Foo',
textDirection: TextDirection.ltr,
children: <Matcher>[
matchesSemantics(
label: 'Bar',
textDirection: TextDirection.ltr,
),
],
));
handle.dispose();
});
testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
const Key key = Key('semantics');
await tester.pumpWidget(Semantics(
key: key,
namesRoute: true,
header: true,
button: true,
link: true,
onTap: () { },
onLongPress: () { },
identifier: 'ident',
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
onTapHint: 'scan',
onLongPressHint: 'fill',
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () { },
const CustomSemanticsAction(label: 'bar'): () { },
},
));
// This should fail due to the mis-match between the `namesRoute` value.
void failedExpectation() => expect(tester.getSemantics(find.byKey(key)),
matchesSemantics(
// Adding the explicit `false` for test readability
// ignore: avoid_redundant_argument_values
namesRoute: false,
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar'),
],
),
);
expect(failedExpectation, throwsA(isA<TestFailure>()));
handle.dispose();
});
});
group('containsSemantics', () {
testWidgets('matches SemanticsData', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
const Key key = Key('semantics');
await tester.pumpWidget(Semantics(
key: key,
namesRoute: true,
header: true,
button: true,
link: true,
onTap: () { },
onLongPress: () { },
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
onTapHint: 'scan',
onLongPressHint: 'fill',
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () { },
const CustomSemanticsAction(label: 'bar'): () { },
},
));
expect(
tester.getSemantics(find.byKey(key)),
containsSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar'),
],
),
);
expect(
tester.getSemantics(find.byKey(key)),
isNot(containsSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'barz'),
],
)),
reason: 'CustomSemanticsAction "barz" should not have matched "bar".'
);
expect(
tester.getSemantics(find.byKey(key)),
isNot(matchesSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scans',
onLongPressHint: 'fills',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar'),
],
)),
reason: 'onTapHint "scans" should not have matched "scan".',
);
handle.dispose();
});
testWidgets('can match all semantics flags and actions enabled', (WidgetTester tester) async {
int actions = 0;
int flags = 0;
const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
for (final SemanticsAction action in SemanticsAction.values) {
actions |= action.index;
}
for (final SemanticsFlag flag in SemanticsFlag.values) {
flags |= flag.index;
}
final SemanticsData data = SemanticsData(
flags: flags,
actions: actions,
identifier: 'i',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
attributedDecreasedValue: AttributedString('d'),
attributedHint: AttributedString('e'),
tooltip: 'f',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
platformViewId: 105,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
currentValueLength: 10,
maxValueLength: 15,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
expect(
node,
containsSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
size: const Size(10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
/* Flags */
hasCheckedState: true,
isChecked: true,
isSelected: true,
isButton: true,
isSlider: true,
isKeyboardKey: true,
isLink: true,
isTextField: true,
isReadOnly: true,
hasEnabledState: true,
isFocused: true,
isFocusable: true,
isEnabled: true,
isInMutuallyExclusiveGroup: true,
isHeader: true,
isObscured: true,
isMultiline: true,
namesRoute: true,
scopesRoute: true,
isHidden: true,
isImage: true,
isLiveRegion: true,
hasToggledState: true,
isToggled: true,
hasImplicitScrolling: true,
hasExpandedState: true,
isExpanded: true,
/* Actions */
hasTapAction: true,
hasLongPressAction: true,
hasScrollLeftAction: true,
hasScrollRightAction: true,
hasScrollUpAction: true,
hasScrollDownAction: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
hasShowOnScreenAction: true,
hasMoveCursorForwardByCharacterAction: true,
hasMoveCursorBackwardByCharacterAction: true,
hasMoveCursorForwardByWordAction: true,
hasMoveCursorBackwardByWordAction: true,
hasSetTextAction: true,
hasSetSelectionAction: true,
hasCopyAction: true,
hasCutAction: true,
hasPasteAction: true,
hasDidGainAccessibilityFocusAction: true,
hasDidLoseAccessibilityFocusAction: true,
hasDismissAction: true,
customActions: <CustomSemanticsAction>[action],
),
);
});
testWidgets('can match all flags and actions disabled', (WidgetTester tester) async {
final SemanticsData data = SemanticsData(
flags: 0,
actions: 0,
identifier: 'i',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
attributedDecreasedValue: AttributedString('d'),
attributedHint: AttributedString('e'),
tooltip: 'f',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
expect(
node,
containsSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
size: const Size(10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
/* Flags */
hasCheckedState: false,
isChecked: false,
isSelected: false,
isButton: false,
isSlider: false,
isKeyboardKey: false,
isLink: false,
isTextField: false,
isReadOnly: false,
hasEnabledState: false,
isFocused: false,
isFocusable: false,
isEnabled: false,
isInMutuallyExclusiveGroup: false,
isHeader: false,
isObscured: false,
isMultiline: false,
namesRoute: false,
scopesRoute: false,
isHidden: false,
isImage: false,
isLiveRegion: false,
hasToggledState: false,
isToggled: false,
hasImplicitScrolling: false,
hasExpandedState: false,
isExpanded: false,
/* Actions */
hasTapAction: false,
hasLongPressAction: false,
hasScrollLeftAction: false,
hasScrollRightAction: false,
hasScrollUpAction: false,
hasScrollDownAction: false,
hasIncreaseAction: false,
hasDecreaseAction: false,
hasShowOnScreenAction: false,
hasMoveCursorForwardByCharacterAction: false,
hasMoveCursorBackwardByCharacterAction: false,
hasMoveCursorForwardByWordAction: false,
hasMoveCursorBackwardByWordAction: false,
hasSetTextAction: false,
hasSetSelectionAction: false,
hasCopyAction: false,
hasCutAction: false,
hasPasteAction: false,
hasDidGainAccessibilityFocusAction: false,
hasDidLoseAccessibilityFocusAction: false,
hasDismissAction: false,
),
);
});
testWidgets('only matches given flags and actions', (WidgetTester tester) async {
int allActions = 0;
int allFlags = 0;
for (final SemanticsAction action in SemanticsAction.values) {
allActions |= action.index;
}
for (final SemanticsFlag flag in SemanticsFlag.values) {
allFlags |= flag.index;
}
final SemanticsData emptyData = SemanticsData(
flags: 0,
actions: 0,
identifier: 'i',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
attributedDecreasedValue: AttributedString('d'),
attributedHint: AttributedString('e'),
tooltip: 'f',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
);
final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);
const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
final SemanticsData fullData = SemanticsData(
flags: allFlags,
actions: allActions,
identifier: 'i',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
attributedDecreasedValue: AttributedString('d'),
attributedHint: AttributedString('e'),
tooltip: 'f',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
);
final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);
expect(
emptyNode,
containsSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
size: const Size(10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
),
);
expect(
fullNode,
containsSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
size: const Size(10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
customActions: <CustomSemanticsAction>[action],
),
);
});
testWidgets('can match child semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
const Key key = Key('a');
await tester.pumpWidget(Semantics(
key: key,
label: 'Foo',
container: true,
explicitChildNodes: true,
textDirection: TextDirection.ltr,
child: Semantics(
label: 'Bar',
textDirection: TextDirection.ltr,
),
));
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(
node,
containsSemantics(
label: 'Foo',
textDirection: TextDirection.ltr,
children: <Matcher>[
containsSemantics(
label: 'Bar',
textDirection: TextDirection.ltr,
),
],
),
);
handle.dispose();
});
testWidgets('can match only custom actions', (WidgetTester tester) async {
const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
final SemanticsData data = SemanticsData(
flags: 0,
actions: SemanticsAction.customAction.index,
identifier: 'i',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
attributedDecreasedValue: AttributedString('d'),
attributedHint: AttributedString('e'),
tooltip: 'f',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
expect(node, containsSemantics(customActions: <CustomSemanticsAction>[action]));
});
testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
const Key key = Key('semantics');
await tester.pumpWidget(Semantics(
key: key,
namesRoute: true,
header: true,
button: true,
link: true,
onTap: () { },
onLongPress: () { },
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
onTapHint: 'scan',
onLongPressHint: 'fill',
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () { },
const CustomSemanticsAction(label: 'bar'): () { },
},
));
// This should fail due to the mis-match between the `namesRoute` value.
void failedExpectation() => expect(tester.getSemantics(find.byKey(key)),
containsSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
increasedValue: 'a',
decreasedValue: 'b',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isLink: true,
isHeader: true,
namesRoute: false,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar'),
],
),
);
expect(failedExpectation, throwsA(isA<TestFailure>()));
handle.dispose();
});
});
group('findsAtLeastNWidgets', () {
Widget boilerplate(Widget child) {
return Directionality(
textDirection: TextDirection.ltr,
child: child,
);
}
testWidgets('succeeds when finds more then the specified count',
(WidgetTester tester) async {
await tester.pumpWidget(boilerplate(const Column(
children: <Widget>[Text('1'), Text('2'), Text('3')],
)));
expect(find.byType(Text), findsAtLeastNWidgets(2));
});
testWidgets('succeeds when finds the exact specified count',
(WidgetTester tester) async {
await tester.pumpWidget(boilerplate(const Column(
children: <Widget>[Text('1'), Text('2')],
)));
expect(find.byType(Text), findsAtLeastNWidgets(2));
});
testWidgets('fails when finds less then specified count',
(WidgetTester tester) async {
await tester.pumpWidget(boilerplate(const Column(
children: <Widget>[Text('1'), Text('2')],
)));
expect(find.byType(Text), isNot(findsAtLeastNWidgets(3)));
});
});
group('findsOneWidget', () {
testWidgets('finds exactly one widget', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
expect(find.text('foo'), findsOneWidget);
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
late TestFailure failure;
try {
expect(find.text('foo', skipOffstage: false), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: exactly one matching candidate\n'));
expect(message, contains('Actual: _TextWidgetFinder:<Found 0 widgets with text "foo"'));
expect(message, contains('Which: means none were found but one was expected\n'));
});
});
group('findsNothing', () {
testWidgets('finds no widgets', (WidgetTester tester) async {
expect(find.text('foo'), findsNothing);
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
late TestFailure failure;
try {
expect(find.text('foo', skipOffstage: false), findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: no matching candidates\n'));
expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"'));
expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
late TestFailure failure;
try {
expect(find.text('foo'), findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: no matching candidates\n'));
expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"'));
expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
});
}
enum _ComparatorBehavior {
returnTrue,
returnFalse,
throwTestFailure,
}
enum _ComparatorInvocation {
compare,
update,
}
class _FakeComparator implements GoldenFileComparator {
_ComparatorBehavior behavior = _ComparatorBehavior.returnTrue;
_ComparatorInvocation? invocation;
Uint8List? imageBytes;
Uri? golden;
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) {
invocation = _ComparatorInvocation.compare;
this.imageBytes = imageBytes;
this.golden = golden;
switch (behavior) {
case _ComparatorBehavior.returnTrue:
return Future<bool>.value(true);
case _ComparatorBehavior.returnFalse:
return Future<bool>.value(false);
case _ComparatorBehavior.throwTestFailure:
fail('fake message');
}
}
@override
Future<void> update(Uri golden, Uint8List imageBytes) {
invocation = _ComparatorInvocation.update;
this.golden = golden;
this.imageBytes = imageBytes;
return Future<void>.value();
}
@override
Uri getTestUri(Uri key, int? version) {
return key;
}
}
class _FakeSemanticsNode extends SemanticsNode {
_FakeSemanticsNode(this.data);
SemanticsData data;
@override
SemanticsData getSemanticsData() => data;
}
@immutable
class _CustomColor extends Color {
const _CustomColor(super.value, {this.isEqual});
final bool? isEqual;
@override
bool operator ==(Object other) => isEqual ?? super == other;
@override
int get hashCode => Object.hash(super.hashCode, isEqual);
}