Bug 1838974 - Bookmarks engine roundtrips data it doesn't know about r=markh,lina,sync-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D181593
This commit is contained in:
Sammy Khamis 2023-07-07 19:55:40 +00:00
Родитель 2cd140ad77
Коммит 525249fbdf
7 изменённых файлов: 476 добавлений и 24 удалений

Просмотреть файл

@ -1389,3 +1389,124 @@ add_bookmark_test(async function test_livemarks(engine) {
await cleanup(engine, server);
}
});
add_bookmark_test(async function test_unknown_fields(engine) {
let store = engine._store;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("bookmarks");
try {
let folder1 = await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
type: PlacesUtils.bookmarks.TYPE_FOLDER,
title: "Folder 1",
});
let bmk1 = await PlacesUtils.bookmarks.insert({
parentGuid: folder1.guid,
url: "http://getfirefox.com/",
title: "Get Firefox!",
});
let bmk2 = await PlacesUtils.bookmarks.insert({
parentGuid: folder1.guid,
url: "http://getthunderbird.com/",
title: "Get Thunderbird!",
});
let toolbar_record = await store.createRecord("toolbar");
collection.insert("toolbar", encryptPayload(toolbar_record.cleartext));
let folder1_record_without_unknown_fields = await store.createRecord(
folder1.guid
);
collection.insert(
folder1.guid,
encryptPayload(folder1_record_without_unknown_fields.cleartext)
);
// First bookmark record has an unknown string field
let bmk1_record = await store.createRecord(bmk1.guid);
console.log("bmk1_record: ", bmk1_record);
bmk1_record.cleartext.unknownStrField =
"an unknown field from another client";
collection.insert(bmk1.guid, encryptPayload(bmk1_record.cleartext));
// Second bookmark record as an unknown object field
let bmk2_record = await store.createRecord(bmk2.guid);
bmk2_record.cleartext.unknownObjField = {
name: "an unknown object from another client",
};
collection.insert(bmk2.guid, encryptPayload(bmk2_record.cleartext));
// Sync the two bookmarks
await sync_engine_and_validate_telem(engine, true);
// Add a folder could also have an unknown field
let folder1_record = await store.createRecord(folder1.guid);
folder1_record.cleartext.unknownStrField =
"a folder could also have an unknown field!";
collection.insert(folder1.guid, encryptPayload(folder1_record.cleartext));
// sync the new updates
await engine.setLastSync(1);
await sync_engine_and_validate_telem(engine, true);
let payloads = collection.payloads();
// Validate the server has the unknown fields at the top level (and now unknownFields)
let server_bmk1 = payloads.find(payload => payload.id == bmk1.guid);
deepEqual(
server_bmk1.unknownStrField,
"an unknown field from another client",
"unknown fields correctly on the record"
);
Assert.equal(server_bmk1.unknownFields, null);
// Check that the mirror table has unknown fields
let db = await PlacesUtils.promiseDBConnection();
let rows = await db.executeCached(
`
SELECT guid, title, unknownFields from items WHERE guid IN
(:bmk1, :bmk2, :folder1)`,
{ bmk1: bmk1.guid, bmk2: bmk2.guid, folder1: folder1.guid }
);
// We should have 3 rows that came from the server
Assert.equal(rows.length, 3);
// Bookmark 1 - unknown string field
let remote_bmk1 = rows.find(
row => row.getResultByName("guid") == bmk1.guid
);
Assert.equal(remote_bmk1.getResultByName("title"), "Get Firefox!");
deepEqual(JSON.parse(remote_bmk1.getResultByName("unknownFields")), {
unknownStrField: "an unknown field from another client",
});
// Bookmark 2 - unknown object field
let remote_bmk2 = rows.find(
row => row.getResultByName("guid") == bmk2.guid
);
Assert.equal(remote_bmk2.getResultByName("title"), "Get Thunderbird!");
deepEqual(JSON.parse(remote_bmk2.getResultByName("unknownFields")), {
unknownObjField: {
name: "an unknown object from another client",
},
});
// Folder with unknown field
// check the server still has the unknown field
deepEqual(
payloads.find(payload => payload.id == folder1.guid).unknownStrField,
"a folder could also have an unknown field!",
"Server still has the unknown field"
);
let remote_folder = rows.find(
row => row.getResultByName("guid") == folder1.guid
);
Assert.equal(remote_folder.getResultByName("title"), "Folder 1");
deepEqual(JSON.parse(remote_folder.getResultByName("unknownFields")), {
unknownStrField: "a folder could also have an unknown field!",
});
} finally {
await cleanup(engine, server);
}
});

Просмотреть файл

@ -74,7 +74,7 @@ const DB_TITLE_LENGTH_MAX = 4096;
// The current mirror database schema version. Bump for migrations, then add
// migration code to `migrateMirrorSchema`.
const MIRROR_SCHEMA_VERSION = 8;
const MIRROR_SCHEMA_VERSION = 9;
// Use a shared jankYielder in these functions
XPCOMUtils.defineLazyGetter(lazy, "yieldState", () => lazy.Async.yieldState());
@ -806,13 +806,21 @@ export class SyncedBookmarksMirror {
? Ci.mozISyncedBookmarksMerger.VALIDITY_VALID
: Ci.mozISyncedBookmarksMerger.VALIDITY_REPLACE;
let unknownFields = extractUnknownFields(record.cleartext, [
"bmkUri",
"description",
"keyword",
"tags",
"title",
...COMMON_UNKNOWN_FIELDS,
]);
await this.db.executeCached(
`
REPLACE INTO items(guid, parentGuid, serverModified, needsMerge, kind,
dateAdded, title, keyword, validity,
dateAdded, title, keyword, validity, unknownFields,
urlId)
VALUES(:guid, :parentGuid, :serverModified, :needsMerge, :kind,
:dateAdded, NULLIF(:title, ''), :keyword, :validity,
:dateAdded, NULLIF(:title, ''), :keyword, :validity, :unknownFields,
(SELECT id FROM urls
WHERE hash = hash(:url) AND
url = :url))`,
@ -827,6 +835,7 @@ export class SyncedBookmarksMirror {
keyword,
url: url ? url.href : null,
validity,
unknownFields,
}
);
@ -923,18 +932,29 @@ export class SyncedBookmarksMirror {
let dateAdded = determineDateAdded(record);
let title = validateTitle(record.title);
let unknownFields = extractUnknownFields(record.cleartext, [
"bmkUri",
"description",
"folderName",
"keyword",
"queryId",
"tags",
"title",
...COMMON_UNKNOWN_FIELDS,
]);
await this.db.executeCached(
`
REPLACE INTO items(guid, parentGuid, serverModified, needsMerge, kind,
dateAdded, title,
urlId,
validity)
validity, unknownFields)
VALUES(:guid, :parentGuid, :serverModified, :needsMerge, :kind,
:dateAdded, NULLIF(:title, ''),
(SELECT id FROM urls
WHERE hash = hash(:url) AND
url = :url),
:validity)`,
:validity, :unknownFields)`,
{
guid,
parentGuid,
@ -945,6 +965,7 @@ export class SyncedBookmarksMirror {
title,
url: url ? url.href : null,
validity,
unknownFields,
}
);
}
@ -957,13 +978,18 @@ export class SyncedBookmarksMirror {
let serverModified = determineServerModified(record);
let dateAdded = determineDateAdded(record);
let title = validateTitle(record.title);
let unknownFields = extractUnknownFields(record.cleartext, [
"children",
"description",
"title",
...COMMON_UNKNOWN_FIELDS,
]);
await this.db.executeCached(
`
REPLACE INTO items(guid, parentGuid, serverModified, needsMerge, kind,
dateAdded, title)
dateAdded, title, unknownFields)
VALUES(:guid, :parentGuid, :serverModified, :needsMerge, :kind,
:dateAdded, NULLIF(:title, ''))`,
:dateAdded, NULLIF(:title, ''), :unknownFields)`,
{
guid,
parentGuid,
@ -972,6 +998,7 @@ export class SyncedBookmarksMirror {
kind: Ci.mozISyncedBookmarksMerger.KIND_FOLDER,
dateAdded,
title,
unknownFields,
}
);
@ -1022,12 +1049,21 @@ export class SyncedBookmarksMirror {
? Ci.mozISyncedBookmarksMerger.VALIDITY_VALID
: Ci.mozISyncedBookmarksMerger.VALIDITY_REPLACE;
let unknownFields = extractUnknownFields(record.cleartext, [
"children",
"description",
"feedUri",
"siteUri",
"title",
...COMMON_UNKNOWN_FIELDS,
]);
await this.db.executeCached(
`
REPLACE INTO items(guid, parentGuid, serverModified, needsMerge, kind,
dateAdded, title, feedURL, siteURL, validity)
dateAdded, title, feedURL, siteURL, validity, unknownFields)
VALUES(:guid, :parentGuid, :serverModified, :needsMerge, :kind,
:dateAdded, NULLIF(:title, ''), :feedURL, :siteURL, :validity)`,
:dateAdded, NULLIF(:title, ''), :feedURL, :siteURL, :validity, :unknownFields)`,
{
guid,
parentGuid,
@ -1039,6 +1075,7 @@ export class SyncedBookmarksMirror {
feedURL: feedURL ? feedURL.href : null,
siteURL: siteURL ? siteURL.href : null,
validity,
unknownFields,
}
);
}
@ -1050,13 +1087,17 @@ export class SyncedBookmarksMirror {
);
let serverModified = determineServerModified(record);
let dateAdded = determineDateAdded(record);
let unknownFields = extractUnknownFields(record.cleartext, [
"pos",
...COMMON_UNKNOWN_FIELDS,
]);
await this.db.executeCached(
`
REPLACE INTO items(guid, parentGuid, serverModified, needsMerge, kind,
dateAdded)
dateAdded, unknownFields)
VALUES(:guid, :parentGuid, :serverModified, :needsMerge, :kind,
:dateAdded)`,
:dateAdded, :unknownFields)`,
{
guid,
parentGuid,
@ -1064,6 +1105,7 @@ export class SyncedBookmarksMirror {
needsMerge,
kind: Ci.mozISyncedBookmarksMerger.KIND_SEPARATOR,
dateAdded,
unknownFields,
}
);
}
@ -1184,7 +1226,7 @@ export class SyncedBookmarksMirror {
await this.db.execute(
`SELECT id, syncChangeCounter, guid, isDeleted, type, isQuery,
tagFolderName, keyword, url, IFNULL(title, '') AS title,
position, parentGuid,
position, parentGuid, unknownFields,
IFNULL(parentTitle, '') AS parentTitle, dateAdded
FROM itemsToUpload`,
null,
@ -1227,6 +1269,10 @@ export class SyncedBookmarksMirror {
let parentRecordId =
lazy.PlacesSyncUtils.bookmarks.guidToRecordId(parentGuid);
let unknownFieldsRow = row.getResultByName("unknownFields");
let unknownFields = unknownFieldsRow
? JSON.parse(unknownFieldsRow)
: null;
let type = row.getResultByName("type");
switch (type) {
case lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK: {
@ -1253,6 +1299,7 @@ export class SyncedBookmarksMirror {
title: row.getResultByName("title"),
// folderName should never be an empty string or null
folderName: row.getResultByName("tagFolderName") || undefined,
...unknownFields,
};
changeRecords[recordId] = new BookmarkChangeRecord(
syncChangeCounter,
@ -1270,6 +1317,7 @@ export class SyncedBookmarksMirror {
dateAdded: row.getResultByName("dateAdded") || undefined,
bmkUri: row.getResultByName("url"),
title: row.getResultByName("title"),
...unknownFields,
};
let keyword = row.getResultByName("keyword");
if (keyword) {
@ -1296,6 +1344,7 @@ export class SyncedBookmarksMirror {
parentName: row.getResultByName("parentTitle"),
dateAdded: row.getResultByName("dateAdded") || undefined,
title: row.getResultByName("title"),
...unknownFields,
};
let localId = row.getResultByName("id");
let childRecordIds = childRecordIdsByLocalParentId.get(localId);
@ -1317,6 +1366,7 @@ export class SyncedBookmarksMirror {
dateAdded: row.getResultByName("dateAdded") || undefined,
// Older Desktops use `pos` for deduping.
pos: row.getResultByName("position"),
...unknownFields,
};
changeRecords[recordId] = new BookmarkChangeRecord(
syncChangeCounter,
@ -1503,6 +1553,19 @@ async function migrateMirrorSchema(db, currentSchemaVersion) {
WHERE EXISTS (SELECT 1 FROM mirror.items
WHERE guid = b.guid)`);
}
if (currentSchemaVersion < 9) {
// Adding unknownFields to the mirror table, which allows us to
// keep fields we may not yet understand from other clients and roundtrip
// them during the sync process
let columns = await db.execute(`PRAGMA table_info(items)`);
// migration needs to be idempotent, so we check if the column exists first
let exists = columns.find(
row => row.getResultByName("name") === "unknownFields"
);
if (!exists) {
await db.execute(`ALTER TABLE items ADD COLUMN unknownFields TEXT`);
}
}
}
/**
@ -1543,7 +1606,8 @@ async function initializeMirrorDatabase(db) {
loadInSidebar BOOLEAN,
smartBookmarkName TEXT,
feedURL TEXT,
siteURL TEXT
siteURL TEXT,
unknownFields TEXT
)`);
await db.execute(`CREATE TABLE mirror.structure(
@ -1932,7 +1996,8 @@ async function initializeTempMirrorEntities(db) {
url TEXT,
tagFolderName TEXT,
keyword TEXT,
position INTEGER
position INTEGER,
unknownFields TEXT
)`);
await db.execute(`CREATE TEMP TABLE structureToUpload(
@ -2527,4 +2592,40 @@ function anyAborted(finalizeSignal, interruptSignal = null) {
return controller.signal;
}
// Common unknown fields for places items
const COMMON_UNKNOWN_FIELDS = [
"dateAdded",
"hasDupe",
"id",
"modified",
"parentid",
"parentName",
"type",
];
// Other clients might have new fields we don't quite understand yet,
// so we add it to a "unknownFields" field to roundtrip back to the server
// so other clients don't experience data loss
function extractUnknownFields(record, validFields) {
let { unknownFields, hasUnknownFields } = Object.keys(record).reduce(
({ unknownFields, hasUnknownFields }, key) => {
if (validFields.includes(key)) {
return { unknownFields, hasUnknownFields };
}
unknownFields[key] = record[key];
return { unknownFields, hasUnknownFields: true };
},
{ unknownFields: {}, hasUnknownFields: false }
);
// If we found some unknown fields, we stringify it to be able
// to properly encrypt it for roundtripping since we can't know if
// it contained sensitive fields or not
if (hasUnknownFields) {
// For simplicity, we store the unknown fields as a string
// since we never operate on it and just need it for roundtripping
return JSON.stringify(unknownFields);
}
return null;
}
// In conclusion, this is why bookmark syncing is hard.

Просмотреть файл

@ -1104,7 +1104,8 @@ fn stage_items_to_upload(
"INSERT OR IGNORE INTO itemsToUpload(id, guid, syncChangeCounter,
parentGuid, parentTitle, dateAdded,
type, title, placeId, isQuery, url,
keyword, position, tagFolderName)
keyword, position, tagFolderName,
unknownFields)
{}
JOIN itemsToApply n ON n.mergedGuid = b.guid
WHERE n.localDateAddedMicroseconds < n.remoteDateAddedMicroseconds",
@ -1118,7 +1119,8 @@ fn stage_items_to_upload(
parentGuid, parentTitle,
dateAdded, type, title,
placeId, isQuery, url, keyword,
position, tagFolderName)
position, tagFolderName,
unknownFields)
{}
WHERE b.guid IN ({})",
UploadItemsFragment("b"),
@ -1304,10 +1306,12 @@ impl fmt::Display for UploadItemsFragment {
(SELECT keyword FROM moz_keywords WHERE place_id = h.id),
{0}.position,
(SELECT get_query_param(substr(url, 7), 'tag')
WHERE substr(h.url, 1, 6) = 'place:') AS tagFolderName
WHERE substr(h.url, 1, 6) = 'place:') AS tagFolderName,
v.unknownFields
FROM moz_bookmarks {0}
JOIN moz_bookmarks p ON p.id = {0}.parent
LEFT JOIN moz_places h ON h.id = {0}.fk",
LEFT JOIN moz_places h ON h.id = {0}.fk
LEFT JOIN items v ON v.guid = {0}.guid",
self.0
)
}

Двоичные данные
toolkit/components/places/tests/sync/mirror_v8.sqlite Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -2,7 +2,7 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Keep in sync with `SyncedBookmarksMirror.jsm`.
const CURRENT_MIRROR_SCHEMA_VERSION = 8;
const CURRENT_MIRROR_SCHEMA_VERSION = 9;
// The oldest schema version that we support. Any databases with schemas older
// than this will be dropped and recreated.
@ -159,8 +159,8 @@ add_task(async function test_database_corrupt() {
await buf.finalize();
});
add_task(async function test_migrate_v8() {
let buf = await openMirror("test_migrate_v8");
add_task(async function test_migrate_v7_v9() {
let buf = await openMirror("test_migrate_v7_v9");
await PlacesUtils.bookmarks.insertTree({
guid: PlacesUtils.bookmarks.menuGuid,
@ -205,8 +205,8 @@ add_task(async function test_migrate_v8() {
await buf.finalize();
// reopen it.
buf = await openMirror("test_migrate_v8");
Assert.equal(await buf.db.getSchemaVersion("mirror"), 8, "did upgrade");
buf = await openMirror("test_migrate_v7_v9");
Assert.equal(await buf.db.getSchemaVersion("mirror"), 9, "did upgrade");
let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
"bookmarkAAAA",
@ -226,3 +226,21 @@ add_task(async function test_migrate_v8() {
Assert.equal(fieldsMenu.syncStatus, PlacesUtils.bookmarks.SYNC_STATUS.NORMAL);
await buf.finalize();
});
add_task(async function test_migrate_v8_v9() {
let dbFile = await setupFixtureFile("mirror_v8.sqlite");
let buf = await SyncedBookmarksMirror.open({
path: dbFile.path,
recordStepTelemetry() {},
recordValidationTelemetry() {},
});
Assert.equal(await buf.db.getSchemaVersion("mirror"), 9, "did upgrade");
// Verify the new column is there
Assert.ok(await buf.db.execute("SELECT unknownFields FROM items"));
await buf.finalize();
await PlacesUtils.bookmarks.eraseEverything();
await PlacesSyncUtils.bookmarks.reset();
});

Просмотреть файл

@ -0,0 +1,206 @@
/* 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_bookmark_unknown_fields() {
let buf = await openMirror("unknown_fields");
await PlacesUtils.bookmarks.insertTree({
guid: PlacesUtils.bookmarks.menuGuid,
children: [
{
guid: "mozBmk______",
url: "https://mozilla.org",
title: "Mozilla",
tags: ["moz", "dot", "org"],
},
],
});
await storeRecords(
buf,
shuffle([
{
id: "menu",
parentid: "places",
type: "folder",
children: ["mozBmk______"],
},
{
id: "mozBmk______",
parentid: "menu",
type: "bookmark",
title: "Mozilla",
bmkUri: "https://mozilla.org",
tags: ["moz", "dot", "org"],
unknownStr: "an unknown field",
},
]),
{ needsMerge: false }
);
await PlacesTestUtils.markBookmarksAsSynced();
await storeRecords(
buf,
[
{
id: "mozBmk______",
parentid: "menu",
type: "bookmark",
title: "New Mozilla",
bmkUri: "https://mozilla.org",
tags: ["moz", "dot", "org"],
unknownStr: "a new unknown field",
},
],
{ needsMerge: true }
);
let controller = new AbortController();
const wasMerged = await buf.merge(controller.signal);
Assert.ok(wasMerged);
let itemRows = await buf.db.execute(`SELECT guid, unknownFields FROM items`);
let updatedBookmark = itemRows.find(
row => row.getResultByName("guid") == "mozBmk______"
);
deepEqual(JSON.parse(updatedBookmark.getResultByName("unknownFields")), {
unknownStr: "a new unknown field",
});
await buf.finalize();
await PlacesUtils.bookmarks.eraseEverything();
await PlacesSyncUtils.bookmarks.reset();
});
add_task(async function test_changes_unknown_fields_all_types() {
let buf = await openMirror("unknown_fields_all");
await storeRecords(
buf,
[
{
id: "menu",
parentid: "places",
type: "folder",
title: "menu",
children: ["bookmarkAAAA", "separatorAAA", "queryAAAAAAA"],
unknownFolderField: "an unknown folder field",
},
{
id: "bookmarkAAAA",
parentid: "menu",
type: "bookmark",
title: "Mozilla2",
bmkUri: "https://mozilla.org",
tags: ["moz", "dot", "org"],
unknownStrField: "an unknown bookmark field",
unknownStrObj: { newField: "unknown pt deux" },
},
{
id: "separatorAAA",
parentid: "menu",
type: "separator",
unknownSepField: "an unknown separator field",
},
{
id: "queryAAAAAAA",
parentid: "menu",
type: "bookmark",
title: "a query",
bmkUri: "place:foo",
unknownQueryField: "an unknown query field",
},
],
{ needsMerge: true }
);
await PlacesTestUtils.markBookmarksAsSynced();
let changesToUpload = await buf.apply();
// Should be no local changes needing to be uploaded
deepEqual(changesToUpload, {});
// Make updates to all the type of bookmarks
await PlacesUtils.bookmarks.update({
guid: "menu________",
title: "updated menu",
});
await PlacesUtils.bookmarks.update({
guid: "bookmarkAAAA",
title: "Mozilla3",
});
await PlacesUtils.bookmarks.update({ guid: "separatorAAA", index: 2 });
await PlacesUtils.bookmarks.update({
guid: "queryAAAAAAA",
title: "an updated query",
});
// We should now have a bunch of changes to upload
changesToUpload = await buf.apply();
const { menu, bookmarkAAAA, separatorAAA, queryAAAAAAA } = changesToUpload;
// Validate we have the updated title as well as the unknown fields
Assert.equal(menu.cleartext.title, "updated menu");
Assert.equal(menu.cleartext.unknownFolderField, "an unknown folder field");
// Test bookmark unknown fields
Assert.equal(bookmarkAAAA.cleartext.title, "Mozilla3");
Assert.equal(
bookmarkAAAA.cleartext.unknownStrField,
"an unknown bookmark field"
);
deepEqual(bookmarkAAAA.cleartext.unknownStrObj, {
newField: "unknown pt deux",
});
// Test separator unknown fields
Assert.equal(
separatorAAA.cleartext.unknownSepField,
"an unknown separator field"
);
// Test query unknown fields
Assert.equal(queryAAAAAAA.cleartext.title, "an updated query");
Assert.equal(
queryAAAAAAA.cleartext.unknownQueryField,
"an unknown query field"
);
let itemRows = await buf.db.execute(`SELECT guid, unknownFields FROM items`);
// Test bookmark correctly JSON'd in the mirror
let remoteBookmark = itemRows.find(
row => row.getResultByName("guid") == "bookmarkAAAA"
);
deepEqual(JSON.parse(remoteBookmark.getResultByName("unknownFields")), {
unknownStrField: "an unknown bookmark field",
unknownStrObj: { newField: "unknown pt deux" },
});
// Test folder correctly JSON'd in the mirror
let remoteFolder = itemRows.find(
row => row.getResultByName("guid") == "menu________"
);
deepEqual(JSON.parse(remoteFolder.getResultByName("unknownFields")), {
unknownFolderField: "an unknown folder field",
});
// Test query correctly JSON'd in the mirror
let remoteQuery = itemRows.find(
row => row.getResultByName("guid") == "queryAAAAAAA"
);
deepEqual(JSON.parse(remoteQuery.getResultByName("unknownFields")), {
unknownQueryField: "an unknown query field",
});
// Test separator correctly JSON'd in the mirror
let remoteSeparator = itemRows.find(
row => row.getResultByName("guid") == "separatorAAA"
);
deepEqual(JSON.parse(remoteSeparator.getResultByName("unknownFields")), {
unknownSepField: "an unknown separator field",
});
await buf.finalize();
await PlacesUtils.bookmarks.eraseEverything();
await PlacesSyncUtils.bookmarks.reset();
});

Просмотреть файл

@ -6,6 +6,7 @@ support-files =
mirror_corrupt.sqlite
mirror_v1.sqlite
mirror_v5.sqlite
mirror_v8.sqlite
[test_bookmark_abort_merging.js]
[test_bookmark_chunking.js]
@ -20,4 +21,5 @@ support-files =
[test_bookmark_reconcile.js]
[test_bookmark_structure_changes.js]
[test_bookmark_value_changes.js]
[test_bookmark_unknown_fields.js]
[test_sync_utils.js]