From 4d405170badf19ad366af72c8de91abd047eccfd Mon Sep 17 00:00:00 2001 From: ashuntu Date: Wed, 4 Dec 2024 16:50:10 -0600 Subject: [PATCH 01/10] Add Landscape skip page This is quite a large commit since there are a lot of moving pieces. The existing Landscape page needs its skip functionality removed, the skip page uses an updated version of copy, the skip page uses an entirely new layout, etc. I plan on expanding the use of reusable navigation and other layout widgets since the new designs will be much more consistent across pages. --- gui/packages/ubuntupro/lib/app.dart | 3 + gui/packages/ubuntupro/lib/l10n/app_en.arb | 7 +- .../lib/pages/landscape/landscape_model.dart | 2 +- .../lib/pages/landscape/landscape_page.dart | 26 +----- .../landscape_skip/landscape_skip_page.dart | 76 ++++++++++++++++ .../lib/pages/widgets/navigation_row.dart | 27 ++++++ .../lib/pages/widgets/page_widgets.dart | 89 +++++++++++++++++++ .../lib/pages/widgets/radio_tile.dart | 37 ++++++++ gui/packages/ubuntupro/lib/routes.dart | 1 + .../pages/landscape/landscape_page_test.dart | 1 - 10 files changed, 243 insertions(+), 26 deletions(-) create mode 100644 gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart create mode 100644 gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart create mode 100644 gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart diff --git a/gui/packages/ubuntupro/lib/app.dart b/gui/packages/ubuntupro/lib/app.dart index adaf848a2..749656acf 100644 --- a/gui/packages/ubuntupro/lib/app.dart +++ b/gui/packages/ubuntupro/lib/app.dart @@ -11,6 +11,7 @@ import 'core/agent_connection.dart'; import 'core/agent_monitor.dart'; import 'core/settings.dart'; import 'pages/landscape/landscape_page.dart'; +import 'pages/landscape_skip/landscape_skip_page.dart'; import 'pages/startup/startup_page.dart'; import 'pages/subscribe_now/subscribe_now_page.dart'; import 'pages/subscription_status/subscription_status_page.dart'; @@ -76,6 +77,8 @@ class Pro4WSLApp extends StatelessWidget { }, ), if (settings.isLandscapeConfigurationEnabled) ...{ + Routes.skipLandscape: + WizardRoute(builder: (_) => const LandscapeSkipPage()), Routes.configureLandscape: const WizardRoute(builder: LandscapePage.create), Routes.subscriptionStatus: WizardRoute( diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index 1cbdf00a9..af2b8692b 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -57,7 +57,12 @@ "purchaseStatusServer":"Something went wrong with Microsoft Store. Please try again later.", "purchaseStatusUnknown": "Unknown error when trying to purchase the subscription. Consider restarting this app.", - "landscapeHeading": "Configure the connection to {landscapeLink} to manage your Ubuntu WSL instances remotely.", + "landscapeSkip": "Skip for now", + "landscapeSkipDescription": "You can always configure Landscape later", + "landscapeSkipRegister": "Register with Landscape", + + "landscapeTitle": "Landscape", + "landscapeHeading": "Configure the connection to Landscape to manage your Ubuntu WSL instances remotely. {landscapeLink}", "@landscapeHeading": { "placeholders": { "landscapeLink": { diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart index a3f6426d5..0e3cc6145 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart @@ -19,7 +19,7 @@ class LandscapeModel extends ChangeNotifier { LandscapeModel(this.client); /// The URL to be shown in the UI. - final landscapeURI = Uri.https('ubuntu.com', '/landscape'); + static final landscapeURI = Uri.https('ubuntu.com', '/landscape'); /// Whether the current form is complete (ready to be submitted). bool get isComplete => _active.isComplete; diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart index c2df76073..84bf06b30 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart @@ -25,7 +25,6 @@ class LandscapePage extends StatelessWidget { super.key, required this.onApplyConfig, required this.onBack, - required this.onSkip, }); /// Callable invoked when this page successfully applies the configuration. @@ -34,9 +33,6 @@ class LandscapePage extends StatelessWidget { /// Callable invoked when the user navigates back. final void Function() onBack; - /// Callable invoked when the user skips this page. - final void Function() onSkip; - @override Widget build(BuildContext context) { final lang = AppLocalizations.of(context); @@ -49,21 +45,17 @@ class LandscapePage extends StatelessWidget { ), ), ), - ).copyWith( - a: const TextStyle( - decoration: TextDecoration.underline, - ), ); return LandingPage( svgAsset: 'assets/Landscape-tag.svg', - title: 'Landscape', + title: lang.landscapeTitle, children: [ // Only rebuilds if the value of model.landscapeURI changes (never in production) Selector( - selector: (_, model) => model.landscapeURI, + selector: (_, model) => LandscapeModel.landscapeURI, builder: (context, uri, _) => MarkdownBody( - data: lang.landscapeHeading('[Landscape]($uri)'), + data: lang.landscapeHeading('[${lang.learnMore}]($uri)'), onTapLink: (_, href, __) => launchUrl(uri), styleSheet: linkStyle, ), @@ -79,7 +71,6 @@ class LandscapePage extends StatelessWidget { selector: (_, model) => model.isComplete, builder: (context, isComplete, _) => _NavigationButtonRow( onBack: onBack, - onSkip: onSkip, onNext: isComplete ? () => _tryApplyConfig(context) : null, ), ), @@ -114,13 +105,11 @@ class LandscapePage extends StatelessWidget { landscapePage = LandscapePage( onApplyConfig: Wizard.of(context).back, onBack: Wizard.of(context).back, - onSkip: Wizard.of(context).back, ); } else { landscapePage = LandscapePage( onApplyConfig: Wizard.of(context).next, onBack: Wizard.of(context).back, - onSkip: Wizard.of(context).next, ); } @@ -242,12 +231,10 @@ class LandscapeConfigForm extends StatelessWidget { class _NavigationButtonRow extends StatelessWidget { const _NavigationButtonRow({ this.onBack, - this.onSkip, this.onNext, }); final void Function()? onBack; - final void Function()? onSkip; final void Function()? onNext; @override @@ -261,13 +248,6 @@ class _NavigationButtonRow extends StatelessWidget { child: Text(lang.buttonBack), ), const Spacer(), - FilledButton( - onPressed: onSkip, - child: Text(lang.buttonSkip), - ), - const SizedBox( - width: 16.0, - ), ElevatedButton( onPressed: onNext, child: Text(lang.buttonNext), diff --git a/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart new file mode 100644 index 000000000..42811dfc1 --- /dev/null +++ b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:wizard_router/wizard_router.dart'; + +import '../../routes.dart'; +import '../landscape/landscape_model.dart'; +import '../widgets/navigation_row.dart'; +import '../widgets/page_widgets.dart'; +import '../widgets/radio_tile.dart'; + +enum _SkipEnum { + skip, + register, +} + +class LandscapeSkipPage extends StatefulWidget { + const LandscapeSkipPage({super.key}); + + @override + State createState() => _LandscapeSkipPageState(); +} + +class _LandscapeSkipPageState extends State { + _SkipEnum groupValue = _SkipEnum.skip; + + @override + Widget build(BuildContext context) { + final lang = AppLocalizations.of(context); + final wizard = Wizard.of(context); + + return ColumnPage( + svgAsset: 'assets/Landscape-tag.svg', + title: lang.landscapeTitle, + left: [ + MarkdownBody( + data: lang.landscapeHeading( + '[${lang.learnMore}](${LandscapeModel.landscapeURI})', + ), + onTapLink: (_, href, __) => launchUrl(LandscapeModel.landscapeURI), + ), + ], + right: [ + RadioTile( + value: _SkipEnum.skip, + title: lang.landscapeSkip, + subtitle: lang.landscapeSkipDescription, + groupValue: groupValue, + onChanged: (v) => setState(() { + groupValue = v!; + }), + ), + RadioTile( + value: _SkipEnum.register, + title: lang.landscapeSkipRegister, + groupValue: groupValue, + onChanged: (v) => setState(() { + groupValue = v!; + }), + ), + ], + navigationRow: NavigationRow( + onBack: wizard.back, + onNext: () { + switch (groupValue) { + case _SkipEnum.skip: + wizard.jump(Routes.subscriptionStatus); + case _SkipEnum.register: + wizard.next(); + } + }, + ), + ); + } +} diff --git a/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart new file mode 100644 index 000000000..f9269c875 --- /dev/null +++ b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class NavigationRow extends StatelessWidget { + const NavigationRow({ + required this.onBack, + required this.onNext, + this.backText, + this.nextText, + super.key, + }); + + final void Function()? onBack; + final String? backText; + final void Function()? onNext; + final String? nextText; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + OutlinedButton(onPressed: onBack, child: Text(backText ?? 'Back')), + FilledButton(onPressed: onNext, child: Text(nextText ?? 'Next')), + ], + ); + } +} diff --git a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart index a27003310..432313c5e 100644 --- a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart @@ -27,6 +27,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:yaru/yaru.dart'; +import 'navigation_row.dart'; import 'status_bar.dart'; /// The simplest material page that covers most of the use cases in this app, @@ -168,3 +169,91 @@ class _PageContent extends StatelessWidget { ); } } + +/// Two-column, vertically centered page. The left column always contains the +/// svg image and title, with the left children below it. Both columns are equal +/// in width. Optionally, a [NavigationRow] may be provided that will span the +/// width below both columns. +class ColumnPage extends StatelessWidget { + const ColumnPage({ + required this.left, + required this.right, + this.svgAsset = 'assets/Ubuntu-tag.svg', + this.title = 'Ubuntu Pro', + this.navigationRow, + super.key, + }); + + final List left; + final List right; + final String svgAsset; + final String title; + final NavigationRow? navigationRow; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Pro4WSLPage( + body: Padding( + padding: const EdgeInsets.fromLTRB(32.0, 32.0, 32.0, 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Left column + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: SvgPicture.asset( + svgAsset, + height: 70, + ), + ), + const WidgetSpan( + child: SizedBox( + width: 8, + ), + ), + TextSpan( + text: title, + style: theme.textTheme.displaySmall + ?.copyWith(fontWeight: FontWeight.w100), + ), + ], + ), + ), + const SizedBox(height: 24), + ...left, + ], + ), + ), + // Spacer + const SizedBox(width: 32), + // Right column + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: right, + ), + ), + ], + ), + ), + if (navigationRow != null) navigationRow!, + ], + ), + ), + ); + } +} diff --git a/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart b/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart new file mode 100644 index 000000000..6220ca6a3 --- /dev/null +++ b/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; + +class RadioTile extends StatelessWidget { + const RadioTile({ + required this.value, + required this.title, + required this.groupValue, + required this.onChanged, + this.subtitle, + super.key, + }); + + final String title; + final String? subtitle; + final T value, groupValue; + final Function(T?)? onChanged; + + @override + Widget build(BuildContext context) { + // Adds a nice visual clue that the tile is selected. + return YaruSelectableContainer( + selected: groupValue == value, + selectionColor: Theme.of(context).colorScheme.tertiaryContainer, + child: YaruRadioListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.comfortable, + dense: true, + title: Text(title), + subtitle: subtitle != null ? Text(subtitle!) : null, + value: value, + groupValue: groupValue, + onChanged: onChanged, + ), + ); + } +} diff --git a/gui/packages/ubuntupro/lib/routes.dart b/gui/packages/ubuntupro/lib/routes.dart index 28a9c61cd..248b69226 100644 --- a/gui/packages/ubuntupro/lib/routes.dart +++ b/gui/packages/ubuntupro/lib/routes.dart @@ -1,6 +1,7 @@ abstract class Routes { static const startup = '/startup'; static const subscribeNow = '/subscribe-now'; + static const skipLandscape = '/skip-landscape'; static const configureLandscape = '/configure-landscape'; static const subscriptionStatus = '/subscription-status'; static const configureLandscapeLate = '/configure-landscape-late'; diff --git a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart index 65e34df52..087d4a208 100644 --- a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart @@ -403,7 +403,6 @@ Widget buildApp( return buildSingleRouteMultiProviderApp( child: LandscapePage( onApplyConfig: () {}, - onSkip: () {}, onBack: () {}, ), providers: [ChangeNotifierProvider.value(value: model)], From 0145af2009eacab9bcbb56c3f64d86c100483f1a Mon Sep 17 00:00:00 2001 From: ashuntu Date: Wed, 4 Dec 2024 17:24:20 -0600 Subject: [PATCH 02/10] Add translations for NavigationRow --- gui/packages/ubuntupro/lib/l10n/app_en.arb | 2 +- .../ubuntupro/lib/pages/widgets/navigation_row.dart | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index af2b8692b..29c613f3f 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -100,7 +100,7 @@ } }, - "buttonNext": "Continue", + "buttonNext": "Next", "buttonSkip": "Skip", "buttonBack": "Back", diff --git a/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart index f9269c875..894f49784 100644 --- a/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart +++ b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class NavigationRow extends StatelessWidget { const NavigationRow({ @@ -16,11 +17,19 @@ class NavigationRow extends StatelessWidget { @override Widget build(BuildContext context) { + final lang = AppLocalizations.of(context); + return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - OutlinedButton(onPressed: onBack, child: Text(backText ?? 'Back')), - FilledButton(onPressed: onNext, child: Text(nextText ?? 'Next')), + OutlinedButton( + onPressed: onBack, + child: Text(backText ?? lang.buttonBack), + ), + FilledButton( + onPressed: onNext, + child: Text(nextText ?? lang.buttonNext), + ), ], ); } From 2c34adc6ba06fb4e1cf8a58af382ddfff2cea48c Mon Sep 17 00:00:00 2001 From: ashuntu Date: Wed, 4 Dec 2024 17:59:26 -0600 Subject: [PATCH 03/10] Add tests for Landscape skip page --- .../landscape_skip/landscape_page_test.dart | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart diff --git a/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart b/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart new file mode 100644 index 000000000..dd73de702 --- /dev/null +++ b/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ubuntupro/pages/landscape_skip/landscape_skip_page.dart'; +import 'package:wizard_router/wizard_router.dart'; +import 'package:yaru/yaru.dart'; +import 'package:yaru_test/yaru_test.dart'; + +import '../../utils/build_multiprovider_app.dart'; + +void main() { + testWidgets('default state', (tester) async { + final app = buildApp(); + await tester.pumpWidget(app); + final context = tester.element(find.byType(LandscapeSkipPage)); + final lang = AppLocalizations.of(context); + + final backButton = find.button(lang.buttonBack); + expect(backButton, findsOne); + // for the purposes of these tests, we don't really care what kind of button + // it is, just that it's enabled + expect(tester.widget(backButton).enabled, isTrue); + + final nextButton = find.button(lang.buttonNext); + expect(nextButton, findsOne); + expect(tester.widget(nextButton).enabled, isTrue); + + final skipRadioTile = find.ancestor( + of: find.text(lang.landscapeSkip), + matching: find.byType(YaruSelectableContainer), + ); + expect( + tester.widget(skipRadioTile).selected, + isTrue, + ); + + final registerRadioTile = find.ancestor( + of: find.text(lang.landscapeSkipRegister), + matching: find.byType(YaruSelectableContainer), + ); + expect( + tester.widget(registerRadioTile).selected, + isFalse, + ); + }); + + testWidgets('tiles selectable', (tester) async { + final app = buildApp(); + await tester.pumpWidget(app); + final context = tester.element(find.byType(LandscapeSkipPage)); + final lang = AppLocalizations.of(context); + + final skipRadioTile = find.ancestor( + of: find.text(lang.landscapeSkip), + matching: find.byType(YaruSelectableContainer), + ); + final registerRadioTile = find.ancestor( + of: find.text(lang.landscapeSkipRegister), + matching: find.byType(YaruSelectableContainer), + ); + + await tester.tap(registerRadioTile); + await tester.pump(); + expect( + tester.widget(registerRadioTile).selected, + isTrue, + ); + expect( + tester.widget(skipRadioTile).selected, + isFalse, + ); + + await tester.tap(skipRadioTile); + await tester.pump(); + expect( + tester.widget(registerRadioTile).selected, + isFalse, + ); + expect( + tester.widget(skipRadioTile).selected, + isTrue, + ); + }); +} + +Widget buildApp() { + return buildMultiProviderWizardApp( + routes: { + '/': WizardRoute(builder: (_) => const LandscapeSkipPage()), + }, + ); +} From 3a8a4d015e7f07f6ba620316cc62be53f89ce8fd Mon Sep 17 00:00:00 2001 From: ashuntu Date: Thu, 5 Dec 2024 15:49:01 -0600 Subject: [PATCH 04/10] Fix integration tests --- .../ubuntu_pro_for_wsl_test.dart | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart index 60e1e98bb..26e210dca 100644 --- a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart +++ b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart @@ -13,6 +13,7 @@ import 'package:ubuntupro/core/environment.dart'; import 'package:ubuntupro/launch_agent.dart'; import 'package:ubuntupro/main.dart' as app; import 'package:ubuntupro/pages/landscape/landscape_page.dart'; +import 'package:ubuntupro/pages/landscape_skip/landscape_skip_page.dart'; import 'package:ubuntupro/pages/startup/startup_page.dart'; import 'package:ubuntupro/pages/subscribe_now/subscribe_now_page.dart'; import 'package:ubuntupro/pages/subscription_status/subscription_status_page.dart'; @@ -128,18 +129,15 @@ void main() { await tester.tap(button); await tester.pumpAndSettle(); - // check that we transitioned to the LandscapePage - l10n = tester.l10n(); + // check we transition to skip page + l10n = tester.l10n(); final radios = find.byType(YaruSelectableContainer); - expect(radios, findsNWidgets(3)); - await tester.tap(radios.at(1)); - await tester.pump(); - await tester.tap(radios.at(0)); - await tester.pump(); - await tester.tap(radios.at(2)); - await tester.pump(); - final skip = find.button(l10n.buttonSkip); + expect(radios, findsNWidgets(2)); + final skip = find.text(l10n.landscapeSkip); await tester.tap(skip); + await tester.pump(); + final next = find.button(l10n.buttonNext); + await tester.tap(next); await tester.pumpAndSettle(); // checks that we transitioned to the SubscriptionStatusPage @@ -172,6 +170,17 @@ void main() { await tester.tap(button); await tester.pumpAndSettle(); + // check we transition to skip page + l10n = tester.l10n(); + final radios = find.byType(YaruSelectableContainer); + expect(radios, findsNWidgets(2)); + final register = find.text(l10n.landscapeSkipRegister); + await tester.tap(register); + await tester.pump(); + final next = find.button(l10n.buttonNext); + await tester.tap(next); + await tester.pumpAndSettle(); + // check that we transitioned to the LandscapePage l10n = tester.l10n(); final selfHostedRadio = find.ancestor( From 3ae112127b76e970f788d79ae3ef7dedfeb64f69 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Thu, 5 Dec 2024 16:29:08 -0600 Subject: [PATCH 05/10] Add tests for launching learn more link Also modifies the existing test for the SubscribeNow page's launcher to unify the launcher mocks. --- .../landscape_skip/landscape_page_test.dart | 17 ++++++ .../subcribe_now/subscribe_now_page_test.dart | 52 ++----------------- .../test/utils/url_launcher_mock.dart | 48 +++++++++++++++++ 3 files changed, 69 insertions(+), 48 deletions(-) create mode 100644 gui/packages/ubuntupro/test/utils/url_launcher_mock.dart diff --git a/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart b/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart index dd73de702..1372d8551 100644 --- a/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart @@ -2,13 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ubuntupro/pages/landscape_skip/landscape_skip_page.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:wizard_router/wizard_router.dart'; import 'package:yaru/yaru.dart'; import 'package:yaru_test/yaru_test.dart'; import '../../utils/build_multiprovider_app.dart'; +import '../../utils/url_launcher_mock.dart'; void main() { + final launcher = FakeUrlLauncher(); + UrlLauncherPlatform.instance = launcher; + testWidgets('default state', (tester) async { final app = buildApp(); await tester.pumpWidget(app); @@ -81,6 +86,18 @@ void main() { isTrue, ); }); + + testWidgets('launch web page', (tester) async { + final app = buildApp(); + await tester.pumpWidget(app); + final context = tester.element(find.byType(LandscapeSkipPage)); + final lang = AppLocalizations.of(context); + + expect(launcher.launched, isFalse); + await tester.tapOnText(find.textRange.ofSubstring(lang.learnMore)); + await tester.pump(); + expect(launcher.launched, isTrue); + }); } Widget buildApp() { diff --git a/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_page_test.dart b/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_page_test.dart index 0fd25844a..92f5595f5 100644 --- a/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_page_test.dart @@ -11,11 +11,11 @@ import 'package:ubuntu_service/ubuntu_service.dart'; import 'package:ubuntupro/core/agent_api_client.dart'; import 'package:ubuntupro/pages/subscribe_now/subscribe_now_model.dart'; import 'package:ubuntupro/pages/subscribe_now/subscribe_now_page.dart'; -import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:wizard_router/wizard_router.dart'; import '../../utils/build_multiprovider_app.dart'; +import '../../utils/url_launcher_mock.dart'; import 'subscribe_now_page_test.mocks.dart'; @GenerateMocks([SubscribeNowModel]) @@ -36,9 +36,11 @@ void main() { final app = buildApp(model, onSubscribeNoop); await tester.pumpWidget(app); + final context = tester.element(find.byType(SubscribeNowPage)); + final lang = AppLocalizations.of(context); expect(launcher.launched, isFalse); - await tester.tapOnText(find.textRange.ofSubstring('Learn more')); + await tester.tapOnText(find.textRange.ofSubstring(lang.learnMore)); await tester.pump(); expect(launcher.launched, isTrue); }); @@ -177,49 +179,3 @@ Widget buildApp( void onSubscribeNoop(SubscriptionInfo _) {} class FakeAgentApiClient extends Fake implements AgentApiClient {} - -class FakeUrlLauncher extends UrlLauncherPlatform { - bool launched = false; - - @override - Future canLaunch(String url) async { - return true; - } - - @override - Future closeWebView() async {} - - @override - Future launchUrl(String url, LaunchOptions options) async { - launched = true; - return true; - } - - @override - Future supportsCloseForMode(PreferredLaunchMode mode) async { - return true; - } - - @override - Future supportsMode(PreferredLaunchMode mode) async { - return true; - } - - @override - Future launch( - String url, { - required bool useSafariVC, - required bool useWebView, - required bool enableJavaScript, - required bool enableDomStorage, - required bool universalLinksOnly, - required Map headers, - String? webOnlyWindowName, - }) async { - launched = true; - return true; - } - - @override - LinkDelegate? get linkDelegate => null; -} diff --git a/gui/packages/ubuntupro/test/utils/url_launcher_mock.dart b/gui/packages/ubuntupro/test/utils/url_launcher_mock.dart new file mode 100644 index 000000000..33c53358e --- /dev/null +++ b/gui/packages/ubuntupro/test/utils/url_launcher_mock.dart @@ -0,0 +1,48 @@ +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +class FakeUrlLauncher extends UrlLauncherPlatform { + bool launched = false; + + @override + Future canLaunch(String url) async { + return true; + } + + @override + Future closeWebView() async {} + + @override + Future launchUrl(String url, LaunchOptions options) async { + launched = true; + return true; + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + return true; + } + + @override + Future supportsMode(PreferredLaunchMode mode) async { + return true; + } + + @override + Future launch( + String url, { + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, + }) async { + launched = true; + return true; + } + + @override + LinkDelegate? get linkDelegate => null; +} From 51c1a151214a707a10ab0d389fdc9d714a1ab114 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Thu, 5 Dec 2024 17:05:16 -0600 Subject: [PATCH 06/10] Add Landscape url launch test --- .../pages/landscape/landscape_page_test.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart index 087d4a208..5a25d63e5 100644 --- a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart @@ -12,11 +12,13 @@ import 'package:ubuntupro/core/agent_api_client.dart'; import 'package:ubuntupro/pages/landscape/landscape_model.dart'; import 'package:ubuntupro/pages/landscape/landscape_page.dart'; import 'package:ubuntupro/pages/widgets/page_widgets.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:wizard_router/wizard_router.dart'; import 'package:yaru/yaru.dart'; import 'package:yaru_test/yaru_test.dart'; import '../../utils/build_multiprovider_app.dart'; +import '../../utils/url_launcher_mock.dart'; import 'landscape_page_test.mocks.dart'; @GenerateMocks([AgentApiClient]) @@ -29,6 +31,22 @@ void main() { binding.platformDispatcher.textScaleFactorTestValue = 0.6; FilePicker.platform = FakeFilePicker([caCert]); + final launcher = FakeUrlLauncher(); + UrlLauncherPlatform.instance = launcher; + + testWidgets('launch web page', (tester) async { + final model = LandscapeModel(MockAgentApiClient()); + final app = buildApp(model); + await tester.pumpWidget(app); + final context = tester.element(find.byType(LandscapePage)); + final lang = AppLocalizations.of(context); + + expect(launcher.launched, isFalse); + await tester.tapOnText(find.textRange.ofSubstring(lang.learnMore)); + await tester.pump(); + expect(launcher.launched, isTrue); + }); + group('input sections', () { testWidgets('default state', (tester) async { final model = LandscapeModel(MockAgentApiClient()); From de2ddff8b00088a27e9bc9cc4cd9af5c677029c0 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Mon, 9 Dec 2024 16:25:22 -0600 Subject: [PATCH 07/10] Adjust Landscape copy --- gui/packages/ubuntupro/lib/constants.dart | 2 ++ gui/packages/ubuntupro/lib/l10n/app_en.arb | 3 +-- gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart | 3 ++- .../lib/pages/landscape_skip/landscape_skip_page.dart | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gui/packages/ubuntupro/lib/constants.dart b/gui/packages/ubuntupro/lib/constants.dart index ae46f9372..eda5ff338 100644 --- a/gui/packages/ubuntupro/lib/constants.dart +++ b/gui/packages/ubuntupro/lib/constants.dart @@ -12,3 +12,5 @@ const kVersion = String.fromEnvironment( 'UP4W_FULL_VERSION', defaultValue: 'Dev', ); + +const kLandscapeTitle = 'Landscape'; diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index 29c613f3f..65fc1a72c 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -58,10 +58,9 @@ "purchaseStatusUnknown": "Unknown error when trying to purchase the subscription. Consider restarting this app.", "landscapeSkip": "Skip for now", - "landscapeSkipDescription": "You can always configure Landscape later", + "landscapeSkipDescription": "You can always configure Landscape later in this app", "landscapeSkipRegister": "Register with Landscape", - "landscapeTitle": "Landscape", "landscapeHeading": "Configure the connection to Landscape to manage your Ubuntu WSL instances remotely. {landscapeLink}", "@landscapeHeading": { "placeholders": { diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart index 84bf06b30..81a9b3a69 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart @@ -14,6 +14,7 @@ import 'package:yaru/yaru.dart'; import '/core/agent_api_client.dart'; import '/pages/widgets/page_widgets.dart'; +import '../../constants.dart'; import 'landscape_model.dart'; const _kHeight = 8.0; @@ -49,7 +50,7 @@ class LandscapePage extends StatelessWidget { return LandingPage( svgAsset: 'assets/Landscape-tag.svg', - title: lang.landscapeTitle, + title: kLandscapeTitle, children: [ // Only rebuilds if the value of model.landscapeURI changes (never in production) Selector( diff --git a/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart index 42811dfc1..736f08e5b 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:wizard_router/wizard_router.dart'; +import '../../constants.dart'; import '../../routes.dart'; import '../landscape/landscape_model.dart'; import '../widgets/navigation_row.dart'; @@ -32,7 +33,7 @@ class _LandscapeSkipPageState extends State { return ColumnPage( svgAsset: 'assets/Landscape-tag.svg', - title: lang.landscapeTitle, + title: kLandscapeTitle, left: [ MarkdownBody( data: lang.landscapeHeading( From 5a94f3dbea80d85c1bb39a6f0e2ed3fd26e03a33 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Mon, 9 Dec 2024 17:06:30 -0600 Subject: [PATCH 08/10] Move wizard logic to routes declaration --- gui/packages/ubuntupro/lib/app.dart | 13 +++++++++++-- .../landscape_skip/landscape_skip_page.dart | 18 +++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/gui/packages/ubuntupro/lib/app.dart b/gui/packages/ubuntupro/lib/app.dart index 749656acf..b0eb64334 100644 --- a/gui/packages/ubuntupro/lib/app.dart +++ b/gui/packages/ubuntupro/lib/app.dart @@ -77,8 +77,17 @@ class Pro4WSLApp extends StatelessWidget { }, ), if (settings.isLandscapeConfigurationEnabled) ...{ - Routes.skipLandscape: - WizardRoute(builder: (_) => const LandscapeSkipPage()), + Routes.skipLandscape: WizardRoute( + builder: (_) => const LandscapeSkipPage(), + onNext: (settings) { + switch (settings.arguments as SkipEnum) { + case SkipEnum.skip: + return Routes.subscriptionStatus; + default: + return null; + } + }, + ), Routes.configureLandscape: const WizardRoute(builder: LandscapePage.create), Routes.subscriptionStatus: WizardRoute( diff --git a/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart index 736f08e5b..0ab232067 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart @@ -5,13 +5,12 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:wizard_router/wizard_router.dart'; import '../../constants.dart'; -import '../../routes.dart'; import '../landscape/landscape_model.dart'; import '../widgets/navigation_row.dart'; import '../widgets/page_widgets.dart'; import '../widgets/radio_tile.dart'; -enum _SkipEnum { +enum SkipEnum { skip, register, } @@ -24,7 +23,7 @@ class LandscapeSkipPage extends StatefulWidget { } class _LandscapeSkipPageState extends State { - _SkipEnum groupValue = _SkipEnum.skip; + SkipEnum groupValue = SkipEnum.skip; @override Widget build(BuildContext context) { @@ -44,7 +43,7 @@ class _LandscapeSkipPageState extends State { ], right: [ RadioTile( - value: _SkipEnum.skip, + value: SkipEnum.skip, title: lang.landscapeSkip, subtitle: lang.landscapeSkipDescription, groupValue: groupValue, @@ -53,7 +52,7 @@ class _LandscapeSkipPageState extends State { }), ), RadioTile( - value: _SkipEnum.register, + value: SkipEnum.register, title: lang.landscapeSkipRegister, groupValue: groupValue, onChanged: (v) => setState(() { @@ -63,14 +62,7 @@ class _LandscapeSkipPageState extends State { ], navigationRow: NavigationRow( onBack: wizard.back, - onNext: () { - switch (groupValue) { - case _SkipEnum.skip: - wizard.jump(Routes.subscriptionStatus); - case _SkipEnum.register: - wizard.next(); - } - }, + onNext: () => wizard.next(arguments: groupValue), ), ); } From c5bcd689d5d9f59a9def9b7895d5892e6f161211 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Mon, 9 Dec 2024 17:09:49 -0600 Subject: [PATCH 09/10] Rename Landscape skip page tests --- .../{landscape_page_test.dart => landscape_skip_page_test.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gui/packages/ubuntupro/test/pages/landscape_skip/{landscape_page_test.dart => landscape_skip_page_test.dart} (100%) diff --git a/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart b/gui/packages/ubuntupro/test/pages/landscape_skip/landscape_skip_page_test.dart similarity index 100% rename from gui/packages/ubuntupro/test/pages/landscape_skip/landscape_page_test.dart rename to gui/packages/ubuntupro/test/pages/landscape_skip/landscape_skip_page_test.dart From 6d5257308bc1bbd84095f8f112e044c2c0b97c36 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Wed, 11 Dec 2024 11:36:51 -0600 Subject: [PATCH 10/10] Convert relative import to constant import --- gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart index 81a9b3a69..f181ae92f 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart @@ -12,9 +12,9 @@ import 'package:wizard_router/wizard_router.dart'; import 'package:yaru/widgets.dart'; import 'package:yaru/yaru.dart'; +import '/constants.dart'; import '/core/agent_api_client.dart'; import '/pages/widgets/page_widgets.dart'; -import '../../constants.dart'; import 'landscape_model.dart'; const _kHeight = 8.0;