Unverified Commit 235b64ed authored by Yegor's avatar Yegor Committed by GitHub

make date picker accessible (#13502)

* make date picker accessible

* make test file lookup location-independent

* address some comments

* always wrap in IgnorePointer

* no bitmasks for flags and actions

* recommend List<*>
parent dc9c9537
......@@ -196,6 +196,17 @@ abstract class MaterialLocalizations {
/// - Russian: ср, сент. 27
String formatMediumDate(DateTime date);
/// Formats day of week, month, day of month and year in a long-width format.
///
/// Does not abbreviate names. Appears in spoken announcements of the date
/// picker invoked using [showDatePicker], when accessibility mode is on.
///
/// Examples:
///
/// - US English: Wednesday, September 27, 2017
/// - Russian: Среда, Сентябрь 27, 2017
String formatFullDate(DateTime date);
/// Formats the month and the year of the given [date].
///
/// The returned string does not contain the day of the month. This appears
......@@ -275,7 +286,7 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
const DefaultMaterialLocalizations();
// Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6
static const List<String>_shortWeekdays = const <String>[
static const List<String> _shortWeekdays = const <String>[
'Mon',
'Tue',
'Wed',
......@@ -285,6 +296,17 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
'Sun',
];
// Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6
static const List<String> _weekdays = const <String>[
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
static const List<String> _narrowWeekdays = const <String>[
'S',
'M',
......@@ -365,6 +387,12 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
return '$day, $month ${date.day}';
}
@override
String formatFullDate(DateTime date) {
final String month = _months[date.month - DateTime.JANUARY];
return '${_weekdays[date.weekday - DateTime.MONDAY]}, $month ${date.day}, ${date.year}';
}
@override
String formatMonthYear(DateTime date) {
final String year = formatYear(date);
......
......@@ -46,7 +46,8 @@ class TestSemantics {
this.transform,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(flags != null),
}) : assert(flags is int || flags is List<SemanticsFlags>),
assert(actions is int || actions is List<SemanticsAction>),
assert(label != null),
assert(value != null),
assert(increasedValue != null),
......@@ -70,7 +71,8 @@ class TestSemantics {
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : id = 0,
assert(flags != null),
assert(flags is int || flags is List<SemanticsFlags>),
assert(actions is int || actions is List<SemanticsAction>),
assert(label != null),
assert(increasedValue != null),
assert(decreasedValue != null),
......@@ -103,7 +105,8 @@ class TestSemantics {
Matrix4 transform,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(flags != null),
}) : assert(flags is int || flags is List<SemanticsFlags>),
assert(actions is int || actions is List<SemanticsAction>),
assert(label != null),
assert(value != null),
assert(increasedValue != null),
......@@ -119,11 +122,24 @@ class TestSemantics {
/// they are created.
final int id;
/// A bit field of [SemanticsFlags] that apply to this node.
final int flags;
/// The [SemanticsFlags] set on this node.
///
/// There are two ways to specify this property: as an `int` that encodes the
/// flags as a bit field, or as a `List<SemanticsFlags>` that are _on_.
///
/// Using `List<SemanticsFlags>` is recommended due to better readability.
final dynamic flags;
/// A bit field of [SemanticsActions] that apply to this node.
final int actions;
/// The [SemanticsAction]s set on this node.
///
/// There are two ways to specify this property: as an `int` that encodes the
/// actions as a bit field, or as a `List<SemanticsAction>`.
///
/// Using `List<SemanticsAction>` is recommended due to better readability.
///
/// The tester does not check the function corresponding to the action, but
/// only its existence.
final dynamic actions;
/// A textual description of this node.
final String label;
......@@ -204,10 +220,19 @@ class TestSemantics {
return fail('could not find node with id $id.');
if (!ignoreId && id != node.id)
return fail('expected node id $id but found id ${node.id}.');
if (flags != nodeData.flags)
final int flagsBitmask = flags is int
? flags
: flags.fold<int>(0, (int bitmask, SemanticsFlags flag) => bitmask | flag.index);
if (flagsBitmask != nodeData.flags)
return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.');
if (actions != nodeData.actions)
final int actionsBitmask = actions is int
? actions
: actions.fold<int>(0, (int bitmask, SemanticsAction action) => bitmask | action.index);
if (actionsBitmask != nodeData.actions)
return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
if (label != nodeData.label)
return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".');
if (value != nodeData.value)
......@@ -340,6 +365,109 @@ class SemanticsTester {
visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode);
return result;
}
/// Generates an expression that creates a [TestSemantics] reflecting the
/// current tree of [SemanticsNode]s.
///
/// Use this method to generate code for unit tests. It works similar to
/// screenshot testing. The very first time you add semantics to a widget you
/// verify manually that the widget behaves correctly. You then use ths method
/// to generate test code for this widget.
///
/// Example:
///
/// ```dart
/// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
/// var semantics = new SemanticsTester(tester);
/// await tester.pumpWidget(new MyWidget());
/// print(semantics.generateTestSemanticsExpressionForCurrentSemanticsTree());
/// semantics.dispose();
/// });
/// ```
///
/// You can now copy the code printed to the console into a unit test:
///
/// ```dart
/// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
/// var semantics = new SemanticsTester(tester);
/// await tester.pumpWidget(new MyWidget());
/// expect(semantics, hasSemantics(
/// // Generated code:
/// new TestSemantics(
/// ... properties and child nodes ...
/// ),
/// ignoreRect: true,
/// ignoreTransform: true,
/// ignoreId: true,
/// ));
/// semantics.dispose();
/// });
///
/// At this point the unit test should automatically pass because it was
/// generated from the actual [SemanticsNode]s. Next time the semantics tree
/// changes, the test code may either be updated manually, or regenerated and
/// replaced using this method again.
///
/// Avoid submitting huge piles of generated test code. This will make test
/// code hard to review and it will make it tempting to regenerate test code
/// every time and ignore potential regressions. Make sure you do not
/// over-test. Prefer breaking your widgets into smaller widgets and test them
/// individually.
String generateTestSemanticsExpressionForCurrentSemanticsTree() {
final SemanticsNode node = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
return _generateSemanticsTestForNode(node, 0);
}
String _flagsToSemanticsFlagsExpression(int bitmap) {
return SemanticsFlags.values.values
.where((SemanticsFlags flag) => (flag.index & bitmap) != 0)
.join(', ');
}
String _actionsToSemanticsActionExpression(int bitmap) {
return SemanticsAction.values.values
.where((SemanticsAction action) => (action.index & bitmap) != 0)
.join(', ');
}
/// Recursively generates [TestSemantics] code for [node] and its children,
/// indenting the expression by `indentAmount`.
String _generateSemanticsTestForNode(SemanticsNode node, int indentAmount) {
final String indent = ' ' * indentAmount;
final StringBuffer buf = new StringBuffer();
final SemanticsData nodeData = node.getSemanticsData();
buf.writeln('new TestSemantics(');
if (nodeData.flags != 0)
buf.writeln(' flags: <SemanticsFlags>[${_flagsToSemanticsFlagsExpression(nodeData.flags)}],');
if (nodeData.actions != 0)
buf.writeln(' actions: <SemanticsAction>[${_actionsToSemanticsActionExpression(nodeData.actions)}],');
if (node.label != null && node.label.isNotEmpty)
buf.writeln(' label: r\'${node.label}\',');
if (node.value != null && node.value.isNotEmpty)
buf.writeln(' value: r\'${node.value}\',');
if (node.increasedValue != null && node.increasedValue.isNotEmpty)
buf.writeln(' increasedValue: r\'${node.increasedValue}\',');
if (node.decreasedValue != null && node.decreasedValue.isNotEmpty)
buf.writeln(' decreasedValue: r\'${node.decreasedValue}\',');
if (node.hint != null && node.hint.isNotEmpty)
buf.writeln(' hint: r\'${node.hint}\',');
if (node.textDirection != null)
buf.writeln(' textDirection: ${node.textDirection},');
if (node.hasChildren) {
buf.writeln(' children: <TestSemantics>[');
node.visitChildren((SemanticsNode child) {
buf
..write(_generateSemanticsTestForNode(child, 2))
..writeln(',');
return true;
});
buf.writeln(' ],');
}
buf.write(')');
return buf.toString().split('\n').map((String l) => '$indent$l').join('\n');
}
}
class _HasSemantics extends Matcher {
......
// Copyright 2017 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 'dart:async';
import 'dart:io';
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
group('generateTestSemanticsExpressionForCurrentSemanticsTree', () {
_tests();
});
}
void _tests() {
setUp(() {
debugResetSemanticsIdCounter();
});
Future<Null> pumpTestWidget(WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp(
home: new ListView(
children: <Widget>[
const Text('Plain text'),
new Semantics(
selected: true,
checked: true,
onTap: () {},
onDecrease: () {},
value: 'test-value',
increasedValue: 'test-increasedValue',
decreasedValue: 'test-decreasedValue',
hint: 'test-hint',
textDirection: TextDirection.rtl,
child: const Text('Interactive text'),
),
],
),
));
}
// This test generates code using generateTestSemanticsExpressionForCurrentSemanticsTree
// then compares it to the code used in the 'generated code is correct' test
// below. When you update the implementation of generateTestSemanticsExpressionForCurrentSemanticsTree
// also update this code to reflect the new output.
//
// This test is flexible w.r.t. leading and trailing whitespace.
testWidgets('generates code', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await pumpTestWidget(tester);
final String code = semantics
.generateTestSemanticsExpressionForCurrentSemanticsTree()
.split('\n')
.map((String line) => line.trim())
.join('\n')
.trim() + ',';
File findThisTestFile(Directory directory) {
for (FileSystemEntity entity in directory.listSync()) {
if (entity is Directory) {
final File childSearch = findThisTestFile(entity);
if (childSearch != null) {
return childSearch;
}
} else if (entity is File && entity.path.endsWith('semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart')) {
return entity;
}
}
return null;
}
final File thisTestFile = findThisTestFile(Directory.current);
expect(thisTestFile, isNotNull);
String expectedCode = thisTestFile.readAsStringSync();
expectedCode = expectedCode.substring(
expectedCode.indexOf('>' * 12) + 12,
expectedCode.indexOf('<' * 12) - 3,
)
.split('\n')
.map((String line) => line.trim())
.join('\n')
.trim();
semantics.dispose();
expect(code, expectedCode);
});
testWidgets('generated code is correct', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await pumpTestWidget(tester);
expect(
semantics,
hasSemantics(
// The code below delimited by > and < characters is generated by
// generateTestSemanticsExpressionForCurrentSemanticsTree function.
// You must update it when changing the output generated by
// generateTestSemanticsExpressionForCurrentSemanticsTree. Otherwise,
// the test 'generates code', defined above, will fail.
// >>>>>>>>>>>>
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: r'Plain text',
textDirection: TextDirection.ltr,
),
new TestSemantics(
flags: <SemanticsFlags>[SemanticsFlags.hasCheckedState, SemanticsFlags.isChecked, SemanticsFlags.isSelected],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.decrease],
label: r'‪Interactive text‬',
value: r'test-value',
increasedValue: r'test-increasedValue',
decreasedValue: r'test-decreasedValue',
hint: r'test-hint',
textDirection: TextDirection.rtl,
),
],
),
],
),
],
),
// <<<<<<<<<<<<
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
)
);
semantics.dispose();
});
}
......@@ -76,14 +76,18 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
if (intl.DateFormat.localeExists(_localeName)) {
_fullYearFormat = new intl.DateFormat.y(_localeName);
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName);
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(_localeName);
_yearMonthFormat = new intl.DateFormat('yMMMM', _localeName);
} else if (intl.DateFormat.localeExists(locale.languageCode)) {
_fullYearFormat = new intl.DateFormat.y(locale.languageCode);
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode);
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
_yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode);
} else {
_fullYearFormat = new intl.DateFormat.y();
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern);
_longDateFormat = new intl.DateFormat.yMMMMEEEEd();
_yearMonthFormat = new intl.DateFormat('yMMMM');
}
......@@ -115,6 +119,8 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
intl.DateFormat _mediumDateFormat;
intl.DateFormat _longDateFormat;
intl.DateFormat _yearMonthFormat;
static String _computeLocaleName(Locale locale) {
......@@ -169,6 +175,11 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
return _mediumDateFormat.format(date);
}
@override
String formatFullDate(DateTime date) {
return _longDateFormat.format(date);
}
@override
String formatMonthYear(DateTime date) {
return _yearMonthFormat.format(date);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment