Skip to content

Commit

Permalink
feat: wallet lister (#745)
Browse files Browse the repository at this point in the history
* fix: add unique hero tags

* feat: add voices wallet tile

* refactor: cleanup tests

* refactor: rename icon to iconSrc
  • Loading branch information
dtscalac authored Sep 3, 2024
1 parent 2e42134 commit 3a03d8b
Show file tree
Hide file tree
Showing 8 changed files with 393 additions and 25 deletions.
70 changes: 70 additions & 0 deletions catalyst_voices/lib/widgets/menu/voices_wallet_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:catalyst_cardano/catalyst_cardano.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:flutter/material.dart';

/// A replacement for the [ListTile] with customized
/// styling that displays a [CardanoWallet].
class VoicesWalletTile extends StatelessWidget {
/// URI or base64 encoded icon of the wallet extension.
final String? iconSrc;

/// The name of the wallet extension.
final Widget? name;

/// A callback called when the widget is pressed.
final VoidCallback? onTap;

/// The default constructor for the [VoicesWalletTile].
const VoicesWalletTile({
super.key,
this.iconSrc,
this.name,
this.onTap,
});

@override
Widget build(BuildContext context) {
final icon = this.iconSrc;
final name = this.name;

return ListTile(
leading: SizedBox(
width: 40,
height: 40,
child: icon == null
? _IconPlaceholder()
: Image.network(
icon,
errorBuilder: (context, error, stackTrace) {
return _IconPlaceholder();
},
),
),
horizontalTitleGap: 16,
title: name == null
? null
: DefaultTextStyle(
style: Theme.of(context).textTheme.labelLarge!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: name,
),
trailing: Icon(
CatalystVoicesIcons.chevron_right,
size: 24,
),
onTap: onTap,
);
}
}

class _IconPlaceholder extends StatelessWidget {
const _IconPlaceholder();

@override
Widget build(BuildContext context) {
return CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
);
}
}
2 changes: 2 additions & 0 deletions catalyst_voices/lib/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export 'indicators/voices_linear_progress_indicator.dart';
export 'indicators/voices_status_indicator.dart';
export 'menu/voices_menu.dart';
export 'menu/voices_node_menu.dart';
export 'menu/voices_list_tile.dart';
export 'menu/voices_wallet_tile.dart';
export 'seed_phrase/seed_phrases_completer.dart';
export 'seed_phrase/seed_phrases_picker.dart';
export 'seed_phrase/seed_phrases_sequencer.dart';
Expand Down
13 changes: 5 additions & 8 deletions catalyst_voices/test/widgets/avatars/voices_avatar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import 'package:flutter_test/flutter_test.dart';

void main() {
group(VoicesAvatar, () {
testWidgets('VoicesAvatar renders with default properties',
(WidgetTester tester) async {
testWidgets('VoicesAvatar renders with default properties', (tester) async {
// Create the widget by wrapping it in a MaterialApp for theme access.
await tester.pumpWidget(
const MaterialApp(
Expand All @@ -28,7 +27,7 @@ void main() {
});

testWidgets('VoicesAvatar applies custom radius and padding',
(WidgetTester tester) async {
(tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
Expand Down Expand Up @@ -57,7 +56,7 @@ void main() {
});

testWidgets('VoicesAvatar uses custom foreground and background colors',
(WidgetTester tester) async {
(tester) async {
const foregroundColor = Colors.red;
const backgroundColor = Colors.green;

Expand Down Expand Up @@ -89,8 +88,7 @@ void main() {
expect(iconThemeWidget.data.color, foregroundColor);
});

testWidgets('VoicesAvatar calls onTap when tapped',
(WidgetTester tester) async {
testWidgets('VoicesAvatar calls onTap when tapped', (tester) async {
var tapped = false;

await tester.pumpWidget(
Expand All @@ -114,8 +112,7 @@ void main() {
expect(tapped, true);
});

testWidgets('VoicesAvatar applies theme colors by default',
(WidgetTester tester) async {
testWidgets('VoicesAvatar applies theme colors by default', (tester) async {
final customTheme = ThemeData(
colorScheme: const ColorScheme.light(
primary: Colors.blue,
Expand Down
171 changes: 171 additions & 0 deletions catalyst_voices/test/widgets/menu/voices_wallet_tile_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import 'package:catalyst_voices/widgets/menu/voices_wallet_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../helpers/pump_app.dart';

void main() {
group('VoicesWalletTile Tests', () {
testWidgets('renders correctly with icon and name', (tester) async {
const testName = Text('Test Wallet');

await tester.pumpApp(
Scaffold(
body: VoicesWalletTile(
iconSrc: _testIcon,
name: testName,
),
),
);
await tester.pumpAndSettle();

expect(find.byType(Image), findsOneWidget);
expect(find.byType(Text), findsOneWidget);
expect(find.text('Test Wallet'), findsOneWidget);
});

testWidgets('renders placeholder when icon is null', (tester) async {
await tester.pumpApp(
Scaffold(
body: VoicesWalletTile(
name: const Text('Test Wallet'),
),
),
);
await tester.pumpAndSettle();

expect(find.byType(_IconPlaceholder), findsOneWidget);
});

testWidgets('does not render name when it is null', (tester) async {
await tester.pumpApp(
Scaffold(
body: VoicesWalletTile(
iconSrc: _testIcon,
),
),
);
await tester.pumpAndSettle();

expect(find.byType(Text), findsNothing);
});

testWidgets('triggers onTap callback when tapped', (tester) async {
bool tapped = false;

await tester.pumpApp(
Scaffold(
body: VoicesWalletTile(
iconSrc: _testIcon,
name: const Text('Test Wallet'),
onTap: () {
tapped = true;
},
),
),
);
await tester.pumpAndSettle();

await tester.tap(find.byType(ListTile));
await tester.pumpAndSettle();

expect(tapped, isTrue);
});

testWidgets('shows placeholder on image load failure', (tester) async {
await tester.pumpApp(
Scaffold(
body: VoicesWalletTile(
iconSrc: 'https://example.com/non_existent_icon.png',
name: const Text('Test Wallet'),
),
),
);
await tester.pumpAndSettle();

// Simulate a loading error.
final imageFinder = find.byType(Image);
final imageWidget = tester.widget<Image>(imageFinder);
final errorBuilder = imageWidget.errorBuilder;
if (errorBuilder != null) {
await tester.pumpApp(
Scaffold(
body: errorBuilder(
tester.element(imageFinder),
Exception('Error'),
StackTrace.current,
),
),
);
}

expect(find.byType(_IconPlaceholder), findsOneWidget);
});
});
}

const _testIcon =
'data:image/png;base64,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'
'Jggg==';

typedef _IconPlaceholder = CircleAvatar;
27 changes: 10 additions & 17 deletions catalyst_voices/test/widgets/text_field/voices_text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import 'package:flutter_test/flutter_test.dart';

void main() {
group('VoicesTextField Widget Tests', () {
testWidgets('renders correctly with default parameters',
(WidgetTester tester) async {
testWidgets('renders correctly with default parameters', (tester) async {
await tester.pumpWidget(
const _MaterialApp(
child: VoicesTextField(),
Expand All @@ -18,8 +17,7 @@ void main() {
expect(find.byType(TextFormField), findsOneWidget);
});

testWidgets('displays label text when provided',
(WidgetTester tester) async {
testWidgets('displays label text when provided', (tester) async {
const labelText = 'Test Label';

await tester.pumpWidget(
Expand All @@ -34,8 +32,7 @@ void main() {
expect(find.text(labelText), findsOneWidget);
});

testWidgets('handles text input and updates controller',
(WidgetTester tester) async {
testWidgets('handles text input and updates controller', (tester) async {
final controller = TextEditingController();

await tester.pumpWidget(
Expand All @@ -53,8 +50,7 @@ void main() {
expect(controller.text, 'Hello World');
});

testWidgets('applies custom decorations correctly',
(WidgetTester tester) async {
testWidgets('applies custom decorations correctly', (tester) async {
const hintText = 'Enter your text here';
const errorText = 'Error message';

Expand All @@ -74,8 +70,7 @@ void main() {
expect(find.text(errorText), findsOneWidget);
});

testWidgets('validates input and displays error correctly',
(WidgetTester tester) async {
testWidgets('validates input and displays error correctly', (tester) async {
const errorText = 'Invalid input';

await tester.pumpWidget(
Expand All @@ -101,7 +96,7 @@ void main() {
});

testWidgets('displays correct suffix icon based on validation result',
(WidgetTester tester) async {
(tester) async {
await tester.pumpWidget(
_MaterialApp(
child: VoicesTextField(
Expand All @@ -118,7 +113,7 @@ void main() {
expect(find.byIcon(CatalystVoicesIcons.check_circle), findsOneWidget);
});

testWidgets('renders correctly when disabled', (WidgetTester tester) async {
testWidgets('renders correctly when disabled', (tester) async {
await tester.pumpWidget(
const _MaterialApp(
child: VoicesTextField(
Expand All @@ -137,8 +132,7 @@ void main() {
});

group('VoicesTextField Validator Logic Tests', () {
testWidgets('displays error when validation fails',
(WidgetTester tester) async {
testWidgets('displays error when validation fails', (tester) async {
const errorMessage = 'This field is required';

// Define a validator that returns an error if the input is empty
Expand Down Expand Up @@ -177,8 +171,7 @@ void main() {
expect(find.text(errorMessage), findsNothing);
});

testWidgets('displays success when validation passes',
(WidgetTester tester) async {
testWidgets('displays success when validation passes', (tester) async {
// Define a validator that always returns success
VoicesTextFieldValidationResult validator(value) {
return const VoicesTextFieldValidationResult(
Expand All @@ -203,7 +196,7 @@ void main() {
});

testWidgets('displays warning when validation returns warning',
(WidgetTester tester) async {
(tester) async {
const warningMessage = 'This is a warning';

// Define a validator that returns a warning for specific input
Expand Down
Loading

0 comments on commit 3a03d8b

Please sign in to comment.