mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This reverts commit ef29db350f0951ab976e2fdb5d092e65578329e5. <!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> reland https://github.com/flutter/flutter/pull/165173 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2033 lines
67 KiB
Dart
2033 lines
67 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 'dart:ui' as ui;
|
|
|
|
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;
|
|
|
|
SemanticsFlags allFlags = SemanticsFlags(
|
|
isChecked: ui.CheckedState.isTrue,
|
|
isSelected: ui.Tristate.isTrue,
|
|
isEnabled: ui.Tristate.isTrue,
|
|
isToggled: ui.Tristate.isTrue,
|
|
isExpanded: ui.Tristate.isTrue,
|
|
isRequired: ui.Tristate.isTrue,
|
|
isFocused: ui.Tristate.isTrue,
|
|
isButton: true,
|
|
isTextField: true,
|
|
isInMutuallyExclusiveGroup: true,
|
|
isHeader: true,
|
|
isObscured: true,
|
|
scopesRoute: true,
|
|
namesRoute: true,
|
|
isHidden: true,
|
|
isImage: true,
|
|
isLiveRegion: true,
|
|
hasImplicitScrolling: true,
|
|
isMultiline: true,
|
|
isReadOnly: true,
|
|
isLink: true,
|
|
isSlider: true,
|
|
isKeyboardKey: true,
|
|
);
|
|
|
|
/// 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 (var 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 sb = StringBuffer();
|
|
if (_lines.isNotEmpty) {
|
|
sb.write('$prefixLineOne${_lines.first}');
|
|
}
|
|
|
|
for (var 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('equalsIgnoringHashCodes - wrong line', () {
|
|
TestFailure? failure;
|
|
try {
|
|
expect(
|
|
'1\n2\n3\n4\n5\n6\n7\n8\n9\n10',
|
|
equalsIgnoringHashCodes('1\n2\n3\n4\n5\n6\na\n8\n9\n10'),
|
|
);
|
|
} on TestFailure catch (e) {
|
|
failure = e;
|
|
}
|
|
|
|
expect(failure, isNotNull);
|
|
if (failure != null) {
|
|
final String? message = failure.message;
|
|
expect(message, contains('Lines 7 differed'));
|
|
expect(message, contains("'a'"));
|
|
expect(message, contains("'7'"));
|
|
}
|
|
});
|
|
|
|
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('isSameColorSwatchAs', () {
|
|
expect(
|
|
const ColorSwatch<String>(0xaaaaaaaa, <String, Color>{
|
|
'foo': Color(0xaaaaaaaa),
|
|
'bar': Color(0xbbbbbbbb),
|
|
}),
|
|
isSameColorSwatchAs(
|
|
const ColorSwatch<String>(0xaaaaaaaa, <String, Color>{
|
|
'foo': Color(0xaaaaaaaa),
|
|
'bar': Color(0xbbbbbbbb),
|
|
}),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
const ColorSwatch<String>(0xaaaaaaaa, <String, Color>{
|
|
'foo': Color(0xaaaaaaaa),
|
|
'bar': Color(0xbbbbbbbb),
|
|
}),
|
|
isNot(
|
|
isSameColorSwatchAs(
|
|
const ColorSwatch<String>(0xaaaaaaaa, <String, Color>{
|
|
'foo': Color(0xaaaaaaaa),
|
|
'bar': Color(0xcccccccc),
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
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)),
|
|
);
|
|
|
|
expect(const Color(0x00000000), isNot(isSameColorAs(const Color(0x00000002))));
|
|
|
|
expect(const Color(0x00000000), isSameColorAs(const Color(0x00000002), threshold: 0.008));
|
|
});
|
|
|
|
testWidgets('isSystemTextScaler', (WidgetTester tester) async {
|
|
addTearDown(tester.platformDispatcher.clearAllTestValues);
|
|
tester.platformDispatcher.textScaleFactorTestValue = 123;
|
|
|
|
final mediaQueryData = MediaQueryData.fromView(tester.view);
|
|
final TextScaler systemScaler = mediaQueryData.textScaler;
|
|
expect(systemScaler, isSystemTextScaler());
|
|
expect(systemScaler, isSystemTextScaler(withScaleFactor: 123));
|
|
expect(systemScaler, isNot(isSystemTextScaler(withScaleFactor: 2)));
|
|
});
|
|
|
|
group('coversSameAreaAs', () {
|
|
test('empty Paths', () {
|
|
expect(
|
|
Path(),
|
|
coversSameAreaAs(Path(), areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0)),
|
|
);
|
|
});
|
|
|
|
test('mismatch', () {
|
|
final 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 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 rectPath = Path()..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
|
|
final 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 rectPath = Path()..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
|
|
final 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('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 {
|
|
var actions = 0;
|
|
const action = CustomSemanticsAction(label: 'test');
|
|
for (final SemanticsAction action in SemanticsAction.values) {
|
|
actions |= action.index;
|
|
}
|
|
|
|
final data = SemanticsData(
|
|
flagsCollection: allFlags,
|
|
actions: actions,
|
|
identifier: 'i',
|
|
traversalParentIdentifier: '01',
|
|
traversalChildIdentifier: '01',
|
|
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),
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 105,
|
|
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
|
|
currentValueLength: 10,
|
|
maxValueLength: 15,
|
|
headingLevel: 0,
|
|
linkUrl: Uri(path: 'l'),
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
minValue: '0',
|
|
maxValue: '0',
|
|
);
|
|
final 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,
|
|
hasSelectedState: 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,
|
|
hasRequiredState: true,
|
|
isRequired: 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,
|
|
hasFocusAction: true,
|
|
customActions: <CustomSemanticsAction>[action],
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Can match child semantics', (WidgetTester tester) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
const 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('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();
|
|
});
|
|
|
|
testWidgets('matchesSemantics captures NBSP in failure descriptions', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final data = SemanticsData(
|
|
flagsCollection: SemanticsFlags.none,
|
|
actions: 0,
|
|
identifier: '',
|
|
traversalChildIdentifier: '',
|
|
traversalParentIdentifier: '',
|
|
attributedLabel: AttributedString('label\u202f0'),
|
|
attributedHint: AttributedString('hint\u202f0'),
|
|
attributedValue: AttributedString('value\u202f0'),
|
|
attributedIncreasedValue: AttributedString('increasedValue\u202f0'),
|
|
attributedDecreasedValue: AttributedString('decreasedValue\u202f0'),
|
|
tooltip: 'tooltip\u202f0',
|
|
textDirection: TextDirection.ltr,
|
|
rect: Rect.zero,
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 0,
|
|
maxValueLength: 0,
|
|
currentValueLength: 0,
|
|
headingLevel: 0,
|
|
linkUrl: null,
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
maxValue: '',
|
|
minValue: '',
|
|
);
|
|
final node = _FakeSemanticsNode(data);
|
|
|
|
// Label
|
|
expect(
|
|
() => expect(node, matchesSemantics(label: 'label 0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r'label was label\u202f0'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(node, matchesSemantics(attributedLabel: AttributedString('label 0'))),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r"attributedLabel was: AttributedString('label\u202f0'"),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Hint
|
|
expect(
|
|
() => expect(node, matchesSemantics(hint: 'hint 0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r'hint was hint\u202f0'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(node, matchesSemantics(attributedHint: AttributedString('hint 0'))),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r"attributedHint was: AttributedString('hint\u202f0'"),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Value
|
|
expect(
|
|
() => expect(node, matchesSemantics(value: 'value 0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r'value was value\u202f0'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(node, matchesSemantics(attributedValue: AttributedString('value 0'))),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r"attributedValue was: AttributedString('value\u202f0'"),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Increased Value
|
|
expect(
|
|
() => expect(node, matchesSemantics(increasedValue: 'increasedValue 0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r'increasedValue was increasedValue\u202f0'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(
|
|
node,
|
|
matchesSemantics(attributedIncreasedValue: AttributedString('increasedValue 0')),
|
|
),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r"attributedIncreasedValue was: AttributedString('increasedValue\u202f0'"),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Decreased Value
|
|
expect(
|
|
() => expect(node, matchesSemantics(decreasedValue: 'decreasedValue 0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r'decreasedValue was decreasedValue\u202f0'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(
|
|
node,
|
|
matchesSemantics(attributedDecreasedValue: AttributedString('decreasedValue 0')),
|
|
),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r"attributedDecreasedValue was: AttributedString('decreasedValue\u202f0'"),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Tooltip
|
|
expect(
|
|
() => expect(node, matchesSemantics(tooltip: 'tooltip 0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
contains(r'tooltip was tooltip\u202f0'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'matchesSemantics shows correct error message when actuals have normal space but expects have NBSP',
|
|
(WidgetTester tester) async {
|
|
final data = SemanticsData(
|
|
flagsCollection: SemanticsFlags.none,
|
|
actions: 0,
|
|
identifier: '',
|
|
traversalChildIdentifier: '',
|
|
traversalParentIdentifier: '',
|
|
attributedLabel: AttributedString('label 0'),
|
|
attributedHint: AttributedString('hint 0'),
|
|
attributedValue: AttributedString('value 0'),
|
|
attributedIncreasedValue: AttributedString('increasedValue 0'),
|
|
attributedDecreasedValue: AttributedString('decreasedValue 0'),
|
|
tooltip: 'tooltip 0',
|
|
textDirection: TextDirection.ltr,
|
|
rect: Rect.zero,
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 0,
|
|
maxValueLength: 0,
|
|
currentValueLength: 0,
|
|
headingLevel: 0,
|
|
linkUrl: null,
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
minValue: '',
|
|
maxValue: '',
|
|
);
|
|
final node = _FakeSemanticsNode(data);
|
|
|
|
// Label
|
|
expect(
|
|
() => expect(node, matchesSemantics(label: 'label\u202f0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(contains(r'with label: label\u202f0'), contains(r'label was label 0')),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(node, matchesSemantics(attributedLabel: AttributedString('label\u202f0'))),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(
|
|
contains(r"with attributedLabel: AttributedString('label\u202f0'"),
|
|
contains(r"attributedLabel was: AttributedString('label 0'"),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Hint
|
|
expect(
|
|
() => expect(node, matchesSemantics(hint: 'hint\u202f0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(contains(r'with hint: hint\u202f0'), contains(r'hint was hint 0')),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(node, matchesSemantics(attributedHint: AttributedString('hint\u202f0'))),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(
|
|
contains(r"with attributedHint: AttributedString('hint\u202f0'"),
|
|
contains(r"attributedHint was: AttributedString('hint 0'"),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Value
|
|
expect(
|
|
() => expect(node, matchesSemantics(value: 'value\u202f0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(contains(r'with value: value\u202f0'), contains(r'value was value 0')),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(node, matchesSemantics(attributedValue: AttributedString('value\u202f0'))),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(
|
|
contains(r"with attributedValue: AttributedString('value\u202f0'"),
|
|
contains(r"attributedValue was: AttributedString('value 0'"),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Increased Value
|
|
expect(
|
|
() => expect(node, matchesSemantics(increasedValue: 'increasedValue\u202f0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(
|
|
contains(r'with increasedValue: increasedValue\u202f0'),
|
|
contains(r'increasedValue was increasedValue 0'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(
|
|
node,
|
|
matchesSemantics(attributedIncreasedValue: AttributedString('increasedValue\u202f0')),
|
|
),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(
|
|
contains(
|
|
r"with attributedIncreasedValue: AttributedString('increasedValue\u202f0'",
|
|
),
|
|
contains(r"attributedIncreasedValue was: AttributedString('increasedValue 0'"),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Decreased Value
|
|
expect(
|
|
() => expect(node, matchesSemantics(decreasedValue: 'decreasedValue\u202f0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(
|
|
contains(r'with decreasedValue: decreasedValue\u202f0'),
|
|
contains(r'decreasedValue was decreasedValue 0'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => expect(
|
|
node,
|
|
matchesSemantics(attributedDecreasedValue: AttributedString('decreasedValue\u202f0')),
|
|
),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(
|
|
contains(
|
|
r"with attributedDecreasedValue: AttributedString('decreasedValue\u202f0'",
|
|
),
|
|
contains(r"attributedDecreasedValue was: AttributedString('decreasedValue 0'"),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Tooltip
|
|
expect(
|
|
() => expect(node, matchesSemantics(tooltip: 'tooltip\u202f0')),
|
|
throwsA(
|
|
isA<TestFailure>().having(
|
|
(TestFailure e) => e.message,
|
|
'message',
|
|
allOf(contains(r'with tooltip: tooltip\u202f0'), contains(r'tooltip was tooltip 0')),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('containsSemantics', () {
|
|
testWidgets('matches SemanticsData', (WidgetTester tester) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
|
|
const 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 {
|
|
var actions = 0;
|
|
const action = CustomSemanticsAction(label: 'test');
|
|
for (final SemanticsAction action in SemanticsAction.values) {
|
|
actions |= action.index;
|
|
}
|
|
|
|
final data = SemanticsData(
|
|
flagsCollection: allFlags,
|
|
actions: actions,
|
|
identifier: 'i',
|
|
traversalChildIdentifier: '01',
|
|
traversalParentIdentifier: '01',
|
|
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),
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 105,
|
|
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
|
|
currentValueLength: 10,
|
|
maxValueLength: 15,
|
|
headingLevel: 0,
|
|
linkUrl: Uri(path: 'l'),
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
minValue: '0',
|
|
maxValue: '0',
|
|
);
|
|
final 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,
|
|
hasRequiredState: true,
|
|
isRequired: 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,
|
|
hasFocusAction: true,
|
|
customActions: <CustomSemanticsAction>[action],
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('can match all flags and actions disabled', (WidgetTester tester) async {
|
|
final data = SemanticsData(
|
|
flagsCollection: SemanticsFlags.none,
|
|
actions: 0,
|
|
identifier: 'i',
|
|
traversalParentIdentifier: '01',
|
|
traversalChildIdentifier: '01',
|
|
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),
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 105,
|
|
currentValueLength: 10,
|
|
maxValueLength: 15,
|
|
headingLevel: 0,
|
|
linkUrl: null,
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
minValue: '0',
|
|
maxValue: '0',
|
|
);
|
|
final 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,
|
|
hasRequiredState: false,
|
|
isRequired: 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,
|
|
hasFocusAction: false,
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('only matches given flags and actions', (WidgetTester tester) async {
|
|
var allActions = 0;
|
|
|
|
for (final SemanticsAction action in SemanticsAction.values) {
|
|
allActions |= action.index;
|
|
}
|
|
|
|
final emptyData = SemanticsData(
|
|
flagsCollection: SemanticsFlags.none,
|
|
actions: 0,
|
|
identifier: 'i',
|
|
traversalChildIdentifier: '01',
|
|
traversalParentIdentifier: '01',
|
|
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),
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 105,
|
|
currentValueLength: 10,
|
|
maxValueLength: 15,
|
|
headingLevel: 0,
|
|
linkUrl: null,
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
minValue: '0',
|
|
maxValue: '0',
|
|
);
|
|
final emptyNode = _FakeSemanticsNode(emptyData);
|
|
|
|
const action = CustomSemanticsAction(label: 'test');
|
|
final fullData = SemanticsData(
|
|
flagsCollection: allFlags,
|
|
actions: allActions,
|
|
identifier: 'i',
|
|
traversalChildIdentifier: '01',
|
|
traversalParentIdentifier: '01',
|
|
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),
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 105,
|
|
currentValueLength: 10,
|
|
maxValueLength: 15,
|
|
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
|
|
headingLevel: 0,
|
|
linkUrl: Uri(path: 'l'),
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
minValue: '0',
|
|
maxValue: '0',
|
|
);
|
|
final 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('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 validation result', (WidgetTester tester) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
const key = Key('a');
|
|
await tester.pumpWidget(
|
|
Semantics(
|
|
key: key,
|
|
label: 'Foo',
|
|
validationResult: SemanticsValidationResult.valid,
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
|
|
|
expect(
|
|
node,
|
|
containsSemantics(
|
|
label: 'Foo',
|
|
validationResult: SemanticsValidationResult.valid,
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
testWidgets('can ignore validation result', (WidgetTester tester) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
const key = Key('a');
|
|
await tester.pumpWidget(
|
|
Semantics(
|
|
key: key,
|
|
label: 'Foo',
|
|
validationResult: SemanticsValidationResult.valid,
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
|
// It is important that validationResult is passed as null to containsSemantics,
|
|
// because this is testing that null means "ignore the validation result value".
|
|
expect(node, containsSemantics(label: 'Foo', textDirection: TextDirection.ltr));
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
testWidgets('can match only custom actions', (WidgetTester tester) async {
|
|
const action = CustomSemanticsAction(label: 'test');
|
|
final data = SemanticsData(
|
|
flagsCollection: SemanticsFlags.none,
|
|
actions: SemanticsAction.customAction.index,
|
|
identifier: 'i',
|
|
traversalChildIdentifier: '01',
|
|
traversalParentIdentifier: '01',
|
|
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),
|
|
textSelection: null,
|
|
scrollIndex: null,
|
|
scrollChildCount: null,
|
|
scrollPosition: null,
|
|
scrollExtentMax: null,
|
|
scrollExtentMin: null,
|
|
platformViewId: 105,
|
|
currentValueLength: 10,
|
|
maxValueLength: 15,
|
|
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
|
|
headingLevel: 0,
|
|
linkUrl: null,
|
|
role: ui.SemanticsRole.none,
|
|
controlsNodes: null,
|
|
validationResult: SemanticsValidationResult.none,
|
|
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
|
inputType: ui.SemanticsInputType.none,
|
|
locale: null,
|
|
minValue: '0',
|
|
maxValue: '0',
|
|
);
|
|
final 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('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);
|
|
}
|