added tests

This commit is contained in:
Fabian Freund
2026-01-03 19:53:35 +01:00
parent 17f10193ff
commit 3cfd510125
5 changed files with 1824 additions and 0 deletions
@@ -0,0 +1,731 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:io';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:weblibre/features/geckoview/features/bookmarks/utils/bookmark_html_utils.dart';
@GenerateMocks([GeckoBookmarksService])
import 'bookmark_html_utils_test.mocks.dart';
void main() {
late MockGeckoBookmarksService mockService;
late BookmarkHTMLUtils utils;
setUp(() {
mockService = MockGeckoBookmarksService();
utils = BookmarkHTMLUtils(mockService);
});
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 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');
final count = await utils.importFromHTML(htmlString, replace: true);
// Should import valid bookmarks and skip the corrupt one
expect(count, greaterThan(0));
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
});
test('should import from valid HTML file', () async {
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');
final count = await utils.importFromHTML(htmlString, replace: true);
expect(count, greaterThan(0));
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
// Verify some bookmarks were added
verify(mockService.addItem(any, any, any, any)).called(greaterThan(0));
});
test('should handle empty HTML', () async {
const emptyHtml = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
</DL>
''';
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
final count = await utils.importFromHTML(emptyHtml, replace: true);
expect(count, equals(0));
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
});
test('should not erase when replace is false', () async {
const simpleHtml = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><A HREF="https://example.com">Example</A>
</DL>
''';
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
await utils.importFromHTML(simpleHtml);
verifyNever(mockService.eraseEverything(any));
});
test('should handle bookmarks with special characters in title', () async {
const htmlWithSpecialChars = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><A HREF="https://example.com">&lt;unescaped="test"&gt;</A>
</DL>
''';
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
final count = await utils.importFromHTML(htmlWithSpecialChars);
expect(count, equals(1));
final captured = verify(
mockService.addItem(any, any, captureAny, any),
).captured;
// Should properly decode HTML entities
expect(captured[0], equals('<unescaped="test">'));
});
test('should import bookmarks with timestamps', () async {
const htmlWithDates = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><A HREF="https://example.com" ADD_DATE="1177375336" LAST_MODIFIED="1177375423">Test</A>
</DL>
''';
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
final count = await utils.importFromHTML(htmlWithDates);
expect(count, equals(1));
});
test('should handle folder hierarchy', () async {
const htmlWithFolders = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><H3>Parent Folder</H3>
<DL><p>
<DT><A HREF="https://example.com/1">Child 1</A>
<DT><H3>Nested Folder</H3>
<DL><p>
<DT><A HREF="https://example.com/2">Grandchild</A>
</DL><p>
</DL><p>
</DL>
''';
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);
expect(count, equals(2)); // 2 bookmarks
verify(mockService.addFolder(any, any, any)).called(2); // 2 folders
});
test('should recognize toolbar folder', () async {
const htmlWithToolbar = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><H3 PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
<DL><p>
<DT><A HREF="https://example.com">Toolbar Bookmark</A>
</DL><p>
</DL>
''';
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
await utils.importFromHTML(htmlWithToolbar, replace: true);
// When replace is true, should add to toolbar
final captured = verify(
mockService.addItem(captureAny, any, any, any),
).captured;
expect(captured[0], equals(BookmarkRoot.toolbar.id));
});
test('should recognize unfiled folder', () async {
const htmlWithUnfiled = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
<DL><p>
<DT><A HREF="https://example.com">Unfiled Bookmark</A>
</DL><p>
</DL>
''';
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
await utils.importFromHTML(htmlWithUnfiled, replace: true);
final captured = verify(
mockService.addItem(captureAny, any, any, any),
).captured;
expect(captured[0], equals(BookmarkRoot.unfiled.id));
});
test('should handle separators', () async {
const htmlWithSeparator = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><A HREF="https://example.com/1">Bookmark 1</A>
<HR>
<DT><A HREF="https://example.com/2">Bookmark 2</A>
</DL>
''';
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
final count = await utils.importFromHTML(htmlWithSeparator);
// Should import 2 bookmarks (separator is not supported by Android API)
expect(count, equals(2));
});
test('should skip bookmarks without URLs', () async {
const htmlWithoutUrl = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><A>No URL</A>
<DT><A HREF="https://example.com">Valid</A>
</DL>
''';
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
final count = await utils.importFromHTML(htmlWithoutUrl);
expect(count, equals(1)); // Only the valid one
});
test('should skip bookmarks with invalid URLs', () async {
const htmlWithInvalidUrl = '''
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><A HREF="not a url">Invalid</A>
<DT><A HREF="https://example.com">Valid</A>
</DL>
''';
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'guid');
final count = await utils.importFromHTML(htmlWithInvalidUrl);
expect(count, equals(1));
});
test('should handle single frame HTML', () async {
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');
final count = await utils.importFromHTML(htmlString);
expect(count, greaterThan(0));
});
});
group('BookmarkHTMLUtils - Export', () {
test('should export bookmark tree to HTML', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: 'Test Bookmark',
url: 'https://example.com',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
expect(html, contains('<!DOCTYPE NETSCAPE-Bookmark-file-1>'));
expect(html, contains('<H1>Bookmarks Menu</H1>'));
expect(html, contains('https://example.com'));
expect(html, contains('Test Bookmark'));
});
test('should escape HTML entities in export', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: '<unescaped="test">',
url: 'https://example.com',
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
// Should escape special characters
expect(html, contains('&lt;unescaped=&quot;test&quot;&gt;'));
expect(html, isNot(contains('<unescaped="test">')));
});
test('should include date attributes in export', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: 'Test',
url: 'https://example.com',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
expect(html, contains('ADD_DATE='));
expect(html, contains('LAST_MODIFIED='));
});
test('should export toolbar with title as H1 when root', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.toolbar.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks Toolbar',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.toolbar);
// When toolbar is the root, it becomes H1 without special attributes
expect(html, contains('<H1>Bookmarks Toolbar</H1>'));
expect(html, isNot(contains('PERSONAL_TOOLBAR_FOLDER')));
});
test('should export unfiled with title as H1 when root', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.unfiled.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Unsorted Bookmarks',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.unfiled);
// When unfiled is the root, it becomes H1 without special attributes
expect(html, contains('<H1>Unsorted Bookmarks</H1>'));
expect(html, isNot(contains('UNFILED_BOOKMARKS_FOLDER')));
});
test('should export separators', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: 'First',
url: 'https://example.com/1',
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.item,
),
BookmarkNode(
guid: 'separator___',
parentGuid: BookmarkRoot.menu.id,
position: 1,
title: null,
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.separator,
),
BookmarkNode(
guid: 'bookmark2___',
parentGuid: BookmarkRoot.menu.id,
position: 2,
title: 'Second',
url: 'https://example.com/2',
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
expect(html, contains('<HR>'));
});
test('should skip bookmarks with invalid URLs during export', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'invalid1____',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: 'Invalid',
url: '', // Empty URL
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.item,
),
BookmarkNode(
guid: 'valid1______',
parentGuid: BookmarkRoot.menu.id,
position: 1,
title: 'Valid',
url: 'https://example.com',
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
// Should only contain the valid bookmark
expect(html, contains('https://example.com'));
expect(html, contains('Valid'));
expect(html, isNot(contains('Invalid')));
});
test('should export nested folders', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'folder1_____',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: 'Parent Folder',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: 'folder1_____',
position: 0,
title: 'Nested Bookmark',
url: 'https://example.com',
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.item,
),
],
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
expect(html, contains('Parent Folder'));
expect(html, contains('Nested Bookmark'));
expect(html, contains('<H3'));
expect(html, contains('</H3>'));
});
test('should throw when tree cannot be fetched', () async {
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => null);
expect(
() => utils.exportToHTML(root: BookmarkRoot.menu),
throwsA(isA<Exception>()),
);
});
test('should include proper HTML header', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
expect(html, contains('<!DOCTYPE NETSCAPE-Bookmark-file-1>'));
expect(html, contains('<META HTTP-EQUIV="Content-Type"'));
expect(html, contains('<TITLE>Bookmarks</TITLE>'));
expect(html, contains('Content-Security-Policy'));
});
test('should properly indent HTML structure', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks',
url: null,
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: 'Test',
url: 'https://example.com',
dateAdded: 0,
lastModified: 0,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
// Should contain indentation
expect(html, contains(' <DT>'));
expect(html, contains('<DL><p>'));
expect(html, contains('</DL>'));
});
});
group('BookmarkHTMLUtils - Import/Export Round-Trip', () {
test('should preserve data through export and re-import', () async {
final mockNode = BookmarkNode(
guid: BookmarkRoot.menu.id,
parentGuid: BookmarkRoot.root.id,
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'folder1_____',
parentGuid: BookmarkRoot.menu.id,
position: 0,
title: 'Test Folder',
url: null,
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: 'folder1_____',
position: 0,
title: 'Test Bookmark',
url: 'https://example.com',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
// Export
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
expect(html, isNotEmpty);
// 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___');
final count = await utils.importFromHTML(html, replace: true);
expect(count, equals(1)); // One bookmark imported
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
});
});
}
@@ -0,0 +1,158 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in weblibre/test/features/geckoview/features/bookmarks/utils/bookmark_html_utils_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i3;
import 'package:flutter_mozilla_components/src/domain/services/gecko_bookmarks.dart'
as _i2;
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart' as _i4;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i5;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member
/// A class which mocks [GeckoBookmarksService].
///
/// See the documentation for Mockito's code generation for more information.
class MockGeckoBookmarksService extends _i1.Mock
implements _i2.GeckoBookmarksService {
MockGeckoBookmarksService() {
_i1.throwOnMissingStub(this);
}
@override
_i3.Future<_i4.BookmarkNode?> getTree(
String? guid, {
bool? recursive = false,
}) =>
(super.noSuchMethod(
Invocation.method(#getTree, [guid], {#recursive: recursive}),
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
)
as _i3.Future<_i4.BookmarkNode?>);
@override
_i3.Future<_i4.BookmarkNode?> getBookmark(String? guid) =>
(super.noSuchMethod(
Invocation.method(#getBookmark, [guid]),
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
)
as _i3.Future<_i4.BookmarkNode?>);
@override
_i3.Future<List<_i4.BookmarkNode>> getBookmarksWithUrl(Uri? url) =>
(super.noSuchMethod(
Invocation.method(#getBookmarksWithUrl, [url]),
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
<_i4.BookmarkNode>[],
),
)
as _i3.Future<List<_i4.BookmarkNode>>);
@override
_i3.Future<List<_i4.BookmarkNode>> getRecentBookmarks(
int? limit, {
Duration? maxAge = Duration.zero,
DateTime? currentTime,
}) =>
(super.noSuchMethod(
Invocation.method(
#getRecentBookmarks,
[limit],
{#maxAge: maxAge, #currentTime: currentTime},
),
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
<_i4.BookmarkNode>[],
),
)
as _i3.Future<List<_i4.BookmarkNode>>);
@override
_i3.Future<List<_i4.BookmarkNode>> searchBookmarks(
String? query, {
int? limit = 10,
}) =>
(super.noSuchMethod(
Invocation.method(#searchBookmarks, [query], {#limit: limit}),
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
<_i4.BookmarkNode>[],
),
)
as _i3.Future<List<_i4.BookmarkNode>>);
@override
_i3.Future<String> addItem(
String? parentGuid,
Uri? url,
String? title,
int? position,
) =>
(super.noSuchMethod(
Invocation.method(#addItem, [parentGuid, url, title, position]),
returnValue: _i3.Future<String>.value(
_i5.dummyValue<String>(
this,
Invocation.method(#addItem, [parentGuid, url, title, position]),
),
),
)
as _i3.Future<String>);
@override
_i3.Future<String> addFolder(
String? parentGuid,
String? title,
int? position,
) =>
(super.noSuchMethod(
Invocation.method(#addFolder, [parentGuid, title, position]),
returnValue: _i3.Future<String>.value(
_i5.dummyValue<String>(
this,
Invocation.method(#addFolder, [parentGuid, title, position]),
),
),
)
as _i3.Future<String>);
@override
_i3.Future<void> updateNode(String? guid, _i4.BookmarkInfo? info) =>
(super.noSuchMethod(
Invocation.method(#updateNode, [guid, info]),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
)
as _i3.Future<void>);
@override
_i3.Future<bool> deleteNode(String? guid) =>
(super.noSuchMethod(
Invocation.method(#deleteNode, [guid]),
returnValue: _i3.Future<bool>.value(false),
)
as _i3.Future<bool>);
@override
_i3.Future<void> eraseEverything(_i2.BookmarkRoot? root) =>
(super.noSuchMethod(
Invocation.method(#eraseEverything, [root]),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
)
as _i3.Future<void>);
}
@@ -0,0 +1,776 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:convert';
import 'dart:io';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:weblibre/features/geckoview/features/bookmarks/utils/bookmark_json_utils.dart';
@GenerateMocks([GeckoBookmarksService])
import 'bookmark_json_utils_test.mocks.dart';
void main() {
late MockGeckoBookmarksService mockService;
late BookmarkJSONUtils utils;
setUp(() {
mockService = MockGeckoBookmarksService();
utils = BookmarkJSONUtils(mockService);
});
group('BookmarkJSONUtils - Import', () {
test('should reject invalid JSON format', () {
const invalidJson = '[]';
expect(
() => utils.importFromJSON(invalidJson),
throwsA(isA<Exception>()),
);
});
test('should return 0 for empty children', () async {
const emptyJson = '{"children": []}';
final count = await utils.importFromJSON(emptyJson);
expect(count, equals(0));
});
test('should return 0 when children is null', () async {
const noChildrenJson = '{"guid": "root________"}';
final count = await utils.importFromJSON(noChildrenJson);
expect(count, equals(0));
});
test('should filter out tags folder during import', () async {
final jsonData = {
'children': [
{
'guid': 'tags________',
'root': 'tagsFolder',
'type': 'text/x-moz-place-container',
'children': [],
},
{
'guid': 'menu________',
'root': 'bookmarksMenuFolder',
'type': 'text/x-moz-place-container',
'children': [],
},
],
};
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
final count = await utils.importFromJSON(
jsonEncode(jsonData),
replace: true,
);
// Only the menu folder should be processed, tags should be filtered
expect(count, equals(0)); // No bookmarks, just folders
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
});
test('should erase everything when replace is true', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [],
},
],
};
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
await utils.importFromJSON(
jsonEncode(jsonData),
replace: true,
);
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
});
test('should not erase when replace is false', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [],
},
],
};
await utils.importFromJSON(jsonEncode(jsonData));
verifyNever(mockService.eraseEverything(any));
});
test('should import bookmarks with URI field', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'bookmark1___',
'title': 'Test Bookmark',
'type': 'text/x-moz-place',
'uri': 'https://example.com',
},
],
},
],
};
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'bookmark1___');
final count = await utils.importFromJSON(jsonEncode(jsonData));
expect(count, equals(1));
verify(
mockService.addItem(
'menu________',
Uri.parse('https://example.com'),
'Test Bookmark',
0,
),
).called(1);
});
test('should import bookmarks with URL field', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'bookmark1___',
'title': 'Test Bookmark',
'type': 'text/x-moz-place',
'url': 'https://example.com',
},
],
},
],
};
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'bookmark1___');
final count = await utils.importFromJSON(jsonEncode(jsonData));
expect(count, equals(1));
verify(
mockService.addItem(
'menu________',
Uri.parse('https://example.com'),
'Test Bookmark',
0,
),
).called(1);
});
test('should skip bookmarks with invalid URLs', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'invalid1____',
'title': 'Invalid URL',
'type': 'text/x-moz-place',
'uri': 'not a valid url',
},
{
'guid': 'valid1______',
'title': 'Valid URL',
'type': 'text/x-moz-place',
'uri': 'https://example.com',
},
],
},
],
};
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'valid1______');
final count = await utils.importFromJSON(jsonEncode(jsonData));
// Only one valid bookmark should be imported
expect(count, equals(1));
// Note: position is 1 because the invalid bookmark was skipped first
verify(
mockService.addItem(
'menu________',
Uri.parse('https://example.com'),
'Valid URL',
1,
),
).called(1);
});
test('should import nested folders recursively', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'folder1_____',
'title': 'Folder 1',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'bookmark1___',
'title': 'Nested Bookmark',
'type': 'text/x-moz-place',
'uri': 'https://example.com',
},
],
},
],
},
],
};
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));
expect(count, equals(1));
verify(mockService.addFolder('menu________', 'Folder 1', 0)).called(1);
verify(
mockService.addItem(
'folder1_____',
Uri.parse('https://example.com'),
'Nested Bookmark',
0,
),
).called(1);
});
test('should handle separators gracefully (skip them)', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'bookmark1___',
'title': 'First Bookmark',
'type': 'text/x-moz-place',
'uri': 'https://example.com/1',
},
{
'guid': 'separator___',
'type': 'text/x-moz-place-separator',
},
{
'guid': 'bookmark2___',
'title': 'Second Bookmark',
'type': 'text/x-moz-place',
'uri': 'https://example.com/2',
},
],
},
],
};
when(mockService.addItem(any, any, any, any))
.thenAnswer((invocation) async => 'generated_guid');
final count = await utils.importFromJSON(jsonEncode(jsonData));
// Two bookmarks, separator should be skipped
expect(count, equals(2));
verify(mockService.addItem(any, any, any, any)).called(2);
});
test('should fixup place: queries with folder shortcuts', () async {
final jsonData = {
'children': [
{
'guid': 'unfiled_____',
'type': 'text/x-moz-place-container',
'id': '5',
'children': [
{
'guid': 'folder1_____',
'title': 'Test Folder',
'type': 'text/x-moz-place-container',
'id': '6',
'children': [],
},
{
'guid': 'shortcut1___',
'title': 'Folder Shortcut',
'type': 'text/x-moz-place',
'uri': 'place:folder=6',
},
],
},
],
};
when(mockService.addFolder(any, any, any))
.thenAnswer((_) async => 'folder1_____');
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'shortcut1___');
await utils.importFromJSON(jsonEncode(jsonData));
// Capture the URI argument to verify it was fixed up
// Note: position is 1 because the folder was added first at position 0
final captured = verify(
mockService.addItem('unfiled_____', captureAny, 'Folder Shortcut', 1),
).captured;
expect((captured[0] as Uri).toString(), contains('parent=folder1_____'));
});
test('should handle invalid folder references in place: queries', () async {
final jsonData = {
'children': [
{
'guid': 'unfiled_____',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'shortcut1___',
'title': 'Invalid Folder Shortcut',
'type': 'text/x-moz-place',
'uri': 'place:folder=999999',
},
],
},
],
};
when(mockService.addItem(any, any, any, any))
.thenAnswer((_) async => 'shortcut1___');
await utils.importFromJSON(jsonEncode(jsonData));
final captured = verify(
mockService.addItem(
'unfiled_____',
captureAny,
'Invalid Folder Shortcut',
0,
),
).captured;
final url = (captured[0] as Uri).toString();
expect(url, contains('invalidOldParentId=999999'));
expect(url, contains('excludeItems=1'));
});
test('should count imported bookmarks correctly from fixture', () async {
// Load the fixture
final fixtureFile = File('test/utils/bookmarks/fixtures/bookmarks.json');
final jsonString = await fixtureFile.readAsString();
// 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');
final count = await utils.importFromJSON(jsonString, replace: true);
// The fixture has several bookmarks - we should count only valid ones
expect(count, greaterThan(0));
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
});
test('should handle import errors gracefully', () async {
final jsonData = {
'children': [
{
'guid': 'menu________',
'type': 'text/x-moz-place-container',
'children': [
{
'guid': 'bookmark1___',
'title': 'Test Bookmark',
'type': 'text/x-moz-place',
'uri': 'https://example.com',
},
],
},
],
};
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));
expect(count, equals(0)); // Failed to add
});
});
group('BookmarkJSONUtils - Export', () {
test('should export bookmark tree to JSON', () async {
final mockNode = BookmarkNode(
guid: 'menu________',
parentGuid: 'root________',
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: 'menu________',
position: 0,
title: 'Test Bookmark',
url: 'https://example.com',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final result = await utils.exportToJson(root: BookmarkRoot.menu);
expect(result, isNotNull);
expect(result!['guid'], equals('menu________'));
expect(result['title'], equals('Bookmarks Menu'));
expect(result['type'], equals('text/x-moz-place-container'));
expect(result['root'], equals('bookmarksMenuFolder'));
expect(result['children'], isA<List>());
expect((result['children'] as List).length, equals(1));
final child = (result['children'] as List)[0] as Map<String, dynamic>;
expect(child['guid'], equals('bookmark1___'));
expect(child['title'], equals('Test Bookmark'));
expect(child['url'], equals('https://example.com'));
expect(child['type'], equals('text/x-moz-place'));
});
test('should skip bookmarks with invalid URLs during export', () async {
final mockNode = BookmarkNode(
guid: 'menu________',
parentGuid: 'root________',
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'invalid1____',
parentGuid: 'menu________',
position: 0,
title: 'Invalid Bookmark',
url: '', // Empty URL
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
BookmarkNode(
guid: 'valid1______',
parentGuid: 'menu________',
position: 1,
title: 'Valid Bookmark',
url: 'https://example.com',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final result = await utils.exportToJson(root: BookmarkRoot.menu);
expect(result, isNotNull);
final children = result!['children'] as List;
// Only the valid bookmark should be exported
expect(children.length, equals(1));
expect(children[0]['guid'], equals('valid1______'));
});
test('should handle separators in export', () async {
final mockNode = BookmarkNode(
guid: 'menu________',
parentGuid: 'root________',
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'separator___',
parentGuid: 'menu________',
position: 0,
title: 'should be ignored',
url: null,
dateAdded: 1361551979380988,
lastModified: 1361551979380988,
type: BookmarkNodeType.separator,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final result = await utils.exportToJson(root: BookmarkRoot.menu);
expect(result, isNotNull);
final children = result!['children'] as List;
expect(children.length, equals(1));
final separator = children[0];
expect(separator['type'], equals('text/x-moz-place-separator'));
expect(separator['title'], equals('')); // Title should be empty
});
test('should assign correct root names', () async {
final testCases = [
(BookmarkRoot.menu, 'bookmarksMenuFolder'),
(BookmarkRoot.toolbar, 'toolbarFolder'),
(BookmarkRoot.unfiled, 'unfiledBookmarksFolder'),
(BookmarkRoot.mobile, 'mobileFolder'),
];
for (final testCase in testCases) {
final mockNode = BookmarkNode(
guid: testCase.$1.id,
parentGuid: 'root________',
position: 0,
title: 'Test Root',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [],
);
when(mockService.getTree(testCase.$1.id, recursive: true))
.thenAnswer((_) async => mockNode);
final result = await utils.exportToJson(root: testCase.$1);
expect(result, isNotNull);
expect(result!['root'], equals(testCase.$2));
}
});
test('should preserve correct index values for children', () async {
final mockNode = BookmarkNode(
guid: 'menu________',
parentGuid: 'root________',
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: 'menu________',
position: 0,
title: 'First',
url: 'https://example.com/1',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
BookmarkNode(
guid: 'bookmark2___',
parentGuid: 'menu________',
position: 1,
title: 'Second',
url: 'https://example.com/2',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
BookmarkNode(
guid: 'bookmark3___',
parentGuid: 'menu________',
position: 2,
title: 'Third',
url: 'https://example.com/3',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final result = await utils.exportToJson(root: BookmarkRoot.menu);
expect(result, isNotNull);
final children = result!['children'] as List;
expect(children.length, equals(3));
expect(children[0]['index'], equals(0));
expect(children[1]['index'], equals(1));
expect(children[2]['index'], equals(2));
});
test('should throw when tree cannot be fetched', () async {
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => null);
expect(
() => utils.exportToJson(root: BookmarkRoot.menu),
throwsA(isA<Exception>()),
);
});
test('should include typeCode in export', () async {
final mockNode = BookmarkNode(
guid: 'menu________',
parentGuid: 'root________',
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: 'menu________',
position: 0,
title: 'Test Bookmark',
url: 'https://example.com',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
final result = await utils.exportToJson(root: BookmarkRoot.menu);
expect(result, isNotNull);
expect(result!['typeCode'], equals(BookmarkNodeType.folder.index + 1));
final child = (result['children'] as List)[0] as Map<String, dynamic>;
expect(child['typeCode'], equals(BookmarkNodeType.item.index + 1));
});
});
group('BookmarkJSONUtils - Import/Export Round-Trip', () {
test('should preserve data through export and re-import', () async {
// Setup initial data
final mockNode = BookmarkNode(
guid: 'menu________',
parentGuid: 'root________',
position: 0,
title: 'Bookmarks Menu',
url: null,
dateAdded: 1361551978957783,
lastModified: 1361551979382837,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'folder1_____',
parentGuid: 'menu________',
position: 0,
title: 'Test Folder',
url: null,
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.folder,
children: [
BookmarkNode(
guid: 'bookmark1___',
parentGuid: 'folder1_____',
position: 0,
title: 'Test Bookmark',
url: 'https://example.com',
dateAdded: 1361551979350273,
lastModified: 1361551979376699,
type: BookmarkNodeType.item,
),
],
),
],
);
when(mockService.getTree(any, recursive: true))
.thenAnswer((_) async => mockNode);
// Export
final exported = await utils.exportToJson(root: BookmarkRoot.menu);
expect(exported, isNotNull);
// 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___');
final jsonString = jsonEncode({'children': [exported]});
final count = await utils.importFromJSON(jsonString, replace: true);
expect(count, equals(1)); // One bookmark imported
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
});
});
}
@@ -0,0 +1,158 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in weblibre/test/features/geckoview/features/bookmarks/utils/bookmark_json_utils_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i3;
import 'package:flutter_mozilla_components/src/domain/services/gecko_bookmarks.dart'
as _i2;
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart' as _i4;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i5;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member
/// A class which mocks [GeckoBookmarksService].
///
/// See the documentation for Mockito's code generation for more information.
class MockGeckoBookmarksService extends _i1.Mock
implements _i2.GeckoBookmarksService {
MockGeckoBookmarksService() {
_i1.throwOnMissingStub(this);
}
@override
_i3.Future<_i4.BookmarkNode?> getTree(
String? guid, {
bool? recursive = false,
}) =>
(super.noSuchMethod(
Invocation.method(#getTree, [guid], {#recursive: recursive}),
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
)
as _i3.Future<_i4.BookmarkNode?>);
@override
_i3.Future<_i4.BookmarkNode?> getBookmark(String? guid) =>
(super.noSuchMethod(
Invocation.method(#getBookmark, [guid]),
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
)
as _i3.Future<_i4.BookmarkNode?>);
@override
_i3.Future<List<_i4.BookmarkNode>> getBookmarksWithUrl(Uri? url) =>
(super.noSuchMethod(
Invocation.method(#getBookmarksWithUrl, [url]),
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
<_i4.BookmarkNode>[],
),
)
as _i3.Future<List<_i4.BookmarkNode>>);
@override
_i3.Future<List<_i4.BookmarkNode>> getRecentBookmarks(
int? limit, {
Duration? maxAge = Duration.zero,
DateTime? currentTime,
}) =>
(super.noSuchMethod(
Invocation.method(
#getRecentBookmarks,
[limit],
{#maxAge: maxAge, #currentTime: currentTime},
),
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
<_i4.BookmarkNode>[],
),
)
as _i3.Future<List<_i4.BookmarkNode>>);
@override
_i3.Future<List<_i4.BookmarkNode>> searchBookmarks(
String? query, {
int? limit = 10,
}) =>
(super.noSuchMethod(
Invocation.method(#searchBookmarks, [query], {#limit: limit}),
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
<_i4.BookmarkNode>[],
),
)
as _i3.Future<List<_i4.BookmarkNode>>);
@override
_i3.Future<String> addItem(
String? parentGuid,
Uri? url,
String? title,
int? position,
) =>
(super.noSuchMethod(
Invocation.method(#addItem, [parentGuid, url, title, position]),
returnValue: _i3.Future<String>.value(
_i5.dummyValue<String>(
this,
Invocation.method(#addItem, [parentGuid, url, title, position]),
),
),
)
as _i3.Future<String>);
@override
_i3.Future<String> addFolder(
String? parentGuid,
String? title,
int? position,
) =>
(super.noSuchMethod(
Invocation.method(#addFolder, [parentGuid, title, position]),
returnValue: _i3.Future<String>.value(
_i5.dummyValue<String>(
this,
Invocation.method(#addFolder, [parentGuid, title, position]),
),
),
)
as _i3.Future<String>);
@override
_i3.Future<void> updateNode(String? guid, _i4.BookmarkInfo? info) =>
(super.noSuchMethod(
Invocation.method(#updateNode, [guid, info]),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
)
as _i3.Future<void>);
@override
_i3.Future<bool> deleteNode(String? guid) =>
(super.noSuchMethod(
Invocation.method(#deleteNode, [guid]),
returnValue: _i3.Future<bool>.value(false),
)
as _i3.Future<bool>);
@override
_i3.Future<void> eraseEverything(_i2.BookmarkRoot? root) =>
(super.noSuchMethod(
Invocation.method(#eraseEverything, [root]),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
)
as _i3.Future<void>);
}