fix plans and exception

This commit is contained in:
otsmr 2026-10-06 07:24:14 +02:00
parent 76a9455c7c
commit 7d17ffba1f
12 changed files with 188 additions and 93 deletions

View file

@ -1408,12 +1408,6 @@ abstract class AppLocalizations {
/// **'Monthly'** /// **'Monthly'**
String get monthly; String get monthly;
/// No description provided for @proFeature1.
///
/// In en, this message translates to:
/// **'Unlimited media file uploads'**
String get proFeature1;
/// No description provided for @proFeature2. /// No description provided for @proFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1432,12 +1426,6 @@ abstract class AppLocalizations {
/// **'Restore flames'** /// **'Restore flames'**
String get proFeature4; String get proFeature4;
/// No description provided for @familyFeature1.
///
/// In en, this message translates to:
/// **'Unlimited media file uploads'**
String get familyFeature1;
/// No description provided for @familyFeature2. /// No description provided for @familyFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1462,12 +1450,6 @@ abstract class AppLocalizations {
/// **'10 Media file uploads per day'** /// **'10 Media file uploads per day'**
String get freeFeature1; String get freeFeature1;
/// No description provided for @plusFeature1.
///
/// In en, this message translates to:
/// **'Unlimited media file uploads'**
String get plusFeature1;
/// No description provided for @plusFeature2. /// No description provided for @plusFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -756,9 +756,6 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get monthly => 'شهريًا'; String get monthly => 'شهريًا';
@override
String get proFeature1 => 'رفع غير محدود لملفات الوسائط';
@override @override
String get proFeature2 => 'مستخدم Plus إضافي واحد'; String get proFeature2 => 'مستخدم Plus إضافي واحد';
@ -768,9 +765,6 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get proFeature4 => 'استعادة الشعلات'; String get proFeature4 => 'استعادة الشعلات';
@override
String get familyFeature1 => 'رفع غير محدود لملفات الوسائط';
@override @override
String get familyFeature2 => '4 مستخدمين Plus إضافيين'; String get familyFeature2 => '4 مستخدمين Plus إضافيين';
@ -783,9 +777,6 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get freeFeature1 => 'رفع 10 ملفات وسائط يوميًا'; String get freeFeature1 => 'رفع 10 ملفات وسائط يوميًا';
@override
String get plusFeature1 => 'رفع غير محدود لملفات الوسائط';
@override @override
String get plusFeature2 => 'ميزات إضافية (قريبًا)'; String get plusFeature2 => 'ميزات إضافية (قريبًا)';

View file

@ -754,9 +754,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get monthly => 'Monatlich'; String get monthly => 'Monatlich';
@override
String get proFeature1 => 'Unbegrenzte Medien-Datei-Uploads';
@override @override
String get proFeature2 => '1 zusätzlicher Plus Benutzer'; String get proFeature2 => '1 zusätzlicher Plus Benutzer';
@ -766,9 +763,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get proFeature4 => 'Flammen wiederherstellen'; String get proFeature4 => 'Flammen wiederherstellen';
@override
String get familyFeature1 => 'Unbegrenzte Medien-Datei-Uploads';
@override @override
String get familyFeature2 => '4 zusätzliche Plus Benutzer'; String get familyFeature2 => '4 zusätzliche Plus Benutzer';
@ -781,9 +775,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag'; String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag';
@override
String get plusFeature1 => 'Unbegrenzte Medien-Datei-Uploads';
@override @override
String get plusFeature2 => 'Zusatzfunktionen (coming-soon)'; String get plusFeature2 => 'Zusatzfunktionen (coming-soon)';

View file

@ -750,9 +750,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get monthly => 'Monthly'; String get monthly => 'Monthly';
@override
String get proFeature1 => 'Unlimited media file uploads';
@override @override
String get proFeature2 => '1 additional Plus user'; String get proFeature2 => '1 additional Plus user';
@ -762,9 +759,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get proFeature4 => 'Restore flames'; String get proFeature4 => 'Restore flames';
@override
String get familyFeature1 => 'Unlimited media file uploads';
@override @override
String get familyFeature2 => '4 additional Plus user'; String get familyFeature2 => '4 additional Plus user';
@ -777,9 +771,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get freeFeature1 => '10 Media file uploads per day'; String get freeFeature1 => '10 Media file uploads per day';
@override
String get plusFeature1 => 'Unlimited media file uploads';
@override @override
String get plusFeature2 => 'Additional features (coming-soon)'; String get plusFeature2 => 'Additional features (coming-soon)';

View file

@ -218,16 +218,13 @@
"yearly": "سنويًا", "yearly": "سنويًا",
"month": "شهر", "month": "شهر",
"monthly": "شهريًا", "monthly": "شهريًا",
"proFeature1": "رفع غير محدود لملفات الوسائط",
"proFeature2": "مستخدم Plus إضافي واحد", "proFeature2": "مستخدم Plus إضافي واحد",
"proFeature3": "مساحة 25 GB لتخزين الذكريات", "proFeature3": "مساحة 25 GB لتخزين الذكريات",
"proFeature4": "استعادة الشعلات", "proFeature4": "استعادة الشعلات",
"familyFeature1": "رفع غير محدود لملفات الوسائط",
"familyFeature2": "4 مستخدمين Plus إضافيين", "familyFeature2": "4 مستخدمين Plus إضافيين",
"familyFeature3": "مساحة 50 GB لتخزين الذكريات", "familyFeature3": "مساحة 50 GB لتخزين الذكريات",
"familyFeature4": "استعادة الشعلات", "familyFeature4": "استعادة الشعلات",
"freeFeature1": "رفع 10 ملفات وسائط يوميًا", "freeFeature1": "رفع 10 ملفات وسائط يوميًا",
"plusFeature1": "رفع غير محدود لملفات الوسائط",
"plusFeature2": "ميزات إضافية (قريبًا)", "plusFeature2": "ميزات إضافية (قريبًا)",
"manageAdditionalUsers": "إدارة المستخدمين الإضافيين", "manageAdditionalUsers": "إدارة المستخدمين الإضافيين",
"open": "فتح", "open": "فتح",

View file

@ -255,7 +255,6 @@
"errorPlanNotAllowed": "Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.", "errorPlanNotAllowed": "Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.",
"errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.", "errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.",
"errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.", "errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.",
"proFeature1": "Unbegrenzte Medien-Datei-Uploads",
"proFeature2": "1 zusätzlicher Plus Benutzer", "proFeature2": "1 zusätzlicher Plus Benutzer",
"proFeature3": "25GB Memories Speicher", "proFeature3": "25GB Memories Speicher",
"proFeature4": "Flammen wiederherstellen", "proFeature4": "Flammen wiederherstellen",
@ -263,12 +262,10 @@
"month": "Monat", "month": "Monat",
"yearly": "Jährlich", "yearly": "Jährlich",
"monthly": "Monatlich", "monthly": "Monatlich",
"familyFeature1": "Unbegrenzte Medien-Datei-Uploads",
"familyFeature2": "4 zusätzliche Plus Benutzer", "familyFeature2": "4 zusätzliche Plus Benutzer",
"familyFeature3": "50GB Memories Speicher", "familyFeature3": "50GB Memories Speicher",
"familyFeature4": "Flammen wiederherstellen", "familyFeature4": "Flammen wiederherstellen",
"freeFeature1": "10 Medien-Datei-Uploads pro Tag", "freeFeature1": "10 Medien-Datei-Uploads pro Tag",
"plusFeature1": "Unbegrenzte Medien-Datei-Uploads",
"plusFeature2": "Zusatzfunktionen (coming-soon)", "plusFeature2": "Zusatzfunktionen (coming-soon)",
"manageAdditionalUsers": "Zusätzliche Benutzer verwalten", "manageAdditionalUsers": "Zusätzliche Benutzer verwalten",
"open": "Offene", "open": "Offene",

View file

@ -269,16 +269,13 @@
"yearly": "Yearly", "yearly": "Yearly",
"month": "month", "month": "month",
"monthly": "Monthly", "monthly": "Monthly",
"proFeature1": "Unlimited media file uploads",
"proFeature2": "1 additional Plus user", "proFeature2": "1 additional Plus user",
"proFeature3": "25GB Memories storage", "proFeature3": "25GB Memories storage",
"proFeature4": "Restore flames", "proFeature4": "Restore flames",
"familyFeature1": "Unlimited media file uploads",
"familyFeature2": "4 additional Plus user", "familyFeature2": "4 additional Plus user",
"familyFeature3": "50GB Memories storage", "familyFeature3": "50GB Memories storage",
"familyFeature4": "Restore flames", "familyFeature4": "Restore flames",
"freeFeature1": "10 Media file uploads per day", "freeFeature1": "10 Media file uploads per day",
"plusFeature1": "Unlimited media file uploads",
"plusFeature2": "Additional features (coming-soon)", "plusFeature2": "Additional features (coming-soon)",
"manageAdditionalUsers": "Manage additional users", "manageAdditionalUsers": "Manage additional users",
"open": "Open", "open": "Open",

View file

@ -184,10 +184,14 @@ class EmojiPicker extends StatefulWidget {
/// EmojiPickerState /// EmojiPickerState
class EmojiPickerState extends State<EmojiPicker> { class EmojiPickerState extends State<EmojiPicker> {
final List<CategoryEmoji> _categoryEmoji = List.empty(growable: true); List<CategoryEmoji> _categoryEmoji = List.empty(growable: true);
List<RecentEmoji> _recentEmoji = List.empty(growable: true); List<RecentEmoji> _recentEmoji = List.empty(growable: true);
late EmojiViewState _state; late EmojiViewState _state;
// Identifies the newest asynchronous emoji load so an older load cannot
// replace data produced for a more recent config.
int _emojiLoadGeneration = 0;
// Prevent emojis to be reloaded with every build // Prevent emojis to be reloaded with every build
bool _loaded = false; bool _loaded = false;
@ -243,8 +247,10 @@ class EmojiPickerState extends State<EmojiPicker> {
if (oldWidget.config.height != widget.config.height) { if (oldWidget.config.height != widget.config.height) {
_currentPickerHeight = widget.config.height; _currentPickerHeight = widget.config.height;
} }
if (oldWidget.config != widget.config) { if (_emojiDataConfigChanged(oldWidget.config, widget.config)) {
// Config changed - rebuild EmojiPickerView completely // Only reload data when a setting that affects the category contents
// changes. Visual config and callback changes already flow through the
// normal widget rebuild and must not start overlapping async loads.
_loaded = false; _loaded = false;
_updateEmojis(); _updateEmojis();
} }
@ -264,6 +270,15 @@ class EmojiPickerState extends State<EmojiPicker> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
bool _emojiDataConfigChanged(Config oldConfig, Config newConfig) {
return oldConfig.locale != newConfig.locale ||
oldConfig.emojiSet != newConfig.emojiSet ||
oldConfig.checkPlatformCompatibility !=
newConfig.checkPlatformCompatibility ||
oldConfig.categoryViewConfig.recentTabBehavior !=
newConfig.categoryViewConfig.recentTabBehavior;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.alternateView != null) { if (widget.alternateView != null) {
@ -445,33 +460,44 @@ class EmojiPickerState extends State<EmojiPicker> {
// Initialize emoji data // Initialize emoji data
Future<void> _updateEmojis() async { Future<void> _updateEmojis() async {
_categoryEmoji.clear(); final loadGeneration = ++_emojiLoadGeneration;
final config = widget.config;
final categoryEmoji = <CategoryEmoji>[];
var recentEmoji = _recentEmoji;
if ([ if ([
RecentTabBehavior.RECENT, RecentTabBehavior.RECENT,
RecentTabBehavior.POPULAR, RecentTabBehavior.POPULAR,
].contains(widget.config.categoryViewConfig.recentTabBehavior)) { ].contains(config.categoryViewConfig.recentTabBehavior)) {
final futureOrRecent = _emojiPickerInternalUtils.getRecentEmojis(); final futureOrRecent = _emojiPickerInternalUtils.getRecentEmojis();
_recentEmoji = futureOrRecent is List<RecentEmoji> recentEmoji = futureOrRecent is List<RecentEmoji>
? futureOrRecent ? futureOrRecent
: await futureOrRecent; : await futureOrRecent;
final recentEmojiMap = _recentEmoji.map((e) => e.emoji).toList(); final recentEmojiMap = recentEmoji.map((e) => e.emoji).toList();
_categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap)); categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
} }
final data = final data =
widget.config.emojiSet?.call(widget.config.locale) ?? config.emojiSet?.call(config.locale) ??
getDefaultEmojiLocale(widget.config.locale); getDefaultEmojiLocale(config.locale);
if (widget.config.checkPlatformCompatibility) { if (config.checkPlatformCompatibility) {
final futureOrCategories = _emojiPickerInternalUtils.filterUnsupported( final futureOrCategories = _emojiPickerInternalUtils.filterUnsupported(
data, data,
); );
_categoryEmoji.addAll( categoryEmoji.addAll(
futureOrCategories is List<CategoryEmoji> futureOrCategories is List<CategoryEmoji>
? futureOrCategories ? futureOrCategories
: await futureOrCategories, : await futureOrCategories,
); );
} else { } else {
_categoryEmoji.addAll(data); categoryEmoji.addAll(data);
} }
if (!mounted || loadGeneration != _emojiLoadGeneration) {
return;
}
_recentEmoji = recentEmoji;
_categoryEmoji = categoryEmoji;
_state = EmojiViewState( _state = EmojiViewState(
_categoryEmoji, _categoryEmoji,
_onEmojiSelected, _onEmojiSelected,

View file

@ -22,7 +22,7 @@ class DefaultEmojiPickerView extends EmojiPickerView {
} }
class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView> class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
with SingleTickerProviderStateMixin, SkinToneOverlayStateMixin { with TickerProviderStateMixin, SkinToneOverlayStateMixin {
late TabController _tabController; late TabController _tabController;
late PageController _legacyPageController; late PageController _legacyPageController;
final _scrollController = ScrollController(); final _scrollController = ScrollController();
@ -39,12 +39,65 @@ class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
@override @override
void initState() { void initState() {
// Use controller's current category if available, final initCategory = _createCategoryControllers();
// otherwise use config's initCategory _scrollController.addListener(_onEmojiScroll);
// Listen to programmatic category changes from controller
widget.state.categoryNavigationNotifier.addListener(
_onCategoryNavigationChanged,
);
_loadRememberedSkinTone();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_scrollToCategory(initCategory, animate: false);
}
});
super.initState();
}
@override
void didUpdateWidget(covariant DefaultEmojiPickerView oldWidget) {
super.didUpdateWidget(oldWidget);
if (identical(oldWidget.state, widget.state)) {
return;
}
final previousCategory =
_activeCategoryIndex < oldWidget.state.categoryEmoji.length
? oldWidget.state.categoryEmoji[_activeCategoryIndex].category
: null;
oldWidget.state.categoryNavigationNotifier.removeListener(
_onCategoryNavigationChanged,
);
closeSkinToneOverlay();
_tabController.dispose();
_legacyPageController.dispose();
_categoryOffsets = const [];
_isNavigatingToCategory = false;
final initCategory = _createCategoryControllers(
preferredCategory: previousCategory,
);
widget.state.categoryNavigationNotifier.addListener(
_onCategoryNavigationChanged,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_scrollToCategory(initCategory, animate: false);
}
});
}
int _createCategoryControllers({Category? preferredCategory}) {
// Prefer an externally controlled category, then preserve the current
// category across data reloads, and finally fall back to the config.
final targetCategory = final targetCategory =
widget.state.currentCategory ?? widget.state.currentCategory ??
preferredCategory ??
widget.config.categoryViewConfig.initCategory; widget.config.categoryViewConfig.initCategory;
var initCategory = widget.state.categoryEmoji.indexWhere( var initCategory = widget.state.categoryEmoji.indexWhere(
(element) => element.category == targetCategory, (element) => element.category == targetCategory,
); );
@ -62,22 +115,7 @@ class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
initialPage: initCategory, initialPage: initCategory,
onPageRequested: _scrollToCategory, onPageRequested: _scrollToCategory,
); );
_scrollController.addListener(_onEmojiScroll); return initCategory;
// Listen to programmatic category changes from controller
widget.state.categoryNavigationNotifier.addListener(
_onCategoryNavigationChanged,
);
_loadRememberedSkinTone();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_scrollToCategory(initCategory, animate: false);
}
});
super.initState();
} }
void _loadRememberedSkinTone() { void _loadRememberedSkinTone() {
@ -110,6 +148,7 @@ class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
_onCategoryNavigationChanged, _onCategoryNavigationChanged,
); );
closeSkinToneOverlay(); closeSkinToneOverlay();
_tabController.dispose();
_legacyPageController.dispose(); _legacyPageController.dispose();
_scrollController.removeListener(_onEmojiScroll); _scrollController.removeListener(_onEmojiScroll);
_scrollController.dispose(); _scrollController.dispose();

View file

@ -368,21 +368,19 @@ class _PlanCardState extends State<PlanCard> {
case 'Free': case 'Free':
features = [context.lang.freeFeature1]; features = [context.lang.freeFeature1];
case 'Plus': case 'Plus':
features = [context.lang.plusFeature1]; //, context.lang.plusFeature2]; features = [];
case 'Tester': case 'Tester':
case 'Pro': case 'Pro':
features = [ features = [
context.lang.proFeature1,
context.lang.proFeature2,
context.lang.proFeature3, context.lang.proFeature3,
context.lang.proFeature2,
context.lang.proFeature4, context.lang.proFeature4,
context.lang.subscriptionAllApps, context.lang.subscriptionAllApps,
]; ];
case 'Family': case 'Family':
features = [ features = [
context.lang.familyFeature1,
context.lang.familyFeature2,
context.lang.familyFeature3, context.lang.familyFeature3,
context.lang.familyFeature2,
context.lang.familyFeature4, context.lang.familyFeature4,
context.lang.subscriptionAllApps, context.lang.subscriptionAllApps,
]; ];

View file

@ -17,6 +17,13 @@ use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
// Temporary companion guard for the server's iOS 0.6.x wake-up workaround.
// The server can bound APNs attempts, but only this device-side query can bound
// events that were already downloaded into the local notification outbox.
// Keep Android's existing batch size unchanged.
#[cfg(target_os = "ios")]
const MAX_BATCH_SIZE: i64 = 40;
#[cfg(not(target_os = "ios"))]
const MAX_BATCH_SIZE: i64 = 100; const MAX_BATCH_SIZE: i64 = 100;
const EN_ARB: &str = include_str!("../../../lib/src/localization/translations/en.arb"); const EN_ARB: &str = include_str!("../../../lib/src/localization/translations/en.arb");
const DE_ARB: &str = include_str!("../../../lib/src/localization/translations/de.arb"); const DE_ARB: &str = include_str!("../../../lib/src/localization/translations/de.arb");

View file

@ -6,9 +6,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:twonly/src/visual/components/emoji_picker/emoji_picker.dart'; import 'package:twonly/src/visual/components/emoji_picker/emoji_picker.dart';
import 'package:twonly/src/visual/components/emoji_picker/emoji_picker_internal_utils.dart'; import 'package:twonly/src/visual/components/emoji_picker/emoji_picker_internal_utils.dart';
List<CategoryEmoji> _duplicatedEmojiSet(Locale _) => [
...defaultEmojiSet,
...defaultEmojiSet,
];
// Use for golden tests, helpful in debugging // Use for golden tests, helpful in debugging
// await expectLater( // await expectLater(
// find.byType(MaterialApp), // find.byType(MaterialApp),
@ -18,7 +24,10 @@ import 'package:twonly/src/visual/components/emoji_picker/emoji_picker_internal_
void main() { void main() {
group('EmojiPicker Tests', () { group('EmojiPicker Tests', () {
// Caches are static and would otherwise leak between test cases. // Caches are static and would otherwise leak between test cases.
setUp(EmojiPickerInternalUtils.resetCaches); setUp(() {
SharedPreferences.setMockInitialValues({});
EmojiPickerInternalUtils.resetCaches();
});
testWidgets('Should allow user to select an emoji', ( testWidgets('Should allow user to select an emoji', (
tester, tester,
@ -135,6 +144,76 @@ void main() {
expect(scrollable.position.pixels, greaterThan(0)); expect(scrollable.position.pixels, greaterThan(0));
}); });
testWidgets('Recreates category navigation when the emoji set changes', (
tester,
) async {
var config = const Config(checkPlatformCompatibility: false);
late StateSetter setHarnessState;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (context, setState) {
setHarnessState = setState;
return EmojiPicker(config: config);
},
),
),
),
);
await tester.pumpAndSettle();
var tabBar = tester.widget<TabBar>(find.byType(TabBar));
expect(tabBar.controller!.length, 9);
expect(tabBar.tabs.length, 9);
setHarnessState(() {
config = const Config(
checkPlatformCompatibility: false,
emojiSet: _duplicatedEmojiSet,
);
});
await tester.pumpAndSettle();
tabBar = tester.widget<TabBar>(find.byType(TabBar));
expect(tabBar.controller!.length, 17);
expect(tabBar.tabs.length, 17);
});
testWidgets('Keeps only the newest emoji load when configs overlap', (
tester,
) async {
var config = const Config(checkPlatformCompatibility: false);
late StateSetter setHarnessState;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (context, setState) {
setHarnessState = setState;
return EmojiPicker(config: config);
},
),
),
),
);
// Change the config while the first recent-emoji read is still pending.
setHarnessState(() {
config = const Config(
checkPlatformCompatibility: false,
emojiSet: _duplicatedEmojiSet,
);
});
await tester.pumpAndSettle();
final tabBar = tester.widget<TabBar>(find.byType(TabBar));
expect(tabBar.controller!.length, 17);
expect(tabBar.tabs.length, 17);
});
testWidgets('Drag handle grows picker from its configured height', ( testWidgets('Drag handle grows picker from its configured height', (
tester, tester,
) async { ) async {