mirror of
https://github.com/block/buzz.git
synced 2026-11-02 15:31:27 +00:00
Removes aggregate unread dots from mobile filters, Inbox, and Solo
section headings. The aggregate-dot removal is an explicitly requested
product change. Solo and Team names match the current `block/buzz-app`
sidebar’s light/dark colours and 400/600 read/unread weights (not this
repository’s older `desktop/` implementation); Solo rows show
owned-agent working dots with reduced-motion support.
Restores the community-switch loading and avatar flight with a leftward
arc to the community menu in its current top-right position.
Mobile uses one fixed light/dark application palette; legacy catalog
theme names do not select navigation palettes. The 600 unread weight
follows [the current desktop sidebar
override](36a283b238/src/bundled/channels/Channels.module.css (L47)).
Working status uses live agent typing; desktop’s local waiting state is
not available on mobile.
Validation: clean analysis; 3,997 mobile tests passed, 28 skipped; four
push-configuration checks passed. Navigation styling visually approved
on iPhone; community-switch regression covers portrait/landscape and
normal/reduced motion; authenticated-shell recovery is covered. Hosted
CI passed at `bfeb0fecd`. Updated Pairing Preview installed on iPhone
for transition testing.
### Snapshots
Actual Flutter widgets with sample conversations; static working dots
for reduced motion.
| Light | Dark |
|---|---|
| 
| 
|
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
291 lines
11 KiB
Dart
291 lines
11 KiB
Dart
import 'package:buzz/features/channels/default_agent_launcher.dart';
|
|
import 'package:buzz/features/agents/agents_page.dart';
|
|
import 'dart:io';
|
|
import 'package:buzz/shared/widgets/ios_message_create_button.dart';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:buzz/features/home/home_page.dart';
|
|
import 'package:buzz/features/profile/profile_provider.dart';
|
|
import 'package:buzz/features/profile/settings_profile_header.dart';
|
|
import 'package:buzz/features/profile/user_status_provider.dart';
|
|
import 'package:buzz/features/settings/settings_page.dart';
|
|
import 'package:buzz/shared/custom_emoji/custom_emoji_provider.dart';
|
|
import 'package:buzz/shared/relay/relay.dart';
|
|
import 'package:buzz/shared/profile/user_profile.dart';
|
|
import 'package:buzz/features/profile/user_status.dart';
|
|
import 'package:buzz/shared/theme/theme.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:nostr/nostr.dart' as nostr;
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) {
|
|
testWidgets(
|
|
'Settings overlays are visible above Home on ${platform.name}',
|
|
(tester) async {
|
|
if (Platform.environment.containsKey('SETTINGS_SCREENSHOTS')) {
|
|
await tester.runAsync(() async {
|
|
for (final font in {
|
|
'Inter': 'assets/fonts/InterVariable.ttf',
|
|
'BuzzTabler': 'assets/fonts/TablerIcons.ttf',
|
|
}.entries) {
|
|
await (FontLoader(
|
|
font.key,
|
|
)..addFont(rootBundle.load(font.value))).load();
|
|
}
|
|
});
|
|
}
|
|
debugDefaultTargetPlatformOverride = platform;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
tester.view.physicalSize = const Size(390, 844);
|
|
tester.view.devicePixelRatio = 1;
|
|
tester.view.padding = FakeViewPadding(top: 47, bottom: 34);
|
|
tester.view.viewPadding = FakeViewPadding(top: 47, bottom: 34);
|
|
addTearDown(tester.view.reset);
|
|
PackageInfo.setMockInitialValues(
|
|
appName: 'Buzz',
|
|
packageName: 'xyz.block.buzz',
|
|
version: '0.16.0',
|
|
buildNumber: '432',
|
|
buildSignature: '',
|
|
);
|
|
SharedPreferences.setMockInitialValues({});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final presence = _Presence();
|
|
final nativeChannels = <MethodChannel>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform_views,
|
|
(call) async {
|
|
if (call.method == 'create') {
|
|
final args = call.arguments as Map;
|
|
final channel = MethodChannel(
|
|
'${args['viewType']}/${args['id']}',
|
|
);
|
|
nativeChannels.add(channel);
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
(_) async => null,
|
|
);
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
for (final channel in [
|
|
SystemChannels.platform_views,
|
|
...nativeChannels,
|
|
]) {
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
);
|
|
}
|
|
});
|
|
String? copied;
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(call) async {
|
|
if (call.method == 'Clipboard.setData') {
|
|
copied = (call.arguments as Map)['text'] as String;
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
);
|
|
});
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
savedPrefsProvider.overrideWithValue(prefs),
|
|
relayConfigProvider.overrideWith(_Config.new),
|
|
appLifecycleProvider.overrideWith(_Lifecycle.new),
|
|
profileProvider.overrideWith(_Profile.new),
|
|
presenceProvider.overrideWith(() => presence),
|
|
userStatusProvider.overrideWith(_Status.new),
|
|
customEmojiListProvider.overrideWithValue(const []),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.light(),
|
|
builder: (_, child) => RepaintBoundary(
|
|
key: const ValueKey('settings-test-capture'),
|
|
child: child!,
|
|
),
|
|
home: HomePage(
|
|
onAgents: showAgents,
|
|
defaultAgentBuilder: (context) => DefaultAgentLauncher(
|
|
onChooseAgent: () => showAgents(context),
|
|
),
|
|
settingsPageBuilder: (_) => SettingsPage(
|
|
embedded: true,
|
|
profileHeader: const SettingsProfileHeader(),
|
|
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
if (platform == TargetPlatform.iOS) {
|
|
tester
|
|
.widget<IosMessageCreateButton>(
|
|
find.byWidgetPredicate(
|
|
(w) =>
|
|
w is IosMessageCreateButton &&
|
|
w.accessibilityLabel == 'Community menu',
|
|
),
|
|
)
|
|
.actions
|
|
.firstWhere((action) => action.label == 'Settings')
|
|
.onPressed!();
|
|
} else {
|
|
await tester.tap(find.byTooltip('Community menu'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Settings'));
|
|
}
|
|
await tester.pumpAndSettle();
|
|
if (platform == TargetPlatform.iOS) {
|
|
expect(find.byType(HomePage), findsNothing);
|
|
expect(find.byType(HomePage, skipOffstage: false), findsOneWidget);
|
|
final menu = tester.widget<UiKitView>(
|
|
find.byKey(const ValueKey('settings-presence-menu')),
|
|
);
|
|
expect(menu.viewType, 'buzz/presence_menu');
|
|
expect(menu.creationParams, containsPair('presence', 'offline'));
|
|
final nativeMenu = nativeChannels.singleWhere(
|
|
(channel) => channel.name.startsWith('buzz/presence_menu/'),
|
|
);
|
|
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
|
nativeMenu.name,
|
|
const StandardMethodCodec().encodeMethodCall(
|
|
const MethodCall('selected', 'online'),
|
|
),
|
|
(_) {},
|
|
);
|
|
await tester.pumpAndSettle();
|
|
} else {
|
|
await tester.tap(
|
|
find.byKey(const ValueKey('settings-presence-menu')),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final online = find.byKey(const ValueKey('settings-presence-online'));
|
|
expect(online.hitTestable(), findsOneWidget);
|
|
final fade = tester.widget<FadeTransition>(
|
|
find.byKey(const ValueKey('activity-popover-fade')),
|
|
);
|
|
expect(fade.opacity.value, 1);
|
|
final menu = tester.getRect(
|
|
find.byKey(const ValueKey('settings-presence-popover')),
|
|
);
|
|
final anchor = tester.getRect(
|
|
find.byKey(const ValueKey('settings-presence-target')),
|
|
);
|
|
expect(menu.center.dx, closeTo(anchor.center.dx, 0.1));
|
|
expect(menu.top, closeTo(anchor.bottom + Grid.half, 0.1));
|
|
expect(menu.bottom, lessThanOrEqualTo(810));
|
|
await _capture(tester, '${platform.name}-availability');
|
|
await tester.tapAt(const Offset(20, 700));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.byKey(const ValueKey('settings-presence-popover')),
|
|
findsNothing,
|
|
);
|
|
expect(presence.selected, isEmpty);
|
|
await tester.tap(
|
|
find.byKey(const ValueKey('settings-presence-menu')),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(online);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
expect(presence.selected, ['online']);
|
|
await tester.tap(find.text('Copy public key (npub)'));
|
|
await tester.pumpAndSettle();
|
|
expect(copied, startsWith('npub1'));
|
|
final snackbar = find.byType(SnackBar);
|
|
expect(snackbar, findsOneWidget);
|
|
final settings = find.byType(SettingsPage);
|
|
expect(
|
|
find.descendant(of: settings, matching: snackbar),
|
|
findsOneWidget,
|
|
);
|
|
final message = find.text('Public key (npub) copied');
|
|
expect(message.hitTestable(), findsOneWidget);
|
|
final toastSurface = find
|
|
.ancestor(of: message, matching: find.byType(Material))
|
|
.first;
|
|
expect(tester.getRect(toastSurface).bottom, lessThanOrEqualTo(810));
|
|
await _capture(tester, '${platform.name}-copied');
|
|
Navigator.of(tester.element(settings)).pop();
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(SnackBar), findsNothing);
|
|
expect(tester.takeException(), isNull);
|
|
debugDefaultTargetPlatformOverride = null;
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Presence extends PresenceNotifier {
|
|
final selected = <String>[];
|
|
@override
|
|
Future<String> build() async => 'offline';
|
|
@override
|
|
Future<void> setPresence(String status) async {
|
|
selected.add(status);
|
|
state = AsyncData(status);
|
|
}
|
|
}
|
|
|
|
class _Profile extends ProfileNotifier {
|
|
@override
|
|
Future<UserProfile?> build() async =>
|
|
UserProfile(pubkey: 'aabb', displayName: 'Test');
|
|
}
|
|
|
|
class _Status extends UserStatusNotifier {
|
|
@override
|
|
Future<UserStatus?> build() async => null;
|
|
}
|
|
|
|
class _Config extends RelayConfigNotifier {
|
|
@override
|
|
RelayConfig build() => RelayConfig(
|
|
baseUrl: 'https://relay.test',
|
|
nsec: nostr.Keys(
|
|
'1111111111111111111111111111111111111111111111111111111111111111',
|
|
).nsec,
|
|
);
|
|
}
|
|
|
|
Future<void> _capture(WidgetTester tester, String name) async {
|
|
final directory = Platform.environment['SETTINGS_SCREENSHOTS'];
|
|
if (directory == null) return;
|
|
final boundary = tester.renderObject<RenderRepaintBoundary>(
|
|
find.byKey(const ValueKey('settings-test-capture')),
|
|
);
|
|
await tester.runAsync(() async {
|
|
final image = await boundary.toImage(pixelRatio: 2);
|
|
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
await Directory(directory).create(recursive: true);
|
|
await File(
|
|
'$directory/$name.png',
|
|
).writeAsBytes(bytes!.buffer.asUint8List());
|
|
image.dispose();
|
|
});
|
|
}
|
|
|
|
class _Lifecycle extends AppLifecycleNotifier {
|
|
@override
|
|
AppLifecycleState build() => AppLifecycleState.resumed;
|
|
}
|