matchers_test.dart 23.2 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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

  group('coversSameAreaAs', () {
    test('empty Paths', () {
      expect(
243
        Path(),
244
        coversSameAreaAs(
245
          Path(),
Dan Field's avatar
Dan Field committed
246
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
247 248 249 250 251
        ),
      );
    });

    test('mismatch', () {
252
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
253
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
254
      expect(
255
        Path(),
256 257
        isNot(coversSameAreaAs(
          rectPath,
Dan Field's avatar
Dan Field committed
258
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
259 260 261 262 263
        )),
      );
    });

    test('mismatch out of examined area', () {
264
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
265 266
        ..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));
267
      expect(
268
        Path(),
269 270
        coversSameAreaAs(
          rectPath,
Dan Field's avatar
Dan Field committed
271
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 4.0, 4.0),
272 273 274 275 276
        ),
      );
    });

    test('differently constructed rects match', () {
277
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
278
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
279
      final Path linePath = Path()
280 281 282 283 284 285 286 287 288
        ..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
289
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
290 291 292 293
        ),
      );
    });

294
    test('partially overlapping paths', () {
295
      final Path rectPath = Path()
Dan Field's avatar
Dan Field committed
296
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
297
      final Path linePath = Path()
298 299 300 301 302 303 304 305 306
        ..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
307
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
308 309 310 311
        )),
      );
    });
  });
312 313 314 315 316

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

    Widget boilerplate(Widget child) {
317
      return Directionality(
318 319 320 321 322 323
        textDirection: TextDirection.ltr,
        child: child,
      );
    }

    setUp(() {
324
      comparator = _FakeComparator();
325 326 327 328
      goldenFileComparator = comparator;
    });

    group('matches', () {
329

330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
      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 {
368
        await tester.pumpWidget(boilerplate(Container()));
369 370 371 372 373 374 375 376 377 378 379
        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 {
380
        await tester.pumpWidget(boilerplate(Column(
381
          children: const <Widget>[Text('hello'), Text('world')],
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
        )));
        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'));
402
      autoUpdateGoldenFiles = false;
403 404
    });
  });
405 406 407 408

  group('matchesSemanticsData', () {
    testWidgets('matches SemanticsData', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
409
      const Key key = Key('semantics');
410
      await tester.pumpWidget(Semantics(
411 412 413 414
        key: key,
        namesRoute: true,
        header: true,
        button: true,
415 416
        onTap: () { },
        onLongPress: () { },
417 418 419
        label: 'foo',
        hint: 'bar',
        value: 'baz',
420 421
        increasedValue: 'a',
        decreasedValue: 'b',
422
        textDirection: TextDirection.rtl,
423 424 425
        onTapHint: 'scan',
        onLongPressHint: 'fill',
        customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
426 427
          const CustomSemanticsAction(label: 'foo'): () { },
          const CustomSemanticsAction(label: 'bar'): () { },
428
        },
429 430
      ));

431 432
      expect(tester.getSemantics(find.byKey(key)),
        matchesSemantics(
433 434 435
          label: 'foo',
          hint: 'bar',
          value: 'baz',
436 437
          increasedValue: 'a',
          decreasedValue: 'b',
438 439
          textDirection: TextDirection.rtl,
          hasTapAction: true,
440
          hasLongPressAction: true,
441 442 443
          isButton: true,
          isHeader: true,
          namesRoute: true,
444 445 446 447
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
448
            const CustomSemanticsAction(label: 'bar'),
449
          ],
450 451
        ),
      );
452 453

      // Doesn't match custom actions
454 455
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
456 457 458 459 460 461 462 463 464 465 466 467 468
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
469
            const CustomSemanticsAction(label: 'barz'),
470 471 472 473 474
          ],
        )),
      );

      // Doesn't match wrong hints
475 476
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
477 478 479 480 481 482 483 484 485 486 487 488 489
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scans',
          onLongPressHint: 'fills',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
490
            const CustomSemanticsAction(label: 'bar'),
491 492 493 494
          ],
        )),
      );

495 496 497 498 499 500
      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
501
      const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
502 503 504
      for (int index in SemanticsAction.values.keys)
        actions |= index;
      for (int index in SemanticsFlag.values.keys)
505 506 507
        // TODO(mdebbar): Remove this if after https://github.com/flutter/engine/pull/9894
        if (SemanticsFlag.values[index] != SemanticsFlag.isMultiline)
          flags |= index;
508
      final SemanticsData data = SemanticsData(
509 510
        flags: flags,
        actions: actions,
511 512 513 514 515
        label: 'a',
        increasedValue: 'b',
        value: 'c',
        decreasedValue: 'd',
        hint: 'e',
516
        textDirection: TextDirection.ltr,
Dan Field's avatar
Dan Field committed
517
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
518 519
        elevation: 3.0,
        thickness: 4.0,
520
        textSelection: null,
521 522
        scrollIndex: null,
        scrollChildCount: null,
523 524 525
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
526
        platformViewId: 105,
527
        customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
528
      );
529 530
      final _FakeSemanticsNode node = _FakeSemanticsNode();
      node.data = data;
531

532
      expect(node, matchesSemantics(
Dan Field's avatar
Dan Field committed
533
         rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
534
         size: const Size(10.0, 10.0),
535 536
         elevation: 3.0,
         thickness: 4.0,
537
         platformViewId: 105,
538 539 540 541 542 543
         /* Flags */
         hasCheckedState: true,
         isChecked: true,
         isSelected: true,
         isButton: true,
         isTextField: true,
544
         isReadOnly: true,
545 546 547 548 549 550
         hasEnabledState: true,
         isFocused: true,
         isEnabled: true,
         isInMutuallyExclusiveGroup: true,
         isHeader: true,
         isObscured: true,
551 552
         // TODO(mdebbar): Uncomment after https://github.com/flutter/engine/pull/9894
         //isMultiline: true,
553 554 555
         namesRoute: true,
         scopesRoute: true,
         isHidden: true,
556 557 558 559
         isImage: true,
         isLiveRegion: true,
         hasToggledState: true,
         isToggled: true,
560
         hasImplicitScrolling: true,
561 562 563 564 565 566 567 568 569 570 571 572
         /* Actions */
         hasTapAction: true,
         hasLongPressAction: true,
         hasScrollLeftAction: true,
         hasScrollRightAction: true,
         hasScrollUpAction: true,
         hasScrollDownAction: true,
         hasIncreaseAction: true,
         hasDecreaseAction: true,
         hasShowOnScreenAction: true,
         hasMoveCursorForwardByCharacterAction: true,
         hasMoveCursorBackwardByCharacterAction: true,
573 574
         hasMoveCursorForwardByWordAction: true,
         hasMoveCursorBackwardByWordAction: true,
575 576 577 578 579 580
         hasSetSelectionAction: true,
         hasCopyAction: true,
         hasCutAction: true,
         hasPasteAction: true,
         hasDidGainAccessibilityFocusAction: true,
         hasDidLoseAccessibilityFocusAction: true,
581
         hasDismissAction: true,
582
         customActions: <CustomSemanticsAction>[action],
583 584
      ));
    });
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613

    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();
    });
614
  });
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
}

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:
641
        return Future<bool>.value(true);
642
      case _ComparatorBehavior.returnFalse:
643
        return Future<bool>.value(false);
644
      case _ComparatorBehavior.throwTestFailure:
645
        throw TestFailure('fake message');
646
    }
647
    return Future<bool>.value(false);
648 649 650 651 652 653 654
  }

  @override
  Future<void> update(Uri golden, Uint8List imageBytes) {
    invocation = _ComparatorInvocation.update;
    this.golden = golden;
    this.imageBytes = imageBytes;
655
    return Future<void>.value();
656
  }
657 658 659 660 661

  @override
  Uri getTestUri(Uri key, int version) {
    return key;
  }
662
}
663 664 665 666 667

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