Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d3e16ea
[url_launcher][web] Better support for semantics in the Link widget
mdebbar May 9, 2024
cce04c4
tests
mdebbar May 20, 2024
b3d16c7
click on mismatched link
mdebbar May 22, 2024
f684065
more tests
mdebbar May 22, 2024
c1556cc
semantics tests
mdebbar May 31, 2024
d98eab2
apply the target attribute on semantic links
mdebbar May 31, 2024
66df2e2
always use plural 'semantics'
mdebbar Jun 3, 2024
c359fd4
changes to semanticsIdentifier
mdebbar Jun 3, 2024
7fd5b7b
more attributes on <a>
mdebbar Jun 4, 2024
80e7158
more readable
mdebbar Jun 4, 2024
9d02101
correct attribute name 'flt-semantics-identifier'
mdebbar Aug 13, 2024
3b33642
Merge branch 'main' into new_link_semantics
mdebbar Aug 13, 2024
5334922
use the new linkUrl semantics property
mdebbar Aug 13, 2024
9955360
triggerIfReady
mdebbar Aug 13, 2024
af67dfc
docs
mdebbar Aug 13, 2024
393776a
Merge branch 'main' into link_semantics
mdebbar Nov 22, 2024
969e697
Merge branch 'main' into link_semantics
mdebbar Dec 17, 2024
2c37827
Merge branch 'main' into link_semantics
mdebbar Dec 19, 2024
aa0c3d7
bump minimum sdk version
mdebbar Dec 23, 2024
ba6c60f
Merge branch 'main' into link_semantics
mdebbar Dec 27, 2024
f296c49
dart format
mdebbar Dec 27, 2024
46ca0e7
Merge branch 'main' into link_semantics
ditman Jan 8, 2025
114731d
Help test pass in wasm
ditman Jan 8, 2025
4cd3f68
dart format .
ditman Jan 8, 2025
c203542
Tag as v2.4.0, update CHANGELOG.
ditman Jan 9, 2025
c12a920
Update sdk lower bound in example.
ditman Jan 9, 2025
4e703d5
Address PR comments.
ditman Jan 10, 2025
286d33d
Rename signal handlers to onSignalName
ditman Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
more tests
  • Loading branch information
mdebbar committed May 31, 2024
commit f6840659f3d5cba0b622238d348be7e7d0c30fe5
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ void main() {

testWidgets('click to navigate to external link',
(WidgetTester tester) async {
final Uri uri = Uri.parse('https://google.com');
final Uri uri = Uri.parse('https://flutter.dev');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
Expand Down Expand Up @@ -327,7 +327,7 @@ void main() {

testWidgets('keydown to navigate to external link',
(WidgetTester tester) async {
final Uri uri = Uri.parse('https://google.com');
final Uri uri = Uri.parse('https://flutter.dev');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
Expand All @@ -354,7 +354,7 @@ void main() {
// External links that are triggered by keyboard are handled by calling
// `launchUrl`, and there's no change to the app's route name.
expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, <String>['https://google.com']);
expect(testPlugin.launches, <String>['https://flutter.dev']);
expect(event.defaultPrevented, isFalse);

// Needed when testing on on Chrome98 headless in CI.
Expand Down Expand Up @@ -438,6 +438,197 @@ void main() {
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});

testWidgets('trigger signals are reset after a delay',
(WidgetTester tester) async {
final Uri uri = Uri.parse('/foobar');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
routes: <String, WidgetBuilder>{
'/foobar': (BuildContext context) => const Text('Internal route'),
},
home: WebLinkDelegate(TestLinkInfo(
uri: uri,
target: LinkTarget.blank,
builder: (BuildContext context, FollowLink? followLink) {
followLinkCallback = followLink;
return const SizedBox(width: 100, height: 100);
},
)),
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();

expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);

final html.Element anchor = _findSingleAnchor();

// A large delay between signals should reset the previous signal.
await followLinkCallback!();
await Future<void>.delayed(const Duration(seconds: 1));
final html.Event event1 = _simulateClick(anchor);

// The link shouldn't have been triggered.
expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);
expect(event1.defaultPrevented, isFalse);

await Future<void>.delayed(const Duration(seconds: 1));

// Signals with large delay (in reverse order).
final html.Event event2 = _simulateClick(anchor);
await Future<void>.delayed(const Duration(seconds: 1));
await followLinkCallback!();

// The link shouldn't have been triggered.
expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);
expect(event2.defaultPrevented, isFalse);

await Future<void>.delayed(const Duration(seconds: 1));

// A small delay is okay.
await followLinkCallback!();
await Future<void>.delayed(const Duration(milliseconds: 100));
final html.Event event3 = _simulateClick(anchor);

// The link should've been triggered now.
expect(pushedRouteNames, <String>['/foobar']);
expect(testPlugin.launches, isEmpty);
expect(event3.defaultPrevented, isTrue);

// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});

testWidgets('ignores clicks on non-Flutter link',
(WidgetTester tester) async {
final Uri uri = Uri.parse('/foobar');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
routes: <String, WidgetBuilder>{
'/foobar': (BuildContext context) => const Text('Internal route'),
},
home: WebLinkDelegate(TestLinkInfo(
uri: uri,
target: LinkTarget.blank,
builder: (BuildContext context, FollowLink? followLink) {
followLinkCallback = followLink;
return const SizedBox(width: 100, height: 100);
},
)),
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();

expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);

final html.Element nonFlutterAnchor = html.document.createElement('a');
nonFlutterAnchor.setAttribute('href', '/non-flutter');

await followLinkCallback!();
final html.Event event = _simulateClick(nonFlutterAnchor);

// The link shouldn't have been triggered.
expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);
expect(event.defaultPrevented, isFalse);

// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});

testWidgets('handles cmd+click correctly', (WidgetTester tester) async {
final Uri uri = Uri.parse('/foobar');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
routes: <String, WidgetBuilder>{
'/foobar': (BuildContext context) => const Text('Internal route'),
},
home: WebLinkDelegate(TestLinkInfo(
uri: uri,
target: LinkTarget.blank,
builder: (BuildContext context, FollowLink? followLink) {
followLinkCallback = followLink;
return const SizedBox(width: 100, height: 100);
},
)),
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();

expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);

final html.Element anchor = _findSingleAnchor();

await followLinkCallback!();
final html.Event event = _simulateClick(anchor, metaKey: true);

// When a modifier key is present, we should let the browser handle the
// navigation. That means we do nothing on our side.
expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);
expect(event.defaultPrevented, isFalse);

// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});

testWidgets('ignores keydown when it is a modifier key',
(WidgetTester tester) async {
final Uri uri = Uri.parse('/foobar');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
routes: <String, WidgetBuilder>{
'/foobar': (BuildContext context) => const Text('Internal route'),
},
home: WebLinkDelegate(TestLinkInfo(
uri: uri,
target: LinkTarget.blank,
builder: (BuildContext context, FollowLink? followLink) {
followLinkCallback = followLink;
return const SizedBox(width: 100, height: 100);
},
)),
));
// Platform view creation happens asynchronously.
await tester.pumpAndSettle();

expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);

final html.Element anchor = _findSingleAnchor();

final html.KeyboardEvent event1 = _simulateKeydown(anchor, metaKey: true);
await followLinkCallback!();

// When the pressed key is a modifier key, we should ignore it.
expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, isEmpty);
expect(event1.defaultPrevented, isFalse);

// If later we receive another trigger, it should work.
final html.KeyboardEvent event2 = _simulateKeydown(anchor);

// Now the link should be triggered.
expect(pushedRouteNames, <String>['/foobar']);
expect(testPlugin.launches, isEmpty);
expect(event2.defaultPrevented, isFalse);

// Needed when testing on on Chrome98 headless in CI.
// See https://github.com/flutter/flutter/issues/121161
await tester.pumpAndSettle();
});
});

group('Follows links (reversed order)', () {
Expand Down Expand Up @@ -536,7 +727,7 @@ void main() {

testWidgets('click to navigate to external link',
(WidgetTester tester) async {
final Uri uri = Uri.parse('https://google.com');
final Uri uri = Uri.parse('https://flutter.dev');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
Expand Down Expand Up @@ -574,7 +765,7 @@ void main() {

testWidgets('keydown to navigate to external link',
(WidgetTester tester) async {
final Uri uri = Uri.parse('https://google.com');
final Uri uri = Uri.parse('https://flutter.dev');
FollowLink? followLinkCallback;

await tester.pumpWidget(MaterialApp(
Expand All @@ -601,7 +792,7 @@ void main() {
// External links that are triggered by keyboard are handled by calling
// `launchUrl`, and there's no change to the app's route name.
expect(pushedRouteNames, isEmpty);
expect(testPlugin.launches, <String>['https://google.com']);
expect(testPlugin.launches, <String>['https://flutter.dev']);
expect(event.defaultPrevented, isFalse);

// Needed when testing on on Chrome98 headless in CI.
Expand Down Expand Up @@ -641,23 +832,25 @@ html.Element _findSingleAnchor() {
return _findAllAnchors().single;
}

html.MouseEvent _simulateClick(html.Element target) {
html.MouseEvent _simulateClick(html.Element target, {bool? metaKey}) {
final html.MouseEvent mouseEvent = html.MouseEvent(
'click',
html.MouseEventInit()
..bubbles = true
..cancelable = true,
..cancelable = true
..metaKey = metaKey ?? false,
);
LinkViewController.handleGlobalClick(event: mouseEvent, target: target);
return mouseEvent;
}

html.KeyboardEvent _simulateKeydown(html.Element target) {
html.KeyboardEvent _simulateKeydown(html.Element target, {bool? metaKey}) {
final html.KeyboardEvent keydownEvent = html.KeyboardEvent(
'keydown',
html.KeyboardEventInit()
..bubbles = true
..cancelable = true,
..cancelable = true
..metaKey = metaKey ?? false,
);
LinkViewController.handleGlobalKeydown(event: keydownEvent);
return keydownEvent;
Expand Down