blob: a509ab2d2cf4edc7aadfba2b52bcac28f4310590 [file] [log] [blame]
// Copyright 2014 The Flutter 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:collection';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
typedef TraversalTestFunction = Future<void> Function(TraversalTester tester);
const Size tenByTen = Size(10.0, 10.0);
void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
void testTraversal(String description, TraversalTestFunction testFunction) {
testWidgets(description, (WidgetTester tester) async {
final TraversalTester traversalTester = TraversalTester(tester);
await testFunction(traversalTester);
traversalTester.dispose();
});
}
// ┌───┐ ┌───┐
// │ A │>│ B │
// └───┘ └───┘
testTraversal('Semantics traverses horizontally left-to-right', (TraversalTester tester) async {
await tester.test(
textDirection: TextDirection.ltr,
children: <String, Rect>{
'A': Offset.zero & tenByTen,
'B': const Offset(20.0, 0.0) & tenByTen,
},
expectedTraversal: 'A B',
);
});
// ┌───┐ ┌───┐
// │ A │<│ B │
// └───┘ └───┘
testTraversal('Semantics traverses horizontally right-to-left', (TraversalTester tester) async {
await tester.test(
textDirection: TextDirection.rtl,
children: <String, Rect>{
'A': Offset.zero & tenByTen,
'B': const Offset(20.0, 0.0) & tenByTen,
},
expectedTraversal: 'B A',
);
});
// ┌───┐
// │ A │
// └───┘
// V
// ┌───┐
// │ B │
// └───┘
testTraversal('Semantics traverses vertically top-to-bottom', (TraversalTester tester) async {
for (final TextDirection textDirection in TextDirection.values) {
await tester.test(
textDirection: textDirection,
children: <String, Rect>{
'A': Offset.zero & tenByTen,
'B': const Offset(0.0, 20.0) & tenByTen,
},
expectedTraversal: 'A B',
);
}
});
// ┌───┐ ┌───┐
// │ A │>│ B │
// └───┘ └───┘
// ┌─────┘
// V
// ┌───┐ ┌───┐
// │ C │>│ D │
// └───┘ └───┘
testTraversal('Semantics traverses a grid left-to-right', (TraversalTester tester) async {
await tester.test(
textDirection: TextDirection.ltr,
children: <String, Rect>{
'A': Offset.zero & tenByTen,
'B': const Offset(20.0, 0.0) & tenByTen,
'C': const Offset(0.0, 20.0) & tenByTen,
'D': const Offset(20.0, 20.0) & tenByTen,
},
expectedTraversal: 'A B C D',
);
});
// ┌───┐ ┌───┐
// │ A │<│ B │
// └───┘ └───┘
// └─────┐
// V
// ┌───┐ ┌───┐
// │ C │<│ D │
// └───┘ └───┘
testTraversal('Semantics traverses a grid right-to-left', (TraversalTester tester) async {
await tester.test(
textDirection: TextDirection.rtl,
children: <String, Rect>{
'A': Offset.zero & tenByTen,
'B': const Offset(20.0, 0.0) & tenByTen,
'C': const Offset(0.0, 20.0) & tenByTen,
'D': const Offset(20.0, 20.0) & tenByTen,
},
expectedTraversal: 'B A D C',
);
});
// ┌───┐ ┌───┐
// │ A │ │ C │
// └───┘<->┌───┐<->└───┘
// │ B │
// └───┘
testTraversal('Semantics traverses vertically overlapping nodes horizontally', (TraversalTester tester) async {
final Map<String, Rect> children = <String, Rect>{
'A': Offset.zero & tenByTen,
'B': const Offset(20.0, 5.0) & tenByTen,
'C': const Offset(40.0, 0.0) & tenByTen,
};
await tester.test(
textDirection: TextDirection.ltr,
children: children,
expectedTraversal: 'A B C',
);
await tester.test(
textDirection: TextDirection.rtl,
children: children,
expectedTraversal: 'C B A',
);
});
// LTR:
// ┌───┐ ┌───┐ ┌───┐ ┌───┐
// │ A │>│ B │>│ C │>│ D │
// └───┘ └───┘ └───┘ └───┘
// ┌─────────────────┘
// V
// ┌───┐ ┌─────────┐ ┌───┐
// │ E │>│ │>│ G │
// └───┘ │ F │ └───┘
// ┌───|─────────|───┘
// ┌───┐ │ │ ┌───┐
// │ H │─|─────────|>│ I │
// └───┘ └─────────┘ └───┘
// ┌─────────────────┘
// V
// ┌───┐ ┌───┐ ┌───┐ ┌───┐
// │ J │>│ K │>│ L │>│ M │
// └───┘ └───┘ └───┘ └───┘
//
// RTL:
// ┌───┐ ┌───┐ ┌───┐ ┌───┐
// │ A │<│ B │<│ C │<│ D │
// └───┘ └───┘ └───┘ └───┘
// └─────────────────┐
// V
// ┌───┐ ┌─────────┐ ┌───┐
// │ E │<│ │<│ G │
// └───┘ │ F │ └───┘
// └──|─────────|────┐
// ┌───┐ │ │ ┌───┐
// │ H │<|─────────|─│ I │
// └───┘ └─────────┘ └───┘
// └─────────────────┐
// V
// ┌───┐ ┌───┐ ┌───┐ ┌───┐
// │ J │<│ K │<│ L │<│ M │
// └───┘ └───┘ └───┘ └───┘
testTraversal('Semantics traverses vertical groups, then horizontal groups, then knots', (TraversalTester tester) async {
final Map<String, Rect> children = <String, Rect>{
'A': Offset.zero & tenByTen,
'B': const Offset(20.0, 0.0) & tenByTen,
'C': const Offset(40.0, 0.0) & tenByTen,
'D': const Offset(60.0, 0.0) & tenByTen,
'E': const Offset(0.0, 20.0) & tenByTen,
'F': const Offset(20.0, 20.0) & (tenByTen * 2.0),
'G': const Offset(60.0, 20.0) & tenByTen,
'H': const Offset(0.0, 40.0) & tenByTen,
'I': const Offset(60.0, 40.0) & tenByTen,
'J': const Offset(0.0, 60.0) & tenByTen,
'K': const Offset(20.0, 60.0) & tenByTen,
'L': const Offset(40.0, 60.0) & tenByTen,
'M': const Offset(60.0, 60.0) & tenByTen,
};
await tester.test(
textDirection: TextDirection.ltr,
children: children,
expectedTraversal: 'A B C D E F G H I J K L M',
);
await tester.test(
textDirection: TextDirection.rtl,
children: children,
expectedTraversal: 'D C B A G F E I H M L K J',
);
});
// The following test tests traversal of the simplest "knot", which is two
// nodes overlapping both vertically and horizontally. For example:
//
// ┌─────────┐
// │ │
// │ A │
// │ ┌───┼─────┐
// │ │ │ │
// └─────┼───┘ │
// │ B │
// │ │
// └─────────┘
//
// The outcome depends on the relative positions of the centers of `Rect`s of
// their respective boxes, specifically the direction (i.e. angle) of the
// vector pointing from A to B. We test different angles, one for each octant:
//
// -3π/4 -π/2 -π/4
// ╲ │ ╱
// ╲ 1│2 ╱
// ╲ │ ╱
// i=0 ╲│╱ 3
// π ──────┼────── 0
// 7 ╱│╲ 4
// ╱ │ ╲
// ╱ 6│5 ╲
// ╱ │ ╲
// 3π/4 π/2 π/4
//
// For LTR, angles falling into octants 3, 4, 5, and 6, produce A -> B, all
// others produce B -> A.
//
// For RTL, angles falling into octants 5, 6, 7, and 0, produce A -> B, all
// others produce B -> A.
testTraversal('Semantics sorts knots', (TraversalTester tester) async {
const double start = -math.pi + math.pi / 8.0;
for (int i = 0; i < 8; i += 1) {
final double angle = start + i.toDouble() * math.pi / 4.0;
// These values should be truncated so that double precision rounding
// issues won't impact the heights/widths and throw off the traversal
// ordering.
final double dx = (math.cos(angle) * 15.0) / 10.0;
final double dy = (math.sin(angle) * 15.0) / 10.0;
final Map<String, Rect> children = <String, Rect>{
'A': const Offset(10.0, 10.0) & tenByTen,
'B': Offset(10.0 + dx, 10.0 + dy) & tenByTen,
};
try {
await tester.test(
textDirection: TextDirection.ltr,
children: children,
expectedTraversal: 3 <= i && i <= 6 ? 'A B' : 'B A',
);
await tester.test(
textDirection: TextDirection.rtl,
children: children,
expectedTraversal: 1 <= i && i <= 4 ? 'B A' : 'A B',
);
} catch (error) {
fail(
'Test failed with i == $i, angle == ${angle / math.pi}π\n'
'$error',
);
}
}
});
}
class TraversalTester {
TraversalTester(this.tester) : semantics = SemanticsTester(tester);
final WidgetTester tester;
final SemanticsTester semantics;
Future<void> test({
required TextDirection textDirection,
required Map<String, Rect> children,
required String expectedTraversal,
}) async {
assert(children is LinkedHashMap);
await tester.pumpWidget(
Directionality(
textDirection: textDirection,
child: Semantics(
textDirection: textDirection,
child: CustomMultiChildLayout(
delegate: TestLayoutDelegate(children),
children: children.keys.map<Widget>((String label) {
return LayoutId(
id: label,
child: Semantics(
container: true,
explicitChildNodes: true,
label: label,
child: SizedBox(
width: children[label]!.width,
height: children[label]!.height,
),
),
);
}).toList(),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
textDirection: textDirection,
children: expectedTraversal.split(' ').map<TestSemantics>((String label) {
return TestSemantics(
label: label,
);
}).toList(),
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
));
}
void dispose() {
semantics.dispose();
}
}
class TestLayoutDelegate extends MultiChildLayoutDelegate {
TestLayoutDelegate(this.children);
final Map<String, Rect> children;
@override
void performLayout(Size size) {
children.forEach((String label, Rect rect) {
layoutChild(label, BoxConstraints.loose(size));
positionChild(label, rect.topLeft);
});
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => oldDelegate == this;
}