Add avatar-menu navigation style and inline Android search bar
- New Settings > Appearance > "Navigation menu" toggle (SettingsController.navMenuStyle, NooNavMenuStyle.drawer/avatarMenu): the hamburger-opens-a-Drawer pattern stays the default, or the avatar button opens a full-width dropdown (showAvatarMenu) instead, holding the same hidden-tabs + Settings content the drawer does. Removes one of the two top-corner targets the shell asks a thumb to reach, with no permanent chrome added. - NooTopBar gains androidTitleReplacement so Android's top bar can swap its title for an inline search-field-styled launcher (ShellSearchLauncher) when search isn't in the bottom bar, instead of a bare search icon beside it. - ShellAvatarButton's onTap/label are now overridable so AppTopBar can repoint it per the new setting without this file depending on avatar_menu.dart (which already depends on it for shared helpers). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:noo/models/app_tab.dart';
|
||||
import 'package:noo/models/saved_account.dart';
|
||||
import 'package:noo/providers/connectivity_controller.dart';
|
||||
import 'package:noo/providers/files_controller.dart';
|
||||
import 'package:noo/providers/session_controller.dart';
|
||||
import 'package:noo/providers/settings_controller.dart';
|
||||
import 'package:noo/providers/sync_status_controller.dart';
|
||||
import 'package:noo/providers/trash_controller.dart';
|
||||
import 'package:noo/theme/app_theme.dart';
|
||||
import 'package:noo/widgets/app_top_bar.dart';
|
||||
import 'package:noo/widgets/noo/nav/noo_top_bar.dart';
|
||||
|
||||
/// Covers `SettingsController.navMenuStyle`'s two options as wired through
|
||||
/// `AppTopBar`/`ShellAvatarButton`: [NooNavMenuStyle.drawer] (today's
|
||||
/// hamburger-opens-a-left-Drawer pattern, unchanged) vs
|
||||
/// [NooNavMenuStyle.avatarMenu] (no hamburger at all - the avatar opens
|
||||
/// `showAvatarMenu`'s dropdown instead of the account switcher).
|
||||
void main() {
|
||||
const secureStorageChannel = MethodChannel(
|
||||
'plugins.it_nomads.com/flutter_secure_storage',
|
||||
);
|
||||
const connectivityChannel = MethodChannel(
|
||||
'dev.fluttercommunity.plus/connectivity',
|
||||
);
|
||||
const connectivityStatusChannel = EventChannel(
|
||||
'dev.fluttercommunity.plus/connectivity_status',
|
||||
);
|
||||
|
||||
const accountId = 'server_example_com__alice';
|
||||
const passwordKey = 'nc_app_password_$accountId';
|
||||
const password = 'app-password';
|
||||
|
||||
setUpAll(() {
|
||||
GoogleFonts.config.allowRuntimeFetching = false;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(secureStorageChannel, (call) async {
|
||||
if (call.method == 'readAll') {
|
||||
return <String, String>{passwordKey: password};
|
||||
}
|
||||
if (call.method == 'read') {
|
||||
final key = (call.arguments as Map)['key'] as String?;
|
||||
return key == passwordKey ? password : null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
// Offline, same as `account_view_test.dart`'s setup: keeps
|
||||
// SessionController in a provisional login and stops
|
||||
// FilesController/SyncStatusController/TrashController's network/timer
|
||||
// machinery from hanging the test.
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(connectivityChannel, (call) async {
|
||||
if (call.method == 'check') return <String>['none'];
|
||||
return null;
|
||||
});
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockStreamHandler(
|
||||
connectivityStatusChannel,
|
||||
MockStreamHandler.inline(onListen: (arguments, events) {}),
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'account_migration_v1_done': true,
|
||||
'accounts_list': jsonEncode([
|
||||
const SavedAccount(
|
||||
id: accountId,
|
||||
serverUrl: 'https://server.example.com',
|
||||
username: 'alice',
|
||||
).toJson(),
|
||||
]),
|
||||
'active_account_id': accountId,
|
||||
});
|
||||
});
|
||||
|
||||
Future<void> pumpTopBar(
|
||||
WidgetTester tester, {
|
||||
required NooNavMenuStyle navMenuStyle,
|
||||
}) async {
|
||||
await tester.binding.setSurfaceSize(const Size(400, 800));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => ConnectivityController()),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => SessionController(context.read()),
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => SettingsController()),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => FilesController(context.read()),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) =>
|
||||
SyncStatusController(context.read(), context.read()),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => TrashController(context.read()),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: AppTheme.light(AppTheme.defaultAccent, useDynamicColor: false),
|
||||
home: Scaffold(
|
||||
drawer: const Drawer(child: Text('drawer open')),
|
||||
appBar: AppTopBar(
|
||||
style: NooNavStyle.android,
|
||||
tab: AppTab.files,
|
||||
navMenuStyle: navMenuStyle,
|
||||
),
|
||||
body: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Not `pumpAndSettle` - the same reasoning as `account_view_test.dart`:
|
||||
// the active account's dependent controllers start periodic
|
||||
// timers/futures that would spin it forever.
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('avatarMenu style: no hamburger, avatar opens the dropdown', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTopBar(tester, navMenuStyle: NooNavMenuStyle.avatarMenu);
|
||||
|
||||
expect(find.byTooltip('Menu'), findsOneWidget);
|
||||
expect(find.byIcon(LucideIcons.menu), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('Menu'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
// The dropdown's own header + Settings row - not the account
|
||||
// switcher's "Add account"/"Manage accounts" rows.
|
||||
expect(find.text('alice'), findsOneWidget);
|
||||
expect(find.text('Settings'), findsOneWidget);
|
||||
expect(find.text('Add account'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('drawer style (default): hamburger opens the real drawer', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTopBar(tester, navMenuStyle: NooNavMenuStyle.drawer);
|
||||
|
||||
expect(find.byTooltip('Menu'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Menu'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('drawer open'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'drawer style (default): avatar still opens the account switcher',
|
||||
(tester) async {
|
||||
await pumpTopBar(tester, navMenuStyle: NooNavMenuStyle.drawer);
|
||||
|
||||
expect(find.byTooltip('Accounts'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Accounts'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.text('Add account'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user