mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
## Summary Polish mobile Home and Solo: group Search/Inbox beside a separate profile avatar, move Inbox and Settings to bottom-up pages with Close controls, and place tabs opposite the plus/compose action. Morph plus to compose clockwise when entering Solo and reverse counterclockwise when returning Home, with a directional quarter-turn of the icon inside a level button and a subtle scale pulse, respecting reduced motion. Align Search’s Close control with Inbox, simplify its placeholder, and initialize selected filter contrast correctly. Collapse the plus menu when leaving Home. Center Solo’s native-glass New chat empty state and keep it stationary behind the keyboard. Standardize Tabler icons and neutral backgrounds, retain SF Back/Close on iOS, use native Liquid Glass headers, tab container, and actions (solid grey selected tab, slightly darker on iOS) on iOS and black actions on Android, and smooth the expanded menu corners. Repair native header safe-area/return layout and compose movement after cancellation. ### Related issue None found for this UI refinement; follows #8337. ### Testing - Formatting, analysis, full mobile suite: 3,487 passed, 4 skipped; 3 push-configuration tests passed. File-size checks passed. - Base UI built and installed on Pixel and iPhone in place. Pixel navigation/New chat cancellation checked. Subsequent glass and rotation refinements are previewed on iPhone; motion feel awaits human confirmation. - Draft pending human confirmation of the final Settings entrance/return behavior on iPhone. ### Pixel snapshots | Solo | Expanded actions | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-solo.png" width="280" alt="Solo empty state with New chat" /> | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-actions.png" width="280" alt="Expanded channel actions with smooth corners" /> | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
713 lines
23 KiB
Dart
713 lines
23 KiB
Dart
import 'dart:async';
|
|
import 'package:buzz/shared/relay/media_image.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:buzz/shared/theme/theme.dart';
|
|
import 'package:buzz/shared/widgets/frosted_app_bar.dart';
|
|
import 'package:buzz/shared/widgets/frosted_scaffold.dart';
|
|
import 'package:buzz/shared/widgets/ios_navigation_bar.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';
|
|
|
|
void main() {
|
|
testWidgets(
|
|
'native toolbar receives stable safe inset inside a translated route',
|
|
(tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(viewPadding: EdgeInsets.only(top: 62)),
|
|
child: Transform.translate(
|
|
offset: const Offset(0, 600),
|
|
child: const SizedBox(
|
|
height: 120,
|
|
child: IosNavigationBar(title: 'Settings'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.widget<UiKitView>(find.byType(UiKitView)).creationParams,
|
|
containsPair('safeTop', 62.0),
|
|
);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'native readiness waits for configuration and layout acknowledgement',
|
|
(tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
const channel = MethodChannel('buzz/ios_navigation_bar/844');
|
|
final applied = Completer<void>();
|
|
final readiness = <bool>[];
|
|
final calls = <String>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
|
call,
|
|
) async {
|
|
calls.add(call.method);
|
|
if (call.method == 'prepareForReveal') await applied.future;
|
|
return null;
|
|
});
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
),
|
|
);
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: SizedBox(
|
|
height: 100,
|
|
child: IosNavigationBar(
|
|
title: 'Bravo',
|
|
onReadyChanged: readiness.add,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
tester.widget<UiKitView>(find.byType(UiKitView)).onPlatformViewCreated!(
|
|
844,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(calls, contains('prepareForReveal'));
|
|
expect(readiness.last, false);
|
|
applied.complete();
|
|
await tester.pumpAndSettle();
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(readiness.last, true);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
},
|
|
);
|
|
|
|
testWidgets('native landing ignores pending profile artwork', (tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
final pending = Completer<http.Response>();
|
|
var requested = false;
|
|
final client = MockClient((_) {
|
|
requested = true;
|
|
return pending.future;
|
|
});
|
|
addTearDown(client.close);
|
|
const channel = MethodChannel('buzz/ios_navigation_bar/846');
|
|
final readiness = <bool>[];
|
|
final calls = <String>[];
|
|
final createdChannels = <MethodChannel>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform_views,
|
|
(call) async {
|
|
if (call.method == 'create') {
|
|
final id = (call.arguments as Map)['id'];
|
|
final native = MethodChannel('${IosNavigationBar.viewType}/$id');
|
|
createdChannels.add(native);
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
native,
|
|
(call) async {
|
|
calls.add(call.method);
|
|
return null;
|
|
},
|
|
);
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform_views,
|
|
null,
|
|
);
|
|
for (final native in createdChannels) {
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
native,
|
|
null,
|
|
);
|
|
}
|
|
});
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
|
call,
|
|
) async {
|
|
calls.add(call.method);
|
|
return null;
|
|
});
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
),
|
|
);
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [mediaHttpClientProvider.overrideWithValue(client)],
|
|
child: _testApp(
|
|
home: IosNavigationBar(
|
|
title: 'Bravo',
|
|
leading: IosNavigationAction(
|
|
label: 'Community settings',
|
|
avatarInitial: 'B',
|
|
onAvatarBoundsChanged: (_) {},
|
|
),
|
|
actions: const [
|
|
IosNavigationAction(
|
|
label: 'Profile',
|
|
avatarInitial: 'P',
|
|
imageUrl: 'https://slow.example.com/profile.png',
|
|
),
|
|
],
|
|
onReadyChanged: readiness.add,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
tester.widget<UiKitView>(find.byType(UiKitView)).onPlatformViewCreated!(
|
|
846,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
Map leading() =>
|
|
(tester.widget<UiKitView>(find.byType(UiKitView)).creationParams
|
|
as Map)['leading']
|
|
as Map;
|
|
await tester.runAsync(() async {
|
|
for (
|
|
var attempt = 0;
|
|
attempt < 100 && leading()['imageData'] == null;
|
|
attempt++
|
|
) {
|
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
await tester.pump();
|
|
}
|
|
});
|
|
await tester.pumpAndSettle();
|
|
expect(requested, isTrue);
|
|
expect(pending.isCompleted, isFalse);
|
|
expect(calls, contains('prepareForReveal'));
|
|
expect(readiness.last, isTrue);
|
|
expect(leading()['imageData'], isNotNull);
|
|
pending.complete(http.Response('offline', 503));
|
|
await tester.pumpAndSettle();
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
|
|
testWidgets(
|
|
'native avatar reports global transition bounds and retains a hidden slot',
|
|
(tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
const channel = MethodChannel('${IosNavigationBar.viewType}/842');
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
(_) async => null,
|
|
);
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
),
|
|
);
|
|
Rect? measured;
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: Padding(
|
|
padding: const EdgeInsets.only(top: 30),
|
|
child: IosNavigationBar(
|
|
title: 'Alpha',
|
|
leading: IosNavigationAction(
|
|
label: 'Community settings',
|
|
avatarInitial: 'A',
|
|
avatarHidden: true,
|
|
onAvatarBoundsChanged: (bounds) => measured = bounds,
|
|
onPressed: () {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final view = tester.widget<UiKitView>(find.byType(UiKitView));
|
|
final leading = (view.creationParams as Map)['leading'] as Map;
|
|
expect(leading['tracksAvatarBounds'], isTrue);
|
|
expect(leading['avatarHidden'], isTrue);
|
|
view.onPlatformViewCreated!(842);
|
|
await tester.pump();
|
|
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
|
channel.name,
|
|
channel.codec.encodeMethodCall(
|
|
const MethodCall('avatarBounds', {
|
|
'id': 'leading',
|
|
'x': 16.0,
|
|
'y': 4.0,
|
|
'width': 36.0,
|
|
'height': 36.0,
|
|
}),
|
|
),
|
|
(_) {},
|
|
);
|
|
expect(measured, const Rect.fromLTWH(16, 34, 36, 36));
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
},
|
|
);
|
|
|
|
for (final reverse in [false, true]) {
|
|
testWidgets('initial material tracks content above reverse=$reverse', (
|
|
tester,
|
|
) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
ValueListenable<double>? offset;
|
|
Widget page(int count) => _testApp(
|
|
home: FrostedScaffold(
|
|
appBar: const FrostedAppBar(title: Text('Conversation')),
|
|
body: Builder(
|
|
builder: (context) {
|
|
offset = IosNavigationScrollScope.maybeOf(context);
|
|
return ListView(
|
|
controller: controller,
|
|
reverse: reverse,
|
|
children: [
|
|
for (var i = 0; i < count; i++)
|
|
SizedBox(height: 60, child: Text('Message $i')),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpWidget(page(40));
|
|
await tester.pumpAndSettle();
|
|
expect(offset!.value, reverse ? greaterThan(0) : 0);
|
|
controller.jumpTo(controller.position.maxScrollExtent);
|
|
await tester.pumpAndSettle();
|
|
expect(offset!.value, reverse ? 0 : greaterThan(0));
|
|
await tester.pumpWidget(page(1));
|
|
await tester.pumpAndSettle();
|
|
expect(offset!.value, 0);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
}
|
|
|
|
testWidgets('native material follows live theme surface changes', (
|
|
tester,
|
|
) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
const channel = MethodChannel('buzz/ios_navigation_bar/845');
|
|
final configurations = <Map<Object?, Object?>>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
|
call,
|
|
) async {
|
|
if (call.method == 'configure') configurations.add(call.arguments as Map);
|
|
return null;
|
|
});
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
),
|
|
);
|
|
Widget app(Color surface) => ProviderScope(
|
|
child: MaterialApp(
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: surface,
|
|
).copyWith(surface: surface),
|
|
),
|
|
home: const Scaffold(
|
|
body: IosNavigationBar(title: 'general', subtitle: '3 members'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpWidget(app(const Color(0xFFFFEEDD)));
|
|
final view = tester.widget<UiKitView>(find.byType(UiKitView));
|
|
expect(view.creationParams, containsPair('background', 0xFFFFEEDD));
|
|
view.onPlatformViewCreated!(845);
|
|
await tester.pump();
|
|
await tester.pumpWidget(app(const Color(0xFF223344)));
|
|
await tester.pumpAndSettle();
|
|
expect(configurations.last['background'], 0xFF223344);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
|
|
testWidgets('larger native metrics keep a deeply scrolled title collapsed', (
|
|
tester,
|
|
) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
const channel = MethodChannel('buzz/ios_navigation_bar/843');
|
|
final calls = <MethodCall>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
|
call,
|
|
) async {
|
|
calls.add(call);
|
|
return null;
|
|
});
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
),
|
|
);
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: FrostedScaffold(
|
|
nativePinnedBody: true,
|
|
appBar: const FrostedAppBar(
|
|
title: Text('Search'),
|
|
nativeLargeTitle: true,
|
|
),
|
|
body: ListView(
|
|
children: [
|
|
for (var i = 0; i < 40; i++)
|
|
SizedBox(height: 60, child: Text('Row $i')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
tester.widget<UiKitView>(find.byType(UiKitView)).onPlatformViewCreated!(
|
|
843,
|
|
);
|
|
await tester.pump();
|
|
await tester.drag(find.byType(ListView), const Offset(0, -250));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
calls.where((call) => call.method == 'scroll').last.arguments,
|
|
greaterThan(52),
|
|
);
|
|
final bodyTop = tester.getTopLeft(find.byType(ListView)).dy;
|
|
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
|
channel.name,
|
|
channel.codec.encodeMethodCall(
|
|
const MethodCall('metrics', {
|
|
'compactHeight': 44.0,
|
|
'expandedHeight': 124.0,
|
|
}),
|
|
),
|
|
(_) {},
|
|
);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(
|
|
calls.where((call) => call.method == 'scroll').last.arguments,
|
|
greaterThan(80),
|
|
);
|
|
expect(tester.getSize(find.byType(UiKitView)).height, 44);
|
|
expect(tester.getTopLeft(find.byType(ListView)).dy, bodyTop);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
|
|
testWidgets(
|
|
'iOS routes use UIKit and route native menus to current callbacks',
|
|
(tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
const channel = MethodChannel('${IosNavigationBar.viewType}/42');
|
|
final calls = <MethodCall>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
|
call,
|
|
) async {
|
|
calls.add(call);
|
|
return null;
|
|
});
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
),
|
|
);
|
|
var selected = 0;
|
|
var callbackVersion = 1;
|
|
Widget page() {
|
|
final version = callbackVersion;
|
|
return _testApp(
|
|
theme: AppTheme.light(),
|
|
home: FrostedScaffold(
|
|
appBar: FrostedAppBar(
|
|
nativeTitle: 'Activity',
|
|
nativeLargeTitle: true,
|
|
nativeActions: [
|
|
IosNavigationAction(
|
|
label: 'Filter',
|
|
symbol: 'line.3.horizontal.decrease',
|
|
children: [
|
|
IosNavigationAction(
|
|
label: 'Mentions',
|
|
onPressed: () => selected = version,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
body: ListView(
|
|
children: [
|
|
for (var i = 0; i < 40; i++)
|
|
SizedBox(height: 60, child: Text('Item $i')),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(page());
|
|
final view = tester.widget<UiKitView>(find.byType(UiKitView));
|
|
expect(view.viewType, IosNavigationBar.viewType);
|
|
expect(view.creationParams, containsPair('largeTitle', true));
|
|
view.onPlatformViewCreated!(42);
|
|
await tester.pump();
|
|
callbackVersion = 2;
|
|
await tester.pumpWidget(page());
|
|
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
|
channel.name,
|
|
channel.codec.encodeMethodCall(const MethodCall('action', '0.0')),
|
|
(_) {},
|
|
);
|
|
expect(selected, 2);
|
|
await tester.drag(find.byType(ListView), const Offset(0, -180));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
calls.where((call) => call.method == 'scroll').last.arguments,
|
|
greaterThan(52),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
},
|
|
);
|
|
|
|
testWidgets('native back button respects the Flutter route stack', (
|
|
tester,
|
|
) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
final navigator = GlobalKey<NavigatorState>();
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
navigatorKey: navigator,
|
|
home: const Scaffold(body: Text('Root')),
|
|
),
|
|
);
|
|
navigator.currentState!.push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => const FrostedScaffold(
|
|
appBar: FrostedAppBar(title: Text('Theme')),
|
|
body: SizedBox.expand(),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final native = tester.widget<IosNavigationBar>(
|
|
find.byType(IosNavigationBar),
|
|
);
|
|
expect(native.title, 'Theme');
|
|
expect(native.onBack, isNotNull);
|
|
native.onBack!();
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Root'), findsOneWidget);
|
|
expect(find.byType(UiKitView), findsNothing);
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
|
|
testWidgets('UIKit measurements resize the bar and page spacing together', (
|
|
tester,
|
|
) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
const channel = MethodChannel('${IosNavigationBar.viewType}/91');
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
(_) async => null,
|
|
);
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
channel,
|
|
null,
|
|
),
|
|
);
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: Builder(
|
|
builder: (context) => FrostedScaffold(
|
|
appBar: const FrostedAppBar(
|
|
title: Text('Home'),
|
|
nativeLargeTitle: true,
|
|
),
|
|
body: ListView(
|
|
children: [
|
|
SizedBox(
|
|
key: const ValueKey('body-inset'),
|
|
height: frostedAppBarHeight(context, nativeLargeTitle: true),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final view = tester.widget<UiKitView>(find.byType(UiKitView));
|
|
view.onPlatformViewCreated!(91);
|
|
await tester.pump();
|
|
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
|
channel.name,
|
|
channel.codec.encodeMethodCall(
|
|
const MethodCall('metrics', {
|
|
'compactHeight': 56.0,
|
|
'expandedHeight': 112.0,
|
|
}),
|
|
),
|
|
(_) {},
|
|
);
|
|
await tester.pump();
|
|
expect(tester.getSize(find.byType(UiKitView)).height, 112);
|
|
expect(
|
|
tester.getSize(find.byKey(const ValueKey('body-inset'))).height,
|
|
112,
|
|
);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
|
|
testWidgets('loading avatars use initials instead of navigation symbols', (
|
|
tester,
|
|
) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: const IosNavigationBar(
|
|
title: 'Home',
|
|
leading: IosNavigationAction(
|
|
label: 'Community',
|
|
symbol: 'building.2',
|
|
avatarInitial: 'K',
|
|
avatarIdentity: 'community-one',
|
|
),
|
|
actions: [
|
|
IosNavigationAction(
|
|
label: 'Profile',
|
|
symbol: 'person.crop.circle',
|
|
avatarInitial: 'A',
|
|
avatarIdentity: 'profile-one',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
final params =
|
|
tester.widget<UiKitView>(find.byType(UiKitView)).creationParams! as Map;
|
|
expect((params['leading'] as Map)['symbol'], isNull);
|
|
expect((params['leading'] as Map)['avatarInitial'], 'K');
|
|
final profile = (params['actions'] as List).single as Map;
|
|
expect(profile['symbol'], isNull);
|
|
expect(profile['avatarInitial'], 'A');
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
|
|
testWidgets(
|
|
'retains decoded avatars on theme refresh but isolates identities',
|
|
(tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
final messenger = tester.binding.defaultBinaryMessenger;
|
|
final channels = <MethodChannel>[];
|
|
messenger.setMockMethodCallHandler(SystemChannels.platform_views, (
|
|
call,
|
|
) async {
|
|
if (call.method == 'create') {
|
|
final id = (call.arguments as Map)['id'];
|
|
final channel = MethodChannel('${IosNavigationBar.viewType}/$id');
|
|
channels.add(channel);
|
|
messenger.setMockMethodCallHandler(channel, (_) async => null);
|
|
}
|
|
return null;
|
|
});
|
|
addTearDown(() {
|
|
messenger.setMockMethodCallHandler(SystemChannels.platform_views, null);
|
|
for (final channel in channels) {
|
|
messenger.setMockMethodCallHandler(channel, null);
|
|
}
|
|
});
|
|
Widget page(String identity, ThemeData theme) => _testApp(
|
|
theme: theme,
|
|
home: IosNavigationBar(
|
|
title: 'Home',
|
|
leading: IosNavigationAction(
|
|
label: 'Profile',
|
|
avatarInitial: 'K',
|
|
avatarIdentity: identity,
|
|
),
|
|
),
|
|
);
|
|
Map avatar() =>
|
|
(tester.widget<UiKitView>(find.byType(UiKitView)).creationParams!
|
|
as Map)['leading']
|
|
as Map;
|
|
await tester.pumpWidget(page('one', AppTheme.light()));
|
|
await tester.runAsync(() async {
|
|
for (
|
|
var attempt = 0;
|
|
attempt < 100 && avatar()['imageData'] == null;
|
|
attempt++
|
|
) {
|
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
await tester.pump();
|
|
}
|
|
});
|
|
final image = avatar()['imageData'];
|
|
expect(image, isNotNull);
|
|
await tester.pumpWidget(page('one', AppTheme.dark()));
|
|
expect(avatar()['imageData'], image);
|
|
await tester.pumpWidget(page('two', AppTheme.dark()));
|
|
expect(avatar()['imageData'], isNull);
|
|
expect(avatar()['symbol'], isNull);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
},
|
|
);
|
|
|
|
testWidgets('Android retains its Flutter header', (tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
theme: AppTheme.light(),
|
|
home: const FrostedScaffold(
|
|
appBar: FrostedAppBar(title: Text('Settings')),
|
|
body: SizedBox.expand(),
|
|
),
|
|
),
|
|
);
|
|
expect(find.byType(UiKitView), findsNothing);
|
|
expect(find.text('Settings'), findsOneWidget);
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
}
|
|
|
|
Widget _testApp({
|
|
ThemeData? theme,
|
|
required Widget home,
|
|
GlobalKey<NavigatorState>? navigatorKey,
|
|
}) => ProviderScope(
|
|
child: MaterialApp(
|
|
theme: theme,
|
|
home: home,
|
|
navigatorKey: navigatorKey,
|
|
builder: (context, child) => IosNavigationMetricsHost(child: child!),
|
|
),
|
|
);
|