mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
454 lines
15 KiB
Dart
454 lines
15 KiB
Dart
/*
|
|
* Modified version of https://github.com/Fintasys/emoji_picker_flutter
|
|
* MIT License
|
|
* Copyright (c) 2024 Stefan Humm
|
|
*/
|
|
|
|
import 'package:flutter/material.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_internal_utils.dart';
|
|
|
|
List<CategoryEmoji> _duplicatedEmojiSet(Locale _) => [
|
|
...defaultEmojiSet,
|
|
...defaultEmojiSet,
|
|
];
|
|
|
|
// Use for golden tests, helpful in debugging
|
|
// await expectLater(
|
|
// find.byType(MaterialApp),
|
|
// matchesGoldenFile('overlay.png'),
|
|
// );
|
|
|
|
void main() {
|
|
group('EmojiPicker Tests', () {
|
|
// Caches are static and would otherwise leak between test cases.
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
EmojiPickerInternalUtils.resetCaches();
|
|
});
|
|
|
|
testWidgets('Should allow user to select an emoji', (
|
|
tester,
|
|
) async {
|
|
final controller0 = TextEditingController();
|
|
Emoji? emojiSelected;
|
|
Category? categorySelected;
|
|
|
|
// Build our app and trigger a frame.
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: EmojiPicker(
|
|
textEditingController: controller0,
|
|
onEmojiSelected: (category, emoji) {
|
|
emojiSelected = emoji;
|
|
categorySelected = category;
|
|
},
|
|
config: const Config(
|
|
categoryViewConfig: CategoryViewConfig(
|
|
recentTabBehavior: RecentTabBehavior.NONE,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Wait for the emojis to load if they are being loaded asynchronously
|
|
await tester.pumpAndSettle();
|
|
|
|
// Find an emoji in the picker
|
|
final emoji = find.text('🙂').hitTestable();
|
|
|
|
// Verify if we can find the emoji
|
|
expect(emoji, findsOneWidget);
|
|
|
|
// Tap on the emoji, this should trigger the selection action
|
|
await tester.tap(emoji);
|
|
|
|
// Call pumpAndSettle in case the UI needs to settle after an interaction
|
|
await tester.pumpAndSettle();
|
|
|
|
// Check if the emoji is added to the text controller
|
|
expect(controller0.text, contains('🙂'));
|
|
|
|
// Check if the emoji been passed to the 'onEmojiSelected' callback
|
|
expect(
|
|
emojiSelected,
|
|
equals(const Emoji('🙂', 'face | happy | slightly | smile | smiling')),
|
|
);
|
|
|
|
// Check if the category been passed to the 'onEmojiSelected' callback
|
|
expect(categorySelected, equals(Category.SMILEYS));
|
|
});
|
|
|
|
testWidgets('Shows the redesigned header and one continuous emoji list', (
|
|
tester,
|
|
) async {
|
|
var stickerPressed = false;
|
|
final controller = EmojiPickerController(
|
|
initialCategory: Category.SMILEYS,
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: EmojiPicker(
|
|
controller: controller,
|
|
config: Config(
|
|
height: 400,
|
|
categoryViewConfig: const CategoryViewConfig(
|
|
recentTabBehavior: RecentTabBehavior.NONE,
|
|
),
|
|
bottomActionBarConfig: BottomActionBarConfig(
|
|
onStickerButtonPressed: () => stickerPressed = true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('emojiPickerDragHandle')), findsOneWidget);
|
|
expect(find.text('Emoji'), findsOneWidget);
|
|
expect(find.text('Sticker'), findsOneWidget);
|
|
expect(find.byType(CustomScrollView), findsOneWidget);
|
|
expect(find.byType(PageView), findsNothing);
|
|
expect(const EmojiViewConfig().categorySpacing, 12);
|
|
expect(const ResizeConfig().showDragHandle, isTrue);
|
|
expect(const ResizeConfig().handleWidth, 60);
|
|
expect(const ResizeConfig().handleHeight, 3);
|
|
expect(const ResizeConfig().handleHitAreaHeight, 23);
|
|
expect(const ResizeConfig().handleBorderRadius, 32);
|
|
|
|
final tabBar = tester.widget<TabBar>(find.byType(TabBar));
|
|
expect(tabBar.indicator, isA<BoxDecoration>());
|
|
expect(tabBar.dividerColor, Colors.transparent);
|
|
|
|
await tester.tap(find.text('Sticker'));
|
|
expect(stickerPressed, isTrue);
|
|
|
|
await tester.tap(find.byIcon(const CategoryIcons().flagIcon));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.currentCategory, Category.FLAGS);
|
|
final scrollable = tester.state<ScrollableState>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('emojiScrollView')),
|
|
matching: find.byType(Scrollable),
|
|
),
|
|
);
|
|
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', (
|
|
tester,
|
|
) async {
|
|
var showing = true;
|
|
late StateSetter setHarnessState;
|
|
final pickerController = EmojiPickerController();
|
|
const pickerColor = Color(0xFF123456);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData.dark(),
|
|
home: Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (context, setState) {
|
|
setHarnessState = setState;
|
|
return Offstage(
|
|
offstage: !showing,
|
|
child: Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: EmojiPicker(
|
|
controller: pickerController,
|
|
config: const Config(
|
|
resizeConfig: ResizeConfig(maxHeight: 420),
|
|
emojiViewConfig: EmojiViewConfig(
|
|
backgroundColor: pickerColor,
|
|
),
|
|
categoryViewConfig: CategoryViewConfig(
|
|
recentTabBehavior: RecentTabBehavior.NONE,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final picker = find.byKey(const Key('emojiPickerResizableContainer'));
|
|
expect(tester.getSize(picker).height, 256);
|
|
|
|
final handleBackground = tester.widget<DecoratedBox>(
|
|
find.byKey(const Key('emojiPickerDragHandleBackground')),
|
|
);
|
|
final handleDecoration = handleBackground.decoration as BoxDecoration;
|
|
expect(handleDecoration.color, pickerColor);
|
|
expect(
|
|
handleDecoration.borderRadius,
|
|
const BorderRadius.vertical(top: Radius.circular(18)),
|
|
);
|
|
final dragIndicator = tester.widget<Container>(
|
|
find.byKey(const Key('emojiPickerDragIndicator')),
|
|
);
|
|
final dragDecoration = dragIndicator.decoration! as BoxDecoration;
|
|
expect(dragDecoration.color, ThemeData.dark().colorScheme.outline);
|
|
expect(dragDecoration.borderRadius, BorderRadius.circular(32));
|
|
|
|
await tester.drag(
|
|
find.byKey(const Key('emojiPickerDragHandle')),
|
|
const Offset(0, -100),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.getSize(picker).height, 356);
|
|
|
|
setHarnessState(() => showing = false);
|
|
await tester.pump();
|
|
setHarnessState(() => showing = true);
|
|
await tester.pump();
|
|
|
|
expect(tester.getSize(picker).height, 256);
|
|
});
|
|
|
|
testWidgets('Drag handle can be hidden', (tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: EmojiPicker(
|
|
config: Config(
|
|
resizeConfig: ResizeConfig(showDragHandle: false),
|
|
categoryViewConfig: CategoryViewConfig(
|
|
recentTabBehavior: RecentTabBehavior.NONE,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('emojiPickerDragHandle')), findsNothing);
|
|
expect(
|
|
tester
|
|
.getSize(find.byKey(const Key('emojiPickerResizableContainer')))
|
|
.height,
|
|
256,
|
|
);
|
|
});
|
|
|
|
testWidgets('Should allow to select an emoji with skintone on longPress', (
|
|
tester,
|
|
) async {
|
|
final controller0 = TextEditingController();
|
|
final utils = EmojiPickerUtils();
|
|
const emoji = Emoji('👍', 'Thumbs Up', hasSkinTone: true);
|
|
Emoji? emojiSelected;
|
|
Category? categorySelected;
|
|
|
|
// Build our app and trigger a frame.
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Padding(
|
|
padding: const EdgeInsets.only(top: 64),
|
|
child: EmojiPicker(
|
|
textEditingController: controller0,
|
|
onEmojiSelected: (category, emoji) {
|
|
emojiSelected = emoji;
|
|
categorySelected = category;
|
|
},
|
|
config: const Config(
|
|
height: 500,
|
|
categoryViewConfig: CategoryViewConfig(
|
|
recentTabBehavior: RecentTabBehavior.NONE,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Wait for the emojis to load if they are being loaded asynchronously
|
|
await tester.pumpAndSettle();
|
|
|
|
// Find an emoji in the picker
|
|
final emojiToFind = find.text(emoji.emoji);
|
|
|
|
// Scroll until the emoji to be found appears.
|
|
await tester.dragUntilVisible(
|
|
emojiToFind,
|
|
find.byKey(const Key('emojiScrollView')),
|
|
const Offset(0, -300),
|
|
);
|
|
|
|
// Verify if we can find the emoji
|
|
expect(emojiToFind, findsOneWidget);
|
|
|
|
// Tap on the emoji, this should trigger the skintone overlay
|
|
await tester.longPress(emojiToFind);
|
|
|
|
// Call pumpAndSettle in case the UI needs to settle after an interaction
|
|
await tester.pumpAndSettle();
|
|
|
|
/// Check if all skin tones are rendered in overlay
|
|
Finder? skinToneVariantToFind;
|
|
for (var i = 0; i < SkinTone.values.length; i++) {
|
|
skinToneVariantToFind = find.text(
|
|
utils.applySkinTone(emoji, SkinTone.values[i]).emoji,
|
|
);
|
|
// Verify if we can find the skintone variant
|
|
expect(skinToneVariantToFind, findsOneWidget);
|
|
}
|
|
|
|
// Tap on the emoji, this should trigger the selection action
|
|
await tester.tap(skinToneVariantToFind!);
|
|
|
|
// Check if the emoji is added to the text controller
|
|
expect(controller0.text, contains('👍🏿'));
|
|
|
|
// Check if the emoji been passed to the 'onEmojiSelected' callback
|
|
expect(emojiSelected?.emoji, equals('👍🏿'));
|
|
expect(
|
|
emojiSelected?.name,
|
|
equals('+1 | good | hand | like | thumb | up | yes'),
|
|
);
|
|
expect(emojiSelected?.hasSkinTone, equals(true));
|
|
|
|
// Check if the category been passed to the 'onEmojiSelected' callback
|
|
expect(categorySelected, equals(Category.SMILEYS));
|
|
});
|
|
|
|
testWidgets('Clips overflow when constrained tighter than natural height', (
|
|
tester,
|
|
) async {
|
|
final controller0 = TextEditingController();
|
|
|
|
// Constrain the picker far below the natural sum of the category bar
|
|
// and bottom action bar to force the inner Column to overflow (#256).
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SizedBox(
|
|
width: 300,
|
|
height: 30,
|
|
child: EmojiPicker(
|
|
textEditingController: controller0,
|
|
config: const Config(
|
|
height: 30,
|
|
categoryViewConfig: CategoryViewConfig(
|
|
recentTabBehavior: RecentTabBehavior.NONE,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
// A RenderFlex overflow is reported in debug mode; drain it so the test
|
|
// can assert the overflow is clipped rather than left painting stripes.
|
|
final exception = tester.takeException();
|
|
expect(
|
|
exception == null || exception.toString().contains('overflowed'),
|
|
isTrue,
|
|
);
|
|
|
|
// The inner view Column is wrapped in a ClipRect that clips the
|
|
// overflow (#256). Match that specific ClipRect (the one whose direct
|
|
// child is the Column) rather than the incidental ClipRects the grid
|
|
// and scroll views introduce.
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(EmojiContainer),
|
|
matching: find.byWidgetPredicate(
|
|
(widget) => widget is ClipRect && widget.child is Column,
|
|
),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
});
|
|
}
|