mirror of
https://github.com/chenasraf/pantry-flutter.git
synced 2026-05-17 17:28:03 +00:00
test: cover list creation, share queues, and photo FAB menu
This commit is contained in:
@@ -80,7 +80,28 @@ class FakePhotoBoardController extends PhotoBoardController {
|
||||
Future<void> deletePhoto(Photo photo) async {}
|
||||
|
||||
@override
|
||||
Future<void> uploadPhotos(List<XFile> files, {int? folderId}) async {}
|
||||
Future<void> uploadPhotos(List<XFile> files, {int? folderId}) async {
|
||||
lastUploaded = files;
|
||||
lastUploadFolderId = folderId;
|
||||
}
|
||||
|
||||
String? lastCreatedFolderName;
|
||||
List<XFile>? lastUploaded;
|
||||
int? lastUploadFolderId;
|
||||
|
||||
@override
|
||||
Future<PhotoFolder> createFolder(String name) async {
|
||||
lastCreatedFolderName = name;
|
||||
final folder = PhotoFolder(
|
||||
id: 999,
|
||||
houseId: houseId,
|
||||
name: name,
|
||||
sortOrder: 0,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
);
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
/// A fake [NotesController] that does not touch any services.
|
||||
|
||||
47
test/services/pending_note_share_service_test.dart
Normal file
47
test/services/pending_note_share_service_test.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pantry/services/pending_note_share_service.dart';
|
||||
|
||||
void main() {
|
||||
final service = PendingNoteShareService.instance;
|
||||
|
||||
setUp(() {
|
||||
// Drain anything left over from a previous test.
|
||||
service.takeForHouse(1);
|
||||
service.takeForHouse(2);
|
||||
});
|
||||
|
||||
test('takeForHouse returns null when there is no pending share', () {
|
||||
expect(service.takeForHouse(1), isNull);
|
||||
});
|
||||
|
||||
test('push then takeForHouse returns the share and clears it', () {
|
||||
service.push(const PendingNoteShare(houseId: 1, content: 'hello'));
|
||||
|
||||
final taken = service.takeForHouse(1);
|
||||
expect(taken, isNotNull);
|
||||
expect(taken!.content, 'hello');
|
||||
|
||||
// Once taken, the pending slot is empty.
|
||||
expect(service.pending, isNull);
|
||||
expect(service.takeForHouse(1), isNull);
|
||||
});
|
||||
|
||||
test('takeForHouse for a different house returns null and keeps share', () {
|
||||
service.push(const PendingNoteShare(houseId: 1, content: 'hello'));
|
||||
|
||||
expect(service.takeForHouse(2), isNull);
|
||||
expect(service.pending, isNotNull);
|
||||
expect(service.pending!.houseId, 1);
|
||||
});
|
||||
|
||||
test('push notifies listeners', () {
|
||||
var notifications = 0;
|
||||
void listener() => notifications++;
|
||||
service.addListener(listener);
|
||||
|
||||
service.push(const PendingNoteShare(houseId: 1, content: 'hi'));
|
||||
|
||||
service.removeListener(listener);
|
||||
expect(notifications, 1);
|
||||
});
|
||||
}
|
||||
71
test/services/pending_photo_share_service_test.dart
Normal file
71
test/services/pending_photo_share_service_test.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pantry/services/pending_photo_share_service.dart';
|
||||
|
||||
void main() {
|
||||
final service = PendingPhotoShareService.instance;
|
||||
|
||||
// Singleton — drain anything left from a previous test before each case so
|
||||
// tests are order-independent.
|
||||
setUp(() {
|
||||
service.takeForHouse(1);
|
||||
service.takeForHouse(2);
|
||||
service.takeForHouse(3);
|
||||
});
|
||||
|
||||
test('takeForHouse returns empty list when nothing is queued', () {
|
||||
expect(service.takeForHouse(1), isEmpty);
|
||||
});
|
||||
|
||||
test('enqueue makes the share available to the matching house', () {
|
||||
service.enqueue(
|
||||
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
|
||||
);
|
||||
|
||||
final taken = service.takeForHouse(1);
|
||||
|
||||
expect(taken, hasLength(1));
|
||||
expect(taken.first.houseId, 1);
|
||||
expect(taken.first.paths, ['/a.jpg']);
|
||||
});
|
||||
|
||||
test('takeForHouse drains only entries for the requested house', () {
|
||||
service.enqueue(
|
||||
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
|
||||
);
|
||||
service.enqueue(
|
||||
const PendingPhotoShare(houseId: 2, folderId: 7, paths: ['/b.jpg']),
|
||||
);
|
||||
|
||||
final fromHouse1 = service.takeForHouse(1);
|
||||
expect(fromHouse1, hasLength(1));
|
||||
expect(fromHouse1.first.houseId, 1);
|
||||
|
||||
// House 2's entry is still there until claimed.
|
||||
final fromHouse2 = service.takeForHouse(2);
|
||||
expect(fromHouse2, hasLength(1));
|
||||
expect(fromHouse2.first.folderId, 7);
|
||||
expect(fromHouse2.first.paths, ['/b.jpg']);
|
||||
});
|
||||
|
||||
test('takeForHouse removes consumed entries (idempotent on second call)', () {
|
||||
service.enqueue(
|
||||
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
|
||||
);
|
||||
|
||||
expect(service.takeForHouse(1), hasLength(1));
|
||||
expect(service.takeForHouse(1), isEmpty);
|
||||
});
|
||||
|
||||
test('enqueue notifies listeners', () {
|
||||
var notifications = 0;
|
||||
void listener() => notifications++;
|
||||
service.addListener(listener);
|
||||
|
||||
service.enqueue(
|
||||
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
|
||||
);
|
||||
|
||||
service.removeListener(listener);
|
||||
expect(notifications, 1);
|
||||
});
|
||||
}
|
||||
140
test/widgets/checklist_selector_test.dart
Normal file
140
test/widgets/checklist_selector_test.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pantry/models/checklist.dart';
|
||||
import 'package:pantry/widgets/checklist_selector.dart';
|
||||
|
||||
import '../helpers/test_app.dart';
|
||||
import '../helpers/test_models.dart';
|
||||
|
||||
void main() {
|
||||
ChecklistList listA() =>
|
||||
makeChecklistList(id: 1, name: 'Groceries', icon: 'cart');
|
||||
ChecklistList listB() =>
|
||||
makeChecklistList(id: 2, name: 'Hardware', icon: 'wrench');
|
||||
|
||||
testWidgets('renders current list name in the closed state', (tester) async {
|
||||
final a = listA();
|
||||
final b = listB();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(
|
||||
ChecklistSelector(
|
||||
lists: [a, b],
|
||||
currentList: a,
|
||||
onSelected: (_) {},
|
||||
onCreateNew: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Groceries'), findsOneWidget);
|
||||
expect(find.text('Hardware'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('opening the dropdown shows all lists plus "+ New list"', (
|
||||
tester,
|
||||
) async {
|
||||
final a = listA();
|
||||
final b = listB();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(
|
||||
ChecklistSelector(
|
||||
lists: [a, b],
|
||||
currentList: a,
|
||||
onSelected: (_) {},
|
||||
onCreateNew: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(DropdownButtonFormField<int>));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Each list shows in the open menu in addition to the closed-state copy.
|
||||
expect(find.text('Groceries'), findsNWidgets(2));
|
||||
expect(find.text('Hardware'), findsOneWidget);
|
||||
expect(find.text('New list'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.add), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('selecting a different list invokes onSelected', (tester) async {
|
||||
final a = listA();
|
||||
final b = listB();
|
||||
ChecklistList? selected;
|
||||
var createNewCalls = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(
|
||||
ChecklistSelector(
|
||||
lists: [a, b],
|
||||
currentList: a,
|
||||
onSelected: (l) => selected = l,
|
||||
onCreateNew: () => createNewCalls++,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(DropdownButtonFormField<int>));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Hardware'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(selected?.id, 2);
|
||||
expect(createNewCalls, 0);
|
||||
});
|
||||
|
||||
testWidgets('selecting "+ New list" invokes onCreateNew and not onSelected', (
|
||||
tester,
|
||||
) async {
|
||||
final a = listA();
|
||||
final b = listB();
|
||||
ChecklistList? selected;
|
||||
var createNewCalls = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(
|
||||
ChecklistSelector(
|
||||
lists: [a, b],
|
||||
currentList: a,
|
||||
onSelected: (l) => selected = l,
|
||||
onCreateNew: () => createNewCalls++,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(DropdownButtonFormField<int>));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('New list'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(createNewCalls, 1);
|
||||
expect(selected, isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'after tapping "+ New list" the closed state still shows the current list',
|
||||
(tester) async {
|
||||
final a = listA();
|
||||
final b = listB();
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(
|
||||
ChecklistSelector(
|
||||
lists: [a, b],
|
||||
currentList: a,
|
||||
onSelected: (_) {},
|
||||
onCreateNew: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(DropdownButtonFormField<int>));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('New list'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Sentinel must not leak into the closed-state label.
|
||||
expect(find.text('New list'), findsNothing);
|
||||
expect(find.text('Groceries'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
118
test/widgets/photo_add_button_test.dart
Normal file
118
test/widgets/photo_add_button_test.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pantry/widgets/photo_add_button.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders a single main FAB closed by default', (tester) async {
|
||||
final controller = FakePhotoBoardController();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(PhotoAddButton(controller: controller)),
|
||||
);
|
||||
|
||||
expect(find.byType(FloatingActionButton), findsOneWidget);
|
||||
|
||||
// None of the action labels should be visible while closed.
|
||||
expect(find.text('Upload photos'), findsNothing);
|
||||
expect(find.text('Take photo'), findsNothing);
|
||||
expect(find.text('New folder'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('tapping the FAB opens the menu with all three actions', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = FakePhotoBoardController();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(PhotoAddButton(controller: controller)),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Upload photos'), findsOneWidget);
|
||||
expect(find.text('Take photo'), findsOneWidget);
|
||||
expect(find.text('New folder'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.add_photo_alternate), findsOneWidget);
|
||||
expect(find.byIcon(Icons.camera_alt), findsOneWidget);
|
||||
expect(find.byIcon(Icons.create_new_folder), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tapping the FAB a second time closes the menu', (tester) async {
|
||||
final controller = FakePhotoBoardController();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(PhotoAddButton(controller: controller)),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Take photo'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Take photo'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('tapping "New folder" opens the create-folder dialog', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = FakePhotoBoardController();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(PhotoAddButton(controller: controller)),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('New folder'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Dialog field label + the action buttons confirm the dialog opened.
|
||||
expect(find.text('Folder name'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Save'), findsOneWidget);
|
||||
expect(find.widgetWithText(TextButton, 'Cancel'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'submitting the create-folder dialog calls controller.createFolder',
|
||||
(tester) async {
|
||||
final controller = FakePhotoBoardController();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(PhotoAddButton(controller: controller)),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('New folder'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Folder name'),
|
||||
'Vacation',
|
||||
);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.lastCreatedFolderName, 'Vacation');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('empty folder name does not invoke createFolder', (tester) async {
|
||||
final controller = FakePhotoBoardController();
|
||||
await tester.pumpWidget(
|
||||
wrapForTest(PhotoAddButton(controller: controller)),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('New folder'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Don't type anything, just submit.
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.lastCreatedFolderName, isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user