From a3af43b713f3e622bed89b5e1ae8eb4b31179718 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 10 Jul 2023 20:56:28 -0700 Subject: [PATCH 1/3] test [nfc]: Add shorter name "testBinding" for TestZulipBinding.instance We use this a lot in our tests, and its name is kind of uncomfortably long for that. Make it shorter. In particular if in a test one wants the test binding object for Flutter itself (the TestWidgetsFlutterBinding), that's spelled `tester.binding` (where `tester` is the argument passed to the `testWidgets` callback). So this makes our own test binding object be comparably accessible, as `testBinding`. --- test/model/binding.dart | 9 ++++-- test/widgets/action_sheet_test.dart | 20 ++++++------- test/widgets/autocomplete_test.dart | 8 ++--- test/widgets/clipboard_test.dart | 8 ++--- test/widgets/content_test.dart | 30 +++++++++---------- test/widgets/message_list_test.dart | 10 +++---- .../widgets/recent_dm_conversations_test.dart | 6 ++-- test/widgets/store_test.dart | 20 ++++++------- 8 files changed, 58 insertions(+), 53 deletions(-) diff --git a/test/model/binding.dart b/test/model/binding.dart index 2fa197b302..49bd7c672b 100644 --- a/test/model/binding.dart +++ b/test/model/binding.dart @@ -5,6 +5,11 @@ import 'package:zulip/model/store.dart'; import 'test_store.dart'; +/// The binding instance used in tests. +/// +/// This is the Zulip-specific analogue of [WidgetTester.binding]. +TestZulipBinding get testBinding => TestZulipBinding.instance; + /// A concrete binding for use in the `flutter test` environment. /// /// Tests that will mount a [GlobalStoreWidget], or invoke a Flutter plugin, @@ -48,7 +53,7 @@ class TestZulipBinding extends ZulipBinding { /// Tests that mount a [GlobalStoreWidget], or invoke a Flutter plugin, /// or access [globalStore] or other methods on this class, /// should clean up by calling this method. Typically this is done using - /// [addTearDown], like `addTearDown(TestZulipBinding.instance.reset);`. + /// [addTearDown], like `addTearDown(testBinding.reset);`. void reset() { _globalStore?.dispose(); _globalStore = null; @@ -90,7 +95,7 @@ class TestZulipBinding extends ZulipBinding { ), ErrorHint( 'Typically this is accomplished using [addTearDown], like ' - '`addTearDown(TestZulipBinding.instance.reset);`.', + '`addTearDown(testBinding.reset);`.', ), ]); } diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index 40b41f46d7..0d143ae5e3 100644 --- a/test/widgets/action_sheet_test.dart +++ b/test/widgets/action_sheet_test.dart @@ -26,10 +26,10 @@ Future setupToMessageActionSheet(WidgetTester tester, { required Message message, required Narrow narrow, }) async { - addTearDown(TestZulipBinding.instance.reset); + addTearDown(testBinding.reset); - await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); store.addUser(eg.user(userId: message.senderId)); if (message is StreamMessage) { store.addStream(eg.stream(streamId: message.streamId)); @@ -136,7 +136,7 @@ void main() { testWidgets('in stream narrow', (WidgetTester tester) async { final message = eg.streamMessage(); await setupToMessageActionSheet(tester, message: message, narrow: StreamNarrow(message.streamId)); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); final composeBoxController = findComposeBoxController(tester)!; final contentController = composeBoxController.contentController; @@ -154,7 +154,7 @@ void main() { testWidgets('in topic narrow', (WidgetTester tester) async { final message = eg.streamMessage(); await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message)); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); final composeBoxController = findComposeBoxController(tester)!; final contentController = composeBoxController.contentController; @@ -173,7 +173,7 @@ void main() { final message = eg.dmMessage(from: eg.selfUser, to: [eg.otherUser]); await setupToMessageActionSheet(tester, message: message, narrow: DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); final composeBoxController = findComposeBoxController(tester)!; final contentController = composeBoxController.contentController; @@ -191,7 +191,7 @@ void main() { testWidgets('request has an error', (WidgetTester tester) async { final message = eg.streamMessage(); await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message)); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); final composeBoxController = findComposeBoxController(tester)!; final contentController = composeBoxController.contentController; @@ -235,7 +235,7 @@ void main() { }); tearDown(() async { - TestZulipBinding.instance.reset(); + testBinding.reset(); }); Future tapCopyButton(WidgetTester tester) async { @@ -247,7 +247,7 @@ void main() { testWidgets('success', (WidgetTester tester) async { final message = eg.streamMessage(); await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message)); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); prepareRawContentResponseSuccess(store, message: message, rawContent: 'Hello world'); await tapCopyButton(tester); @@ -258,7 +258,7 @@ void main() { testWidgets('request has an error', (WidgetTester tester) async { final message = eg.streamMessage(); await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message)); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); prepareRawContentResponseError(store); await tapCopyButton(tester); diff --git a/test/widgets/autocomplete_test.dart b/test/widgets/autocomplete_test.dart index 311a56ba91..164cd76515 100644 --- a/test/widgets/autocomplete_test.dart +++ b/test/widgets/autocomplete_test.dart @@ -20,9 +20,9 @@ import '../model/test_store.dart'; Future setupToComposeInput(WidgetTester tester, { required List users, }) async { - addTearDown(TestZulipBinding.instance.reset); - await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); store.addUsers([eg.selfUser, eg.otherUser]); store.addUsers(users); final connection = store.connection as FakeApiConnection; @@ -67,7 +67,7 @@ void main() { final user2 = eg.user(userId: 2, fullName: 'User Two'); final user3 = eg.user(userId: 3, fullName: 'User Three'); final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); // Options are filtered correctly for query // TODO(#226): Remove this extra edit when this bug is fixed. diff --git a/test/widgets/clipboard_test.dart b/test/widgets/clipboard_test.dart index a847f08aee..ace8c4d831 100644 --- a/test/widgets/clipboard_test.dart +++ b/test/widgets/clipboard_test.dart @@ -21,7 +21,7 @@ void main() { }); tearDown(() async { - TestZulipBinding.instance.reset(); + testBinding.reset(); }); group('copyWithPopup', () { @@ -59,21 +59,21 @@ void main() { } testWidgets('iOS', (WidgetTester tester) async { - TestZulipBinding.instance.deviceInfoResult = IosDeviceInfo(systemVersion: '16.0'); + testBinding.deviceInfoResult = IosDeviceInfo(systemVersion: '16.0'); await call(tester, text: 'asdf'); await checkClipboardText('asdf'); await checkSnackBar(tester, expected: true); }); testWidgets('Android', (WidgetTester tester) async { - TestZulipBinding.instance.deviceInfoResult = AndroidDeviceInfo(sdkInt: 33); + testBinding.deviceInfoResult = AndroidDeviceInfo(sdkInt: 33); await call(tester, text: 'asdf'); await checkClipboardText('asdf'); await checkSnackBar(tester, expected: false); }); testWidgets('Android <13', (WidgetTester tester) async { - TestZulipBinding.instance.deviceInfoResult = AndroidDeviceInfo(sdkInt: 32); + testBinding.deviceInfoResult = AndroidDeviceInfo(sdkInt: 32); await call(tester, text: 'asdf'); await checkClipboardText('asdf'); await checkSnackBar(tester, expected: true); diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index 03c407ea4a..41f7c16fe1 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -26,8 +26,8 @@ void main() { const fontSize = 48.0; Future prepareContent(WidgetTester tester, String html) async { - final globalStore = TestZulipBinding.instance.globalStore; - addTearDown(TestZulipBinding.instance.reset); + final globalStore = testBinding.globalStore; + addTearDown(testBinding.reset); await globalStore.add(eg.selfAccount, eg.initialSnapshot()); await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp( @@ -45,7 +45,7 @@ void main() { await tester.tap(find.text('hello')); final expectedMode = defaultTargetPlatform == TargetPlatform.android ? LaunchMode.externalApplication : LaunchMode.platformDefault; - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('https://example/'), mode: expectedMode)); }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); @@ -56,14 +56,14 @@ void main() { .translate(fontSize/2, fontSize/2); // middle of first letter await tester.tapAt(base.translate(5*fontSize, 0)); // "foo bXr baz" - check(TestZulipBinding.instance.takeLaunchUrlCalls()).isEmpty(); + check(testBinding.takeLaunchUrlCalls()).isEmpty(); await tester.tapAt(base.translate(1*fontSize, 0)); // "fXo bar baz" - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('https://a/'), mode: expectedModeAndroid)); await tester.tapAt(base.translate(9*fontSize, 0)); // "foo bar bXz" - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('https://b/'), mode: expectedModeAndroid)); }); @@ -71,7 +71,7 @@ void main() { await prepareContent(tester, '

word

'); await tester.tap(find.text('word')); - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('https://a/'), mode: expectedModeAndroid)); }); @@ -82,11 +82,11 @@ void main() { .translate(fontSize/2, fontSize/2); // middle of first letter await tester.tapAt(base.translate(1*fontSize, 0)); // "tXo words" - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('https://a/'), mode: expectedModeAndroid)); await tester.tapAt(base.translate(6*fontSize, 0)); // "two woXds" - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('https://a/'), mode: expectedModeAndroid)); }); @@ -94,7 +94,7 @@ void main() { await prepareContent(tester, '

word

'); await tester.tap(find.text('word')); - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('${eg.realmUrl}a/b?c#d'), mode: expectedModeAndroid)); }); @@ -102,17 +102,17 @@ void main() { await prepareContent(tester, '
word
'); await tester.tap(find.text('word')); - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('https://a/'), mode: expectedModeAndroid)); }); testWidgets('error dialog if invalid link', (tester) async { await prepareContent(tester, '

word

'); - TestZulipBinding.instance.launchUrlResult = false; + testBinding.launchUrlResult = false; await tester.tap(find.text('word')); await tester.pump(); - check(TestZulipBinding.instance.takeLaunchUrlCalls()) + check(testBinding.takeLaunchUrlCalls()) .single.equals((url: Uri.parse('file:///etc/bad'), mode: expectedModeAndroid)); checkErrorDialog(tester, expectedTitle: 'Unable to open link'); }); @@ -122,8 +122,8 @@ void main() { final authHeaders = authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey); Future actualAuthHeader(WidgetTester tester, Uri src) async { - final globalStore = TestZulipBinding.instance.globalStore; - addTearDown(TestZulipBinding.instance.reset); + final globalStore = testBinding.globalStore; + addTearDown(testBinding.reset); await globalStore.add(eg.selfAccount, eg.initialSnapshot()); final httpClient = FakeImageHttpClient(); diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 57241407f3..c39f33aad9 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -32,13 +32,13 @@ void main() { int? messageCount, List? messages, }) async { - addTearDown(TestZulipBinding.instance.reset); + addTearDown(testBinding.reset); addTearDown(tester.view.resetPhysicalSize); tester.view.physicalSize = const Size(600, 800); - await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + store = await testBinding.globalStore.perAccount(eg.selfAccount.id); connection = store.connection as FakeApiConnection; // prepare message list data @@ -181,7 +181,7 @@ void main() { group('MessageWithSender', () { testWidgets('Updates avatar on RealmUserUpdateEvent', (tester) async { - addTearDown(TestZulipBinding.instance.reset); + addTearDown(testBinding.reset); // TODO recognize avatar more reliably: // https://github.com/zulip/zulip-flutter/pull/246#discussion_r1282516308 @@ -202,7 +202,7 @@ void main() { } Future handleNewAvatarEventAndPump(WidgetTester tester, String avatarUrl) async { - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); store.handleEvent(RealmUserUpdateEvent(id: 1, userId: eg.selfUser.userId, avatarUrl: avatarUrl)); await tester.pump(); } diff --git a/test/widgets/recent_dm_conversations_test.dart b/test/widgets/recent_dm_conversations_test.dart index b2b0340961..aa42cb824b 100644 --- a/test/widgets/recent_dm_conversations_test.dart +++ b/test/widgets/recent_dm_conversations_test.dart @@ -26,10 +26,10 @@ Future setupPage(WidgetTester tester, { NavigatorObserver? navigatorObserver, String? newNameForSelfUser, }) async { - addTearDown(TestZulipBinding.instance.reset); + addTearDown(testBinding.reset); - await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); store.addUser(eg.selfUser); for (final user in users) { diff --git a/test/widgets/store_test.dart b/test/widgets/store_test.dart index 661f80b0ff..abce18474c 100644 --- a/test/widgets/store_test.dart +++ b/test/widgets/store_test.dart @@ -49,7 +49,7 @@ void main() { TestZulipBinding.ensureInitialized(); testWidgets('GlobalStoreWidget', (WidgetTester tester) async { - addTearDown(TestZulipBinding.instance.reset); + addTearDown(testBinding.reset); GlobalStore? globalStore; await tester.pumpWidget( @@ -66,17 +66,17 @@ void main() { await tester.pump(); // Then after loading, mounts child instead, with provided store. check(tester.any(find.byType(CircularProgressIndicator))).isFalse(); - check(globalStore).identicalTo(TestZulipBinding.instance.globalStore); + check(globalStore).identicalTo(testBinding.globalStore); - await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); check(globalStore).isNotNull() .accountEntries.single .equals((accountId: eg.selfAccount.id, account: eg.selfAccount)); }); testWidgets('PerAccountStoreWidget basic', (tester) async { - final globalStore = TestZulipBinding.instance.globalStore; - addTearDown(TestZulipBinding.instance.reset); + final globalStore = testBinding.globalStore; + addTearDown(testBinding.reset); await globalStore.add(eg.selfAccount, eg.initialSnapshot()); await tester.pumpWidget( @@ -97,8 +97,8 @@ void main() { }); testWidgets('PerAccountStoreWidget immediate data after first loaded', (tester) async { - final globalStore = TestZulipBinding.instance.globalStore; - addTearDown(TestZulipBinding.instance.reset); + final globalStore = testBinding.globalStore; + addTearDown(testBinding.reset); await globalStore.add(eg.selfAccount, eg.initialSnapshot()); await tester.pumpWidget( @@ -155,7 +155,7 @@ void main() { final widgetWithMixinKey = GlobalKey(); final accountId = eg.selfAccount.id; - await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); Future pumpWithParams({required bool light, required int accountId}) async { await tester.pumpWidget( @@ -190,7 +190,7 @@ void main() { // production code, where we could reasonably add an assert against it. // If forced, we could let this test code proceed despite such an assert…) // hack; the snapshot probably corresponds to selfAccount, not otherAccount. - await TestZulipBinding.instance.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); await pumpWithParams(light: false, accountId: eg.otherAccount.id); // Nudge PerAccountStoreWidget to send its updated store to MyWidgetWithMixin. // @@ -205,7 +205,7 @@ void main() { // (as it will when widget.accountId has changed), and if so, // it will notify dependent widgets. (See its state's didChangeDependencies.) // So, take advantage of that. - TestZulipBinding.instance.globalStore.notifyListeners(); + testBinding.globalStore.notifyListeners(); await tester.pumpAndSettle(); check(widgetWithMixinKey).currentState.isNotNull() ..anyDepChangeCounter.equals(3) From 31fdc1f1f011e7ac58272065090083ed3cb3880c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 10 Jul 2023 21:02:54 -0700 Subject: [PATCH 2/3] test [nfc]: Tighten some testBinding uses now that name is shorter --- test/widgets/content_test.dart | 6 ++---- test/widgets/store_test.dart | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index 41f7c16fe1..5e9c401cd5 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -26,9 +26,8 @@ void main() { const fontSize = 48.0; Future prepareContent(WidgetTester tester, String html) async { - final globalStore = testBinding.globalStore; + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); addTearDown(testBinding.reset); - await globalStore.add(eg.selfAccount, eg.initialSnapshot()); await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp( home: PerAccountStoreWidget(accountId: eg.selfAccount.id, @@ -122,9 +121,8 @@ void main() { final authHeaders = authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey); Future actualAuthHeader(WidgetTester tester, Uri src) async { - final globalStore = testBinding.globalStore; + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); addTearDown(testBinding.reset); - await globalStore.add(eg.selfAccount, eg.initialSnapshot()); final httpClient = FakeImageHttpClient(); debugNetworkImageHttpClientProvider = () => httpClient; diff --git a/test/widgets/store_test.dart b/test/widgets/store_test.dart index abce18474c..71da83403a 100644 --- a/test/widgets/store_test.dart +++ b/test/widgets/store_test.dart @@ -75,9 +75,8 @@ void main() { }); testWidgets('PerAccountStoreWidget basic', (tester) async { - final globalStore = testBinding.globalStore; + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); addTearDown(testBinding.reset); - await globalStore.add(eg.selfAccount, eg.initialSnapshot()); await tester.pumpWidget( Directionality( @@ -97,9 +96,8 @@ void main() { }); testWidgets('PerAccountStoreWidget immediate data after first loaded', (tester) async { - final globalStore = testBinding.globalStore; + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); addTearDown(testBinding.reset); - await globalStore.add(eg.selfAccount, eg.initialSnapshot()); await tester.pumpWidget( Directionality( From 6922fbb0c139853f51388809d9d7bd9c274eca3b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 10 Jul 2023 21:28:03 -0700 Subject: [PATCH 3/3] binding [nfc]: Clean up name of re-exported url_launcher.LaunchMode In particular make the name appropriately specific about what's being launched, since we're exporting it from this more general context. --- lib/model/binding.dart | 4 +++- lib/widgets/content.dart | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/model/binding.dart b/lib/model/binding.dart index 186f174a1a..adf19f9e90 100644 --- a/lib/model/binding.dart +++ b/lib/model/binding.dart @@ -1,11 +1,13 @@ import 'package:device_info_plus/device_info_plus.dart' as device_info_plus; import 'package:flutter/foundation.dart'; import 'package:url_launcher/url_launcher.dart' as url_launcher; -export 'package:url_launcher/url_launcher.dart' show LaunchMode; import '../widgets/store.dart'; import 'store.dart'; +/// Alias for [url_launcher.LaunchMode]. +typedef UrlLaunchMode = url_launcher.LaunchMode; + /// A singleton service providing the app's data and use of Flutter plugins. /// /// Only one instance will be constructed in the lifetime of the app, diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index f81da51d36..602f9e801c 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -695,8 +695,8 @@ void _launchUrl(BuildContext context, String urlString) async { // opening a URL in a browser, where the system gives the user // a bit of UI to choose which browser to use: // https://github.com/zulip/zulip-flutter/issues/74#issuecomment-1514040730 - TargetPlatform.android => LaunchMode.externalApplication, - _ => LaunchMode.platformDefault, + TargetPlatform.android => UrlLaunchMode.externalApplication, + _ => UrlLaunchMode.platformDefault, }, ); } on PlatformException catch (e) {