Skip to content

Commit c1556cc

Browse files
committed
semantics tests
1 parent f684065 commit c1556cc

File tree

2 files changed

+338
-4
lines changed

2 files changed

+338
-4
lines changed

packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,338 @@ void main() {
800800
await tester.pumpAndSettle();
801801
});
802802
});
803+
804+
group('Link semantics', () {
805+
late TestUrlLauncherPlugin testPlugin;
806+
late UrlLauncherPlatform originalPlugin;
807+
808+
setUp(() {
809+
originalPlugin = UrlLauncherPlatform.instance;
810+
testPlugin = TestUrlLauncherPlugin();
811+
UrlLauncherPlatform.instance = testPlugin;
812+
});
813+
814+
tearDown(() {
815+
UrlLauncherPlatform.instance = originalPlugin;
816+
});
817+
818+
testWidgets('produces the correct semantics tree with a button',
819+
(WidgetTester tester) async {
820+
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
821+
final Key linkKey = UniqueKey();
822+
823+
await tester.pumpWidget(Directionality(
824+
textDirection: TextDirection.ltr,
825+
child: WebLinkDelegate(
826+
key: linkKey,
827+
TestLinkInfo(
828+
uri: Uri.parse('https://foobar/example?q=1'),
829+
target: LinkTarget.blank,
830+
builder: (BuildContext context, FollowLink? followLink) {
831+
return ElevatedButton(
832+
onPressed: followLink,
833+
child: const Text('Button Link Text'),
834+
);
835+
},
836+
),
837+
),
838+
));
839+
840+
final Finder linkFinder = find.byKey(linkKey);
841+
final WebLinkDelegateState linkState = tester.state<WebLinkDelegateState>(linkFinder);
842+
final String semanticIdentifier = linkState.semanticIdentifier;
843+
expect(
844+
tester.getSemantics(find.descendant(
845+
of: linkFinder,
846+
matching: find.byType(Semantics).first,
847+
)),
848+
matchesSemantics(
849+
isLink: true,
850+
identifier: 'sem-id-$semanticIdentifier',
851+
value: 'https://foobar/example?q=1',
852+
children: <Matcher>[
853+
matchesSemantics(
854+
hasTapAction: true,
855+
hasEnabledState: true,
856+
isEnabled: true,
857+
isButton: true,
858+
isFocusable: true,
859+
label: 'Button Link Text',
860+
),
861+
],
862+
),
863+
);
864+
865+
semanticsHandle.dispose();
866+
});
867+
868+
testWidgets('produces the correct semantics tree with text',
869+
(WidgetTester tester) async {
870+
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
871+
final Key linkKey = UniqueKey();
872+
873+
await tester.pumpWidget(Directionality(
874+
textDirection: TextDirection.ltr,
875+
child: WebLinkDelegate(
876+
key: linkKey,
877+
TestLinkInfo(
878+
uri: Uri.parse('https://foobar/example?q=1'),
879+
target: LinkTarget.blank,
880+
builder: (BuildContext context, FollowLink? followLink) {
881+
return GestureDetector(
882+
onTap: followLink,
883+
child: const Text('Link Text'),
884+
);
885+
},
886+
),
887+
),
888+
));
889+
890+
final Finder linkFinder = find.byKey(linkKey);
891+
final WebLinkDelegateState linkState = tester.state<WebLinkDelegateState>(linkFinder);
892+
final String semanticIdentifier = linkState.semanticIdentifier;
893+
expect(
894+
tester.getSemantics(find.descendant(
895+
of: linkFinder,
896+
matching: find.byType(Semantics),
897+
)),
898+
matchesSemantics(
899+
isLink: true,
900+
hasTapAction: true,
901+
identifier: 'sem-id-$semanticIdentifier',
902+
value: 'https://foobar/example?q=1',
903+
label: 'Link Text',
904+
),
905+
);
906+
907+
semanticsHandle.dispose();
908+
});
909+
910+
testWidgets('handles clicks on semantic link with a button',
911+
(WidgetTester tester) async {
912+
final Uri uri = Uri.parse('/foobar');
913+
FollowLink? followLinkCallback;
914+
915+
await tester.pumpWidget(MaterialApp(
916+
routes: <String, WidgetBuilder>{
917+
'/foobar': (BuildContext context) => const Text('Internal route'),
918+
},
919+
home: WebLinkDelegate(TestLinkInfo(
920+
uri: uri,
921+
target: LinkTarget.blank,
922+
builder: (BuildContext context, FollowLink? followLink) {
923+
followLinkCallback = followLink;
924+
return ElevatedButton(
925+
onPressed: () {},
926+
child: const Text('My Button Link'),
927+
);
928+
},
929+
)),
930+
));
931+
// Platform view creation happens asynchronously.
932+
await tester.pumpAndSettle();
933+
934+
final WebLinkDelegateState linkState = tester.state<WebLinkDelegateState>(
935+
find.byType(WebLinkDelegate),
936+
);
937+
final String semanticIdentifier = linkState.semanticIdentifier;
938+
939+
final html.Element semanticsHost = html.document.createElement('flt-semantics-host');
940+
html.document.body!.append(semanticsHost);
941+
final html.Element semanticsAnchor = html.document.createElement('a')
942+
..setAttribute('id', 'flt-semantic-node-99')
943+
..setAttribute('semantic-identifier', semanticIdentifier)
944+
..setAttribute('href', '/foobar');
945+
semanticsHost.append(semanticsAnchor);
946+
final html.Element semanticsContainer = html.document.createElement('flt-semantics-container');
947+
semanticsAnchor.append(semanticsContainer);
948+
final html.Element semanticsButton = html.document.createElement('flt-semantics')
949+
..setAttribute('role', 'button')
950+
..textContent = 'My Button Link';
951+
semanticsContainer.append(semanticsButton);
952+
953+
expect(pushedRouteNames, isEmpty);
954+
expect(testPlugin.launches, isEmpty);
955+
956+
await followLinkCallback!();
957+
// Click on the button (child of the anchor).
958+
final html.Event event1 = _simulateClick(semanticsButton);
959+
960+
expect(pushedRouteNames, <String>['/foobar']);
961+
expect(testPlugin.launches, isEmpty);
962+
expect(event1.defaultPrevented, isTrue);
963+
pushedRouteNames.clear();
964+
965+
await followLinkCallback!();
966+
// Click on the anchor itself.
967+
final html.Event event2 = _simulateClick(semanticsAnchor);
968+
969+
expect(pushedRouteNames, <String>['/foobar']);
970+
expect(testPlugin.launches, isEmpty);
971+
expect(event2.defaultPrevented, isTrue);
972+
973+
// Needed when testing on on Chrome98 headless in CI.
974+
// See https://github.com/flutter/flutter/issues/121161
975+
await tester.pumpAndSettle();
976+
});
977+
978+
testWidgets('handles clicks on semantic link with text',
979+
(WidgetTester tester) async {
980+
final Uri uri = Uri.parse('/foobar');
981+
FollowLink? followLinkCallback;
982+
983+
await tester.pumpWidget(MaterialApp(
984+
routes: <String, WidgetBuilder>{
985+
'/foobar': (BuildContext context) => const Text('Internal route'),
986+
},
987+
home: WebLinkDelegate(TestLinkInfo(
988+
uri: uri,
989+
target: LinkTarget.blank,
990+
builder: (BuildContext context, FollowLink? followLink) {
991+
followLinkCallback = followLink;
992+
return GestureDetector(
993+
onTap: () {},
994+
child: const Text('My Link'),
995+
);
996+
},
997+
)),
998+
));
999+
// Platform view creation happens asynchronously.
1000+
await tester.pumpAndSettle();
1001+
1002+
final WebLinkDelegateState linkState = tester.state<WebLinkDelegateState>(
1003+
find.byType(WebLinkDelegate),
1004+
);
1005+
final String semanticIdentifier = linkState.semanticIdentifier;
1006+
1007+
final html.Element semanticsHost = html.document.createElement('flt-semantics-host');
1008+
html.document.body!.append(semanticsHost);
1009+
final html.Element semanticsAnchor = html.document.createElement('a')
1010+
..setAttribute('id', 'flt-semantic-node-99')
1011+
..setAttribute('semantic-identifier', semanticIdentifier)
1012+
..setAttribute('href', '/foobar')
1013+
..textContent = 'My Text Link';
1014+
semanticsHost.append(semanticsAnchor);
1015+
1016+
expect(pushedRouteNames, isEmpty);
1017+
expect(testPlugin.launches, isEmpty);
1018+
1019+
await followLinkCallback!();
1020+
final html.Event event = _simulateClick(semanticsAnchor);
1021+
1022+
expect(pushedRouteNames, <String>['/foobar']);
1023+
expect(testPlugin.launches, isEmpty);
1024+
expect(event.defaultPrevented, isTrue);
1025+
1026+
// Needed when testing on on Chrome98 headless in CI.
1027+
// See https://github.com/flutter/flutter/issues/121161
1028+
await tester.pumpAndSettle();
1029+
});
1030+
1031+
// TODO(mdebbar): Remove this test after the engine PR [1] makes it to stable.
1032+
// [1] https://github.com/flutter/engine/pull/52720
1033+
testWidgets('handles clicks on (old) semantic link with a button',
1034+
(WidgetTester tester) async {
1035+
final Uri uri = Uri.parse('/foobar');
1036+
FollowLink? followLinkCallback;
1037+
1038+
await tester.pumpWidget(MaterialApp(
1039+
routes: <String, WidgetBuilder>{
1040+
'/foobar': (BuildContext context) => const Text('Internal route'),
1041+
},
1042+
home: WebLinkDelegate(TestLinkInfo(
1043+
uri: uri,
1044+
target: LinkTarget.blank,
1045+
builder: (BuildContext context, FollowLink? followLink) {
1046+
followLinkCallback = followLink;
1047+
return const SizedBox(width: 100, height: 100);
1048+
},
1049+
)),
1050+
));
1051+
// Platform view creation happens asynchronously.
1052+
await tester.pumpAndSettle();
1053+
1054+
final html.Element semanticsHost = html.document.createElement('flt-semantics-host');
1055+
html.document.body!.append(semanticsHost);
1056+
final html.Element semanticsAnchor = html.document.createElement('a')
1057+
..setAttribute('id', 'flt-semantic-node-99')
1058+
..setAttribute('href', '#');
1059+
semanticsHost.append(semanticsAnchor);
1060+
final html.Element semanticsContainer = html.document.createElement('flt-semantics-container');
1061+
semanticsAnchor.append(semanticsContainer);
1062+
final html.Element semanticsButton = html.document.createElement('flt-semantics')
1063+
..setAttribute('role', 'button')
1064+
..textContent = 'My Button';
1065+
semanticsContainer.append(semanticsButton);
1066+
1067+
expect(pushedRouteNames, isEmpty);
1068+
expect(testPlugin.launches, isEmpty);
1069+
1070+
await followLinkCallback!();
1071+
final html.Event event1 = _simulateClick(semanticsButton);
1072+
1073+
// Before the changes land in the web engine, this will not trigger the
1074+
// link properly.
1075+
expect(pushedRouteNames, <String>[]);
1076+
expect(testPlugin.launches, isEmpty);
1077+
expect(event1.defaultPrevented, isFalse);
1078+
1079+
// Needed when testing on on Chrome98 headless in CI.
1080+
// See https://github.com/flutter/flutter/issues/121161
1081+
await tester.pumpAndSettle();
1082+
});
1083+
1084+
// TODO(mdebbar): Remove this test after the engine PR [1] makes it to stable.
1085+
// [1] https://github.com/flutter/engine/pull/52720
1086+
testWidgets('handles clicks on (old) semantic link with text',
1087+
(WidgetTester tester) async {
1088+
final Uri uri = Uri.parse('/foobar');
1089+
FollowLink? followLinkCallback;
1090+
1091+
await tester.pumpWidget(MaterialApp(
1092+
routes: <String, WidgetBuilder>{
1093+
'/foobar': (BuildContext context) => const Text('Internal route'),
1094+
},
1095+
home: WebLinkDelegate(TestLinkInfo(
1096+
uri: uri,
1097+
target: LinkTarget.blank,
1098+
builder: (BuildContext context, FollowLink? followLink) {
1099+
followLinkCallback = followLink;
1100+
return GestureDetector(
1101+
onTap: () {},
1102+
child: const Text('My Link'),
1103+
);
1104+
},
1105+
)),
1106+
));
1107+
// Platform view creation happens asynchronously.
1108+
await tester.pumpAndSettle();
1109+
1110+
final html.Element semanticsHost = html.document.createElement('flt-semantics-host');
1111+
html.document.body!.append(semanticsHost);
1112+
final html.Element semanticsAnchor = html.document.createElement('a')
1113+
..setAttribute('id', 'flt-semantic-node-99')
1114+
..setAttribute('href', '#')
1115+
..textContent = 'My Text Link';
1116+
semanticsHost.append(semanticsAnchor);
1117+
1118+
expect(pushedRouteNames, isEmpty);
1119+
expect(testPlugin.launches, isEmpty);
1120+
1121+
await followLinkCallback!();
1122+
final html.Event event = _simulateClick(semanticsAnchor);
1123+
1124+
// Before the changes land in the web engine, this will not trigger the
1125+
// link properly.
1126+
expect(pushedRouteNames, <String>[]);
1127+
expect(testPlugin.launches, isEmpty);
1128+
expect(event.defaultPrevented, isFalse);
1129+
1130+
// Needed when testing on on Chrome98 headless in CI.
1131+
// See https://github.com/flutter/flutter/issues/121161
1132+
await tester.pumpAndSettle();
1133+
});
1134+
});
8031135
}
8041136

8051137
List<html.Element> _findAllAnchors() {

packages/url_launcher/url_launcher_web/lib/src/link.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,14 @@ int _nextSemanticsIdentifier = 0;
7373
/// pushes the route name to the framework.
7474
class WebLinkDelegateState extends State<WebLinkDelegate> {
7575
late LinkViewController _controller;
76-
late final String _semanticIdentifier;
76+
77+
@visibleForTesting
78+
late final String semanticIdentifier;
7779

7880
@override
7981
void initState() {
8082
super.initState();
81-
_semanticIdentifier = 'sem-id-${_nextSemanticsIdentifier++}';
83+
semanticIdentifier = 'sem-id-${_nextSemanticsIdentifier++}';
8284
}
8385

8486
@override
@@ -104,7 +106,7 @@ class WebLinkDelegateState extends State<WebLinkDelegate> {
104106
children: <Widget>[
105107
Semantics(
106108
link: true,
107-
identifier: _semanticIdentifier,
109+
identifier: semanticIdentifier,
108110
value: widget.link.uri?.getHref(),
109111
child: widget.link.builder(
110112
context,
@@ -117,7 +119,7 @@ class WebLinkDelegateState extends State<WebLinkDelegate> {
117119
child: PlatformViewLink(
118120
viewType: linkViewType,
119121
onCreatePlatformView: (PlatformViewCreationParams params) {
120-
_controller = LinkViewController.fromParams(params, _semanticIdentifier);
122+
_controller = LinkViewController.fromParams(params, semanticIdentifier);
121123
return _controller
122124
..setUri(widget.link.uri)
123125
..setTarget(widget.link.target);

0 commit comments

Comments
 (0)