mirror of
https://github.com/alibaba/flutter-go.git
synced 2025-07-12 16:10:10 +08:00
376 lines
12 KiB
Dart
376 lines
12 KiB
Dart
// 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 'dart:collection';
|
||
import 'dart:math' as math;
|
||
import 'package:flutter_web_ui/ui.dart';
|
||
|
||
import 'package:flutter_web/material.dart';
|
||
import 'package:flutter_web/rendering.dart';
|
||
import 'package:flutter_web/semantics.dart';
|
||
import 'package:flutter_web/widgets.dart';
|
||
import 'package:flutter_web_test/flutter_web_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': const Offset(0.0, 0.0) & 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': const Offset(0.0, 0.0) & 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 (TextDirection textDirection in TextDirection.values) {
|
||
await tester.test(
|
||
textDirection: textDirection,
|
||
children: <String, Rect>{
|
||
'A': const Offset(0.0, 0.0) & 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': const Offset(0.0, 0.0) & 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': const Offset(0.0, 0.0) & 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': const Offset(0.0, 0.0) & 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 │ │ │>│ H │
|
||
// └───┘ │ G │ └───┘
|
||
// V │ │ V
|
||
// ┌───┐ │ │ ┌───┐
|
||
// │ F │>│ │ │ I │
|
||
// └───┘ └─────────┘ └───┘
|
||
// ┌─────────────────┘
|
||
// V
|
||
// ┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||
// │ J │>│ K │>│ L │>│ M │
|
||
// └───┘ └───┘ └───┘ └───┘
|
||
//
|
||
// RTL:
|
||
// ┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||
// │ A │<│ B │<│ C │<│ D │
|
||
// └───┘ └───┘ └───┘ └───┘
|
||
// └─────────────────┐
|
||
// V
|
||
// ┌───┐ ┌─────────┐ ┌───┐
|
||
// │ E │<│ │ │ H │
|
||
// └───┘ │ G │ └───┘
|
||
// V │ │ V
|
||
// ┌───┐ │ │ ┌───┐
|
||
// │ F │ │ │<│ 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': const Offset(0.0, 0.0) & 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(0.0, 40.0) & tenByTen,
|
||
'G': const Offset(20.0, 20.0) & (tenByTen * 2.0),
|
||
'H': const Offset(60.0, 20.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 H I G E F 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({
|
||
TextDirection textDirection,
|
||
Map<String, Rect> children,
|
||
String expectedTraversal,
|
||
}) async {
|
||
assert(children is LinkedHashMap);
|
||
await tester.pumpWidget(Container(
|
||
child: 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,
|
||
childOrder: DebugSemanticsDumpOrder.traversalOrder,
|
||
));
|
||
}
|
||
|
||
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;
|
||
}
|