diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index 5ff6c76995b..d6d5a24753f 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -1223,12 +1223,13 @@ packages: source: hosted version: "0.9.2" tray_manager: - dependency: transitive + dependency: "direct overridden" description: - name: tray_manager - sha256: b1975a05e0c6999e983cf9a58a6a098318c896040ccebac5398a3cc9e43b9c69 - url: "https://pub.dev" - source: hosted + path: "." + ref: "fix/libayatana-set-icon-deprecation" + resolved-ref: "056da3fef20e75877e8d2a4ae73d93f51a0f77cc" + url: "https://github.com/provokateurin/tray_manager.git" + source: git version: "0.2.0" typed_data: dependency: transitive diff --git a/packages/app/pubspec.yaml b/packages/app/pubspec.yaml index 915f1af9369..476e3db3a9c 100644 --- a/packages/app/pubspec.yaml +++ b/packages/app/pubspec.yaml @@ -50,6 +50,12 @@ dev_dependencies: shared_preferences: any vector_graphics_compiler: any +dependency_overrides: + tray_manager: + git: + url: https://github.com/provokateurin/tray_manager.git + ref: fix/libayatana-set-icon-deprecation + flutter: uses-material-design: true assets: diff --git a/packages/app/pubspec_overrides.yaml b/packages/app/pubspec_overrides.yaml index 43095c1170f..78fd9656e7d 100644 --- a/packages/app/pubspec_overrides.yaml +++ b/packages/app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: dynamite_runtime,neon_framework,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: dynamite_runtime,neon_framework,neon_lints,nextcloud,sort_box,tray_manager dependency_overrides: dynamite_runtime: path: ../dynamite/dynamite_runtime @@ -22,3 +22,7 @@ dependency_overrides: path: ../nextcloud sort_box: path: ../sort_box + tray_manager: + git: + url: https://github.com/provokateurin/tray_manager.git + ref: fix/libayatana-set-icon-deprecation diff --git a/packages/neon_framework/lib/src/theme/theme.dart b/packages/neon_framework/lib/src/theme/theme.dart index acea710b659..faa014be760 100644 --- a/packages/neon_framework/lib/src/theme/theme.dart +++ b/packages/neon_framework/lib/src/theme/theme.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; +import 'package:neon_framework/src/theme/branding.dart'; import 'package:neon_framework/src/theme/colors.dart'; import 'package:neon_framework/src/theme/neon.dart'; import 'package:neon_framework/src/theme/server.dart'; @@ -19,7 +20,23 @@ class AppTheme { this.useNextcloudTheme = false, this.oledAsDark = false, this.appThemes, - }); + }) : platform = null; + + @visibleForTesting + const AppTheme.test({ + this.serverTheme = const ServerTheme(nextcloudTheme: null), + this.deviceThemeLight, + this.deviceThemeDark, + this.neonTheme = const NeonTheme( + branding: Branding( + name: 'Test App', + logo: Placeholder(), + ), + ), + this.useNextcloudTheme = false, + this.oledAsDark = false, + this.platform, + }) : appThemes = null; /// The theme provided by the Nextcloud server. final ServerTheme serverTheme; @@ -42,6 +59,10 @@ class AppTheme { /// The base theme for the Neon app. final NeonTheme neonTheme; + /// The platform the material widgets should adapt to target. + @visibleForTesting + final TargetPlatform? platform; + ColorScheme _buildColorScheme(final Brightness brightness) { ColorScheme? colorScheme; @@ -83,6 +104,7 @@ class AppTheme { return ThemeData( useMaterial3: true, + platform: platform, colorScheme: colorScheme, scaffoldBackgroundColor: colorScheme.background, cardColor: colorScheme.background, diff --git a/packages/neon_framework/pubspec.yaml b/packages/neon_framework/pubspec.yaml index 101fc186170..4db8673949a 100644 --- a/packages/neon_framework/pubspec.yaml +++ b/packages/neon_framework/pubspec.yaml @@ -59,6 +59,8 @@ dependencies: dev_dependencies: build_runner: ^2.4.7 + flutter_test: + sdk: flutter go_router_builder: ^2.4.0 json_serializable: ^6.7.1 mocktail: ^1.0.2 diff --git a/packages/neon_framework/test/dialog_test.dart b/packages/neon_framework/test/dialog_test.dart new file mode 100644 index 00000000000..53ccf89586c --- /dev/null +++ b/packages/neon_framework/test/dialog_test.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:neon_framework/l10n/localizations_en.dart'; +import 'package:neon_framework/src/theme/theme.dart'; +import 'package:neon_framework/src/widgets/dialog.dart'; +import 'package:neon_framework/utils.dart'; + +Widget wrapDialog(final Widget dialog, [final TargetPlatform platform = TargetPlatform.android]) { + final theme = AppTheme.test(platform: platform); + const locale = Locale('en'); + + return MaterialApp( + theme: theme.lightTheme, + localizationsDelegates: NeonLocalizations.localizationsDelegates, + supportedLocales: NeonLocalizations.supportedLocales, + locale: locale, + home: dialog, + ); +} + +void main() { + group('dialog', () { + group('NeonConfirmationDialog', () { + testWidgets('NeonConfirmationDialog widget', (final widgetTester) async { + const title = 'My Title'; + var dialog = const NeonConfirmationDialog(title: title); + await widgetTester.pumpWidget(wrapDialog(dialog)); + + expect(find.text(title), findsOneWidget); + expect(find.byType(NeonDialogAction), findsExactly(2)); + + // Not true on cupertino platforms + expect(find.byType(OutlinedButton), findsOneWidget); + expect(find.byType(ElevatedButton), findsOneWidget); + + dialog = const NeonConfirmationDialog( + title: title, + isDestructive: false, + ); + + await widgetTester.pumpWidget(wrapDialog(dialog)); + + expect(find.byType(NeonDialogAction), findsExactly(2)); + expect(find.byType(OutlinedButton), findsExactly(2)); + + const icon = Icon(Icons.error); + const content = SizedBox(key: Key('content')); + const confirmAction = SizedBox(key: Key('confirmAction')); + const declineAction = SizedBox(key: Key('declineAction')); + dialog = const NeonConfirmationDialog( + title: title, + icon: icon, + content: content, + confirmAction: confirmAction, + declineAction: declineAction, + ); + await widgetTester.pumpWidget(wrapDialog(dialog)); + + expect(find.byIcon(Icons.error), findsOneWidget); + expect(find.byKey(const Key('content')), findsOneWidget); + expect(find.byKey(const Key('confirmAction')), findsOneWidget); + expect(find.byKey(const Key('declineAction')), findsOneWidget); + }); + + testWidgets('NeonConfirmationDialog actions', (final widgetTester) async { + const title = 'My Title'; + await widgetTester.pumpWidget(wrapDialog(const Placeholder())); + final context = widgetTester.element(find.byType(Placeholder)); + + // confirm + var result = showConfirmationDialog(context: context, title: title); + await widgetTester.pumpAndSettle(); + await widgetTester.tap(find.text(NeonLocalizationsEn().actionContinue)); + expect(await result, isTrue); + + // decline + result = showConfirmationDialog(context: context, title: title); + await widgetTester.pumpAndSettle(); + await widgetTester.tap(find.text(NeonLocalizationsEn().actionCancel)); + expect(await result, isFalse); + + // cancel by tapping outside + result = showConfirmationDialog(context: context, title: title); + await widgetTester.pumpAndSettle(); + await widgetTester.tapAt(Offset.zero); + expect(await result, isFalse); + }); + }); + + group('NeonRenameDialog', () { + testWidgets('NeonRenameDialog widget', (final widgetTester) async { + const title = 'My Title'; + const value = 'My value'; + const dialog = NeonRenameDialog(title: title, value: value); + await widgetTester.pumpWidget(wrapDialog(dialog)); + + expect(find.text(title), findsExactly(2), reason: 'The title is also used for the confirmation button'); + expect(find.text(value), findsOneWidget); + expect(find.byType(TextFormField), findsOneWidget); + }); + + testWidgets('NeonRenameDialog actions', (final widgetTester) async { + const title = 'My Title'; + const value = 'My value'; + await widgetTester.pumpWidget(wrapDialog(const Placeholder())); + final context = widgetTester.element(find.byType(Placeholder)); + + // Equal value should not submit + var result = showRenameDialog(context: context, title: title, initialValue: value); + await widgetTester.pumpAndSettle(); + await widgetTester.enterText(find.byType(TextFormField), value); + await widgetTester.tap(find.byType(NeonDialogAction)); + expect(await result, isNull); + + // Empty value should not submit + result = showRenameDialog(context: context, title: title, initialValue: value); + await widgetTester.pumpAndSettle(); + await widgetTester.enterText(find.byType(TextFormField), ''); + await widgetTester.tap(find.byType(NeonDialogAction)); + + // Different value should submit + await widgetTester.enterText(find.byType(TextFormField), 'My new value'); + await widgetTester.tap(find.byType(NeonDialogAction)); + expect(await result, equals('My new value')); + + // Submit via keyboard + result = showRenameDialog(context: context, title: title, initialValue: value); + await widgetTester.pumpAndSettle(); + await widgetTester.enterText(find.byType(TextFormField), 'My new value'); + await widgetTester.testTextInput.receiveAction(TextInputAction.done); + await widgetTester.tap(find.byType(NeonDialogAction)); + expect(await result, equals('My new value')); + }); + }); + + group('NeonErrorDialog', () { + testWidgets('NeonErrorDialog widget', (final widgetTester) async { + const title = 'My Title'; + const content = 'My content'; + var dialog = const NeonErrorDialog(content: content, title: title); + await widgetTester.pumpWidget(wrapDialog(dialog)); + + expect(find.byIcon(Icons.error), findsOneWidget); + expect(find.text(title), findsOneWidget); + expect(find.text(content), findsOneWidget); + expect(find.byType(NeonDialogAction), findsOneWidget); + + dialog = const NeonErrorDialog(content: content); + await widgetTester.pumpWidget(wrapDialog(dialog)); + + expect(find.text(NeonLocalizationsEn().errorDialog), findsOneWidget); + }); + + testWidgets('NeonErrorDialog actions', (final widgetTester) async { + const content = 'My content'; + await widgetTester.pumpWidget(wrapDialog(const Placeholder())); + final context = widgetTester.element(find.byType(Placeholder)); + + final result = showErrorDialog(context: context, message: content); + await widgetTester.pumpAndSettle(); + await widgetTester.tap(find.text(NeonLocalizationsEn().actionClose)); + await result; + }); + }); + + testWidgets('UnimplementedDialog', (final widgetTester) async { + const title = 'My Title'; + await widgetTester.pumpWidget(wrapDialog(const Placeholder())); + final context = widgetTester.element(find.byType(Placeholder)); + + final result = showUnimplementedDialog(context: context, title: title); + await widgetTester.pumpAndSettle(); + await widgetTester.tap(find.text(NeonLocalizationsEn().actionClose)); + await result; + }); + + testWidgets('NeonDialog', (final widgetTester) async { + var dialog = const NeonDialog( + actions: [], + ); + await widgetTester.pumpWidget(wrapDialog(dialog, TargetPlatform.macOS)); + expect( + find.byType(NeonDialogAction), + findsOneWidget, + reason: 'Dialogs can not be dismissed on cupertino platforms. Expecting a fallback action.', + ); + + dialog = const NeonDialog( + automaticallyShowCancel: false, + actions: [], + ); + await widgetTester.pumpWidget(wrapDialog(dialog, TargetPlatform.macOS)); + expect(find.byType(NeonDialogAction), findsNothing); + }); + }); +}