@@ -800,6 +800,338 @@ void main() {
800
800
await tester.pumpAndSettle ();
801
801
});
802
802
});
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
+ });
803
1135
}
804
1136
805
1137
List <html.Element > _findAllAnchors () {
0 commit comments