matchers_test.dart 24.7 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
import 'dart:typed_data';
6 7
import 'dart:ui';

8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
10 11
import 'package:flutter_test/flutter_test.dart';

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

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

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

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

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

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

106
    expect(_MockToStringDeep.fromLines(
107 108 109 110 111 112 113 114 115
        <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
116
    expect(_MockToStringDeep.fromLines(
117 118 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
        <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')));

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

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

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

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

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

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

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

219
    expect(const Offset(1.0, 0.0), within(distance: 1.0, from: Offset.zero));
220 221
    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
222 223
    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))));
224 225 226 227

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

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

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

    expect(
246
      const _CustomColor(0x87654321),
247 248 249 250 251
      isSameColorAs(const Color(0x87654321)),
    );

    expect(
      const Color(0x12345678),
252
      isNot(isSameColorAs(const _CustomColor(0x87654321))),
253 254 255
    );

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

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

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

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

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

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

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

  group('matchesGoldenFile', () {
340
    late _FakeComparator comparator;
341 342

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

    setUp(() {
350
      comparator = _FakeComparator();
351 352 353 354
      goldenFileComparator = comparator;
    });

    group('matches', () {
355

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
      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'));
      });
    });

    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);
371 372 373 374 375 376 377 378 379
        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);
380 381 382 383 384 385
      });

      testWidgets('if comparator throws', (WidgetTester tester) async {
        comparator.behavior = _ComparatorBehavior.throwTestFailure;
        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('fake message'),
          )),
        );
        expect(comparator.invocation, _ComparatorInvocation.compare);
395 396 397
      });

      testWidgets('if finder finds no widgets', (WidgetTester tester) async {
398
        await tester.pumpWidget(boilerplate(Container()));
399
        final Finder finder = find.byType(Text);
400 401 402 403 404 405 406 407 408
        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);
409 410 411
      });

      testWidgets('if finder finds multiple widgets', (WidgetTester tester) async {
412
        await tester.pumpWidget(boilerplate(Column(
413
          children: const <Widget>[Text('hello'), Text('world')],
414 415
        )));
        final Finder finder = find.byType(Text);
416 417 418 419 420 421 422 423 424
        await expectLater(
          () => expectLater(finder, matchesGoldenFile('foo.png')),
          throwsA(isA<TestFailure>().having(
            (TestFailure error) => error.message,
            'message',
            contains('too many widgets'),
          )),
        );
        expect(comparator.invocation, isNull);
425 426 427 428 429 430 431 432 433 434 435
      });
    });

    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'));
436
      autoUpdateGoldenFiles = false;
437 438
    });
  });
439 440 441 442

  group('matchesSemanticsData', () {
    testWidgets('matches SemanticsData', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
443
      const Key key = Key('semantics');
444
      await tester.pumpWidget(Semantics(
445 446 447 448
        key: key,
        namesRoute: true,
        header: true,
        button: true,
449
        link: true,
450 451
        onTap: () { },
        onLongPress: () { },
452 453 454
        label: 'foo',
        hint: 'bar',
        value: 'baz',
455 456
        increasedValue: 'a',
        decreasedValue: 'b',
457
        textDirection: TextDirection.rtl,
458 459 460
        onTapHint: 'scan',
        onLongPressHint: 'fill',
        customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
461 462
          const CustomSemanticsAction(label: 'foo'): () { },
          const CustomSemanticsAction(label: 'bar'): () { },
463
        },
464 465
      ));

466 467
      expect(tester.getSemantics(find.byKey(key)),
        matchesSemantics(
468 469 470
          label: 'foo',
          hint: 'bar',
          value: 'baz',
471 472
          increasedValue: 'a',
          decreasedValue: 'b',
473 474
          textDirection: TextDirection.rtl,
          hasTapAction: true,
475
          hasLongPressAction: true,
476
          isButton: true,
477
          isLink: true,
478 479
          isHeader: true,
          namesRoute: true,
480 481 482 483
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
484
            const CustomSemanticsAction(label: 'bar'),
485
          ],
486 487
        ),
      );
488 489

      // Doesn't match custom actions
490 491
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
492 493 494 495 496 497 498
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
499
          isLink: true,
500 501 502 503 504 505
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
506
            const CustomSemanticsAction(label: 'barz'),
507 508 509 510 511
          ],
        )),
      );

      // Doesn't match wrong hints
512 513
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
514 515 516 517 518 519 520
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
521
          isLink: true,
522 523 524 525 526 527
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scans',
          onLongPressHint: 'fills',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
528
            const CustomSemanticsAction(label: 'bar'),
529 530 531 532
          ],
        )),
      );

533 534 535 536 537 538
      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
539
      const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
540
      for (final int index in SemanticsAction.values.keys)
541
        actions |= index;
542
      for (final int index in SemanticsFlag.values.keys)
543 544 545
        // TODO(mdebbar): Remove this if after https://github.com/flutter/engine/pull/9894
        if (SemanticsFlag.values[index] != SemanticsFlag.isMultiline)
          flags |= index;
546
      final SemanticsData data = SemanticsData(
547 548
        flags: flags,
        actions: actions,
549 550 551 552 553
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
554
        textDirection: TextDirection.ltr,
Dan Field's avatar
Dan Field committed
555
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
556 557
        elevation: 3.0,
        thickness: 4.0,
558
        textSelection: null,
559 560
        scrollIndex: null,
        scrollChildCount: null,
561 562 563
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
564
        platformViewId: 105,
565
        customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
566 567
        currentValueLength: 10,
        maxValueLength: 15,
568
      );
569
      final _FakeSemanticsNode node = _FakeSemanticsNode(data);
570

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

    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();
    });
660
  });
661 662 663 664 665 666 667 668 669 670 671 672 673 674 675
}

enum _ComparatorBehavior {
  returnTrue,
  returnFalse,
  throwTestFailure,
}

enum _ComparatorInvocation {
  compare,
  update,
}

class _FakeComparator implements GoldenFileComparator {
  _ComparatorBehavior behavior = _ComparatorBehavior.returnTrue;
676 677 678
  _ComparatorInvocation? invocation;
  Uint8List? imageBytes;
  Uri? golden;
679 680 681 682 683 684 685 686

  @override
  Future<bool> compare(Uint8List imageBytes, Uri golden) {
    invocation = _ComparatorInvocation.compare;
    this.imageBytes = imageBytes;
    this.golden = golden;
    switch (behavior) {
      case _ComparatorBehavior.returnTrue:
687
        return Future<bool>.value(true);
688
      case _ComparatorBehavior.returnFalse:
689
        return Future<bool>.value(false);
690
      case _ComparatorBehavior.throwTestFailure:
691
        throw TestFailure('fake message');
692 693 694 695 696 697 698 699
    }
  }

  @override
  Future<void> update(Uri golden, Uint8List imageBytes) {
    invocation = _ComparatorInvocation.update;
    this.golden = golden;
    this.imageBytes = imageBytes;
700
    return Future<void>.value();
701
  }
702 703

  @override
704
  Uri getTestUri(Uri key, int? version) {
705 706
    return key;
  }
707
}
708 709

class _FakeSemanticsNode extends SemanticsNode {
710 711
  _FakeSemanticsNode(this.data);

712 713 714
  SemanticsData data;
  @override
  SemanticsData getSemanticsData() => data;
715
}
716

717
@immutable
718
class _CustomColor extends Color {
719
  const _CustomColor(int value, {this.isEqual}) : super(value);
720
  final bool? isEqual;
721 722

  @override
723
  bool operator ==(Object other) => isEqual ?? super == other;
724 725 726 727

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