From a1838e5b019b77f7db78e1b97b46f08c301b4dcf Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 3 Jan 2026 18:58:19 +0100 Subject: [PATCH] import export working --- .../domain/repositories/bookmarks.dart | 31 +++- .../domain/repositories/bookmarks.g.dart | 2 +- .../screens/bookmark_entry_edit.dart | 4 +- .../presentation/screens/bookmark_list.dart | 125 +++++++++++++ .../bookmarks/utils/bookmark_html_utils.dart | 5 +- .../bookmarks/utils/bookmark_json_utils.dart | 13 +- .../browser_modules/bottom_app_bar.dart | 4 +- .../utils/bookmark_html_utils_test.dart | 169 +++++++++++------- .../utils/bookmark_json_utils_test.dart | 128 +++++++------ 9 files changed, 343 insertions(+), 138 deletions(-) diff --git a/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart index 4d1561ed..61e5cff9 100644 --- a/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart +++ b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart @@ -21,12 +21,16 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:nullability/nullability.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/utils/bookmark_html_utils.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/utils/bookmark_json_utils.dart'; part 'bookmarks.g.dart'; @Riverpod(keepAlive: true) class BookmarksRepository extends _$BookmarksRepository { final _service = GeckoBookmarksService(); + late final _jsonUtils = BookmarkJSONUtils(_service); + late final _htmlUtils = BookmarkHTMLUtils(_service); Future addBookmark({ required String parentGuid, @@ -73,12 +77,31 @@ class BookmarksRepository extends _$BookmarksRepository { ref.invalidateSelf(); } + Future importFromJSON(String jsonString, {bool replace = false}) async { + final count = await _jsonUtils.importFromJSON(jsonString, replace: replace); + ref.invalidateSelf(); + return count; + } + + Future importFromHTML(String htmlString, {bool replace = false}) async { + final count = await _htmlUtils.importFromHTML(htmlString, replace: replace); + ref.invalidateSelf(); + return count; + } + + Future?> exportToJson({ + required BookmarkRoot root, + }) async { + return await _jsonUtils.exportToJson(root: root); + } + + Future exportToHTML({required BookmarkRoot root}) async { + return await _htmlUtils.exportToHTML(root: root); + } + @override Future build() async { - final node = await _service.getTree( - BookmarkRoot.mobile.id, - recursive: true, - ); + final node = await _service.getTree(BookmarkRoot.root.id, recursive: true); return node.mapNotNull(BookmarkItem.parseRecursive); } } diff --git a/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.g.dart b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.g.dart index e927b639..4014387c 100644 --- a/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.g.dart +++ b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.g.dart @@ -34,7 +34,7 @@ final class BookmarksRepositoryProvider } String _$bookmarksRepositoryHash() => - r'94d9f0f6c589455b16b0831de4ed1f87dcee704e'; + r'932441e926afcb1d60dc34b8a8756b7ad8fe0a27'; abstract class _$BookmarksRepository extends $AsyncNotifier { FutureOr build(); diff --git a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart index 2f042e65..9daf2761 100644 --- a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart +++ b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart @@ -48,7 +48,7 @@ class BookmarkEntryEditScreen extends HookConsumerWidget { final treeKey = useMemoized(() => GlobalKey()); final folderList = ref.watch( - bookmarksProvider(BookmarkRoot.mobile.id), + bookmarksProvider(BookmarkRoot.root.id), ); final nameTextController = useTextEditingController( @@ -242,7 +242,7 @@ class BookmarkEntryEditScreen extends HookConsumerWidget { // ignore: unused_result ref.refresh( bookmarksProvider( - BookmarkRoot.mobile.id, + BookmarkRoot.root.id, ), ); }, diff --git a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart index 1731281b..27f32d89 100644 --- a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart +++ b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart @@ -18,8 +18,11 @@ * along with this program. If not, see . */ import 'dart:convert'; +import 'dart:io'; import 'package:animated_tree_view/animated_tree_view.dart'; +import 'package:convert/convert.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; @@ -29,11 +32,13 @@ import 'package:nullability/nullability.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart'; import 'package:weblibre/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart'; import 'package:weblibre/presentation/hooks/listenable_callback.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; +import 'package:weblibre/utils/ui_helper.dart'; class BookmarkListScreen extends HookConsumerWidget { final String entryGuid; @@ -101,6 +106,38 @@ class BookmarkListScreen extends HookConsumerWidget { }); }, ), + SubmenuButton( + leadingIcon: const Icon(MdiIcons.import), + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(MdiIcons.codeJson), + child: const Text('JSON'), + onPressed: () => _handleImport(context, ref, 'json'), + ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.xml), + child: const Text('HTML'), + onPressed: () => _handleImport(context, ref, 'html'), + ), + ], + child: const Text('Import'), + ), + SubmenuButton( + leadingIcon: const Icon(MdiIcons.export), + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(MdiIcons.codeJson), + child: const Text('JSON'), + onPressed: () => _handleExport(context, ref, 'json'), + ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.xml), + child: const Text('HTML'), + onPressed: () => _handleExport(context, ref, 'html'), + ), + ], + child: const Text('Export'), + ), ], builder: (context, controller, child) => IconButton( onPressed: () { @@ -148,6 +185,7 @@ class BookmarkListScreen extends HookConsumerWidget { return TreeView.simple( key: treeKey, tree: root, + showRootNode: entryGuid != BookmarkRoot.root.id, onTreeReady: (controller) { if (textFilterEnabled.value) { controller.expandAllChildren(root, recursive: true); @@ -299,4 +337,91 @@ class BookmarkListScreen extends HookConsumerWidget { ), ); } + + Future _handleImport( + BuildContext context, + WidgetRef ref, + String format, + ) async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: format == 'json' ? ['json'] : ['html', 'htm'], + ); + + if (result == null || result.files.isEmpty) return; + + final file = result.files.first; + if (file.path == null) { + if (context.mounted) { + showErrorMessage(context, 'Failed to read file'); + } + return; + } + + final content = await File(file.path!).readAsString(); + final repository = ref.read(bookmarksRepositoryProvider.notifier); + + final count = format == 'json' + ? await repository.importFromJSON(content) + : await repository.importFromHTML(content); + + if (context.mounted) { + showInfoMessage(context, 'Imported $count bookmarks successfully'); + } + } catch (e) { + if (context.mounted) { + showErrorMessage(context, 'Import failed: $e'); + } + } + } + + Future _handleExport( + BuildContext context, + WidgetRef ref, + String format, + ) async { + try { + // Create the backup content first + final repository = ref.read(bookmarksRepositoryProvider.notifier); + + String content; + if (format == 'json') { + final data = await repository.exportToJson(root: BookmarkRoot.root); + if (data == null) { + throw Exception('Failed to export bookmarks'); + } + content = const JsonEncoder.withIndent(' ').convert(data); + } else { + content = await repository.exportToHTML(root: BookmarkRoot.root); + } + + // Convert to bytes for the file picker + final bytes = utf8.encode(content); + + // Now show the save dialog with the content ready + final dateFormatter = FixedDateTimeFormatter('YYYY-MM-DD_hhmmss'); + final timestamp = dateFormatter.encode(DateTime.now()); + final defaultFileName = + 'bookmarks_$timestamp.${format == 'json' ? 'json' : 'html'}'; + + final outputPath = await FilePicker.platform.saveFile( + dialogTitle: 'Export Bookmarks', + fileName: defaultFileName, + type: FileType.custom, + allowedExtensions: format == 'json' ? ['json'] : ['html', 'htm'], + bytes: bytes, + ); + + if (outputPath == null) return; + + if (context.mounted) { + showInfoMessage(context, 'Bookmarks exported successfully'); + } + } catch (e) { + if (context.mounted) { + showErrorMessage(context, 'Export failed: $e'); + } + } + } } diff --git a/app/lib/features/geckoview/features/bookmarks/utils/bookmark_html_utils.dart b/app/lib/features/geckoview/features/bookmarks/utils/bookmark_html_utils.dart index f213605a..346b4568 100644 --- a/app/lib/features/geckoview/features/bookmarks/utils/bookmark_html_utils.dart +++ b/app/lib/features/geckoview/features/bookmarks/utils/bookmark_html_utils.dart @@ -1,6 +1,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as html_parser; +import 'package:weblibre/core/logger.dart'; const _containerNormal = 0; const _containerToolbar = 1; @@ -426,7 +427,7 @@ class _BookmarkImporter { count++; } } catch (e) { - print('Failed to import bookmark "$title": $e'); + logger.e('Failed to import bookmark "$title": $e'); } } } else if (type == BookmarkNodeType.folder.index) { @@ -436,7 +437,7 @@ class _BookmarkImporter { child['guid'] = newGuid; count += await _insertTree(child); } catch (e) { - print('Failed to import folder "$title": $e'); + logger.e('Failed to import folder "$title": $e'); } } } diff --git a/app/lib/features/geckoview/features/bookmarks/utils/bookmark_json_utils.dart b/app/lib/features/geckoview/features/bookmarks/utils/bookmark_json_utils.dart index 5f71dd74..73874212 100644 --- a/app/lib/features/geckoview/features/bookmarks/utils/bookmark_json_utils.dart +++ b/app/lib/features/geckoview/features/bookmarks/utils/bookmark_json_utils.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:weblibre/core/logger.dart'; class BookmarkJSONUtils { final GeckoBookmarksService _service; @@ -24,7 +25,7 @@ class BookmarkJSONUtils { return await _import(data, replace: replace); } catch (ex) { - print('Failed to import bookmarks: $ex'); + logger.e('Failed to import bookmarks: $ex'); rethrow; } } @@ -130,10 +131,10 @@ class BookmarkJSONUtils { await _service.addItem(parentGuid, uri, title, i); count++; } else { - print('Skipping invalid URL: $url'); + logger.w('Skipping invalid URL: $url'); } } catch (e) { - print('Failed to import bookmark "$title": $e'); + logger.e('Failed to import bookmark "$title": $e'); } } } else if (type == BookmarkNodeType.folder) { @@ -145,7 +146,7 @@ class BookmarkJSONUtils { // Recursively insert children count += await _insertTree(child, folderIdToGuidMap); } catch (e) { - print('Failed to import folder "$title": $e'); + logger.e('Failed to import folder "$title": $e'); } } // Note: Separators are not supported by the Android API @@ -237,13 +238,13 @@ class BookmarkJSONUtils { // Skip invalid bookmarks if (node.type == BookmarkNodeType.item) { if (node.url == null || node.url!.isEmpty) { - print('Skipping bookmark with invalid URL: ${node.guid}'); + logger.w('Skipping bookmark with invalid URL: ${node.guid}'); return null; } try { Uri.parse(node.url!); } catch (e) { - print('Skipping bookmark with malformed URL: ${node.url}'); + logger.w('Skipping bookmark with malformed URL: ${node.url}'); return null; } } diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 306bc5c4..05de9fb0 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -417,7 +417,7 @@ class ContextualToolbar extends HookConsumerWidget { IconButton( onPressed: () async { await BookmarkListRoute( - entryGuid: BookmarkRoot.mobile.id, + entryGuid: BookmarkRoot.root.id, ).push(context); }, icon: const Icon(MdiIcons.bookmarkMultiple), @@ -806,7 +806,7 @@ class NavigationMenuButton extends HookConsumerWidget { MenuItemButton( onPressed: () async { await BookmarkListRoute( - entryGuid: BookmarkRoot.mobile.id, + entryGuid: BookmarkRoot.root.id, ).push(context); }, leadingIcon: const Icon(MdiIcons.bookmarkMultiple), diff --git a/app/test/features/geckoview/features/bookmarks/utils/bookmark_html_utils_test.dart b/app/test/features/geckoview/features/bookmarks/utils/bookmark_html_utils_test.dart index 25413471..71b0d038 100644 --- a/app/test/features/geckoview/features/bookmarks/utils/bookmark_html_utils_test.dart +++ b/app/test/features/geckoview/features/bookmarks/utils/bookmark_html_utils_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + /* * Copyright (c) 2024-2025 Fabian Freund. * @@ -41,16 +43,19 @@ void main() { group('BookmarkHTMLUtils - Import', () { test('should handle corrupt HTML file with malformed URIs', () async { // Load the corrupt fixture - final fixtureFile = - File('test/utils/bookmarks/fixtures/bookmarks.corrupt.html'); + final fixtureFile = File( + 'test/utils/bookmarks/fixtures/bookmarks.corrupt.html', + ); final htmlString = await fixtureFile.readAsString(); // Mock the service calls when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'generated_guid'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'generated_guid'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'generated_guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'generated_guid'); final count = await utils.importFromHTML(htmlString, replace: true); @@ -60,15 +65,18 @@ void main() { }); test('should import from valid HTML file', () async { - final fixtureFile = - File('test/utils/bookmarks/fixtures/bookmarks.preplaces.html'); + final fixtureFile = File( + 'test/utils/bookmarks/fixtures/bookmarks.preplaces.html', + ); final htmlString = await fixtureFile.readAsString(); when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'folder_guid'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark_guid'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'folder_guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark_guid'); final count = await utils.importFromHTML(htmlString, replace: true); @@ -105,8 +113,9 @@ void main() { '''; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); await utils.importFromHTML(simpleHtml); @@ -123,8 +132,9 @@ void main() { '''; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); final count = await utils.importFromHTML(htmlWithSpecialChars); @@ -146,8 +156,9 @@ void main() { '''; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); final count = await utils.importFromHTML(htmlWithDates); @@ -171,10 +182,12 @@ void main() { '''; - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'folder_guid'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark_guid'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'folder_guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark_guid'); final count = await utils.importFromHTML(htmlWithFolders); @@ -196,8 +209,9 @@ void main() { '''; when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); await utils.importFromHTML(htmlWithToolbar, replace: true); @@ -222,8 +236,9 @@ void main() { '''; when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); await utils.importFromHTML(htmlWithUnfiled, replace: true); @@ -245,8 +260,9 @@ void main() { '''; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); final count = await utils.importFromHTML(htmlWithSeparator); @@ -265,8 +281,9 @@ void main() { '''; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); final count = await utils.importFromHTML(htmlWithoutUrl); @@ -284,8 +301,9 @@ void main() { '''; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'guid'); final count = await utils.importFromHTML(htmlWithInvalidUrl); @@ -293,14 +311,17 @@ void main() { }); test('should handle single frame HTML', () async { - final fixtureFile = - File('test/utils/bookmarks/fixtures/bookmarks_html_singleframe.html'); + final fixtureFile = File( + 'test/utils/bookmarks/fixtures/bookmarks_html_singleframe.html', + ); final htmlString = await fixtureFile.readAsString(); - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'folder_guid'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark_guid'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'folder_guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark_guid'); final count = await utils.importFromHTML(htmlString); @@ -333,8 +354,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -368,8 +390,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -402,8 +425,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -424,8 +448,9 @@ void main() { children: [], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.toolbar); @@ -447,8 +472,9 @@ void main() { children: [], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.unfiled); @@ -501,8 +527,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -543,8 +570,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -590,8 +618,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -601,9 +630,10 @@ void main() { expect(html, contains('')); }); - test('should throw when tree cannot be fetched', () async { - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => null); + test('should throw when tree cannot be fetched', () { + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => null); expect( () => utils.exportToHTML(root: BookmarkRoot.menu), @@ -624,8 +654,9 @@ void main() { children: [], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -659,8 +690,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -708,8 +740,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); // Export final html = await utils.exportToHTML(root: BookmarkRoot.menu); @@ -717,10 +750,12 @@ void main() { // Re-import when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'folder1_____'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark1___'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'folder1_____'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark1___'); final count = await utils.importFromHTML(html, replace: true); diff --git a/app/test/features/geckoview/features/bookmarks/utils/bookmark_json_utils_test.dart b/app/test/features/geckoview/features/bookmarks/utils/bookmark_json_utils_test.dart index cc7e950d..c167d305 100644 --- a/app/test/features/geckoview/features/bookmarks/utils/bookmark_json_utils_test.dart +++ b/app/test/features/geckoview/features/bookmarks/utils/bookmark_json_utils_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values, avoid_dynamic_calls + /* * Copyright (c) 2024-2025 Fabian Freund. * @@ -108,10 +110,7 @@ void main() { when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - await utils.importFromJSON( - jsonEncode(jsonData), - replace: true, - ); + await utils.importFromJSON(jsonEncode(jsonData), replace: true); verify(mockService.eraseEverything(BookmarkRoot.root)).called(1); }); @@ -150,8 +149,9 @@ void main() { ], }; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark1___'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark1___'); final count = await utils.importFromJSON(jsonEncode(jsonData)); @@ -184,8 +184,9 @@ void main() { ], }; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark1___'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark1___'); final count = await utils.importFromJSON(jsonEncode(jsonData)); @@ -224,8 +225,9 @@ void main() { ], }; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'valid1______'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'valid1______'); final count = await utils.importFromJSON(jsonEncode(jsonData)); @@ -267,10 +269,12 @@ void main() { ], }; - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'folder1_____'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark1___'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'folder1_____'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark1___'); final count = await utils.importFromJSON(jsonEncode(jsonData)); @@ -299,10 +303,7 @@ void main() { 'type': 'text/x-moz-place', 'uri': 'https://example.com/1', }, - { - 'guid': 'separator___', - 'type': 'text/x-moz-place-separator', - }, + {'guid': 'separator___', 'type': 'text/x-moz-place-separator'}, { 'guid': 'bookmark2___', 'title': 'Second Bookmark', @@ -314,8 +315,9 @@ void main() { ], }; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((invocation) async => 'generated_guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((invocation) async => 'generated_guid'); final count = await utils.importFromJSON(jsonEncode(jsonData)); @@ -350,10 +352,12 @@ void main() { ], }; - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'folder1_____'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'shortcut1___'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'folder1_____'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'shortcut1___'); await utils.importFromJSON(jsonEncode(jsonData)); @@ -384,8 +388,9 @@ void main() { ], }; - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'shortcut1___'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'shortcut1___'); await utils.importFromJSON(jsonEncode(jsonData)); @@ -410,10 +415,12 @@ void main() { // Mock the service calls when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - when(mockService.addFolder(any, any, any)) - .thenAnswer((invocation) async => 'generated_guid'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((invocation) async => 'generated_guid'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((invocation) async => 'generated_guid'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((invocation) async => 'generated_guid'); final count = await utils.importFromJSON(jsonString, replace: true); @@ -440,8 +447,9 @@ void main() { ], }; - when(mockService.addItem(any, any, any, any)) - .thenThrow(Exception('Database error')); + when( + mockService.addItem(any, any, any, any), + ).thenThrow(Exception('Database error')); // Should not throw, but should log and continue final count = await utils.importFromJSON(jsonEncode(jsonData)); @@ -475,8 +483,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final result = await utils.exportToJson(root: BookmarkRoot.menu); @@ -529,8 +538,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final result = await utils.exportToJson(root: BookmarkRoot.menu); @@ -565,8 +575,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final result = await utils.exportToJson(root: BookmarkRoot.menu); @@ -600,8 +611,9 @@ void main() { children: [], ); - when(mockService.getTree(testCase.$1.id, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(testCase.$1.id, recursive: true), + ).thenAnswer((_) async => mockNode); final result = await utils.exportToJson(root: testCase.$1); @@ -654,8 +666,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final result = await utils.exportToJson(root: BookmarkRoot.menu); @@ -667,9 +680,10 @@ void main() { expect(children[2]['index'], equals(2)); }); - test('should throw when tree cannot be fetched', () async { - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => null); + test('should throw when tree cannot be fetched', () { + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => null); expect( () => utils.exportToJson(root: BookmarkRoot.menu), @@ -701,8 +715,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); final result = await utils.exportToJson(root: BookmarkRoot.menu); @@ -752,8 +767,9 @@ void main() { ], ); - when(mockService.getTree(any, recursive: true)) - .thenAnswer((_) async => mockNode); + when( + mockService.getTree(any, recursive: true), + ).thenAnswer((_) async => mockNode); // Export final exported = await utils.exportToJson(root: BookmarkRoot.menu); @@ -761,12 +777,16 @@ void main() { // Re-import when(mockService.eraseEverything(any)).thenAnswer((_) async {}); - when(mockService.addFolder(any, any, any)) - .thenAnswer((_) async => 'folder1_____'); - when(mockService.addItem(any, any, any, any)) - .thenAnswer((_) async => 'bookmark1___'); + when( + mockService.addFolder(any, any, any), + ).thenAnswer((_) async => 'folder1_____'); + when( + mockService.addItem(any, any, any, any), + ).thenAnswer((_) async => 'bookmark1___'); - final jsonString = jsonEncode({'children': [exported]}); + final jsonString = jsonEncode({ + 'children': [exported], + }); final count = await utils.importFromJSON(jsonString, replace: true); expect(count, equals(1)); // One bookmark imported