mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
1import 'package:bloc_test/bloc_test.dart';
2import 'package:flutter/material.dart';
3import 'package:flutter_bloc/flutter_bloc.dart';
4import 'package:flutter_test/flutter_test.dart';
5import 'package:in_app_purchase/in_app_purchase.dart';
6import 'package:lazurite/features/tips/cubit/tip_cubit.dart';
7import 'package:lazurite/features/tips/cubit/tip_state.dart';
8import 'package:lazurite/features/tips/presentation/tip_sheet.dart';
9import 'package:mocktail/mocktail.dart';
10
11class MockTipCubit extends MockCubit<TipState> implements TipCubit {}
12
13void main() {
14 late MockTipCubit cubit;
15 late ProductDetails coffee;
16 late ProductDetails latte;
17
18 setUp(() {
19 cubit = MockTipCubit();
20 coffee = ProductDetails(
21 id: 'tip_coffee',
22 title: 'Coffee',
23 description: 'Small tip',
24 price: r'$1.99',
25 rawPrice: 1.99,
26 currencyCode: 'USD',
27 currencySymbol: r'$',
28 );
29 latte = ProductDetails(
30 id: 'tip_latte',
31 title: 'Latte',
32 description: 'Large tip',
33 price: r'$4.99',
34 rawPrice: 4.99,
35 currencyCode: 'USD',
36 currencySymbol: r'$',
37 );
38 registerFallbackValue(coffee);
39
40 when(() => cubit.loadProducts()).thenAnswer((_) async {});
41 when(() => cubit.purchaseTip(any())).thenAnswer((_) async {});
42 });
43
44 Widget buildSubject(TipState state) {
45 when(() => cubit.state).thenReturn(state);
46 whenListen(cubit, const Stream<TipState>.empty(), initialState: state);
47
48 return MaterialApp(
49 home: Scaffold(
50 body: BlocProvider<TipCubit>.value(value: cubit, child: const TipSheet()),
51 ),
52 );
53 }
54
55 testWidgets('renders loading skeletons while products load', (tester) async {
56 await tester.pumpWidget(buildSubject(const TipState(storeStatus: TipStoreStatus.loading)));
57
58 expect(find.byKey(const Key('tip_skeleton_0')), findsOneWidget);
59 expect(find.byKey(const Key('tip_skeleton_1')), findsOneWidget);
60 });
61
62 testWidgets('renders products with localized prices and ads note', (tester) async {
63 await tester.pumpWidget(
64 buildSubject(TipState(storeStatus: TipStoreStatus.available, products: [latte, coffee], adsRemoved: false)),
65 );
66
67 expect(find.text('Coffee'), findsOneWidget);
68 expect(find.text('Latte'), findsOneWidget);
69 expect(find.text(r'$1.99'), findsOneWidget);
70 expect(find.text(r'$4.99'), findsOneWidget);
71 expect(find.text('Your first tip removes ads forever.'), findsOneWidget);
72 });
73
74 testWidgets('renders thank-you banner when ads are already removed', (tester) async {
75 await tester.pumpWidget(
76 buildSubject(TipState(storeStatus: TipStoreStatus.available, products: [coffee, latte], adsRemoved: true)),
77 );
78
79 expect(find.text('Ads removed — thanks for your support!'), findsOneWidget);
80 expect(find.text('Your first tip removes ads forever.'), findsNothing);
81 });
82
83 testWidgets('renders store unavailable state and retries', (tester) async {
84 await tester.pumpWidget(buildSubject(const TipState(storeStatus: TipStoreStatus.unavailable)));
85
86 expect(find.text('Store unavailable'), findsOneWidget);
87
88 await tester.tap(find.text('Retry'));
89 await tester.pump();
90
91 verify(() => cubit.loadProducts()).called(1);
92 });
93
94 testWidgets('shows pending spinner and disables other purchases', (tester) async {
95 await tester.pumpWidget(
96 buildSubject(
97 TipState(
98 storeStatus: TipStoreStatus.available,
99 products: [coffee, latte],
100 purchaseStatus: TipPurchaseStatus.pending,
101 ),
102 ),
103 );
104
105 expect(find.byType(CircularProgressIndicator), findsNWidgets(2));
106 expect(find.byType(FilledButton), findsNothing);
107 });
108}