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

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

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

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

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

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

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

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

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

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

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

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

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

206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
  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: const Offset(0.0, 0.0)));
    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
223 224
    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))));
225 226 227 228

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

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

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

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

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

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

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

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

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

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

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

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

  group('matchesGoldenFile', () {
    _FakeComparator comparator;

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

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

    group('matches', () {
356

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
      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);
        try {
          await expectLater(finder, matchesGoldenFile('foo.png'));
          fail('TestFailure expected but not thrown');
        } on TestFailure catch (error) {
          expect(comparator.invocation, _ComparatorInvocation.compare);
          expect(error.message, contains('does not match'));
        }
      });

      testWidgets('if comparator throws', (WidgetTester tester) async {
        comparator.behavior = _ComparatorBehavior.throwTestFailure;
        await tester.pumpWidget(boilerplate(const Text('hello')));
        final Finder finder = find.byType(Text);
        try {
          await expectLater(finder, matchesGoldenFile('foo.png'));
          fail('TestFailure expected but not thrown');
        } on TestFailure catch (error) {
          expect(comparator.invocation, _ComparatorInvocation.compare);
          expect(error.message, contains('fake message'));
        }
      });

      testWidgets('if finder finds no widgets', (WidgetTester tester) async {
395
        await tester.pumpWidget(boilerplate(Container()));
396 397 398 399 400 401 402 403 404 405 406
        final Finder finder = find.byType(Text);
        try {
          await expectLater(finder, matchesGoldenFile('foo.png'));
          fail('TestFailure expected but not thrown');
        } on TestFailure catch (error) {
          expect(comparator.invocation, isNull);
          expect(error.message, contains('no widget was found'));
        }
      });

      testWidgets('if finder finds multiple widgets', (WidgetTester tester) async {
407
        await tester.pumpWidget(boilerplate(Column(
408
          children: const <Widget>[Text('hello'), Text('world')],
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
        )));
        final Finder finder = find.byType(Text);
        try {
          await expectLater(finder, matchesGoldenFile('foo.png'));
          fail('TestFailure expected but not thrown');
        } on TestFailure catch (error) {
          expect(comparator.invocation, isNull);
          expect(error.message, contains('too many widgets'));
        }
      });
    });

    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'));
429
      autoUpdateGoldenFiles = false;
430 431
    });
  });
432 433 434 435

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

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

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

      // Doesn't match wrong hints
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: 'scans',
          onLongPressHint: 'fills',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
521
            const CustomSemanticsAction(label: 'bar'),
522 523 524 525
          ],
        )),
      );

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

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

    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();
    });
651
  });
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
}

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:
678
        return Future<bool>.value(true);
679
      case _ComparatorBehavior.returnFalse:
680
        return Future<bool>.value(false);
681
      case _ComparatorBehavior.throwTestFailure:
682
        throw TestFailure('fake message');
683
    }
684
    return Future<bool>.value(false);
685 686 687 688 689 690 691
  }

  @override
  Future<void> update(Uri golden, Uint8List imageBytes) {
    invocation = _ComparatorInvocation.update;
    this.golden = golden;
    this.imageBytes = imageBytes;
692
    return Future<void>.value();
693
  }
694 695 696 697 698

  @override
  Uri getTestUri(Uri key, int version) {
    return key;
  }
699
}
700 701 702 703 704

class _FakeSemanticsNode extends SemanticsNode {
  SemanticsData data;
  @override
  SemanticsData getSemanticsData() => data;
705
}
706

707
@immutable
708
class _CustomColor extends Color {
709 710
  const _CustomColor(int value, {this.isEqual}) : super(value);
  final bool isEqual;
711 712

  @override
713
  bool operator ==(Object other) => isEqual ?? super == other;
714 715 716 717

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