Files
OpenParcelBox/mobile-app/app/test/widget_test.dart
T

960 lines
28 KiB
Dart

import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:app/backup_service.dart';
import 'package:app/app_strings.dart';
import 'package:app/ble_controller.dart';
import 'package:app/main.dart';
import 'package:app/models.dart';
import 'package:app/secure_box_store.dart';
class _MemoryStore extends SecureBoxStore {
_MemoryStore({
this.language = 'en',
this.boxes = const <SavedBox>[],
this.lastBackupAt,
Map<String, Map<String, dynamic>>? cachedStates,
}) : cachedStates = cachedStates ?? <String, Map<String, dynamic>>{};
final String? language;
final List<SavedBox> boxes;
DateTime? lastBackupAt;
final Map<String, Map<String, dynamic>> cachedStates;
@override
Future<List<SavedBox>> loadBoxes() async => boxes;
@override
Future<String?> loadLanguage() async => language;
@override
Future<void> saveBoxes(List<SavedBox> boxes) async {}
@override
Future<Map<String, dynamic>?> loadCachedState(String identityKey) async {
final state = cachedStates[identityKey];
return state == null ? null : Map<String, dynamic>.from(state);
}
@override
Future<void> saveCachedState(
String identityKey,
Map<String, dynamic> state,
) async {
cachedStates[identityKey] = Map<String, dynamic>.from(state);
}
@override
Future<void> deleteCachedState(String identityKey) async {
cachedStates.remove(identityKey);
}
@override
Future<DateTime?> loadLastBackupAt() async => lastBackupAt;
@override
Future<void> saveLastBackupAt(DateTime date) async {
lastBackupAt = date;
}
}
class _FakeBackupService extends BackupService {
bool fileWasPicked = false;
@override
Future<Uint8List?> pickBackupFile() async {
fileWasPicked = true;
return Uint8List(0);
}
@override
Future<List<SavedBox>> importBoxes(Uint8List bytes, String password) async =>
<SavedBox>[];
}
class _SequenceRandom implements Random {
_SequenceRandom(this.values);
final List<int> values;
int _index = 0;
@override
int nextInt(int max) => values[_index++] % max;
@override
bool nextBool() => nextInt(2) == 1;
@override
double nextDouble() => nextInt(1 << 20) / (1 << 20);
}
class _RecordingController extends OpenParcelBoxBleController {
_RecordingController({required Random random})
: super(
store: _MemoryStore(),
reconnectOnInitialize: false,
random: random,
);
OpenParcelBoxCommand? lastCommand;
@override
Future<void> send(OpenParcelBoxCommand command, {bool refresh = true}) async {
lastCommand = command;
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('connection timeouts never trigger Android bond recovery', () {
final timeout = FlutterBluePlusException(
ErrorPlatform.fbp,
'connect',
FbpErrorCode.timeout.index,
'Timed out',
);
final encryptedGattFailure = FlutterBluePlusException(
ErrorPlatform.android,
'setNotifyValue',
5,
'Insufficient authentication',
);
expect(isRecoverableAndroidBondFailure(timeout), isFalse);
expect(isRecoverableAndroidBondFailure(encryptedGattFailure), isTrue);
});
const transparentPixel = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
setUpAll(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMessageHandler('flutter/assets', (ByteData? message) async {
final key = String.fromCharCodes(message!.buffer.asUint8List());
if (key == 'AssetManifest.bin') {
return const StandardMessageCodec().encodeMessage(<String, Object>{
'images/background.jpg': <Object>[
<String, Object>{'asset': 'images/background.jpg'},
],
'images/openparcelbox-logo-256.png': <Object>[
<String, Object>{'asset': 'images/openparcelbox-logo-256.png'},
],
});
}
if (key == 'images/background.jpg' ||
key == 'images/openparcelbox-logo-256.png') {
final bytes = Uint8List.fromList(transparentPixel);
return ByteData.sublistView(bytes);
}
return null;
});
});
testWidgets('startup splash shows the background and centered logo', (
WidgetTester tester,
) async {
await tester.pumpWidget(MyApp(store: _MemoryStore()));
expect(
find.image(const AssetImage('images/background.jpg')),
findsOneWidget,
);
expect(
find.image(const AssetImage('images/openparcelbox-logo-256.png')),
findsOneWidget,
);
await tester.pump(const Duration(milliseconds: 900));
});
testWidgets('empty home only exposes registration and restore actions', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MyApp(store: _MemoryStore(), splashDuration: Duration.zero),
);
await tester.pumpAndSettle();
expect(find.text('Add a new OpenParcelBox'), findsOneWidget);
expect(find.text('Restore a backup'), findsOneWidget);
expect(find.byIcon(Icons.lock_open), findsNothing);
expect(find.text('Permanent codes'), findsNothing);
final background = tester.widget<DecoratedBox>(
find.byKey(const ValueKey<String>('app-background')),
);
final decoration = background.decoration as BoxDecoration;
expect(decoration.image?.image, const AssetImage('images/background.jpg'));
});
test('saved guest identity names survive backup serialization', () {
const box = SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: '1234',
role: BoxRole.guest,
identityName: 'Alice',
);
final restored = SavedBox.fromJson(box.toJson());
expect(restored.identityName, 'Alice');
});
test('restored connection records can require BLE rediscovery', () {
const box = SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: '1234',
role: BoxRole.administrator,
);
final portable = box.copyWith(needsRediscovery: true);
final restored = SavedBox.fromJson(portable.toJson());
expect(restored.identityKey, box.identityKey);
expect(restored.needsRediscovery, isTrue);
});
test('guest invitations transfer the guest identity key', () {
final controller = OpenParcelBoxBleController(store: _MemoryStore())
..currentBox = const SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: 'ADMIN',
role: BoxRole.administrator,
);
const guest = GuestIdentity(name: 'Alice', key: 'GUEST-KEY');
final invitation =
jsonDecode(controller.invitationPayload(guest)) as Map<String, dynamic>;
expect(invitation['format'], 'openparcelbox-invite-v1');
expect(invitation['box_name'], 'Front gate');
expect(invitation['guest_name'], 'Alice');
expect(invitation['guest_key'], 'GUEST-KEY');
});
test('cached box state is available before BLE reconnection', () async {
final store = _MemoryStore(
boxes: const <SavedBox>[
SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: 'ADMIN',
role: BoxRole.administrator,
),
],
cachedStates: <String, Map<String, dynamic>>{
'ADMIN': <String, dynamic>{
'codes': <Map<String, dynamic>>[
<String, dynamic>{'slot': 0, 'code': '123456', 'kind': 'permanent'},
<String, dynamic>{'slot': 1, 'code': '654321', 'kind': 'one_time'},
],
'nfc_tags': <Map<String, dynamic>>[
<String, dynamic>{'slot': 0, 'uid': '60:4F:E2:B5', 'name': 'Alice'},
],
'history': <Map<String, dynamic>>[
<String, dynamic>{
'unix_ms': 1784318264772,
'kind': 0,
'actor': 'Administrator',
},
],
'guests': <Map<String, dynamic>>[
<String, dynamic>{'name': 'Bob', 'key': 'GUEST'},
],
},
},
);
final controller = OpenParcelBoxBleController(
store: store,
reconnectOnInitialize: false,
);
await controller.initialize();
expect(controller.permanentCodes.single.code, '123456');
expect(controller.oneTimeCodes.single.code, '654321');
expect(controller.nfcTags.single.name, 'Alice');
expect(controller.history.single.actor, 'Administrator');
expect(controller.guests.single.name, 'Bob');
controller.dispose();
});
test(
'generated codes skip every active permanent and temporary code',
() async {
final controller =
_RecordingController(
random: _SequenceRandom(<int>[123456, 654321, 42]),
)
..currentBox = const SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: 'ADMIN',
role: BoxRole.administrator,
)
..permanentCodes.add(
const AccessCode(
id: 'slot-0',
code: '123456',
kind: CredentialKind.permanent,
),
)
..oneTimeCodes.add(
const AccessCode(
id: 'slot-1',
code: '654321',
kind: CredentialKind.oneTime,
),
);
await controller.addGeneratedCode(CredentialKind.permanent);
expect(controller.lastCommand?.name, 'add_code');
expect(controller.lastCommand?.payload['code'], '000042');
expect(controller.lastCommand?.payload['exclusive'], 1);
controller.dispose();
},
);
test(
'manual permanent-code replacement uses the atomic BLE command',
() async {
final controller = _RecordingController(random: _SequenceRandom(<int>[0]))
..currentBox = const SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: 'ADMIN',
role: BoxRole.administrator,
);
const previous = AccessCode(
id: 'slot-0',
code: '123456',
kind: CredentialKind.permanent,
);
await controller.replacePermanentCode(previous, '654321');
expect(controller.lastCommand?.name, 'replace_code');
expect(controller.lastCommand?.payload['old_code'], '123456');
expect(controller.lastCommand?.payload['new_code'], '654321');
expect(controller.lastCommand?.payload['kind'], 'permanent');
controller.dispose();
},
);
testWidgets('code editor uses a numeric keyboard and repeated backspace', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: CodeEditorDialog(
title: 'Edit permanent code',
description: 'Enter six digits.',
initialCode: '123456',
cancelLabel: 'Cancel',
confirmLabel: 'Save',
),
),
),
);
await tester.pump();
final input = tester.widget<TextField>(
find.byKey(const ValueKey<String>('code-input')),
);
expect(input.keyboardType, TextInputType.number);
await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
await tester.pump();
final editedController = tester
.widget<TextField>(find.byKey(const ValueKey<String>('code-input')))
.controller;
expect(editedController?.text, '123');
expect(
editedController?.selection,
const TextSelection.collapsed(offset: 3),
);
expect(find.text('4'), findsNothing);
expect(find.text('5'), findsNothing);
expect(find.text('6'), findsNothing);
expect(tester.takeException(), isNull);
});
testWidgets('styled modal keeps its final row above Android navigation', (
WidgetTester tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => MediaQuery(
data: MediaQuery.of(
context,
).copyWith(viewPadding: const EdgeInsets.only(bottom: 24)),
child: child!,
),
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () => showOpbModal(
context,
title: 'Codes',
child: Column(
children: List<Widget>.generate(
8,
(index) => Container(
key: ValueKey<String>('modal-code-$index'),
height: 72,
margin: const EdgeInsets.only(bottom: 8),
),
),
),
),
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
final finalRow = find.byKey(const ValueKey<String>('modal-code-7'));
await tester.ensureVisible(finalRow);
await tester.pumpAndSettle();
expect(tester.getRect(finalRow).bottom, lessThanOrEqualTo(760));
expect(tester.takeException(), isNull);
});
test('guest logic rejects permanent-code mutations', () {
final controller = OpenParcelBoxBleController(store: _MemoryStore())
..currentBox = const SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: '1234',
role: BoxRole.guest,
);
expect(
() => controller.addGeneratedCode(CredentialKind.permanent),
throwsStateError,
);
expect(
() => controller.removeCode(
const AccessCode(
id: 'slot-0',
code: '123456',
kind: CredentialKind.permanent,
),
),
throwsStateError,
);
controller.dispose();
});
test('NFC enrollment state and commands use the two-step box flow', () async {
const box = SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: '1234',
role: BoxRole.administrator,
);
final controller = OpenParcelBoxBleController(
store: _MemoryStore(
boxes: const <SavedBox>[box],
cachedStates: <String, Map<String, dynamic>>{
box.identityKey: <String, dynamic>{
'nfc_enrollment': <String, dynamic>{
'active': false,
'uid': '60:4F:E2:B5',
'error': '',
},
},
},
),
reconnectOnInitialize: false,
);
await controller.initialize();
expect(controller.nfcEnrollmentUid, '60:4F:E2:B5');
expect(controller.nfcEnrollmentError, isNull);
controller.dispose();
final recording = _RecordingController(random: _SequenceRandom(<int>[]));
await recording.startNfcEnrollment();
expect(recording.lastCommand?.name, 'start_nfc_enrollment');
expect(recording.lastCommand?.payload, isEmpty);
await recording.cancelNfcEnrollment();
expect(recording.lastCommand?.name, 'cancel_nfc_enrollment');
recording.dispose();
});
testWidgets('NFC enrollment keeps errors visible then shows naming field', (
WidgetTester tester,
) async {
final controller = _RecordingController(random: _SequenceRandom(<int>[]))
..nfcEnrollmentError = 'timeout';
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NfcEnrollmentDialog(
controller: controller,
strings: AppStrings(const Locale('en')),
),
),
),
);
await tester.pump();
expect(
find.byKey(const ValueKey<String>('nfc-enrollment-modal')),
findsOneWidget,
);
expect(
find.byKey(const ValueKey<String>('nfc-enrollment-error')),
findsOneWidget,
);
expect(find.text('No tag was detected within 15 seconds.'), findsOneWidget);
controller
..nfcEnrollmentError = null
..nfcEnrollmentUid = '60:4F:E2:B5'
..notifyListeners();
await tester.pump();
expect(
find.byKey(const ValueKey<String>('nfc-enrollment-name')),
findsOneWidget,
);
expect(find.text('60:4F:E2:B5'), findsOneWidget);
await tester.pumpWidget(const SizedBox.shrink());
controller.dispose();
});
testWidgets('administrator compact actions share one bottom row', (
WidgetTester tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MyApp(
store: _MemoryStore(
lastBackupAt: null,
boxes: const <SavedBox>[
SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: '1234',
role: BoxRole.administrator,
),
],
),
splashDuration: Duration.zero,
),
);
await tester.pumpAndSettle();
final tags = tester.getRect(
find.byKey(const ValueKey<String>('compact-NFC tags')),
);
final guests = tester.getRect(
find.byKey(const ValueKey<String>('compact-Guests')),
);
final settings = tester.getRect(
find.byKey(const ValueKey<String>('compact-Settings')),
);
final quickActions = tester.getRect(
find.byKey(const ValueKey<String>('quick-actions')),
);
final codeActions = tester.getRect(
find.byKey(const ValueKey<String>('code-actions')),
);
expect(tags.top, guests.top);
expect(guests.top, settings.top);
expect(tags.width, guests.width);
expect(guests.width, settings.width);
expect(800 - settings.bottom, 20);
expect(codeActions.top - quickActions.bottom, 28);
await tester.tap(find.byKey(const ValueKey<String>('compact-Settings')));
await tester.pumpAndSettle();
expect(find.text('Last backup: never'), findsOneWidget);
expect(find.text('Factory-reset the box'), findsNothing);
expect(find.text('Disconnected'), findsOneWidget);
expect(find.text('Reconnect'), findsOneWidget);
await tester.ensureVisible(
find.widgetWithText(OutlinedButton, 'Forget the box'),
);
await tester.tap(find.widgetWithText(OutlinedButton, 'Forget the box'));
await tester.pumpAndSettle();
expect(
find.textContaining(
'This deletes the connection information, administrator identity, '
'and Bluetooth bond',
),
findsOneWidget,
);
expect(find.byType(CheckboxListTile), findsOneWidget);
expect(tester.widget<Checkbox>(find.byType(Checkbox)).value, isFalse);
expect(tester.widget<Checkbox>(find.byType(Checkbox)).onChanged, isNull);
expect(
find.text('This action is unavailable while offline.'),
findsOneWidget,
);
await tester.tap(find.byType(Checkbox));
await tester.pump();
expect(tester.widget<Checkbox>(find.byType(Checkbox)).value, isFalse);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('guest can read history and manage only temporary codes', (
WidgetTester tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MyApp(
store: _MemoryStore(
boxes: const <SavedBox>[
SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: '1234',
role: BoxRole.guest,
identityName: 'Alice',
),
],
),
splashDuration: Duration.zero,
),
);
await tester.pumpAndSettle();
final temporary = tester.getRect(
find.byKey(const ValueKey<String>('temporary-code-action')),
);
final permanent = tester.getRect(
find.byKey(const ValueKey<String>('permanent-code-action')),
);
expect(temporary.left, lessThan(permanent.left));
await tester.tap(find.text('History'));
await tester.pumpAndSettle();
expect(find.text('No opening has been recorded.'), findsOneWidget);
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey<String>('permanent-code-action')),
);
await tester.pumpAndSettle();
expect(find.textContaining('Generate a code'), findsNothing);
expect(find.text('Stored codes: 0/8'), findsOneWidget);
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey<String>('temporary-code-action')),
);
await tester.pumpAndSettle();
expect(find.textContaining('Generate a code'), findsOneWidget);
final generateButton = tester.widget<FilledButton>(
find.widgetWithText(FilledButton, 'Generate a code (0/20)'),
);
expect(generateButton.onPressed, isNull);
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey<String>('compact-Settings')));
await tester.pumpAndSettle();
expect(find.text('Firmware update (coming later)'), findsNothing);
expect(find.text('Factory-reset the box'), findsNothing);
await tester.ensureVisible(
find.widgetWithText(OutlinedButton, 'Forget the box'),
);
await tester.tap(find.widgetWithText(OutlinedButton, 'Forget the box'));
await tester.pumpAndSettle();
expect(
find.textContaining(
'This deletes the box connection information and Bluetooth bond '
'from this phone.',
),
findsOneWidget,
);
expect(find.byType(CheckboxListTile), findsNothing);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('permanent and temporary codes can be copied', (
WidgetTester tester,
) async {
String? clipboardText;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall call) async {
if (call.method == 'Clipboard.setData') {
clipboardText =
(call.arguments as Map<Object?, Object?>)['text'] as String?;
}
return null;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
),
);
await tester.binding.setSurfaceSize(const Size(400, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MyApp(
store: _MemoryStore(
boxes: const <SavedBox>[
SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: 'ADMIN',
role: BoxRole.administrator,
),
],
cachedStates: <String, Map<String, dynamic>>{
'ADMIN': <String, dynamic>{
'codes': <Map<String, dynamic>>[
<String, dynamic>{
'slot': 0,
'code': '123456',
'kind': 'permanent',
},
<String, dynamic>{
'slot': 1,
'code': '654321',
'kind': 'one_time',
},
],
},
},
),
splashDuration: Duration.zero,
),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey<String>('permanent-code-action')),
);
await tester.pumpAndSettle();
final copyButton = find.byTooltip('Copy code');
expect(copyButton, findsOneWidget);
expect(
tester.getRect(copyButton).left,
lessThan(tester.getRect(find.byTooltip('Edit code')).left),
);
await tester.tap(copyButton);
await tester.pump();
expect(clipboardText, '123456');
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey<String>('temporary-code-action')),
);
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Copy code'));
await tester.pump();
expect(clipboardText, '654321');
expect(tester.takeException(), isNull);
});
testWidgets('registration opens the styled setup modal', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MyApp(store: _MemoryStore(), splashDuration: Duration.zero),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Add a new OpenParcelBox'));
await tester.pumpAndSettle();
expect(find.text('Box identifier'), findsOneWidget);
expect(find.text('Scan a guest QR code'), findsOneWidget);
expect(find.text('Detected OpenParcelBox devices'), findsOneWidget);
expect(find.text('Add this box'), findsOneWidget);
final addButton = tester.widget<FilledButton>(
find.widgetWithText(FilledButton, 'Add this box'),
);
expect(addButton.onPressed, isNull);
expect(find.byIcon(Icons.close), findsOneWidget);
});
testWidgets('registration text field works with the French locale', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MyApp(
store: _MemoryStore(language: 'fr'),
splashDuration: Duration.zero,
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Ajouter une nouvelle OpenParcelBox'));
await tester.pumpAndSettle();
expect(find.byType(TextFormField), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets(
'closing registration after editing does not dispose input early',
(WidgetTester tester) async {
await tester.pumpWidget(
MyApp(store: _MemoryStore(), splashDuration: Duration.zero),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Add a new OpenParcelBox'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextFormField), 'Front gate');
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
expect(find.text('Add a new OpenParcelBox'), findsOneWidget);
expect(tester.takeException(), isNull);
},
);
testWidgets('restore picks a file before requesting its password', (
WidgetTester tester,
) async {
final backupService = _FakeBackupService();
await tester.pumpWidget(
MyApp(
store: _MemoryStore(),
backupService: backupService,
splashDuration: Duration.zero,
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Restore a backup'));
await tester.pumpAndSettle();
expect(backupService.fileWasPicked, isTrue);
expect(find.text('Password'), findsNWidgets(2));
expect(
find.text('Enter the password used when this backup was created.'),
findsOneWidget,
);
expect(
tester
.widget<FilledButton>(find.widgetWithText(FilledButton, 'Confirm'))
.onPressed,
isNull,
);
expect(
tester.widget<TextField>(find.byType(TextField)).obscureText,
isTrue,
);
await tester.tap(find.byIcon(Icons.visibility));
await tester.pump();
expect(
tester.widget<TextField>(find.byType(TextField)).obscureText,
isFalse,
);
expect(find.byIcon(Icons.visibility_off), findsOneWidget);
await tester.enterText(find.byType(TextField), 'secret');
await tester.pump();
await tester.tap(find.text('Confirm'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('Backup restored.'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}