accessibility_test.dart 14.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Copyright 2018 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.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('text contrast guideline', () {
    testWidgets('black text on white background - Text Widget - direct style', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
        const Text(
          'this is a test',
          style: TextStyle(fontSize: 14.0, color: Colors.black),
        ),
      ));
      await expectLater(tester, meetsGuideline(textContrastGuideline));
      handle.dispose();
    });

    testWidgets('white text on black background - Text Widget - direct style', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
25
        Container(
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
          width: 200.0,
          height: 200.0,
          color: Colors.black,
          child: const Text(
            'this is a test',
            style: TextStyle(fontSize: 14.0, color: Colors.white),
          ),
        ),
      ));
      await expectLater(tester, meetsGuideline(textContrastGuideline));
      handle.dispose();
    });

    testWidgets('black text on white background - Text Widget - inherited style', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
42
        DefaultTextStyle(
43
          style: const TextStyle(fontSize: 14.0, color: Colors.black),
44
          child: Container(
45 46 47 48 49 50 51 52 53 54 55 56
            color: Colors.white,
            child: const Text('this is a test'),
          ),
        ),
      ));
      await expectLater(tester, meetsGuideline(textContrastGuideline));
      handle.dispose();
    });

    testWidgets('white text on black background - Text Widget - inherited style', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
57
        DefaultTextStyle(
58
          style: const TextStyle(fontSize: 14.0, color: Colors.white),
59
          child: Container(
60 61 62 63 64 65 66 67 68 69 70 71 72
            width: 200.0,
            height: 200.0,
            color: Colors.black,
            child: const Text('this is a test'),
          ),
        ),
      ));
      await expectLater(tester, meetsGuideline(textContrastGuideline));
      handle.dispose();
    });

    testWidgets('Material text field - amber on amber', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
73 74 75
      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: Container(
76 77 78
            width: 200.0,
            height: 200.0,
            color: Colors.amberAccent,
79
            child: TextField(
80
              style: const TextStyle(color: Colors.amber),
81
              controller: TextEditingController(text: 'this is a test'),
82 83 84 85 86 87 88 89 90 91
            ),
          ),
        ),
      ));
      await expectLater(tester, doesNotMeetGuideline(textContrastGuideline));
      handle.dispose();
    });

    testWidgets('Material text field - default style', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
92 93 94
      await tester.pumpWidget(MaterialApp(
          home: Scaffold(
            body: Center(
95 96 97 98 99
              child: SizedBox(
                width: 100,
                child: TextField(
                  controller: TextEditingController(text: 'this is a test'),
                ),
100 101 102 103 104
              ),
            ),
          ),
        ),
      );
105
      await tester.idle();
106 107 108 109
      await expectLater(tester, meetsGuideline(textContrastGuideline));
      handle.dispose();
    });

110
    testWidgets('yellow text on yellow background fails with correct message', (WidgetTester tester) async {
111 112
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
113
        Container(
114 115 116 117 118 119 120 121 122 123 124 125
          width: 200.0,
          height: 200.0,
          color: Colors.yellow,
          child: const Text(
            'this is a test',
            style: TextStyle(fontSize: 14.0, color: Colors.yellowAccent),
          ),
        ),
      ));
      final Evaluation result = await textContrastGuideline.evaluate(tester);
      expect(result.passed, false);
      expect(result.reason,
126
        'SemanticsNode#3(Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), label: "this is a test",'
127
        ' textDirection: ltr):\nExpected contrast ratio of at least '
128
        '4.5 but found 1.17 for a font size of 14.0. The '
129
        'computed light color was: Color(0xfffafafa), The computed dark color was:'
130
        ' Color(0xffffeb3b)\n'
131 132 133
        'See also: https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html');
      handle.dispose();
    });
134 135 136 137

    testWidgets('label without corresponding text is skipped', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
138
        Semantics(
139 140
          label: 'This is not text',
          container: true,
141
          child: Container(
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
            width: 200.0,
            height: 200.0,
            child: const Placeholder(),
          ),
        ),
      ));

      final Evaluation result = await textContrastGuideline.evaluate(tester);
      expect(result.passed, true);
      handle.dispose();
    });

    testWidgets('offscreen text is skipped', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
157
        Stack(
158
          children: <Widget>[
159
            Positioned(
160
              left: -300.0,
161
              child: Container(
162 163 164 165 166 167 168 169
                width: 200.0,
                height: 200.0,
                color: Colors.yellow,
                child: const Text(
                  'this is a test',
                  style: TextStyle(fontSize: 14.0, color: Colors.yellowAccent),
                ),
              ),
170
            ),
171 172 173 174 175 176 177 178
          ],
        )
      ));

      final Evaluation result = await textContrastGuideline.evaluate(tester);
      expect(result.passed, true);
      handle.dispose();
    });
179 180 181 182 183 184
  });

  group('tap target size guideline', () {
    testWidgets('Tappable box at 48 by 48', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
185
        SizedBox(
186 187
          width: 48.0,
          height: 48.0,
188
          child: GestureDetector(
189
            onTap: () { },
190 191 192 193 194 195 196 197 198 199
          ),
        ),
      ));
      await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
      handle.dispose();
    });

    testWidgets('Tappable box at 47 by 48', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
200
        SizedBox(
201 202
          width: 47.0,
          height: 48.0,
203
          child: GestureDetector(
204
            onTap: () { },
205 206 207 208 209 210 211 212 213 214
          ),
        ),
      ));
      await expectLater(tester, doesNotMeetGuideline(androidTapTargetGuideline));
      handle.dispose();
    });

    testWidgets('Tappable box at 48 by 47', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
215
        SizedBox(
216 217
          width: 48.0,
          height: 47.0,
218
          child: GestureDetector(
219
            onTap: () { },
220 221 222 223 224 225 226 227 228 229
          ),
        ),
      ));
      await expectLater(tester, doesNotMeetGuideline(androidTapTargetGuideline));
      handle.dispose();
    });

    testWidgets('Tappable box at 48 by 48 shrunk by transform', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
230
        Transform.scale(
231
          scale: 0.5, // should have new height of 24 by 24.
232
          child: SizedBox(
233 234
            width: 48.0,
            height: 48.0,
235
            child: GestureDetector(
236
              onTap: () { },
237 238 239 240 241 242 243 244 245 246 247
            ),
          ),
        ),
      ));
      await expectLater(tester, doesNotMeetGuideline(androidTapTargetGuideline));
      handle.dispose();
    });

    testWidgets('Too small tap target fails with the correct message', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
248
        SizedBox(
249 250
          width: 48.0,
          height: 47.0,
251
          child: GestureDetector(
252
            onTap: () { },
253 254 255 256 257 258
          ),
        ),
      ));
      final Evaluation result = await androidTapTargetGuideline.evaluate(tester);
      expect(result.passed, false);
      expect(result.reason,
259
        'SemanticsNode#3(Rect.fromLTRB(376.0, 276.5, 424.0, 323.5), actions: [tap]): expected tap '
260 261 262 263
        'target size of at least Size(48.0, 48.0), but found Size(48.0, 47.0)\n'
        'See also: https://support.google.com/accessibility/android/answer/7101858?hl=en');
      handle.dispose();
    });
264 265 266

    testWidgets('Box that overlaps edge of window is skipped', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
267
      final Widget smallBox = SizedBox(
268 269
        width: 48.0,
        height: 47.0,
270
        child: GestureDetector(
271
          onTap: () { },
272 273 274
        ),
      );
      await tester.pumpWidget(
275 276
        MaterialApp(
          home: Stack(
277
            children: <Widget>[
278
              Positioned(
279 280 281 282 283 284 285 286 287 288 289 290 291
                left: 0.0,
                top: -1.0,
                child: smallBox,
              ),
            ],
          ),
        ),
      );

      final Evaluation overlappingTopResult = await androidTapTargetGuideline.evaluate(tester);
      expect(overlappingTopResult.passed, true);

      await tester.pumpWidget(
292 293
        MaterialApp(
          home: Stack(
294
            children: <Widget>[
295
              Positioned(
296 297 298 299 300 301 302 303 304 305 306 307 308
                left: -1.0,
                top: 0.0,
                child: smallBox,
              ),
            ],
          ),
        ),
      );

      final Evaluation overlappingLeftResult = await androidTapTargetGuideline.evaluate(tester);
      expect(overlappingLeftResult.passed, true);

      await tester.pumpWidget(
309 310
        MaterialApp(
          home: Stack(
311
            children: <Widget>[
312
              Positioned(
313 314 315 316 317 318 319 320 321 322 323 324
                bottom: -1.0,
                child: smallBox,
              ),
            ],
          ),
        ),
      );

      final Evaluation overlappingBottomResult = await androidTapTargetGuideline.evaluate(tester);
      expect(overlappingBottomResult.passed, true);

      await tester.pumpWidget(
325 326
        MaterialApp(
          home: Stack(
327
            children: <Widget>[
328
              Positioned(
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
                right: -1.0,
                child: smallBox,
              ),
            ],
          ),
        ),
      );

      final Evaluation overlappingRightResult = await androidTapTargetGuideline.evaluate(tester);
      expect(overlappingRightResult.passed, true);
      handle.dispose();
    });

    testWidgets('Does not fail on mergedIntoParent child', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
345 346
        MergeSemantics(
          child: Semantics(
347
            container: true,
348
            child: SizedBox(
349 350
              width: 50.0,
              height: 50.0,
351
              child: Semantics(
352
                container: true,
353
                child: GestureDetector(
354
                  onTap: () { },
355
                  child: const SizedBox(width: 4.0, height: 4.0),
356 357
                ),
              ),
358
            ),
359
          ),
360 361 362 363 364 365 366
        )
      ));

      final Evaluation overlappingRightResult = await androidTapTargetGuideline.evaluate(tester);
      expect(overlappingRightResult.passed, true);
      handle.dispose();
    });
367
  });
368

369
  group('Labeled tappable node guideline', () {
370 371 372 373 374
    testWidgets('Passes when node is labeled', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(Semantics(
        container: true,
        child: const SizedBox(width: 10.0, height: 10.0),
375
        onTap: () { },
376 377 378 379 380 381 382 383 384 385 386
        label: 'test',
      )));
      final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
      expect(result.passed, true);
      handle.dispose();
    });
    testWidgets('Fails if long-press has no label', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(Semantics(
        container: true,
        child: const SizedBox(width: 10.0, height: 10.0),
387
        onLongPress: () { },
388 389 390 391 392 393 394 395 396 397 398 399
        label: '',
      )));
      final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
      expect(result.passed, false);
      handle.dispose();
    });

    testWidgets('Fails if tap has no label', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(Semantics(
        container: true,
        child: const SizedBox(width: 10.0, height: 10.0),
400
        onTap: () { },
401 402 403 404 405 406 407 408 409 410 411
        label: '',
      )));
      final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
      expect(result.passed, false);
      handle.dispose();
    });

    testWidgets('Passes if tap is merged into labeled node', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(Semantics(
        container: true,
412
        onLongPress: () { },
413 414 415 416 417 418 419 420 421 422 423 424
        label: '',
        child: Semantics(
          label: 'test',
          child: const SizedBox(width: 10.0, height: 10.0),
        ),
      )));
      final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
      expect(result.passed, true);
      handle.dispose();
    });
  });

425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
  testWidgets('regression test for material widget', (WidgetTester tester) async {
    final SemanticsHandle handle = tester.ensureSemantics();
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData.light(),
        home: Scaffold(
          backgroundColor: Colors.white,
          body: RaisedButton(
            color: const Color(0xFFFBBC04),
            elevation: 0,
            onPressed: () {},
            child: const Text('Button', style: TextStyle(color: Colors.black)),
        ),
      ),
    ));
    await expectLater(tester, meetsGuideline(textContrastGuideline));
    handle.dispose();
  });
443 444 445
}

Widget _boilerplate(Widget child) {
446 447
  return MaterialApp(
    home: Scaffold(body: Center(child: child)),
448 449
  );
}