diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/places/tests/unit | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/places/tests/unit')
113 files changed, 16761 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/unit/bookmarks.corrupt.html b/toolkit/components/places/tests/unit/bookmarks.corrupt.html new file mode 100644 index 0000000000..3cf43367fb --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks.corrupt.html @@ -0,0 +1,36 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<TITLE>Bookmarks</TITLE> +<H1 LAST_MODIFIED="1177541029">Bookmarks</H1> + +<DL><p> + <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3> + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A> + <DT><A HREF="b0rked" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A> + </DL><p> + <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3> +<DD>folder test comment + <DL><p> + <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A> +<DD>item description + </DL> + <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3> + <DL><p> + <DT><A HREF="http://example.tld">Example.tld</A> + </DL><p> + <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3> +<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A> + <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A> + <DT><A HREF="http://bogus-icon.mozilla.com/" ICON="b0rked" ID="rdf:#$GvPhC3">Getting Started</A> +<DD>Livemark test comment + </DL><p> +</DL><p> diff --git a/toolkit/components/places/tests/unit/bookmarks.json b/toolkit/components/places/tests/unit/bookmarks.json new file mode 100644 index 0000000000..27ed9ce5ca --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks.json @@ -0,0 +1,307 @@ +{ + "guid": "root________", + "title": "", + "id": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551978957783, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "id": 2, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551979382837, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "OCyeUO5uu9FF", + "title": "Mozilla Firefox", + "id": 6, + "parent": 2, + "dateAdded": 1361551979350273, + "lastModified": 1361551979376699, + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "OCyeUO5uu9FG", + "title": "Help and Tutorials", + "id": 7, + "parent": 6, + "dateAdded": 1361551979356436, + "lastModified": 1361551979362718, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/help/", + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FH", + "index": 1, + "title": "Customize Firefox", + "id": 8, + "parent": 6, + "dateAdded": 1361551979365662, + "lastModified": 1361551979368077, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/", + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FJ", + "index": 3, + "title": "About Us", + "id": 10, + "parent": 6, + "dateAdded": 1361551979376699, + "lastModified": 1361551979379060, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/about/", + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FI", + "index": 2, + "title": "Get Involved", + "id": 9, + "parent": 6, + "dateAdded": 1361551979371071, + "lastModified": 1361551979373745, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/community/", + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "QFM-QnE2ZpMz", + "title": "Test null postData", + "index": 4, + "dateAdded": 1481639510868000, + "lastModified": 1489563704300000, + "id": 17, + "charset": "UTF-8", + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "value": "The best" + } + ], + "type": "text/x-moz-place", + "uri": "http://example.com/search?q=%s&suggid=", + "postData": null + } + ] + }, + { + "guid": "OCyeUO5uu9FK", + "index": 1, + "title": "", + "id": 11, + "parent": 2, + "dateAdded": 1361551979380988, + "lastModified": 1361551979380988, + "type": "text/x-moz-place-separator" + }, + { + "guid": "OCyeUO5uu9FL", + "index": 2, + "title": "test", + "id": 12, + "parent": 2, + "dateAdded": 1177541020000000, + "lastModified": 1177541050000000, + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "folder test comment" + } + ], + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "OCyeUO5uu9GX", + "title": "test post keyword", + "id": 13, + "parent": 12, + "dateAdded": 1177375336000000, + "lastModified": 1177375423000000, + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "item description" + }, + { + "name": "bookmarkProperties/loadInSidebar", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 1, + "value": 1 + } + ], + "type": "text/x-moz-place", + "uri": "http://test/post", + "keyword": "test", + "charset": "ISO-8859-1", + "postData": "hidden1%3Dbar&text1%3D%25s" + } + ] + } + ] + }, + { + "index": 1, + "title": "Bookmarks Toolbar", + "id": 3, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1177541050000000, + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar" + } + ], + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "guid": "OCyeUO5uu9FB", + "title": "Getting Started", + "id": 15, + "parent": 3, + "dateAdded": 1361551979409695, + "lastModified": 1361551979412080, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/central/", + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FR", + "index": 1, + "title": "Latest Headlines", + "id": 16, + "parent": 3, + "dateAdded": 1361551979451584, + "lastModified": 1361551979457086, + "livemark": 1, + "annos": [ + { + "name": "livemark/feedURI", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" + }, + { + "name": "livemark/siteURI", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" + } + ], + "type": "text/x-moz-place-container", + "children": [] + } + ] + }, + { + "index": 2, + "title": "Tags", + "id": 4, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551978957783, + "type": "text/x-moz-place-container", + "root": "tagsFolder", + "children": [] + }, + { + "index": 3, + "title": "Unsorted Bookmarks", + "id": 5, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1177541050000000, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder", + "children": [ + { + "guid": "OCyeUO5uu9FW", + "title": "Example.tld", + "id": 14, + "parent": 5, + "dateAdded": 1361551979401846, + "lastModified": 1361551979402952, + "type": "text/x-moz-place", + "uri": "http://example.tld/" + }, + { + "guid": "Cfkety492Afk", + "title": "test tagged bookmark", + "id": 15, + "parent": 5, + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "type": "text/x-moz-place", + "uri": "http://example.tld/tagged", + "tags": "foo" + }, + { + "guid": "lOZGoFR1eXbl", + "title": "Bookmarks Toolbar Shortcut", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 16, + "type": "text/x-moz-place", + "uri": "place:folder=TOOLBAR" + }, + { + "guid": "7yJWnBVhjRtP", + "title": "Folder Shortcut", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 17, + "type": "text/x-moz-place", + "uri": "place:folder=6" + }, + { + "guid": "vm5QXWuWc12l", + "title": "Folder Shortcut 2", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 18, + "type": "text/x-moz-place", + "uri": "place:folder=6123443" + }, + { + "guid": "Icg1XlIozA1D", + "title": "Folder Shortcut 3", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 18, + "type": "text/x-moz-place", + "uri": "place:folder=6&folder=BOOKMARKS_MENU" + } + ] + } + ] +} diff --git a/toolkit/components/places/tests/unit/bookmarks.preplaces.html b/toolkit/components/places/tests/unit/bookmarks.preplaces.html new file mode 100644 index 0000000000..0ddf7725b4 --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks.preplaces.html @@ -0,0 +1,36 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<TITLE>Bookmarks</TITLE> +<H1 LAST_MODIFIED="1177541029">Bookmarks</H1> + +<DL><p> + <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3> + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A> + </DL><p> + <HR> + <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3> +<DD>folder test comment + <DL><p> + <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A> +<DD>item description + </DL> + <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3> + <DL><p> + <DT><A HREF="http://example.tld">Example.tld</A> + </DL><p> + <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3> +<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A> + <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A> + <DT><A LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines No Site</A> +<DD>Livemark test comment + </DL><p> +</DL><p> diff --git a/toolkit/components/places/tests/unit/bookmarks_corrupt.json b/toolkit/components/places/tests/unit/bookmarks_corrupt.json new file mode 100644 index 0000000000..93f21d3ece --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks_corrupt.json @@ -0,0 +1,72 @@ +{ + "guid": "root________", + "title": "", + "id": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551978957783, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "id": 2, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551979382837, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "OCyeUO5uu9FG", + "title": "Help and Tutorials", + "id": 7, + "dateAdded": 1361551979356436, + "lastModified": 1361551979362718, + "type": "x/invalid", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/help/" + }, + { + "guid": "OCyeUO5uu9FH", + "index": 1, + "title": "Customize Firefox", + "id": 8, + "dateAdded": 1361551979365662, + "lastModified": 1361551979368077, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/" + }, + { + "guid": "OCyeUO5uu9FG", + "title": "Bad URL", + "id": 9, + "dateAdded": 1361551979356436, + "lastModified": 1361551979362718, + "type": "text/x-moz-place", + "uri": "http:///" + } + ] + }, + { + "guid": "toolbar_____", + "title": "Bookmarks Toolbar", + "id": 2, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551979382837, + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "guid": "OCyeUO5uu9FG", + "title": "Bad URL", + "id": 9, + "dateAdded": 1361551979356436, + "lastModified": 1361551979362718, + "type": "text/x-moz-place", + "uri": "http:///" + } + ] + } + ] +} diff --git a/toolkit/components/places/tests/unit/bookmarks_html_localized.html b/toolkit/components/places/tests/unit/bookmarks_html_localized.html new file mode 100644 index 0000000000..bc3bacc54d --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks_html_localized.html @@ -0,0 +1,21 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<HTML> +<HEAD> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<TITLE>Bookmarks</TITLE> +<LINK REL="localization" HREF="bookmarks_html_localized.ftl"> +</HEAD> +<BODY> +<H1 LAST_MODIFIED="1177541029">Bookmarks</H1> + +<DL><p> + <DT><H3 ID="rdf:#$ZvPhC3" data-l10n-id="bookmarks-html-localized-folder">bookmarks-html-localized-folder</H3> + <DL><p> + <DT><A HREF="http://www.mozilla.com/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1" data-l10n-id="bookmarks-html-localized-bookmark">bookmarks-html-localized-bookmark</A> + </DL><p> +</DL><p> +</BODY> +</HTML> diff --git a/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html new file mode 100644 index 0000000000..9fe662f320 --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> + <HTML> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> + <Title>Bookmarks</Title> + <H1>Bookmarks</H1> + <DT><H3>Subtitle</H3> + <DL><p> + <DT><A HREF="http://www.mozilla.org/">Mozilla</A> + </DL><p> +</HTML> diff --git a/toolkit/components/places/tests/unit/bookmarks_iconuri.json b/toolkit/components/places/tests/unit/bookmarks_iconuri.json new file mode 100644 index 0000000000..4059c1d53f --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks_iconuri.json @@ -0,0 +1,307 @@ +{ + "guid": "root________", + "title": "", + "id": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551978957783, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "id": 2, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551979382837, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "OCyeUO5uu9FF", + "title": "Mozilla Firefox", + "id": 6, + "parent": 2, + "dateAdded": 1361551979350273, + "lastModified": 1361551979376699, + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "OCyeUO5uu9FG", + "title": "Help and Tutorials", + "id": 7, + "parent": 6, + "dateAdded": 1361551979356436, + "lastModified": 1361551979362718, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/help/", + "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FH", + "index": 1, + "title": "Customize Firefox", + "id": 8, + "parent": 6, + "dateAdded": 1361551979365662, + "lastModified": 1361551979368077, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/", + "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FJ", + "index": 3, + "title": "About Us", + "id": 10, + "parent": 6, + "dateAdded": 1361551979376699, + "lastModified": 1361551979379060, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/about/", + "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FI", + "index": 2, + "title": "Get Involved", + "id": 9, + "parent": 6, + "dateAdded": 1361551979371071, + "lastModified": 1361551979373745, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/community/", + "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "QFM-QnE2ZpMz", + "title": "Test null postData", + "index": 4, + "dateAdded": 1481639510868000, + "lastModified": 1489563704300000, + "id": 17, + "charset": "UTF-8", + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "value": "The best" + } + ], + "type": "text/x-moz-place", + "uri": "http://example.com/search?q=%s&suggid=", + "postData": null + } + ] + }, + { + "guid": "OCyeUO5uu9FK", + "index": 1, + "title": "", + "id": 11, + "parent": 2, + "dateAdded": 1361551979380988, + "lastModified": 1361551979380988, + "type": "text/x-moz-place-separator" + }, + { + "guid": "OCyeUO5uu9FL", + "index": 2, + "title": "test", + "id": 12, + "parent": 2, + "dateAdded": 1177541020000000, + "lastModified": 1177541050000000, + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "folder test comment" + } + ], + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "OCyeUO5uu9GX", + "title": "test post keyword", + "id": 13, + "parent": 12, + "dateAdded": 1177375336000000, + "lastModified": 1177375423000000, + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "item description" + }, + { + "name": "bookmarkProperties/loadInSidebar", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 1, + "value": 1 + } + ], + "type": "text/x-moz-place", + "uri": "http://test/post", + "keyword": "test", + "charset": "ISO-8859-1", + "postData": "hidden1%3Dbar&text1%3D%25s" + } + ] + } + ] + }, + { + "index": 1, + "title": "Bookmarks Toolbar", + "id": 3, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1177541050000000, + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar" + } + ], + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "guid": "OCyeUO5uu9FB", + "title": "Getting Started", + "id": 15, + "parent": 3, + "dateAdded": 1361551979409695, + "lastModified": 1361551979412080, + "type": "text/x-moz-place", + "uri": "http://en-us.www.mozilla.com/en-US/firefox/central/", + "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { + "guid": "OCyeUO5uu9FR", + "index": 1, + "title": "Latest Headlines", + "id": 16, + "parent": 3, + "dateAdded": 1361551979451584, + "lastModified": 1361551979457086, + "livemark": 1, + "annos": [ + { + "name": "livemark/feedURI", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" + }, + { + "name": "livemark/siteURI", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" + } + ], + "type": "text/x-moz-place-container", + "children": [] + } + ] + }, + { + "index": 2, + "title": "Tags", + "id": 4, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1361551978957783, + "type": "text/x-moz-place-container", + "root": "tagsFolder", + "children": [] + }, + { + "index": 3, + "title": "Unsorted Bookmarks", + "id": 5, + "parent": 1, + "dateAdded": 1361551978957783, + "lastModified": 1177541050000000, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder", + "children": [ + { + "guid": "OCyeUO5uu9FW", + "title": "Example.tld", + "id": 14, + "parent": 5, + "dateAdded": 1361551979401846, + "lastModified": 1361551979402952, + "type": "text/x-moz-place", + "uri": "http://example.tld/" + }, + { + "guid": "Cfkety492Afk", + "title": "test tagged bookmark", + "id": 15, + "parent": 5, + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "type": "text/x-moz-place", + "uri": "http://example.tld/tagged", + "tags": "foo" + }, + { + "guid": "lOZGoFR1eXbl", + "title": "Bookmarks Toolbar Shortcut", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 16, + "type": "text/x-moz-place", + "uri": "place:folder=TOOLBAR" + }, + { + "guid": "7yJWnBVhjRtP", + "title": "Folder Shortcut", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 17, + "type": "text/x-moz-place", + "uri": "place:folder=6" + }, + { + "guid": "vm5QXWuWc12l", + "title": "Folder Shortcut 2", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 18, + "type": "text/x-moz-place", + "uri": "place:folder=6123443" + }, + { + "guid": "Icg1XlIozA1D", + "title": "Folder Shortcut 3", + "dateAdded": 1507025843703345, + "lastModified": 1507025844703124, + "id": 18, + "type": "text/x-moz-place", + "uri": "place:folder=6&folder=BOOKMARKS_MENU" + } + ] + } + ] +} diff --git a/toolkit/components/places/tests/unit/head_bookmarks.js b/toolkit/components/places/tests/unit/head_bookmarks.js new file mode 100644 index 0000000000..5f6250aa5d --- /dev/null +++ b/toolkit/components/places/tests/unit/head_bookmarks.js @@ -0,0 +1,30 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Import common head. +{ + /* import-globals-from ../head_common.js */ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.init(this, false); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" +); + +add_task(async function setup() { + await AddonTestUtils.promiseStartupManager(); +}); diff --git a/toolkit/components/places/tests/unit/missingBuiltIn.sqlite b/toolkit/components/places/tests/unit/missingBuiltIn.sqlite Binary files differnew file mode 100644 index 0000000000..4dbbb1ac75 --- /dev/null +++ b/toolkit/components/places/tests/unit/missingBuiltIn.sqlite diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json new file mode 100644 index 0000000000..930b7a8382 --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json @@ -0,0 +1,135 @@ +{ + "guid": "root________", + "title": "", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731479000, + "id": 1, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731768000, + "id": 2, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "X6lUyOspVYwi", + "title": "Test Pilot", + "index": 0, + "dateAdded": 1475084731768000, + "lastModified": 1475084731768000, + "id": 3, + "type": "text/x-moz-place", + "uri": "https://testpilot.firefox.com/" + }, + { + "guid": "XF4yRP6bTuil", + "title": "Mobile bookmarks query", + "index": 1, + "dateAdded": 1475084731768000, + "lastModified": 1475084731768000, + "id": 11, + "type": "text/x-moz-place", + "uri": "place:folder=101" + } + ] + }, + { + "guid": "toolbar_____", + "title": "Bookmarks Toolbar", + "index": 1, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 4, + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "guid": "buy7711R3ZgE", + "title": "MDN", + "index": 0, + "dateAdded": 1475084731769000, + "lastModified": 1475084731769000, + "id": 5, + "type": "text/x-moz-place", + "uri": "https://developer.mozilla.org" + } + ] + }, + { + "guid": "3qmd_imziEBE", + "title": "Mobile Bookmarks", + "index": 5, + "dateAdded": 1475084731479000, + "lastModified": 1475084731770000, + "id": 101, + "annos": [ + { + "name": "mobile/bookmarksRoot", + "flags": 0, + "expires": 4, + "value": 1 + }, + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "A description of the mobile folder that should be ignored on import" + } + ], + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "_o8e1_zxTJFg", + "title": "Get Firefox!", + "index": 0, + "dateAdded": 1475084731769000, + "lastModified": 1475084731769000, + "id": 7, + "type": "text/x-moz-place", + "uri": "http://getfirefox.com/" + }, + { + "guid": "QCtSqkVYUbXB", + "title": "Get Thunderbird!", + "index": 1, + "dateAdded": 1475084731770000, + "lastModified": 1475084731770000, + "id": 8, + "type": "text/x-moz-place", + "uri": "http://getthunderbird.com/" + } + ] + }, + { + "guid": "unfiled_____", + "title": "Other Bookmarks", + "index": 3, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 9, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder", + "children": [ + { + "guid": "KIa9iKZab2Z5", + "title": "Add-ons", + "index": 0, + "dateAdded": 1475084731769000, + "lastModified": 1475084731769000, + "id": 10, + "type": "text/x-moz-place", + "uri": "https://addons.mozilla.org" + } + ] + } + ] +} diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json new file mode 100644 index 0000000000..8d376bf69c --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json @@ -0,0 +1,101 @@ +{ + "guid": "root________", + "title": "", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731479000, + "id": 1, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731768000, + "id": 2, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "Utodo9b0oVws", + "title": "Firefox Accounts", + "index": 0, + "dateAdded": 1475084731955000, + "lastModified": 1475084731955000, + "id": 3, + "type": "text/x-moz-place", + "uri": "https://accounts.firefox.com/" + } + ] + }, + { + "guid": "toolbar_____", + "title": "Bookmarks Toolbar", + "index": 1, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 4, + "type": "text/x-moz-place-container", + "root": "toolbarFolder" + }, + { + "guid": "3qmd_imziEBE", + "title": "Mobile Bookmarks", + "index": 5, + "dateAdded": 1475084731479000, + "lastModified": 1475084731770000, + "id": 5, + "annos": [ + { + "name": "mobile/bookmarksRoot", + "flags": 0, + "expires": 4, + "value": 1 + }, + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "A description of the mobile folder that should be ignored on import" + } + ], + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "a17yW6-nTxEJ", + "title": "Mozilla", + "index": 0, + "dateAdded": 1475084731959000, + "lastModified": 1475084731959000, + "id": 6, + "type": "text/x-moz-place", + "uri": "https://mozilla.org/" + }, + { + "guid": "xV10h9Wi3FBM", + "title": "Bugzilla", + "index": 1, + "dateAdded": 1475084731961000, + "lastModified": 1475084731961000, + "id": 7, + "type": "text/x-moz-place", + "uri": "https://bugzilla.mozilla.org/" + } + ] + }, + { + "guid": "unfiled_____", + "title": "Other Bookmarks", + "index": 3, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 8, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder" + } + ] +} diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json new file mode 100644 index 0000000000..3c5cb63194 --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json @@ -0,0 +1,159 @@ +{ + "guid": "root________", + "title": "", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731479000, + "id": 1, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731768000, + "id": 2, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "buy7711R3ZgE", + "title": "MDN", + "index": 0, + "dateAdded": 1475084731769000, + "lastModified": 1475084731769000, + "id": 3, + "type": "text/x-moz-place", + "uri": "https://developer.mozilla.org" + }, + { + "guid": "F_LBgd1fS_uQ", + "title": "Mobile bookmarks query for first folder", + "index": 1, + "dateAdded": 1475084731768000, + "lastModified": 1475084731768000, + "id": 11, + "type": "text/x-moz-place", + "uri": "place:folder=101" + }, + { + "guid": "oIpmQXMWsXvY", + "title": "Mobile bookmarks query for second folder", + "index": 2, + "dateAdded": 1475084731768000, + "lastModified": 1475084731768000, + "id": 12, + "type": "text/x-moz-place", + "uri": "place:folder=102" + } + ] + }, + { + "guid": "3qmd_imziEBE", + "title": "Mobile Bookmarks", + "index": 5, + "dateAdded": 1475084731479000, + "lastModified": 1475084731770000, + "id": 101, + "annos": [ + { + "name": "mobile/bookmarksRoot", + "flags": 0, + "expires": 4, + "value": 1 + }, + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "A description of the mobile folder that should be ignored on import" + } + ], + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "a17yW6-nTxEJ", + "title": "Mozilla", + "index": 0, + "dateAdded": 1475084731959000, + "lastModified": 1475084731959000, + "id": 5, + "type": "text/x-moz-place", + "uri": "https://mozilla.org/" + } + ] + }, + { + "guid": "toolbar_____", + "title": "Bookmarks Toolbar", + "index": 1, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 6, + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "guid": "Utodo9b0oVws", + "title": "Firefox Accounts", + "index": 0, + "dateAdded": 1475084731955000, + "lastModified": 1475084731955000, + "id": 7, + "type": "text/x-moz-place", + "uri": "https://accounts.firefox.com/" + } + ] + }, + { + "guid": "o4YjJpgsufU-", + "title": "Mobile Bookmarks", + "index": 7, + "dateAdded": 1475084731479000, + "lastModified": 1475084731770000, + "id": 102, + "annos": [ + { "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 } + ], + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "sSZ86WT9WbN3", + "title": "DXR", + "index": 0, + "dateAdded": 1475084731769000, + "lastModified": 1475084731769000, + "id": 9, + "type": "text/x-moz-place", + "uri": "https://dxr.mozilla.org" + } + ] + }, + { + "guid": "unfiled_____", + "title": "Other Bookmarks", + "index": 3, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 10, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder", + "children": [ + { + "guid": "xV10h9Wi3FBM", + "title": "Bugzilla", + "index": 1, + "dateAdded": 1475084731961000, + "lastModified": 1475084731961000, + "id": 11, + "type": "text/x-moz-place", + "uri": "https://bugzilla.mozilla.org/" + } + ] + } + ] +} diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json new file mode 100644 index 0000000000..33908e1fea --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json @@ -0,0 +1,89 @@ +{ + "guid": "root________", + "title": "", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731479000, + "id": 1, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731768000, + "id": 2, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "X6lUyOspVYwi", + "title": "Test Pilot", + "index": 0, + "dateAdded": 1475084731768000, + "lastModified": 1475084731768000, + "id": 3, + "type": "text/x-moz-place", + "uri": "https://testpilot.firefox.com/" + } + ] + }, + { + "guid": "toolbar_____", + "title": "Bookmarks Toolbar", + "index": 1, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 4, + "type": "text/x-moz-place-container", + "root": "toolbarFolder" + }, + { + "guid": "unfiled_____", + "title": "Other Bookmarks", + "index": 3, + "dateAdded": 1475084731479000, + "lastModified": 1475084731742000, + "id": 5, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder" + }, + { + "guid": "mobile______", + "title": "Mobile Bookmarks", + "index": 4, + "dateAdded": 1475084731479000, + "lastModified": 1475084731770000, + "id": 6, + "annos": [ + { "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 } + ], + "type": "text/x-moz-place-container", + "root": "mobileFolder", + "children": [ + { + "guid": "_o8e1_zxTJFg", + "title": "Get Firefox!", + "index": 0, + "dateAdded": 1475084731769000, + "lastModified": 1475084731769000, + "id": 7, + "type": "text/x-moz-place", + "uri": "http://getfirefox.com/" + }, + { + "guid": "QCtSqkVYUbXB", + "title": "Get Thunderbird!", + "index": 1, + "dateAdded": 1475084731770000, + "lastModified": 1475084731770000, + "id": 8, + "type": "text/x-moz-place", + "uri": "http://getthunderbird.com/" + } + ] + } + ] +} diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json new file mode 100644 index 0000000000..97af52c44a --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json @@ -0,0 +1,89 @@ +{ + "guid": "root________", + "title": "", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731479000, + "id": 1, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "Bookmarks Menu", + "index": 0, + "dateAdded": 1475084731479000, + "lastModified": 1475084731955000, + "id": 2, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "Utodo9b0oVws", + "title": "Firefox Accounts", + "index": 0, + "dateAdded": 1475084731955000, + "lastModified": 1475084731955000, + "id": 3, + "type": "text/x-moz-place", + "uri": "https://accounts.firefox.com/" + } + ] + }, + { + "guid": "toolbar_____", + "title": "Bookmarks Toolbar", + "index": 1, + "dateAdded": 1475084731479000, + "lastModified": 1475084731938000, + "id": 4, + "type": "text/x-moz-place-container", + "root": "toolbarFolder" + }, + { + "guid": "unfiled_____", + "title": "Other Bookmarks", + "index": 3, + "dateAdded": 1475084731479000, + "lastModified": 1475084731938000, + "id": 5, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder" + }, + { + "guid": "mobile______", + "title": "Mobile Bookmarks", + "index": 4, + "dateAdded": 1475084731479000, + "lastModified": 1475084731961000, + "id": 6, + "annos": [ + { "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 } + ], + "type": "text/x-moz-place-container", + "root": "mobileFolder", + "children": [ + { + "guid": "a17yW6-nTxEJ", + "title": "Mozilla", + "index": 0, + "dateAdded": 1475084731959000, + "lastModified": 1475084731959000, + "id": 7, + "type": "text/x-moz-place", + "uri": "https://mozilla.org/" + }, + { + "guid": "xV10h9Wi3FBM", + "title": "Bugzilla", + "index": 1, + "dateAdded": 1475084731961000, + "lastModified": 1475084731961000, + "id": 8, + "type": "text/x-moz-place", + "uri": "https://bugzilla.mozilla.org/" + } + ] + } + ] +} diff --git a/toolkit/components/places/tests/unit/noRoot.sqlite b/toolkit/components/places/tests/unit/noRoot.sqlite Binary files differnew file mode 100644 index 0000000000..02bd907891 --- /dev/null +++ b/toolkit/components/places/tests/unit/noRoot.sqlite diff --git a/toolkit/components/places/tests/unit/places.sparse.sqlite b/toolkit/components/places/tests/unit/places.sparse.sqlite Binary files differnew file mode 100644 index 0000000000..915089021c --- /dev/null +++ b/toolkit/components/places/tests/unit/places.sparse.sqlite diff --git a/toolkit/components/places/tests/unit/test_1085291.js b/toolkit/components/places/tests/unit/test_1085291.js new file mode 100644 index 0000000000..b7ec4181f0 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_1085291.js @@ -0,0 +1,48 @@ +add_task(async function () { + // test that nodes inserted by incremental update for bookmarks of all types + // have the extra bookmark properties (bookmarkGuid, dateAdded, lastModified). + + // getFolderContents opens the root node. + let root = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid + ).root; + + async function insertAndTest(bmInfo) { + bmInfo = await PlacesUtils.bookmarks.insert(bmInfo); + let node = root.getChild(root.childCount - 1); + Assert.equal(node.bookmarkGuid, bmInfo.guid); + Assert.equal(node.dateAdded, bmInfo.dateAdded * 1000); + Assert.equal(node.lastModified, bmInfo.lastModified * 1000); + } + + // Normal bookmark. + await insertAndTest({ + parentGuid: root.bookmarkGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + title: "Test Bookmark", + url: "http://test.url.tld", + }); + + // place: query + await insertAndTest({ + parentGuid: root.bookmarkGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + title: "Test Query", + url: `place:parent=${PlacesUtils.bookmarks.menuGuid}`, + }); + + // folder + await insertAndTest({ + parentGuid: root.bookmarkGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "Test Folder", + }); + + // separator + await insertAndTest({ + parentGuid: root.bookmarkGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_1105208.js b/toolkit/components/places/tests/unit/test_1105208.js new file mode 100644 index 0000000000..6b3f31f96a --- /dev/null +++ b/toolkit/components/places/tests/unit/test_1105208.js @@ -0,0 +1,25 @@ +// Test that result node for folder shortcuts get the target folder title if +// the shortcut itself has no title set. +add_task(async function () { + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "fake", + }); + + let shortcutInfo = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: `place:parent=${folder.guid}`, + }); + + let unfiledRoot = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.unfiledGuid + ).root; + let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.equal(shortcutNode.bookmarkGuid, shortcutInfo.guid); + + Assert.equal(shortcutNode.title, folder.title); + + unfiledRoot.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_1105866.js b/toolkit/components/places/tests/unit/test_1105866.js new file mode 100644 index 0000000000..76ecfb6b7d --- /dev/null +++ b/toolkit/components/places/tests/unit/test_1105866.js @@ -0,0 +1,77 @@ +add_task(async function test_folder_shortcuts() { + let shortcutInfo = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`, + }); + + let unfiledRoot = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.unfiledGuid + ).root; + let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.strictEqual( + shortcutNode.itemId, + await PlacesUtils.promiseItemId(shortcutInfo.guid) + ); + Assert.strictEqual( + PlacesUtils.asQuery(shortcutNode).folderItemId, + await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.toolbarGuid) + ); + Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid); + Assert.strictEqual( + PlacesUtils.asQuery(shortcutNode).targetFolderGuid, + PlacesUtils.bookmarks.toolbarGuid + ); + + // test that a node added incrementally also behaves just as well. + shortcutInfo = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: `place:parent=${PlacesUtils.bookmarks.menuGuid}`, + }); + shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.strictEqual( + shortcutNode.itemId, + await PlacesUtils.promiseItemId(shortcutInfo.guid) + ); + Assert.strictEqual( + PlacesUtils.asQuery(shortcutNode).folderItemId, + await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.menuGuid) + ); + Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid); + Assert.strictEqual( + PlacesUtils.asQuery(shortcutNode).targetFolderGuid, + PlacesUtils.bookmarks.menuGuid + ); + + unfiledRoot.containerOpen = false; +}); + +add_task(async function test_plain_folder() { + let folderInfo = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + let unfiledRoot = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.unfiledGuid + ).root; + let lastChild = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.strictEqual(lastChild.bookmarkGuid, folderInfo.guid); + Assert.strictEqual( + PlacesUtils.asQuery(lastChild).targetFolderGuid, + folderInfo.guid + ); +}); + +add_task(async function test_non_item_query() { + let options = PlacesUtils.history.getNewQueryOptions(); + let root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + Assert.strictEqual(root.itemId, -1); + Assert.strictEqual(PlacesUtils.asQuery(root).folderItemId, -1); + Assert.strictEqual(root.bookmarkGuid, ""); + Assert.strictEqual(PlacesUtils.asQuery(root).targetFolderGuid, ""); +}); diff --git a/toolkit/components/places/tests/unit/test_1606731.js b/toolkit/components/places/tests/unit/test_1606731.js new file mode 100644 index 0000000000..89e8ec0498 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_1606731.js @@ -0,0 +1,21 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let hs = PlacesUtils.history; + +/* Test for diacritic-insensitive history search */ + +add_task(async function test_execute() { + const TEST_URL = "http://example.net/El_%C3%81rea_51"; + const SEARCH_TERM = "area"; + await PlacesTestUtils.addVisits(uri(TEST_URL)); + let query = hs.getNewQuery(); + query.searchTerms = SEARCH_TERM; + let options = hs.getNewQueryOptions(); + let result = hs.executeQuery(query, options); + result.root.containerOpen = true; + Assert.ok(result.root.childCount == 1); +}); diff --git a/toolkit/components/places/tests/unit/test_331487.js b/toolkit/components/places/tests/unit/test_331487.js new file mode 100644 index 0000000000..2d4f5f8279 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_331487.js @@ -0,0 +1,113 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService + ); +} catch (ex) { + do_throw("Could not get history service\n"); +} + +add_task(async function test_hierarchical_query() { + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "1 title", + url: "http://a1.com/", + }, + { + title: "subfolder 1", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "2 title", + url: "http://a2.com/", + }, + { + title: "subfolder 2", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "3 title", + url: "http://a3.com/", + }, + ], + }, + ], + }, + ], + }, + ], + }); + + let [folderGuid, b1, sf1, b2, sf2, b3] = bookmarks.map( + bookmark => bookmark.guid + ); + + // bookmark query that should result in the "hierarchical" result + // because there is one query, one folder, + // no begin time, no end time, no domain, no uri, no search term + // and no max results. See GetSimpleBookmarksQueryFolder() + // for more details. + var options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + var query = histsvc.getNewQuery(); + query.setParents([folderGuid]); + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 2); + Assert.equal(root.getChild(0).bookmarkGuid, b1); + Assert.equal(root.getChild(1).bookmarkGuid, sf1); + + // check the contents of the subfolder + var sf1Node = root.getChild(1); + sf1Node = sf1Node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + sf1Node.containerOpen = true; + Assert.equal(sf1Node.childCount, 2); + Assert.equal(sf1Node.getChild(0).bookmarkGuid, b2); + Assert.equal(sf1Node.getChild(1).bookmarkGuid, sf2); + + // check the contents of the subfolder's subfolder + var sf2Node = sf1Node.getChild(1); + sf2Node = sf2Node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + sf2Node.containerOpen = true; + Assert.equal(sf2Node.childCount, 1); + Assert.equal(sf2Node.getChild(0).bookmarkGuid, b3); + + sf2Node.containerOpen = false; + sf1Node.containerOpen = false; + root.containerOpen = false; + + // bookmark query that should result in a flat list + // because we specified max results + options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + options.maxResults = 10; + query = histsvc.getNewQuery(); + query.setParents([folderGuid]); + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 3); + Assert.equal(root.getChild(0).bookmarkGuid, b1); + Assert.equal(root.getChild(1).bookmarkGuid, b2); + Assert.equal(root.getChild(2).bookmarkGuid, b3); + root.containerOpen = false; + + // XXX TODO + // test that if we have: more than one query, + // multiple folders, a begin time, an end time, a domain, a uri + // or a search term, that we get the (correct) flat list results + // (like we do when specified maxResults) +}); diff --git a/toolkit/components/places/tests/unit/test_384370.js b/toolkit/components/places/tests/unit/test_384370.js new file mode 100644 index 0000000000..88c0fd5976 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_384370.js @@ -0,0 +1,188 @@ +var tagData = [ + { uri: uri("http://slint.us"), tags: ["indie", "kentucky", "music"] }, + { + uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), + tags: ["dinosaur", "dj", "rad word"], + }, +]; + +var bookmarkData = [ + { uri: uri("http://slint.us"), title: "indie, kentucky, music" }, + { + uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), + title: "dinosaur, dj, rad word", + }, +]; + +/* + HTML+FEATURES SUMMARY: + - import legacy bookmarks + - export as json, import, test (tests integrity of html > json) + - export as html, import, test (tests integrity of json > html) + + BACKUP/RESTORE SUMMARY: + - create a bookmark in each root + - tag multiple URIs with multiple tags + - export as json, import, test +*/ +add_task(async function () { + // Remove eventual bookmarks.exported.json. + let jsonFile = PathUtils.join( + PathUtils.profileDir, + "bookmarks.exported.json" + ); + await IOUtils.remove(jsonFile, { ignoreAbsent: true }); + + // Test importing a pre-Places canonical bookmarks file. + // Note: we do not empty the db before this import to catch bugs like 380999 + let htmlFile = PathUtils.join(do_get_cwd().path, "bookmarks.preplaces.html"); + await BookmarkHTMLUtils.importFromFile(htmlFile, { replace: true }); + + // Populate the database. + for (let { uri, tags } of tagData) { + PlacesUtils.tagging.tagURI(uri, tags); + } + for (let { uri, title } of bookmarkData) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: uri, + title, + }); + } + for (let { uri, title } of bookmarkData) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: uri, + title, + }); + } + + await validate("initial database"); + + // Test exporting a Places canonical json file. + // 1. export to bookmarks.exported.json + await BookmarkJSONUtils.exportToFile(jsonFile); + info("exported json"); + + // 2. empty bookmarks db + // 3. import bookmarks.exported.json + await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true }); + info("imported json"); + + // 4. run the test-suite + await validate("re-imported json"); + info("validated import"); +}); + +async function validate(infoMsg) { + info(`Validating ${infoMsg}: testMenuBookmarks`); + await testMenuBookmarks(); + info(`Validating ${infoMsg}: testToolbarBookmarks`); + await testToolbarBookmarks(); + info(`Validating ${infoMsg}: testUnfiledBookmarks`); + testUnfiledBookmarks(); + info(`Validating ${infoMsg}: testTags`); + testTags(); + await PlacesTestUtils.promiseAsyncUpdates(); +} + +// Tests a bookmarks datastore that has a set of bookmarks, etc +// that flex each supported field and feature. +async function testMenuBookmarks() { + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root; + Assert.equal(root.childCount, 3); + + let separatorNode = root.getChild(1); + Assert.equal(separatorNode.type, separatorNode.RESULT_TYPE_SEPARATOR); + + let folderNode = root.getChild(2); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + Assert.equal(folderNode.title, "test"); + let folder = await PlacesUtils.bookmarks.fetch(folderNode.bookmarkGuid); + Assert.equal(folder.dateAdded.getTime(), 1177541020000); + + Assert.equal(PlacesUtils.asQuery(folderNode).hasChildren, true); + + // open test folder, and test the children + folderNode.containerOpen = true; + Assert.equal(folderNode.childCount, 1); + + let bookmarkNode = folderNode.getChild(0); + Assert.equal("http://test/post", bookmarkNode.uri); + Assert.equal("test post keyword", bookmarkNode.title); + Assert.equal(bookmarkNode.dateAdded, 1177375336000000); + + let entry = await PlacesUtils.keywords.fetch({ url: bookmarkNode.uri }); + Assert.equal("test", entry.keyword); + Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData); + + let pageInfo = await PlacesUtils.history.fetch(bookmarkNode.uri, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + "ISO-8859-1", + "Should have the correct charset" + ); + + folderNode.containerOpen = false; + root.containerOpen = false; +} + +async function testToolbarBookmarks() { + let root = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid + ).root; + + // child count (add 2 for pre-existing items, one of the feeds is skipped + // because it doesn't have href) + Assert.equal(root.childCount, bookmarkData.length + 2); + + // Livemarks are no more supported but may still exist in old html files. + let legacyLivemarkNode = root.getChild(1); + Assert.equal("Latest Headlines", legacyLivemarkNode.title); + Assert.equal( + "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/", + legacyLivemarkNode.uri + ); + Assert.equal( + legacyLivemarkNode.type, + Ci.nsINavHistoryResultNode.RESULT_TYPE_URI + ); + + // test added bookmark data + let bookmarkNode = root.getChild(2); + Assert.equal(bookmarkNode.uri, bookmarkData[0].uri.spec); + Assert.equal(bookmarkNode.title, bookmarkData[0].title); + bookmarkNode = root.getChild(3); + Assert.equal(bookmarkNode.uri, bookmarkData[1].uri.spec); + Assert.equal(bookmarkNode.title, bookmarkData[1].title); + + root.containerOpen = false; +} + +function testUnfiledBookmarks() { + let root = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.unfiledGuid + ).root; + // child count (add 1 for pre-existing item) + Assert.equal(root.childCount, bookmarkData.length + 1); + for (let i = 1; i < root.childCount; ++i) { + let child = root.getChild(i); + Assert.equal(child.uri, bookmarkData[i - 1].uri.spec); + Assert.equal(child.title, bookmarkData[i - 1].title); + if (child.tags) { + Assert.equal(child.tags, bookmarkData[i - 1].title); + } + } + root.containerOpen = false; +} + +function testTags() { + for (let { uri, tags } of tagData) { + info("Test tags for " + uri.spec + ": " + tags + "\n"); + let foundTags = PlacesUtils.tagging.getTagsForURI(uri); + Assert.equal(foundTags.length, tags.length); + Assert.ok(tags.every(tag => foundTags.includes(tag))); + } +} diff --git a/toolkit/components/places/tests/unit/test_385397.js b/toolkit/components/places/tests/unit/test_385397.js new file mode 100644 index 0000000000..7746b89657 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_385397.js @@ -0,0 +1,152 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TOTAL_SITES = 20; + +add_task(async function test_execute() { + let now = (Date.now() - 10000) * 1000; + + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test-" + i + ".com/"; + let testURI = uri(site); + let testImageURI = uri(site + "blank.gif"); + let when = now + i * TOTAL_SITES * 1000; + await PlacesTestUtils.addVisits([ + { uri: testURI, visitDate: when, transition: TRANSITION_TYPED }, + { + uri: testImageURI, + visitDate: when + 1000, + transition: TRANSITION_EMBED, + }, + { + uri: testImageURI, + visitDate: when + 2000, + transition: TRANSITION_FRAMED_LINK, + }, + { uri: testURI, visitDate: when + 3000, transition: TRANSITION_LINK }, + ]); + } + + // verify our visits AS_VISIT, ordered by date descending + // including hidden + // we should get 80 visits: + // http://www.test-19.com/ + // http://www.test-19.com/blank.gif + // http://www.test-19.com/ + // http://www.test-19.com/ + // ... + // http://www.test-0.com/ + // http://www.test-0.com/blank.gif + // http://www.test-0.com/ + // http://www.test-0.com/ + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + options.includeHidden = true; + let root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + root.containerOpen = true; + let cc = root.childCount; + // Embed visits are not added to the database, thus they won't appear. + Assert.equal(cc, 3 * TOTAL_SITES); + for (let i = 0; i < TOTAL_SITES; i++) { + let index = i * 3; + let node = root.getChild(index); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + node = root.getChild(++index); + Assert.equal(node.uri, site + "blank.gif"); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + node = root.getChild(++index); + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // verify our visits AS_VISIT, ordered by date descending + // we should get 40 visits: + // http://www.test-19.com/ + // http://www.test-19.com/ + // ... + // http://www.test-0.com/ + // http://www.test-0.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + root.containerOpen = true; + cc = root.childCount; + // 2 * TOTAL_SITES because we count the TYPED and LINK, but not EMBED or FRAMED + Assert.equal(cc, 2 * TOTAL_SITES); + for (let i = 0; i < TOTAL_SITES; i++) { + let index = i * 2; + let node = root.getChild(index); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + node = root.getChild(++index); + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // test our optimized query for the places menu + // place:type=0&sort=4&maxResults=10 + // verify our visits AS_URI, ordered by date descending + // we should get 10 visits: + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.maxResults = 10; + options.resultType = options.RESULTS_AS_URI; + root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + root.containerOpen = true; + cc = root.childCount; + Assert.equal(cc, options.maxResults); + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // test without a maxResults, which executes a different query + // but the first 10 results should be the same. + // verify our visits AS_URI, ordered by date descending + // we should get 20 visits, but the first 10 should be + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_URI; + root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + root.containerOpen = true; + cc = root.childCount; + Assert.equal(cc, TOTAL_SITES); + for (let i = 0; i < 10; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_399266.js b/toolkit/components/places/tests/unit/test_399266.js new file mode 100644 index 0000000000..6f99c710f8 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_399266.js @@ -0,0 +1,82 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TOTAL_SITES = 20; + +add_task(async function test_execute() { + let places = []; + for (let i = 0; i < TOTAL_SITES; i++) { + for (let j = 0; j <= i; j++) { + places.push({ + uri: uri("http://www.test-" + i + ".com/"), + transition: TRANSITION_TYPED, + }); + // because these are embedded visits, they should not show up on our + // query results. If they do, we have a problem. + places.push({ + uri: uri("http://www.hidden.com/hidden.gif"), + transition: TRANSITION_EMBED, + }); + places.push({ + uri: uri("http://www.alsohidden.com/hidden.gif"), + transition: TRANSITION_FRAMED_LINK, + }); + } + } + await PlacesTestUtils.addVisits(places); + + // test our optimized query for the "Most Visited" item + // in the "Smart Bookmarks" folder + // place:queryType=0&sort=8&maxResults=10 + // verify our visits AS_URI, ordered by visit count descending + // we should get 10 visits: + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING; + options.maxResults = 10; + options.resultType = options.RESULTS_AS_URI; + let root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + root.containerOpen = true; + let cc = root.childCount; + Assert.equal(cc, options.maxResults); + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // test without a maxResults, which executes a different query + // but the first 10 results should be the same. + // verify our visits AS_URI, ordered by visit count descending + // we should get 20 visits, but the first 10 should be + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING; + options.resultType = options.RESULTS_AS_URI; + root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + root.containerOpen = true; + cc = root.childCount; + Assert.equal(cc, TOTAL_SITES); + for (let i = 0; i < 10; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + Assert.equal(node.uri, site); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_402799.js b/toolkit/components/places/tests/unit/test_402799.js new file mode 100644 index 0000000000..f621911cce --- /dev/null +++ b/toolkit/components/places/tests/unit/test_402799.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history services +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService + ); +} catch (ex) { + do_throw("Could not get history services\n"); +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService( + Ci.nsITaggingService + ); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + +add_task(async function test_query_only_returns_bookmarks_not_tags() { + const url = "http://foo.bar/"; + + // create 2 bookmarks on the same uri + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "title 1", + url, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "title 2", + url, + }); + // add some tags + tagssvc.tagURI(uri(url), ["foo", "bar", "foobar", "foo bar"]); + + // check that a generic bookmark query returns only real bookmarks + let options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + + let query = histsvc.getNewQuery(); + let result = histsvc.executeQuery(query, options); + let root = result.root; + + root.containerOpen = true; + let cc = root.childCount; + Assert.equal(cc, 2); + let node1 = root.getChild(0); + node1 = await PlacesUtils.bookmarks.fetch(node1.bookmarkGuid); + Assert.equal(node1.parentGuid, PlacesUtils.bookmarks.menuGuid); + let node2 = root.getChild(1); + node2 = await PlacesUtils.bookmarks.fetch(node2.bookmarkGuid); + Assert.equal(node2.parentGuid, PlacesUtils.bookmarks.toolbarGuid); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_412132.js b/toolkit/components/places/tests/unit/test_412132.js new file mode 100644 index 0000000000..65653b5c0a --- /dev/null +++ b/toolkit/components/places/tests/unit/test_412132.js @@ -0,0 +1,181 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * TEST DESCRIPTION: + * + * Tests patch to Bug 412132: + * https://bugzilla.mozilla.org/show_bug.cgi?id=412132 + */ + +const TEST_URL0 = "http://example.com/"; +const TEST_URL1 = "http://example.com/1"; +const TEST_URL2 = "http://example.com/2"; + +add_task(async function changeuri_unvisited_bookmark() { + info( + "After changing URI of bookmark, frecency of bookmark's " + + "original URI should be zero if original URI is unvisited and " + + "no longer bookmarked." + ); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URL1, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL1, + }), + 0, + "Bookmarked => frecency of URI should be != 0" + ); + + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + url: TEST_URL2, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL1, + }), + 0, + "Unvisited URI no longer bookmarked => frecency should = 0" + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function changeuri_visited_bookmark() { + info( + "After changing URI of bookmark, frecency of bookmark's " + + "original URI should not be zero if original URI is visited." + ); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URL1, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL1, + }), + 0, + "Bookmarked => frecency of URI should be != 0" + ); + + await PlacesTestUtils.addVisits(TEST_URL1); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + url: TEST_URL2, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL1, + }), + 0, + "*Visited* URI no longer bookmarked => frecency should != 0" + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function changeuri_bookmark_still_bookmarked() { + info( + "After changing URI of bookmark, frecency of bookmark's " + + "original URI should not be zero if original URI is still " + + "bookmarked." + ); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark 1 title", + url: TEST_URL1, + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark 2 title", + url: TEST_URL1, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL1, + }), + 0, + "Bookmarked => frecency of URI should be != 0" + ); + + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + url: TEST_URL2, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("URI still bookmarked => frecency should != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL2, + }), + 0 + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function changeuri_nonexistent_bookmark() { + // Try a bogus guid. + await Assert.rejects( + PlacesUtils.bookmarks.update({ + guid: "ABCDEDFGHIJK", + url: TEST_URL2, + }), + /No bookmarks found for the provided GUID/, + "Changing the URI of a non-existent bookmark should fail." + ); + + // Now add a bookmark, delete it, and check. + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URL0, + }); + + await PlacesUtils.bookmarks.remove(bookmark.guid); + + await Assert.rejects( + PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + url: TEST_URL2, + }), + /No bookmarks found for the provided GUID/, + "Changing the URI of a non-existent bookmark should fail." + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); diff --git a/toolkit/components/places/tests/unit/test_415460.js b/toolkit/components/places/tests/unit/test_415460.js new file mode 100644 index 0000000000..3f0f7a1edb --- /dev/null +++ b/toolkit/components/places/tests/unit/test_415460.js @@ -0,0 +1,37 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService +); + +/** + * Checks to see that a search has exactly one result in the database. + * + * @param aTerms + * The terms to search for. + * @returns true if the search returns one result, false otherwise. + */ +function search_has_result(aTerms) { + var options = hs.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI; + var query = hs.getNewQuery(); + query.searchTerms = aTerms; + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + root.containerOpen = false; + return cc == 1; +} + +add_task(async function test_execute() { + const SEARCH_TERM = "ユニコード"; + const TEST_URL = "http://example.com/" + SEARCH_TERM + "/"; + await PlacesTestUtils.addVisits(uri(TEST_URL)); + Assert.ok(search_has_result(SEARCH_TERM)); +}); diff --git a/toolkit/components/places/tests/unit/test_415757.js b/toolkit/components/places/tests/unit/test_415757.js new file mode 100644 index 0000000000..a069bc6aa3 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_415757.js @@ -0,0 +1,92 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Checks to see that a URI is in the database. + * + * @param aURI + * The URI to check. + * @returns true if the URI is in the DB, false otherwise. + */ +function uri_in_db(aURI) { + var options = PlacesUtils.history.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI; + var query = PlacesUtils.history.getNewQuery(); + query.uri = aURI; + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + root.containerOpen = false; + return cc == 1; +} + +const TOTAL_SITES = 20; + +// main +add_task(async function test_execute() { + // add pages to global history + for (let i = 0; i < TOTAL_SITES; i++) { + let uri = "http://www.test-" + i + ".com/"; + let when = Date.now() * 1000 + i * TOTAL_SITES; + await PlacesTestUtils.addVisits({ uri, visitDate: when }); + } + for (let i = 0; i < TOTAL_SITES; i++) { + let uri = "http://www.test.com/" + i + "/"; + let when = Date.now() * 1000 + i * TOTAL_SITES; + await PlacesTestUtils.addVisits({ uri, visitDate: when }); + } + + // set a page annotation on one of the urls that will be removed + var testAnnoDeletedURI = "http://www.test.com/1/"; + var testAnnoDeletedName = "foo"; + var testAnnoDeletedValue = "bar"; + await PlacesUtils.history.update({ + url: testAnnoDeletedURI, + annotations: new Map([[testAnnoDeletedName, testAnnoDeletedValue]]), + }); + + // set a page annotation on one of the urls that will NOT be removed + var testAnnoRetainedURI = "http://www.test-1.com/"; + var testAnnoRetainedName = "foo"; + var testAnnoRetainedValue = "bar"; + await PlacesUtils.history.update({ + url: testAnnoRetainedURI, + annotations: new Map([[testAnnoRetainedName, testAnnoRetainedValue]]), + }); + + // remove pages from www.test.com + await PlacesUtils.history.removeByFilter({ host: "www.test.com" }); + + // check that all pages in www.test.com have been removed + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test.com/" + i + "/"; + let testURI = uri(site); + Assert.ok(!uri_in_db(testURI)); + } + + // check that all pages in www.test-X.com have NOT been removed + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test-" + i + ".com/"; + let testURI = uri(site); + Assert.ok(uri_in_db(testURI)); + } + + // check that annotation on the removed item does not exists + await assertNoOrphanPageAnnotations(); + + // check that annotation on the NOT removed item still exists + let pageInfo = await PlacesUtils.history.fetch(testAnnoRetainedURI, { + includeAnnotations: true, + }); + + Assert.equal( + pageInfo.annotations.get(testAnnoRetainedName), + testAnnoRetainedValue, + "Should have kept the annotation for the non-removed items" + ); +}); diff --git a/toolkit/components/places/tests/unit/test_419792_node_tags_property.js b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js new file mode 100644 index 0000000000..5efadb39fe --- /dev/null +++ b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js @@ -0,0 +1,52 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// get services +var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService +); +var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService( + Ci.nsITaggingService +); + +add_task(async function test_query_node_tags_property() { + // get toolbar node + var options = histsvc.getNewQueryOptions(); + var query = histsvc.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var result = histsvc.executeQuery(query, options); + var toolbarNode = result.root; + toolbarNode.containerOpen = true; + + // add a bookmark + var bookmarkURI = uri("http://foo.com"); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "", + url: bookmarkURI, + }); + + // get the node for the new bookmark + var node = toolbarNode.getChild(toolbarNode.childCount - 1); + Assert.equal(node.bookmarkGuid, bookmark.guid); + + // confirm there's no tags via the .tags property + Assert.equal(node.tags, null); + + // add a tag + tagssvc.tagURI(bookmarkURI, ["foo"]); + Assert.equal(node.tags, "foo"); + + // add another tag, to test delimiter and sorting + tagssvc.tagURI(bookmarkURI, ["bar"]); + Assert.equal(node.tags, "bar, foo"); + + // remove the tags, confirming the property is cleared + tagssvc.untagURI(bookmarkURI, null); + Assert.equal(node.tags, null); + + toolbarNode.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_425563.js b/toolkit/components/places/tests/unit/test_425563.js new file mode 100644 index 0000000000..8ecf8bbcc8 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_425563.js @@ -0,0 +1,76 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function test_execute() { + let count_visited_URIs = [ + "http://www.test-link.com/", + "http://www.test-typed.com/", + "http://www.test-bookmark.com/", + "http://www.test-redirect-permanent.com/", + "http://www.test-redirect-temporary.com/", + ]; + + let notcount_visited_URIs = [ + "http://www.test-download.com/", + "http://www.test-framed.com/", + "http://www.test-reload.com/", + ]; + + // add visits, one for each transition type + await PlacesTestUtils.addVisits([ + { uri: uri("http://www.test-link.com/"), transition: TRANSITION_LINK }, + { uri: uri("http://www.test-typed.com/"), transition: TRANSITION_TYPED }, + { + uri: uri("http://www.test-bookmark.com/"), + transition: TRANSITION_BOOKMARK, + }, + { + uri: uri("http://www.test-framed.com/"), + transition: TRANSITION_FRAMED_LINK, + }, + { + uri: uri("http://www.test-redirect-permanent.com/"), + transition: TRANSITION_REDIRECT_PERMANENT, + }, + { + uri: uri("http://www.test-redirect-temporary.com/"), + transition: TRANSITION_REDIRECT_TEMPORARY, + }, + { + uri: uri("http://www.test-download.com/"), + transition: TRANSITION_DOWNLOAD, + }, + { uri: uri("http://www.test-reload.com/"), transition: TRANSITION_RELOAD }, + ]); + + // check that all links are marked as visited + for (let visited_uri of count_visited_URIs) { + Assert.ok(await PlacesUtils.history.hasVisits(uri(visited_uri))); + } + for (let visited_uri of notcount_visited_URIs) { + Assert.ok(await PlacesUtils.history.hasVisits(uri(visited_uri))); + } + + // check that visit_count does not take in count embed and downloads + // maxVisits query are directly binded to visit_count + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + options.includeHidden = true; + let query = PlacesUtils.history.getNewQuery(); + query.minVisits = 1; + let root = PlacesUtils.history.executeQuery(query, options).root; + + root.containerOpen = true; + let cc = root.childCount; + Assert.equal(cc, count_visited_URIs.length); + + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + Assert.notEqual(count_visited_URIs.indexOf(node.uri), -1); + } + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js new file mode 100644 index 0000000000..d5926d0c17 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + +- add a folder +- add a folder-shortcut to the new folder +- query for the shortcut +- remove the folder-shortcut +- confirm the shortcut is removed from the query results + +*/ + +add_task(async function test_query_with_remove_shortcut() { + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + let query = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "", + url: `place:parent=${folder.guid}`, + }); + + var root = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid, + false, + true + ).root; + + var oldCount = root.childCount; + + await PlacesUtils.bookmarks.remove(query.guid); + + Assert.equal(root.childCount, oldCount - 1); + + root.containerOpen = false; + + await PlacesTestUtils.promiseAsyncUpdates(); +}); diff --git a/toolkit/components/places/tests/unit/test_433317_query_title_update.js b/toolkit/components/places/tests/unit/test_433317_query_title_update.js new file mode 100644 index 0000000000..d8f69064d9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_433317_query_title_update.js @@ -0,0 +1,43 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function test_query_title_update() { + try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService + ); + } catch (ex) { + do_throw("Unable to initialize Places services"); + } + + // create a query bookmark + let bmQuery = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "test query", + url: "place:", + }); + + // query for that query + var options = histsvc.getNewQueryOptions(); + let query = histsvc.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var queryNode = root.getChild(0); + Assert.equal(queryNode.title, "test query"); + + // change the title + await PlacesUtils.bookmarks.update({ + guid: bmQuery.guid, + title: "foo", + }); + + // confirm the node was updated + Assert.equal(queryNode.title, "foo"); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js new file mode 100644 index 0000000000..231722e72b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js @@ -0,0 +1,52 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function test_execute() { + try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService + ); + } catch (ex) { + do_throw("Unable to initialize Places services"); + } + + // add a visit + var testURI = uri("http://test"); + await PlacesTestUtils.addVisits(testURI); + + // query for the visit + var options = histsvc.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI; + var query = histsvc.getNewQuery(); + query.uri = testURI; + var result = histsvc.executeQuery(query, options); + var root = result.root; + + // check hasChildren while the container is closed + Assert.equal(root.hasChildren, true); + + // now check via the saved search path + var queryURI = histsvc.queryToQueryString(query, options); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "test query", + url: queryURI, + }); + + // query for that query + options = histsvc.getNewQueryOptions(); + query = histsvc.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + var queryNode = root.getChild(0); + Assert.equal(queryNode.title, "test query"); + queryNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + Assert.equal(queryNode.hasChildren, true); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_454977.js b/toolkit/components/places/tests/unit/test_454977.js new file mode 100644 index 0000000000..aa6437988f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_454977.js @@ -0,0 +1,121 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Cache actual visit_count value, filled by add_visit, used by check_results +var visit_count = 0; + +// Returns the Place ID corresponding to an added visit. +async function task_add_visit(aURI, aVisitType) { + // Wait for a visits notification and get the visitId. + let visitId; + let visitsPromise = PlacesTestUtils.waitForNotification( + "page-visited", + visits => { + visitId = visits[0].visitId; + let { url } = visits[0]; + return url == aURI.spec; + } + ); + + // Add visits. + await PlacesTestUtils.addVisits([ + { + uri: aURI, + transition: aVisitType, + }, + ]); + + if (aVisitType != TRANSITION_EMBED) { + await visitsPromise; + } + + // Increase visit_count if applicable + if ( + aVisitType != 0 && + aVisitType != TRANSITION_EMBED && + aVisitType != TRANSITION_FRAMED_LINK && + aVisitType != TRANSITION_DOWNLOAD && + aVisitType != TRANSITION_RELOAD + ) { + visit_count++; + } + + // Get the place id + if (visitId > 0) { + let sql = "SELECT place_id FROM moz_historyvisits WHERE id = ?1"; + let stmt = DBConn().createStatement(sql); + stmt.bindByIndex(0, visitId); + Assert.ok(stmt.executeStep()); + let placeId = stmt.getInt64(0); + stmt.finalize(); + Assert.ok(placeId > 0); + return placeId; + } + return 0; +} + +/** + * Checks for results consistency, using visit_count as constraint + * @param aExpectedCount + * Number of history results we are expecting (excluded hidden ones) + * @param aExpectedCountWithHidden + * Number of history results we are expecting (included hidden ones) + */ +function check_results(aExpectedCount, aExpectedCountWithHidden) { + let query = PlacesUtils.history.getNewQuery(); + // used to check visit_count + query.minVisits = visit_count; + query.maxVisits = visit_count; + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + // Children without hidden ones + Assert.equal(root.childCount, aExpectedCount); + root.containerOpen = false; + + // Execute again with includeHidden = true + // This will ensure visit_count is correct + options.includeHidden = true; + root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + // Children with hidden ones + Assert.equal(root.childCount, aExpectedCountWithHidden); + root.containerOpen = false; +} + +// main + +add_task(async function test_execute() { + const TEST_URI = uri("http://test.mozilla.org/"); + + // Add a visit that force hidden + await task_add_visit(TEST_URI, TRANSITION_EMBED); + check_results(0, 0); + + let placeId = await task_add_visit(TEST_URI, TRANSITION_FRAMED_LINK); + check_results(0, 1); + + // Add a visit that force unhide and check the place id. + // - We expect that the place gets hidden = 0 while retaining the same + // place id and a correct visit_count. + Assert.equal(await task_add_visit(TEST_URI, TRANSITION_TYPED), placeId); + check_results(1, 1); + + // Add a visit that should not increase visit_count + Assert.equal(await task_add_visit(TEST_URI, TRANSITION_RELOAD), placeId); + check_results(1, 1); + + // Add a visit that should not increase visit_count + Assert.equal(await task_add_visit(TEST_URI, TRANSITION_DOWNLOAD), placeId); + check_results(1, 1); + + // Add a visit, check that hidden is not overwritten + // - We expect that the place has still hidden = 0, while retaining + // correct visit_count. + await task_add_visit(TEST_URI, TRANSITION_EMBED); + check_results(1, 1); +}); diff --git a/toolkit/components/places/tests/unit/test_463863.js b/toolkit/components/places/tests/unit/test_463863.js new file mode 100644 index 0000000000..d524c00cd3 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_463863.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * TEST DESCRIPTION: + * + * This test checks that in a basic history query all transition types visits + * appear but TRANSITION_EMBED and TRANSITION_FRAMED_LINK ones. + */ + +var transitions = [ + TRANSITION_LINK, + TRANSITION_TYPED, + TRANSITION_BOOKMARK, + TRANSITION_EMBED, + TRANSITION_FRAMED_LINK, + TRANSITION_REDIRECT_PERMANENT, + TRANSITION_REDIRECT_TEMPORARY, + TRANSITION_DOWNLOAD, +]; + +function runQuery(aResultType) { + let options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = aResultType; + let root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), + options + ).root; + root.containerOpen = true; + let cc = root.childCount; + Assert.equal(cc, transitions.length - 2); + + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + // Check that all transition types but EMBED and FRAMED appear in results + Assert.notEqual(node.uri.substr(6, 1), TRANSITION_EMBED); + Assert.notEqual(node.uri.substr(6, 1), TRANSITION_FRAMED_LINK); + } + root.containerOpen = false; +} + +add_task(async function test_execute() { + // add visits, one for each transition type + for (let transition of transitions) { + await PlacesTestUtils.addVisits({ + uri: uri("http://" + transition + ".mozilla.org/"), + transition, + }); + } + + runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT); + runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_URI); +}); diff --git a/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js new file mode 100644 index 0000000000..73bb5e9af4 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js @@ -0,0 +1,19 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + let query = PlacesUtils.history.getNewQuery(); + let options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = options.RESULT_TYPE_QUERY; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + Assert.equal( + PlacesUtils.asQuery(root).query.uri, + null, + "Should be null and not crash the browser" + ); + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js new file mode 100644 index 0000000000..221377e184 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js @@ -0,0 +1,130 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * TEST DESCRIPTION: + * + * This test checks that setting a sort on a RESULTS_AS_DATE_QUERY query, + * children of inside containers are sorted accordingly. + */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService +); + +// Will be inserted in this order, so last one will be the newest visit. +var pages = [ + "http://a.mozilla.org/1/", + "http://a.mozilla.org/2/", + "http://a.mozilla.org/3/", + "http://a.mozilla.org/4/", + "http://b.mozilla.org/5/", + "http://b.mozilla.org/6/", + "http://b.mozilla.org/7/", + "http://b.mozilla.org/8/", +]; + +add_task(async function test_initialize() { + // Add visits. + let now = new Date(); + for (let pageIndex = 0; pageIndex < pages.length; ++pageIndex) { + let page = pages[pageIndex]; + await PlacesTestUtils.addVisits({ + uri: uri(page), + visitDate: new Date(now - (pages.length - pageIndex)), + }); + } +}); + +/** + * Tests that sorting date query by none will sort by title asc. + */ +add_task(function () { + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_QUERY; + // This should sort by title asc. + options.sortingMode = options.SORT_BY_NONE; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var dayContainer = root + .getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayContainer.containerOpen = true; + + var cc = dayContainer.childCount; + Assert.equal(cc, pages.length); + for (var i = 0; i < cc; i++) { + var node = dayContainer.getChild(i); + Assert.equal(pages[i], node.uri); + } + + dayContainer.containerOpen = false; + root.containerOpen = false; +}); + +/** + * Tests that sorting date query by date will sort accordingly. + */ +add_task(function () { + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_QUERY; + // This should sort by title asc. + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var dayContainer = root + .getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayContainer.containerOpen = true; + + var cc = dayContainer.childCount; + Assert.equal(cc, pages.length); + for (var i = 0; i < cc; i++) { + var node = dayContainer.getChild(i); + Assert.equal(pages[pages.length - i - 1], node.uri); + } + + dayContainer.containerOpen = false; + root.containerOpen = false; +}); + +/** + * Tests that sorting date site query by date will still sort by title asc. + */ +add_task(function () { + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_SITE_QUERY; + // This should sort by title asc. + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var dayContainer = root + .getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayContainer.containerOpen = true; + var siteContainer = dayContainer + .getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + Assert.equal(siteContainer.title, "a.mozilla.org"); + siteContainer.containerOpen = true; + + var cc = siteContainer.childCount; + Assert.equal(cc, pages.length / 2); + for (var i = 0; i < cc / 2; i++) { + var node = siteContainer.getChild(i); + Assert.equal(pages[i], node.uri); + } + + siteContainer.containerOpen = false; + dayContainer.containerOpen = false; + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_536081.js b/toolkit/components/places/tests/unit/test_536081.js new file mode 100644 index 0000000000..4105b09d1b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_536081.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_URL = { + u: "http://www.google.com/search?q=testing%3Bthis&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:unofficial&client=firefox-a", + s: "goog", +}; + +add_task(async function () { + print("Testing url: " + TEST_URL.u); + await PlacesTestUtils.addVisits(uri(TEST_URL.u)); + + let query = PlacesUtils.history.getNewQuery(); + query.searchTerms = TEST_URL.s; + let options = PlacesUtils.history.getNewQueryOptions(); + + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + let cc = root.childCount; + Assert.equal(cc, 1); + + print("Checking url is in the query."); + let node = root.getChild(0); + print("Found " + node.uri); + + Assert.ok(await PlacesTestUtils.isPageInDB(TEST_URL.u)); + + root.containerOpen = false; + await PlacesUtils.history.remove(node.uri); + + Assert.equal(false, await PlacesTestUtils.isPageInDB(TEST_URL.u)); +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesDBUtils_removeOldCorruptDBs.js b/toolkit/components/places/tests/unit/test_PlacesDBUtils_removeOldCorruptDBs.js new file mode 100644 index 0000000000..26ec85ffc4 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesDBUtils_removeOldCorruptDBs.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEMP_FILES_TO_CREATE = 5; +const LAST_MODIFICATION_DAY = [5, 10, 15, 20, 25]; +const TEST_CURRENT_TIME = Date.now(); +const MS_PER_DAY = 86400000; +const RETAIN_DAYS = 14; + +async function createfiles() { + for (let i = 0; i < TEMP_FILES_TO_CREATE; i++) { + let setTime = TEST_CURRENT_TIME; + setTime -= LAST_MODIFICATION_DAY[i] * MS_PER_DAY; + let fileName = "places.sqlite" + (i > 0 ? "-" + i : "") + ".corrupt"; + let filePath = PathUtils.join(PathUtils.profileDir, fileName); + await IOUtils.writeUTF8(filePath, "test-file-delete-me", { + tmpPath: filePath + ".tmp", + }); + Assert.ok(await IOUtils.exists(filePath), "file created: " + filePath); + await IOUtils.setModificationTime(filePath, setTime); + } +} + +add_task(async function removefiles() { + await createfiles(); + await PlacesDBUtils.runTasks([PlacesDBUtils.removeOldCorruptDBs]); + for (let i = 0; i < TEMP_FILES_TO_CREATE; i++) { + let fileName = "places.sqlite" + (i > 0 ? "-" + i : "") + ".corrupt"; + let filePath = PathUtils.join(PathUtils.profileDir, fileName); + if (LAST_MODIFICATION_DAY[i] >= RETAIN_DAYS) { + Assert.ok( + !(await IOUtils.exists(filePath)), + "Old corrupt file has been removed" + filePath + ); + } else { + Assert.ok( + await IOUtils.exists(filePath), + "Files that are not old are not removed" + filePath + ); + } + } +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesQuery_history.js b/toolkit/components/places/tests/unit/test_PlacesQuery_history.js new file mode 100644 index 0000000000..02648ead05 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesQuery_history.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PlacesQuery } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesQuery.sys.mjs" +); + +add_task(async function test_visits_cache_is_updated() { + const placesQuery = new PlacesQuery(); + const now = new Date(); + info("Insert the first visit."); + await PlacesUtils.history.insert({ + url: "https://www.example.com/", + title: "Example Domain", + visits: [{ date: now }], + }); + let history = await placesQuery.getHistory(); + Assert.equal(history.length, 1); + Assert.equal(history[0].url, "https://www.example.com/"); + Assert.equal(history[0].date.getTime(), now.getTime()); + Assert.equal(history[0].title, "Example Domain"); + + info("Insert the next visit."); + let historyUpdated = PromiseUtils.defer(); + placesQuery.observeHistory(newHistory => { + history = newHistory; + historyUpdated.resolve(); + }); + await PlacesUtils.history.insert({ + url: "https://example.net/", + visits: [{ date: now }], + }); + await historyUpdated.promise; + Assert.equal(history.length, 2); + Assert.equal( + history[0].url, + "https://example.net/", + "The most recent visit should come first." + ); + Assert.equal(history[0].date.getTime(), now.getTime()); + + info("Remove the first visit."); + historyUpdated = PromiseUtils.defer(); + await PlacesUtils.history.remove("https://www.example.com/"); + await historyUpdated.promise; + Assert.equal(history.length, 1); + Assert.equal(history[0].url, "https://example.net/"); + + info("Remove all visits."); + historyUpdated = PromiseUtils.defer(); + await PlacesUtils.history.clear(); + await historyUpdated.promise; + Assert.equal(history.length, 0); + placesQuery.close(); +}); + +add_task(async function test_filter_visits_by_age() { + const placesQuery = new PlacesQuery(); + await PlacesUtils.history.insertMany([ + { + url: "https://www.example.com/", + visits: [{ date: new Date("2000-01-01T12:00:00") }], + }, + { + url: "https://example.net/", + visits: [{ date: new Date() }], + }, + ]); + const history = await placesQuery.getHistory({ daysOld: 1 }); + Assert.equal(history.length, 1, "The older visit should be excluded."); + Assert.equal(history[0].url, "https://example.net/"); + await PlacesUtils.history.clear(); + placesQuery.close(); +}); + +add_task(async function test_filter_redirecting_visits() { + const placesQuery = new PlacesQuery(); + await PlacesUtils.history.insertMany([ + { + url: "http://google.com/", + visits: [{ transition: PlacesUtils.history.TRANSITIONS.TYPED }], + }, + { + url: "https://www.google.com/", + visits: [ + { + transition: PlacesUtils.history.TRANSITIONS.REDIRECT_PERMANENT, + referrer: "http://google.com/", + }, + ], + }, + ]); + const history = await placesQuery.getHistory(); + Assert.equal(history.length, 1, "Redirecting visits should be excluded."); + Assert.equal(history[0].url, "https://www.google.com/"); + await PlacesUtils.history.clear(); + placesQuery.close(); +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js new file mode 100644 index 0000000000..8ec7eccfee --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js @@ -0,0 +1,32 @@ +add_task(async function () { + info("Add a bookmark."); + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let id = await PlacesUtils.promiseItemId(bm.guid); + Assert.equal(await PlacesUtils.promiseItemGuid(id), bm.guid); + + // Ensure invalidating a non-existent itemId doesn't throw. + PlacesUtils.invalidateCachedGuidFor(null); + PlacesUtils.invalidateCachedGuidFor(9999); + + info("Change the GUID."); + await PlacesUtils.withConnectionWrapper("test", async function (db) { + await db.execute("UPDATE moz_bookmarks SET guid = :guid WHERE id = :id", { + guid: "123456789012", + id, + }); + }); + // The cache should still point to the wrong id. + Assert.equal(await PlacesUtils.promiseItemGuid(id), bm.guid); + + info("Invalidate the cache."); + PlacesUtils.invalidateCachedGuidFor(id); + Assert.equal(await PlacesUtils.promiseItemGuid(id), "123456789012"); + Assert.equal(await PlacesUtils.promiseItemId("123456789012"), id); + await Assert.rejects( + PlacesUtils.promiseItemId(bm.guid), + /no item found for the given GUID/ + ); +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuids.js b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuids.js new file mode 100644 index 0000000000..72304e8524 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuids.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +async function changeGuid(db, id, newGuid) { + await db.execute( + `UPDATE moz_bookmarks SET + guid = :newGuid + WHERE id = :id`, + { id, newGuid } + ); +} + +add_task(async function test_invalidateCachedGuids() { + info("Add bookmarks"); + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + guid: "bookmarkAAAA", + url: "http://example.com/a", + title: "A", + }, + { + guid: "bookmarkBBBB", + url: "http://example.com/b", + title: "B", + }, + ], + }); + + let ids = await PlacesUtils.promiseManyItemIds([ + "bookmarkAAAA", + "bookmarkBBBB", + ]); + Assert.equal( + await PlacesUtils.promiseItemGuid(ids.get("bookmarkAAAA")), + "bookmarkAAAA" + ); + Assert.equal( + await PlacesUtils.promiseItemGuid(ids.get("bookmarkBBBB")), + "bookmarkBBBB" + ); + + info("Change GUIDs"); + await PlacesUtils.withConnectionWrapper( + "test_invalidateCachedGuids", + async function (db) { + await db.executeTransaction(async function () { + await changeGuid(db, ids.get("bookmarkAAAA"), "bookmarkCCCC"); + await changeGuid(db, ids.get("bookmarkBBBB"), "bookmarkDDDD"); + }); + } + ); + Assert.equal( + await PlacesUtils.promiseItemId("bookmarkAAAA"), + ids.get("bookmarkAAAA") + ); + Assert.equal( + await PlacesUtils.promiseItemId("bookmarkBBBB"), + ids.get("bookmarkBBBB") + ); + + info("Invalidate the cache"); + PlacesUtils.invalidateCachedGuids(); + + let newIds = await PlacesUtils.promiseManyItemIds([ + "bookmarkCCCC", + "bookmarkDDDD", + ]); + Assert.equal( + await PlacesUtils.promiseItemGuid(newIds.get("bookmarkCCCC")), + "bookmarkCCCC" + ); + Assert.equal( + await PlacesUtils.promiseItemGuid(newIds.get("bookmarkDDDD")), + "bookmarkDDDD" + ); + await Assert.rejects( + PlacesUtils.promiseItemId("bookmarkAAAA"), + /no item found for the given GUID/ + ); + await Assert.rejects( + PlacesUtils.promiseItemId("bookmarkBBBB"), + /no item found for the given GUID/ + ); +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_isRootItem.js b/toolkit/components/places/tests/unit/test_PlacesUtils_isRootItem.js new file mode 100644 index 0000000000..f626d437e0 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesUtils_isRootItem.js @@ -0,0 +1,21 @@ +"use strict"; + +const GUIDS = [ + PlacesUtils.bookmarks.rootGuid, + ...PlacesUtils.bookmarks.userContentRoots, + PlacesUtils.bookmarks.tagsGuid, +]; + +add_task(async function test_isRootItem() { + for (let guid of GUIDS) { + Assert.ok( + PlacesUtils.isRootItem(guid), + `Should correctly identify root item ${guid}` + ); + } + + Assert.ok( + !PlacesUtils.isRootItem("fakeguid1234"), + "Should not identify other items as root." + ); +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_unwrapNodes_place.js b/toolkit/components/places/tests/unit/test_PlacesUtils_unwrapNodes_place.js new file mode 100644 index 0000000000..8944dc22f4 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesUtils_unwrapNodes_place.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that unwrapNodes properly filters out place: uris from text flavors. + +add_task(function () { + let tests = [ + // Single url. + ["place:type=0&sort=1:", PlacesUtils.TYPE_X_MOZ_URL], + // Multiple urls. + [ + "place:type=0&sort=1:\nfirst\nplace:type=0&sort=1\nsecond", + PlacesUtils.TYPE_X_MOZ_URL, + ], + // Url == title. + ["place:type=0&sort=1:\nplace:type=0&sort=1", PlacesUtils.TYPE_X_MOZ_URL], + // Malformed. + [ + "place:type=0&sort=1:\nplace:type=0&sort=1\nmalformed", + PlacesUtils.TYPE_X_MOZ_URL, + ], + // Single url. + ["place:type=0&sort=1:", PlacesUtils.TYPE_PLAINTEXT], + // Multiple urls. + ["place:type=0&sort=1:\nplace:type=0&sort=1", PlacesUtils.TYPE_PLAINTEXT], + ]; + for (let [blob, type] of tests) { + Assert.deepEqual( + PlacesUtils.unwrapNodes(blob, type), + [], + "No valid entries should be found" + ); + } +}); diff --git a/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js new file mode 100644 index 0000000000..084415fb37 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This is a test for asyncExecuteLegacyQuery API. + +add_task(async function test_history_query() { + let uri = "http://test.visit.mozilla.com/"; + let title = "Test visit"; + await PlacesTestUtils.addVisits({ uri, title }); + + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING; + let query = PlacesUtils.history.getNewQuery(); + + return new Promise(resolve => { + PlacesUtils.history.asyncExecuteLegacyQuery(query, options, { + handleResult(aResultSet) { + for (let row; (row = aResultSet.getNextRow()); ) { + try { + Assert.equal(row.getResultByIndex(1), uri); + Assert.equal(row.getResultByIndex(2), title); + } catch (e) { + do_throw("Error while fetching page data."); + } + } + }, + handleError(aError) { + do_throw( + "Async execution error (" + aError.result + "): " + aError.message + ); + }, + handleCompletion(aReason) { + cleanupTest().then(resolve); + }, + }); + }); +}); + +add_task(async function test_bookmarks_query() { + let url = "http://test.bookmark.mozilla.com/"; + let title = "Test bookmark"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title, + url, + }); + + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = + Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING; + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let query = PlacesUtils.history.getNewQuery(); + + return new Promise(resolve => { + PlacesUtils.history.asyncExecuteLegacyQuery(query, options, { + handleResult(aResultSet) { + for (let row; (row = aResultSet.getNextRow()); ) { + try { + Assert.equal(row.getResultByIndex(1), url); + Assert.equal(row.getResultByIndex(2), title); + } catch (e) { + do_throw("Error while fetching page data."); + } + } + }, + handleError(aError) { + do_throw( + "Async execution error (" + aError.result + "): " + aError.message + ); + }, + handleCompletion(aReason) { + cleanupTest().then(resolve); + }, + }); + }); +}); + +function cleanupTest() { + return Promise.all([ + PlacesUtils.history.clear(), + PlacesUtils.bookmarks.eraseEverything(), + ]); +} diff --git a/toolkit/components/places/tests/unit/test_async_transactions.js b/toolkit/components/places/tests/unit/test_async_transactions.js new file mode 100644 index 0000000000..71701ec540 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_async_transactions.js @@ -0,0 +1,2214 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const bmsvc = PlacesUtils.bookmarks; +const obsvc = PlacesUtils.observers; +const tagssvc = PlacesUtils.tagging; +const PT = PlacesTransactions; +const menuGuid = PlacesUtils.bookmarks.menuGuid; + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +// Create and add bookmarks observer. +var observer = { + tagRelatedGuids: new Set(), + + reset() { + this.itemsAdded = new Map(); + this.itemsRemoved = new Map(); + this.itemsMoved = new Map(); + this.itemsKeywordChanged = new Map(); + this.itemsTitleChanged = new Map(); + this.itemsUrlChanged = new Map(); + }, + + handlePlacesEvents(events) { + for (let event of events) { + switch (event.type) { + case "bookmark-added": + // Ignore tag items. + if (event.isTagging) { + this.tagRelatedGuids.add(event.guid); + return; + } + + this.itemsAdded.set(event.guid, { + itemId: event.id, + parentGuid: event.parentGuid, + index: event.index, + itemType: event.itemType, + title: event.title, + url: event.url, + }); + break; + case "bookmark-removed": + if (this.tagRelatedGuids.has(event.guid)) { + return; + } + + this.itemsRemoved.set(event.guid, { + parentGuid: event.parentGuid, + index: event.index, + itemType: event.itemType, + }); + break; + case "bookmark-moved": + this.itemsMoved.set(event.guid, { + oldParentGuid: event.oldParentGuid, + oldIndex: event.oldIndex, + newParentGuid: event.parentGuid, + newIndex: event.index, + itemType: event.itemType, + }); + break; + case "bookmark-keyword-changed": + if (this.tagRelatedGuids.has(event.guid)) { + return; + } + + this.itemsKeywordChanged.set(event.guid, { + keyword: event.keyword, + }); + break; + case "bookmark-title-changed": + if (this.tagRelatedGuids.has(event.guid)) { + return; + } + + this.itemsTitleChanged.set(event.guid, { + title: event.title, + parentGuid: event.parentGuid, + }); + break; + case "bookmark-url-changed": + if (this.tagRelatedGuids.has(event.guid)) { + return; + } + + this.itemsUrlChanged.set(event.guid, { + url: event.url, + }); + break; + } + } + }, +}; +observer.reset(); + +// index at which items should begin +var bmStartIndex = 0; + +function run_test() { + observer.handlePlacesEvents = observer.handlePlacesEvents.bind(observer); + obsvc.addListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-keyword-changed", + "bookmark-title-changed", + "bookmark-url-changed", + ], + observer.handlePlacesEvents + ); + registerCleanupFunction(function () { + obsvc.removeListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-keyword-changed", + "bookmark-title-changed", + "bookmark-url-changed", + ], + observer.handlePlacesEvents + ); + }); + + run_next_test(); +} + +function sanityCheckTransactionHistory() { + Assert.ok(PT.undoPosition <= PT.length); + + let check_entry_throws = f => { + try { + f(); + do_throw("PT.entry should throw for invalid input"); + } catch (ex) {} + }; + check_entry_throws(() => PT.entry(-1)); + check_entry_throws(() => PT.entry({})); + check_entry_throws(() => PT.entry(PT.length)); + + if (PT.undoPosition < PT.length) { + Assert.equal(PT.topUndoEntry, PT.entry(PT.undoPosition)); + } else { + Assert.equal(null, PT.topUndoEntry); + } + if (PT.undoPosition > 0) { + Assert.equal(PT.topRedoEntry, PT.entry(PT.undoPosition - 1)); + } else { + Assert.equal(null, PT.topRedoEntry); + } +} + +function getTransactionsHistoryState() { + let history = []; + for (let i = 0; i < PT.length; i++) { + history.push(PT.entry(i)); + } + return [history, PT.undoPosition]; +} + +function ensureUndoState(aExpectedEntries = [], aExpectedUndoPosition = 0) { + // ensureUndoState is called in various places during this test, so it's + // a good places to sanity-check the transaction-history APIs in all + // cases. + sanityCheckTransactionHistory(); + + let [actualEntries, actualUndoPosition] = getTransactionsHistoryState(); + Assert.equal(actualEntries.length, aExpectedEntries.length); + Assert.equal(actualUndoPosition, aExpectedUndoPosition); + + function checkEqualEntries(aExpectedEntry, aActualEntry) { + Assert.equal(aExpectedEntry.length, aActualEntry.length); + aExpectedEntry.forEach((t, i) => Assert.equal(t, aActualEntry[i])); + } + aExpectedEntries.forEach((e, i) => checkEqualEntries(e, actualEntries[i])); +} + +function ensureItemsAdded(...items) { + let expectedResultsCount = items.length; + + for (let item of items) { + if ("children" in item) { + expectedResultsCount += item.children.length; + } + Assert.ok( + observer.itemsAdded.has(item.guid), + `Should have the expected guid ${item.guid}` + ); + let info = observer.itemsAdded.get(item.guid); + Assert.equal( + info.parentGuid, + item.parentGuid, + "Should have notified the correct parentGuid" + ); + for (let propName of ["title", "index", "itemType"]) { + if (propName in item) { + Assert.equal(info[propName], item[propName]); + } + } + if ("url" in item) { + Assert.ok( + Services.io.newURI(info.url).equals(Services.io.newURI(item.url)), + "Should have the correct url" + ); + } + } + + Assert.equal( + observer.itemsAdded.size, + expectedResultsCount, + "Should have added the correct number of items" + ); +} + +function ensureItemsRemoved(...items) { + let expectedResultsCount = items.length; + + for (let item of items) { + // We accept both guids and full info object here. + if (typeof item == "string") { + Assert.ok( + observer.itemsRemoved.has(item), + `Should have removed the expected guid ${item}` + ); + } else { + if ("children" in item) { + expectedResultsCount += item.children.length; + } + + Assert.ok( + observer.itemsRemoved.has(item.guid), + `Should have removed expected guid ${item.guid}` + ); + let info = observer.itemsRemoved.get(item.guid); + Assert.equal( + info.parentGuid, + item.parentGuid, + "Should have notified the correct parentGuid" + ); + if ("index" in item) { + Assert.equal(info.index, item.index); + } + } + } + + Assert.equal( + observer.itemsRemoved.size, + expectedResultsCount, + "Should have removed the correct number of items" + ); +} + +function ensureItemsMoved(...items) { + Assert.equal( + observer.itemsMoved.size, + items.length, + "Should have received the correct number of moved notifications" + ); + for (let item of items) { + Assert.ok( + observer.itemsMoved.has(item.guid), + `Observer should have a move for ${item.guid}` + ); + let info = observer.itemsMoved.get(item.guid); + Assert.equal( + info.oldParentGuid, + item.oldParentGuid, + "Should have the correct old parent guid" + ); + Assert.equal( + info.oldIndex, + item.oldIndex, + "Should have the correct old index" + ); + Assert.equal( + info.newParentGuid, + item.newParentGuid, + "Should have the correct new parent guid" + ); + Assert.equal( + info.newIndex, + item.newIndex, + "Should have the correct new index" + ); + } +} + +function ensureItemsKeywordChanged(...items) { + for (const item of items) { + Assert.ok( + observer.itemsKeywordChanged.has(item.guid), + `Observer should have a keyword changed for ${item.guid}` + ); + const info = observer.itemsKeywordChanged.get(item.guid); + Assert.equal(info.keyword, item.keyword, "Should have the correct keyword"); + } +} + +function ensureItemsTitleChanged(...items) { + Assert.equal( + observer.itemsTitleChanged.size, + items.length, + "Should have received the correct number of bookmark-title-changed notifications" + ); + for (const item of items) { + Assert.ok( + observer.itemsTitleChanged.has(item.guid), + `Observer should have a title changed for ${item.guid}` + ); + const info = observer.itemsTitleChanged.get(item.guid); + Assert.equal(info.title, item.title, "Should have the correct title"); + Assert.equal( + info.parentGuid, + item.parentGuid, + "Should have the correct parent guid" + ); + } +} + +function ensureItemsUrlChanged(...items) { + Assert.equal( + observer.itemsUrlChanged.size, + items.length, + "Should have received the correct number of bookmark-url-changed notifications" + ); + for (const item of items) { + Assert.ok( + observer.itemsUrlChanged.has(item.guid), + `Observer should have a title changed for ${item.guid}` + ); + const info = observer.itemsUrlChanged.get(item.guid); + Assert.equal(info.url, item.url, "Should have the correct url"); + } +} + +function ensureTagsForURI(aURI, aTags) { + let tagsSet = tagssvc.getTagsForURI(Services.io.newURI(aURI)); + Assert.equal(tagsSet.length, aTags.length); + Assert.ok(aTags.every(t => tagsSet.includes(t))); +} + +function createTestFolderInfo( + title = "Test Folder", + parentGuid = menuGuid, + children = undefined +) { + let info = { parentGuid, title }; + if (children) { + info.children = children; + } + return info; +} + +function removeAllDatesInTree(tree) { + if ("lastModified" in tree) { + delete tree.lastModified; + } + if ("dateAdded" in tree) { + delete tree.dateAdded; + } + + if (!tree.children) { + return; + } + + for (let child of tree.children) { + removeAllDatesInTree(child); + } +} + +// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the +// same. +// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set. +async function ensureEqualBookmarksTrees( + aOriginal, + aNew, + aIsRestoredItem = true, + aCheckParentAndPosition = false, + aIgnoreAllDates = false +) { + // Note "id" is not-enumerable, and is therefore skipped by Object.keys (both + // ours and the one at deepEqual). This is fine for us because ids are not + // restored by Redo. + if (aIsRestoredItem) { + if (aIgnoreAllDates) { + removeAllDatesInTree(aOriginal); + removeAllDatesInTree(aNew); + } else if (!aOriginal.lastModified) { + // Ignore lastModified for newly created items, for performance reasons. + aNew.lastModified = aOriginal.lastModified; + } + Assert.deepEqual(aOriginal, aNew); + return; + } + + for (let property of Object.keys(aOriginal)) { + if (property == "children") { + Assert.equal(aOriginal.children.length, aNew.children.length); + for (let i = 0; i < aOriginal.children.length; i++) { + await ensureEqualBookmarksTrees( + aOriginal.children[i], + aNew.children[i], + false, + true, + aIgnoreAllDates + ); + } + } else if (property == "guid") { + // guid shouldn't be copied if the item was not restored. + Assert.notEqual(aOriginal.guid, aNew.guid); + } else if (property == "dateAdded") { + // dateAdded shouldn't be copied if the item was not restored. + Assert.ok(is_time_ordered(aOriginal.dateAdded, aNew.dateAdded)); + } else if (property == "lastModified") { + // same same, except for the never-changed case + if (!aOriginal.lastModified) { + Assert.ok(!aNew.lastModified); + } else { + Assert.ok(is_time_ordered(aOriginal.lastModified, aNew.lastModified)); + } + } else if ( + aCheckParentAndPosition || + (property != "parentGuid" && property != "index") + ) { + Assert.deepEqual(aOriginal[property], aNew[property]); + } + } +} + +async function ensureBookmarksTreeRestoredCorrectly( + ...aOriginalBookmarksTrees +) { + for (let originalTree of aOriginalBookmarksTrees) { + let restoredTree = await PlacesUtils.promiseBookmarksTree( + originalTree.guid + ); + await ensureEqualBookmarksTrees(originalTree, restoredTree); + } +} + +async function ensureBookmarksTreeRestoredCorrectlyExceptDates( + ...aOriginalBookmarksTrees +) { + for (let originalTree of aOriginalBookmarksTrees) { + let restoredTree = await PlacesUtils.promiseBookmarksTree( + originalTree.guid + ); + await ensureEqualBookmarksTrees( + originalTree, + restoredTree, + true, + false, + true + ); + } +} + +async function ensureNonExistent(...aGuids) { + for (let guid of aGuids) { + Assert.strictEqual(await PlacesUtils.promiseBookmarksTree(guid), null); + } +} + +add_task(async function test_recycled_transactions() { + async function ensureTransactThrowsFor(aTransaction) { + let [txns, undoPosition] = getTransactionsHistoryState(); + try { + await aTransaction.transact(); + do_throw("Shouldn't be able to use the same transaction twice"); + } catch (ex) {} + ensureUndoState(txns, undoPosition); + } + + let txn_a = PT.NewFolder(createTestFolderInfo()); + await txn_a.transact(); + ensureUndoState([[txn_a]], 0); + await ensureTransactThrowsFor(txn_a); + + await PT.undo(); + ensureUndoState([[txn_a]], 1); + ensureTransactThrowsFor(txn_a); + + await PT.clearTransactionsHistory(); + ensureUndoState(); + ensureTransactThrowsFor(txn_a); + + let txn_b = PT.NewFolder(createTestFolderInfo()); + await PT.batch(async function () { + try { + await txn_a.transact(); + do_throw("Shouldn't be able to use the same transaction twice"); + } catch (ex) {} + ensureUndoState(); + await txn_b.transact(); + }); + ensureUndoState([[txn_b]], 0); + + await PT.undo(); + ensureUndoState([[txn_b]], 1); + ensureTransactThrowsFor(txn_a); + ensureTransactThrowsFor(txn_b); + + await PT.clearTransactionsHistory(); + ensureUndoState(); + observer.reset(); +}); + +add_task(async function test_new_folder_with_children() { + let folder_info = createTestFolderInfo( + "Test folder", + PlacesUtils.bookmarks.menuGuid, + [ + { + url: "http://test_create_item.com", + title: "Test creating an item", + }, + ] + ); + ensureUndoState(); + let txn = PT.NewFolder(folder_info); + folder_info.guid = await txn.transact(); + let originalInfo = await PlacesUtils.promiseBookmarksTree(folder_info.guid); + let ensureDo = async function (aRedo = false) { + ensureUndoState([[txn]], 0); + ensureItemsAdded(folder_info); + if (aRedo) { + // Ignore lastModified in the comparison, for performance reasons. + originalInfo.lastModified = null; + await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo); + } + observer.reset(); + }; + + let ensureUndo = () => { + ensureUndoState([[txn]], 1); + ensureItemsRemoved({ + guid: folder_info.guid, + parentGuid: folder_info.parentGuid, + index: bmStartIndex, + children: [ + { + title: "Test creating an item", + url: "http://test_create_item.com", + }, + ], + }); + observer.reset(); + }; + + await ensureDo(); + await PT.undo(); + await ensureUndo(); + await PT.redo(); + await ensureDo(true); + await PT.undo(); + ensureUndo(); + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_new_bookmark() { + let bm_info = { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://test_create_item.com", + index: bmStartIndex, + title: "Test creating an item", + }; + + ensureUndoState(); + let txn = PT.NewBookmark(bm_info); + bm_info.guid = await txn.transact(); + + let originalInfo = await PlacesUtils.promiseBookmarksTree(bm_info.guid); + let ensureDo = async function (aRedo = false) { + ensureUndoState([[txn]], 0); + await ensureItemsAdded(bm_info); + if (aRedo) { + await ensureBookmarksTreeRestoredCorrectly(originalInfo); + } + observer.reset(); + }; + let ensureUndo = () => { + ensureUndoState([[txn]], 1); + ensureItemsRemoved({ + guid: bm_info.guid, + parentGuid: bm_info.parentGuid, + index: bmStartIndex, + }); + observer.reset(); + }; + + await ensureDo(); + await PT.undo(); + ensureUndo(); + await PT.redo(true); + await ensureDo(); + await PT.undo(); + ensureUndo(); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_merge_create_folder_and_item() { + let folder_info = createTestFolderInfo(); + let bm_info = { + url: "http://test_create_item_to_folder.com", + title: "Test Bookmark", + index: bmStartIndex, + }; + + let [folderTxnResult, bkmTxnResult] = await PT.batch(async function () { + let folderTxn = PT.NewFolder(folder_info); + folder_info.guid = bm_info.parentGuid = await folderTxn.transact(); + let bkmTxn = PT.NewBookmark(bm_info); + bm_info.guid = await bkmTxn.transact(); + return [folderTxn, bkmTxn]; + }); + + let ensureDo = async function () { + ensureUndoState([[bkmTxnResult, folderTxnResult]], 0); + await ensureItemsAdded(folder_info, bm_info); + observer.reset(); + }; + + let ensureUndo = () => { + ensureUndoState([[bkmTxnResult, folderTxnResult]], 1); + ensureItemsRemoved(folder_info, bm_info); + observer.reset(); + }; + + await ensureDo(); + await PT.undo(); + ensureUndo(); + await PT.redo(); + await ensureDo(); + await PT.undo(); + ensureUndo(); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_move_items_to_folder() { + let folder_a_info = createTestFolderInfo("Folder A"); + let bkm_a_info = { url: "http://test_move_items.com", title: "Bookmark A" }; + let bkm_b_info = { url: "http://test_move_items.com", title: "Bookmark B" }; + + // Test moving items within the same folder. + let [folder_a_txn_result, bkm_a_txn_result, bkm_b_txn_result] = + await PT.batch(async function () { + let folder_a_txn = PT.NewFolder(folder_a_info); + + folder_a_info.guid = + bkm_a_info.parentGuid = + bkm_b_info.parentGuid = + await folder_a_txn.transact(); + let bkm_a_txn = PT.NewBookmark(bkm_a_info); + bkm_a_info.guid = await bkm_a_txn.transact(); + let bkm_b_txn = PT.NewBookmark(bkm_b_info); + bkm_b_info.guid = await bkm_b_txn.transact(); + return [folder_a_txn, bkm_a_txn, bkm_b_txn]; + }); + + ensureUndoState( + [[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], + 0 + ); + + let moveTxn = PT.Move({ + guid: bkm_a_info.guid, + newParentGuid: folder_a_info.guid, + }); + await moveTxn.transact(); + + let ensureDo = () => { + ensureUndoState( + [[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], + 0 + ); + ensureItemsMoved({ + guid: bkm_a_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 0, + newIndex: 1, + }); + observer.reset(); + }; + let ensureUndo = () => { + ensureUndoState( + [[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], + 1 + ); + ensureItemsMoved({ + guid: bkm_a_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 1, + newIndex: 0, + }); + observer.reset(); + }; + + ensureDo(); + await PT.undo(); + ensureUndo(); + await PT.redo(); + ensureDo(); + await PT.undo(); + ensureUndo(); + + await PT.clearTransactionsHistory(false, true); + ensureUndoState( + [[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], + 0 + ); + + // Test moving items between folders. + let folder_b_info = createTestFolderInfo("Folder B"); + let folder_b_txn = PT.NewFolder(folder_b_info); + folder_b_info.guid = await folder_b_txn.transact(); + ensureUndoState( + [[folder_b_txn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], + 0 + ); + + moveTxn = PT.Move({ + guid: bkm_a_info.guid, + newParentGuid: folder_b_info.guid, + newIndex: bmsvc.DEFAULT_INDEX, + }); + await moveTxn.transact(); + + ensureDo = () => { + ensureUndoState( + [ + [moveTxn], + [folder_b_txn], + [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result], + ], + 0 + ); + ensureItemsMoved({ + guid: bkm_a_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_b_info.guid, + oldIndex: 0, + newIndex: 0, + }); + observer.reset(); + }; + ensureUndo = () => { + ensureUndoState( + [ + [moveTxn], + [folder_b_txn], + [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result], + ], + 1 + ); + ensureItemsMoved({ + guid: bkm_a_info.guid, + oldParentGuid: folder_b_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 0, + newIndex: 0, + }); + observer.reset(); + }; + + ensureDo(); + await PT.undo(); + ensureUndo(); + await PT.redo(); + ensureDo(); + await PT.undo(); + ensureUndo(); + + // Clean up + await PT.undo(); // folder_b_txn + await PT.undo(); // folder_a_txn + the bookmarks; + Assert.equal(observer.itemsRemoved.size, 4); + ensureUndoState( + [ + [moveTxn], + [folder_b_txn], + [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result], + ], + 3 + ); + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_move_multiple_items_to_folder() { + let folder_a_info = createTestFolderInfo("Folder A"); + let bkm_a_info = { url: "http://test_move_items.com", title: "Bookmark A" }; + let bkm_b_info = { url: "http://test_move_items.com", title: "Bookmark B" }; + let bkm_c_info = { url: "http://test_move_items.com", title: "Bookmark C" }; + + // Test moving items within the same folder. + let [ + folder_a_txn_result, + bkm_a_txn_result, + bkm_b_txn_result, + bkm_c_txn_result, + ] = await PT.batch(async function () { + let folder_a_txn = PT.NewFolder(folder_a_info); + + folder_a_info.guid = + bkm_a_info.parentGuid = + bkm_b_info.parentGuid = + bkm_c_info.parentGuid = + await folder_a_txn.transact(); + let bkm_a_txn = PT.NewBookmark(bkm_a_info); + bkm_a_info.guid = await bkm_a_txn.transact(); + let bkm_b_txn = PT.NewBookmark(bkm_b_info); + bkm_b_info.guid = await bkm_b_txn.transact(); + let bkm_c_txn = PT.NewBookmark(bkm_c_info); + bkm_c_info.guid = await bkm_c_txn.transact(); + return [folder_a_txn, bkm_a_txn, bkm_b_txn, bkm_c_txn]; + }); + + ensureUndoState( + [ + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 0 + ); + + let moveTxn = PT.Move({ + guids: [bkm_a_info.guid, bkm_b_info.guid], + newParentGuid: folder_a_info.guid, + }); + await moveTxn.transact(); + + let ensureDo = () => { + ensureUndoState( + [ + [moveTxn], + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 0 + ); + ensureItemsMoved( + { + guid: bkm_a_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 0, + newIndex: 2, + }, + { + guid: bkm_b_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 1, + newIndex: 2, + } + ); + observer.reset(); + }; + let ensureUndo = () => { + ensureUndoState( + [ + [moveTxn], + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 1 + ); + ensureItemsMoved( + { + guid: bkm_a_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 1, + newIndex: 0, + }, + { + guid: bkm_b_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 2, + newIndex: 1, + } + ); + observer.reset(); + }; + + ensureDo(); + await PT.undo(); + ensureUndo(); + await PT.redo(); + ensureDo(); + await PT.undo(); + ensureUndo(); + + await PT.clearTransactionsHistory(false, true); + ensureUndoState( + [ + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 0 + ); + + // Test moving items between folders. + let folder_b_info = createTestFolderInfo("Folder B"); + let folder_b_txn = PT.NewFolder(folder_b_info); + folder_b_info.guid = await folder_b_txn.transact(); + ensureUndoState( + [ + [folder_b_txn], + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 0 + ); + + moveTxn = PT.Move({ + guid: bkm_a_info.guid, + newParentGuid: folder_b_info.guid, + newIndex: bmsvc.DEFAULT_INDEX, + }); + await moveTxn.transact(); + + ensureDo = () => { + ensureUndoState( + [ + [moveTxn], + [folder_b_txn], + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 0 + ); + ensureItemsMoved({ + guid: bkm_a_info.guid, + oldParentGuid: folder_a_info.guid, + newParentGuid: folder_b_info.guid, + oldIndex: 0, + newIndex: 0, + }); + observer.reset(); + }; + ensureUndo = () => { + ensureUndoState( + [ + [moveTxn], + [folder_b_txn], + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 1 + ); + ensureItemsMoved({ + guid: bkm_a_info.guid, + oldParentGuid: folder_b_info.guid, + newParentGuid: folder_a_info.guid, + oldIndex: 0, + newIndex: 0, + }); + observer.reset(); + }; + + ensureDo(); + await PT.undo(); + ensureUndo(); + await PT.redo(); + ensureDo(); + await PT.undo(); + ensureUndo(); + + // Clean up + await PT.undo(); // folder_b_txn + await PT.undo(); // folder_a_txn + the bookmarks; + Assert.equal(observer.itemsRemoved.size, 5); + ensureUndoState( + [ + [moveTxn], + [folder_b_txn], + [ + bkm_c_txn_result, + bkm_b_txn_result, + bkm_a_txn_result, + folder_a_txn_result, + ], + ], + 3 + ); + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_remove_folder() { + let folder_level_1_info = createTestFolderInfo("Folder Level 1"); + let folder_level_2_info = { title: "Folder Level 2" }; + let [folder_level_1_txn_result, folder_level_2_txn_result] = await PT.batch( + async function () { + let folder_level_1_txn = PT.NewFolder(folder_level_1_info); + folder_level_1_info.guid = await folder_level_1_txn.transact(); + folder_level_2_info.parentGuid = folder_level_1_info.guid; + let folder_level_2_txn = PT.NewFolder(folder_level_2_info); + folder_level_2_info.guid = await folder_level_2_txn.transact(); + return [folder_level_1_txn, folder_level_2_txn]; + } + ); + + let original_folder_level_1_tree = await PlacesUtils.promiseBookmarksTree( + folder_level_1_info.guid + ); + let original_folder_level_2_tree = Object.assign( + { parentGuid: original_folder_level_1_tree.guid }, + original_folder_level_1_tree.children[0] + ); + + ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]); + await ensureItemsAdded(folder_level_1_info, folder_level_2_info); + observer.reset(); + + let remove_folder_2_txn = PT.Remove(folder_level_2_info); + await remove_folder_2_txn.transact(); + + ensureUndoState([ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ]); + await ensureItemsRemoved(folder_level_2_info); + + // Undo Remove "Folder Level 2" + await PT.undo(); + ensureUndoState( + [ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ], + 1 + ); + await ensureItemsAdded(folder_level_2_info); + await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree); + observer.reset(); + + // Redo Remove "Folder Level 2" + await PT.redo(); + ensureUndoState([ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ]); + await ensureItemsRemoved(folder_level_2_info); + observer.reset(); + + // Undo it again + await PT.undo(); + ensureUndoState( + [ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ], + 1 + ); + await ensureItemsAdded(folder_level_2_info); + await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree); + observer.reset(); + + // Undo the creation of both folders + await PT.undo(); + ensureUndoState( + [ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ], + 2 + ); + await ensureItemsRemoved(folder_level_2_info, folder_level_1_info); + observer.reset(); + + // Redo the creation of both folders + await PT.redo(); + ensureUndoState( + [ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ], + 1 + ); + await ensureItemsAdded(folder_level_1_info, folder_level_2_info); + await ensureBookmarksTreeRestoredCorrectlyExceptDates( + original_folder_level_1_tree + ); + observer.reset(); + + // Redo Remove "Folder Level 2" + await PT.redo(); + ensureUndoState([ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ]); + await ensureItemsRemoved(folder_level_2_info); + observer.reset(); + + // Undo everything one last time + await PT.undo(); + ensureUndoState( + [ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ], + 1 + ); + await ensureItemsAdded(folder_level_2_info); + observer.reset(); + + await PT.undo(); + ensureUndoState( + [ + [remove_folder_2_txn], + [folder_level_2_txn_result, folder_level_1_txn_result], + ], + 2 + ); + await ensureItemsRemoved(folder_level_2_info, folder_level_2_info); + observer.reset(); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_add_and_remove_bookmarks_with_additional_info() { + const testURI = "http://add.remove.tag"; + const TAG_1 = "TestTag1"; + const TAG_2 = "TestTag2"; + + let folder_info = createTestFolderInfo(); + folder_info.guid = await PT.NewFolder(folder_info).transact(); + let ensureTags = ensureTagsForURI.bind(null, testURI); + + // Check that the NewBookmark transaction preserves tags. + observer.reset(); + let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] }; + b1_info.guid = await PT.NewBookmark(b1_info).transact(); + let b1_originalInfo = await PlacesUtils.promiseBookmarksTree(b1_info.guid); + ensureTags([TAG_1]); + await PT.undo(); + ensureTags([]); + + observer.reset(); + await PT.redo(); + await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo); + ensureTags([TAG_1]); + + // Check if the Remove transaction removes and restores tags of children + // correctly. + await PT.Remove(folder_info.guid).transact(); + ensureTags([]); + + observer.reset(); + await PT.undo(); + await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo); + ensureTags([TAG_1]); + + await PT.redo(); + ensureTags([]); + + observer.reset(); + await PT.undo(); + await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo); + ensureTags([TAG_1]); + + // * Check that no-op tagging (the uri is already tagged with TAG_1) is + // also a no-op on undo. + observer.reset(); + let b2_info = { + parentGuid: folder_info.guid, + url: testURI, + tags: [TAG_1, TAG_2], + }; + b2_info.guid = await PT.NewBookmark(b2_info).transact(); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + await PT.undo(); + await ensureItemsRemoved(b2_info); + ensureTags([TAG_1]); + + // Check if Remove correctly restores tags. + observer.reset(); + await PT.redo(); + ensureTags([TAG_1, TAG_2]); + + // Test Remove for multiple items. + observer.reset(); + await PT.Remove(b1_info.guid).transact(); + await PT.Remove(b2_info.guid).transact(); + await PT.Remove(folder_info.guid).transact(); + await ensureItemsRemoved(b1_info, b2_info, folder_info); + ensureTags([]); + + observer.reset(); + await PT.undo(); + await ensureItemsAdded(folder_info); + ensureTags([]); + + observer.reset(); + await PT.undo(); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + await PT.undo(); + await ensureItemsAdded(b1_info); + ensureTags([TAG_1, TAG_2]); + + // The redo calls below cleanup everything we did. + observer.reset(); + await PT.redo(); + await ensureItemsRemoved(b1_info); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + await PT.redo(); + // The tag containers are removed in async and take some time + let oldCountTag1 = 0; + let oldCountTag2 = 0; + let allTags = await bmsvc.fetchTags(); + for (let i of allTags) { + if (i.name == TAG_1) { + oldCountTag1 = i.count; + } + if (i.name == TAG_2) { + oldCountTag2 = i.count; + } + } + await TestUtils.waitForCondition(async () => { + allTags = await bmsvc.fetchTags(); + let newCountTag1 = 0; + let newCountTag2 = 0; + for (let i of allTags) { + if (i.name == TAG_1) { + newCountTag1 = i.count; + } + if (i.name == TAG_2) { + newCountTag2 = i.count; + } + } + return newCountTag1 == oldCountTag1 - 1 && newCountTag2 == oldCountTag2 - 1; + }); + await ensureItemsRemoved(b2_info); + + ensureTags([]); + + observer.reset(); + await PT.redo(); + await ensureItemsRemoved(folder_info); + ensureTags([]); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_creating_and_removing_a_separator() { + let folder_info = createTestFolderInfo(); + let separator_info = {}; + let undoEntries = []; + + observer.reset(); + let create_txns = await PT.batch(async function () { + let folder_txn = PT.NewFolder(folder_info); + folder_info.guid = separator_info.parentGuid = await folder_txn.transact(); + let separator_txn = PT.NewSeparator(separator_info); + separator_info.guid = await separator_txn.transact(); + return [separator_txn, folder_txn]; + }); + undoEntries.unshift(create_txns); + ensureUndoState(undoEntries, 0); + ensureItemsAdded(folder_info, separator_info); + + observer.reset(); + await PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsRemoved(folder_info, separator_info); + + observer.reset(); + await PT.redo(); + ensureUndoState(undoEntries, 0); + ensureItemsAdded(folder_info, separator_info); + + observer.reset(); + let remove_sep_txn = PT.Remove(separator_info); + await remove_sep_txn.transact(); + undoEntries.unshift([remove_sep_txn]); + ensureUndoState(undoEntries, 0); + ensureItemsRemoved(separator_info); + + observer.reset(); + await PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsAdded(separator_info); + + observer.reset(); + await PT.undo(); + ensureUndoState(undoEntries, 2); + ensureItemsRemoved(folder_info, separator_info); + + observer.reset(); + await PT.redo(); + ensureUndoState(undoEntries, 1); + ensureItemsAdded(folder_info, separator_info); + + // Clear redo entries and check that |redo| does nothing + observer.reset(); + await PT.clearTransactionsHistory(false, true); + undoEntries.shift(); + ensureUndoState(undoEntries, 0); + await PT.redo(); + ensureItemsAdded(); + ensureItemsRemoved(); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsRemoved(folder_info, separator_info); + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_edit_title() { + let bm_info = { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://test_create_item.com", + title: "Original Title", + }; + + function ensureTitleChange(aCurrentTitle) { + ensureItemsTitleChanged({ + guid: bm_info.guid, + title: aCurrentTitle, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + } + + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + + observer.reset(); + await PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact(); + ensureTitleChange("New Title"); + + observer.reset(); + await PT.undo(); + ensureTitleChange("Original Title"); + + observer.reset(); + await PT.redo(); + ensureTitleChange("New Title"); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureTitleChange("Original Title"); + await PT.undo(); + ensureItemsRemoved(bm_info); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_edit_url() { + let oldURI = "http://old.test_editing_item_uri.com/"; + let newURI = "http://new.test_editing_item_uri.com/"; + let bm_info = { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: oldURI, + tags: ["TestTag"], + }; + function ensureURIAndTags( + aPreChangeURI, + aPostChangeURI, + aOLdURITagsPreserved + ) { + ensureItemsUrlChanged({ + guid: bm_info.guid, + url: aPostChangeURI, + }); + ensureTagsForURI(aPostChangeURI, bm_info.tags); + ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []); + } + + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + ensureTagsForURI(oldURI, bm_info.tags); + + // When there's a single bookmark for the same url, tags should be moved. + observer.reset(); + await PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact(); + ensureURIAndTags(oldURI, newURI, false); + + observer.reset(); + await PT.undo(); + ensureURIAndTags(newURI, oldURI, false); + + observer.reset(); + await PT.redo(); + ensureURIAndTags(oldURI, newURI, false); + + observer.reset(); + await PT.undo(); + ensureURIAndTags(newURI, oldURI, false); + + // When there're multiple bookmarks for the same url, tags should be copied. + let bm2_info = Object.create(bm_info); + bm2_info.guid = await PT.NewBookmark(bm2_info).transact(); + let bm3_info = Object.create(bm_info); + bm3_info.url = newURI; + bm3_info.guid = await PT.NewBookmark(bm3_info).transact(); + + observer.reset(); + await PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact(); + ensureURIAndTags(oldURI, newURI, true); + + observer.reset(); + await PT.undo(); + ensureURIAndTags(newURI, oldURI, true); + + observer.reset(); + await PT.redo(); + ensureURIAndTags(oldURI, newURI, true); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureURIAndTags(newURI, oldURI, true); + await PT.undo(); + await PT.undo(); + await PT.undo(); + ensureItemsRemoved(bm3_info, bm2_info, bm_info); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_edit_keyword() { + let bm_info = { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://test.edit.keyword/", + }; + const KEYWORD = "test_keyword"; + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + function ensureKeywordChange(aCurrentKeyword = "") { + ensureItemsKeywordChanged({ + guid: bm_info.guid, + keyword: aCurrentKeyword, + }); + } + + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + + observer.reset(); + await PT.EditKeyword({ + guid: bm_info.guid, + keyword: KEYWORD, + postData: "postData", + }).transact(); + ensureKeywordChange(KEYWORD); + let entry = await PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData"); + + observer.reset(); + await PT.undo(); + ensureKeywordChange(); + entry = await PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry, null); + + observer.reset(); + await PT.redo(); + ensureKeywordChange(KEYWORD); + entry = await PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData"); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureKeywordChange(); + await PT.undo(); + ensureItemsRemoved(bm_info); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_edit_keyword_null_postData() { + let bm_info = { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://test.edit.keyword/", + }; + const KEYWORD = "test_keyword"; + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + function ensureKeywordChange(aCurrentKeyword = "") { + ensureItemsKeywordChanged({ + guid: bm_info.guid, + keyword: aCurrentKeyword, + }); + } + + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + + observer.reset(); + await PT.EditKeyword({ + guid: bm_info.guid, + keyword: KEYWORD, + postData: null, + }).transact(); + ensureKeywordChange(KEYWORD); + let entry = await PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, null); + + observer.reset(); + await PT.undo(); + ensureKeywordChange(); + entry = await PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry, null); + + observer.reset(); + await PT.redo(); + ensureKeywordChange(KEYWORD); + entry = await PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, null); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureKeywordChange(); + await PT.undo(); + ensureItemsRemoved(bm_info); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_edit_specific_keyword() { + let bm_info = { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://test.edit.keyword/", + }; + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") { + ensureItemsKeywordChanged({ + guid: bm_info.guid, + keyword: aCurrentKeyword, + }); + } + + await PlacesUtils.keywords.insert({ + keyword: "kw1", + url: bm_info.url, + postData: "postData1", + }); + await PlacesUtils.keywords.insert({ + keyword: "kw2", + url: bm_info.url, + postData: "postData2", + }); + bm_info.guid = await PT.NewBookmark(bm_info).transact(); + + observer.reset(); + await PT.EditKeyword({ + guid: bm_info.guid, + keyword: "keyword", + oldKeyword: "kw2", + }).transact(); + ensureKeywordChange("keyword", "kw2"); + let entry = await PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData1"); + entry = await PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData2"); + entry = await PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry, null); + + observer.reset(); + await PT.undo(); + ensureKeywordChange("kw2", "keyword"); + entry = await PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData1"); + entry = await PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData2"); + entry = await PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry, null); + + observer.reset(); + await PT.redo(); + ensureKeywordChange("keyword", "kw2"); + entry = await PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData1"); + entry = await PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry.url.href, bm_info.url); + Assert.equal(entry.postData, "postData2"); + entry = await PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry, null); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureKeywordChange("kw2"); + await PT.undo(); + ensureItemsRemoved(bm_info); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_tag_uri() { + // This also tests passing uri specs. + let bm_info_a = { + url: "http://bookmarked.uri", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }; + let bm_info_b = { + url: "http://bookmarked2.uri", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }; + let unbookmarked_uri = "http://un.bookmarked.uri"; + + await PT.batch(async function () { + bm_info_a.guid = await PT.NewBookmark(bm_info_a).transact(); + bm_info_b.guid = await PT.NewBookmark(bm_info_b).transact(); + }); + + async function doTest(aInfo) { + let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls; + let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags; + + let tagWillAlsoBookmark = new Set(); + for (let url of urls) { + if (!(await bmsvc.fetch({ url }))) { + tagWillAlsoBookmark.add(url); + } + } + + async function ensureTagsSet() { + for (let url of urls) { + ensureTagsForURI(url, tags); + Assert.ok(await bmsvc.fetch({ url })); + } + } + async function ensureTagsUnset() { + for (let url of urls) { + ensureTagsForURI(url, []); + if (tagWillAlsoBookmark.has(url)) { + Assert.ok(!(await bmsvc.fetch({ url }))); + } else { + Assert.ok(await bmsvc.fetch({ url })); + } + } + } + + await PT.Tag(aInfo).transact(); + await ensureTagsSet(); + await PT.undo(); + await ensureTagsUnset(); + await PT.redo(); + await ensureTagsSet(); + await PT.undo(); + await ensureTagsUnset(); + } + + await doTest({ url: bm_info_a.url, tags: ["MyTag"] }); + await doTest({ urls: [bm_info_a.url], tag: "MyTag" }); + await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] }); + await doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" }); + // Duplicate URLs listed. + await doTest({ + urls: [bm_info_a.url, bm_info_b.url, bm_info_a.url], + tag: "D", + }); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureItemsRemoved(bm_info_a, bm_info_b); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_untag_uri() { + let bm_info_a = { + url: "http://bookmarked.uri", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + tags: ["A", "B"], + }; + let bm_info_b = { + url: "http://bookmarked2.uri", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + tag: "B", + }; + + await PT.batch(async function () { + bm_info_a.guid = await PT.NewBookmark(bm_info_a).transact(); + ensureTagsForURI(bm_info_a.url, bm_info_a.tags); + bm_info_b.guid = await PT.NewBookmark(bm_info_b).transact(); + ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]); + }); + + async function doTest(aInfo) { + let urls, tagsRemoved; + if (typeof aInfo == "string") { + urls = [aInfo]; + tagsRemoved = []; + } else if (Array.isArray(aInfo)) { + urls = aInfo; + tagsRemoved = []; + } else { + urls = "url" in aInfo ? [aInfo.url] : aInfo.urls; + tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags; + } + + let preRemovalTags = new Map(); + for (let url of urls) { + preRemovalTags.set(url, tagssvc.getTagsForURI(Services.io.newURI(url))); + } + + function ensureTagsSet() { + for (let url of urls) { + ensureTagsForURI(url, preRemovalTags.get(url)); + } + } + function ensureTagsUnset() { + for (let url of urls) { + let expectedTags = !tagsRemoved.length + ? [] + : preRemovalTags.get(url).filter(tag => !tagsRemoved.includes(tag)); + ensureTagsForURI(url, expectedTags); + } + } + + await PT.Untag(aInfo).transact(); + await ensureTagsUnset(); + await PT.undo(); + await ensureTagsSet(); + await PT.redo(); + await ensureTagsUnset(); + await PT.undo(); + await ensureTagsSet(); + } + + await doTest(bm_info_a); + await doTest(bm_info_b); + await doTest(bm_info_a.url); + await doTest(bm_info_b.url); + await doTest([bm_info_a.url, bm_info_b.url]); + await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] }); + await doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" }); + await doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" }); + await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] }); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureItemsRemoved(bm_info_a, bm_info_b); + + await PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(async function test_sort_folder_by_name() { + let folder_info = createTestFolderInfo(); + + let url = "http://sort.by.name/"; + let preSep = ["3", "2", "1"].map(i => ({ title: i, url })); + let sep = {}; + let postSep = ["c", "b", "a"].map(l => ({ title: l, url })); + let originalOrder = [...preSep, sep, ...postSep]; + let sortedOrder = [ + ...preSep.slice(0).reverse(), + sep, + ...postSep.slice(0).reverse(), + ]; + await PT.batch(async function () { + folder_info.guid = await PT.NewFolder(folder_info).transact(); + for (let info of originalOrder) { + info.parentGuid = folder_info.guid; + info.guid = await (info == sep + ? PT.NewSeparator(info).transact() + : PT.NewBookmark(info).transact()); + } + }); + + let folderContainer = PlacesUtils.getFolderContents(folder_info.guid).root; + function ensureOrder(aOrder) { + for (let i = 0; i < folderContainer.childCount; i++) { + Assert.equal(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid); + } + } + + ensureOrder(originalOrder); + await PT.SortByName(folder_info.guid).transact(); + ensureOrder(sortedOrder); + await PT.undo(); + ensureOrder(originalOrder); + await PT.redo(); + ensureOrder(sortedOrder); + + // Cleanup + observer.reset(); + await PT.undo(); + ensureOrder(originalOrder); + await PT.undo(); + ensureItemsRemoved(...originalOrder, folder_info); +}); + +add_task(async function test_copy() { + async function duplicate_and_test(aOriginalGuid) { + let txn = PT.Copy({ + guid: aOriginalGuid, + newParentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let duplicateGuid = await txn.transact(); + let originalInfo = await PlacesUtils.promiseBookmarksTree(aOriginalGuid); + let duplicateInfo = await PlacesUtils.promiseBookmarksTree(duplicateGuid); + await ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false); + + async function redo() { + await PT.redo(); + await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo); + await PT.redo(); + await ensureBookmarksTreeRestoredCorrectlyExceptDates(duplicateInfo); + } + async function undo() { + await PT.undo(); + // also undo the original item addition. + await PT.undo(); + await ensureNonExistent(aOriginalGuid, duplicateGuid); + } + + await undo(); + await redo(); + await undo(); + await redo(); + + // Cleanup. This also remove the original item. + await PT.undo(); + observer.reset(); + await PT.clearTransactionsHistory(); + } + + let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); + Preferences.set("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(function () { + Preferences.set("privacy.reduceTimerPrecision", timerPrecision); + }); + + // Test duplicating leafs (bookmark, separator, empty folder) + PT.NewBookmark({ + url: "http://test.item.duplicate", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + annos: [{ name: "Anno", value: "AnnoValue" }], + }); + let sepTxn = PT.NewSeparator({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 1, + }); + + let emptyFolderTxn = PT.NewFolder(createTestFolderInfo()); + for (let txn of [sepTxn, emptyFolderTxn]) { + let guid = await txn.transact(); + await duplicate_and_test(guid); + } + + // Test duplicating a folder having some contents. + let filledFolderGuid = await PT.batch(async function () { + let folderGuid = await PT.NewFolder(createTestFolderInfo()).transact(); + let nestedFolderGuid = await PT.NewFolder({ + parentGuid: folderGuid, + title: "Nested Folder", + }).transact(); + // Insert a bookmark under the nested folder. + await PT.NewBookmark({ + url: "http://nested.nested.bookmark", + parentGuid: nestedFolderGuid, + }).transact(); + // Insert a separator below the nested folder + await PT.NewSeparator({ parentGuid: folderGuid }).transact(); + // And another bookmark. + await PT.NewBookmark({ + url: "http://nested.bookmark", + parentGuid: folderGuid, + }).transact(); + return folderGuid; + }); + + await duplicate_and_test(filledFolderGuid); + + // Cleanup + await PT.clearTransactionsHistory(); +}); + +add_task(async function test_array_input_for_batch() { + let folderTxn = PT.NewFolder(createTestFolderInfo()); + let folderGuid = await folderTxn.transact(); + + let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid }); + let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid }); + await PT.batch([sep1_txn, sep2_txn]); + ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0); + + let ensureChildCount = async function (count) { + let tree = await PlacesUtils.promiseBookmarksTree(folderGuid); + if (count == 0) { + Assert.ok(!("children" in tree)); + } else { + Assert.equal(tree.children.length, count); + } + }; + + await ensureChildCount(2); + await PT.undo(); + await ensureChildCount(0); + await PT.redo(); + await ensureChildCount(2); + await PT.undo(); + await ensureChildCount(0); + + await PT.undo(); + Assert.equal(await PlacesUtils.promiseBookmarksTree(folderGuid), null); + + // Cleanup + await PT.clearTransactionsHistory(); +}); + +add_task(async function test_invalid_uri_spec_throws() { + Assert.throws( + () => + PT.NewBookmark({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "invalid uri spec", + title: "test bookmark", + }), + /invalid uri spec is not a valid URL/ + ); + Assert.throws( + () => PT.Tag({ tag: "TheTag", urls: ["invalid uri spec"] }), + /TypeError: URL constructor: invalid uri spec is not a valid URL/ + ); + Assert.throws( + () => PT.Tag({ tag: "TheTag", urls: ["about:blank", "invalid uri spec"] }), + /TypeError: URL constructor: invalid uri spec is not a valid URL/ + ); +}); + +add_task(async function test_remove_multiple() { + let guids = []; + await PT.batch(async function () { + let folderGuid = await PT.NewFolder({ + title: "Test Folder", + parentGuid: menuGuid, + }).transact(); + let nestedFolderGuid = await PT.NewFolder({ + title: "Nested Test Folder", + parentGuid: folderGuid, + }).transact(); + await PT.NewSeparator(nestedFolderGuid).transact(); + + guids.push(folderGuid); + + let bmGuid = await PT.NewBookmark({ + url: "http://test.bookmark.removed", + parentGuid: menuGuid, + }).transact(); + guids.push(bmGuid); + }); + + let originalInfos = []; + for (let guid of guids) { + originalInfos.push(await PlacesUtils.promiseBookmarksTree(guid)); + } + + await PT.Remove(guids).transact(); + await ensureNonExistent(...guids); + await PT.undo(); + await ensureBookmarksTreeRestoredCorrectly(...originalInfos); + await PT.redo(); + await ensureNonExistent(...guids); + await PT.undo(); + await ensureBookmarksTreeRestoredCorrectly(...originalInfos); + + // Undo the New* transactions batch. + await PT.undo(); + await ensureNonExistent(...guids); + + // Redo it. + await PT.redo(); + await ensureBookmarksTreeRestoredCorrectlyExceptDates(...originalInfos); + + // Redo remove. + await PT.redo(); + await ensureNonExistent(...guids); + + // Cleanup + await PT.clearTransactionsHistory(); + observer.reset(); +}); + +add_task(async function test_renameTag() { + let url = "http://test.edit.keyword/"; + await PT.Tag({ url, tags: ["t1", "t2"] }).transact(); + ensureTagsForURI(url, ["t1", "t2"]); + + // Create bookmark queries that point to the modified tag. + let bm1 = await PlacesUtils.bookmarks.insert({ + url: "place:tag=t2", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + url: "place:tag=t2&sort=1", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + // This points to 2 tags, and as such won't be touched. + let bm3 = await PlacesUtils.bookmarks.insert({ + url: "place:tag=t2&tag=t1", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + await PT.RenameTag({ oldTag: "t2", tag: "t3" }).transact(); + ensureTagsForURI(url, ["t1", "t3"]); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href, + "place:tag=t3", + "The fitst bookmark has been updated" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href, + "place:tag=t3&sort=1", + "The second bookmark has been updated" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href, + "place:tag=t3&tag=t1", + "The third bookmark has been updated" + ); + + await PT.undo(); + ensureTagsForURI(url, ["t1", "t2"]); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href, + "place:tag=t2", + "The fitst bookmark has been restored" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href, + "place:tag=t2&sort=1", + "The second bookmark has been restored" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href, + "place:tag=t2&tag=t1", + "The third bookmark has been restored" + ); + + await PT.redo(); + ensureTagsForURI(url, ["t1", "t3"]); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href, + "place:tag=t3", + "The fitst bookmark has been updated" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href, + "place:tag=t3&sort=1", + "The second bookmark has been updated" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href, + "place:tag=t3&tag=t1", + "The third bookmark has been updated" + ); + + await PT.undo(); + ensureTagsForURI(url, ["t1", "t2"]); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href, + "place:tag=t2", + "The fitst bookmark has been restored" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href, + "place:tag=t2&sort=1", + "The second bookmark has been restored" + ); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href, + "place:tag=t2&tag=t1", + "The third bookmark has been restored" + ); + + await PT.undo(); + ensureTagsForURI(url, []); + + await PT.clearTransactionsHistory(); + ensureUndoState(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_remove_invalid_url() { + let folderGuid = await PT.NewFolder({ + title: "Test Folder", + parentGuid: menuGuid, + }).transact(); + + let guid = "invalid_____"; + let folderedGuid = "invalid____2"; + let url = "invalid-uri"; + await PlacesUtils.withConnectionWrapper("test_bookmarks_remove", async db => { + await db.execute( + ` + INSERT INTO moz_places(url, url_hash, title, rev_host, guid) + VALUES (:url, hash(:url), 'Invalid URI', '.', GENERATE_GUID()) + `, + { url } + ); + await db.execute( + `INSERT INTO moz_bookmarks (type, fk, parent, position, guid) + VALUES (:type, + (SELECT id FROM moz_places WHERE url = :url), + (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid), + (SELECT MAX(position) + 1 FROM moz_bookmarks WHERE parent = (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid)), + :guid) + `, + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + guid, + } + ); + await db.execute( + `INSERT INTO moz_bookmarks (type, fk, parent, position, guid) + VALUES (:type, + (SELECT id FROM moz_places WHERE url = :url), + (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid), + (SELECT MAX(position) + 1 FROM moz_bookmarks WHERE parent = (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid)), + :guid) + `, + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url, + parentGuid: folderGuid, + guid: folderedGuid, + } + ); + }); + + let guids = [folderGuid, guid]; + await PT.Remove(guids).transact(); + await ensureNonExistent(...guids, folderedGuid); + // Shouldn't throw, should restore the folder but not the bookmarks. + await PT.undo(); + await ensureNonExistent(guid, folderedGuid); + Assert.ok( + await PlacesUtils.bookmarks.fetch(folderGuid), + "The folder should have been re-created" + ); + await PT.redo(); + await ensureNonExistent(guids, folderedGuid); + // Cleanup + await PT.clearTransactionsHistory(); + observer.reset(); +}); diff --git a/toolkit/components/places/tests/unit/test_autocomplete_match_fallbackTitle.js b/toolkit/components/places/tests/unit/test_autocomplete_match_fallbackTitle.js new file mode 100644 index 0000000000..109ca1edce --- /dev/null +++ b/toolkit/components/places/tests/unit/test_autocomplete_match_fallbackTitle.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This is a test for the fallbackTitle argument of autocomplete_match. + +add_task(async function test_match() { + async function search(text) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute( + ` + SELECT AUTOCOMPLETE_MATCH(:text, 'http://mozilla.org/', 'Main title', + NULL, NULL, 1, 1, NULL, + :matchBehavior, :searchBehavior, + 'Fallback title') + `, + { + text, + matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE, + searchBehavior: 643, + } + ); + return !!rows[0].getResultByIndex(0); + } + Assert.ok(await search("mai"), "Match on main title"); + Assert.ok(await search("fall"), "Match on fallback title"); +}); diff --git a/toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js b/toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js new file mode 100644 index 0000000000..568ad43841 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks that changing a tag for a bookmark with multiple tags +// notifies bookmark-tags-changed event only once, and not once per tag. + +add_task(async function run_test() { + let tags = ["a", "b", "c"]; + let uri = Services.io.newURI("http://1.moz.org/"); + + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: uri, + title: "Bookmark 1", + }); + PlacesUtils.tagging.tagURI(uri, tags); + + let promise = PromiseUtils.defer(); + + let bookmarksObserver = { + _changedCount: 0, + handlePlacesEvents(events) { + for (let event of events) { + switch (event.type) { + case "bookmark-removed": + if (event.guid == bookmark.guid) { + PlacesUtils.observers.removeListener( + ["bookmark-removed"], + this.handlePlacesEvents + ); + Assert.equal(this._changedCount, 2); + promise.resolve(); + } + break; + case "bookmark-tags-changed": + Assert.equal(event.guid, bookmark.guid); + this._changedCount++; + break; + } + } + }, + }; + bookmarksObserver.handlePlacesEvents = + bookmarksObserver.handlePlacesEvents.bind(bookmarksObserver); + PlacesUtils.observers.addListener( + ["bookmark-removed", "bookmark-tags-changed"], + bookmarksObserver.handlePlacesEvents + ); + + PlacesUtils.tagging.tagURI(uri, ["d"]); + PlacesUtils.tagging.tagURI(uri, ["e"]); + + await promise; + + await PlacesUtils.bookmarks.remove(bookmark.guid); +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html.js b/toolkit/components/places/tests/unit/test_bookmarks_html.js new file mode 100644 index 0000000000..4b3f04b444 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js @@ -0,0 +1,417 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// An object representing the contents of bookmarks.preplaces.html. +var test_bookmarks = { + menu: [ + { + title: "Mozilla Firefox", + children: [ + { + title: "Help and Tutorials", + url: "http://en-us.www.mozilla.com/en-US/firefox/help/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + title: "Customize Firefox", + url: "http://en-us.www.mozilla.com/en-US/firefox/customize/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + title: "Get Involved", + url: "http://en-us.www.mozilla.com/en-US/firefox/community/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + title: "About Us", + url: "http://en-us.www.mozilla.com/en-US/about/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + ], + }, + { + type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR, + }, + { + title: "test", + dateAdded: 1177541020000000, + lastModified: 1177541050000000, + children: [ + { + title: "test post keyword", + dateAdded: 1177375336000000, + lastModified: 1177375423000000, + keyword: "test", + postData: "hidden1%3Dbar&text1%3D%25s", + charset: "ISO-8859-1", + url: "http://test/post", + }, + ], + }, + ], + toolbar: [ + { + title: "Getting Started", + url: "http://en-us.www.mozilla.com/en-US/firefox/central/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + title: "Latest Headlines", + url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/", + feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml", + }, + // This will be ignored, because it has no url. + { + title: "Latest Headlines No Site", + feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml", + ignore: true, + }, + ], + unfiled: [{ title: "Example.tld", url: "http://example.tld/" }], +}; + +// Pre-Places bookmarks.html file pointer. +var gBookmarksFileOld; +// Places bookmarks.html file pointer. +var gBookmarksFileNew; + +add_task(async function setup() { + // File pointer to legacy bookmarks file. + gBookmarksFileOld = PathUtils.join( + do_get_cwd().path, + "bookmarks.preplaces.html" + ); + + // File pointer to a new Places-exported bookmarks file. + gBookmarksFileNew = PathUtils.join( + PathUtils.profileDir, + "bookmarks.exported.html" + ); + await IOUtils.remove(gBookmarksFileNew, { ignoreAbsent: true }); + + // This test must be the first one, since it setups the new bookmarks.html. + // Test importing a pre-Places canonical bookmarks file. + // 1. import bookmarks.preplaces.html + // 2. run the test-suite + // Note: we do not empty the db before this import to catch bugs like 380999 + await BookmarkHTMLUtils.importFromFile(gBookmarksFileOld, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); + + await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + await PlacesTestUtils.promiseAsyncUpdates(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_count() { + // Ensure the bookmarks count is correct when importing in various cases + let count = await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { + replace: true, + }); + Assert.equal( + count, + 8, + "There should be 8 imported bookmarks when importing from an empty database" + ); + await PlacesTestUtils.promiseAsyncUpdates(); + await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + await PlacesTestUtils.promiseAsyncUpdates(); + + count = -1; + count = await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { + replace: true, + }); + Assert.equal( + count, + 8, + "There should be 8 imported bookmarks when replacing existing bookmarks" + ); + await PlacesTestUtils.promiseAsyncUpdates(); + + count = -1; + count = await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew); + Assert.equal( + count, + 8, + "There should be 8 imported bookmarks even when we are not replacing existing bookmarks" + ); + await PlacesTestUtils.promiseAsyncUpdates(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_new() { + // Test importing a Places bookmarks.html file. + // 1. import bookmarks.exported.html + // 2. run the test-suite + await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + + await testImportedBookmarks(); + await PlacesTestUtils.promiseAsyncUpdates(); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_emptytitle_export() { + // Test exporting and importing with an empty-titled bookmark. + // 1. import bookmarks + // 2. create an empty-titled bookmark. + // 3. export to bookmarks.exported.html + // 4. empty bookmarks db + // 5. import bookmarks.exported.html + // 6. run the test-suite + // 7. remove the empty-titled bookmark + // 8. export to bookmarks.exported.html + // 9. empty bookmarks db and continue + + await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + + const NOTITLE_URL = "http://notitle.mozilla.org/"; + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: NOTITLE_URL, + }); + test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL }); + + await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + await PlacesTestUtils.promiseAsyncUpdates(); + await PlacesUtils.bookmarks.eraseEverything(); + + await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); + + // Cleanup. + test_bookmarks.unfiled.pop(); + // HTML imports don't restore GUIDs yet. + let reimportedBookmark = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + Assert.equal(reimportedBookmark.url.href, bookmark.url.href); + await PlacesUtils.bookmarks.remove(reimportedBookmark); + + await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + await PlacesTestUtils.promiseAsyncUpdates(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_chromefavicon() { + // Test exporting and importing with a bookmark pointing to a chrome favicon. + // 1. import bookmarks + // 2. create a bookmark pointing to a chrome favicon. + // 3. export to bookmarks.exported.html + // 4. empty bookmarks db + // 5. import bookmarks.exported.html + // 6. run the test-suite + // 7. remove the bookmark pointing to a chrome favicon. + // 8. export to bookmarks.exported.html + // 9. empty bookmarks db and continue + + const PAGE_URI = NetUtil.newURI("http://example.com/chromefavicon_page"); + const CHROME_FAVICON_URI = NetUtil.newURI( + "chrome://global/skin/icons/delete.svg" + ); + const CHROME_FAVICON_URI_2 = NetUtil.newURI( + "chrome://global/skin/icons/error.svg" + ); + + info("Importing from html"); + await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + + info("Insert bookmark"); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: PAGE_URI, + title: "Test", + }); + + info("Set favicon"); + await new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + PAGE_URI, + CHROME_FAVICON_URI, + true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }); + + let data = await new Promise(resolve => { + PlacesUtils.favicons.getFaviconDataForPage( + PAGE_URI, + (uri, dataLen, faviconData, mimeType) => resolve(faviconData) + ); + }); + + let base64Icon = + "data:image/png;base64," + + base64EncodeString(String.fromCharCode.apply(String, data)); + + test_bookmarks.unfiled.push({ + title: "Test", + url: PAGE_URI.spec, + icon: base64Icon, + }); + + info("Export to html"); + await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + await PlacesTestUtils.promiseAsyncUpdates(); + + info("Set favicon"); + // Change the favicon to check it's really imported again later. + await new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + PAGE_URI, + CHROME_FAVICON_URI_2, + true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }); + + info("import from html"); + await PlacesUtils.bookmarks.eraseEverything(); + await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + + info("Test imported bookmarks"); + await testImportedBookmarks(); + + // Cleanup. + test_bookmarks.unfiled.pop(); + // HTML imports don't restore GUIDs yet. + let reimportedBookmark = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + await PlacesUtils.bookmarks.remove(reimportedBookmark); + + await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + await PlacesTestUtils.promiseAsyncUpdates(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_ontop() { + // Test importing the exported bookmarks.html file *on top of* the existing + // bookmarks. + // 1. empty bookmarks db + // 2. import the exported bookmarks file + // 3. export to file + // 3. import the exported bookmarks file + // 4. run the test-suite + + await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + await PlacesTestUtils.promiseAsyncUpdates(); + + await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); + await PlacesTestUtils.promiseAsyncUpdates(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +async function testImportedBookmarks() { + for (let group in test_bookmarks) { + info("[testImportedBookmarks()] Checking group '" + group + "'"); + + let root = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks[`${group}Guid`] + ).root; + + let items = test_bookmarks[group].filter(b => !b.ignore); + Assert.equal(root.childCount, items.length); + + for (let key in items) { + await checkItem(items[key], root.getChild(key)); + } + + root.containerOpen = false; + } +} + +function checkItem(aExpected, aNode) { + return (async function () { + let bookmark = await PlacesUtils.bookmarks.fetch(aNode.bookmarkGuid); + + for (let prop in aExpected) { + switch (prop) { + case "type": + Assert.equal(aNode.type, aExpected.type); + break; + case "title": + Assert.equal(aNode.title, aExpected.title); + break; + case "dateAdded": + Assert.equal( + PlacesUtils.toPRTime(bookmark.dateAdded), + aExpected.dateAdded + ); + break; + case "lastModified": + Assert.equal( + PlacesUtils.toPRTime(bookmark.lastModified), + aExpected.lastModified + ); + break; + case "url": + Assert.equal(aNode.uri, aExpected.url); + break; + case "icon": + let { data } = await getFaviconDataForPage(aExpected.url); + let base64Icon = + "data:image/png;base64," + + base64EncodeString(String.fromCharCode.apply(String, data)); + Assert.ok(base64Icon == aExpected.icon); + break; + case "keyword": { + let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.keyword, aExpected.keyword); + break; + } + case "postData": { + let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.postData, aExpected.postData); + break; + } + case "charset": + let pageInfo = await PlacesUtils.history.fetch(aNode.uri, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + aExpected.charset + ); + break; + case "feedUrl": + // No more supported. + break; + case "children": + let folder = aNode.QueryInterface( + Ci.nsINavHistoryContainerResultNode + ); + Assert.equal(folder.hasChildren, !!aExpected.children.length); + folder.containerOpen = true; + Assert.equal(folder.childCount, aExpected.children.length); + + for (let index = 0; index < aExpected.children.length; index++) { + await checkItem(aExpected.children[index], folder.getChild(index)); + } + + folder.containerOpen = false; + break; + default: + throw new Error("Unknown property"); + } + } + })(); +} diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js new file mode 100644 index 0000000000..061c8c0c5f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js @@ -0,0 +1,126 @@ +/* + * This test ensures that importing/exporting to HTML does not stop + * if a malformed uri is found. + */ + +const TEST_FAVICON_PAGE_URL = + "http://en-US.www.mozilla.com/en-US/firefox/central/"; +const TEST_FAVICON_DATA_SIZE = 580; + +add_task(async function test_corrupt_file() { + // Import bookmarks from the corrupt file. + let corruptHtml = PathUtils.join(do_get_cwd().path, "bookmarks.corrupt.html"); + await BookmarkHTMLUtils.importFromFile(corruptHtml, { replace: true }); + + // Check that bookmarks that are not corrupt have been imported. + await PlacesTestUtils.promiseAsyncUpdates(); + await database_check(); +}); + +add_task(async function test_corrupt_database() { + // Create corruption in the database, then export. + let corruptBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://test.mozilla.org", + title: "We love belugas", + }); + await PlacesUtils.withConnectionWrapper("test", async function (db) { + await db.execute("UPDATE moz_bookmarks SET fk = NULL WHERE guid = :guid", { + guid: corruptBookmark.guid, + }); + }); + + let bookmarksFile = PathUtils.join( + PathUtils.profileDir, + "bookmarks.exported.html" + ); + await IOUtils.remove(bookmarksFile, { ignoreAbsent: true }); + await BookmarkHTMLUtils.exportToFile(bookmarksFile); + + // Import again and check for correctness. + await PlacesUtils.bookmarks.eraseEverything(); + await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + await database_check(); +}); + +/* + * Check for imported bookmarks correctness + * + * @return {Promise} + * @resolves When the checks are finished. + * @rejects Never. + */ +var database_check = async function () { + // BOOKMARKS MENU + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root; + Assert.equal(root.childCount, 2); + + let folderNode = root.getChild(1); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + Assert.equal(folderNode.title, "test"); + + let bookmark = await PlacesUtils.bookmarks.fetch({ + guid: folderNode.bookmarkGuid, + }); + Assert.equal(PlacesUtils.toPRTime(bookmark.dateAdded), 1177541020000000); + Assert.equal(PlacesUtils.toPRTime(bookmark.lastModified), 1177541050000000); + + // open test folder, and test the children + PlacesUtils.asQuery(folderNode); + Assert.equal(folderNode.hasChildren, true); + folderNode.containerOpen = true; + Assert.equal(folderNode.childCount, 1); + + let bookmarkNode = folderNode.getChild(0); + Assert.equal("http://test/post", bookmarkNode.uri); + Assert.equal("test post keyword", bookmarkNode.title); + + let entry = await PlacesUtils.keywords.fetch({ url: bookmarkNode.uri }); + Assert.equal("test", entry.keyword); + Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData); + + Assert.equal(bookmarkNode.dateAdded, 1177375336000000); + Assert.equal(bookmarkNode.lastModified, 1177375423000000); + + let pageInfo = await PlacesUtils.history.fetch(bookmarkNode.uri, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + "ISO-8859-1", + "Should have the correct charset" + ); + + // clean up + folderNode.containerOpen = false; + root.containerOpen = false; + + // BOOKMARKS TOOLBAR + root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.toolbarGuid).root; + Assert.equal(root.childCount, 3); + + // cleanup + root.containerOpen = false; + + // UNFILED BOOKMARKS + root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.unfiledGuid).root; + Assert.equal(root.childCount, 1); + root.containerOpen = false; + + // favicons + await new Promise(resolve => { + PlacesUtils.favicons.getFaviconDataForPage( + uri(TEST_FAVICON_PAGE_URL), + (aURI, aDataLen, aData, aMimeType) => { + // aURI should never be null when aDataLen > 0. + Assert.notEqual(aURI, null); + // Favicon data is stored in the bookmarks file as a "data:" URI. For + // simplicity, instead of converting the data we receive to a "data:" URI + // and comparing it, we just check the data size. + Assert.equal(TEST_FAVICON_DATA_SIZE, aDataLen); + resolve(); + } + ); + }); +}; diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_escape_entities.js b/toolkit/components/places/tests/unit/test_bookmarks_html_escape_entities.js new file mode 100644 index 0000000000..5349d3948c --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_escape_entities.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Checks that html entities are escaped in bookmarks.html files. + +add_task(async function () { + // Removes bookmarks.html if the file already exists. + let HTMLFile = PathUtils.join(PathUtils.profileDir, "bookmarks.html"); + await IOUtils.remove(HTMLFile, { ignoreAbsent: true }); + + let unescaped = '<unescaped="test">'; + // Adds bookmarks and tags to the database. + const url = 'http://www.google.it/"/'; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: unescaped, + }); + await PlacesUtils.keywords.insert({ + url, + keyword: unescaped, + postData: unescaped, + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(url), [unescaped]); + await PlacesUtils.history.update({ + url, + annotations: new Map([[PlacesUtils.CHARSET_ANNO, unescaped]]), + }); + + // Exports the bookmarks as a HTML file. + await BookmarkHTMLUtils.exportToFile(HTMLFile); + await PlacesUtils.bookmarks.remove(bm); + + // Check there are no unescaped entities in the html file. + let xml = await new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.onload = () => { + try { + resolve(xhr.responseXML); + } catch (e) { + reject(e); + } + }; + xhr.onabort = + xhr.onerror = + xhr.ontimeout = + () => { + reject(new Error("xmlhttprequest failed")); + }; + xhr.open("GET", PathUtils.toFileURI(HTMLFile)); + xhr.responseType = "document"; + xhr.overrideMimeType("text/html"); + xhr.send(); + }); + + let checksCount = 5; + for ( + let current = xml; + current; + current = + current.firstChild || + current.nextSibling || + current.parentNode.nextSibling + ) { + switch (current.nodeType) { + case current.ELEMENT_NODE: + for (let { name, value } of current.attributes) { + info("Found attribute: " + name); + // Check tags, keyword, postData and charSet. + if ( + ["tags", "last_charset", "shortcuturl", "post_data"].includes(name) + ) { + Assert.equal( + value, + unescaped, + `Attribute ${name} should be complete` + ); + checksCount--; + } + } + break; + case current.TEXT_NODE: + // Check Title. + if (!current.data.startsWith("\n") && current.data.includes("test")) { + Assert.equal( + current.data.trim(), + unescaped, + "Text node should be complete" + ); + checksCount--; + } + break; + } + } + Assert.equal(checksCount, 0, "All the checks ran"); +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js new file mode 100644 index 0000000000..f01048e1a5 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js @@ -0,0 +1,64 @@ +var bookmarkData = [ + { + uri: uri("http://www.toastytech.com"), + title: "Nathan's Toasty Technology Page", + tags: ["technology", "personal", "retro"], + }, + { + uri: uri("http://www.reddit.com"), + title: "reddit: the front page of the internet", + tags: ["social media", "news", "humour"], + }, + { + uri: uri("http://www.4chan.org"), + title: "4chan", + tags: ["discussion", "imageboard", "anime"], + }, +]; + +/* + TEST SUMMARY + - Add bookmarks with tags + - Export tagged bookmarks as HTML file + - Delete bookmarks + - Import bookmarks from HTML file + - Check that all bookmarks are successfully imported with tags +*/ + +add_task(async function test_import_tags() { + // Removes bookmarks.html if the file already exists. + let HTMLFile = PathUtils.join(PathUtils.profileDir, "bookmarks.html"); + await IOUtils.remove(HTMLFile, { ignoreAbsent: true }); + + // Adds bookmarks and tags to the database. + let bookmarkList = new Set(); + for (let { uri, title, tags } of bookmarkData) { + bookmarkList.add( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: uri, + title, + }) + ); + PlacesUtils.tagging.tagURI(uri, tags); + } + + // Exports the bookmarks as a HTML file. + await BookmarkHTMLUtils.exportToFile(HTMLFile); + + // Deletes bookmarks and tags from the database. + for (let bookmark of bookmarkList) { + await PlacesUtils.bookmarks.remove(bookmark.guid); + } + + // Re-imports the bookmarks from the HTML file. + await BookmarkHTMLUtils.importFromFile(HTMLFile, { replace: true }); + + // Tests to ensure that the tags are still present for each bookmark URI. + for (let { uri, tags } of bookmarkData) { + info("Test tags for " + uri.spec + ": " + tags + "\n"); + let foundTags = PlacesUtils.tagging.getTagsForURI(uri); + Assert.equal(foundTags.length, tags.length); + Assert.ok(tags.every(tag => foundTags.includes(tag))); + } +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_localized.js b/toolkit/components/places/tests/unit/test_bookmarks_html_localized.js new file mode 100644 index 0000000000..4ef2393efb --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_localized.js @@ -0,0 +1,51 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { BookmarkHTMLUtils } = ChromeUtils.importESModule( + "resource://gre/modules/BookmarkHTMLUtils.sys.mjs" +); + +add_task(async function setup_l10n() { + // A single localized string. + const mockSource = L10nFileSource.createMock( + "test", + "app", + ["en-US"], + "/localization/{locale}/", + [ + { + path: "/localization/en-US/bookmarks_html_localized.ftl", + source: ` +bookmarks-html-localized-folder = Localized Folder +bookmarks-html-localized-bookmark = Localized Bookmark +`, + }, + ] + ); + + L10nRegistry.getInstance().registerSources([mockSource]); +}); + +add_task(async function test_bookmarks_html_localized() { + let bookmarksFile = PathUtils.join( + do_get_cwd().path, + "bookmarks_html_localized.html" + ); + await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true }); + + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root; + Assert.equal(root.childCount, 1); + let folder = root.getChild(0); + PlacesUtils.asContainer(folder).containerOpen = true; + // Folder title is localized. + Assert.equal(folder.title, "Localized Folder"); + Assert.equal(folder.childCount, 1); + let bookmark = folder.getChild(0); + Assert.equal(bookmark.uri, "http://www.mozilla.com/firefox/help/"); + // Bookmark title is localized. + Assert.equal(bookmark.title, "Localized Bookmark"); + folder.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js new file mode 100644 index 0000000000..07131feafe --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js @@ -0,0 +1,31 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test for bug #801450 + +// Get Services +const { BookmarkHTMLUtils } = ChromeUtils.importESModule( + "resource://gre/modules/BookmarkHTMLUtils.sys.mjs" +); + +add_task(async function test_bookmarks_html_singleframe() { + let bookmarksFile = PathUtils.join( + do_get_cwd().path, + "bookmarks_html_singleframe.html" + ); + await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true }); + + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root; + Assert.equal(root.childCount, 1); + let folder = root.getChild(0); + PlacesUtils.asContainer(folder).containerOpen = true; + Assert.equal(folder.title, "Subtitle"); + Assert.equal(folder.childCount, 1); + let bookmark = folder.getChild(0); + Assert.equal(bookmark.uri, "http://www.mozilla.org/"); + Assert.equal(bookmark.title, "Mozilla"); + folder.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_json.js b/toolkit/components/places/tests/unit/test_bookmarks_json.js new file mode 100644 index 0000000000..19aaac7f95 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js @@ -0,0 +1,368 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { BookmarkJSONUtils } = ChromeUtils.importESModule( + "resource://gre/modules/BookmarkJSONUtils.sys.mjs" +); + +// An object representing the contents of bookmarks.json. +var test_bookmarks = { + menu: [ + { + guid: "OCyeUO5uu9FF", + title: "Mozilla Firefox", + children: [ + { + guid: "OCyeUO5uu9FG", + title: "Help and Tutorials", + url: "http://en-us.www.mozilla.com/en-US/firefox/help/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + guid: "OCyeUO5uu9FH", + title: "Customize Firefox", + url: "http://en-us.www.mozilla.com/en-US/firefox/customize/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + guid: "OCyeUO5uu9FI", + title: "Get Involved", + url: "http://en-us.www.mozilla.com/en-US/firefox/community/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + guid: "OCyeUO5uu9FJ", + title: "About Us", + url: "http://en-us.www.mozilla.com/en-US/about/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + guid: "QFM-QnE2ZpMz", + title: "Test null postData", + url: "http://example.com/search?q=%s&suggid=", + }, + ], + }, + { + guid: "OCyeUO5uu9FK", + type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR, + }, + { + guid: "OCyeUO5uu9FL", + title: "test", + dateAdded: 1177541020000000, + lastModified: 1177541050000000, + children: [ + { + guid: "OCyeUO5uu9GX", + title: "test post keyword", + dateAdded: 1177375336000000, + lastModified: 1177375423000000, + keyword: "test", + postData: "hidden1%3Dbar&text1%3D%25s", + charset: "ISO-8859-1", + }, + ], + }, + ], + toolbar: [ + { + guid: "OCyeUO5uu9FB", + title: "Getting Started", + url: "http://en-us.www.mozilla.com/en-US/firefox/central/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==", + }, + { + guid: "OCyeUO5uu9FR", + title: "Latest Headlines", + // This used to be a livemark, but we don't import them anymore, instead + // it will be imported as an empty folder, because the json format stores + // it like that: an empty folder with a couple annotations. Since + // annotations will go away, there won't be a clean way to import it as a + // bookmark instead. + // Note: date gets truncated to milliseconds, whereas the value in bookmarks.json + // has full microseconds. + dateAdded: 1361551979451000, + lastModified: 1361551979457000, + }, + ], + unfiled: [ + { guid: "OCyeUO5uu9FW", title: "Example.tld", url: "http://example.tld/" }, + { + guid: "Cfkety492Afk", + title: "test tagged bookmark", + dateAdded: 1507025843703000, + lastModified: 1507025844703000, + url: "http://example.tld/tagged", + tags: ["foo"], + }, + { + guid: "lOZGoFR1eXbl", + title: "Bookmarks Toolbar Shortcut", + dateAdded: 1507025843703000, + lastModified: 1507025844703000, + url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`, + }, + { + guid: "7yJWnBVhjRtP", + title: "Folder Shortcut", + dateAdded: 1507025843703000, + lastModified: 1507025844703000, + url: `place:parent=OCyeUO5uu9FF`, + }, + { + guid: "vm5QXWuWc12l", + title: "Folder Shortcut 2", + dateAdded: 1507025843703000, + lastModified: 1507025844703000, + url: "place:invalidOldParentId=6123443&excludeItems=1", + }, + { + guid: "Icg1XlIozA1D", + title: "Folder Shortcut 3", + dateAdded: 1507025843703000, + lastModified: 1507025844703000, + url: `place:parent=OCyeUO5uu9FF&parent=${PlacesUtils.bookmarks.menuGuid}`, + }, + ], +}; + +// Exported bookmarks file pointer. +var bookmarksExportedFile; + +add_task(async function test_import_bookmarks_disallowed_url() { + await Assert.rejects( + BookmarkJSONUtils.importFromURL("http://example.com/bookmarks.json"), + /importFromURL can only be used with/, + "Should reject importing from an http based url" + ); + await Assert.rejects( + BookmarkJSONUtils.importFromURL("https://example.com/bookmarks.json"), + /importFromURL can only be used with/, + "Should reject importing from an https based url" + ); +}); + +add_task(async function test_import_bookmarks_count() { + // Ensure the bookmarks count is correct when importing in various cases + await PlacesUtils.bookmarks.eraseEverything(); + let bookmarksFile = PathUtils.join(do_get_cwd().path, "bookmarks.json"); + bookmarksExportedFile = PathUtils.join( + PathUtils.profileDir, + "bookmarks.exported.json" + ); + + let count = await BookmarkJSONUtils.importFromFile(bookmarksFile, { + replace: true, + }); + Assert.equal( + count, + 13, + "There should be 13 imported bookmarks when importing from an empty database" + ); + + await BookmarkJSONUtils.exportToFile(bookmarksExportedFile); + count = -1; + count = await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { + replace: true, + }); + Assert.equal( + count, + 13, + "There should be 13 imported bookmarks when replacing existing bookmarks" + ); + + await PlacesUtils.bookmarks.eraseEverything(); + count = -1; + let bookmarksUrl = PathUtils.toFileURI(bookmarksFile); + count = await BookmarkJSONUtils.importFromURL(bookmarksUrl); + Assert.equal( + count, + 13, + "There should be 13 imported bookmarks when importing from a URL" + ); + + // Clean up task + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_bookmarks() { + let bookmarksFile = PathUtils.join(do_get_cwd().path, "bookmarks.json"); + + await BookmarkJSONUtils.importFromFile(bookmarksFile, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); +}); + +add_task(async function test_export_bookmarks() { + bookmarksExportedFile = PathUtils.join( + PathUtils.profileDir, + "bookmarks.exported.json" + ); + await BookmarkJSONUtils.exportToFile(bookmarksExportedFile); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function test_import_exported_bookmarks() { + await PlacesUtils.bookmarks.eraseEverything(); + await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { + replace: true, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); +}); + +add_task(async function test_import_ontop() { + await PlacesUtils.bookmarks.eraseEverything(); + await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { + replace: true, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + await BookmarkJSONUtils.exportToFile(bookmarksExportedFile); + await PlacesTestUtils.promiseAsyncUpdates(); + await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { + replace: true, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); +}); + +add_task(async function test_import_iconuri() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + let bookmarksFile = PathUtils.join( + do_get_cwd().path, + "bookmarks_iconuri.json" + ); + + await BookmarkJSONUtils.importFromFile(bookmarksFile, { + replace: true, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); +}); + +add_task(async function test_export_bookmarks_with_iconuri() { + bookmarksExportedFile = PathUtils.join( + PathUtils.profileDir, + "bookmarks.exported.json" + ); + await BookmarkJSONUtils.exportToFile(bookmarksExportedFile); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function test_import_exported_bookmarks_with_iconuri() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { + replace: true, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + await testImportedBookmarks(); +}); + +add_task(async function test_clean() { + await PlacesUtils.bookmarks.eraseEverything(); +}); + +async function testImportedBookmarks() { + for (let group in test_bookmarks) { + info("[testImportedBookmarks()] Checking group '" + group + "'"); + + let root = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks[`${group}Guid`] + ).root; + + let items = test_bookmarks[group]; + Assert.equal(root.childCount, items.length); + + for (let key in items) { + await checkItem(items[key], root.getChild(key)); + } + + root.containerOpen = false; + } +} + +async function checkItem(aExpected, aNode) { + let bookmark = await PlacesUtils.bookmarks.fetch(aNode.bookmarkGuid); + + for (let prop in aExpected) { + switch (prop) { + case "type": + Assert.equal(aNode.type, aExpected.type); + break; + case "title": + Assert.equal(aNode.title, aExpected.title); + break; + case "dateAdded": + Assert.equal( + PlacesUtils.toPRTime(bookmark.dateAdded), + aExpected.dateAdded + ); + break; + case "lastModified": + Assert.equal( + PlacesUtils.toPRTime(bookmark.lastModified), + aExpected.lastModified + ); + break; + case "url": + Assert.equal(aNode.uri, aExpected.url); + break; + case "icon": + let { data } = await getFaviconDataForPage(aExpected.url); + let base64Icon = + "data:image/png;base64," + + base64EncodeString(String.fromCharCode.apply(String, data)); + Assert.equal(base64Icon, aExpected.icon); + break; + case "keyword": { + let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.keyword, aExpected.keyword); + break; + } + case "guid": + Assert.equal(bookmark.guid, aExpected.guid); + break; + case "postData": { + let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.postData, aExpected.postData); + break; + } + case "charset": + let pageInfo = await PlacesUtils.history.fetch(aNode.uri, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + aExpected.charset + ); + break; + case "children": + let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + Assert.equal(folder.hasChildren, !!aExpected.children.length); + folder.containerOpen = true; + Assert.equal(folder.childCount, aExpected.children.length); + + for (let index = 0; index < aExpected.children.length; index++) { + await checkItem(aExpected.children[index], folder.getChild(index)); + } + + folder.containerOpen = false; + break; + case "tags": + let uri = Services.io.newURI(aNode.uri); + Assert.deepEqual( + PlacesUtils.tagging.getTagsForURI(uri), + aExpected.tags, + "should have the expected tags" + ); + break; + default: + throw new Error("Unknown property"); + } + } +} diff --git a/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js b/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js new file mode 100644 index 0000000000..b31f1da5bb --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests for importing a corrupt json file. + * + * The corrupt json file attempts to import into: + * - the menu folder: + * - A bookmark with an invalid type. + * - A valid bookmark. + * - A bookmark with an invalid url. + * - the toolbar folder: + * - A bookmark with an invalid url. + * + * The menu case ensure that we strip out invalid bookmarks, but retain valid + * ones. + * The toolbar case ensures that if no valid bookmarks remain, then we do not + * throw an error. + */ + +const { BookmarkJSONUtils } = ChromeUtils.importESModule( + "resource://gre/modules/BookmarkJSONUtils.sys.mjs" +); + +add_task(async function test_import_bookmarks() { + let bookmarksFile = PathUtils.join( + do_get_cwd().path, + "bookmarks_corrupt.json" + ); + + await BookmarkJSONUtils.importFromFile(bookmarksFile, { replace: true }); + await PlacesTestUtils.promiseAsyncUpdates(); + + let bookmarks = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.menuGuid + ); + + Assert.equal( + bookmarks.children.length, + 1, + "should only be one bookmark in the menu" + ); + let bookmark = bookmarks.children[0]; + Assert.equal(bookmark.guid, "OCyeUO5uu9FH", "should have correct guid"); + Assert.equal( + bookmark.title, + "Customize Firefox", + "should have correct title" + ); + Assert.equal( + bookmark.uri, + "http://en-us.www.mozilla.com/en-US/firefox/customize/", + "should have correct uri" + ); + + bookmarks = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.toolbarGuid + ); + + Assert.ok( + !bookmarks.children, + "should not have any bookmarks in the toolbar" + ); +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js new file mode 100644 index 0000000000..892b2d1d04 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js @@ -0,0 +1,319 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { BookmarkHTMLUtils } = ChromeUtils.importESModule( + "resource://gre/modules/BookmarkHTMLUtils.sys.mjs" +); + +/** + * Tests the bookmarks-restore-* nsIObserver notifications after restoring + * bookmarks from JSON and HTML. See bug 470314. + */ + +// The topics and data passed to nsIObserver.observe() on bookmarks restore +const NSIOBSERVER_TOPIC_BEGIN = "bookmarks-restore-begin"; +const NSIOBSERVER_TOPIC_SUCCESS = "bookmarks-restore-success"; +const NSIOBSERVER_TOPIC_FAILED = "bookmarks-restore-failed"; +const NSIOBSERVER_DATA_JSON = "json"; +const NSIOBSERVER_DATA_HTML = "html"; +const NSIOBSERVER_DATA_HTML_INIT = "html-initial"; + +// Bookmarks are added for these URIs +var uris = [ + "http://example.com/1", + "http://example.com/2", + "http://example.com/3", + "http://example.com/4", + "http://example.com/5", +]; + +/** + * Adds some bookmarks for the URIs in |uris|. + */ +async function addBookmarks() { + for (let url of uris) { + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + Assert.ok(await PlacesUtils.bookmarks.fetch({ url }), "Url is bookmarked"); + } +} + +/** + * Creates an file in the profile directory. + * + * @param aBasename + * e.g., "foo.txt" in the path /some/long/path/foo.txt + * @return {Promise} + * @resolves to an OS.File path + */ +async function promiseFile(aBasename) { + let path = PathUtils.join(PathUtils.profileDir, aBasename); + info("opening " + path); + + await IOUtils.writeUTF8(path, ""); + return path; +} + +/** + * Register observers via promiseTopicObserved helper. + * + * @param {boolean} expectSuccess pass true when expect a success notification + * @return {Promise[]} + */ +function registerObservers(expectSuccess) { + let promiseBegin = promiseTopicObserved(NSIOBSERVER_TOPIC_BEGIN); + let promiseResult; + if (expectSuccess) { + promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_SUCCESS); + } else { + promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_FAILED); + } + + return [promiseBegin, promiseResult]; +} + +/** + * Check notification results. + * + * @param {Promise[]} expectPromises array contain promiseBegin and promiseResult + * @param {object} expectedData contain data and folderId + */ +async function checkObservers(expectPromises, expectedData) { + let [promiseBegin, promiseResult] = expectPromises; + + let beginData = (await promiseBegin)[1]; + Assert.equal( + beginData, + expectedData.data, + "Data for current test should be what is expected" + ); + + let [resultSubject, resultData] = await promiseResult; + Assert.equal( + resultData, + expectedData.data, + "Data for current test should be what is expected" + ); + + // Make sure folder ID is what is expected. For importing HTML into a + // folder, this will be an integer, otherwise null. + if (resultSubject) { + Assert.equal( + resultSubject.QueryInterface(Ci.nsISupportsPRInt64).data, + expectedData.folderId + ); + } else { + Assert.equal(expectedData.folderId, null); + } +} + +/** + * Run after every test cases. + */ +async function teardown(file, begin, success, fail) { + // On restore failed, file may not exist, so wrap in try-catch. + await IOUtils.remove(file, { ignoreAbsent: true }); + + // clean up bookmarks + await PlacesUtils.bookmarks.eraseEverything(); +} + +add_task(async function test_json_restore_normal() { + // data: the data passed to nsIObserver.observe() corresponding to the test + // folderId: for HTML restore into a folder, the folder ID to restore into; + // otherwise, set it to null + let expectedData = { + data: NSIOBSERVER_DATA_JSON, + folderId: null, + }; + let expectPromises = registerObservers(true); + + info("JSON restore: normal restore should succeed"); + let file = await promiseFile("bookmarks-test_restoreNotification.json"); + await addBookmarks(); + + await BookmarkJSONUtils.exportToFile(file); + await PlacesUtils.bookmarks.eraseEverything(); + try { + await BookmarkJSONUtils.importFromFile(file, { replace: true }); + } catch (e) { + do_throw(" Restore should not have failed " + e); + } + + await checkObservers(expectPromises, expectedData); + await teardown(file); +}); + +add_task(async function test_json_restore_empty() { + let expectedData = { + data: NSIOBSERVER_DATA_JSON, + folderId: null, + }; + let expectPromises = registerObservers(false); + + info("JSON restore: empty file should fail"); + let file = await promiseFile("bookmarks-test_restoreNotification.json"); + await Assert.rejects( + BookmarkJSONUtils.importFromFile(file, { replace: true }), + /SyntaxError/, + "Restore should reject for an empty file." + ); + + await checkObservers(expectPromises, expectedData); + await teardown(file); +}); + +add_task(async function test_json_restore_nonexist() { + let expectedData = { + data: NSIOBSERVER_DATA_JSON, + folderId: null, + }; + let expectPromises = registerObservers(false); + + info("JSON restore: nonexistent file should fail"); + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("this file doesn't exist because nobody created it 1"); + await Assert.rejects( + BookmarkJSONUtils.importFromFile(file.path, { replace: true }), + /Cannot restore from nonexisting json file/, + "Restore should reject for a non-existent file." + ); + + await checkObservers(expectPromises, expectedData); + await teardown(file.path); +}); + +add_task(async function test_html_restore_normal() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML, + folderId: null, + }; + let expectPromises = registerObservers(true); + + info("HTML restore: normal restore should succeed"); + let file = await promiseFile("bookmarks-test_restoreNotification.html"); + await addBookmarks(); + await BookmarkHTMLUtils.exportToFile(file); + await PlacesUtils.bookmarks.eraseEverything(); + try { + BookmarkHTMLUtils.importFromFile(file).catch( + do_report_unexpected_exception + ); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + await checkObservers(expectPromises, expectedData); + await teardown(file); +}); + +add_task(async function test_html_restore_empty() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML, + folderId: null, + }; + let expectPromises = registerObservers(true); + + info("HTML restore: empty file should succeed"); + let file = await promiseFile("bookmarks-test_restoreNotification.init.html"); + try { + BookmarkHTMLUtils.importFromFile(file).catch( + do_report_unexpected_exception + ); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + await checkObservers(expectPromises, expectedData); + await teardown(file); +}); + +add_task(async function test_html_restore_nonexist() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML, + folderId: null, + }; + let expectPromises = registerObservers(false); + + info("HTML restore: nonexistent file should fail"); + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("this file doesn't exist because nobody created it 2"); + await Assert.rejects( + BookmarkHTMLUtils.importFromFile(file.path), + /Cannot import from nonexisting html file/, + "Restore should reject for a non-existent file." + ); + + await checkObservers(expectPromises, expectedData); + await teardown(file.path); +}); + +add_task(async function test_html_init_restore_normal() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML_INIT, + folderId: null, + }; + let expectPromises = registerObservers(true); + + info("HTML initial restore: normal restore should succeed"); + let file = await promiseFile("bookmarks-test_restoreNotification.init.html"); + await addBookmarks(); + await BookmarkHTMLUtils.exportToFile(file); + await PlacesUtils.bookmarks.eraseEverything(); + try { + BookmarkHTMLUtils.importFromFile(file, { replace: true }).catch( + do_report_unexpected_exception + ); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + await checkObservers(expectPromises, expectedData); + await teardown(file); +}); + +add_task(async function test_html_init_restore_empty() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML_INIT, + folderId: null, + }; + let expectPromises = registerObservers(true); + + info("HTML initial restore: empty file should succeed"); + let file = await promiseFile("bookmarks-test_restoreNotification.init.html"); + try { + BookmarkHTMLUtils.importFromFile(file, { replace: true }).catch( + do_report_unexpected_exception + ); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + await checkObservers(expectPromises, expectedData); + await teardown(file); +}); + +add_task(async function test_html_init_restore_nonexist() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML_INIT, + folderId: null, + }; + let expectPromises = registerObservers(false); + + info("HTML initial restore: nonexistent file should fail"); + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("this file doesn't exist because nobody created it 3"); + await Assert.rejects( + BookmarkHTMLUtils.importFromFile(file.path, { replace: true }), + /Cannot import from nonexisting html file/, + "Restore should reject for a non-existent file." + ); + + await checkObservers(expectPromises, expectedData); + await teardown(file.path); +}); diff --git a/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js new file mode 100644 index 0000000000..1e1f7bc9c7 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test_brokenFolderShortcut() { + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + url: "http://1.moz.org/", + title: "Bookmark 1", + }, + { + url: "place:parent=1234", + title: "Shortcut 1", + }, + { + url: "place:parent=-1", + title: "Shortcut 2", + }, + { + url: "http://2.moz.org/", + title: "Bookmark 2", + }, + ], + }); + + // Add also a simple visit. + await PlacesTestUtils.addVisits(uri("http://3.moz.org/")); + + // Query containing a broken folder shortcuts among results. + let query = PlacesUtils.history.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.unfiledGuid]); + let options = PlacesUtils.history.getNewQueryOptions(); + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + Assert.equal(root.childCount, 4); + + let shortcut = root.getChild(1); + Assert.equal(shortcut.uri, "place:parent=1234"); + PlacesUtils.asContainer(shortcut); + shortcut.containerOpen = true; + Assert.equal(shortcut.childCount, 0); + shortcut.containerOpen = false; + // Remove the broken shortcut while the containing result is open. + await PlacesUtils.bookmarks.remove(bookmarks[1]); + Assert.equal(root.childCount, 3); + + shortcut = root.getChild(1); + Assert.equal(shortcut.uri, "place:parent=-1"); + PlacesUtils.asContainer(shortcut); + shortcut.containerOpen = true; + Assert.equal(shortcut.childCount, 0); + shortcut.containerOpen = false; + // Remove the broken shortcut while the containing result is open. + await PlacesUtils.bookmarks.remove(bookmarks[2]); + Assert.equal(root.childCount, 2); + + root.containerOpen = false; + + // Broken folder shortcut as root node. + query = PlacesUtils.history.getNewQuery(); + query.setParents([1234]); + options = PlacesUtils.history.getNewQueryOptions(); + root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + Assert.equal(root.childCount, 0); + root.containerOpen = false; + + // Broken folder shortcut as root node with folder=-1. + query = PlacesUtils.history.getNewQuery(); + query.setParents([-1]); + options = PlacesUtils.history.getNewQueryOptions(); + root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + Assert.equal(root.childCount, 0); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_browserhistory.js b/toolkit/components/places/tests/unit/test_browserhistory.js new file mode 100644 index 0000000000..f737262ae7 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_browserhistory.js @@ -0,0 +1,122 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_URI = "http://mozilla.com/"; +const TEST_SUBDOMAIN_URI = "http://foobar.mozilla.com/"; + +async function checkEmptyHistory() { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.executeCached("SELECT count(*) FROM moz_historyvisits"); + return !rows[0].getResultByIndex(0); +} + +add_task(async function test_addPage() { + await PlacesTestUtils.addVisits(TEST_URI); + Assert.ok(!(await checkEmptyHistory()), "History has entries"); +}); + +add_task(async function test_removePage() { + await PlacesUtils.history.remove(TEST_URI); + Assert.ok(await checkEmptyHistory(), "History is empty"); +}); + +add_task(async function test_removePages() { + let pages = []; + for (let i = 0; i < 8; i++) { + pages.push(TEST_URI + i); + } + + await PlacesTestUtils.addVisits(pages.map(uri => ({ uri }))); + // Bookmarked item should not be removed from moz_places. + const ANNO_INDEX = 1; + const ANNO_NAME = "testAnno"; + const ANNO_VALUE = "foo"; + const BOOKMARK_INDEX = 2; + await PlacesUtils.history.update({ + url: pages[ANNO_INDEX], + annotations: new Map([[ANNO_NAME, ANNO_VALUE]]), + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: pages[BOOKMARK_INDEX], + title: "test bookmark", + }); + await PlacesUtils.history.update({ + url: pages[BOOKMARK_INDEX], + annotations: new Map([[ANNO_NAME, ANNO_VALUE]]), + }); + + await PlacesUtils.history.remove(pages); + Assert.ok(await checkEmptyHistory(), "History is empty"); + + // Check that the bookmark and its annotation still exist. + let folder = await PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.unfiledGuid + ); + Assert.equal(folder.root.childCount, 1); + let pageInfo = await PlacesUtils.history.fetch(pages[BOOKMARK_INDEX], { + includeAnnotations: true, + }); + Assert.equal(pageInfo.annotations.get(ANNO_NAME), ANNO_VALUE); + + // Check the annotation on the non-bookmarked page does not exist anymore. + await assertNoOrphanPageAnnotations(); + + // Cleanup. + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_removePagesByTimeframe() { + let visits = []; + let startDate = (Date.now() - 10000) * 1000; + for (let i = 0; i < 10; i++) { + visits.push({ + uri: TEST_URI + i, + visitDate: startDate + i * 1000, + }); + } + + await PlacesTestUtils.addVisits(visits); + + // Delete all pages except the first and the last. + await PlacesUtils.history.removeByFilter({ + beginDate: PlacesUtils.toDate(startDate + 1000), + endDate: PlacesUtils.toDate(startDate + 8000), + }); + + // Check that we have removed the correct pages. + for (let i = 0; i < 10; i++) { + Assert.equal(page_in_database(TEST_URI + i) == 0, i > 0 && i < 9); + } + + // Clear remaining items and check that all pages have been removed. + await PlacesUtils.history.removeByFilter({ + beginDate: PlacesUtils.toDate(startDate), + endDate: PlacesUtils.toDate(startDate + 9000), + }); + Assert.ok(await checkEmptyHistory(), "History is empty"); +}); + +add_task(async function test_removePagesFromHost() { + await PlacesTestUtils.addVisits(TEST_URI); + await PlacesUtils.history.removeByFilter({ host: ".mozilla.com" }); + Assert.ok(await checkEmptyHistory(), "History is empty"); +}); + +add_task(async function test_removePagesFromHost_keepSubdomains() { + await PlacesTestUtils.addVisits([ + { uri: TEST_URI }, + { uri: TEST_SUBDOMAIN_URI }, + ]); + await PlacesUtils.history.removeByFilter({ host: "mozilla.com" }); + Assert.ok(!(await checkEmptyHistory()), "History has entries"); +}); + +add_task(async function test_history_clear() { + await PlacesUtils.history.clear(); + Assert.ok(await checkEmptyHistory(), "History is empty"); +}); diff --git a/toolkit/components/places/tests/unit/test_childlessTags.js b/toolkit/components/places/tests/unit/test_childlessTags.js new file mode 100644 index 0000000000..9f489a266b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_childlessTags.js @@ -0,0 +1,140 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Ensures that removal of a bookmark untags the bookmark if it's no longer + * contained in any regular, non-tag folders. See bug 444849. + */ + +var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService +); + +var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService( + Ci.nsITaggingService +); + +const BOOKMARK_URI = uri("http://example.com/"); + +add_task(async function test_removing_tagged_bookmark_removes_tag() { + print(" Make a bookmark."); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: BOOKMARK_URI, + title: "test bookmark", + }); + + print(" Tag it up."); + let tags = ["foo", "bar"]; + tagssvc.tagURI(BOOKMARK_URI, tags); + ensureTagsExist(tags); + let root = getTagRoot(); + root.containerOpen = true; + let oldCount = root.childCount; + root.containerOpen = false; + + print(" Remove the bookmark. The tags should no longer exist."); + let wait = TestUtils.waitForCondition(() => { + root = getTagRoot(); + root.containerOpen = true; + let val = root.childCount == oldCount - 2; + root.containerOpen = false; + return val; + }); + await PlacesUtils.bookmarks.remove(bookmark.guid); + await wait; + ensureTagsExist([]); +}); + +add_task( + async function test_removing_folder_containing_tagged_bookmark_removes_tag() { + print(" Make a folder."); + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + print(" Stick a bookmark in the folder."); + var bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: BOOKMARK_URI, + title: "test bookmark", + }); + + print(" Tag the bookmark."); + var tags = ["foo", "bar"]; + tagssvc.tagURI(BOOKMARK_URI, tags); + ensureTagsExist(tags); + + // The tag containers are removed in async and take some time + let oldCountFoo = await tagCount("foo"); + let oldCountBar = await tagCount("bar"); + + print(" Remove the folder. The tags should no longer exist."); + + let wait = TestUtils.waitForCondition(async () => { + let newCountFoo = await tagCount("foo"); + let newCountBar = await tagCount("bar"); + return newCountFoo == oldCountFoo - 1 && newCountBar == oldCountBar - 1; + }); + await PlacesUtils.bookmarks.remove(bookmark.guid); + await wait; + ensureTagsExist([]); + } +); + +async function tagCount(aTag) { + let allTags = await PlacesUtils.bookmarks.fetchTags(); + for (let i of allTags) { + if (i.name == aTag) { + return i.count; + } + } + return 0; +} + +function getTagRoot() { + var query = histsvc.getNewQuery(); + var opts = histsvc.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_TAGS_ROOT; + var resultRoot = histsvc.executeQuery(query, opts).root; + return resultRoot; +} +/** + * Runs a tag query and ensures that the tags returned are those and only those + * in aTags. aTags may be empty, in which case this function ensures that no + * tags exist. + * + * @param aTags + * An array of tags (strings) + */ +function ensureTagsExist(aTags) { + var query = histsvc.getNewQuery(); + var opts = histsvc.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_TAGS_ROOT; + var resultRoot = histsvc.executeQuery(query, opts).root; + + // Dupe aTags. + var tags = aTags.slice(0); + + resultRoot.containerOpen = true; + + // Ensure that the number of tags returned from the query is the same as the + // number in |tags|. + Assert.equal(resultRoot.childCount, tags.length); + + // For each tag result from the query, ensure that it's contained in |tags|. + // Remove the tag from |tags| so that we ensure the sets are equal. + for (let i = 0; i < resultRoot.childCount; i++) { + var tag = resultRoot.getChild(i).title; + var indexOfTag = tags.indexOf(tag); + Assert.ok(indexOfTag >= 0); + tags.splice(indexOfTag, 1); + } + + resultRoot.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_frecency_decay.js b/toolkit/components/places/tests/unit/test_frecency_decay.js new file mode 100644 index 0000000000..8fbb08aecc --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_decay.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_FREC_DECAY_RATE_DEF = 0.975; + +/** + * Promises that the pages-rank-changed event has been seen. + * + * @returns {Promise} A promise which is resolved when the notification is seen. + */ +function promiseRankingChanged() { + return PlacesTestUtils.waitForNotification("pages-rank-changed"); +} + +add_task(async function setup() { + Services.prefs.setCharPref( + "places.frecency.decayRate", + PREF_FREC_DECAY_RATE_DEF + ); +}); + +add_task(async function test_isFrecencyDecaying() { + let db = await PlacesUtils.promiseDBConnection(); + async function queryFrecencyDecaying() { + return ( + await db.executeCached(`SELECT is_frecency_decaying()`) + )[0].getResultByIndex(0); + } + PlacesUtils.history.isFrecencyDecaying = true; + Assert.equal(PlacesUtils.history.isFrecencyDecaying, true); + Assert.equal(await queryFrecencyDecaying(), true); + PlacesUtils.history.isFrecencyDecaying = false; + Assert.equal(PlacesUtils.history.isFrecencyDecaying, false); + Assert.equal(await queryFrecencyDecaying(), false); +}); + +add_task(async function test_frecency_decay() { + let unvisitedBookmarkFrecency = Services.prefs.getIntPref( + "places.frecency.unvisitedBookmarkBonus" + ); + + // Add a bookmark and check its frecency. + let url = "http://example.com/b"; + let promiseOne = promiseRankingChanged(); + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await promiseOne; + + let histogram = TelemetryTestUtils.getAndClearHistogram( + "PLACES_IDLE_FRECENCY_DECAY_TIME_MS" + ); + info("Trigger frecency decay."); + Assert.equal(PlacesUtils.history.isFrecencyDecaying, false); + let promiseRanking = promiseRankingChanged(); + + PlacesFrecencyRecalculator.observe(null, "idle-daily", ""); + Assert.equal(PlacesUtils.history.isFrecencyDecaying, true); + info("Wait for completion."); + await PlacesFrecencyRecalculator.pendingFrecencyDecayPromise; + + await promiseRanking; + Assert.equal(PlacesUtils.history.isFrecencyDecaying, false); + + // Now check the new frecency is correct. + let newFrecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url } + ); + + Assert.equal( + newFrecency, + Math.round(unvisitedBookmarkFrecency * PREF_FREC_DECAY_RATE_DEF), + "Frecencies should match" + ); + + let snapshot = histogram.snapshot(); + Assert.greater(snapshot.sum, 0); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_observers.js b/toolkit/components/places/tests/unit/test_frecency_observers.js new file mode 100644 index 0000000000..44747b06f9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_observers.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Each of these tests a path that triggers a frecency update. Together they +// hit all sites that update a frecency. + +// InsertVisitedURIs::UpdateFrecency and History::InsertPlace +add_task( + async function test_InsertVisitedURIs_UpdateFrecency_and_History_InsertPlace() { + // InsertPlace is at the end of a path that UpdateFrecency is also on, so kill + // two birds with one stone and expect two notifications. Trigger the path by + // adding a download. + let url = Services.io.newURI("http://example.com/a"); + let promise = onRankingChanged(); + await PlacesUtils.history.insert({ + url, + visits: [ + { + transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD, + }, + ], + }); + await promise; + } +); + +// nsNavHistory::UpdateFrecency +add_task(async function test_nsNavHistory_UpdateFrecency() { + let url = Services.io.newURI("http://example.com/b"); + let promise = onRankingChanged(); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: "test", + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await promise; +}); + +// History.jsm invalidateFrecencies() +add_task(async function test_invalidateFrecencies() { + let url = Services.io.newURI("http://test-invalidateFrecencies.com/"); + // Bookmarking the URI is enough to add it to moz_places, and importantly, it + // means that removeByFilter doesn't remove it from moz_places, so its + // frecency is able to be changed. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: "test", + }); + let promise = onRankingChanged(); + await PlacesUtils.history.removeByFilter({ host: url.host }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await promise; +}); + +// History.jsm clear() should not cause a frecency recalculation since pages +// are removed. +add_task(async function test_clear() { + let received = []; + let listener = events => + (received = received.concat(events.map(e => e.type))); + PlacesObservers.addListener( + ["history-cleared", "pages-rank-changed"], + listener + ); + await PlacesUtils.history.clear(); + PlacesObservers.removeListener( + ["history-cleared", "pages-rank-changed"], + listener + ); + Assert.deepEqual(received, ["history-cleared"]); +}); + +add_task(async function test_nsNavHistory_idleDaily() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://test-site1.org", + title: "test", + }); + PlacesFrecencyRecalculator.observe(null, "idle-daily", ""); + await Promise.all([onRankingChanged()]); +}); + +add_task(async function test_nsNavHistory_recalculate() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://test-site1.org", + title: "test", + }); + await Promise.all([ + onRankingChanged(), + PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(), + ]); +}); + +function onRankingChanged() { + return PlacesTestUtils.waitForNotification("pages-rank-changed"); +} diff --git a/toolkit/components/places/tests/unit/test_frecency_origins_alternative.js b/toolkit/components/places/tests/unit/test_frecency_origins_alternative.js new file mode 100644 index 0000000000..5519149cac --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_origins_alternative.js @@ -0,0 +1,301 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests alternative origins frecency. +// Note: the order of the tests here matters, since we are emulating subsquent +// starts of the recalculator component with different initial conditions. + +const FEATURE_PREF = "places.frecency.origins.alternative.featureGate"; + +async function restartRecalculator() { + let subject = {}; + PlacesFrecencyRecalculator.observe( + subject, + "test-alternative-frecency-init", + "" + ); + await subject.promise; +} + +async function getAllOrigins() { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute(`SELECT * FROM moz_origins`); + Assert.greater(rows.length, 0); + return rows.map(r => ({ + host: r.getResultByName("host"), + frecency: r.getResultByName("frecency"), + recalc_frecency: r.getResultByName("recalc_frecency"), + alt_frecency: r.getResultByName("alt_frecency"), + recalc_alt_frecency: r.getResultByName("recalc_alt_frecency"), + })); +} + +add_setup(async function () { + await PlacesTestUtils.addVisits([ + "https://testdomain1.moz.org", + "https://testdomain2.moz.org", + "https://testdomain3.moz.org", + ]); + registerCleanupFunction(PlacesUtils.history.clear); +}); + +add_task(async function test_normal_init() { + // Ensure moz_meta doesn't report anything. + Assert.ok( + !PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled, + "Check the pref is disabled by default" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.ok( + ObjectUtils.isEmpty( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + Object.create(null) + ) + ), + "Check there's no variables stored" + ); +}); + +add_task( + { + pref_set: [[FEATURE_PREF, true]], + }, + async function test_enable_init() { + // Set alt_frecency to NULL and recalc_alt_frecency = 0 for the entries in + // moz_origins to verify they are recalculated. + await PlacesTestUtils.updateDatabaseValues("moz_origins", { + alt_frecency: null, + recalc_alt_frecency: 0, + }); + + await restartRecalculator(); + + // Ensure moz_meta doesn't report anything. + Assert.ok( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled, + "Check the pref is enabled" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.equal( + ( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins + .metadataKey, + Object.create(null) + ) + ).version, + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables + .version, + "Check the algorithm version has been stored" + ); + + // Check all alternative frecencies have been calculated, since we just have + // a few. + let origins = await getAllOrigins(); + Assert.ok( + origins.every(o => o.recalc_alt_frecency == 0), + "All the entries have been recalculated" + ); + Assert.ok( + origins.every(o => o.alt_frecency > 0), + "All the entries have been recalculated" + ); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + } +); + +add_task( + { + pref_set: [[FEATURE_PREF, true]], + }, + async function test_different_version() { + let origins = await getAllOrigins(); + Assert.ok( + origins.every(o => o.recalc_alt_frecency == 0), + "All the entries should not need recalculation" + ); + + // Set alt_frecency to NULL for the entries in moz_origins to verify they + // are recalculated. + await PlacesTestUtils.updateDatabaseValues("moz_origins", { + alt_frecency: null, + }); + + // It doesn't matter that the version is, it just have to be different. + let variables = await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + Object.create(null) + ); + variables.version = 999; + await PlacesUtils.metadata.set( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + variables + ); + + await restartRecalculator(); + + // Check alternative frecency has been marked for recalculation. + // Note just after init we reculate a chunk, and this test code is expected + // to run before that... though we can't be sure, so if this starts failing + // intermittently we'll have to add more synchronization test code. + origins = await getAllOrigins(); + // Ensure moz_meta has been updated. + Assert.ok( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled, + "Check the pref is enabled" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.deepEqual( + ( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins + .metadataKey, + Object.create(null) + ) + ).version, + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables + .version, + "Check the algorithm version has been stored" + ); + + // Check all alternative frecencies have been calculated, since we just have + // a few. + origins = await getAllOrigins(); + Assert.ok( + origins.every(o => o.recalc_alt_frecency == 0), + "All the entries have been recalculated" + ); + Assert.ok( + origins.every(o => o.alt_frecency > 0), + "All the entries have been recalculated" + ); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + } +); + +add_task( + { + pref_set: [[FEATURE_PREF, true]], + }, + async function test_different_variables() { + let origins = await getAllOrigins(); + Assert.ok( + origins.every(o => o.recalc_alt_frecency == 0), + "All the entries should not need recalculation" + ); + + // Set alt_frecency to NULL for the entries in moz_origins to verify they + // are recalculated. + await PlacesTestUtils.updateDatabaseValues("moz_origins", { + alt_frecency: null, + }); + + // Change variables. + let variables = await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + Object.create(null) + ); + Assert.greater(Object.keys(variables).length, 1); + Assert.ok("version" in variables, "At least the version is always present"); + await PlacesUtils.metadata.set( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + { + version: + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables + .version, + someVar: 1, + } + ); + + await restartRecalculator(); + + // Ensure moz_meta has been updated. + Assert.ok( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled, + "Check the pref is enabled" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.deepEqual( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + Object.create(null) + ), + variables, + "Check the algorithm variables have been stored" + ); + + // Check all alternative frecencies have been calculated, since we just have + // a few. + origins = await getAllOrigins(); + Assert.ok( + origins.every(o => o.recalc_alt_frecency == 0), + "All the entries have been recalculated" + ); + Assert.ok( + origins.every(o => o.alt_frecency > 0), + "All the entries have been recalculated" + ); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + } +); + +add_task(async function test_disable() { + let origins = await getAllOrigins(); + Assert.ok( + origins.every(o => o.recalc_alt_frecency == 0), + "All the entries should not need recalculation" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.equal( + ( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + Object.create(null) + ) + ).version, + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables + .version, + "Check the algorithm version has been stored" + ); + + await restartRecalculator(); + + // Check alternative frecency has not been marked for recalculation. + origins = await getAllOrigins(); + Assert.ok( + origins.every(o => o.recalc_alt_frecency == 0), + "The entries not have been marked for recalc" + ); + Assert.ok( + origins.every(o => o.alt_frecency === null), + "All the alt_frecency values should have been nullified" + ); + + // Ensure moz_meta has been updated. + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.ok( + ObjectUtils.isEmpty( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey, + Object.create(null) + ) + ), + "Check the algorithm variables has been removed" + ); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_origins_recalc.js b/toolkit/components/places/tests/unit/test_frecency_origins_recalc.js new file mode 100644 index 0000000000..b96f9ecb58 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_origins_recalc.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that recalc_frecency in the moz_origins table works is consistent. + +// This test does not completely cover origins frecency recalculation because +// the current system uses temp tables and triggers to make the recalculation, +// but it's likely that will change in the future and then we can add to this. + +add_task(async function test() { + // test recalc_frecency is set to 1 when frecency of a page changes. + // Add a couple visits, then remove one of them. + const now = new Date(); + const url = "https://mozilla.org/test/"; + await PlacesTestUtils.addVisits([ + { + url, + visitDate: now, + }, + { + url, + visitDate: new Date(new Date().setDate(now.getDate() - 30)), + }, + ]); + // TODO: use PlacesTestUtils.getDatabaseValue once available. + let db = await PlacesUtils.promiseDBConnection(); + Assert.equal( + ( + await db.execute(`SELECT recalc_frecency FROM moz_origins`) + )[0].getResultByIndex(0), + 0, + "Should have been calculated already" + ); + Assert.equal( + ( + await db.execute(`SELECT recalc_alt_frecency FROM moz_origins`) + )[0].getResultByIndex(0), + 1, + "Should not have been calculated" + ); + + // Remove only one visit (otherwise the page would be orphaned). + await PlacesUtils.history.removeVisitsByFilter({ + beginDate: new Date(now.valueOf() - 10000), + endDate: new Date(now.valueOf() + 10000), + }); + Assert.equal( + ( + await db.execute(`SELECT recalc_frecency FROM moz_origins`) + )[0].getResultByIndex(0), + 0, + "Should have been calculated already" + ); + Assert.equal( + ( + await db.execute(`SELECT recalc_alt_frecency FROM moz_origins`) + )[0].getResultByIndex(0), + 1, + "Should not have been calculated yet" + ); + // test recalc_frecency is set back to 0 when frecency of the origin is set +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_pages_alternative.js b/toolkit/components/places/tests/unit/test_frecency_pages_alternative.js new file mode 100644 index 0000000000..7fc21d9eb3 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_pages_alternative.js @@ -0,0 +1,352 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests alternative pages frecency. +// Note: the order of the tests here matters, since we are emulating subsquent +// starts of the recalculator component with different initial conditions. + +const FEATURE_PREF = "places.frecency.pages.alternative.featureGate"; + +async function restartRecalculator() { + let subject = {}; + PlacesFrecencyRecalculator.observe( + subject, + "test-alternative-frecency-init", + "" + ); + await subject.promise; +} + +async function getAllPages() { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute(`SELECT * FROM moz_places`); + Assert.greater(rows.length, 0); + return rows.map(r => ({ + url: r.getResultByName("url"), + frecency: r.getResultByName("frecency"), + recalc_frecency: r.getResultByName("recalc_frecency"), + alt_frecency: r.getResultByName("alt_frecency"), + recalc_alt_frecency: r.getResultByName("recalc_alt_frecency"), + })); +} + +add_setup(async function () { + await PlacesTestUtils.addVisits([ + "https://testdomain1.moz.org", + "https://testdomain2.moz.org", + "https://testdomain3.moz.org", + ]); + registerCleanupFunction(PlacesUtils.history.clear); +}); + +add_task( + { + pref_set: [[FEATURE_PREF, false]], + }, + async function test_normal_init() { + // The test starts with the pref enabled, otherwise we'd not have the SQL + // function defined. So here we disable it, then enable again later. + await restartRecalculator(); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.ok( + ObjectUtils.isEmpty( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ) + ), + "Check there's no variables stored" + ); + } +); + +add_task( + { + pref_set: [[FEATURE_PREF, true]], + }, + async function test_enable_init() { + // Set alt_frecency to NULL and recalc_alt_frecency = 0 for the entries in + // moz_places to verify they are recalculated. + await PlacesTestUtils.updateDatabaseValues("moz_places", { + alt_frecency: null, + recalc_alt_frecency: 0, + }); + + await restartRecalculator(); + + // Ensure moz_meta doesn't report anything. + Assert.ok( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled, + "Check the pref is enabled" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.equal( + ( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ) + ).version, + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables + .version, + "Check the algorithm version has been stored" + ); + + // Check all alternative frecencies have been calculated, since we just have + // a few. + let pages = await getAllPages(); + Assert.ok( + pages.every(p => p.recalc_alt_frecency == 0), + "All the entries have been recalculated" + ); + Assert.ok( + pages.every(p => p.alt_frecency > 0), + "All the entries have been recalculated" + ); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + } +); + +add_task( + { + pref_set: [[FEATURE_PREF, true]], + }, + async function test_different_version() { + let pages = await getAllPages(); + Assert.ok( + pages.every(p => p.recalc_alt_frecency == 0), + "All the entries should not need recalculation" + ); + + // Set alt_frecency to NULL to verify all the entries are recalculated. + await PlacesTestUtils.updateDatabaseValues("moz_places", { + alt_frecency: null, + }); + + // It doesn't matter that the version is, it just have to be different. + let variables = await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ); + variables.version = 999; + await PlacesUtils.metadata.set( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + variables + ); + + await restartRecalculator(); + + // Check alternative frecency has been marked for recalculation. + // Note just after init we reculate a chunk, and this test code is expected + // to run before that... though we can't be sure, so if this starts failing + // intermittently we'll have to add more synchronization test code. + pages = await getAllPages(); + // Ensure moz_meta has been updated. + Assert.ok( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled, + "Check the pref is enabled" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.deepEqual( + ( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ) + ).version, + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables + .version, + "Check the algorithm version has been stored" + ); + + // Check all alternative frecencies have been calculated, since we just have + // a few. + pages = await getAllPages(); + Assert.ok( + pages.every(p => p.recalc_alt_frecency == 0), + "All the entries have been recalculated" + ); + Assert.ok( + pages.every(p => p.alt_frecency > 0), + "All the entries have been recalculated" + ); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + } +); + +add_task( + { + pref_set: [[FEATURE_PREF, true]], + }, + async function test_different_variables() { + let pages = await getAllPages(); + Assert.ok( + pages.every(p => p.recalc_alt_frecency == 0), + "All the entries should not need recalculation" + ); + + // Set alt_frecency to NULL to verify all the entries are recalculated. + await PlacesTestUtils.updateDatabaseValues("moz_places", { + alt_frecency: null, + }); + + // Change variables. + let variables = await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ); + Assert.greater(Object.keys(variables).length, 1); + Assert.ok("version" in variables, "At least the version is always present"); + await PlacesUtils.metadata.set( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + { + version: + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables + .version, + someVar: 1, + } + ); + + await restartRecalculator(); + + // Ensure moz_meta has been updated. + Assert.ok( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled, + "Check the pref is enabled" + ); + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.deepEqual( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ), + variables, + "Check the algorithm variables have been stored" + ); + + // Check all alternative frecencies have been calculated, since we just have + // a few. + pages = await getAllPages(); + Assert.ok( + pages.every(p => p.recalc_alt_frecency == 0), + "All the entries have been recalculated" + ); + Assert.ok( + pages.every(p => p.alt_frecency > 0), + "All the entries have been recalculated" + ); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + } +); + +add_task( + { + pref_set: [[FEATURE_PREF, false]], + }, + async function test_disable() { + let pages = await getAllPages(); + Assert.ok( + pages.every(p => p.recalc_alt_frecency == 0), + "All the entries should not need recalculation" + ); + + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.equal( + ( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ) + ).version, + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables + .version, + "Check the algorithm version has been stored" + ); + + await restartRecalculator(); + + // Check alternative frecency has not been marked for recalculation. + pages = await getAllPages(); + Assert.ok( + pages.every(p => p.recalc_alt_frecency == 0), + "The entries not have been marked for recalc" + ); + Assert.ok( + pages.every(p => p.alt_frecency === null), + "All the alt_frecency values should have been nullified" + ); + + // Ensure moz_meta has been updated. + // Avoid hitting the cache, we want to check the actual database value. + PlacesUtils.metadata.cache.clear(); + Assert.ok( + ObjectUtils.isEmpty( + await PlacesUtils.metadata.get( + PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey, + Object.create(null) + ) + ), + "Check the algorithm variables has been removed" + ); + } +); + +add_task( + { + pref_set: [[FEATURE_PREF, true]], + }, + async function test_score() { + await restartRecalculator(); + + // This is not intended to cover the algorithm as a whole, but just as a + // sanity check for scores. + + await PlacesTestUtils.addVisits([ + { + url: "https://low.moz.org", + transition: PlacesUtils.history.TRANSITIONS.FRAMED_LINK, + }, + { + url: "https://old.moz.org", + visitDate: (Date.now() - 2 * 86400000) * 1000, + }, + { url: "https://base.moz.org" }, + { url: "https://manyvisits.moz.org" }, + { url: "https://manyvisits.moz.org" }, + { url: "https://manyvisits.moz.org" }, + { url: "https://manyvisits.moz.org" }, + ]); + await PlacesUtils.bookmarks.insert({ + url: "https://unvisitedbookmark.moz.org", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + let getFrecency = url => + PlacesTestUtils.getDatabaseValue("moz_places", "alt_frecency", { + url, + }); + let low = await getFrecency("https://low.moz.org/"); + let old = await getFrecency("https://old.moz.org/"); + Assert.greater(old, low); + let base = await getFrecency("https://base.moz.org/"); + Assert.greater(base, old); + let unvisitedBm = await getFrecency("https://unvisitedbookmark.moz.org/"); + Assert.greater(unvisitedBm, base); + let manyVisits = await getFrecency("https://manyvisits.moz.org/"); + Assert.greater(manyVisits, unvisitedBm); + } +); diff --git a/toolkit/components/places/tests/unit/test_frecency_pages_recalc_alt.js b/toolkit/components/places/tests/unit/test_frecency_pages_recalc_alt.js new file mode 100644 index 0000000000..e492099845 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_pages_recalc_alt.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that recalc_alt_frecency in the moz_places table is updated correctly. + +add_task(async function test() { + info("test recalc_alt_frecency is set to 1 when a visit is added"); + const now = new Date(); + const URL = "https://mozilla.org/test/"; + let getRecalc = url => + PlacesTestUtils.getDatabaseValue("moz_places", "recalc_alt_frecency", { + url, + }); + let setRecalc = (url, val) => + PlacesTestUtils.updateDatabaseValues( + "moz_places", + { recalc_alt_frecency: val }, + { url } + ); + let getFrecency = url => + PlacesTestUtils.getDatabaseValue("moz_places", "alt_frecency", { + url, + }); + await PlacesTestUtils.addVisits([ + { + url: URL, + visitDate: now, + }, + { + url: URL, + visitDate: new Date(new Date().setDate(now.getDate() - 30)), + }, + ]); + Assert.equal(await getRecalc(URL), 1); + Assert.greater(await getFrecency(URL), 0); + + info("Remove just one visit (otherwise the page would be orphaned)."); + await PlacesUtils.history.removeVisitsByFilter({ + beginDate: new Date(now.valueOf() - 10000), + endDate: new Date(now.valueOf() + 10000), + }); + Assert.equal(await getRecalc(URL), 1); + await setRecalc(URL, 0); + + info("Add a bookmark to the page"); + let bm = await PlacesUtils.bookmarks.insert({ + url: URL, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + Assert.equal(await getRecalc(URL), 1); + await setRecalc(URL, 0); + + info("Clear history"); + await PlacesUtils.history.clear(); + Assert.equal(await getRecalc(URL), 1); + await setRecalc(URL, 0); + + // Add back a visit so the page is not an orphan once we remove the bookmark. + await PlacesTestUtils.addVisits(URL); + Assert.equal(await getRecalc(URL), 0); + Assert.greater(await getFrecency(URL), 0); + + info("change the bookmark URL"); + const URL2 = "https://editedbookmark.org/"; + bm.url = URL2; + await PlacesUtils.bookmarks.update(bm); + Assert.equal(await getRecalc(URL), 1); + Assert.equal(await getRecalc(URL2), 1); + await setRecalc(URL, 0); + await setRecalc(URL2, 0); + + info("Remove the bookmark from the page"); + await PlacesUtils.bookmarks.remove(bm); + Assert.equal(await getRecalc(URL2), 1); + await setRecalc(URL2, 0); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_recalc_triggers.js b/toolkit/components/places/tests/unit/test_frecency_recalc_triggers.js new file mode 100644 index 0000000000..2e75a6d459 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_recalc_triggers.js @@ -0,0 +1,281 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that all the operations affecting frecency are either recalculating + * immediately or triggering a recalculation. + * Operations that should recalculate immediately: + * - adding visits + * Operations that should just trigger a recalculation: + * - removing visits + * - adding a bookmark + * - removing a bookmark + * - changing url of a bookmark + * + * Also check setting a frecency resets recalc_frecency to 0. + **/ + +const TEST_URL = "https://example.com/"; +const TEST_URL_2 = "https://example2.com/"; + +// NOTE: Until we fix Bug 1806666 this test has to run queries manually because +// the official APIs recalculate frecency immediately. After the fix, these +// helpers can be removed and the test can be much simpler. +function insertVisit(url) { + return PlacesUtils.withConnectionWrapper("insertVisit", async db => { + await db.execute( + `INSERT INTO moz_historyvisits(place_id, visit_date, visit_type) + VALUES ((SELECT id FROM moz_places WHERE url = :url), 1648226608386000, 1)`, + { url } + ); + }); +} +function removeVisit(url) { + return PlacesUtils.withConnectionWrapper("insertVisit", async db => { + await db.execute( + `DELETE FROM moz_historyvisits WHERE place_id + = (SELECT id FROM moz_places WHERE url = :url)`, + { url } + ); + }); +} +function resetFrecency(url) { + return PlacesUtils.withConnectionWrapper("insertVisit", async db => { + await db.execute(`UPDATE moz_places SET frecency = -1 WHERE url = :url`, { + url, + }); + }); +} + +add_task(async function test_visit() { + // First add a bookmark so the page is not orphaned. + let bm = await PlacesUtils.bookmarks.insert({ + url: TEST_URL, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + + info("Add a visit check frecency is calculated immediately"); + await PlacesTestUtils.addVisits(TEST_URL); + let originalFrecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { + url: TEST_URL, + } + ); + Assert.greater(originalFrecency, 0, "frecency was recalculated immediately"); + let recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 0, "frecency doesn't need a recalc"); + + info("Add a visit (raw query) check frecency is not calculated immediately"); + await insertVisit(TEST_URL); + let frecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { + url: TEST_URL, + } + ); + Assert.equal(frecency, originalFrecency, "frecency is unchanged"); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 1, "frecency needs a recalc"); + + info("Check setting frecency resets recalc_frecency"); + await resetFrecency(TEST_URL); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 0, "frecency doesn't need a recalc"); + + info("Removing a visit sets recalc_frecency"); + await removeVisit(TEST_URL); + frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL, + }); + Assert.equal(frecency, -1, "frecency is unchanged"); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 1, "frecency needs a recalc"); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.remove(bm); +}); + +add_task(async function test_bookmark() { + // First add a visit so the page is not orphaned. + await PlacesTestUtils.addVisits([TEST_URL, TEST_URL_2]); + + let originalFrecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { + url: TEST_URL, + } + ); + Assert.greater(originalFrecency, 0); + + info("Check adding a bookmark sets recalc_frecency"); + let bm = await PlacesUtils.bookmarks.insert({ + url: TEST_URL, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + + let frecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { + url: TEST_URL, + } + ); + Assert.equal(frecency, originalFrecency, "frecency is unchanged"); + let recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 1, "frecency needs a recalc"); + + info("Check changing a bookmark url sets recalc_frecency on both urls"); + await PlacesUtils.bookmarks.update({ + guid: bm.guid, + url: TEST_URL_2, + }); + frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL, + }); + Assert.equal(frecency, originalFrecency, "frecency is unchanged"); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 1, "frecency needs a recalc"); + frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL_2, + }); + Assert.ok(frecency > 0, "frecency is valid"); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL_2, + } + ); + Assert.equal(recalc, 1, "frecency needs a recalc"); + + info("Check setting frecency resets recalc_frecency"); + await resetFrecency(TEST_URL); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 0, "frecency doesn't need a recalc"); + await resetFrecency(TEST_URL_2); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL_2, + } + ); + Assert.equal(recalc, 0, "frecency doesn't need a recalc"); + + info("Removing a bookmark sets recalc_frecency"); + await PlacesUtils.bookmarks.remove(bm.guid); + frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL, + }); + Assert.equal(frecency, -1, "frecency is unchanged"); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL, + } + ); + Assert.equal(recalc, 0, "frecency doesn't need a recalc"); + frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URL_2, + }); + Assert.equal(frecency, -1, "frecency is unchanged"); + recalc = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "recalc_frecency", + { + url: TEST_URL_2, + } + ); + Assert.equal(recalc, 1, "frecency needs a recalc"); + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_bookmark_frecency_zero() { + info("A url with frecency 0 should be recalculated if bookmarked"); + let url = "https://zerofrecency.org/"; + await PlacesTestUtils.addVisits({ url, transition: TRANSITION_FRAMED_LINK }); + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url }), + 0 + ); + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "recalc_frecency", { + url, + }), + 0 + ); + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "recalc_frecency", { + url, + }), + 1 + ); + info("place: uris should not be recalculated"); + url = "place:test"; + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url }), + 0 + ); + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "recalc_frecency", { + url, + }), + 0 + ); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_recalculator.js b/toolkit/components/places/tests/unit/test_frecency_recalculator.js new file mode 100644 index 0000000000..91ed743f79 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_recalculator.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test PlacesFrecencyRecalculator scheduling. + +// Enable the collection (during test) for all products so even products +// that don't collect the data will be able to run the test without failure. +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +async function getOriginFrecency(origin) { + let db = await PlacesUtils.promiseDBConnection(); + return ( + await db.execute( + `SELECT frecency + FROM moz_origins + WHERE host = :origin`, + { origin } + ) + )[0].getResultByIndex(0); +} + +async function resetOriginFrecency(origin) { + await PlacesUtils.withConnectionWrapper( + "test_frecency_recalculator reset origin", + async db => { + await db.executeCached( + `UPDATE moz_origins + SET frecency = -1 + WHERE host = :origin`, + { origin } + ); + } + ); +} + +async function addVisitsAndSetRecalc(urls) { + await PlacesTestUtils.addVisits(urls); + await PlacesUtils.withConnectionWrapper( + "test_frecency_recalculator set recalc", + async db => { + await db.executeCached( + `UPDATE moz_places + SET frecency = -1 + WHERE url in ( + ${PlacesUtils.sqlBindPlaceholders(urls)} + )`, + urls + ); + await db.executeCached(`DELETE FROM moz_updateoriginsupdate_temp`); + await db.executeCached( + `UPDATE moz_places + SET recalc_frecency = (CASE WHEN url in ( + ${PlacesUtils.sqlBindPlaceholders(urls)} + ) THEN 1 ELSE 0 END)`, + urls + ); + } + ); +} + +add_task(async function test() { + info("On startup a recalculation is always pending."); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + // If everything gets recalculated, then it should not be pending anymore. + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + Assert.ok( + !PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should not be pending" + ); + + // If after a recalculation there's outdated entries left, a new recalculation + // should be pending. + info("Insert outdated frecencies"); + const url1 = new URL("https://test1.moz.org/"); + const url2 = new URL("https://test2.moz.org/"); + await addVisitsAndSetRecalc([url1.href, url2.href]); + await resetOriginFrecency(url1.host); + await resetOriginFrecency(url2.host); + + Assert.ok( + PlacesUtils.history.shouldStartFrecencyRecalculation, + "Should have set shouldStartFrecencyRecalculation" + ); + await PlacesFrecencyRecalculator.recalculateSomeFrecencies({ chunkSize: 1 }); + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + await PlacesFrecencyRecalculator.recalculateSomeFrecencies({ chunkSize: 2 }); + Assert.ok( + !PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should not be pending" + ); + Assert.ok( + !PlacesUtils.history.shouldStartFrecencyRecalculation, + "Should have unset shouldStartFrecencyRecalculation" + ); + + Assert.greater(await getOriginFrecency(url1.host), 0); + Assert.greater(await getOriginFrecency(url2.host), 0); + + info("Changing recalc_frecency of an entry adds a pending recalculation."); + PlacesUtils.history.shouldStartFrecencyRecalculation = false; + let promiseNotify = TestUtils.topicObserved("frecency-recalculation-needed"); + await PlacesUtils.withConnectionWrapper( + "test_frecency_recalculator", + async db => { + await db.executeCached( + `UPDATE moz_places SET recalc_frecency = 1 WHERE url = :url`, + { url: url1.href } + ); + } + ); + Assert.ok( + PlacesUtils.history.shouldStartFrecencyRecalculation, + "Should have set shouldStartFrecencyRecalculation" + ); + + await promiseNotify; + Assert.ok( + PlacesFrecencyRecalculator.isRecalculationPending, + "Recalculation should be pending" + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + Assert.ok( + !PlacesUtils.history.shouldStartFrecencyRecalculation, + "Should have unset shouldStartFrecencyRecalculation" + ); +}); + +add_task(async function test_chunk_time_telemetry() { + await PlacesUtils.bookmarks.insert({ + url: "https://test-bookmark.com", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + Assert.ok( + PlacesUtils.history.shouldStartFrecencyRecalculation, + "Should have set shouldStartFrecencyRecalculation" + ); + let histogram = TelemetryTestUtils.getAndClearHistogram( + "PLACES_FRECENCY_RECALC_CHUNK_TIME_MS" + ); + let subject = {}; + PlacesFrecencyRecalculator.observe(subject, "test-execute-taskFn", ""); + await subject.promise; + let snapshot = histogram.snapshot(); + Assert.equal( + Object.values(snapshot.values).reduce((a, b) => a + b, 0), + 1 + ); + Assert.greater(snapshot.sum, 0); + Assert.ok( + !PlacesUtils.history.shouldStartFrecencyRecalculation, + "Should have unset shouldStartFrecencyRecalculation" + ); + + // It should now not report any new time, since there's nothing to recalculate. + histogram.clear(); + PlacesFrecencyRecalculator.observe(subject, "test-execute-taskFn", ""); + await subject.promise; + snapshot = histogram.snapshot(); + Assert.equal( + Object.values(snapshot.values).reduce((a, b) => a + b, 0), + 0 + ); + Assert.ok( + !PlacesUtils.history.shouldStartFrecencyRecalculation, + "Should still not have set shouldStartFrecencyRecalculation" + ); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_unvisited_bookmark.js b/toolkit/components/places/tests/unit/test_frecency_unvisited_bookmark.js new file mode 100644 index 0000000000..ae7bd3d813 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_unvisited_bookmark.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests frecency of unvisited bookmarks. + +add_task(async function () { + // Randomly sorted by date. + const now = new Date(); + const bookmarks = [ + { + url: "https://example.com/1", + date: new Date(new Date().setDate(now.getDate() - 30)), + }, + { + url: "https://example.com/2", + date: new Date(new Date().setDate(now.getDate() - 1)), + }, + { + url: "https://example.com/3", + date: new Date(new Date().setDate(now.getDate() - 100)), + }, + { + url: "https://example.com/1", // Same url but much older. + date: new Date(new Date().setDate(now.getDate() - 120)), + }, + ]; + + for (let bookmark of bookmarks) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: bookmark.url, + dateAdded: bookmark.date, + }); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // The newest bookmark should have an higher frecency. + Assert.greater( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: bookmarks[1].url, + }), + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: bookmarks[0].url, + }) + ); + Assert.greater( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: bookmarks[0].url, + }), + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: bookmarks[2].url, + }) + ); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_zero_updated.js b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js new file mode 100644 index 0000000000..44c329635e --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests a zero frecency is correctly updated when inserting new valid visits. + +add_task(async function () { + const TEST_URI = NetUtil.newURI("http://example.com/"); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URI, + title: "A title", + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + Assert.ok( + (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + })) > 0 + ); + + // Removing the bookmark should leave an orphan page with zero frecency. + // Note this would usually be expired later by expiration. + await PlacesUtils.bookmarks.remove(bookmark.guid); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + // Now add a valid visit to the page, frecency should increase. + await PlacesTestUtils.addVisits({ uri: TEST_URI }); + Assert.ok( + (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + })) > 0 + ); +}); diff --git a/toolkit/components/places/tests/unit/test_getChildIndex.js b/toolkit/components/places/tests/unit/test_getChildIndex.js new file mode 100644 index 0000000000..35eb6fd22b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_getChildIndex.js @@ -0,0 +1,73 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests nsNavHistoryContainerResultNode::GetChildIndex(aNode) functionality. + */ + +add_task(async function test_get_child_index() { + // Add a bookmark to the menu. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://test.mozilla.org/bookmark/", + title: "Test bookmark", + }); + + // Add a bookmark to unfiled folder. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://test.mozilla.org/unfiled/", + title: "Unfiled bookmark", + }); + + // Get the unfiled bookmark node. + let unfiledNode = getNodeAt(PlacesUtils.bookmarks.unfiledGuid, 0); + if (!unfiledNode) { + do_throw("Unable to find bookmark in hierarchy!"); + } + Assert.equal(unfiledNode.title, "Unfiled bookmark"); + + let hs = PlacesUtils.history; + let query = hs.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.menuGuid]); + let options = hs.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = hs.executeQuery(query, options).root; + root.containerOpen = true; + + // Check functionality for proper nodes. + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + print("Now testing: " + node.title); + Assert.equal(root.getChildIndex(node), i); + } + + // Now search for an invalid node and expect an exception. + try { + root.getChildIndex(unfiledNode); + do_throw("Searching for an invalid node should have thrown."); + } catch (ex) { + print("We correctly got an exception."); + } + + root.containerOpen = false; +}); + +function getNodeAt(aFolderGuid, aIndex) { + let hs = PlacesUtils.history; + let query = hs.getNewQuery(); + query.setParents([aFolderGuid]); + let options = hs.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = hs.executeQuery(query, options).root; + root.containerOpen = true; + if (root.childCount < aIndex) { + do_throw("Not enough children to find bookmark!"); + } + let node = root.getChild(aIndex); + root.containerOpen = false; + return node; +} diff --git a/toolkit/components/places/tests/unit/test_get_query_param_sql_function.js b/toolkit/components/places/tests/unit/test_get_query_param_sql_function.js new file mode 100644 index 0000000000..072f53ceac --- /dev/null +++ b/toolkit/components/places/tests/unit/test_get_query_param_sql_function.js @@ -0,0 +1,21 @@ +add_task(async function test_get_query_param_sql_function() { + let db = await PlacesUtils.promiseDBConnection(); + await Assert.rejects( + db.execute(`SELECT get_query_param()`), + /wrong number of arguments/ + ); + let rows = await db.execute(`SELECT + get_query_param('a=b&c=d', 'a'), + get_query_param('a=b&c=d', 'c'), + get_query_param('a=b&a=c', 'a'), + get_query_param('a=b&c=d', 'e'), + get_query_param('a', 'a'), + get_query_param(NULL, NULL), + get_query_param('a=b&c=d', NULL), + get_query_param(NULL, 'a')`); + let results = ["b", "d", "b", null, "", null, null, null]; + equal(rows[0].numEntries, results.length); + for (let i = 0; i < results.length; ++i) { + equal(rows[0].getResultByIndex(i), results[i]); + } +}); diff --git a/toolkit/components/places/tests/unit/test_hash.js b/toolkit/components/places/tests/unit/test_hash.js new file mode 100644 index 0000000000..701cb3d151 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_hash.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + // Check particular unicode urls with insertion and selection APIs to ensure + // url hashes match properly. + const URLS = [ + "http://президент.президент/президент/", + "https://www.аррӏе.com/аррӏе/", + "http://名がドメイン/", + ]; + + for (let url of URLS) { + await PlacesTestUtils.addVisits(url); + Assert.ok(await PlacesUtils.history.fetch(url), "Found the added visit"); + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + Assert.ok( + await PlacesUtils.bookmarks.fetch({ url }), + "Found the added bookmark" + ); + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute( + "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url", + { url: new URL(url).href } + ); + Assert.equal(rows.length, 1, "Matched the place from the database"); + let id = rows[0].getResultByName("id"); + + // Now, suppose the urls has been inserted without proper parsing and retry. + // This should normally not happen through the API, but we have evidence + // it somehow happened. + await PlacesUtils.withConnectionWrapper("test_hash.js", async wdb => { + await wdb.execute( + ` + UPDATE moz_places SET url_hash = hash(:url), url = :url + WHERE id = :id + `, + { url, id } + ); + rows = await wdb.execute( + "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url", + { url } + ); + Assert.equal(rows.length, 1, "Matched the place from the database"); + }); + } +}); diff --git a/toolkit/components/places/tests/unit/test_history.js b/toolkit/components/places/tests/unit/test_history.js new file mode 100644 index 0000000000..a96d1fa5f0 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history.js @@ -0,0 +1,178 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history services +var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService +); + +/** + * Checks to see that a URI is in the database. + * + * @param aURI + * The URI to check. + * @returns true if the URI is in the DB, false otherwise. + */ +function uri_in_db(aURI) { + var options = histsvc.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI; + var query = histsvc.getNewQuery(); + query.uri = aURI; + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + root.containerOpen = false; + return cc == 1; +} + +// main + +add_task(async function test_execute() { + // we have a new profile, so we should have imported bookmarks + Assert.equal(histsvc.databaseStatus, histsvc.DATABASE_STATUS_CREATE); + + // add a visit + var testURI = uri("http://mozilla.com"); + await PlacesTestUtils.addVisits(testURI); + + // now query for the visit, setting sorting and limit such that + // we should retrieve only the visit we just added + var options = histsvc.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.maxResults = 1; + options.resultType = options.RESULTS_AS_VISIT; + var query = histsvc.getNewQuery(); + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + for (var i = 0; i < cc; ++i) { + var node = root.getChild(i); + // test node properties in RESULTS_AS_VISIT + Assert.equal(node.uri, testURI.spec); + Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // add another visit for the same URI, and a third visit for a different URI + var testURI2 = uri("http://google.com/"); + await PlacesTestUtils.addVisits(testURI); + await PlacesTestUtils.addVisits(testURI2); + + options.maxResults = 5; + options.resultType = options.RESULTS_AS_URI; + + // test minVisits + query.minVisits = 0; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 2); + result.root.containerOpen = false; + query.minVisits = 1; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 2); + result.root.containerOpen = false; + query.minVisits = 2; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 1); + query.minVisits = 3; + result.root.containerOpen = false; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 0); + result.root.containerOpen = false; + + // test maxVisits + query.minVisits = -1; + query.maxVisits = -1; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 2); + result.root.containerOpen = false; + query.maxVisits = 0; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 0); + result.root.containerOpen = false; + query.maxVisits = 1; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 1); + result.root.containerOpen = false; + query.maxVisits = 2; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 2); + result.root.containerOpen = false; + query.maxVisits = 3; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 2); + result.root.containerOpen = false; + + // test annotation-based queries + await PlacesUtils.history.update({ + url: "http://mozilla.com/", + annotations: new Map([["testAnno", 123]]), + }); + query.annotation = "testAnno"; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 1); + Assert.equal(result.root.getChild(0).uri, "http://mozilla.com/"); + result.root.containerOpen = false; + + // test annotationIsNot + query.annotationIsNot = true; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + Assert.equal(result.root.childCount, 1); + Assert.equal(result.root.getChild(0).uri, "http://google.com/"); + result.root.containerOpen = false; + + // By default history is enabled. + Assert.ok(!histsvc.historyDisabled); + + // test getPageTitle + await PlacesTestUtils.addVisits({ + uri: uri("http://example.com"), + title: "title", + }); + let placeInfo = await PlacesUtils.history.fetch("http://example.com"); + Assert.equal(placeInfo.title, "title"); + + // query for the visit + Assert.ok(uri_in_db(testURI)); + + // test for schema changes in bug 373239 + // get direct db connection + var db = histsvc.DBConnection; + var q = "SELECT id FROM moz_bookmarks"; + var statement; + try { + statement = db.createStatement(q); + } catch (ex) { + do_throw("bookmarks table does not have id field, schema is too old!"); + } finally { + statement.finalize(); + } + + // bug 394741 - regressed history text searches + await PlacesTestUtils.addVisits(uri("http://mozilla.com")); + options = histsvc.getNewQueryOptions(); + // options.resultType = options.RESULTS_AS_VISIT; + query = histsvc.getNewQuery(); + query.searchTerms = "moz"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.ok(root.childCount > 0); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_history_clear.js b/toolkit/components/places/tests/unit/test_history_clear.js new file mode 100644 index 0000000000..7c121a5b84 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_clear.js @@ -0,0 +1,146 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var mDBConn = DBConn(); + +add_task(async function test_history_clear() { + await PlacesTestUtils.addVisits([ + { uri: uri("http://typed.mozilla.org/"), transition: TRANSITION_TYPED }, + { uri: uri("http://link.mozilla.org/"), transition: TRANSITION_LINK }, + { + uri: uri("http://download.mozilla.org/"), + transition: TRANSITION_DOWNLOAD, + }, + { + uri: uri("http://redir_temp.mozilla.org/"), + transition: TRANSITION_REDIRECT_TEMPORARY, + referrer: "http://link.mozilla.org/", + }, + { + uri: uri("http://redir_perm.mozilla.org/"), + transition: TRANSITION_REDIRECT_PERMANENT, + referrer: "http://link.mozilla.org/", + }, + ]); + + // add a place: bookmark + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: `place:parent=${PlacesUtils.bookmarks.tagsGuid}`, + title: "shortcut", + }); + + // Add an expire never annotation + // Actually expire never annotations are removed as soon as a page is removed + // from the database, so this should act as a normal visit. + await PlacesUtils.history.update({ + url: "http://download.mozilla.org/", + annotations: new Map([["never", "never"]]), + }); + + // Add a bookmark + // Bookmarked page should have history cleared and frecency to be recalculated + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://typed.mozilla.org/", + title: "bookmark", + }); + + await PlacesTestUtils.addVisits([ + { uri: uri("http://typed.mozilla.org/"), transition: TRANSITION_BOOKMARK }, + { uri: uri("http://frecency.mozilla.org/"), transition: TRANSITION_LINK }, + ]); + await PlacesTestUtils.promiseAsyncUpdates(); + + // Clear history and wait for the history-cleared event notification. + let promiseClearHistory = + PlacesTestUtils.waitForNotification("history-cleared"); + await PlacesUtils.history.clear(); + await promiseClearHistory; + await PlacesTestUtils.promiseAsyncUpdates(); + + // Check that frecency for not cleared items (bookmarks) has been marked + // as to be recalculated. + let stmt = mDBConn.createStatement( + "SELECT h.id FROM moz_places h WHERE frecency <> 0 AND h.recalc_frecency = 0 " + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + + stmt = mDBConn.createStatement( + `SELECT h.id FROM moz_places h WHERE h.recalc_frecency = 1 + AND EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1` + ); + Assert.ok(stmt.executeStep()); + stmt.finalize(); + + // Check that all visit_counts have been brought to 0 + stmt = mDBConn.createStatement( + "SELECT id FROM moz_places WHERE visit_count <> 0 LIMIT 1" + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + + // Check that history tables are empty + stmt = mDBConn.createStatement( + "SELECT * FROM (SELECT id FROM moz_historyvisits LIMIT 1)" + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + + // Check that all moz_places entries except bookmarks and place: have been removed + stmt = mDBConn.createStatement( + `SELECT h.id FROM moz_places h WHERE + url_hash NOT BETWEEN hash('place', 'prefix_lo') AND hash('place', 'prefix_hi') + AND NOT EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1` + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + + // Check that we only have favicons for retained places + stmt = mDBConn.createStatement( + `SELECT 1 + FROM moz_pages_w_icons + LEFT JOIN moz_places h ON url_hash = page_url_hash AND url = page_url + WHERE h.id ISNULL` + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + stmt = mDBConn.createStatement( + `SELECT 1 + FROM moz_icons WHERE id NOT IN ( + SELECT icon_id FROM moz_icons_to_pages + )` + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + + // Check that we only have annotations for retained places + stmt = mDBConn.createStatement( + `SELECT a.id FROM moz_annos a WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE id = a.place_id) LIMIT 1` + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + + // Check that we only have inputhistory for retained places + stmt = mDBConn.createStatement( + `SELECT i.place_id FROM moz_inputhistory i WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE id = i.place_id) LIMIT 1` + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); + + // Check that place:uris have frecency 0 + stmt = mDBConn.createStatement( + `SELECT h.id FROM moz_places h + WHERE url_hash BETWEEN hash('place', 'prefix_lo') + AND hash('place', 'prefix_hi') + AND h.frecency <> 0 LIMIT 1` + ); + Assert.ok(!stmt.executeStep()); + stmt.finalize(); +}); diff --git a/toolkit/components/places/tests/unit/test_history_notifications.js b/toolkit/components/places/tests/unit/test_history_notifications.js new file mode 100644 index 0000000000..339080b042 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_notifications.js @@ -0,0 +1,50 @@ +const NS_PLACES_INIT_COMPLETE_TOPIC = "places-init-complete"; +let gLockedConn; + +add_task(async function setup() { + // Create a dummy places.sqlite and open an unshared connection on it + let db = Services.dirsvc.get("ProfD", Ci.nsIFile); + db.append("places.sqlite"); + gLockedConn = Services.storage.openUnsharedDatabase(db); + Assert.ok(db.exists(), "The database should have been created"); + + // We need an exclusive lock on the db + gLockedConn.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE"); + // Exclusive locking is lazy applied, we need to make a write to activate it + gLockedConn.executeSimpleSQL("PRAGMA USER_VERSION = 1"); +}); + +add_task(async function locked() { + // Try to create history service while the db is locked. + // It should be possible to create the service, but any method using the + // database will fail. + let resolved = false; + let promiseComplete = promiseTopicObserved( + NS_PLACES_INIT_COMPLETE_TOPIC + ).then(() => (resolved = true)); + let history = Cc["@mozilla.org/browser/nav-history-service;1"].createInstance( + Ci.nsINavHistoryService + ); + // The notification shouldn't happen until something tries to use the database. + await new Promise(resolve => do_timeout(100, resolve)); + Assert.equal( + resolved, + false, + "The notification should not have been fired yet" + ); + // This will initialize the database. + Assert.equal(history.databaseStatus, history.DATABASE_STATUS_LOCKED); + await promiseComplete; + + // Close our connection and try to cleanup the file (could fail on Windows) + gLockedConn.close(); + let db = Services.dirsvc.get("ProfD", Ci.nsIFile); + db.append("places.sqlite"); + if (db.exists()) { + try { + db.remove(false); + } catch (e) { + info("Unable to remove dummy places.sqlite"); + } + } +}); diff --git a/toolkit/components/places/tests/unit/test_history_observer.js b/toolkit/components/places/tests/unit/test_history_observer.js new file mode 100644 index 0000000000..1a9323f890 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_observer.js @@ -0,0 +1,186 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Registers a one-time places observer for 'page-visited', + * which resolves a promise on being called. + */ +function promiseVisitAdded(callback) { + return new Promise(resolve => { + async function listener(events) { + PlacesObservers.removeListener(["page-visited"], listener); + Assert.equal(events.length, 1, "Right number of visits notified"); + Assert.equal(events[0].type, "page-visited"); + await callback(events[0]); + resolve(); + } + PlacesObservers.addListener(["page-visited"], listener); + }); +} + +/** + * Asynchronous task that adds a visit to the history database. + */ +async function task_add_visit(uri, timestamp, transition) { + uri = uri || NetUtil.newURI("http://firefox.com/"); + timestamp = timestamp || Date.now() * 1000; + await PlacesTestUtils.addVisits({ + uri, + transition: transition || TRANSITION_TYPED, + visitDate: timestamp, + }); + return [uri, timestamp]; +} + +add_task(async function test_visitAdded() { + let promiseNotify = promiseVisitAdded(async function (visit) { + Assert.ok(visit.visitId > 0); + Assert.equal(visit.url, testuri.spec); + Assert.equal(visit.visitTime, testtime / 1000); + Assert.equal(visit.referringVisitId, 0); + Assert.equal(visit.transitionType, TRANSITION_TYPED); + let uri = NetUtil.newURI(visit.url); + await check_guid_for_uri(uri, visit.pageGuid); + Assert.ok(!visit.hidden); + Assert.equal(visit.visitCount, 1); + Assert.equal(visit.typedCount, 1); + }); + let testuri = NetUtil.newURI("http://firefox.com/"); + let testtime = Date.now() * 1000; + await task_add_visit(testuri, testtime); + await promiseNotify; +}); + +add_task(async function test_visitAdded() { + let promiseNotify = promiseVisitAdded(async function (visit) { + Assert.ok(visit.visitId > 0); + Assert.equal(visit.url, testuri.spec); + Assert.equal(visit.visitTime, testtime / 1000); + Assert.equal(visit.referringVisitId, 0); + Assert.equal(visit.transitionType, TRANSITION_FRAMED_LINK); + let uri = NetUtil.newURI(visit.url); + await check_guid_for_uri(uri, visit.pageGuid); + Assert.ok(visit.hidden); + Assert.equal(visit.visitCount, 1); + Assert.equal(visit.typedCount, 0); + }); + let testuri = NetUtil.newURI("http://hidden.firefox.com/"); + let testtime = Date.now() * 1000; + await task_add_visit(testuri, testtime, TRANSITION_FRAMED_LINK); + await promiseNotify; +}); + +add_task(async function test_multiple_onVisit() { + let testuri = NetUtil.newURI("http://self.firefox.com/"); + let promiseNotifications = new Promise(resolve => { + async function listener(aEvents) { + Assert.equal(aEvents.length, 3, "Right number of visits notified"); + for (let i = 0; i < aEvents.length; i++) { + Assert.equal(aEvents[i].type, "page-visited"); + let visit = aEvents[i]; + Assert.equal(testuri.spec, visit.url); + Assert.ok(visit.visitId > 0); + Assert.ok(visit.visitTime > 0); + Assert.ok(!visit.hidden); + let uri = NetUtil.newURI(visit.url); + await check_guid_for_uri(uri, visit.pageGuid); + switch (i) { + case 0: + Assert.equal(visit.referringVisitId, 0); + Assert.equal(visit.transitionType, TRANSITION_LINK); + Assert.equal(visit.visitCount, 1); + Assert.equal(visit.typedCount, 0); + break; + case 1: + Assert.ok(visit.referringVisitId > 0); + Assert.equal(visit.transitionType, TRANSITION_LINK); + Assert.equal(visit.visitCount, 2); + Assert.equal(visit.typedCount, 0); + break; + case 2: + Assert.equal(visit.referringVisitId, 0); + Assert.equal(visit.transitionType, TRANSITION_TYPED); + Assert.equal(visit.visitCount, 3); + Assert.equal(visit.typedCount, 1); + + PlacesObservers.removeListener(["page-visited"], listener); + resolve(); + break; + } + } + } + PlacesObservers.addListener(["page-visited"], listener); + }); + await PlacesTestUtils.addVisits([ + { uri: testuri, transition: TRANSITION_LINK }, + { uri: testuri, referrer: testuri, transition: TRANSITION_LINK }, + { uri: testuri, transition: TRANSITION_TYPED }, + ]); + await promiseNotifications; +}); + +add_task(async function test_pageRemovedFromStore() { + let [testuri] = await task_add_visit(); + let testguid = await PlacesTestUtils.getDatabaseValue("moz_places", "guid", { + url: testuri, + }); + + const promiseNotify = PlacesTestUtils.waitForNotification("page-removed"); + + await PlacesUtils.history.remove(testuri); + + const events = await promiseNotify; + Assert.equal(events.length, 1, "Right number of page-removed notified"); + Assert.equal(events[0].type, "page-removed"); + Assert.ok(events[0].isRemovedFromStore); + Assert.equal(events[0].url, testuri.spec); + Assert.equal(events[0].pageGuid, testguid); + Assert.equal(events[0].reason, PlacesVisitRemoved.REASON_DELETED); +}); + +add_task(async function test_pageRemovedAllVisits() { + const promiseNotify = PlacesTestUtils.waitForNotification("page-removed"); + + let msecs24hrsAgo = Date.now() - 86400 * 1000; + let [testuri] = await task_add_visit(undefined, msecs24hrsAgo * 1000); + // Add a bookmark so the page is not removed. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test", + url: testuri, + }); + let testguid = await PlacesTestUtils.getDatabaseValue("moz_places", "guid", { + url: testuri, + }); + await PlacesUtils.history.remove(testuri); + + const events = await promiseNotify; + Assert.equal(events.length, 1, "Right number of page-removed notified"); + Assert.equal(events[0].type, "page-removed"); + Assert.ok(!events[0].isRemovedFromStore); + Assert.equal(events[0].url, testuri.spec); + // Can't use do_check_guid_for_uri() here because the visit is already gone. + Assert.equal(events[0].pageGuid, testguid); + Assert.equal(events[0].reason, PlacesVisitRemoved.REASON_DELETED); + Assert.ok(!events[0].isPartialVisistsRemoval); // All visits have been removed. +}); + +add_task(async function test_pageTitleChanged() { + const [testuri] = await task_add_visit(); + const title = "test-title"; + + const promiseNotify = + PlacesTestUtils.waitForNotification("page-title-changed"); + + await PlacesTestUtils.addVisits({ + uri: testuri, + title, + }); + + const events = await promiseNotify; + Assert.equal(events.length, 1, "Right number of title changed notified"); + Assert.equal(events[0].type, "page-title-changed"); + Assert.equal(events[0].url, testuri.spec); + Assert.equal(events[0].title, title); + await check_guid_for_uri(testuri, events[0].pageGuid); +}); diff --git a/toolkit/components/places/tests/unit/test_history_sidebar.js b/toolkit/components/places/tests/unit/test_history_sidebar.js new file mode 100644 index 0000000000..868ef79f70 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_sidebar.js @@ -0,0 +1,418 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let nowObj = new Date(); + +/** + * Normalizes a Date to midnight. + * + * @param {Date} inputDate + * @return normalized Date + */ +function toMidnight(inputDate) { + let date = new Date(inputDate); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + return date; +} + +/** + * Adds a test URI visit to the database. + * + * @param aURI + * The URI to add a visit for. + * @param aTime + * Reference "now" time. + * @param aDayOffset + * number of days to add, pass a negative value to subtract them. + */ +async function addNormalizedVisit(aURI, aTime, aDayOffset) { + let dateObj = toMidnight(aTime); + // Days where DST changes should be taken into account. + let previousDateObj = new Date(dateObj.getTime() + aDayOffset * 86400000); + let DSTCorrection = + (dateObj.getTimezoneOffset() - previousDateObj.getTimezoneOffset()) * + 60 * + 1000; + // Substract aDayOffset + let PRTimeWithOffset = (previousDateObj.getTime() - DSTCorrection) * 1000; + info( + "Adding visit to " + + aURI.spec + + " at " + + PlacesUtils.toDate(PRTimeWithOffset) + ); + await PlacesTestUtils.addVisits({ + uri: aURI, + visitDate: PRTimeWithOffset, + }); +} + +function openRootForResultType(resultType) { + let options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = resultType; + let query = PlacesUtils.history.getNewQuery(); + let result = PlacesUtils.history.executeQuery(query, options); + let root = result.root; + root.containerOpen = true; + return result; +} + +function daysForMonthsAgo(months) { + let oldTime = toMidnight(new Date()); + // Set day before month, otherwise we could try to calculate 30 February, or + // other nonexistent days. + oldTime.setDate(1); + oldTime.setMonth(nowObj.getMonth() - months); + // Stay larger for eventual timezone issues, add 2 days. + return parseInt((nowObj - oldTime) / (1000 * 60 * 60 * 24)) + 2; +} + +// This test relies on en-US locale +// Offset is number of days +let containers = [ + { label: "Today", offset: 0, visible: true }, + { label: "Yesterday", offset: -1, visible: true }, + { label: "Last 7 days", offset: -2, visible: true }, + { label: "This month", offset: -8, visible: nowObj.getDate() > 8 }, + { label: "", offset: -daysForMonthsAgo(0), visible: true }, + { label: "", offset: -daysForMonthsAgo(1), visible: true }, + { label: "", offset: -daysForMonthsAgo(2), visible: true }, + { label: "", offset: -daysForMonthsAgo(3), visible: true }, + { label: "", offset: -daysForMonthsAgo(4), visible: true }, + { label: "Older than 6 months", offset: -daysForMonthsAgo(5), visible: true }, +]; + +let visibleContainers = containers.filter(container => container.visible); + +/** + * Asynchronous task that fills history and checks containers' labels. + */ +add_task(async function task_fill_history() { + info("*** TEST Fill History"); + // We can't use "now" because our hardcoded offsets would be invalid for some + // date. So we hardcode a date. + for (let i = 0; i < containers.length; i++) { + let container = containers[i]; + let testURI = uri("http://mirror" + i + ".mozilla.com/b"); + await addNormalizedVisit(testURI, nowObj, container.offset); + testURI = uri("http://mirror" + i + ".mozilla.com/a"); + await addNormalizedVisit(testURI, nowObj, container.offset); + testURI = uri("http://mirror" + i + ".google.com/b"); + await addNormalizedVisit(testURI, nowObj, container.offset); + testURI = uri("http://mirror" + i + ".google.com/a"); + await addNormalizedVisit(testURI, nowObj, container.offset); + // Bug 485703 - Hide date containers not containing additional entries + // compared to previous ones. + // Check after every new container is added. + check_visit(container.offset); + } + + let root = openRootForResultType( + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY + ).root; + let cc = root.childCount; + info("Found containers:"); + let previousLabels = []; + for (let i = 0; i < cc; i++) { + let container = visibleContainers[i]; + let node = root.getChild(i); + info(node.title); + if (container.label) { + Assert.equal(node.title, container.label); + } + // Check labels are not repeated. + Assert.ok(!previousLabels.includes(node.title)); + previousLabels.push(node.title); + } + Assert.equal(cc, visibleContainers.length); + root.containerOpen = false; +}); + +/** + * Bug 485703 - Hide date containers not containing additional entries compared + * to previous ones. + */ +function check_visit(aOffset) { + let root = openRootForResultType( + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY + ).root; + + let unexpected = []; + switch (aOffset) { + case 0: + unexpected = ["Yesterday", "Last 7 days", "This month"]; + break; + case -1: + unexpected = ["Last 7 days", "This month"]; + break; + case -2: + unexpected = ["This month"]; + break; + default: + // Other containers are tested later. + } + + info("Found containers:"); + let cc = root.childCount; + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + info(node.title); + Assert.ok(!unexpected.includes(node.title)); + } + root.containerOpen = false; +} + +/** + * Queries history grouped by date and site, checking containers' labels and + * children. + */ +add_task(async function test_RESULTS_AS_DATE_SITE_QUERY() { + info("*** TEST RESULTS_AS_DATE_SITE_QUERY"); + let result = openRootForResultType( + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY + ); + let root = result.root; + + // Check one of the days + let dayNode = PlacesUtils.asContainer(root.getChild(0)); + dayNode.containerOpen = true; + Assert.equal(dayNode.childCount, 2); + + // Items should be sorted by host + let site1 = PlacesUtils.asContainer(dayNode.getChild(0)); + Assert.equal(site1.title, "mirror0.google.com"); + + let site2 = PlacesUtils.asContainer(dayNode.getChild(1)); + Assert.equal(site2.title, "mirror0.mozilla.com"); + + site1.containerOpen = true; + Assert.equal(site1.childCount, 2); + + // Inside of host sites are sorted by title + let site1visit = site1.getChild(0); + Assert.equal(site1visit.uri, "http://mirror0.google.com/a"); + + // Bug 473157: changing sorting mode should not affect the containers + result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING; + + // Check one of the days + dayNode = PlacesUtils.asContainer(root.getChild(0)); + dayNode.containerOpen = true; + Assert.equal(dayNode.childCount, 2); + + // Hosts are still sorted by title + site1 = PlacesUtils.asContainer(dayNode.getChild(0)); + Assert.equal(site1.title, "mirror0.google.com"); + + site2 = PlacesUtils.asContainer(dayNode.getChild(1)); + Assert.equal(site2.title, "mirror0.mozilla.com"); + + site1.containerOpen = true; + Assert.equal(site1.childCount, 2); + + // But URLs are now sorted by title descending + site1visit = site1.getChild(0); + Assert.equal(site1visit.uri, "http://mirror0.google.com/b"); + + site1.containerOpen = false; + dayNode.containerOpen = false; + root.containerOpen = false; +}); + +/** + * Queries history grouped by date, checking containers' labels and children. + */ +add_task(async function test_RESULTS_AS_DATE_QUERY() { + info("*** TEST RESULTS_AS_DATE_QUERY"); + let result = openRootForResultType( + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY + ); + let root = result.root; + let cc = root.childCount; + Assert.equal(cc, visibleContainers.length); + info("Found containers:"); + for (let i = 0; i < cc; i++) { + let container = visibleContainers[i]; + let node = root.getChild(i); + info(node.title); + if (container.label) { + Assert.equal(node.title, container.label); + } + } + + // Check one of the days + let dayNode = PlacesUtils.asContainer(root.getChild(0)); + dayNode.containerOpen = true; + Assert.equal(dayNode.childCount, 4); + + // Items should be sorted by title + let visit1 = dayNode.getChild(0); + Assert.equal(visit1.uri, "http://mirror0.google.com/a"); + + let visit2 = dayNode.getChild(3); + Assert.equal(visit2.uri, "http://mirror0.mozilla.com/b"); + + // Bug 473157: changing sorting mode should not affect the containers + result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING; + + // Check one of the days + dayNode = PlacesUtils.asContainer(root.getChild(0)); + dayNode.containerOpen = true; + Assert.equal(dayNode.childCount, 4); + + // But URLs are now sorted by title descending + visit1 = dayNode.getChild(0); + Assert.equal(visit1.uri, "http://mirror0.mozilla.com/b"); + + visit2 = dayNode.getChild(3); + Assert.equal(visit2.uri, "http://mirror0.google.com/a"); + + dayNode.containerOpen = false; + root.containerOpen = false; +}); + +/** + * Queries history grouped by site, checking containers' labels and children. + */ +add_task(async function test_RESULTS_AS_SITE_QUERY() { + info("*** TEST RESULTS_AS_SITE_QUERY"); + // add a bookmark with a domain not in the set of visits in the db + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://foobar", + title: "", + }); + + let options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_SITE_QUERY; + options.sortingMode = options.SORT_BY_TITLE_ASCENDING; + let query = PlacesUtils.history.getNewQuery(); + let result = PlacesUtils.history.executeQuery(query, options); + let root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, containers.length * 2); + + // Expected results: + // "mirror0.google.com", + // "mirror0.mozilla.com", + // "mirror1.google.com", + // "mirror1.mozilla.com", + // "mirror2.google.com", + // "mirror2.mozilla.com", + // "mirror3.google.com", <== We check for this site (index 6) + // "mirror3.mozilla.com", + // "mirror4.google.com", + // "mirror4.mozilla.com", + // "mirror5.google.com", + // "mirror5.mozilla.com", + // ... + + // Items should be sorted by host + let siteNode = PlacesUtils.asContainer(root.getChild(6)); + Assert.equal(siteNode.title, "mirror3.google.com"); + + siteNode.containerOpen = true; + Assert.equal(siteNode.childCount, 2); + + // Inside of host sites are sorted by title + let visitNode = siteNode.getChild(0); + Assert.equal(visitNode.uri, "http://mirror3.google.com/a"); + + // Bug 473157: changing sorting mode should not affect the containers + result.sortingMode = options.SORT_BY_TITLE_DESCENDING; + siteNode = PlacesUtils.asContainer(root.getChild(6)); + Assert.equal(siteNode.title, "mirror3.google.com"); + + siteNode.containerOpen = true; + Assert.equal(siteNode.childCount, 2); + + // But URLs are now sorted by title descending + let visit = siteNode.getChild(0); + Assert.equal(visit.uri, "http://mirror3.google.com/b"); + + siteNode.containerOpen = false; + root.containerOpen = false; + + // Cleanup. + await PlacesUtils.bookmarks.remove(bookmark.guid); +}); + +/** + * Checks that queries grouped by date do liveupdate correctly. + */ +async function test_date_liveupdate(aResultType) { + let midnight = toMidnight(nowObj); + + // TEST 1. Test that the query correctly updates when it is root. + let root = openRootForResultType(aResultType).root; + Assert.equal(root.childCount, visibleContainers.length); + + // Remove "Today". + await PlacesUtils.history.removeByFilter({ + beginDate: new Date(midnight.getTime()), + endDate: new Date(Date.now()), + }); + Assert.equal(root.childCount, visibleContainers.length - 1); + + // Open "Last 7 days" container, this way we will have a container accepting + // the new visit, but we should still add back "Today" container. + let last7Days = PlacesUtils.asContainer(root.getChild(1)); + last7Days.containerOpen = true; + + // Add a visit for "Today". This should add back the missing "Today" + // container. + await addNormalizedVisit(uri("http://www.mozilla.org/"), nowObj, 0); + Assert.equal(root.childCount, visibleContainers.length); + + last7Days.containerOpen = false; + root.containerOpen = false; + + // TEST 2. Test that the query correctly updates even if it is not root. + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "place:type=" + aResultType, + title: "", + }); + + // Query toolbar and open our query container, then check again liveupdate. + root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.toolbarGuid).root; + Assert.equal(root.childCount, 1); + let dateContainer = PlacesUtils.asContainer(root.getChild(0)); + dateContainer.containerOpen = true; + + Assert.equal(dateContainer.childCount, visibleContainers.length); + // Remove "Today". + await PlacesUtils.history.removeByFilter({ + beginDate: new Date(midnight.getTime()), + endDate: new Date(Date.now()), + }); + Assert.equal(dateContainer.childCount, visibleContainers.length - 1); + // Add a visit for "Today". + await addNormalizedVisit(uri("http://www.mozilla.org/"), nowObj, 0); + Assert.equal(dateContainer.childCount, visibleContainers.length); + + dateContainer.containerOpen = false; + root.containerOpen = false; + + // Cleanup. + await PlacesUtils.bookmarks.remove(bookmark.guid); +} + +add_task(async function test_history_sidebar() { + await test_date_liveupdate( + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY + ); + await test_date_liveupdate( + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY + ); + + // The remaining views are + // RESULTS_AS_URI + SORT_BY_VISITCOUNT_DESCENDING + // -> test_399266.js + // RESULTS_AS_URI + SORT_BY_DATE_DESCENDING + // -> test_385397.js +}); diff --git a/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js new file mode 100644 index 0000000000..167b8786e4 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js @@ -0,0 +1,326 @@ +async function importFromFixture(fixture, replace) { + let cwd = do_get_cwd().path; + let path = PathUtils.join(cwd, fixture); + + info(`Importing from ${path}`); + await BookmarkJSONUtils.importFromFile(path, { replace }); + await PlacesTestUtils.promiseAsyncUpdates(); +} + +async function treeEquals(guid, expected, message) { + let root = await PlacesUtils.promiseBookmarksTree(guid); + let bookmarks = (function nodeToEntry(node) { + let entry = { guid: node.guid, index: node.index }; + if (node.children) { + entry.children = node.children.map(nodeToEntry); + } + return entry; + })(root); + + info(`Checking if ${guid} tree matches ${JSON.stringify(expected)}`); + info(`Got bookmarks tree for ${guid}: ${JSON.stringify(bookmarks)}`); + + deepEqual(bookmarks, expected, message); +} + +add_task(async function test_restore_mobile_bookmarks_root() { + await importFromFixture( + "mobile_bookmarks_root_import.json", + /* replace */ true + ); + + await treeEquals( + PlacesUtils.bookmarks.rootGuid, + { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [ + { + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [{ guid: "X6lUyOspVYwi", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + }, + { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + }, + { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + children: [ + { guid: "_o8e1_zxTJFg", index: 0 }, + { guid: "QCtSqkVYUbXB", index: 1 }, + ], + }, + ], + }, + "Should restore mobile bookmarks from root" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_mobile_bookmarks_root() { + await importFromFixture( + "mobile_bookmarks_root_import.json", + /* replace */ false + ); + await importFromFixture( + "mobile_bookmarks_root_merge.json", + /* replace */ false + ); + + await treeEquals( + PlacesUtils.bookmarks.rootGuid, + { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [ + { + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "X6lUyOspVYwi", index: 0 }, + { guid: "Utodo9b0oVws", index: 1 }, + ], + }, + { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + }, + { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + }, + { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + children: [ + // The first two are in ..._import.json, the second two are in + // ..._merge.json + { guid: "_o8e1_zxTJFg", index: 0 }, + { guid: "QCtSqkVYUbXB", index: 1 }, + { guid: "a17yW6-nTxEJ", index: 2 }, + { guid: "xV10h9Wi3FBM", index: 3 }, + ], + }, + ], + }, + "Should merge bookmarks root contents" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_restore_mobile_bookmarks_folder() { + // This tests importing a mobile bookmarks folder with the annotation, + // and the old, random guid. + await importFromFixture( + "mobile_bookmarks_folder_import.json", + /* replace */ true + ); + + await treeEquals( + PlacesUtils.bookmarks.rootGuid, + { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [ + { + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "X6lUyOspVYwi", index: 0 }, + { guid: "XF4yRP6bTuil", index: 1 }, + ], + }, + { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "buy7711R3ZgE", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "KIa9iKZab2Z5", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + children: [ + { guid: "_o8e1_zxTJFg", index: 0 }, + { guid: "QCtSqkVYUbXB", index: 1 }, + ], + }, + ], + }, + "Should restore mobile bookmark folder contents into mobile root" + ); + + let queryById = await PlacesUtils.bookmarks.fetch("XF4yRP6bTuil"); + equal( + queryById.url.href, + `place:parent=${PlacesUtils.bookmarks.mobileGuid}`, + "Should rewrite mobile query to point to root GUID" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_mobile_bookmarks_folder() { + await importFromFixture( + "mobile_bookmarks_folder_import.json", + /* replace */ false + ); + await importFromFixture( + "mobile_bookmarks_folder_merge.json", + /* replace */ false + ); + + await treeEquals( + PlacesUtils.bookmarks.rootGuid, + { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [ + { + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "X6lUyOspVYwi", index: 0 }, + { guid: "XF4yRP6bTuil", index: 1 }, + { guid: "Utodo9b0oVws", index: 2 }, + ], + }, + { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "buy7711R3ZgE", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "KIa9iKZab2Z5", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + children: [ + { guid: "_o8e1_zxTJFg", index: 0 }, + { guid: "QCtSqkVYUbXB", index: 1 }, + { guid: "a17yW6-nTxEJ", index: 2 }, + { guid: "xV10h9Wi3FBM", index: 3 }, + ], + }, + ], + }, + "Should merge bookmarks folder contents into mobile root" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_restore_multiple_bookmarks_folders() { + await importFromFixture( + "mobile_bookmarks_multiple_folders.json", + /* replace */ true + ); + + await treeEquals( + PlacesUtils.bookmarks.rootGuid, + { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [ + { + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "buy7711R3ZgE", index: 0 }, + { guid: "F_LBgd1fS_uQ", index: 1 }, + { guid: "oIpmQXMWsXvY", index: 2 }, + ], + }, + { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "Utodo9b0oVws", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "xV10h9Wi3FBM", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + children: [ + { guid: "a17yW6-nTxEJ", index: 0 }, + { guid: "sSZ86WT9WbN3", index: 1 }, + ], + }, + ], + }, + "Should restore multiple bookmarks folder contents into root" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_import_multiple_bookmarks_folders() { + await importFromFixture( + "mobile_bookmarks_root_import.json", + /* replace */ false + ); + await importFromFixture( + "mobile_bookmarks_multiple_folders.json", + /* replace */ false + ); + + await treeEquals( + PlacesUtils.bookmarks.rootGuid, + { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [ + { + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "X6lUyOspVYwi", index: 0 }, + { guid: "buy7711R3ZgE", index: 1 }, + { guid: "F_LBgd1fS_uQ", index: 2 }, + { guid: "oIpmQXMWsXvY", index: 3 }, + ], + }, + { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "Utodo9b0oVws", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "xV10h9Wi3FBM", index: 0 }], + }, + { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + children: [ + { guid: "_o8e1_zxTJFg", index: 0 }, + { guid: "QCtSqkVYUbXB", index: 1 }, + { guid: "a17yW6-nTxEJ", index: 2 }, + { guid: "sSZ86WT9WbN3", index: 3 }, + ], + }, + ], + }, + "Should merge multiple mobile folders into root" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_isPageInDB.js b/toolkit/components/places/tests/unit/test_isPageInDB.js new file mode 100644 index 0000000000..2eda125994 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_isPageInDB.js @@ -0,0 +1,10 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +add_task(async function test_execute() { + var good_uri = uri("http://mozilla.com"); + var bad_uri = uri("http://google.com"); + await PlacesTestUtils.addVisits({ uri: good_uri }); + Assert.ok(await PlacesTestUtils.isPageInDB(good_uri)); + Assert.equal(false, await PlacesTestUtils.isPageInDB(bad_uri)); +}); diff --git a/toolkit/components/places/tests/unit/test_isURIVisited.js b/toolkit/components/places/tests/unit/test_isURIVisited.js new file mode 100644 index 0000000000..cf3a8c139f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_isURIVisited.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests functionality of the isURIVisited API. + +const SCHEMES = { + "http://": true, + "https://": true, + "ftp://": true, + "file:///": true, + "about:": false, + // nsIIOService.newURI() can throw if e.g. the app knows about imap:// + // but the account is not set up and so the URL is invalid for it. + // "imap://": false, + "news://": false, + "mailbox:": false, + "moz-anno:favicon:http://": false, + "view-source:http://": false, + "chrome://browser/content/browser.xhtml?": false, + "resource://": false, + "data:,": false, + "javascript:": false, +}; + +add_task(async function test_isURIVisited() { + let history = Cc["@mozilla.org/browser/history;1"].getService( + Ci.mozIAsyncHistory + ); + + function visitsPromise(uri) { + return new Promise(resolve => { + history.isURIVisited(uri, (receivedURI, visited) => { + resolve([receivedURI, visited]); + }); + }); + } + + for (let scheme in SCHEMES) { + info("Testing scheme " + scheme); + for (let t in PlacesUtils.history.TRANSITIONS) { + if (t == "EMBED") { + continue; + } + info("With transition " + t); + let aTransition = PlacesUtils.history.TRANSITIONS[t]; + + let aURI = Services.io.newURI(scheme + "mozilla.org/"); + + let [receivedURI1, visited1] = await visitsPromise(aURI); + Assert.ok(aURI.equals(receivedURI1)); + Assert.ok(!visited1); + + if (PlacesUtils.history.canAddURI(aURI)) { + await PlacesTestUtils.addVisits([ + { + uri: aURI, + transition: aTransition, + }, + ]); + info("Added visit for " + aURI.spec); + } + + let [receivedURI2, visited2] = await visitsPromise(aURI); + Assert.ok(aURI.equals(receivedURI2)); + Assert.equal(SCHEMES[scheme], visited2); + + await PlacesUtils.history.clear(); + let [receivedURI3, visited3] = await visitsPromise(aURI); + Assert.ok(aURI.equals(receivedURI3)); + Assert.ok(!visited3); + } + } +}); diff --git a/toolkit/components/places/tests/unit/test_isvisited.js b/toolkit/components/places/tests/unit/test_isvisited.js new file mode 100644 index 0000000000..42bd913f55 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_isvisited.js @@ -0,0 +1,69 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function test_execute() { + var referrer = uri("about:blank"); + + // add a http:// uri + var uri1 = uri("http://mozilla.com"); + await PlacesTestUtils.addVisits({ uri: uri1, referrer }); + await check_guid_for_uri(uri1); + Assert.ok(await PlacesUtils.history.hasVisits(uri1)); + + // add a https:// uri + var uri2 = uri("https://etrade.com"); + await PlacesTestUtils.addVisits({ uri: uri2, referrer }); + await check_guid_for_uri(uri2); + Assert.ok(await PlacesUtils.history.hasVisits(uri2)); + + // add a ftp:// uri + var uri3 = uri("ftp://ftp.mozilla.org"); + await PlacesTestUtils.addVisits({ uri: uri3, referrer }); + await check_guid_for_uri(uri3); + Assert.ok(await PlacesUtils.history.hasVisits(uri3)); + + // check if a nonexistent uri is visited + var uri4 = uri("http://foobarcheese.com"); + Assert.equal(false, await PlacesUtils.history.hasVisits(uri4)); + + // check that certain schemes never show up as visited + // even if we attempt to add them to history + // see CanAddURI() in nsNavHistory.cpp + const URLS = [ + "about:config", + "imap://cyrus.andrew.cmu.edu/archive.imap", + "news://new.mozilla.org/mozilla.dev.apps.firefox", + "mailbox:Inbox", + "moz-anno:favicon:http://mozilla.org/made-up-favicon", + "view-source:http://mozilla.org", + "chrome://browser/content/browser.xhtml", + "resource://gre-resources/hiddenWindow.html", + "data:,Hello%2C%20World!", + "javascript:alert('hello wolrd!');", + "http://localhost/" + "a".repeat(1984), + ]; + for (let currentURL of URLS) { + try { + var cantAddUri = uri(currentURL); + } catch (e) { + // nsIIOService.newURI() can throw if e.g. our app knows about imap:// + // but the account is not set up and so the URL is invalid for us. + // Note this in the log but ignore as it's not the subject of this test. + info("Could not construct URI for '" + currentURL + "'; ignoring"); + } + if (cantAddUri) { + PlacesTestUtils.addVisits({ uri: cantAddUri, referrer }).then( + () => { + do_throw("Should not have added history for invalid URI."); + }, + error => { + Assert.ok(error.message.includes("No items were added to history")); + } + ); + Assert.equal(false, await PlacesUtils.history.hasVisits(cantAddUri)); + } + } +}); diff --git a/toolkit/components/places/tests/unit/test_keywords.js b/toolkit/components/places/tests/unit/test_keywords.js new file mode 100644 index 0000000000..57e81515aa --- /dev/null +++ b/toolkit/components/places/tests/unit/test_keywords.js @@ -0,0 +1,733 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +async function check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) { + // Check case-insensitivity. + aKeyword = aKeyword.toUpperCase(); + + let entry = await PlacesUtils.keywords.fetch(aKeyword); + + Assert.deepEqual( + entry, + await PlacesUtils.keywords.fetch({ keyword: aKeyword }) + ); + + if (aExpectExists) { + Assert.ok(!!entry, "A keyword should exist"); + Assert.equal(entry.url.href, aHref); + Assert.equal(entry.postData, aPostData); + Assert.deepEqual( + entry, + await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }) + ); + let entries = []; + await PlacesUtils.keywords.fetch({ url: aHref }, e => entries.push(e)); + Assert.ok( + entries.some( + e => e.url.href == aHref && e.keyword == aKeyword.toLowerCase() + ) + ); + } else { + Assert.ok( + !entry || entry.url.href != aHref, + "The given keyword entry should not exist" + ); + if (aHref) { + Assert.equal( + null, + await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }) + ); + } else { + Assert.equal( + null, + await PlacesUtils.keywords.fetch({ keyword: aKeyword }) + ); + } + } +} + +/** + * Polls the keywords cache waiting for the given keyword entry. + */ +async function promiseKeyword(keyword, expectedHref) { + let href = null; + do { + await new Promise(resolve => do_timeout(100, resolve)); + let entry = await PlacesUtils.keywords.fetch(keyword); + if (entry) { + href = entry.url.href; + } + } while (href != expectedHref); +} + +async function check_no_orphans() { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.executeCached( + `SELECT id FROM moz_keywords k + WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id) + ` + ); + Assert.equal(rows.length, 0); +} + +function expectBookmarkNotifications() { + const observer = { + notifications: [], + _start() { + this._handle = this._handle.bind(this); + PlacesUtils.observers.addListener( + ["bookmark-keyword-changed"], + this._handle + ); + }, + _handle(events) { + for (const event of events) { + this.notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + url: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + keyword: event.keyword, + lastModified: new Date(event.lastModified), + source: event.source, + isTagging: event.isTagging, + }); + } + }, + check(expected) { + PlacesUtils.observers.removeListener( + ["bookmark-keyword-changed"], + this._handle + ); + Assert.deepEqual(this.notifications, expected); + }, + }; + observer._start(); + return observer; +} + +add_task(async function test_invalid_input() { + Assert.throws(() => PlacesUtils.keywords.fetch(null), /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch(5), /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch(undefined), /Invalid keyword/); + Assert.throws( + () => PlacesUtils.keywords.fetch({ keyword: null }), + /Invalid keyword/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({ keyword: {} }), + /Invalid keyword/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({ keyword: 5 }), + /Invalid keyword/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({}), + /At least keyword or url must be provided/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({ keyword: "test" }, "test"), + /onResult callback must be a valid function/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({ url: "test" }), + /is not a valid URL/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({ url: {} }), + /is not a valid URL/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({ url: null }), + /is not a valid URL/ + ); + Assert.throws( + () => PlacesUtils.keywords.fetch({ url: "" }), + /is not a valid URL/ + ); + + Assert.throws( + () => PlacesUtils.keywords.insert(null), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert("test"), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert(undefined), + /Input should be a valid object/ + ); + Assert.throws(() => PlacesUtils.keywords.insert({}), /Invalid keyword/); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: null }), + /Invalid keyword/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: 5 }), + /Invalid keyword/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "" }), + /Invalid keyword/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "test", postData: 5 }), + /Invalid POST data/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "test", postData: {} }), + /Invalid POST data/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "test" }), + /is not a valid URL/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "test", url: 5 }), + /is not a valid URL/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "test", url: "" }), + /is not a valid URL/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "test", url: null }), + /is not a valid URL/ + ); + Assert.throws( + () => PlacesUtils.keywords.insert({ keyword: "test", url: "mozilla" }), + /is not a valid URL/ + ); + + Assert.throws(() => PlacesUtils.keywords.remove(null), /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.remove(""), /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.remove(5), /Invalid keyword/); +}); + +add_task(async function test_addKeyword() { + await check_keyword(false, "http://example.com/", "keyword"); + let fc = await foreign_count("http://example.com/"); + let observer = expectBookmarkNotifications(); + + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + }); + observer.check([]); + + await check_keyword(true, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.remove("keyword"); + observer.check([]); + + await check_keyword(false, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc); // -1 keyword + + // Check using URL. + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: new URL("http://example.com/"), + }); + await check_keyword(true, "http://example.com/", "keyword"); + await PlacesUtils.keywords.remove("keyword"); + await check_keyword(false, "http://example.com/", "keyword"); + + await check_no_orphans(); +}); + +add_task(async function test_addBookmarkAndKeyword() { + let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); + Preferences.set("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(function () { + Preferences.set("privacy.reduceTimerPrecision", timerPrecision); + }); + + await check_keyword(false, "http://example.com/", "keyword"); + let fc = await foreign_count("http://example.com/"); + let bookmark = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + let observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + }); + + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark.guid), + itemType: bookmark.type, + url: bookmark.url, + guid: bookmark.guid, + parentGuid: bookmark.parentGuid, + keyword: "keyword", + lastModified: new Date(bookmark.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + await check_keyword(true, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.remove("keyword"); + + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark.guid), + itemType: bookmark.type, + url: bookmark.url, + guid: bookmark.guid, + parentGuid: bookmark.parentGuid, + keyword: "", + lastModified: new Date(bookmark.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + await check_keyword(false, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 1); // -1 keyword + + // Add again the keyword, then remove the bookmark. + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + }); + + observer = expectBookmarkNotifications(); + await PlacesUtils.bookmarks.remove(bookmark.guid); + // the notification is synchronous but the removal process is async. + // Unfortunately there's nothing explicit we can wait for. + // eslint-disable-next-line no-empty + while (await foreign_count("http://example.com/")) {} + // We don't get any itemChanged notification since the bookmark has been + // removed already. + observer.check([]); + + await check_keyword(false, "http://example.com/", "keyword"); + + await check_no_orphans(); +}); + +add_task(async function test_addKeywordToURIHavingKeyword() { + await check_keyword(false, "http://example.com/", "keyword"); + let fc = await foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + }); + observer.check([]); + + await check_keyword(true, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword + + await PlacesUtils.keywords.insert({ + keyword: "keyword2", + url: "http://example.com/", + postData: "test=1", + }); + + await check_keyword(true, "http://example.com/", "keyword"); + await check_keyword(true, "http://example.com/", "keyword2", "test=1"); + Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 keyword + let entries = []; + let entry = await PlacesUtils.keywords.fetch( + { url: "http://example.com/" }, + e => entries.push(e) + ); + Assert.equal(entries.length, 2); + Assert.deepEqual(entries[0], entry); + + // Now remove the keywords. + observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.remove("keyword"); + await PlacesUtils.keywords.remove("keyword2"); + observer.check([]); + + await check_keyword(false, "http://example.com/", "keyword"); + await check_keyword(false, "http://example.com/", "keyword2"); + Assert.equal(await foreign_count("http://example.com/"), fc); // -1 keyword + + await check_no_orphans(); +}); + +add_task(async function test_addBookmarkToURIHavingKeyword() { + await check_keyword(false, "http://example.com/", "keyword"); + let fc = await foreign_count("http://example.com/"); + let observer = expectBookmarkNotifications(); + + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + }); + observer.check([]); + + await check_keyword(true, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword + + observer = expectBookmarkNotifications(); + let bookmark = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark + observer.check([]); + + observer = expectBookmarkNotifications(); + await PlacesUtils.bookmarks.remove(bookmark.guid); + // the notification is synchronous but the removal process is async. + // Unfortunately there's nothing explicit we can wait for. + // eslint-disable-next-line no-empty + while (await foreign_count("http://example.com/")) {} + // We don't get any itemChanged notification since the bookmark has been + // removed already. + observer.check([]); + + await check_keyword(false, "http://example.com/", "keyword"); + + await check_no_orphans(); +}); + +add_task(async function test_sameKeywordDifferentURL() { + let fc1 = await foreign_count("http://example1.com/"); + let bookmark1 = await PlacesUtils.bookmarks.insert({ + url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let fc2 = await foreign_count("http://example2.com/"); + let bookmark2 = await PlacesUtils.bookmarks.insert({ + url: "http://example2.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example1.com/", + }); + + await check_keyword(true, "http://example1.com/", "keyword"); + Assert.equal(await foreign_count("http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword + await check_keyword(false, "http://example2.com/", "keyword"); + Assert.equal(await foreign_count("http://example2.com/"), fc2 + 1); // +1 bookmark + + // Assign the same keyword to another url. + let observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example2.com/", + }); + + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark1.guid), + itemType: bookmark1.type, + url: bookmark1.url, + guid: bookmark1.guid, + parentGuid: bookmark1.parentGuid, + keyword: "", + lastModified: new Date(bookmark1.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark2.guid), + itemType: bookmark2.type, + url: bookmark2.url, + guid: bookmark2.guid, + parentGuid: bookmark2.parentGuid, + keyword: "keyword", + lastModified: new Date(bookmark2.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + await check_keyword(false, "http://example1.com/", "keyword"); + Assert.equal(await foreign_count("http://example1.com/"), fc1 + 1); // -1 keyword + await check_keyword(true, "http://example2.com/", "keyword"); + Assert.equal(await foreign_count("http://example2.com/"), fc2 + 2); // +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.remove("keyword"); + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark2.guid), + itemType: bookmark2.type, + url: bookmark2.url, + guid: bookmark2.guid, + parentGuid: bookmark2.parentGuid, + keyword: "", + lastModified: new Date(bookmark2.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + await check_keyword(false, "http://example1.com/", "keyword"); + await check_keyword(false, "http://example2.com/", "keyword"); + Assert.equal(await foreign_count("http://example1.com/"), fc1 + 1); + Assert.equal(await foreign_count("http://example2.com/"), fc2 + 1); // -1 keyword + + await PlacesUtils.bookmarks.remove(bookmark1); + await PlacesUtils.bookmarks.remove(bookmark2); + Assert.equal(await foreign_count("http://example1.com/"), fc1); // -1 bookmark + // eslint-disable-next-line no-empty + while (await foreign_count("http://example2.com/")) {} // -1 keyword + + await check_no_orphans(); +}); + +add_task(async function test_sameURIDifferentKeyword() { + let fc = await foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + let bookmark = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + }); + + await check_keyword(true, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 keyword + + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark.guid), + itemType: bookmark.type, + url: bookmark.url, + guid: bookmark.guid, + parentGuid: bookmark.parentGuid, + keyword: "keyword", + lastModified: new Date(bookmark.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.insert({ + keyword: "keyword2", + url: "http://example.com/", + }); + await check_keyword(false, "http://example.com/", "keyword"); + await check_keyword(true, "http://example.com/", "keyword2"); + Assert.equal(await foreign_count("http://example.com/"), fc + 2); // -1 keyword +1 keyword + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark.guid), + itemType: bookmark.type, + url: bookmark.url, + guid: bookmark.guid, + parentGuid: bookmark.parentGuid, + keyword: "keyword2", + lastModified: new Date(bookmark.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + // Now remove the bookmark. + await PlacesUtils.bookmarks.remove(bookmark); + // eslint-disable-next-line no-empty + while (await foreign_count("http://example.com/")) {} + await check_keyword(false, "http://example.com/", "keyword"); + await check_keyword(false, "http://example.com/", "keyword2"); + + await check_no_orphans(); +}); + +add_task(async function test_deleteKeywordMultipleBookmarks() { + let fc = await foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + let bookmark1 = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let bookmark2 = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + }); + + await check_keyword(true, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 3); // +2 bookmark +1 keyword + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark2.guid), + itemType: bookmark2.type, + url: bookmark2.url, + guid: bookmark2.guid, + parentGuid: bookmark2.parentGuid, + keyword: "keyword", + lastModified: new Date(bookmark2.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark1.guid), + itemType: bookmark1.type, + url: bookmark1.url, + guid: bookmark1.guid, + parentGuid: bookmark1.parentGuid, + keyword: "keyword", + lastModified: new Date(bookmark1.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + observer = expectBookmarkNotifications(); + await PlacesUtils.keywords.remove("keyword"); + await check_keyword(false, "http://example.com/", "keyword"); + Assert.equal(await foreign_count("http://example.com/"), fc + 2); // -1 keyword + observer.check([ + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark2.guid), + itemType: bookmark2.type, + url: bookmark2.url, + guid: bookmark2.guid, + parentGuid: bookmark2.parentGuid, + keyword: "", + lastModified: new Date(bookmark2.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + { + type: "bookmark-keyword-changed", + id: await PlacesUtils.promiseItemId(bookmark1.guid), + itemType: bookmark1.type, + url: bookmark1.url, + guid: bookmark1.guid, + parentGuid: bookmark1.parentGuid, + keyword: "", + lastModified: new Date(bookmark1.lastModified), + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + + // Now remove the bookmarks. + await PlacesUtils.bookmarks.remove(bookmark1); + await PlacesUtils.bookmarks.remove(bookmark2); + Assert.equal(await foreign_count("http://example.com/"), fc); // -2 bookmarks + + await check_no_orphans(); +}); + +add_task(async function test_multipleKeywordsSamePostData() { + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/", + postData: "postData1", + }); + await check_keyword(true, "http://example.com/", "keyword", "postData1"); + // Add another keyword with same postData, should fail. + await PlacesUtils.keywords.insert({ + keyword: "keyword2", + url: "http://example.com/", + postData: "postData1", + }); + await check_keyword(false, "http://example.com/", "keyword", "postData1"); + await check_keyword(true, "http://example.com/", "keyword2", "postData1"); + + await PlacesUtils.keywords.remove("keyword2"); + + await check_no_orphans(); +}); + +add_task(async function test_bookmarkURLChange() { + let fc1 = await foreign_count("http://example1.com/"); + let fc2 = await foreign_count("http://example2.com/"); + let bookmark = await PlacesUtils.bookmarks.insert({ + url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example1.com/", + }); + + await check_keyword(true, "http://example1.com/", "keyword"); + Assert.equal(await foreign_count("http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword + + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + url: "http://example2.com/", + }); + await promiseKeyword("keyword", "http://example2.com/"); + + await check_keyword(false, "http://example1.com/", "keyword"); + await check_keyword(true, "http://example2.com/", "keyword"); + Assert.equal(await foreign_count("http://example1.com/"), fc1); // -1 bookmark -1 keyword + Assert.equal(await foreign_count("http://example2.com/"), fc2 + 2); // +1 bookmark +1 keyword +}); + +add_task(async function test_tagDoesntPreventKeywordRemoval() { + await check_keyword(false, "http://example.com/", "example"); + let fc = await foreign_count("http://example.com/"); + + let httpBookmark = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 bookmark + + PlacesUtils.tagging.tagURI(uri("http://example.com/"), ["example_tag"]); + Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 tag + + await PlacesUtils.keywords.insert({ + keyword: "example", + url: "http://example.com/", + }); + Assert.equal(await foreign_count("http://example.com/"), fc + 3); // +1 bookmark +1 tag +1 keyword + + await check_keyword(true, "http://example.com/", "example"); + + await PlacesUtils.bookmarks.remove(httpBookmark); + + await TestUtils.waitForCondition( + async () => + !(await PlacesUtils.bookmarks.fetch({ url: "http://example.com/" })), + "Wait for bookmark to be removed" + ); + + await check_keyword(false, "http://example.com/", "example"); + Assert.equal(await foreign_count("http://example.com/"), fc); // bookmark, keyword, and tag should all have been removed + + await check_no_orphans(); +}); diff --git a/toolkit/components/places/tests/unit/test_lastModified.js b/toolkit/components/places/tests/unit/test_lastModified.js new file mode 100644 index 0000000000..dc44814548 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_lastModified.js @@ -0,0 +1,78 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function assert_date_eq(a, b) { + if (typeof a != "number") { + a = PlacesUtils.toPRTime(a); + } + if (typeof b != "number") { + b = PlacesUtils.toPRTime(b); + } + Assert.equal(a, b, "The dates should match"); +} + +/** + * Test that inserting a new bookmark will set lastModified to the same + * values as dateAdded. + */ +add_task(async function test_bookmarkLastModified() { + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://www.mozilla.org/", + title: "itemTitle", + }); + + let guid = bookmark.guid; + + // Check the bookmark from the database. + bookmark = await PlacesUtils.bookmarks.fetch(guid); + + let dateAdded = PlacesUtils.toPRTime(bookmark.dateAdded); + assert_date_eq(dateAdded, bookmark.lastModified); + + // Change lastModified, then change dateAdded. LastModified should be set + // to the new dateAdded. + // This could randomly fail on virtual machines due to timing issues, so + // we manually increase the time value. See bug 500640 for details. + await PlacesUtils.bookmarks.update({ + guid, + lastModified: PlacesUtils.toDate(dateAdded + 1000), + }); + + bookmark = await PlacesUtils.bookmarks.fetch(guid); + + assert_date_eq(bookmark.lastModified, dateAdded + 1000); + Assert.ok( + bookmark.dateAdded < bookmark.lastModified, + "Date added should be earlier than last modified." + ); + + await PlacesUtils.bookmarks.update({ + guid, + dateAdded: PlacesUtils.toDate(dateAdded + 2000), + }); + + bookmark = await PlacesUtils.bookmarks.fetch(guid); + + assert_date_eq(bookmark.dateAdded, dateAdded + 2000); + assert_date_eq(bookmark.dateAdded, bookmark.lastModified); + + // If dateAdded is set to older than lastModified, then we shouldn't + // update lastModified to keep sync happy. + let origLastModified = bookmark.lastModified; + + await PlacesUtils.bookmarks.update({ + guid, + dateAdded: PlacesUtils.toDate(dateAdded - 10000), + }); + + bookmark = await PlacesUtils.bookmarks.fetch(guid); + + assert_date_eq(bookmark.dateAdded, dateAdded - 10000); + assert_date_eq(bookmark.lastModified, origLastModified); + + await PlacesUtils.bookmarks.remove(guid); +}); diff --git a/toolkit/components/places/tests/unit/test_markpageas.js b/toolkit/components/places/tests/unit/test_markpageas.js new file mode 100644 index 0000000000..03080f4af6 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_markpageas.js @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gVisits = [ + { url: "http://www.mozilla.com/", transition: TRANSITION_TYPED }, + { url: "http://www.google.com/", transition: TRANSITION_BOOKMARK }, + { url: "http://www.espn.com/", transition: TRANSITION_LINK }, +]; + +add_task(async function test_execute() { + let completionPromise = new Promise(resolveCompletionPromise => { + let visitCount = 0; + function listener(aEvents) { + Assert.equal(aEvents.length, 1, "Right number of visits notified"); + Assert.equal(aEvents[0].type, "page-visited"); + let event = aEvents[0]; + Assert.equal(event.url, gVisits[visitCount].url); + Assert.equal(event.transitionType, gVisits[visitCount].transition); + visitCount++; + + if (visitCount == gVisits.length) { + resolveCompletionPromise(); + PlacesObservers.removeListener(["page-visited"], listener); + } + } + PlacesObservers.addListener(["page-visited"], listener); + }); + + for (var visit of gVisits) { + if (visit.transition == TRANSITION_TYPED) { + PlacesUtils.history.markPageAsTyped(uri(visit.url)); + } else if (visit.transition == TRANSITION_BOOKMARK) { + PlacesUtils.history.markPageAsFollowedBookmark(uri(visit.url)); + } else { + // because it is a top level visit with no referrer, + // it will result in TRANSITION_LINK + } + await PlacesTestUtils.addVisits({ + uri: uri(visit.url), + transition: visit.transition, + }); + } + + await completionPromise; +}); diff --git a/toolkit/components/places/tests/unit/test_metadata.js b/toolkit/components/places/tests/unit/test_metadata.js new file mode 100644 index 0000000000..f202b7f405 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_metadata.js @@ -0,0 +1,253 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test_metadata() { + await PlacesUtils.metadata.set("test/integer", 123); + Assert.strictEqual( + await PlacesUtils.metadata.get("test/integer"), + 123, + "Should store new integer value" + ); + + await PlacesUtils.metadata.set("test/double", 123.45); + Assert.strictEqual( + await PlacesUtils.metadata.get("test/double"), + 123.45, + "Should store new double value" + ); + await PlacesUtils.metadata.set("test/double", 567.89); + Assert.strictEqual( + await PlacesUtils.metadata.get("test/double"), + 567.89, + "Should update existing double value" + ); + + await PlacesUtils.metadata.set("test/boolean", false); + Assert.strictEqual( + await PlacesUtils.metadata.get("test/boolean"), + false, + "Should store new Boolean value" + ); + await PlacesUtils.metadata.set("test/boolean", true); + Assert.strictEqual( + await PlacesUtils.metadata.get("test/boolean"), + true, + "Should update existing Boolean value" + ); + + await PlacesUtils.metadata.set("test/string", "hi"); + Assert.equal( + await PlacesUtils.metadata.get("test/string"), + "hi", + "Should store new string value" + ); + await PlacesUtils.metadata.cache.clear(); + Assert.equal( + await PlacesUtils.metadata.get("test/string"), + "hi", + "Should return string value after clearing cache" + ); + + await Assert.rejects( + PlacesUtils.metadata.get("test/nonexistent"), + /No data stored for key test\/nonexistent/, + "Should reject for a non-existent key and no default value." + ); + Assert.equal( + await PlacesUtils.metadata.get("test/nonexistent", "defaultValue"), + "defaultValue", + "Should return the default value for a non-existent key." + ); + + // Values are untyped; it's OK to store a value of a different type for the + // same key. + await PlacesUtils.metadata.set("test/string", 111); + Assert.strictEqual( + await PlacesUtils.metadata.get("test/string"), + 111, + "Should replace string with integer" + ); + await PlacesUtils.metadata.set("test/string", null); + await Assert.rejects( + PlacesUtils.metadata.get("test/string"), + /No data stored for key test\/string/, + "Should clear value when setting to NULL" + ); + + await PlacesUtils.metadata.delete("test/string", "test/boolean"); + await Assert.rejects( + PlacesUtils.metadata.get("test/string"), + /No data stored for key test\/string/, + "Should delete string value" + ); + await Assert.rejects( + PlacesUtils.metadata.get("test/boolean"), + /No data stored for key test\/boolean/, + "Should delete Boolean value" + ); + Assert.strictEqual( + await PlacesUtils.metadata.get("test/integer"), + 123, + "Should keep undeleted integer value" + ); + + await PlacesTestUtils.clearMetadata(); + await Assert.rejects( + PlacesUtils.metadata.get("test/integer"), + /No data stored for key test\/integer/, + "Should clear integer value" + ); + await Assert.rejects( + PlacesUtils.metadata.get("test/double"), + /No data stored for key test\/double/, + "Should clear double value" + ); +}); + +add_task(async function test_metadata_canonical_keys() { + await PlacesUtils.metadata.set("Test/Integer", 123); + Assert.strictEqual( + await PlacesUtils.metadata.get("tEsT/integer"), + 123, + "New keys should be case-insensitive" + ); + await PlacesUtils.metadata.set("test/integer", 456); + Assert.strictEqual( + await PlacesUtils.metadata.get("TEST/INTEGER"), + 456, + "Existing keys should be case-insensitive" + ); + + await Assert.rejects( + PlacesUtils.metadata.set("", 123), + /Invalid metadata key/, + "Should reject empty keys" + ); + await Assert.rejects( + PlacesUtils.metadata.get(123), + /Invalid metadata key/, + "Should reject numeric keys" + ); + await Assert.rejects( + PlacesUtils.metadata.delete(true), + /Invalid metadata key/, + "Should reject Boolean keys" + ); + await Assert.rejects( + PlacesUtils.metadata.set({}), + /Invalid metadata key/, + "Should reject object keys" + ); + await Assert.rejects( + PlacesUtils.metadata.get(null), + /Invalid metadata key/, + "Should reject null keys" + ); + await Assert.rejects( + PlacesUtils.metadata.delete("!@#$"), + /Invalid metadata key/, + "Should reject keys with invalid characters" + ); +}); + +add_task(async function test_metadata_blobs() { + let blob = new Uint8Array([1, 2, 3]); + await PlacesUtils.metadata.set("test/blob", blob); + + let sameBlob = await PlacesUtils.metadata.get("test/blob"); + Assert.equal( + ChromeUtils.getClassName(sameBlob), + "Uint8Array", + "Should cache typed array for blob value" + ); + Assert.deepEqual(sameBlob, blob, "Should store new blob value"); + + info("Remove blob from cache"); + await PlacesUtils.metadata.cache.clear(); + + let newBlob = await PlacesUtils.metadata.get("test/blob"); + Assert.equal( + ChromeUtils.getClassName(newBlob), + "Uint8Array", + "Should inflate blob into typed array" + ); + Assert.deepEqual( + newBlob, + blob, + "Should return same blob after clearing cache" + ); + + await PlacesTestUtils.clearMetadata(); +}); + +add_task(async function test_metadata_arrays() { + let array = [1, 2, 3, "\u2713 \u00E0 la mode"]; + await PlacesUtils.metadata.set("test/array", array); + + let sameArray = await PlacesUtils.metadata.get("test/array"); + Assert.ok(Array.isArray(sameArray), "Should cache array for array value"); + Assert.deepEqual(sameArray, array, "Should store new array value"); + + info("Remove array from cache"); + await PlacesUtils.metadata.cache.clear(); + + let newArray = await PlacesUtils.metadata.get("test/array"); + Assert.ok(Array.isArray(newArray), "Should inflate into array"); + Assert.deepEqual( + newArray, + array, + "Should return same array after clearing cache" + ); + + await PlacesTestUtils.clearMetadata(); +}); + +add_task(async function test_metadata_objects() { + let object = { foo: 123, bar: "test", meow: "\u2713 \u00E0 la mode" }; + await PlacesUtils.metadata.set("test/object", object); + + let sameObject = await PlacesUtils.metadata.get("test/object"); + Assert.equal( + typeof sameObject, + "object", + "Should cache object for object value" + ); + Assert.deepEqual(sameObject, object, "Should store new object value"); + + info("Remove object from cache"); + await PlacesUtils.metadata.cache.clear(); + + let newObject = await PlacesUtils.metadata.get("test/object"); + Assert.equal(typeof newObject, "object", "Should inflate into object"); + Assert.deepEqual( + newObject, + object, + "Should return same object after clearing cache" + ); + + await PlacesTestUtils.clearMetadata(); +}); + +add_task(async function test_metadata_unparsable() { + await PlacesUtils.withConnectionWrapper("test_medata", db => { + let data = PlacesUtils.metadata._base64Encode("{hjjkhj}"); + + return db.execute(` + INSERT INTO moz_meta (key, value) + VALUES ("test/unparsable", "data:application/json;base64,${data}") + `); + }); + + await Assert.rejects( + PlacesUtils.metadata.get("test/unparsable"), + /SyntaxError: JSON.parse/, + "Should reject for an unparsable value with no default" + ); + Assert.deepEqual( + await PlacesUtils.metadata.get("test/unparsable", { foo: 1 }), + { foo: 1 }, + "Should return the default when encountering an unparsable value." + ); + + await PlacesTestUtils.clearMetadata(); +}); diff --git a/toolkit/components/places/tests/unit/test_missing_builtin_folders.js b/toolkit/components/places/tests/unit/test_missing_builtin_folders.js new file mode 100644 index 0000000000..826cc37ff7 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_missing_builtin_folders.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This file tests that a missing built-in folders (child of root) are correctly + * fixed when the database is loaded. + */ + +const ALL_ROOT_GUIDS = [ + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.tagsGuid, + PlacesUtils.bookmarks.mobileGuid, +]; + +const INITIAL_ROOT_GUIDS = [ + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.tagsGuid, + PlacesUtils.bookmarks.unfiledGuid, +]; + +add_task(async function setup() { + // This file has the toolbar and mobile folders missing. + await setupPlacesDatabase("missingBuiltIn.sqlite"); + + // Check database contents to be migrated. + let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME); + let db = await Sqlite.openConnection({ path }); + + let rows = await db.execute( + ` + SELECT guid FROM moz_bookmarks + WHERE parent = (SELECT id from moz_bookmarks WHERE guid = :guid) + `, + { + guid: PlacesUtils.bookmarks.rootGuid, + } + ); + + let guids = rows.map(row => row.getResultByName("guid")); + Assert.deepEqual( + guids, + INITIAL_ROOT_GUIDS, + "Initial database should have only the expected GUIDs" + ); + + await db.close(); +}); + +add_task(async function test_database_recreates_roots() { + Assert.ok( + PlacesUtils.history.databaseStatus == + PlacesUtils.history.DATABASE_STATUS_OK || + PlacesUtils.history.databaseStatus == + PlacesUtils.history.DATABASE_STATUS_UPGRADED, + "Should successfully access the database for the first time" + ); + + let db = await PlacesUtils.promiseDBConnection(); + let rootId = await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.rootGuid); + for (let guid of ALL_ROOT_GUIDS) { + let rows = await db.execute( + ` + SELECT id, parent FROM moz_bookmarks + WHERE guid = :guid + `, + { guid } + ); + + Assert.equal(rows.length, 1, "Should have exactly one row for the root"); + + Assert.equal( + rows[0].getResultByName("parent"), + rootId, + "Should have been created with the correct parent" + ); + + let root = await PlacesUtils.bookmarks.fetch(guid); + + Assert.equal(root.guid, guid, "GUIDs should match"); + Assert.equal( + root.parentGuid, + PlacesUtils.bookmarks.rootGuid, + "Should have the correct parent GUID" + ); + Assert.equal( + root.type, + PlacesUtils.bookmarks.TYPE_FOLDER, + "Should have the correct type" + ); + + let id = rows[0].getResultByName("id"); + Assert.equal( + await PlacesUtils.promiseItemId(guid), + id, + "Should return the correct id from promiseItemId" + ); + Assert.equal( + await PlacesUtils.promiseItemGuid(id), + guid, + "Should return the correct guid from promiseItemGuid" + ); + } + + let rows = await db.execute( + ` + SELECT 1 FROM moz_bookmarks + WHERE parent = (SELECT id from moz_bookmarks WHERE guid = :guid) + `, + { + guid: PlacesUtils.bookmarks.rootGuid, + } + ); + + Assert.equal( + rows.length, + ALL_ROOT_GUIDS.length, + "Root folder should have the expected number of children" + ); +}); diff --git a/toolkit/components/places/tests/unit/test_missing_root_folder.js b/toolkit/components/places/tests/unit/test_missing_root_folder.js new file mode 100644 index 0000000000..f6f1bb19ef --- /dev/null +++ b/toolkit/components/places/tests/unit/test_missing_root_folder.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This file tests that a missing root folder is correctly fixed when the + * database is loaded. + */ + +const ALL_ROOT_GUIDS = [ + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.tagsGuid, + PlacesUtils.bookmarks.mobileGuid, +]; + +add_task(async function setup() { + // This file has no root folder. + await setupPlacesDatabase("noRoot.sqlite"); + + // Check database contents to be migrated. + let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME); + let db = await Sqlite.openConnection({ path }); + + let rows = await db.execute( + ` + SELECT guid FROM moz_bookmarks + WHERE guid = :guid + `, + { + guid: PlacesUtils.bookmarks.rootGuid, + } + ); + + Assert.equal(rows.length, 0, "Root folder should not exist"); + + await db.close(); +}); + +add_task(async function test_database_recreates_roots() { + Assert.ok( + PlacesUtils.history.databaseStatus == + PlacesUtils.history.DATABASE_STATUS_OK || + PlacesUtils.history.databaseStatus == + PlacesUtils.history.DATABASE_STATUS_UPGRADED, + "Should successfully access the database for the first time" + ); + + let db = await PlacesUtils.promiseDBConnection(); + + let rows = await db.execute( + ` + SELECT id, parent, type FROM moz_bookmarks + WHERE guid = :guid + `, + { guid: PlacesUtils.bookmarks.rootGuid } + ); + + Assert.equal(rows.length, 1, "Should have added exactly one root"); + Assert.greaterOrEqual( + rows[0].getResultByName("id"), + 1, + "Should have a valid root Id" + ); + Assert.equal( + rows[0].getResultByName("parent"), + 0, + "Should have a parent of id 0" + ); + Assert.equal( + rows[0].getResultByName("type"), + PlacesUtils.bookmarks.TYPE_FOLDER, + "Should have a type of folder" + ); + + let id = rows[0].getResultByName("id"); + Assert.equal( + await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.rootGuid), + id, + "Should return the correct id from promiseItemId" + ); + Assert.equal( + await PlacesUtils.promiseItemGuid(id), + PlacesUtils.bookmarks.rootGuid, + "Should return the correct guid from promiseItemGuid" + ); + + // Note: Currently we do not fix the parent of the folders on initial startup. + // There is a maintenance task that will do it, hence we don't check the parents + // here, just that the built-in folders correctly exist and haven't been + // duplicated. + for (let guid of ALL_ROOT_GUIDS) { + rows = await db.execute( + ` + SELECT id FROM moz_bookmarks + WHERE guid = :guid + `, + { guid } + ); + + Assert.equal(rows.length, 1, "Should have exactly one row for the root"); + + let root = await PlacesUtils.bookmarks.fetch(guid); + + Assert.equal(root.guid, guid, "GUIDs should match"); + } +}); diff --git a/toolkit/components/places/tests/unit/test_multi_observation.js b/toolkit/components/places/tests/unit/test_multi_observation.js new file mode 100644 index 0000000000..d56eabf8f1 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_multi_observation.js @@ -0,0 +1,384 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test whether registered listener capture proper events. +// We test the following combinations. +// * Listener: listen to single/multi event(s) +// * Event: fire single/multi type of event(s) +// * Timing: fire event(s) at same time/separately +// And also test notifying empty events. + +add_task(async () => { + info("Test for listening to single event and firing single event"); + + const observer = startObservation(["page-visited"]); + + await PlacesUtils.history.insertMany([ + { + title: "test", + url: "http://example.com/test", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const expectedFiredEvents = [ + [ + { + type: "page-visited", + url: "http://example.com/test", + }, + ], + ]; + assertFiredEvents(observer.firedEvents, expectedFiredEvents); + + await PlacesUtils.history.clear(); +}); + +add_task(async () => { + info("Test for listening to multi events with firing single event"); + + const observer = startObservation(["page-visited", "page-title-changed"]); + + await PlacesUtils.history.insertMany([ + { + title: "test", + url: "http://example.com/test", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const expectedFiredEvents = [ + [ + { + type: "page-visited", + url: "http://example.com/test", + }, + ], + ]; + assertFiredEvents(observer.firedEvents, expectedFiredEvents); + + await PlacesUtils.history.clear(); +}); + +add_task(async () => { + info( + "Test for listening to single event with firing multi events at same time" + ); + + const vistedObserver = startObservation(["page-visited"]); + const titleChangedObserver = startObservation(["page-title-changed"]); + + await PlacesUtils.history.insertMany([ + { + title: "will change", + url: "http://example.com/title", + visits: [{ transition: TRANSITION_LINK }], + }, + { + title: "changed", + url: "http://example.com/title", + referrer: "http://example.com/title", + visits: [{ transition: TRANSITION_LINK }], + }, + { + title: "another", + url: "http://example.com/another", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const expectedVisitedFiredEvents = [ + [ + { + type: "page-visited", + url: "http://example.com/title", + }, + { + type: "page-visited", + url: "http://example.com/title", + }, + { + type: "page-visited", + url: "http://example.com/another", + }, + ], + ]; + assertFiredEvents(vistedObserver.firedEvents, expectedVisitedFiredEvents); + + const expectedTitleChangedFiredEvents = [ + [ + { + type: "page-title-changed", + url: "http://example.com/title", + title: "changed", + }, + ], + ]; + assertFiredEvents( + titleChangedObserver.firedEvents, + expectedTitleChangedFiredEvents + ); + + await PlacesUtils.history.clear(); +}); + +add_task(async () => { + info( + "Test for listening to single event with firing multi events separately" + ); + + const observer = startObservation(["page-visited"]); + + await PlacesUtils.history.insertMany([ + { + title: "test", + url: "http://example.com/test", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + + await PlacesUtils.history.insertMany([ + { + title: "another", + url: "http://example.com/another", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const expectedFiredEvents = [ + [ + { + type: "page-visited", + url: "http://example.com/test", + }, + ], + [ + { + type: "page-visited", + url: "http://example.com/another", + }, + ], + ]; + assertFiredEvents(observer.firedEvents, expectedFiredEvents); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.remove(bookmark.guid); +}); + +add_task(async () => { + info("Test for listening to multi events with firing single event"); + + const observer = startObservation([ + "page-visited", + "page-title-changed", + "bookmark-added", + ]); + + await PlacesUtils.history.insertMany([ + { + title: "test", + url: "http://example.com/test", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const expectedFiredEvents = [ + [ + { + type: "page-visited", + url: "http://example.com/test", + }, + ], + ]; + assertFiredEvents(observer.firedEvents, expectedFiredEvents); + + await PlacesUtils.history.clear(); +}); + +add_task(async () => { + info( + "Test for listening to multi events with firing multi events at same time" + ); + + const observer = startObservation([ + "page-visited", + "page-title-changed", + "bookmark-added", + ]); + + await PlacesUtils.history.insertMany([ + { + title: "will change", + url: "http://example.com/title", + visits: [{ transition: TRANSITION_LINK }], + }, + { + title: "changed", + url: "http://example.com/title", + referrer: "http://example.com/title", + visits: [{ transition: TRANSITION_LINK }], + }, + { + title: "another", + url: "http://example.com/another", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const expectedFiredEvents = [ + [ + { + type: "page-visited", + url: "http://example.com/title", + }, + { + type: "page-visited", + url: "http://example.com/title", + }, + { + type: "page-title-changed", + url: "http://example.com/title", + title: "changed", + }, + { + type: "page-visited", + url: "http://example.com/another", + }, + ], + ]; + assertFiredEvents(observer.firedEvents, expectedFiredEvents); + + await PlacesUtils.history.clear(); +}); + +add_task(async () => { + info( + "Test for listening to multi events with firing multi events separately" + ); + + const observer = startObservation([ + "page-visited", + "page-title-changed", + "bookmark-added", + ]); + + await PlacesUtils.history.insertMany([ + { + title: "will change", + url: "http://example.com/title", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + + await PlacesUtils.history.insertMany([ + { + title: "changed", + url: "http://example.com/title", + referrer: "http://example.com/title", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + + const expectedFiredEvents = [ + [ + { + type: "page-visited", + url: "http://example.com/title", + }, + ], + [ + { + type: "bookmark-added", + title: "a folder", + }, + ], + [ + { + type: "page-visited", + url: "http://example.com/title", + }, + { + type: "page-title-changed", + url: "http://example.com/title", + title: "changed", + }, + ], + ]; + assertFiredEvents(observer.firedEvents, expectedFiredEvents); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.remove(bookmark.guid); +}); + +add_task(async function test_empty_notifications_array() { + info("Test whether listener does not receive empty events"); + + if (AppConstants.DEBUG) { + info( + "Ignore this test since we added a MOZ_ASSERT for empty events in debug build" + ); + return; + } + + const observer = startObservation(["page-visited"]); + PlacesObservers.notifyListeners([]); + Assert.equal(observer.firedEvents.length, 0, "Listener does not receive any"); +}); + +function startObservation(targets) { + const observer = { + firedEvents: [], + handle(events) { + this.firedEvents.push(events); + }, + }; + + PlacesObservers.addListener(targets, observer.handle.bind(observer)); + + return observer; +} + +function assertFiredEvents(firedEvents, expectedFiredEvents) { + Assert.equal( + firedEvents.length, + expectedFiredEvents.length, + "Number events fired is correct" + ); + + for (let i = 0; i < firedEvents.length; i++) { + info(`Check firedEvents[${i}]`); + const events = firedEvents[i]; + const expectedEvents = expectedFiredEvents[i]; + assertEvents(events, expectedEvents); + } +} + +function assertEvents(events, expectedEvents) { + Assert.equal( + events.length, + expectedEvents.length, + "Number events is correct" + ); + + for (let i = 0; i < events.length; i++) { + info(`Check events[${i}]`); + const event = events[i]; + const expectedEvent = expectedEvents[i]; + + for (let field in expectedEvent) { + Assert.equal(event[field], expectedEvent[field], `${field} is correct`); + } + } +} diff --git a/toolkit/components/places/tests/unit/test_multi_word_tags.js b/toolkit/components/places/tests/unit/test_multi_word_tags.js new file mode 100644 index 0000000000..17a68cf972 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_multi_word_tags.js @@ -0,0 +1,147 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService + ); +} catch (ex) { + do_throw("Could not get history service\n"); +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService( + Ci.nsITaggingService + ); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + +add_task(async function run_test() { + var uri1 = uri("http://site.tld/1"); + var uri2 = uri("http://site.tld/2"); + var uri3 = uri("http://site.tld/3"); + var uri4 = uri("http://site.tld/4"); + var uri5 = uri("http://site.tld/5"); + var uri6 = uri("http://site.tld/6"); + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { url: uri1 }, + { url: uri2 }, + { url: uri3 }, + { url: uri4 }, + { url: uri5 }, + { url: uri6 }, + ], + }); + + tagssvc.tagURI(uri1, ["foo"]); + tagssvc.tagURI(uri2, ["bar"]); + tagssvc.tagURI(uri3, ["cheese"]); + tagssvc.tagURI(uri4, ["foo bar"]); + tagssvc.tagURI(uri5, ["bar cheese"]); + tagssvc.tagURI(uri6, ["foo bar cheese"]); + + // Search for "item", should get one result + var options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + + var query = histsvc.getNewQuery(); + query.searchTerms = "foo"; + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 3); + Assert.equal(root.getChild(0).uri, "http://site.tld/1"); + Assert.equal(root.getChild(1).uri, "http://site.tld/4"); + Assert.equal(root.getChild(2).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 4); + Assert.equal(root.getChild(0).uri, "http://site.tld/2"); + Assert.equal(root.getChild(1).uri, "http://site.tld/4"); + Assert.equal(root.getChild(2).uri, "http://site.tld/5"); + Assert.equal(root.getChild(3).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 3); + Assert.equal(root.getChild(0).uri, "http://site.tld/3"); + Assert.equal(root.getChild(1).uri, "http://site.tld/5"); + Assert.equal(root.getChild(2).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "foo bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 2); + Assert.equal(root.getChild(0).uri, "http://site.tld/4"); + Assert.equal(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "bar foo"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 2); + Assert.equal(root.getChild(0).uri, "http://site.tld/4"); + Assert.equal(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "bar cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 2); + Assert.equal(root.getChild(0).uri, "http://site.tld/5"); + Assert.equal(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 2); + Assert.equal(root.getChild(0).uri, "http://site.tld/5"); + Assert.equal(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "foo bar cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 1); + Assert.equal(root.getChild(0).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese foo bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 1); + Assert.equal(root.getChild(0).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese bar foo"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + Assert.equal(root.childCount, 1); + Assert.equal(root.getChild(0).uri, "http://site.tld/6"); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_nested_notifications.js b/toolkit/components/places/tests/unit/test_nested_notifications.js new file mode 100644 index 0000000000..cf1e4b77fa --- /dev/null +++ b/toolkit/components/places/tests/unit/test_nested_notifications.js @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for nested notifications of Places events. + * In this test, we check behavior of listeners of Places event upon firing nested + * notification from inside of listener that received notifications. + */ +add_task(async function () { + // We prepare 6 listeners for the test. + // 1. Listener that added before root notification. + const addRoot = new Observer(); + // 2. Listener that added before root notification + // but removed before first nest notification. + const addRootRemoveFirst = new Observer(); + // 3. Listener that added before root notification + // but removed before second nest notification. + const addRootRemoveSecond = new Observer(); + // 4. Listener that added before first nest notification. + const addFirst = new Observer(); + // 5. Listener that added before first nest notification + // but removed before second nest notification. + const addFirstRemoveSecond = new Observer(); + // 6. Listener that added before second nest notification. + const addSecond = new Observer(); + + // This is a listener listened the root notification + // and do what we have to do for test in the first nest. + const firstNestOperator = () => { + info("Start to operate at first nest"); + + // Remove itself to avoid listening more. + removePlacesListener(firstNestOperator); + + info("Add/Remove test listeners at first nest"); + removePlacesListener(addRootRemoveFirst.handle); + addPlacesListener(addFirst.handle); + addPlacesListener(addFirstRemoveSecond.handle); + + // Add second nest operator. + addPlacesListener(secondNestOperator); + + info("Send notification at first nest"); + notifyPlacesEvent("first"); + }; + + // This is a listener listened the first nest notification + // and do what we have to do for test in the second nest. + const secondNestOperator = () => { + info("Start to operate at second nest"); + + // Remove itself to avoid listening more. + removePlacesListener(secondNestOperator); + + info("Add/Remove test listeners at second nest"); + removePlacesListener(addRootRemoveSecond.handle); + removePlacesListener(addFirstRemoveSecond.handle); + addPlacesListener(addSecond.handle); + + info("Send notification at second nest"); + notifyPlacesEvent("second"); + }; + + info("Add test listeners that handle notification sent at root"); + addPlacesListener(addRoot.handle); + addPlacesListener(addRootRemoveFirst.handle); + addPlacesListener(addRootRemoveSecond.handle); + + // Add first nest operator. + addPlacesListener(firstNestOperator); + + info("Send notification at root"); + notifyPlacesEvent("root"); + + info("Check whether or not test listeners could get expected notifications"); + assertNotifications(addRoot.notifications, [ + [{ guid: "root" }], + [{ guid: "first" }], + [{ guid: "second" }], + ]); + assertNotifications(addRootRemoveFirst.notifications, [[{ guid: "root" }]]); + assertNotifications(addRootRemoveSecond.notifications, [ + [{ guid: "root" }], + [{ guid: "first" }], + ]); + assertNotifications(addFirst.notifications, [ + [{ guid: "first" }], + [{ guid: "second" }], + ]); + assertNotifications(addFirstRemoveSecond.notifications, [ + [{ guid: "first" }], + ]); + assertNotifications(addSecond.notifications, [[{ guid: "second" }]]); +}); + +function addPlacesListener(listener) { + PlacesObservers.addListener(["bookmark-added"], listener); +} + +function removePlacesListener(listener) { + PlacesObservers.removeListener(["bookmark-added"], listener); +} + +function notifyPlacesEvent(guid) { + PlacesObservers.notifyListeners([ + new PlacesBookmarkAddition({ + dateAdded: 0, + guid, + id: -1, + index: 0, + isTagging: false, + itemType: 1, + parentGuid: "fake", + parentId: -2, + source: 0, + title: guid, + url: `http://example.com/${guid}`, + }), + ]); +} + +class Observer { + constructor() { + this.notifications = []; + this.handle = this.handle.bind(this); + } + + handle(events) { + this.notifications.push(events); + } +} + +/** + * Assert notifications the observer received. + * + * @param Array - notifications + * @param Array - expectedNotifications + */ +function assertNotifications(notifications, expectedNotifications) { + Assert.equal( + notifications.length, + expectedNotifications.length, + "Number of notifications is correct" + ); + + for (let i = 0; i < notifications.length; i++) { + info(`Check notifications[${i}]`); + const placesEvents = notifications[i]; + const expectedPlacesEvents = expectedNotifications[i]; + assertPlacesEvents(placesEvents, expectedPlacesEvents); + } +} + +/** + * Assert Places events. + * This function checks given expected event field only. + * + * @param Array - events + * @param Array - expectedEvents + */ +function assertPlacesEvents(events, expectedEvents) { + Assert.equal( + events.length, + expectedEvents.length, + "Number of Places events is correct" + ); + + for (let i = 0; i < events.length; i++) { + info(`Check Places events[${i}]`); + const event = events[i]; + const expectedEvent = expectedEvents[i]; + + for (let field in expectedEvent) { + Assert.equal(event[field], expectedEvent[field], `${field} is correct`); + } + } +} diff --git a/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js new file mode 100644 index 0000000000..8c061c60d7 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js @@ -0,0 +1,285 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var resultObserver = { + insertedNode: null, + nodeInserted(parent, node, newIndex) { + this.insertedNode = node; + }, + removedNode: null, + nodeRemoved(parent, node, oldIndex) { + this.removedNode = node; + }, + + newTitle: "", + nodeChangedByTitle: null, + nodeTitleChanged(node, newTitle) { + this.nodeChangedByTitle = node; + this.newTitle = newTitle; + }, + + newAccessCount: 0, + newTime: 0, + nodeChangedByHistoryDetails: null, + nodeHistoryDetailsChanged(node, oldVisitDate, oldVisitCount) { + this.nodeChangedByHistoryDetails = node; + this.newTime = node.time; + this.newAccessCount = node.accessCount; + }, + + movedNode: null, + nodeMoved(node, oldParent, oldIndex, newParent, newIndex) { + this.movedNode = node; + }, + openedContainer: null, + closedContainer: null, + containerStateChanged(aNode, aOldState, aNewState) { + if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) { + this.openedContainer = aNode; + } else if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) { + this.closedContainer = aNode; + } + }, + invalidatedContainer: null, + invalidateContainer(node) { + this.invalidatedContainer = node; + }, + sortingMode: null, + sortingChanged(sortingMode) { + this.sortingMode = sortingMode; + }, + inBatchMode: false, + batchingCallCount: 0, + batching(aToggleMode) { + Assert.notEqual(this.inBatchMode, aToggleMode); + this.inBatchMode = aToggleMode; + this.batchingCallCount++; + }, + result: null, + reset() { + this.insertedNode = null; + this.removedNode = null; + this.nodeChangedByTitle = null; + this.nodeChangedByHistoryDetails = null; + this.replacedNode = null; + this.movedNode = null; + this.openedContainer = null; + this.closedContainer = null; + this.invalidatedContainer = null; + this.sortingMode = null; + this.inBatchMode = false; + this.batchingCallCount = 0; + }, +}; + +var testURI = uri("http://mozilla.com"); + +add_task(async function check_history_query() { + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + let query = PlacesUtils.history.getNewQuery(); + let result = PlacesUtils.history.executeQuery(query, options); + result.addObserver(resultObserver); + let root = result.root; + root.containerOpen = true; + + Assert.notEqual(resultObserver.openedContainer, null); + + // nsINavHistoryResultObserver.nodeInserted + // add a visit + await PlacesTestUtils.addVisits(testURI); + Assert.equal(testURI.spec, resultObserver.insertedNode.uri); + + // nsINavHistoryResultObserver.nodeHistoryDetailsChanged + // adding a visit causes nodeHistoryDetailsChanged for the folder + Assert.equal(root.uri, resultObserver.nodeChangedByHistoryDetails.uri); + + // nsINavHistoryResultObserver.itemTitleChanged for a leaf node + await PlacesTestUtils.addVisits({ uri: testURI, title: "baz" }); + Assert.equal(resultObserver.nodeChangedByTitle.title, "baz"); + + // nsINavHistoryResultObserver.nodeRemoved + let removedURI = uri("http://google.com"); + await PlacesTestUtils.addVisits(removedURI); + await PlacesUtils.history.remove(removedURI); + Assert.equal(removedURI.spec, resultObserver.removedNode.uri); + + // nsINavHistoryResultObserver.invalidateContainer + await PlacesUtils.history.removeByFilter({ host: "mozilla.com" }); + // This test is disabled for bug 1089691. It is failing bcause the new API + // doesn't send batching notifications and thus the result doesn't invalidate + // the whole container. + // Assert.equal(root.uri, resultObserver.invalidatedContainer.uri); + + // nsINavHistoryResultObserver.sortingChanged + resultObserver.invalidatedContainer = null; + result.sortingMode = options.SORT_BY_TITLE_ASCENDING; + Assert.equal(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING); + Assert.equal(resultObserver.invalidatedContainer, result.root); + + // nsINavHistoryResultObserver.invalidateContainer + await PlacesUtils.history.clear(); + Assert.equal(root.uri, resultObserver.invalidatedContainer.uri); + + root.containerOpen = false; + Assert.equal(resultObserver.closedContainer, resultObserver.openedContainer); + result.removeObserver(resultObserver); + resultObserver.reset(); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function check_bookmarks_query() { + let options = PlacesUtils.history.getNewQueryOptions(); + let query = PlacesUtils.history.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.menuGuid]); + let result = PlacesUtils.history.executeQuery(query, options); + result.addObserver(resultObserver); + let root = result.root; + root.containerOpen = true; + + Assert.notEqual(resultObserver.openedContainer, null); + + // nsINavHistoryResultObserver.nodeInserted + // add a bookmark + let testBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: testURI, + title: "foo", + }); + Assert.equal("foo", resultObserver.insertedNode.title); + Assert.equal(testURI.spec, resultObserver.insertedNode.uri); + + // nsINavHistoryResultObserver.nodeHistoryDetailsChanged + // adding a visit causes nodeHistoryDetailsChanged for the folder + Assert.equal(root.uri, resultObserver.nodeChangedByHistoryDetails.uri); + + // nsINavHistoryResultObserver.nodeTitleChanged for a leaf node + await PlacesUtils.bookmarks.update({ + guid: testBookmark.guid, + title: "baz", + }); + Assert.equal(resultObserver.nodeChangedByTitle.title, "baz"); + Assert.equal(resultObserver.newTitle, "baz"); + + let testBookmark2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://google.com", + title: "foo", + }); + + await PlacesUtils.bookmarks.update({ + guid: testBookmark2.guid, + index: 0, + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + Assert.equal(resultObserver.movedNode.bookmarkGuid, testBookmark2.guid); + + // nsINavHistoryResultObserver.nodeRemoved + await PlacesUtils.bookmarks.remove(testBookmark2.guid); + Assert.equal(testBookmark2.guid, resultObserver.removedNode.bookmarkGuid); + + // XXX nsINavHistoryResultObserver.invalidateContainer + + // nsINavHistoryResultObserver.sortingChanged + resultObserver.invalidatedContainer = null; + result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING; + Assert.equal( + resultObserver.sortingMode, + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING + ); + Assert.equal(resultObserver.invalidatedContainer, result.root); + + root.containerOpen = false; + Assert.equal(resultObserver.closedContainer, resultObserver.openedContainer); + result.removeObserver(resultObserver); + resultObserver.reset(); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function check_mixed_query() { + let options = PlacesUtils.history.getNewQueryOptions(); + let query = PlacesUtils.history.getNewQuery(); + query.onlyBookmarked = true; + let result = PlacesUtils.history.executeQuery(query, options); + result.addObserver(resultObserver); + let root = result.root; + root.containerOpen = true; + + Assert.notEqual(resultObserver.openedContainer, null); + + root.containerOpen = false; + Assert.equal(resultObserver.closedContainer, resultObserver.openedContainer); + result.removeObserver(resultObserver); + resultObserver.reset(); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function check_a_batch_process() { + const options = PlacesUtils.history.getNewQueryOptions(); + const query = PlacesUtils.history.getNewQuery(); + const result = PlacesUtils.history.executeQuery(query, options); + result.addObserver(resultObserver); + + info("Check initial state"); + Assert.equal(resultObserver.inBatchMode, false); + Assert.equal(resultObserver.batchingCallCount, 0); + + info("Check whether batching is called when call onBeginUpdateBatch"); + result.onBeginUpdateBatch(); + Assert.equal(resultObserver.inBatchMode, true); + Assert.equal(resultObserver.batchingCallCount, 1); + + info("Check whether batching is called when call onEndUpdateBatch"); + result.onEndUpdateBatch(); + Assert.equal(resultObserver.inBatchMode, false); + Assert.equal(resultObserver.batchingCallCount, 2); + + result.removeObserver(resultObserver); + resultObserver.reset(); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function check_multi_batch_processes() { + const options = PlacesUtils.history.getNewQueryOptions(); + const query = PlacesUtils.history.getNewQuery(); + const result = PlacesUtils.history.executeQuery(query, options); + result.addObserver(resultObserver); + + info("Check initial state"); + Assert.equal(resultObserver.inBatchMode, false); + Assert.equal(resultObserver.batchingCallCount, 0); + + info("Check whether batching is called when calling onBeginUpdateBatch"); + result.onBeginUpdateBatch(); + Assert.equal(resultObserver.inBatchMode, true); + Assert.equal(resultObserver.batchingCallCount, 1); + + info( + "Check whether batching is not called when calling onBeginUpdateBatch again" + ); + result.onBeginUpdateBatch(); + Assert.equal(resultObserver.inBatchMode, true); + Assert.equal(resultObserver.batchingCallCount, 1); + + info( + "Check whether batching is not called until calling onEndUpdateBatch the same number times that onBeginUpdateBatch is called" + ); + result.onEndUpdateBatch(); + Assert.equal(resultObserver.inBatchMode, true); + Assert.equal(resultObserver.batchingCallCount, 1); + + info( + "Check whether batching is called when calling onEndUpdateBatch the same number times" + ); + result.onEndUpdateBatch(); + Assert.equal(resultObserver.inBatchMode, false); + Assert.equal(resultObserver.batchingCallCount, 2); + + result.removeObserver(resultObserver); + resultObserver.reset(); + await PlacesTestUtils.promiseAsyncUpdates(); +}); diff --git a/toolkit/components/places/tests/unit/test_null_interfaces.js b/toolkit/components/places/tests/unit/test_null_interfaces.js new file mode 100644 index 0000000000..f5608285d6 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_null_interfaces.js @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 489872 to make sure passing nulls to nsNavHistory doesn't crash. + */ + +// Make an array of services to test, each specifying a class id, interface +// and an array of function names that don't throw when passed nulls +var testServices = [ + [ + "browser/nav-history-service;1", + ["nsINavHistoryService"], + [ + "queryStringToQuery", + "removePagesByTimeframe", + "removePagesFromHost", + "getObservers", + ], + ], + [ + "browser/nav-bookmarks-service;1", + ["nsINavBookmarksService"], + ["createFolder", "getObservers"], + ], + ["browser/favicon-service;1", ["nsIFaviconService"], []], + ["browser/tagging-service;1", ["nsITaggingService"], []], +]; +info(testServices.join("\n")); + +function run_test() { + for (let [cid, ifaces, nothrow] of testServices) { + info(`Running test with ${cid} ${ifaces.join(", ")} ${nothrow}`); + let s = Cc["@mozilla.org/" + cid].getService(Ci.nsISupports); + for (let iface of ifaces) { + s.QueryInterface(Ci[iface]); + } + + let okName = function (name) { + info(`Checking if function is okay to test: ${name}`); + let func = s[name]; + + let mesg = ""; + if (typeof func != "function") { + mesg = "Not a function!"; + } else if (!func.length) { + mesg = "No args needed!"; + } else if (name == "QueryInterface") { + mesg = "Ignore QI!"; + } + + if (mesg) { + info(`${mesg} Skipping: ${name}`); + return false; + } + + return true; + }; + + info(`Generating an array of functions to test service: ${s}`); + for (let n of Object.keys(s) + .filter(i => okName(i)) + .sort()) { + info(`\nTesting ${ifaces.join(", ")} function with null args: ${n}`); + + let func = s[n]; + let num = func.length; + info(`Generating array of nulls for #args: ${num}`); + let args = Array(num).fill(null); + + let tryAgain = true; + while (tryAgain) { + try { + info(`Calling with args: ${JSON.stringify(args)}`); + func.apply(s, args); + + info( + `The function did not throw! Is it one of the nothrow? ${nothrow}` + ); + Assert.notEqual(nothrow.indexOf(n), -1); + + info("Must have been an expected nothrow, so no need to try again"); + tryAgain = false; + } catch (ex) { + if (ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) { + info(`Caught an expected exception: ${ex.name}`); + info("Moving on to the next test.."); + tryAgain = false; + } else if (ex.result == Cr.NS_ERROR_XPC_NEED_OUT_OBJECT) { + let pos = Number(ex.message.match(/object arg (\d+)/)[1]); + info(`Function call expects an out object at ${pos}`); + args[pos] = {}; + } else if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) { + info(`Method not implemented exception: ${ex.name}`); + info("Moving on to the next test.."); + tryAgain = false; + } else { + throw ex; + } + } + } + } + } +} diff --git a/toolkit/components/places/tests/unit/test_origins.js b/toolkit/components/places/tests/unit/test_origins.js new file mode 100644 index 0000000000..4e2d0e5fa6 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_origins.js @@ -0,0 +1,1122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Makes sure the moz_origins table and origin frecency stats are updated +// correctly. + +"use strict"; + +// Visiting a URL with a new origin should immediately update moz_origins. +add_task(async function visit() { + await checkDB([]); + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Repeatedly visiting a URL with an initially new origin should update +// moz_origins (with the correct frecency). +add_task(async function visitRepeatedly() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + { uri: "http://example.com/" }, + { uri: "http://example.com/" }, + ]); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Same as previous, but visits are added sequentially. +add_task(async function visitRepeatedlySequential() { + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([]); + await cleanUp(); +}); + +// After removing an origin's URLs, visiting a URL with the origin should +// immediately update moz_origins. +add_task(async function vistAfterDelete() { + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([]); + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Visiting different URLs with the same origin should update moz_origins, and +// moz_origins.frecency should be the sum of the URL frecencies. +add_task(async function visitDifferentURLsSameOrigin() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/1" }, + { uri: "http://example.com/2" }, + { uri: "http://example.com/3" }, + ]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/1", "http://example.com/2", "http://example.com/3"], + ], + ]); + await PlacesUtils.history.remove("http://example.com/1"); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/2", "http://example.com/3"], + ], + ]); + await PlacesUtils.history.remove("http://example.com/2"); + await checkDB([["http://", "example.com", ["http://example.com/3"]]]); + await PlacesUtils.history.remove("http://example.com/3"); + await checkDB([]); + await cleanUp(); +}); + +// Same as previous, but visits are added sequentially. +add_task(async function visitDifferentURLsSameOriginSequential() { + await PlacesTestUtils.addVisits([{ uri: "http://example.com/1" }]); + await checkDB([["http://", "example.com", ["http://example.com/1"]]]); + await PlacesTestUtils.addVisits([{ uri: "http://example.com/2" }]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/1", "http://example.com/2"], + ], + ]); + await PlacesTestUtils.addVisits([{ uri: "http://example.com/3" }]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/1", "http://example.com/2", "http://example.com/3"], + ], + ]); + await PlacesUtils.history.remove("http://example.com/1"); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/2", "http://example.com/3"], + ], + ]); + await PlacesUtils.history.remove("http://example.com/2"); + await checkDB([["http://", "example.com", ["http://example.com/3"]]]); + await PlacesUtils.history.remove("http://example.com/3"); + await checkDB([]); + await cleanUp(); +}); + +// Repeatedly visiting different URLs with the same origin should update +// moz_origins (with the correct frecencies), and moz_origins.frecency should be +// the sum of the URL frecencies. +add_task(async function visitDifferentURLsSameOriginRepeatedly() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/1" }, + { uri: "http://example.com/1" }, + { uri: "http://example.com/1" }, + { uri: "http://example.com/2" }, + { uri: "http://example.com/2" }, + { uri: "http://example.com/3" }, + ]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/1", "http://example.com/2", "http://example.com/3"], + ], + ]); + await PlacesUtils.history.remove("http://example.com/1"); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/2", "http://example.com/3"], + ], + ]); + await PlacesUtils.history.remove("http://example.com/2"); + await checkDB([["http://", "example.com", ["http://example.com/3"]]]); + await PlacesUtils.history.remove("http://example.com/3"); + await checkDB([]); + await cleanUp(); +}); + +// Visiting URLs with different origins should update moz_origins. +add_task(async function visitDifferentOrigins() { + await PlacesTestUtils.addVisits([ + { uri: "http://example1.com/" }, + { uri: "http://example2.com/" }, + { uri: "http://example3.com/" }, + ]); + await checkDB([ + ["http://", "example1.com", ["http://example1.com/"]], + ["http://", "example2.com", ["http://example2.com/"]], + ["http://", "example3.com", ["http://example3.com/"]], + ]); + await PlacesUtils.history.remove("http://example1.com/"); + await checkDB([ + ["http://", "example2.com", ["http://example2.com/"]], + ["http://", "example3.com", ["http://example3.com/"]], + ]); + await PlacesUtils.history.remove("http://example2.com/"); + await checkDB([["http://", "example3.com", ["http://example3.com/"]]]); + await PlacesUtils.history.remove("http://example3.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Same as previous, but visits are added sequentially. +add_task(async function visitDifferentOriginsSequential() { + await PlacesTestUtils.addVisits([{ uri: "http://example1.com/" }]); + await checkDB([["http://", "example1.com", ["http://example1.com/"]]]); + await PlacesTestUtils.addVisits([{ uri: "http://example2.com/" }]); + await checkDB([ + ["http://", "example1.com", ["http://example1.com/"]], + ["http://", "example2.com", ["http://example2.com/"]], + ]); + await PlacesTestUtils.addVisits([{ uri: "http://example3.com/" }]); + await checkDB([ + ["http://", "example1.com", ["http://example1.com/"]], + ["http://", "example2.com", ["http://example2.com/"]], + ["http://", "example3.com", ["http://example3.com/"]], + ]); + await PlacesUtils.history.remove("http://example1.com/"); + await checkDB([ + ["http://", "example2.com", ["http://example2.com/"]], + ["http://", "example3.com", ["http://example3.com/"]], + ]); + await PlacesUtils.history.remove("http://example2.com/"); + await checkDB([["http://", "example3.com", ["http://example3.com/"]]]); + await PlacesUtils.history.remove("http://example3.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Repeatedly visiting URLs with different origins should update moz_origins +// (with the correct frecencies). +add_task(async function visitDifferentOriginsRepeatedly() { + await PlacesTestUtils.addVisits([ + { uri: "http://example1.com/" }, + { uri: "http://example1.com/" }, + { uri: "http://example1.com/" }, + { uri: "http://example2.com/" }, + { uri: "http://example2.com/" }, + { uri: "http://example3.com/" }, + ]); + await checkDB([ + ["http://", "example1.com", ["http://example1.com/"]], + ["http://", "example2.com", ["http://example2.com/"]], + ["http://", "example3.com", ["http://example3.com/"]], + ]); + await PlacesUtils.history.remove("http://example1.com/"); + await checkDB([ + ["http://", "example2.com", ["http://example2.com/"]], + ["http://", "example3.com", ["http://example3.com/"]], + ]); + await PlacesUtils.history.remove("http://example2.com/"); + await checkDB([["http://", "example3.com", ["http://example3.com/"]]]); + await PlacesUtils.history.remove("http://example3.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Visiting URLs, some with the same and some with different origins, should +// update moz_origins. +add_task(async function visitDifferentOriginsDifferentURLs() { + await PlacesTestUtils.addVisits([ + { uri: "http://example1.com/1" }, + { uri: "http://example1.com/2" }, + { uri: "http://example1.com/3" }, + { uri: "http://example2.com/1" }, + { uri: "http://example2.com/2" }, + { uri: "http://example3.com/1" }, + ]); + await checkDB([ + [ + "http://", + "example1.com", + [ + "http://example1.com/1", + "http://example1.com/2", + "http://example1.com/3", + ], + ], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/1"); + await checkDB([ + [ + "http://", + "example1.com", + ["http://example1.com/2", "http://example1.com/3"], + ], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/2"); + await checkDB([ + ["http://", "example1.com", ["http://example1.com/3"]], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/3"); + await checkDB([ + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example2.com/1"); + await checkDB([ + ["http://", "example2.com", ["http://example2.com/2"]], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example2.com/2"); + await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]); + await PlacesUtils.history.remove("http://example3.com/1"); + await checkDB([]); +}); + +// Same as previous, but visits are added sequentially. +add_task(async function visitDifferentOriginsDifferentURLsSequential() { + await PlacesTestUtils.addVisits([{ uri: "http://example1.com/1" }]); + await checkDB([["http://", "example1.com", ["http://example1.com/1"]]]); + await PlacesTestUtils.addVisits([{ uri: "http://example1.com/2" }]); + await checkDB([ + [ + "http://", + "example1.com", + ["http://example1.com/1", "http://example1.com/2"], + ], + ]); + await PlacesTestUtils.addVisits([{ uri: "http://example1.com/3" }]); + await checkDB([ + [ + "http://", + "example1.com", + [ + "http://example1.com/1", + "http://example1.com/2", + "http://example1.com/3", + ], + ], + ]); + await PlacesTestUtils.addVisits([{ uri: "http://example2.com/1" }]); + await checkDB([ + [ + "http://", + "example1.com", + [ + "http://example1.com/1", + "http://example1.com/2", + "http://example1.com/3", + ], + ], + ["http://", "example2.com", ["http://example2.com/1"]], + ]); + await PlacesTestUtils.addVisits([{ uri: "http://example2.com/2" }]); + await checkDB([ + [ + "http://", + "example1.com", + [ + "http://example1.com/1", + "http://example1.com/2", + "http://example1.com/3", + ], + ], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ]); + await PlacesTestUtils.addVisits([{ uri: "http://example3.com/1" }]); + await checkDB([ + [ + "http://", + "example1.com", + [ + "http://example1.com/1", + "http://example1.com/2", + "http://example1.com/3", + ], + ], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/1"); + await checkDB([ + [ + "http://", + "example1.com", + ["http://example1.com/2", "http://example1.com/3"], + ], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/2"); + await checkDB([ + ["http://", "example1.com", ["http://example1.com/3"]], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/3"); + await checkDB([ + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example2.com/1"); + await checkDB([ + ["http://", "example2.com", ["http://example2.com/2"]], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example2.com/2"); + await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]); + await PlacesUtils.history.remove("http://example3.com/1"); + await checkDB([]); +}); + +// Repeatedly visiting URLs, some with the same and some with different origins, +// should update moz_origins (with the correct frecencies). +add_task(async function visitDifferentOriginsDifferentURLsRepeatedly() { + await PlacesTestUtils.addVisits([ + { uri: "http://example1.com/1" }, + { uri: "http://example1.com/1" }, + { uri: "http://example1.com/1" }, + { uri: "http://example1.com/2" }, + { uri: "http://example1.com/2" }, + { uri: "http://example1.com/3" }, + { uri: "http://example2.com/1" }, + { uri: "http://example2.com/1" }, + { uri: "http://example2.com/1" }, + { uri: "http://example2.com/2" }, + { uri: "http://example2.com/2" }, + { uri: "http://example3.com/1" }, + { uri: "http://example3.com/1" }, + ]); + await checkDB([ + [ + "http://", + "example1.com", + [ + "http://example1.com/1", + "http://example1.com/2", + "http://example1.com/3", + ], + ], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/1"); + await checkDB([ + [ + "http://", + "example1.com", + ["http://example1.com/2", "http://example1.com/3"], + ], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/2"); + await checkDB([ + ["http://", "example1.com", ["http://example1.com/3"]], + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example1.com/3"); + await checkDB([ + [ + "http://", + "example2.com", + ["http://example2.com/1", "http://example2.com/2"], + ], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example2.com/1"); + await checkDB([ + ["http://", "example2.com", ["http://example2.com/2"]], + ["http://", "example3.com", ["http://example3.com/1"]], + ]); + await PlacesUtils.history.remove("http://example2.com/2"); + await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]); + await PlacesUtils.history.remove("http://example3.com/1"); + await checkDB([]); +}); + +// Makes sure URIs with the same TLD but different www subdomains are recognized +// as different origins. Makes sure removing one doesn't remove the others. +add_task(async function www1() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + { uri: "http://www.example.com/" }, + { uri: "http://www.www.example.com/" }, + ]); + await checkDB([ + ["http://", "example.com", ["http://example.com/"]], + ["http://", "www.example.com", ["http://www.example.com/"]], + ["http://", "www.www.example.com", ["http://www.www.example.com/"]], + ]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([ + ["http://", "www.example.com", ["http://www.example.com/"]], + ["http://", "www.www.example.com", ["http://www.www.example.com/"]], + ]); + await PlacesUtils.history.remove("http://www.example.com/"); + await checkDB([ + ["http://", "www.www.example.com", ["http://www.www.example.com/"]], + ]); + await PlacesUtils.history.remove("http://www.www.example.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Same as www1, but removes URIs in a different order. +add_task(async function www2() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + { uri: "http://www.example.com/" }, + { uri: "http://www.www.example.com/" }, + ]); + await checkDB([ + ["http://", "example.com", ["http://example.com/"]], + ["http://", "www.example.com", ["http://www.example.com/"]], + ["http://", "www.www.example.com", ["http://www.www.example.com/"]], + ]); + await PlacesUtils.history.remove("http://www.www.example.com/"); + await checkDB([ + ["http://", "example.com", ["http://example.com/"]], + ["http://", "www.example.com", ["http://www.example.com/"]], + ]); + await PlacesUtils.history.remove("http://www.example.com/"); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Makes sure removing an origin without a port doesn't remove the same host +// with a port. +add_task(async function ports1() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + { uri: "http://example.com:8888/" }, + ]); + await checkDB([ + ["http://", "example.com", ["http://example.com/"]], + ["http://", "example.com:8888", ["http://example.com:8888/"]], + ]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([ + ["http://", "example.com:8888", ["http://example.com:8888/"]], + ]); + await PlacesUtils.history.remove("http://example.com:8888/"); + await checkDB([]); + await cleanUp(); +}); + +// Makes sure removing an origin with a port doesn't remove the same host +// without a port. +add_task(async function ports2() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + { uri: "http://example.com:8888/" }, + ]); + await checkDB([ + ["http://", "example.com", ["http://example.com/"]], + ["http://", "example.com:8888", ["http://example.com:8888/"]], + ]); + await PlacesUtils.history.remove("http://example.com:8888/"); + await checkDB([["http://", "example.com", ["http://example.com/"]]]); + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([]); + await cleanUp(); +}); + +// Makes sure multiple URIs with the same origin don't create duplicate origins. +add_task(async function duplicates() { + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + { uri: "http://www.example.com/" }, + { uri: "http://www.www.example.com/" }, + { uri: "https://example.com/" }, + { uri: "ftp://example.com/" }, + { uri: "foo://example.com/" }, + { uri: "bar:example.com/" }, + { uri: "http://example.com:8888/" }, + + { uri: "http://example.com/dupe" }, + { uri: "http://www.example.com/dupe" }, + { uri: "http://www.www.example.com/dupe" }, + { uri: "https://example.com/dupe" }, + { uri: "ftp://example.com/dupe" }, + { uri: "foo://example.com/dupe" }, + { uri: "bar:example.com/dupe" }, + { uri: "http://example.com:8888/dupe" }, + ]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/", "http://example.com/dupe"], + ], + [ + "http://", + "www.example.com", + ["http://www.example.com/", "http://www.example.com/dupe"], + ], + [ + "http://", + "www.www.example.com", + ["http://www.www.example.com/", "http://www.www.example.com/dupe"], + ], + [ + "https://", + "example.com", + ["https://example.com/", "https://example.com/dupe"], + ], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("http://example.com/"); + await checkDB([ + ["http://", "example.com", ["http://example.com/dupe"]], + [ + "http://", + "www.example.com", + ["http://www.example.com/", "http://www.example.com/dupe"], + ], + [ + "http://", + "www.www.example.com", + ["http://www.www.example.com/", "http://www.www.example.com/dupe"], + ], + [ + "https://", + "example.com", + ["https://example.com/", "https://example.com/dupe"], + ], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + await PlacesUtils.history.remove("http://example.com/dupe"); + await checkDB([ + [ + "http://", + "www.example.com", + ["http://www.example.com/", "http://www.example.com/dupe"], + ], + [ + "http://", + "www.www.example.com", + ["http://www.www.example.com/", "http://www.www.example.com/dupe"], + ], + [ + "https://", + "example.com", + ["https://example.com/", "https://example.com/dupe"], + ], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("http://www.example.com/"); + await checkDB([ + ["http://", "www.example.com", ["http://www.example.com/dupe"]], + [ + "http://", + "www.www.example.com", + ["http://www.www.example.com/", "http://www.www.example.com/dupe"], + ], + [ + "https://", + "example.com", + ["https://example.com/", "https://example.com/dupe"], + ], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + await PlacesUtils.history.remove("http://www.example.com/dupe"); + await checkDB([ + [ + "http://", + "www.www.example.com", + ["http://www.www.example.com/", "http://www.www.example.com/dupe"], + ], + [ + "https://", + "example.com", + ["https://example.com/", "https://example.com/dupe"], + ], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("http://www.www.example.com/"); + await checkDB([ + ["http://", "www.www.example.com", ["http://www.www.example.com/dupe"]], + [ + "https://", + "example.com", + ["https://example.com/", "https://example.com/dupe"], + ], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + await PlacesUtils.history.remove("http://www.www.example.com/dupe"); + await checkDB([ + [ + "https://", + "example.com", + ["https://example.com/", "https://example.com/dupe"], + ], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("https://example.com/"); + await checkDB([ + ["https://", "example.com", ["https://example.com/dupe"]], + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + await PlacesUtils.history.remove("https://example.com/dupe"); + await checkDB([ + ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("ftp://example.com/"); + await checkDB([ + ["ftp://", "example.com", ["ftp://example.com/dupe"]], + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + await PlacesUtils.history.remove("ftp://example.com/dupe"); + await checkDB([ + ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("foo://example.com/"); + await checkDB([ + ["foo://", "example.com", ["foo://example.com/dupe"]], + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + await PlacesUtils.history.remove("foo://example.com/dupe"); + await checkDB([ + ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("bar:example.com/"); + await checkDB([ + ["bar:", "example.com", ["bar:example.com/dupe"]], + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + await PlacesUtils.history.remove("bar:example.com/dupe"); + await checkDB([ + [ + "http://", + "example.com:8888", + ["http://example.com:8888/", "http://example.com:8888/dupe"], + ], + ]); + + await PlacesUtils.history.remove("http://example.com:8888/"); + await checkDB([ + ["http://", "example.com:8888", ["http://example.com:8888/dupe"]], + ]); + await PlacesUtils.history.remove("http://example.com:8888/dupe"); + await checkDB([]); + + await cleanUp(); +}); + +// Makes sure adding and removing bookmarks creates origins. +add_task(async function addRemoveBookmarks() { + let bookmarks = []; + let urls = ["http://example.com/", "http://www.example.com/"]; + for (let url of urls) { + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }) + ); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await checkDB([ + ["http://", "example.com", ["http://example.com/"]], + ["http://", "www.example.com", ["http://www.example.com/"]], + ]); + await PlacesUtils.bookmarks.remove(bookmarks[0]); + await PlacesUtils.history.clear(); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await checkDB([["http://", "www.example.com", ["http://www.example.com/"]]]); + await PlacesUtils.bookmarks.remove(bookmarks[1]); + await PlacesUtils.history.clear(); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await checkDB([]); + await cleanUp(); +}); + +// Makes sure changing bookmarks also changes the corresponding origins. +add_task(async function changeBookmarks() { + let bookmarks = []; + let urls = ["http://example.com/", "http://www.example.com/"]; + for (let url of urls) { + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }) + ); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await checkDB([ + ["http://", "example.com", ["http://example.com/"]], + ["http://", "www.example.com", ["http://www.example.com/"]], + ]); + await PlacesUtils.bookmarks.update({ + url: "http://www.example.com/", + guid: bookmarks[0].guid, + }); + await PlacesUtils.history.clear(); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await checkDB([["http://", "www.example.com", ["http://www.example.com/"]]]); + await cleanUp(); +}); + +// A slightly more complex test to make sure origin frecency stats are updated +// when visits and bookmarks are added and removed. +add_task(async function moreOriginFrecencyStats() { + await checkDB([]); + + // Add a URL 0 visit. + await PlacesTestUtils.addVisits([{ uri: "http://example.com/0" }]); + await checkDB([["http://", "example.com", ["http://example.com/0"]]]); + + // Add a URL 1 visit. + await PlacesTestUtils.addVisits([{ uri: "http://example.com/1" }]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/0", "http://example.com/1"], + ], + ]); + + // Add a URL 2 visit. + await PlacesTestUtils.addVisits([{ uri: "http://example.com/2" }]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/0", "http://example.com/1", "http://example.com/2"], + ], + ]); + + // Add another URL 2 visit. + await PlacesTestUtils.addVisits([{ uri: "http://example.com/2" }]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/0", "http://example.com/1", "http://example.com/2"], + ], + ]); + + // Remove URL 2's visits. + await PlacesUtils.history.remove(["http://example.com/2"]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/0", "http://example.com/1"], + ], + ]); + + // Bookmark URL 1. + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "A bookmark", + url: NetUtil.newURI("http://example.com/1"), + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/0", "http://example.com/1"], + ], + ]); + + // Remove URL 1's visit. + await PlacesUtils.history.remove(["http://example.com/1"]); + await checkDB([ + [ + "http://", + "example.com", + ["http://example.com/0", "http://example.com/1"], + ], + ]); + + // Remove URL 1's bookmark. Also need to call history.remove() again to + // remove the URL from moz_places. Otherwise it sticks around and keeps + // contributing to the frecency stats. + await PlacesUtils.bookmarks.remove(bookmark); + await PlacesUtils.history.remove("http://example.com/1"); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await checkDB([["http://", "example.com", ["http://example.com/0"]]]); + + // Remove URL 0. + await PlacesUtils.history.remove(["http://example.com/0"]); + await checkDB([]); + + await cleanUp(); +}); + +/** + * Returns the expected frecency of the origin of the given URLs, i.e., the sum + * of their frecencies. Each URL is expected to have the same origin. + * + * @param urls + * An array of URL strings. + * @return The expected origin frecency. + */ +async function expectedOriginFrecency(urls) { + let value = 0; + for (let url of urls) { + let v = Math.max( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url }), + 0 + ); + value += v; + } + return value; +} + +/** + * Asserts that the moz_origins table and the origin frecency stats are correct. + * + * @param expectedOrigins + * An array of expected origins. Each origin in the array is itself an + * array that looks like this: + * [prefix, host, [url1, url2, ..., urln]] + * The element at index 2 is an array of all the URLs with the origin. + * If you don't care about checking frecencies and origin frecency stats, + * this element can be `undefined`. + */ +async function checkDB(expectedOrigins) { + // Frencencies for bookmarks are generated asynchronously but not within the + // await cycle for bookmarks.insert() etc, so wait for them to happen. + await PlacesTestUtils.promiseAsyncUpdates(); + + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute(` + SELECT prefix, host, frecency + FROM moz_origins + ORDER BY id ASC + `); + let checkFrecencies = + !expectedOrigins.length || expectedOrigins[0][2] !== undefined; + let actualOrigins = rows.map(row => { + let o = []; + for (let c = 0; c < (checkFrecencies ? 3 : 2); c++) { + o.push(row.getResultByIndex(c)); + } + return o; + }); + let expected = []; + for (let origin of expectedOrigins) { + expected.push( + origin + .slice(0, 2) + .concat(checkFrecencies ? await expectedOriginFrecency(origin[2]) : []) + ); + } + Assert.deepEqual(actualOrigins, expected); + if (checkFrecencies) { + await checkStats(expected.map(o => o[2]).filter(o => o > 0)); + } +} + +/** + * Asserts that the origin frecency stats are correct. + * + * @param expectedOriginFrecencies + * An array of expected origin frecencies. + */ +async function checkStats(expectedOriginFrecencies) { + let stats = await promiseStats(); + Assert.equal(stats.count, expectedOriginFrecencies.length); + Assert.equal( + stats.sum, + expectedOriginFrecencies.reduce((sum, f) => sum + f, 0) + ); + Assert.equal( + stats.squares, + expectedOriginFrecencies.reduce((squares, f) => squares + f * f, 0) + ); +} + +/** + * Returns the origin frecency stats. + * + * @return An object: { count, sum, squares } + */ +async function promiseStats() { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute(` + SELECT + IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0), + IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0), + IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0) + `); + return { + count: rows[0].getResultByIndex(0), + sum: rows[0].getResultByIndex(1), + squares: rows[0].getResultByIndex(2), + }; +} + +async function cleanUp() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/toolkit/components/places/tests/unit/test_origins_parsing.js b/toolkit/components/places/tests/unit/test_origins_parsing.js new file mode 100644 index 0000000000..35ba8bdd0d --- /dev/null +++ b/toolkit/components/places/tests/unit/test_origins_parsing.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test is a companion to test_origins.js. It adds many URLs to the +// database and makes sure that their prefixes and hosts are correctly parsed. +// This test can take a while to run, which is why it's split out from +// test_origins.js. + +"use strict"; + +add_task(async function parsing() { + let prefixes = ["http://", "https://", "ftp://", "foo://", "bar:"]; + + let userinfos = ["", "user:pass@", "user:pass:word@", "user:@"]; + + let ports = ["", ":8888"]; + + let paths = [ + "", + + "/", + "/1", + "/1/2", + + "?", + "?1", + "#", + "#1", + + "/?", + "/1?", + "/?1", + "/1?2", + + "/#", + "/1#", + "/#1", + "/1#2", + + "/?#", + "/1?#", + "/?1#", + "/?#1", + "/1?2#", + "/1?#2", + "/?1#2", + ]; + + for (let userinfo of userinfos) { + for (let port of ports) { + for (let path of paths) { + info(`Testing userinfo='${userinfo}' port='${port}' path='${path}'`); + let expectedOrigins = prefixes.map(prefix => [ + prefix, + "example.com" + port, + ]); + let uris = expectedOrigins.map( + ([prefix, hostPort]) => prefix + userinfo + hostPort + path + ); + + await PlacesTestUtils.addVisits(uris.map(uri => ({ uri }))); + await checkDB(expectedOrigins); + + // Remove each URI, one at a time, and make sure the remaining origins + // in the database are correct. + for (let i = 0; i < uris.length; i++) { + await PlacesUtils.history.remove(uris[i]); + await checkDB(expectedOrigins.slice(i + 1, expectedOrigins.length)); + } + await cleanUp(); + } + } + } + await checkDB([]); +}); + +/** + * Asserts that the moz_origins table is correct. + * + * @param expectedOrigins + * An array of expected origins. Each origin in the array is itself an + * array that looks like this: [prefix, host] + */ +async function checkDB(expectedOrigins) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute(` + SELECT prefix, host + FROM moz_origins + ORDER BY id ASC + `); + let actualOrigins = rows.map(row => { + let o = []; + for (let c = 0; c < 2; c++) { + o.push(row.getResultByIndex(c)); + } + return o; + }); + Assert.deepEqual(actualOrigins, expectedOrigins); +} + +async function cleanUp() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js new file mode 100644 index 0000000000..e1fa64a88c --- /dev/null +++ b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js @@ -0,0 +1,256 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const histsvc = PlacesUtils.history; + +add_task(async function test_addBookmarksAndCheckGuids() { + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + url: "http://test1.com/", + title: "1 title", + }, + { + url: "http://test2.com/", + title: "2 title", + }, + { + url: "http://test3.com/", + title: "3 title", + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { + title: "test folder 2", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }, + ], + }); + + let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root; + Assert.equal(root.childCount, 5); + + // check bookmark guids + let bookmarkGuidZero = root.getChild(0).bookmarkGuid; + Assert.equal(bookmarkGuidZero.length, 12); + // bookmarks have bookmark guids + Assert.equal(root.getChild(1).bookmarkGuid.length, 12); + Assert.equal(root.getChild(2).bookmarkGuid.length, 12); + // separator has bookmark guid + Assert.equal(root.getChild(3).bookmarkGuid.length, 12); + // folder has bookmark guid + Assert.equal(root.getChild(4).bookmarkGuid.length, 12); + // all bookmark guids are different. + Assert.notEqual(bookmarkGuidZero, root.getChild(1).bookmarkGuid); + Assert.notEqual(root.getChild(1).bookmarkGuid, root.getChild(2).bookmarkGuid); + Assert.notEqual(root.getChild(2).bookmarkGuid, root.getChild(3).bookmarkGuid); + Assert.notEqual(root.getChild(3).bookmarkGuid, root.getChild(4).bookmarkGuid); + + // check page guids + let pageGuidZero = root.getChild(0).pageGuid; + Assert.equal(pageGuidZero.length, 12); + // bookmarks have page guids + Assert.equal(root.getChild(1).pageGuid.length, 12); + Assert.equal(root.getChild(2).pageGuid.length, 12); + // folder and separator don't have page guids + Assert.equal(root.getChild(3).pageGuid, ""); + Assert.equal(root.getChild(4).pageGuid, ""); + + Assert.notEqual(pageGuidZero, root.getChild(1).pageGuid); + Assert.notEqual(root.getChild(1).pageGuid, root.getChild(2).pageGuid); + + root.containerOpen = false; + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_updateBookmarksAndCheckGuids() { + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + url: "http://test1.com/", + title: "1 title", + }, + { + title: "test folder 2", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }, + ], + }); + + let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root; + Assert.equal(root.childCount, 2); + + // ensure the bookmark and page guids remain the same after modifing other property. + let bookmarkGuidZero = root.getChild(0).bookmarkGuid; + let pageGuidZero = root.getChild(0).pageGuid; + await PlacesUtils.bookmarks.update({ + guid: bookmarks[1].guid, + title: "1 title mod", + }); + Assert.equal(root.getChild(0).title, "1 title mod"); + Assert.equal(root.getChild(0).bookmarkGuid, bookmarkGuidZero); + Assert.equal(root.getChild(0).pageGuid, pageGuidZero); + + let bookmarkGuidOne = root.getChild(1).bookmarkGuid; + let pageGuidOne = root.getChild(1).pageGuid; + + await PlacesUtils.bookmarks.update({ + guid: bookmarks[2].guid, + title: "test foolder 234", + }); + Assert.equal(root.getChild(1).title, "test foolder 234"); + Assert.equal(root.getChild(1).bookmarkGuid, bookmarkGuidOne); + Assert.equal(root.getChild(1).pageGuid, pageGuidOne); + + root.containerOpen = false; + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_addVisitAndCheckGuid() { + // add a visit and test page guid and non-existing bookmark guids. + let sourceURI = uri("http://test4.com/"); + await PlacesTestUtils.addVisits({ uri: sourceURI }); + Assert.equal(await PlacesUtils.bookmarks.fetch({ url: sourceURI }, null)); + + let options = histsvc.getNewQueryOptions(); + let query = histsvc.getNewQuery(); + query.uri = sourceURI; + let root = histsvc.executeQuery(query, options).root; + root.containerOpen = true; + Assert.equal(root.childCount, 1); + + do_check_valid_places_guid(root.getChild(0).pageGuid); + Assert.equal(root.getChild(0).bookmarkGuid, ""); + root.containerOpen = false; + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_addItemsWithInvalidGUIDsFails() { + const INVALID_GUID = "XYZ"; + try { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + guid: INVALID_GUID, + title: "XYZ folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + do_throw("Adding a folder with an invalid guid should fail"); + } catch (ex) {} + + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + try { + PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + guid: INVALID_GUID, + title: "title", + url: "http://test.tld", + }); + do_throw("Adding a bookmark with an invalid guid should fail"); + } catch (ex) {} + + try { + PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + guid: INVALID_GUID, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + do_throw("Adding a separator with an invalid guid should fail"); + } catch (ex) {} + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_addItemsWithGUIDs() { + const FOLDER_GUID = "FOLDER--GUID"; + const BOOKMARK_GUID = "BM------GUID"; + const SEPARATOR_GUID = "SEP-----GUID"; + + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + guid: FOLDER_GUID, + children: [ + { + url: "http://test1.com", + title: "1 title", + guid: BOOKMARK_GUID, + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + guid: SEPARATOR_GUID, + }, + ], + }, + ], + }); + + let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root; + Assert.equal(root.childCount, 2); + Assert.equal(root.bookmarkGuid, FOLDER_GUID); + Assert.equal(root.getChild(0).bookmarkGuid, BOOKMARK_GUID); + Assert.equal(root.getChild(1).bookmarkGuid, SEPARATOR_GUID); + + root.containerOpen = false; + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_emptyGUIDFails() { + try { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + guid: "", + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + do_throw("Adding a folder with an empty guid should fail"); + } catch (ex) {} +}); + +add_task(async function test_usingSameGUIDFails() { + const GUID = "XYZXYZXYZXYZ"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + guid: GUID, + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + try { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + guid: GUID, + title: "test folder 2", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + do_throw("Using the same guid twice should fail"); + } catch (ex) {} + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_placeURIs.js b/toolkit/components/places/tests/unit/test_placeURIs.js new file mode 100644 index 0000000000..093dc89f68 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_placeURIs.js @@ -0,0 +1,18 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + // TODO: Improve testing coverage for QueryToQueryString and QueryStringToQuery + + // Bug 376798 + let query = PlacesUtils.history.getNewQuery(); + let options = PlacesUtils.history.getNewQueryOptions(); + query.setParents([PlacesUtils.bookmarks.rootGuid]); + Assert.equal( + PlacesUtils.history.queryToQueryString(query, options), + `place:parent=${PlacesUtils.bookmarks.rootGuid}` + ); +} diff --git a/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js new file mode 100644 index 0000000000..9824908cef --- /dev/null +++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js @@ -0,0 +1,282 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +async function check_has_child(aParentGuid, aChildGuid) { + let parentTree = await PlacesUtils.promiseBookmarksTree(aParentGuid); + Assert.ok("children" in parentTree); + Assert.notEqual( + parentTree.children.find(e => e.guid == aChildGuid), + null + ); +} + +async function compareToNode(aItem, aNode, aIsRootItem, aExcludedGuids = []) { + // itemId==-1 indicates a non-bookmark node, which is unexpected. + Assert.notEqual(aNode.itemId, -1); + + function check_unset(...aProps) { + for (let p of aProps) { + if (p in aItem) { + Assert.ok(false, `Unexpected property ${p} with value ${aItem[p]}`); + } + } + } + + function compare_prop(aItemProp, aNodeProp = aItemProp, aOptional = false) { + if (aOptional && aNode[aNodeProp] === null) { + check_unset(aItemProp); + } else { + Assert.strictEqual(aItem[aItemProp], aNode[aNodeProp]); + } + } + + // Bug 1013053 - bookmarkIndex is unavailable for the query's root + if (aNode.bookmarkIndex == -1) { + let bookmark = await PlacesUtils.bookmarks.fetch(aNode.bookmarkGuid); + Assert.strictEqual(aItem.index, bookmark.index); + } else { + compare_prop("index", "bookmarkIndex"); + } + + compare_prop("dateAdded"); + compare_prop("lastModified"); + + if (aIsRootItem && aNode.bookmarkGuid != PlacesUtils.bookmarks.rootGuid) { + Assert.ok("parentGuid" in aItem); + await check_has_child(aItem.parentGuid, aItem.guid); + } else { + check_unset("parentGuid"); + } + + const BOOKMARK_ONLY_PROPS = ["uri", "iconUri", "tags", "charset", "keyword"]; + const FOLDER_ONLY_PROPS = ["children", "root"]; + + let nodesCount = 1; + + switch (aNode.type) { + case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER: + Assert.equal(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER); + Assert.equal(aItem.typeCode, PlacesUtils.bookmarks.TYPE_FOLDER); + compare_prop("title", "title", true); + check_unset(...BOOKMARK_ONLY_PROPS); + + let expectedChildrenNodes = []; + + PlacesUtils.asContainer(aNode); + if (!aNode.containerOpen) { + aNode.containerOpen = true; + } + + for (let i = 0; i < aNode.childCount; i++) { + let childNode = aNode.getChild(i); + if ( + childNode.itemId == PlacesUtils.tagsFolderId || + aExcludedGuids.includes(childNode.bookmarkGuid) + ) { + continue; + } + expectedChildrenNodes.push(childNode); + } + + if (expectedChildrenNodes.length) { + Assert.ok(Array.isArray(aItem.children)); + Assert.equal(aItem.children.length, expectedChildrenNodes.length); + for (let i = 0; i < aItem.children.length; i++) { + nodesCount += await compareToNode( + aItem.children[i], + expectedChildrenNodes[i], + false, + aExcludedGuids + ); + } + } else { + check_unset("children"); + } + + let rootName = mapItemGuidToInternalRootName(aItem.guid); + if (rootName) { + Assert.equal(aItem.root, rootName); + } else { + check_unset("root"); + } + break; + case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR: + Assert.equal(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR); + Assert.equal(aItem.typeCode, PlacesUtils.bookmarks.TYPE_SEPARATOR); + check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS); + break; + default: + Assert.equal(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE); + Assert.equal(aItem.typeCode, PlacesUtils.bookmarks.TYPE_BOOKMARK); + compare_prop("uri"); + // node.tags's format is "a, b" whilst promiseBoookmarksTree is "a,b" + if (aNode.tags === null) { + check_unset("tags"); + } else { + Assert.equal(aItem.tags, aNode.tags.replace(/, /g, ",")); + } + + if (aNode.icon) { + try { + await compareFavicons(aNode.icon, aItem.iconUri); + } catch (ex) { + info(ex); + todo_check_true(false); + } + } else { + check_unset(aItem.iconUri); + } + + check_unset(...FOLDER_ONLY_PROPS); + + let pageInfo = await PlacesUtils.history.fetch(aNode.uri, { + includeAnnotations: true, + }); + let expectedCharset = pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO); + if (expectedCharset) { + Assert.equal(aItem.charset, expectedCharset); + } else { + check_unset("charset"); + } + + let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri }); + if (entry) { + Assert.equal(aItem.keyword, entry.keyword); + } else { + check_unset("keyword"); + } + + if ("title" in aItem) { + compare_prop("title"); + } else { + Assert.equal(null, aNode.title); + } + } + + if (aIsRootItem) { + Assert.strictEqual(aItem.itemsCount, nodesCount); + } + + return nodesCount; +} + +var itemsCount = 0; +async function new_bookmark(aInfo) { + ++itemsCount; + if (!("url" in aInfo)) { + aInfo.url = uri("http://test.item." + itemsCount); + } + + if (!("title" in aInfo)) { + aInfo.title = "Test Item (bookmark) " + itemsCount; + } + + await PlacesTransactions.NewBookmark(aInfo).transact(); +} + +function new_folder(aInfo) { + if (!("title" in aInfo)) { + aInfo.title = "Test Item (folder) " + itemsCount; + } + return PlacesTransactions.NewFolder(aInfo).transact(); +} + +// Walks a result nodes tree and test promiseBookmarksTree for each node. +// DO NOT COPY THIS LOGIC: It is done here to accomplish a more comprehensive +// test of the API (the entire hierarchy data is available in the very test). +async function test_promiseBookmarksTreeForEachNode( + aNode, + aOptions, + aExcludedGuids +) { + Assert.ok(aNode.bookmarkGuid && !!aNode.bookmarkGuid.length); + let item = await PlacesUtils.promiseBookmarksTree( + aNode.bookmarkGuid, + aOptions + ); + await compareToNode(item, aNode, true, aExcludedGuids); + + if (!PlacesUtils.nodeIsContainer(aNode)) { + return item; + } + + for (let i = 0; i < aNode.childCount; i++) { + let child = aNode.getChild(i); + if (child.itemId != PlacesUtils.tagsFolderId) { + await test_promiseBookmarksTreeForEachNode( + child, + { includeItemIds: true }, + aExcludedGuids + ); + } + } + return item; +} + +async function test_promiseBookmarksTreeAgainstResult( + aItemGuid = PlacesUtils.bookmarks.rootGuid, + aOptions = { includeItemIds: true }, + aExcludedGuids +) { + let node = PlacesUtils.getFolderContents(aItemGuid).root; + return test_promiseBookmarksTreeForEachNode(node, aOptions, aExcludedGuids); +} + +add_task(async function () { + // Add some bookmarks to cover various use cases. + await new_bookmark({ parentGuid: PlacesUtils.bookmarks.toolbarGuid }); + await new_folder({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + annotations: [ + { name: "TestAnnoA", value: "TestVal" }, + { name: "TestAnnoB", value: 0 }, + ], + }); + let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid }; + await PlacesTransactions.NewSeparator(sepInfo).transact(); + let folderGuid = await new_folder({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + await new_bookmark({ + title: null, + parentGuid: folderGuid, + keyword: "test_keyword", + tags: ["TestTagA", "TestTagB"], + annotations: [{ name: "TestAnnoA", value: "TestVal2" }], + }); + let urlWithCharsetAndFavicon = uri("http://charset.and.favicon"); + await new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon }); + await PlacesUtils.history.update({ + url: urlWithCharsetAndFavicon, + annotations: new Map([[PlacesUtils.CHARSET_ANNO, "UTF-16"]]), + }); + await setFaviconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI); + // Test the default places root without specifying it. + await test_promiseBookmarksTreeAgainstResult(); + + // Do specify it + await test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid); + + // Exclude the bookmarks menu. + // The calllback should be four times - once for the toolbar, once for + // the bookmark we inserted under, and once for the menu (and not + // at all for any of its descendants) and once for the unsorted bookmarks + // folder. However, promiseBookmarksTree is called multiple times, so + // rather than counting the calls, we count the number of unique items + // passed in. + let guidsPassedToExcludeCallback = new Set(); + let placesRootWithoutTheMenu = await test_promiseBookmarksTreeAgainstResult( + PlacesUtils.bookmarks.rootGuid, + { + excludeItemsCallback: aItem => { + guidsPassedToExcludeCallback.add(aItem.guid); + return aItem.root == "bookmarksMenuFolder"; + }, + includeItemIds: true, + }, + [PlacesUtils.bookmarks.menuGuid] + ); + Assert.equal(guidsPassedToExcludeCallback.size, 5); + Assert.equal(placesRootWithoutTheMenu.children.length, 3); +}); diff --git a/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js new file mode 100644 index 0000000000..d8830c5686 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function () { + let uri1 = uri("http://foo.tld/"); + let uri2 = uri("https://bar.tld/"); + + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "foo title" }, + { uri: uri2, title: "bar title" }, + ]); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: uri1, + title: null, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: uri2, + title: null, + }); + + PlacesUtils.tagging.tagURI(uri1, ["tag 1"]); + PlacesUtils.tagging.tagURI(uri2, ["tag 2"]); +}); + +add_task(async function testTagQuery() { + let options = PlacesUtils.history.getNewQueryOptions(); + let query = PlacesUtils.history.getNewQuery(); + query.tags = ["tag 1"]; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + Assert.equal(root.childCount, 1); + Assert.equal(root.getChild(0).title, ""); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_result_sort.js b/toolkit/components/places/tests/unit/test_result_sort.js new file mode 100644 index 0000000000..840b40c339 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_result_sort.js @@ -0,0 +1,112 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const NHQO = Ci.nsINavHistoryQueryOptions; + +add_task(async function test() { + const uri1 = "http://foo.tld/a"; + const uri2 = "http://foo.tld/b"; + + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "Result-sort functionality tests root", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "b", + url: uri1, + }, + { + title: "a", + url: uri2, + }, + { + // url of the first child, title of second + title: "a", + url: uri1, + }, + ], + }, + ], + }); + + let guid1 = bookmarks[1].guid; + let guid2 = bookmarks[2].guid; + let guid3 = bookmarks[3].guid; + + // query with natural order + let result = PlacesUtils.getFolderContents(bookmarks[0].guid); + let root = result.root; + + Assert.equal(root.childCount, 3); + + function checkOrder(a, b, c) { + Assert.equal(root.getChild(0).bookmarkGuid, a); + Assert.equal(root.getChild(1).bookmarkGuid, b); + Assert.equal(root.getChild(2).bookmarkGuid, c); + } + + // natural order + info("Natural order"); + checkOrder(guid1, guid2, guid3); + + // title: guid3 should precede guid2 since we fall-back to URI-based sorting + info("Sort by title asc"); + result.sortingMode = NHQO.SORT_BY_TITLE_ASCENDING; + checkOrder(guid3, guid2, guid1); + + // In reverse + info("Sort by title desc"); + result.sortingMode = NHQO.SORT_BY_TITLE_DESCENDING; + checkOrder(guid1, guid2, guid3); + + // uri sort: guid1 should precede guid3 since we fall-back to natural order + info("Sort by uri asc"); + result.sortingMode = NHQO.SORT_BY_URI_ASCENDING; + checkOrder(guid1, guid3, guid2); + + // test live update + info("Change bookmark uri liveupdate"); + await PlacesUtils.bookmarks.update({ + guid: guid1, + url: uri2, + }); + checkOrder(guid3, guid1, guid2); + await PlacesUtils.bookmarks.update({ + guid: guid1, + url: uri1, + }); + checkOrder(guid1, guid3, guid2); + + // XXXtodo: test history sortings (visit count, visit date) + // XXXtodo: test different item types once folderId and bookmarkId are merged. + + // XXXtodo: test dateAdded sort + // XXXtodo: test lastModified sort + + // Add a visit, then check frecency ordering. + + await PlacesTestUtils.addVisits({ uri: uri2, transition: TRANSITION_TYPED }); + + info("Sort by frecency desc"); + result.sortingMode = NHQO.SORT_BY_FRECENCY_DESCENDING; + for (let i = 0; i < root.childCount; ++i) { + print(root.getChild(i).uri + " " + root.getChild(i).title); + } + // For guid1 and guid3, since they have same frecency and no visits, fallback + // to sort by the newest bookmark. + checkOrder(guid2, guid3, guid1); + info("Sort by frecency asc"); + result.sortingMode = NHQO.SORT_BY_FRECENCY_ASCENDING; + for (let i = 0; i < root.childCount; ++i) { + print(root.getChild(i).uri + " " + root.getChild(i).title); + } + checkOrder(guid1, guid3, guid2); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js new file mode 100644 index 0000000000..2291eddeff --- /dev/null +++ b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js @@ -0,0 +1,106 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +const { history } = PlacesUtils; + +add_task(async function test_addVisitCheckFields() { + let uri = NetUtil.newURI("http://test4.com/"); + await PlacesTestUtils.addVisits([ + { uri }, + { uri, referrer: uri }, + { uri, transition: history.TRANSITION_TYPED }, + ]); + + let options = history.getNewQueryOptions(); + let query = history.getNewQuery(); + + query.uri = uri; + + // Check RESULTS_AS_VISIT node. + options.resultType = options.RESULTS_AS_VISIT; + + let root = history.executeQuery(query, options).root; + root.containerOpen = true; + + equal(root.childCount, 3); + + let child = root.getChild(0); + equal( + child.visitType, + history.TRANSITION_LINK, + "Visit type should be TRANSITION_LINK" + ); + equal(child.visitId, 1, "Visit ID should be 1"); + equal(child.fromVisitId, -1, "Should have no referrer visit ID"); + + child = root.getChild(1); + equal( + child.visitType, + history.TRANSITION_LINK, + "Visit type should be TRANSITION_LINK" + ); + equal(child.visitId, 2, "Visit ID should be 2"); + equal(child.fromVisitId, 1, "First visit should be the referring visit"); + + child = root.getChild(2); + equal( + child.visitType, + history.TRANSITION_TYPED, + "Visit type should be TRANSITION_TYPED" + ); + equal(child.visitId, 3, "Visit ID should be 3"); + equal(child.fromVisitId, -1, "Should have no referrer visit ID"); + + root.containerOpen = false; + + // Check RESULTS_AS_URI node. + options.resultType = options.RESULTS_AS_URI; + + root = history.executeQuery(query, options).root; + root.containerOpen = true; + + equal(root.childCount, 1); + + child = root.getChild(0); + equal(child.visitType, 0, "Visit type should be 0"); + equal(child.visitId, -1, "Visit ID should be -1"); + equal(child.fromVisitId, -1, "Referrer visit id should be -1"); + + root.containerOpen = false; + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_bookmarkFields() { + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "test folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "test title", + url: "http://test4.com", + }, + ], + }, + ], + }); + + let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root; + equal(root.childCount, 1); + + equal(root.visitType, 0, "Visit type should be 0"); + equal(root.visitId, -1, "Visit ID should be -1"); + equal(root.fromVisitId, -1, "Referrer visit id should be -1"); + + let child = root.getChild(0); + equal(child.visitType, 0, "Visit type should be 0"); + equal(child.visitId, -1, "Visit ID should be -1"); + equal(child.fromVisitId, -1, "Referrer visit id should be -1"); + + root.containerOpen = false; + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_sql_function_origin.js b/toolkit/components/places/tests/unit/test_sql_function_origin.js new file mode 100644 index 0000000000..0314ff5040 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_sql_function_origin.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the origin-related SQL functions, which are: +// * get_host_and_port +// * get_prefix +// * strip_prefix_and_userinfo + +// Tests actual URL strings. +add_task(async function urls() { + let sets = [ + ["http:"], + ["", "//"], + ["", "user@", "user:@", "user:pass@", "user:pass:word@"], + ["example.com"], + ["", ":8888"], + ["", "/", "/foo"], + ["", "?", "?bar"], + ["", "#", "#baz"], + ]; + let db = await PlacesUtils.promiseDBConnection(); + for (let parts of permute(sets)) { + let spec = parts.join(""); + let funcs = { + get_prefix: parts.slice(0, 2).join(""), + get_host_and_port: parts.slice(3, 5).join(""), + strip_prefix_and_userinfo: parts.slice(3).join(""), + }; + for (let [func, expectedValue] of Object.entries(funcs)) { + let rows = await db.execute(` + SELECT ${func}("${spec}"); + `); + let value = rows[0].getString(0); + Assert.equal(value, expectedValue, `function=${func} spec="${spec}"`); + } + } +}); + +// Tests strings that aren't URLs. +add_task(async function nonURLs() { + let db = await PlacesUtils.promiseDBConnection(); + + let value = ( + await db.execute(` + SELECT get_prefix("hello"); + `) + )[0].getString(0); + Assert.equal(value, ""); + + value = ( + await db.execute(` + SELECT get_host_and_port("hello"); + `) + )[0].getString(0); + Assert.equal(value, "hello"); + + value = ( + await db.execute(` + SELECT strip_prefix_and_userinfo("hello"); + `) + )[0].getString(0); + Assert.equal(value, "hello"); +}); + +function permute(sets = []) { + if (!sets.length) { + return [[]]; + } + let firstSet = sets[0]; + let otherSets = sets.slice(1); + let permutedSequences = []; + let otherPermutedSequences = permute(otherSets); + for (let other of otherPermutedSequences) { + for (let value of firstSet) { + permutedSequences.push([value].concat(other)); + } + } + return permutedSequences; +} diff --git a/toolkit/components/places/tests/unit/test_sql_guid_functions.js b/toolkit/components/places/tests/unit/test_sql_guid_functions.js new file mode 100644 index 0000000000..b4fbdac612 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_sql_guid_functions.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests that the guid function generates a guid of the proper length, + * with no invalid characters. + */ + +/** + * Checks all our invariants about our guids for a given result. + * + * @param aGuid + * The guid to check. + */ +function check_invariants(aGuid) { + info("Checking guid '" + aGuid + "'"); + + do_check_valid_places_guid(aGuid); +} + +// Test Functions + +function test_guid_invariants() { + const kExpectedChars = 64; + const kAllowedChars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + Assert.equal(kAllowedChars.length, kExpectedChars); + const kGuidLength = 12; + + let checkedChars = []; + for (let i = 0; i < kGuidLength; i++) { + checkedChars[i] = {}; + for (let j = 0; j < kAllowedChars; j++) { + checkedChars[i][kAllowedChars[j]] = false; + } + } + + // We run this until we've seen every character that we expect to see in every + // position. + let seenChars = 0; + let stmt = DBConn().createStatement("SELECT GENERATE_GUID()"); + while (seenChars != kExpectedChars * kGuidLength) { + Assert.ok(stmt.executeStep()); + let guid = stmt.getString(0); + check_invariants(guid); + + for (let i = 0; i < guid.length; i++) { + let character = guid[i]; + if (!checkedChars[i][character]) { + checkedChars[i][character] = true; + seenChars++; + } + } + stmt.reset(); + } + stmt.finalize(); + + // One last reality check - make sure all of our characters were seen. + for (let i = 0; i < kGuidLength; i++) { + for (let j = 0; j < kAllowedChars; j++) { + Assert.ok(checkedChars[i][kAllowedChars[j]]); + } + } + + run_next_test(); +} + +function test_guid_on_background() { + // We should not assert if we execute this asynchronously. + let stmt = DBConn().createAsyncStatement("SELECT GENERATE_GUID()"); + let checked = false; + stmt.executeAsync({ + handleResult(aResult) { + try { + let row = aResult.getNextRow(); + check_invariants(row.getResultByIndex(0)); + Assert.equal(aResult.getNextRow(), null); + checked = true; + } catch (e) { + do_throw(e); + } + }, + handleCompletion(aReason) { + Assert.equal(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED); + Assert.ok(checked); + run_next_test(); + }, + }); + stmt.finalize(); +} + +// Test Runner + +[test_guid_invariants, test_guid_on_background].forEach(fn => add_test(fn)); diff --git a/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js new file mode 100644 index 0000000000..43f899c237 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js @@ -0,0 +1,119 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt(aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin() {}, + onSearchComplete() {}, + + popupOpen: false, + + popup: { + setSelectedIndex(aIndex) {}, + invalidate() {}, + + // nsISupports implementation + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompletePopup"]), + }, + + // nsISupports implementation + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteInput"]), +}; + +async function ensure_tag_results(results, searchTerm) { + var controller = Cc["@mozilla.org/autocomplete/controller;1"].getService( + Ci.nsIAutoCompleteController + ); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["places-tag-autocomplete"]); + + controller.input = input; + + return new Promise(resolve => { + var numSearchesStarted = 0; + input.onSearchBegin = function input_onSearchBegin() { + numSearchesStarted++; + Assert.equal(numSearchesStarted, 1); + }; + + input.onSearchComplete = function input_onSearchComplete() { + Assert.equal(numSearchesStarted, 1); + if (results.length) { + Assert.equal( + controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH + ); + } else { + Assert.equal( + controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH + ); + } + + Assert.equal(controller.matchCount, results.length); + for (var i = 0; i < controller.matchCount; i++) { + Assert.equal(controller.getValueAt(i), results[i]); + } + + resolve(); + }; + + controller.startSearch(searchTerm); + }); +} + +var uri1 = uri("http://site.tld/1"); + +var tests = [ + () => ensure_tag_results(["bar", "Baz", "boo"], "b"), + () => ensure_tag_results(["bar", "Baz"], "ba"), + () => ensure_tag_results(["bar", "Baz"], "Ba"), + () => ensure_tag_results(["bar"], "bar"), + () => ensure_tag_results(["Baz"], "Baz"), + () => ensure_tag_results([], "barb"), + () => ensure_tag_results([], "foo"), + () => + ensure_tag_results(["first tag, bar", "first tag, Baz"], "first tag, ba"), + () => + ensure_tag_results( + ["first tag; bar", "first tag; Baz"], + "first tag; ba" + ), +]; + +/** + * Test tag autocomplete + */ +add_task(async function test_tag_autocomplete() { + PlacesUtils.tagging.tagURI(uri1, ["bar", "Baz", "boo", "*nix"]); + + for (let tagTest of tests) { + await tagTest(); + } +}); diff --git a/toolkit/components/places/tests/unit/test_tagging.js b/toolkit/components/places/tests/unit/test_tagging.js new file mode 100644 index 0000000000..f38db0d0f6 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_tagging.js @@ -0,0 +1,188 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Notice we use createInstance because later we will have to terminate the +// service and restart it. +var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"] + .createInstance() + .QueryInterface(Ci.nsITaggingService); + +function run_test() { + var options = PlacesUtils.history.getNewQueryOptions(); + var query = PlacesUtils.history.getNewQuery(); + + query.setParents([PlacesUtils.bookmarks.tagsGuid]); + var result = PlacesUtils.history.executeQuery(query, options); + var tagRoot = result.root; + tagRoot.containerOpen = true; + + Assert.equal(tagRoot.childCount, 0); + + var uri1 = uri("http://foo.tld/"); + var uri2 = uri("https://bar.tld/"); + + // this also tests that the multiple folders are not created for the same tag + tagssvc.tagURI(uri1, ["tag 1"]); + tagssvc.tagURI(uri2, ["tag 1"]); + Assert.equal(tagRoot.childCount, 1); + + var tag1node = tagRoot + .getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + var tag1itemId = tag1node.itemId; + + Assert.equal(tag1node.title, "tag 1"); + tag1node.containerOpen = true; + Assert.equal(tag1node.childCount, 2); + + // Tagging the same url twice (or even thrice!) with the same tag should be a + // no-op + tagssvc.tagURI(uri1, ["tag 1"]); + Assert.equal(tag1node.childCount, 2); + tagssvc.tagURI(uri1, [tag1itemId]); + Assert.equal(tag1node.childCount, 2); + Assert.equal(tagRoot.childCount, 1); + + // also tests bug 407575 + tagssvc.tagURI(uri1, [tag1itemId, "tag 1", "tag 2", "Tag 1", "Tag 2"]); + Assert.equal(tagRoot.childCount, 2); + Assert.equal(tag1node.childCount, 2); + + // test getTagsForURI + var uri1tags = tagssvc.getTagsForURI(uri1); + Assert.equal(uri1tags.length, 2); + Assert.equal(uri1tags[0], "Tag 1"); + Assert.equal(uri1tags[1], "Tag 2"); + var uri2tags = tagssvc.getTagsForURI(uri2); + Assert.equal(uri2tags.length, 1); + Assert.equal(uri2tags[0], "Tag 1"); + + // test untagging + tagssvc.untagURI(uri1, ["tag 1"]); + Assert.equal(tag1node.childCount, 1); + + let wait = PlacesTestUtils.waitForNotification("bookmark-removed", events => + events.some(event => event.id == tagRoot.itemId) + ); + // removing the last uri from a tag should remove the tag-container + tagssvc.untagURI(uri2, ["tag 1"]); + wait.then(() => { + Assert.equal(tagRoot.childCount, 1); + }); + + // cleanup + tag1node.containerOpen = false; + + // get array of tag folder ids => title + // for testing tagging with mixed folder ids and tags + var child = tagRoot.getChild(0); + var tagId = child.itemId; + var tagTitle = child.title; + + // test mixed id/name tagging + // as well as non-id numeric tags + var uri3 = uri("http://testuri/3"); + tagssvc.tagURI(uri3, [tagId, "tag 3", "456"]); + var tags = tagssvc.getTagsForURI(uri3); + Assert.ok(tags.includes(tagTitle)); + Assert.ok(tags.includes("tag 3")); + Assert.ok(tags.includes("456")); + + // test mixed id/name tagging + tagssvc.untagURI(uri3, [tagId, "tag 3", "456"]); + tags = tagssvc.getTagsForURI(uri3); + Assert.equal(tags.length, 0); + + // Terminate tagging service, fire up a new instance and check that existing + // tags are there. This will ensure that any internal caching system is + // correctly filled at startup and we are not losing previously existing tags. + var uri4 = uri("http://testuri/4"); + tagssvc.tagURI(uri4, [tagId, "tag 3", "456"]); + tagssvc = null; + tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService( + Ci.nsITaggingService + ); + var uri4Tags = tagssvc.getTagsForURI(uri4); + Assert.equal(uri4Tags.length, 3); + Assert.ok(uri4Tags.includes(tagTitle)); + Assert.ok(uri4Tags.includes("tag 3")); + Assert.ok(uri4Tags.includes("456")); + + // Test sparse arrays. + let curChildCount = tagRoot.childCount; + + try { + tagssvc.tagURI(uri1, [undefined, "tagSparse"]); + Assert.equal(tagRoot.childCount, curChildCount + 1); + } catch (ex) { + do_throw("Passing a sparse array should not throw"); + } + try { + wait = PlacesTestUtils.waitForNotification("bookmark-removed", events => + events.some(event => event.id == tagRoot.itemId) + ); + tagssvc.untagURI(uri1, [undefined, "tagSparse"]); + wait.then(() => { + Assert.equal(tagRoot.childCount, curChildCount); + }); + } catch (ex) { + do_throw("Passing a sparse array should not throw"); + } + + // Test that the API throws for bad arguments. + try { + tagssvc.tagURI(uri1, ["", "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + try { + tagssvc.untagURI(uri1, ["", "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + try { + tagssvc.tagURI(uri1, [0, "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + try { + tagssvc.tagURI(uri1, [0, "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + + // Tag name length should be limited to PlacesUtils.bookmarks.MAX_TAG_LENGTH (bug407821) + try { + // generate a long tag name. i.e. looooo...oong_tag + var n = PlacesUtils.bookmarks.MAX_TAG_LENGTH; + var someOos = new Array(n).join("o"); + var longTagName = "l" + someOos + "ng_tag"; + + tagssvc.tagURI(uri1, ["short_tag", longTagName]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + + // cleanup + tagRoot.containerOpen = false; + + // Tagging service should trim tags (Bug967196) + let exampleURI = uri("http://www.example.com/"); + PlacesUtils.tagging.tagURI(exampleURI, [" test "]); + + let exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI); + Assert.equal(exampleTags.length, 1); + Assert.equal(exampleTags[0], "test"); + + PlacesUtils.tagging.untagURI(exampleURI, ["test"]); + exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI); + Assert.equal(exampleTags.length, 0); +} diff --git a/toolkit/components/places/tests/unit/test_telemetry.js b/toolkit/components/places/tests/unit/test_telemetry.js new file mode 100644 index 0000000000..b48bb76a7f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_telemetry.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests common Places telemetry probes by faking the telemetry service. + +// Enable the collection (during test) for all products so even products +// that don't collect the data will be able to run the test without failure. +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +const { PlacesDBUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesDBUtils.sys.mjs" +); + +const histograms = { + PLACES_PAGES_COUNT: val => Assert.equal(val, 1), + PLACES_BOOKMARKS_COUNT: val => Assert.equal(val, 1), + PLACES_TAGS_COUNT: val => Assert.equal(val, 1), + PLACES_KEYWORDS_COUNT: val => Assert.equal(val, 1), + PLACES_SORTED_BOOKMARKS_PERC: val => Assert.equal(val, 100), + PLACES_TAGGED_BOOKMARKS_PERC: val => Assert.equal(val, 100), + PLACES_DATABASE_FILESIZE_MB: val => Assert.ok(val > 0), + PLACES_DATABASE_FAVICONS_FILESIZE_MB: val => Assert.ok(val > 0), + PLACES_EXPIRATION_STEPS_TO_CLEAN2: val => Assert.ok(val > 1), + PLACES_IDLE_MAINTENANCE_TIME_MS: val => Assert.ok(val > 0), + PLACES_ANNOS_PAGES_COUNT: val => Assert.equal(val, 1), + PLACES_MAINTENANCE_DAYSFROMLAST: val => Assert.ok(val >= 0), +}; + +const scalars = { + pages_need_frecency_recalculation: 1, // 1 bookmark is added causing recalc. +}; + +/** + * Forces an expiration run. + * + * @param [optional] aLimit + * Limit for the expiration. Pass -1 for unlimited. + * Any other non-positive value will just expire orphans. + * + * @return {Promise} + * @resolves When expiration finishes. + * @rejects Never. + */ +function promiseForceExpirationStep(aLimit) { + let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); + let expire = Cc["@mozilla.org/places/expiration;1"].getService( + Ci.nsIObserver + ); + expire.observe(null, "places-debug-start-expiration", aLimit); + return promise; +} + +/** + * Returns a PRTime in the past usable to add expirable visits. + * + * param [optional] daysAgo + * Expiration ignores any visit added in the last 7 days, so by default + * this will be set to 7. + * @note to be safe against DST issues we go back one day more. + */ +function getExpirablePRTime(daysAgo = 7) { + let dateObj = new Date(); + // Normalize to midnight + dateObj.setHours(0); + dateObj.setMinutes(0); + dateObj.setSeconds(0); + dateObj.setMilliseconds(0); + dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000); + return dateObj.getTime() * 1000; +} + +add_task(async function test_execute() { + // Put some trash in the database. + let uri = Services.io.newURI("http://moz.org/"); + + PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "moz test", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "moz test", + url: uri, + }, + ], + }, + ], + }); + + PlacesUtils.tagging.tagURI(uri, ["tag"]); + await PlacesUtils.keywords.insert({ url: uri.spec, keyword: "keyword" }); + + // Set a large annotation. + let content = ""; + while (content.length < 1024) { + content += "0"; + } + + await PlacesUtils.history.update({ + url: uri, + annotations: new Map([["test-anno", content]]), + }); + + await PlacesDBUtils.telemetry(); + + await PlacesTestUtils.promiseAsyncUpdates(); + + // Test expiration probes. + let timeInMicroseconds = getExpirablePRTime(8); + + function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; + } + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://" + i + ".moz.org/"), + visitDate: newTimeInMicroseconds(), + }); + } + Services.prefs.setIntPref("places.history.expiration.max_pages", 0); + await promiseForceExpirationStep(2); + await promiseForceExpirationStep(2); + + // Test idle probes. + await PlacesDBUtils.maintenanceOnIdle(); + + for (let histogramId in histograms) { + info("checking histogram " + histogramId); + let validate = histograms[histogramId]; + let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot(); + validate(snapshot.sum); + Assert.ok(Object.values(snapshot.values).reduce((a, b) => a + b, 0) > 0); + } + for (let scalarName in scalars) { + let scalar = "places." + scalarName; + info("checking scalar " + scalar); + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + scalar, + scalars[scalarName], + "Verify scalar value matches" + ); + } +}); diff --git a/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js new file mode 100644 index 0000000000..fa03c0e06b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js @@ -0,0 +1,211 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 455315 + * https://bugzilla.mozilla.org/show_bug.cgi?id=412132 + * + * Ensures that the frecency of a bookmark's URI is what it should be after the + * bookmark is deleted. + */ + +add_task(async function removed_bookmark() { + info( + "After removing bookmark, frecency of bookmark's URI should be " + + "zero if URI is unvisited and no longer bookmarked." + ); + const TEST_URI = Services.io.newURI("http://example.com/1"); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URI, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("Bookmarked => frecency of URI should be != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.remove(bm); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("Unvisited URI no longer bookmarked => frecency should = 0"); + Assert.equal( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function removed_but_visited_bookmark() { + info( + "After removing bookmark, frecency of bookmark's URI should " + + "not be zero if URI is visited." + ); + const TEST_URI = Services.io.newURI("http://example.com/1"); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URI, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("Bookmarked => frecency of URI should be != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesTestUtils.addVisits(TEST_URI); + await PlacesUtils.bookmarks.remove(bm); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("*Visited* URI no longer bookmarked => frecency should != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function remove_bookmark_still_bookmarked() { + info( + "After removing bookmark, frecency of bookmark's URI should " + + "not be zero if URI is still bookmarked." + ); + const TEST_URI = Services.io.newURI("http://example.com/1"); + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark 1 title", + url: TEST_URI, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark 2 title", + url: TEST_URI, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("Bookmarked => frecency of URI should be != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.remove(bm1); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("URI still bookmarked => frecency should != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function cleared_parent_of_visited_bookmark() { + info( + "After removing all children from bookmark's parent, frecency " + + "of bookmark's URI should not be zero if URI is visited." + ); + const TEST_URI = Services.io.newURI("http://example.com/1"); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URI, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("Bookmarked => frecency of URI should be != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesTestUtils.addVisits(TEST_URI); + await PlacesUtils.bookmarks.eraseEverything(); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("*Visited* URI no longer bookmarked => frecency should != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function cleared_parent_of_bookmark_still_bookmarked() { + info( + "After removing all children from bookmark's parent, frecency " + + "of bookmark's URI should not be zero if URI is still " + + "bookmarked." + ); + const TEST_URI = Services.io.newURI("http://example.com/1"); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "bookmark 1 title", + url: TEST_URI, + }); + + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bookmark 2 folder", + }); + await PlacesUtils.bookmarks.insert({ + title: "bookmark 2 title", + parentGuid: folder.guid, + url: TEST_URI, + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + info("Bookmarked => frecency of URI should be != 0"); + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.remove(folder); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + // URI still bookmarked => frecency should != 0. + Assert.notEqual( + await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: TEST_URI, + }), + 0 + ); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); diff --git a/toolkit/components/places/tests/unit/test_utils_backups_create.js b/toolkit/components/places/tests/unit/test_utils_backups_create.js new file mode 100644 index 0000000000..a0bc506695 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js @@ -0,0 +1,152 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Check for correct functionality of bookmarks backups + */ + +const NUMBER_OF_BACKUPS = 10; + +async function createBackups(nBackups, dateObj, bookmarksBackupDir) { + // Generate random dates. + let dates = []; + while (dates.length < nBackups) { + // Use last year to ensure today's backup is the newest. + let randomDate = new Date( + dateObj.getFullYear() - 1, + Math.floor(12 * Math.random()), + Math.floor(28 * Math.random()) + ); + if (!dates.includes(randomDate.getTime())) { + dates.push(randomDate.getTime()); + } + } + // Sort dates from oldest to newest. + dates.sort(); + + // Fake backups are created backwards to ensure we won't consider file + // creation time. + // Create fake backups for the newest dates. + for (let i = dates.length - 1; i >= 0; i--) { + let backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i])); + let backupFile = bookmarksBackupDir.clone(); + backupFile.append(backupFilename); + backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); + info("Creating fake backup " + backupFile.leafName); + if (!backupFile.exists()) { + do_throw("Unable to create fake backup " + backupFile.leafName); + } + } + + return dates; +} + +async function checkBackups(dates, bookmarksBackupDir) { + // Check backups. We have 11 dates but we the max number is 10 so the + // oldest backup should have been removed. + for (let i = 0; i < dates.length; i++) { + let backupFilename; + let shouldExist; + let backupFile; + if (i > 0) { + let files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.nextFile; + if (PlacesBackups.filenamesRegex.test(entry.leafName)) { + backupFilename = entry.leafName; + backupFile = entry; + break; + } + } + shouldExist = true; + } else { + backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i])); + backupFile = bookmarksBackupDir.clone(); + backupFile.append(backupFilename); + shouldExist = false; + } + if (backupFile.exists() != shouldExist) { + do_throw( + "Backup should " + + (shouldExist ? "" : "not") + + " exist: " + + backupFilename + ); + } + } +} + +async function cleanupFiles(bookmarksBackupDir) { + // Cleanup backups folder. + // XXX: Can't use bookmarksBackupDir.remove(true) because file lock happens + // on WIN XP. + let files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.nextFile; + entry.remove(false); + } + // Clear cache to match the manual removing of files + delete PlacesBackups._backupFiles; + Assert.ok(!bookmarksBackupDir.directoryEntries.hasMoreElements()); +} + +add_task(async function test_create_backups() { + let backupFolderPath = await PlacesBackups.getBackupFolder(); + let bookmarksBackupDir = new FileUtils.File(backupFolderPath); + + let dateObj = new Date(); + let dates = await createBackups( + NUMBER_OF_BACKUPS, + dateObj, + bookmarksBackupDir + ); + // Add today's backup. + await PlacesBackups.create(NUMBER_OF_BACKUPS); + dates.push(dateObj.getTime()); + await checkBackups(dates, bookmarksBackupDir); + await cleanupFiles(bookmarksBackupDir); +}); + +add_task(async function test_saveBookmarks_with_no_backups() { + let backupFolderPath = await PlacesBackups.getBackupFolder(); + let bookmarksBackupDir = new FileUtils.File(backupFolderPath); + + Services.prefs.setIntPref("browser.bookmarks.max_backups", 0); + + let filePath = PathUtils.join(do_get_tempdir().path, "backup.json"); + await PlacesBackups.saveBookmarksToJSONFile(filePath); + let files = bookmarksBackupDir.directoryEntries; + Assert.ok(!files.hasMoreElements(), "Should have no backup files."); + await IOUtils.remove(filePath); + // We don't need to call cleanupFiles as we are not creating any + // backups but need to reset the cache. + delete PlacesBackups._backupFiles; +}); + +add_task(async function test_saveBookmarks_with_backups() { + let backupFolderPath = await PlacesBackups.getBackupFolder(); + let bookmarksBackupDir = new FileUtils.File(backupFolderPath); + + Services.prefs.setIntPref("browser.bookmarks.max_backups", NUMBER_OF_BACKUPS); + + let filePath = PathUtils.join(do_get_tempdir().path, "backup.json"); + let dateObj = new Date(); + let dates = await createBackups( + NUMBER_OF_BACKUPS, + dateObj, + bookmarksBackupDir + ); + + await PlacesBackups.saveBookmarksToJSONFile(filePath); + + let backupPath = await PlacesBackups.getMostRecentBackup(); + Assert.ok(await IOUtils.read(backupPath, { decompress: true })); + + dates.push(dateObj.getTime()); + await checkBackups(dates, bookmarksBackupDir); + await IOUtils.remove(filePath); + await cleanupFiles(bookmarksBackupDir); +}); diff --git a/toolkit/components/places/tests/unit/test_utils_backups_hasRecent.js b/toolkit/components/places/tests/unit/test_utils_backups_hasRecent.js new file mode 100644 index 0000000000..77d356b032 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_utils_backups_hasRecent.js @@ -0,0 +1,43 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Check for correct functionality of bookmarks backups + */ + +/** + * Creates a fake empty dated backup. + * @param {Date} date the Date to use for the backup file name + * @param {string} backupsFolderPath the path to the backups folder + * @returns path of the created backup file + */ +async function createFakeBackup(date, backupsFolderPath) { + let backupFilePath = PathUtils.join( + backupsFolderPath, + PlacesBackups.getFilenameForDate(date) + ); + await IOUtils.write(backupFilePath, new Uint8Array()); + return backupFilePath; +} + +add_task(async function test_hasRecentBackup() { + let backupFolderPath = await PlacesBackups.getBackupFolder(); + Assert.ok(!(await PlacesBackups.hasRecentBackup()), "Check no recent backup"); + + await createFakeBackup(new Date(Date.now() - 4 * 86400), backupFolderPath); + Assert.ok(!(await PlacesBackups.hasRecentBackup()), "Check no recent backup"); + PlacesBackups.invalidateCache(); + await createFakeBackup(new Date(Date.now() - 2 * 86400), backupFolderPath); + Assert.ok(await PlacesBackups.hasRecentBackup(), "Check has recent backup"); + PlacesBackups.invalidateCache(); + + try { + await IOUtils.remove(backupFolderPath, { recursive: true }); + } catch (ex) { + // On Windows the files may be locked. + info("Unable to cleanup the backups test folder"); + } +}); diff --git a/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js new file mode 100644 index 0000000000..2d092ef591 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js @@ -0,0 +1,266 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Check for correct functionality of PlacesUtils.getURLsForContainerNode and + * PlacesUtils.hasChildURIs (those helpers share almost all of their code) + */ + +var PU = PlacesUtils; +var hs = PU.history; + +add_task(async function test_getURLsForContainerNode_folder() { + info("*** TEST: folder"); + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + // This is the folder we will check for children. + title: "folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + children: [ + { + title: "inside folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + url: "place:sort=1", + title: "inside query", + }, + ], + }, + ], + }, + ], + }); + + var query = hs.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var options = hs.getNewQueryOptions(); + + info("Check folder without uri nodes"); + check_uri_nodes(query, options, 0); + + info("Check folder with uri nodes"); + // Add an uri node, this should be considered. + await PlacesUtils.bookmarks.insert({ + parentGuid: bookmarks[0].guid, + url: "http://www.mozilla.org/", + title: "bookmark", + }); + check_uri_nodes(query, options, 1); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_getURLsForContainerNode_folder_excludeItems() { + info("*** TEST: folder in an excludeItems root"); + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + // This is the folder we will check for children. + title: "folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + children: [ + { + title: "inside folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + url: "place:sort=1", + title: "inside query", + }, + ], + }, + ], + }, + ], + }); + + var query = hs.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var options = hs.getNewQueryOptions(); + options.excludeItems = true; + + info("Check folder without uri nodes"); + check_uri_nodes(query, options, 0); + + info("Check folder with uri nodes"); + // Add an uri node, this should be considered. + await PlacesUtils.bookmarks.insert({ + parentGuid: bookmarks[0].guid, + url: "http://www.mozilla.org/", + title: "bookmark", + }); + check_uri_nodes(query, options, 1); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_getURLsForContainerNode_query() { + info("*** TEST: query"); + // This is the query we will check for children. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "inside query", + url: `place:parent=${PlacesUtils.bookmarks.menuGuid}&sort=1`, + }); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "inside folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + url: "place:sort=1", + title: "inside query", + }, + ], + }, + ], + }); + + var query = hs.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var options = hs.getNewQueryOptions(); + + info("Check query without uri nodes"); + check_uri_nodes(query, options, 0); + + info("Check query with uri nodes"); + // Add an uri node, this should be considered. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://www.mozilla.org/", + title: "bookmark", + }); + check_uri_nodes(query, options, 1); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_getURLsForContainerNode_query_excludeItems() { + info("*** TEST: excludeItems Query"); + // This is the query we will check for children. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "inside query", + url: `place:parent=${PlacesUtils.bookmarks.menuGuid}&sort=1`, + }); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "inside folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + url: "place:sort=1", + title: "inside query", + }, + ], + }, + ], + }); + + var query = hs.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var options = hs.getNewQueryOptions(); + options.excludeItems = true; + + info("Check folder without uri nodes"); + check_uri_nodes(query, options, 0); + + info("Check folder with uri nodes"); + // Add an uri node, this should be considered. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://www.mozilla.org/", + title: "bookmark", + }); + check_uri_nodes(query, options, 1); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_getURLsForContainerNode_query_excludeQueries() { + info("*** TEST: !expandQueries Query"); + // This is the query we will check for children. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "inside query", + url: `place:parent=${PlacesUtils.bookmarks.menuGuid}&sort=1`, + }); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "inside folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + url: "place:sort=1", + title: "inside query", + }, + ], + }, + ], + }); + + var query = hs.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var options = hs.getNewQueryOptions(); + options.expandQueries = false; + + info("Check folder without uri nodes"); + check_uri_nodes(query, options, 0); + + info("Check folder with uri nodes"); + // Add an uri node, this should be considered. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://www.mozilla.org/", + title: "bookmark", + }); + check_uri_nodes(query, options, 1); +}); + +/** + * Executes a query and checks number of uri nodes in the first container in + * query's results. To correctly test a container ensure that the query will + * return only your container in the first level. + * + * @param aQuery + * nsINavHistoryQuery object defining the query + * @param aOptions + * nsINavHistoryQueryOptions object defining the query's options + * @param aExpectedURINodes + * number of expected uri nodes + */ +function check_uri_nodes(aQuery, aOptions, aExpectedURINodes) { + var result = hs.executeQuery(aQuery, aOptions); + var root = result.root; + root.containerOpen = true; + var node = root.getChild(0); + Assert.equal(PU.hasChildURIs(node), aExpectedURINodes > 0); + Assert.equal(PU.getURLsForContainerNode(node).length, aExpectedURINodes); + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_utils_timeConversion.js b/toolkit/components/places/tests/unit/test_utils_timeConversion.js new file mode 100644 index 0000000000..c71effe709 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_utils_timeConversion.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check time conversion utils. + */ + +add_task(async function toDate() { + Assert.throws(() => PlacesUtils.toDate(), /Invalid value/, "Should throw"); + Assert.throws(() => PlacesUtils.toDate(NaN), /Invalid value/, "Should throw"); + Assert.throws( + () => PlacesUtils.toDate(null), + /Invalid value/, + "Should throw" + ); + Assert.throws(() => PlacesUtils.toDate("1"), /Invalid value/, "Should throw"); + + const now = Date.now(); + const usecs = now * 1000; + Assert.deepEqual(PlacesUtils.toDate(usecs), new Date(now)); +}); + +add_task(async function toPRTime() { + Assert.throws(() => PlacesUtils.toPRTime(), /TypeError/, "Should throw"); + Assert.throws(() => PlacesUtils.toPRTime(null), /TypeError/, "Should throw"); + Assert.throws( + () => PlacesUtils.toPRTime({}), + /Invalid value/, + "Should throw" + ); + Assert.throws( + () => PlacesUtils.toPRTime(NaN), + /Invalid value/, + "Should throw" + ); + Assert.throws( + () => PlacesUtils.toPRTime(new Date(NaN)), + /Invalid value/, + "Should throw" + ); + Assert.throws( + () => PlacesUtils.toPRTime(new URL("https://test.moz")), + /Invalid value/, + "Should throw" + ); + + const now = Date.now(); + const usecs = now * 1000; + Assert.strictEqual(PlacesUtils.toPRTime(now), usecs); + Assert.strictEqual(PlacesUtils.toPRTime(new Date(now)), usecs); +}); diff --git a/toolkit/components/places/tests/unit/test_visitsInDB.js b/toolkit/components/places/tests/unit/test_visitsInDB.js new file mode 100644 index 0000000000..cbd0a04c4b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_visitsInDB.js @@ -0,0 +1,12 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +add_task(async function test_execute() { + const TEST_URI = uri("http://mozilla.com"); + + Assert.equal(0, await PlacesTestUtils.visitsInDB(TEST_URI)); + await PlacesTestUtils.addVisits({ uri: TEST_URI }); + Assert.equal(1, await PlacesTestUtils.visitsInDB(TEST_URI)); + await PlacesTestUtils.addVisits({ uri: TEST_URI }); + Assert.equal(2, await PlacesTestUtils.visitsInDB(TEST_URI)); +}); diff --git a/toolkit/components/places/tests/unit/xpcshell.ini b/toolkit/components/places/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..99f8d04754 --- /dev/null +++ b/toolkit/components/places/tests/unit/xpcshell.ini @@ -0,0 +1,120 @@ +[DEFAULT] +head = head_bookmarks.js +firefox-appdir = browser +prefs = places.loglevel="All" +support-files = + bookmarks.corrupt.html + bookmarks.json + bookmarks_corrupt.json + bookmarks.preplaces.html + bookmarks_html_localized.html + bookmarks_html_singleframe.html + bookmarks_iconuri.json + mobile_bookmarks_folder_import.json + mobile_bookmarks_folder_merge.json + mobile_bookmarks_multiple_folders.json + mobile_bookmarks_root_import.json + mobile_bookmarks_root_merge.json + places.sparse.sqlite + +[test_331487.js] +[test_384370.js] +[test_385397.js] +[test_399266.js] +skip-if = os == "linux" # Bug 821781 +[test_402799.js] +[test_412132.js] +[test_415460.js] +[test_415757.js] +[test_419792_node_tags_property.js] +[test_425563.js] +[test_429505_remove_shortcuts.js] +[test_433317_query_title_update.js] +[test_433525_hasChildren_crash.js] +[test_454977.js] +[test_463863.js] +[test_485442_crash_bug_nsNavHistoryQuery_GetUri.js] +[test_486978_sort_by_date_queries.js] +[test_536081.js] +[test_1085291.js] +[test_1105208.js] +[test_1105866.js] +[test_1606731.js] +[test_PlacesQuery_history.js] +[test_asyncExecuteLegacyQueries.js] +[test_async_transactions.js] +[test_autocomplete_match_fallbackTitle.js] +[test_bookmark-tags-changed_frequency.js] +[test_bookmarks_json.js] +[test_bookmarks_json_corrupt.js] +[test_bookmarks_html.js] +[test_bookmarks_html_corrupt.js] +[test_bookmarks_html_escape_entities.js] +[test_bookmarks_html_import_tags.js] +[test_bookmarks_html_localized.js] +[test_bookmarks_html_singleframe.js] +[test_bookmarks_restore_notification.js] +[test_broken_folderShortcut_result.js] +[test_browserhistory.js] +[test_childlessTags.js] +[test_frecency_decay.js] +[test_frecency_origins_alternative.js] +[test_frecency_origins_recalc.js] +[test_frecency_pages_alternative.js] +prefs = places.frecency.pages.alternative.featureGate=true +[test_frecency_pages_recalc_alt.js] +prefs = places.frecency.pages.alternative.featureGate=true +[test_frecency_recalc_triggers.js] +[test_frecency_recalculator.js] +[test_frecency_unvisited_bookmark.js] +[test_frecency_zero_updated.js] +[test_getChildIndex.js] +[test_hash.js] +[test_history.js] +[test_history_clear.js] +[test_history_notifications.js] +[test_history_observer.js] +[test_history_sidebar.js] +[test_import_mobile_bookmarks.js] +[test_isPageInDB.js] +[test_isURIVisited.js] +[test_isvisited.js] +[test_keywords.js] +[test_lastModified.js] +[test_markpageas.js] +[test_metadata.js] +[test_missing_builtin_folders.js] +support-files = missingBuiltIn.sqlite +[test_missing_root_folder.js] +support-files = noRoot.sqlite +[test_multi_observation.js] +[test_multi_word_tags.js] +[test_nested_notifications.js] +[test_nsINavHistoryViewer.js] +[test_null_interfaces.js] +[test_origins.js] +[test_origins_parsing.js] +[test_pageGuid_bookmarkGuid.js] +[test_frecency_observers.js] +[test_PlacesDBUtils_removeOldCorruptDBs.js] +[test_placeURIs.js] +[test_PlacesUtils_invalidateCachedGuidFor.js] +[test_PlacesUtils_invalidateCachedGuids.js] +[test_PlacesUtils_isRootItem.js] +[test_PlacesUtils_unwrapNodes_place.js] +[test_promiseBookmarksTree.js] +[test_resolveNullBookmarkTitles.js] +[test_result_sort.js] +[test_resultsAsVisit_details.js] +[test_sql_function_origin.js] +[test_sql_guid_functions.js] +[test_tag_autocomplete_search.js] +[test_tagging.js] +[test_telemetry.js] +[test_update_frecency_after_delete.js] +[test_utils_backups_create.js] +[test_utils_backups_hasRecent.js] +[test_utils_getURLsForContainerNode.js] +[test_utils_timeConversion.js] +[test_visitsInDB.js] +[test_get_query_param_sql_function.js] |