mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
Duo keeps conversations and Settings details open across closed, book and flat postures. Book mode splits at the fold; flattening restores the sidebar. Native rail controls, full-width navigation backdrops and sheet-local safe areas keep content aligned. Ordinary iPhones remain single-pane in portrait and landscape; tablet and Duo split layouts remain enabled. Includes the recovered preview's matching pane widths, Buzz empty-pane mark, stable Settings chevrons, Bestie pane routing and Duo-only upward New menu attachment. Current main's Activity redesign, Search content fade and unread-navigation button are retained. Menu selection checks sit beside their labels. Shared mobile changes remain in scope: the native iOS status editor, Appearance preview layout, avatar crop preservation, onboarding footer layout, profile/invite sheet geometry and nested conversation navigation. This PR is not exclusively Duo-gated. Nested navigator observers preserve recording cancellation and navigation callbacks; crop resizing preserves the selected image region. iOS 27.1 APIs remain compile-time and runtime gated. Validation on the reconciled source: Flutter analysis, Activity/Home journeys, the full mobile test suite, file-size checks and Swift package tests. The iPhone single-pane fix was also exercised on an iPhone 18 Pro simulator before reconciliation. Fresh hosted CI and physical-device confirmation of the reconciled build are separate and remain pending. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com>
793 lines
26 KiB
Dart
793 lines
26 KiB
Dart
import 'package:buzz/shared/widgets/dm_header_portrait.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(
|
|
'animated DM portrait opens the same settings as the native title',
|
|
(tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
var settingsOpened = 0;
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(
|
|
viewPadding: EdgeInsets.only(top: 62),
|
|
disableAnimations: true,
|
|
),
|
|
child: SizedBox(
|
|
width: 393,
|
|
height: 166,
|
|
child: IosNavigationBar(
|
|
title: 'Agent',
|
|
onTitlePressed: () => settingsOpened++,
|
|
subtitle: 'Online',
|
|
titleAvatars: const [
|
|
IosNavigationAction(
|
|
label: 'Agent',
|
|
avatarInitial: 'A',
|
|
avatarIsAgent: true,
|
|
imageUrl:
|
|
'https://dwwgwmfqqjotj.cloudfront.net/avatars/20260925T154625166Z/poster/gloopies/gloopies-13.png',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final params =
|
|
tester.widget<UiKitView>(find.byType(UiKitView)).creationParams!
|
|
as Map;
|
|
expect((params['titleAvatars'] as List).length, 1);
|
|
expect(params['titleHasAnimatedPortrait'], isTrue);
|
|
final portrait = find.byType(DmHeaderPortrait);
|
|
expect(tester.getSize(portrait), const Size(64, 64));
|
|
expect(
|
|
tester.getTopLeft(portrait).dy -
|
|
tester.getTopLeft(find.byType(UiKitView)).dy,
|
|
58,
|
|
);
|
|
await tester.tap(
|
|
find.byKey(const ValueKey('navigation-header-avatar-settings')),
|
|
);
|
|
expect(settingsOpened, 1);
|
|
await tester.pumpWidget(
|
|
_testApp(
|
|
home: const MediaQuery(
|
|
data: MediaQueryData(),
|
|
child: SizedBox(
|
|
width: 951,
|
|
height: 128,
|
|
child: IosNavigationBar(
|
|
title: 'Alice',
|
|
centerTitleInPane: true,
|
|
titleAvatars: [IosNavigationAction(label: 'Alice')],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final paneParams =
|
|
tester.widget<UiKitView>(find.byType(UiKitView)).creationParams!
|
|
as Map;
|
|
expect(paneParams['portraitTopPadding'], 12);
|
|
expect(
|
|
tester.getTopLeft(find.byType(DmHeaderPortrait)).dy -
|
|
tester.getTopLeft(find.byType(UiKitView)).dy,
|
|
12,
|
|
);
|
|
await tester.pumpWidget(const SizedBox());
|
|
debugDefaultTargetPlatformOverride = null;
|
|
},
|
|
);
|
|
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!),
|
|
),
|
|
);
|