added tests
This commit is contained in:
@@ -105,6 +105,7 @@ dev_dependencies:
|
||||
go_router_builder: ^4.1.3
|
||||
json_serializable: ^6.11.2
|
||||
lint: ^2.8.0
|
||||
mockito: ^5.4.4
|
||||
riverpod_generator: ^4.0.0+1
|
||||
riverpod_lint: ^3.1.0
|
||||
|
||||
|
||||
@@ -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"><unescaped="test"></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('<unescaped="test">'));
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+158
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+158
@@ -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>);
|
||||
}
|
||||
Reference in New Issue
Block a user