matchers_test.dart 25.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// flutter_ignore_for_file: golden_tag (see analyze.dart)

7
import 'dart:typed_data';
8 9
import 'dart:ui';

10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/widgets.dart';
12 13
import 'package:flutter_test/flutter_test.dart';

14 15
/// Class that makes it easy to mock common toStringDeep behavior.
class _MockToStringDeep {
16
  _MockToStringDeep(String str) : _lines = <String>[] {
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
    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.
33
  final List<String> _lines;
34

35
  String toStringDeep({ String prefixLineOne = '', String prefixOtherLines = '' }) {
36
    final StringBuffer sb = StringBuffer();
37 38 39 40 41 42 43 44 45 46 47 48 49
    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();
}

50
void main() {
51 52 53 54 55
  test('hasOneLineDescription', () {
    expect('Hello', hasOneLineDescription);
    expect('Hello\nHello', isNot(hasOneLineDescription));
    expect(' Hello', isNot(hasOneLineDescription));
    expect('Hello ', isNot(hasOneLineDescription));
56
    expect(Object(), isNot(hasOneLineDescription));
57 58
  });

59
  test('hasAGoodToStringDeep', () {
60
    expect(_MockToStringDeep('Hello\n World\n'), hasAGoodToStringDeep);
61
    // Not terminated with a line break.
62
    expect(_MockToStringDeep('Hello\n World'), isNot(hasAGoodToStringDeep));
63
    // Trailing whitespace on last line.
64
    expect(_MockToStringDeep('Hello\n World \n'),
65
        isNot(hasAGoodToStringDeep));
66
    expect(_MockToStringDeep('Hello\n World\t\n'),
67 68
        isNot(hasAGoodToStringDeep));
    // Leading whitespace on line 1.
69
    expect(_MockToStringDeep(' Hello\n World \n'),
70 71 72
        isNot(hasAGoodToStringDeep));

    // Single line.
73 74
    expect(_MockToStringDeep('Hello World'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('Hello World\n'), isNot(hasAGoodToStringDeep));
75

76
    expect(_MockToStringDeep('Hello: World\nFoo: bar\n'),
77
        hasAGoodToStringDeep);
78
    expect(_MockToStringDeep('Hello: World\nFoo: 42\n'),
79 80
        hasAGoodToStringDeep);
    // Contains default Object.toString().
81
    expect(_MockToStringDeep('Hello: World\nFoo: ${Object()}\n'),
82
        isNot(hasAGoodToStringDeep));
83 84
    expect(_MockToStringDeep('A\n├─B\n'), hasAGoodToStringDeep);
    expect(_MockToStringDeep('A\n├─B\n╘══════\n'), hasAGoodToStringDeep);
85
    // Last line is all whitespace or vertical line art.
86 87 88 89 90 91 92 93 94 95 96 97
    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(
98 99 100 101 102
        'A\n'
        '├─B\n'
        '│\n'
        '└─C\n'), hasAGoodToStringDeep);
    // Last line is all whitespace or vertical line art.
103
    expect(_MockToStringDeep(
104 105 106 107
        'A\n'
        '├─B\n'
        '│\n'), isNot(hasAGoodToStringDeep));

108
    expect(_MockToStringDeep.fromLines(
109 110 111 112 113 114 115 116 117
        <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
118
    expect(_MockToStringDeep.fromLines(
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
        <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('normalizeHashCodesEquals', () {
    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')));

153 154
    expect('FOO#A3b4D', equalsIgnoringHashCodes('FOO#00000'));
    expect('FOO#A3b4J', isNot(equalsIgnoringHashCodes('FOO#00000')));
155 156 157 158 159 160 161 162 163 164 165 166

    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')));
  });

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
  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));
  });
189

190 191
  test('rectMoreOrLessEquals', () {
    expect(
Dan Field's avatar
Dan Field committed
192 193
      const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
      rectMoreOrLessEquals(const Rect.fromLTRB(0.0, 0.0, 10.0, 10.00000000001)),
194 195 196
    );

    expect(
Dan Field's avatar
Dan Field committed
197 198
      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)),
199 200 201
    );

    expect(
Dan Field's avatar
Dan Field committed
202 203
      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),
204 205 206
    );
  });

207 208 209 210 211 212 213 214 215 216 217 218 219 220
  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))));

221
    expect(const Offset(1.0, 0.0), within(distance: 1.0, from: Offset.zero));
222 223
    expect(const Offset(1.0, 0.0), isNot(within(distance: 1.0, from: const Offset(-1.0, 0.0))));

Dan Field's avatar
Dan Field committed
224 225
    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))));
226 227 228 229

    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))));

230 231 232 233 234 235 236 237 238 239
    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,
    );
  });
240

241 242 243
  test('isSameColorAs', () {
    expect(
      const Color(0x87654321),
244
      isSameColorAs(const _CustomColor(0x87654321)),
245 246 247
    );

    expect(
248
      const _CustomColor(0x87654321),
249 250 251 252 253
      isSameColorAs(const Color(0x87654321)),
    );

    expect(
      const Color(0x12345678),
254
      isNot(isSameColorAs(const _CustomColor(0x87654321))),
255 256 257
    );

    expect(
258
      const _CustomColor(0x87654321),
259 260 261 262
      isNot(isSameColorAs(const Color(0x12345678))),
    );

    expect(
263 264
      const _CustomColor(0xFF123456),
      isSameColorAs(const _CustomColor(0xFF123456, isEqual: false)),
265 266 267
    );
  });

268 269 270
  group('coversSameAreaAs', () {
    test('empty Paths', () {
      expect(
271
        Path(),
272
        coversSameAreaAs(
273
          Path(),
Dan Field's avatar
Dan Field committed
274
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
275 276 277 278 279
        ),
      );
    });

    test('mismatch', () {
280
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
281
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
282
      expect(
283
        Path(),
284 285
        isNot(coversSameAreaAs(
          rectPath,
Dan Field's avatar
Dan Field committed
286
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
287 288 289 290 291
        )),
      );
    });

    test('mismatch out of examined area', () {
292
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
293 294
        ..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));
295
      expect(
296
        Path(),
297 298
        coversSameAreaAs(
          rectPath,
Dan Field's avatar
Dan Field committed
299
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 4.0, 4.0),
300 301 302 303 304
        ),
      );
    });

    test('differently constructed rects match', () {
305
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
306
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
307
      final Path linePath = Path()
308 309 310 311 312 313 314 315 316
        ..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,
Dan Field's avatar
Dan Field committed
317
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
318 319 320 321
        ),
      );
    });

322
    test('partially overlapping paths', () {
323
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
324
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
325
      final Path linePath = Path()
326 327 328 329 330 331 332 333 334
        ..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,
Dan Field's avatar
Dan Field committed
335
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
336 337 338 339
        )),
      );
    });
  });
340 341

  group('matchesGoldenFile', () {
342
    late _FakeComparator comparator;
343 344

    Widget boilerplate(Widget child) {
345
      return Directionality(
346 347 348 349 350 351
        textDirection: TextDirection.ltr,
        child: child,
      );
    }

    setUp(() {
352
      comparator = _FakeComparator();
353 354 355 356 357 358 359 360 361 362 363 364
      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'));
      });
365 366 367 368 369 370 371 372 373 374 375 376 377 378

      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'));
      });
379 380 381 382 383 384 385
    });

    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);
386 387 388 389 390 391 392 393 394
        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);
395 396 397 398 399 400
      });

      testWidgets('if comparator throws', (WidgetTester tester) async {
        comparator.behavior = _ComparatorBehavior.throwTestFailure;
        await tester.pumpWidget(boilerplate(const Text('hello')));
        final Finder finder = find.byType(Text);
401 402 403 404 405 406 407 408 409
        await expectLater(
          () => expectLater(finder, matchesGoldenFile('foo.png')),
          throwsA(isA<TestFailure>().having(
            (TestFailure error) => error.message,
            'message',
            contains('fake message'),
          )),
        );
        expect(comparator.invocation, _ComparatorInvocation.compare);
410 411 412
      });

      testWidgets('if finder finds no widgets', (WidgetTester tester) async {
413
        await tester.pumpWidget(boilerplate(Container()));
414
        final Finder finder = find.byType(Text);
415 416 417 418 419 420 421 422 423
        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);
424 425 426
      });

      testWidgets('if finder finds multiple widgets', (WidgetTester tester) async {
427
        await tester.pumpWidget(boilerplate(Column(
428
          children: const <Widget>[Text('hello'), Text('world')],
429 430
        )));
        final Finder finder = find.byType(Text);
431 432 433 434 435 436 437 438 439
        await expectLater(
          () => expectLater(finder, matchesGoldenFile('foo.png')),
          throwsA(isA<TestFailure>().having(
            (TestFailure error) => error.message,
            'message',
            contains('too many widgets'),
          )),
        );
        expect(comparator.invocation, isNull);
440 441 442 443 444 445 446 447 448 449 450
      });
    });

    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'));
451
      autoUpdateGoldenFiles = false;
452 453
    });
  });
454 455 456 457

  group('matchesSemanticsData', () {
    testWidgets('matches SemanticsData', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
458
      const Key key = Key('semantics');
459
      await tester.pumpWidget(Semantics(
460 461 462 463
        key: key,
        namesRoute: true,
        header: true,
        button: true,
464
        link: true,
465 466
        onTap: () { },
        onLongPress: () { },
467 468 469
        label: 'foo',
        hint: 'bar',
        value: 'baz',
470 471
        increasedValue: 'a',
        decreasedValue: 'b',
472
        textDirection: TextDirection.rtl,
473 474 475
        onTapHint: 'scan',
        onLongPressHint: 'fill',
        customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
476 477
          const CustomSemanticsAction(label: 'foo'): () { },
          const CustomSemanticsAction(label: 'bar'): () { },
478
        },
479 480
      ));

481 482
      expect(tester.getSemantics(find.byKey(key)),
        matchesSemantics(
483 484 485
          label: 'foo',
          hint: 'bar',
          value: 'baz',
486 487
          increasedValue: 'a',
          decreasedValue: 'b',
488 489
          textDirection: TextDirection.rtl,
          hasTapAction: true,
490
          hasLongPressAction: true,
491
          isButton: true,
492
          isLink: true,
493 494
          isHeader: true,
          namesRoute: true,
495 496 497 498
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
499
            const CustomSemanticsAction(label: 'bar'),
500
          ],
501 502
        ),
      );
503 504

      // Doesn't match custom actions
505 506
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
507 508 509 510 511 512 513
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
514
          isLink: true,
515 516 517 518 519 520
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
521
            const CustomSemanticsAction(label: 'barz'),
522 523 524 525 526
          ],
        )),
      );

      // Doesn't match wrong hints
527 528
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
529 530 531 532 533 534 535
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
536
          isLink: true,
537 538 539 540 541 542
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scans',
          onLongPressHint: 'fills',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
543
            const CustomSemanticsAction(label: 'bar'),
544 545 546 547
          ],
        )),
      );

548 549 550 551 552 553
      handle.dispose();
    });

    testWidgets('Can match all semantics flags and actions', (WidgetTester tester) async {
      int actions = 0;
      int flags = 0;
Jonah Williams's avatar
Jonah Williams committed
554
      const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
555
      for (final int index in SemanticsAction.values.keys)
556
        actions |= index;
557
      for (final int index in SemanticsFlag.values.keys)
558 559 560
        // TODO(mdebbar): Remove this if after https://github.com/flutter/engine/pull/9894
        if (SemanticsFlag.values[index] != SemanticsFlag.isMultiline)
          flags |= index;
561
      final SemanticsData data = SemanticsData(
562 563
        flags: flags,
        actions: actions,
564 565 566 567 568
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
569
        textDirection: TextDirection.ltr,
Dan Field's avatar
Dan Field committed
570
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
571 572
        elevation: 3.0,
        thickness: 4.0,
573
        textSelection: null,
574 575
        scrollIndex: null,
        scrollChildCount: null,
576 577 578
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
579
        platformViewId: 105,
580
        customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
581 582
        currentValueLength: 10,
        maxValueLength: 15,
583
      );
584
      final _FakeSemanticsNode node = _FakeSemanticsNode(data);
585

586
      expect(node, matchesSemantics(
Dan Field's avatar
Dan Field committed
587
         rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
588
         size: const Size(10.0, 10.0),
589 590
         elevation: 3.0,
         thickness: 4.0,
591
         platformViewId: 105,
592 593
         currentValueLength: 10,
         maxValueLength: 15,
594 595 596 597 598
         /* Flags */
         hasCheckedState: true,
         isChecked: true,
         isSelected: true,
         isButton: true,
599
         isSlider: true,
600
         isKeyboardKey: true,
601
         isLink: true,
602
         isTextField: true,
603
         isReadOnly: true,
604 605
         hasEnabledState: true,
         isFocused: true,
606
         isFocusable: true,
607 608 609 610
         isEnabled: true,
         isInMutuallyExclusiveGroup: true,
         isHeader: true,
         isObscured: true,
611 612
         // TODO(mdebbar): Uncomment after https://github.com/flutter/engine/pull/9894
         //isMultiline: true,
613 614 615
         namesRoute: true,
         scopesRoute: true,
         isHidden: true,
616 617 618 619
         isImage: true,
         isLiveRegion: true,
         hasToggledState: true,
         isToggled: true,
620
         hasImplicitScrolling: true,
621 622 623 624 625 626 627 628 629 630 631 632
         /* Actions */
         hasTapAction: true,
         hasLongPressAction: true,
         hasScrollLeftAction: true,
         hasScrollRightAction: true,
         hasScrollUpAction: true,
         hasScrollDownAction: true,
         hasIncreaseAction: true,
         hasDecreaseAction: true,
         hasShowOnScreenAction: true,
         hasMoveCursorForwardByCharacterAction: true,
         hasMoveCursorBackwardByCharacterAction: true,
633 634
         hasMoveCursorForwardByWordAction: true,
         hasMoveCursorBackwardByWordAction: true,
635
         hasSetTextAction: true,
636 637 638 639 640 641
         hasSetSelectionAction: true,
         hasCopyAction: true,
         hasCutAction: true,
         hasPasteAction: true,
         hasDidGainAccessibilityFocusAction: true,
         hasDidLoseAccessibilityFocusAction: true,
642
         hasDismissAction: true,
643
         customActions: <CustomSemanticsAction>[action],
644
      ));
645
    });
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674

    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();
    });
675
  });
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690
}

enum _ComparatorBehavior {
  returnTrue,
  returnFalse,
  throwTestFailure,
}

enum _ComparatorInvocation {
  compare,
  update,
}

class _FakeComparator implements GoldenFileComparator {
  _ComparatorBehavior behavior = _ComparatorBehavior.returnTrue;
691 692 693
  _ComparatorInvocation? invocation;
  Uint8List? imageBytes;
  Uri? golden;
694 695 696 697 698 699 700 701

  @override
  Future<bool> compare(Uint8List imageBytes, Uri golden) {
    invocation = _ComparatorInvocation.compare;
    this.imageBytes = imageBytes;
    this.golden = golden;
    switch (behavior) {
      case _ComparatorBehavior.returnTrue:
702
        return Future<bool>.value(true);
703
      case _ComparatorBehavior.returnFalse:
704
        return Future<bool>.value(false);
705
      case _ComparatorBehavior.throwTestFailure:
706
        fail('fake message');
707 708 709 710 711 712 713 714
    }
  }

  @override
  Future<void> update(Uri golden, Uint8List imageBytes) {
    invocation = _ComparatorInvocation.update;
    this.golden = golden;
    this.imageBytes = imageBytes;
715
    return Future<void>.value();
716
  }
717 718

  @override
719
  Uri getTestUri(Uri key, int? version) {
720 721
    return key;
  }
722
}
723 724

class _FakeSemanticsNode extends SemanticsNode {
725 726
  _FakeSemanticsNode(this.data);

727 728 729
  SemanticsData data;
  @override
  SemanticsData getSemanticsData() => data;
730
}
731

732
@immutable
733
class _CustomColor extends Color {
734
  const _CustomColor(int value, {this.isEqual}) : super(value);
735
  final bool? isEqual;
736 737

  @override
738
  bool operator ==(Object other) => isEqual ?? super == other;
739 740 741 742

  @override
  int get hashCode => hashValues(super.hashCode, isEqual);
}