--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -433,17 +433,22 @@ FormAutofillParent.prototype = {
// - User applys autofill and changed
// - User fills form manually and the filling data is not duplicated to storage
if (creditCard.guid) {
let originalCCData = this.profileStorage.creditCards.get(creditCard.guid);
let unchanged = Object.keys(creditCard.record).every(field => {
if (creditCard.record[field] === "" && !originalCCData[field]) {
return true;
}
- return creditCard.untouchedFields.includes(field);
+ // Avoid updating the fields that users don't modify.
+ let untouched = creditCard.untouchedFields.includes(field);
+ if (untouched) {
+ creditCard.record[field] = originalCCData[field];
+ }
+ return untouched;
});
if (unchanged) {
this.profileStorage.creditCards.notifyUsed(creditCard.guid);
// Add probe to record credit card autofill(without modification).
Services.telemetry.scalarAdd("formautofill.creditCards.fill_type_autofill", 1);
this._recordFormFillingTime("creditCard", "autofill", timeStartedFillingMS);
return;
@@ -476,17 +481,28 @@ FormAutofillParent.prototype = {
// TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
// APIs are refactored to be async functions (bug 1399367).
if (!await MasterPassword.ensureLoggedIn()) {
log.warn("User canceled master password entry");
return;
}
- this.profileStorage.creditCards.add(creditCard.record);
+ let changedGUIDs = [];
+ // TODO: Autofill(with guid) case should show update doorhanger with update/create new.
+ // It'll be implemented in bug 1403881 and only avoid mergering for now.
+ if (creditCard.guid) {
+ changedGUIDs.push(this.profileStorage.creditCards.add(creditCard.record));
+ } else {
+ changedGUIDs.push(...this.profileStorage.creditCards.mergeToStorage(creditCard.record));
+ if (!changedGUIDs.length) {
+ changedGUIDs.push(this.profileStorage.creditCards.add(creditCard.record));
+ }
+ }
+ changedGUIDs.forEach(guid => this.profileStorage.creditCards.notifyUsed(guid));
},
_onFormSubmit(data, target) {
let {profile: {address, creditCard}, timeStartedFillingMS} = data;
if (address) {
this._onAddressSubmit(address, target, timeStartedFillingMS);
}
--- a/browser/extensions/formautofill/ProfileStorage.jsm
+++ b/browser/extensions/formautofill/ProfileStorage.jsm
@@ -1582,16 +1582,71 @@ class CreditCards extends AutofillRecord
return clonedTargetCreditCard[field] == creditCard[field];
});
if (isDuplicate) {
return creditCard.guid;
}
}
return null;
}
+
+ /**
+ * Merge new credit card into the specified record if cc-number is identical.
+ *
+ * @param {string} guid
+ * Indicates which credit card to merge.
+ * @param {Object} creditCard
+ * The new credit card used to merge into the old one.
+ * @returns {boolean}
+ * Return true if credit card is merged into target with specific guid or false if not.
+ */
+ mergeIfPossible(guid, creditCard) {
+ this.log.debug("mergeIfPossible:", guid, creditCard);
+
+ // Query raw data for comparing the decrypted credit card number
+ let creditCardFound = this.get(guid, {rawData: true});
+ if (!creditCardFound) {
+ throw new Error("No matching credit card.");
+ }
+
+ let creditCardToMerge = this._cloneAndCleanUp(creditCard);
+ this._normalizeRecord(creditCardToMerge);
+
+ for (let field of this.VALID_FIELDS) {
+ let existingField = creditCardFound[field];
+
+ // Make sure credit card field is existed and have value
+ if (field == "cc-number" && (!existingField || !creditCardToMerge[field])) {
+ return false;
+ }
+
+ if (!creditCardToMerge[field]) {
+ creditCardToMerge[field] = existingField;
+ }
+
+ let incomingField = creditCardToMerge[field];
+ if (incomingField && existingField) {
+ if (incomingField != existingField) {
+ this.log.debug("Conflicts: field", field, "has different value.");
+ return false;
+ }
+ }
+ }
+
+ // Early return if the data is the same.
+ let exactlyMatch = this.VALID_FIELDS.every((field) =>
+ creditCardFound[field] === creditCardToMerge[field]
+ );
+ if (exactlyMatch) {
+ return true;
+ }
+
+ this.update(guid, creditCardToMerge, true);
+ return true;
+ }
}
function ProfileStorage(path) {
this._path = path;
this._initializePromise = null;
this.INTERNAL_FIELDS = INTERNAL_FIELDS;
}
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
@@ -57,19 +57,23 @@ add_task(async function test_submit_cred
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
}
);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+ await removeAllRecords();
});
add_task(async function test_submit_untouched_creditCard_form() {
+ await saveCreditCard(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
@@ -78,109 +82,121 @@ add_task(async function test_submit_unto
form.querySelector("input[type=submit]").click();
});
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
}
);
- let creditCards = await getCreditCards();
- is(creditCards.length, 1, "Still 1 credit card in storage");
- is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card");
is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
+ await removeAllRecords();
});
-add_task(async function test_submit_changed_creditCard_form() {
+add_task(async function test_submit_changed_subset_creditCard_form() {
+ await saveCreditCard(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
- await openPopupOn(browser, "form #cc-name");
- await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
- await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.focus();
await new Promise(resolve => setTimeout(resolve, 1000));
name.setUserInput("");
+
+ form.querySelector("#cc-number").setUserInput("1234567812345678");
+ form.querySelector("#cc-exp-month").setUserInput("4");
+ form.querySelector("#cc-exp-year").setUserInput("2017");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
- await clickDoorhangerButton(SECONDARY_BUTTON);
+ await clickDoorhangerButton(MAIN_BUTTON);
}
);
- let creditCards = await getCreditCards();
+ creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card in storage");
- is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+ is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"], "name field still exists");
+ await removeAllRecords();
});
add_task(async function test_submit_duplicate_creditCard_form() {
+ await saveCreditCard(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.focus();
- name.setUserInput("User 1");
- form.querySelector("#cc-number").setUserInput("1111222233334444");
- form.querySelector("#cc-exp-month").setUserInput("12");
+ name.setUserInput("John Doe");
+ form.querySelector("#cc-number").setUserInput("1234567812345678");
+ form.querySelector("#cc-exp-month").setUserInput("4");
form.querySelector("#cc-exp-year").setUserInput("2017");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
}
);
- let creditCards = await getCreditCards();
+ creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card in storage");
- is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+ is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"], "Verify the name field");
is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
+ await removeAllRecords();
});
add_task(async function test_submit_unnormailzed_creditCard_form() {
+ await saveCreditCard(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.focus();
- name.setUserInput("User 1");
-
- form.querySelector("#cc-number").setUserInput("1111222233334444");
- form.querySelector("#cc-exp-month").setUserInput("12");
+ name.setUserInput("John Doe");
+ form.querySelector("#cc-number").setUserInput("1234567812345678");
+ form.querySelector("#cc-exp-month").setUserInput("4");
// Set unnormalized year
form.querySelector("#cc-exp-year").setUserInput("17");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
}
);
- let creditCards = await getCreditCards();
+ creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card in storage");
is(creditCards[0]["cc-exp-year"], "2017", "Verify the expiry year field");
+ await removeAllRecords();
});
add_task(async function test_submit_creditCard_never_save() {
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
await ContentTask.spawn(browser, null, async function() {
@@ -201,17 +217,17 @@ add_task(async function test_submit_cred
await promiseShown;
await clickDoorhangerButton(MENU_BUTTON, 0);
}
);
await sleep(1000);
let creditCards = await getCreditCards();
let creditCardPref = SpecialPowers.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
- is(creditCards.length, 1, "Still 1 credit card in storage");
+ is(creditCards.length, 0, "No credit card in storage");
is(creditCardPref, false, "Credit card is disabled");
SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
});
add_task(async function test_submit_creditCard_saved_with_mp_enabled() {
LoginTestUtils.masterPassword.enable();
// Login with the masterPassword in LoginTestUtils.
let masterPasswordDialogShown = waitForMasterPasswordDialog(true);
@@ -237,20 +253,21 @@ add_task(async function test_submit_cred
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
await masterPasswordDialogShown;
await TestUtils.topicObserved("formautofill-storage-changed");
}
);
let creditCards = await getCreditCards();
- is(creditCards.length, 2, "2 credit cards in storage");
- is(creditCards[1]["cc-name"], "User 0", "Verify the name field");
- is(creditCards[1]["cc-number"], "************1234", "Verify the card number field");
+ is(creditCards.length, 1, "1 credit card in storage");
+ is(creditCards[0]["cc-name"], "User 0", "Verify the name field");
+ is(creditCards[0]["cc-number"], "************1234", "Verify the card number field");
LoginTestUtils.masterPassword.disable();
+ await removeAllRecords();
});
add_task(async function test_submit_creditCard_saved_with_mp_enabled_but_canceled() {
LoginTestUtils.masterPassword.enable();
let masterPasswordDialogShown = waitForMasterPasswordDialog();
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
@@ -273,17 +290,17 @@ add_task(async function test_submit_cred
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
await masterPasswordDialogShown;
}
);
await sleep(1000);
let creditCards = await getCreditCards();
- is(creditCards.length, 2, "Still 2 credit cards in storage");
+ is(creditCards.length, 0, "No credit cards in storage");
LoginTestUtils.masterPassword.disable();
});
add_task(async function test_submit_creditCard_with_sync_account() {
await SpecialPowers.pushPrefEnv({
"set": [
[SYNC_USERNAME_PREF, "foo@bar.com"],
[SYNC_CREDITCARDS_AVAILABLE_PREF, true],
@@ -327,17 +344,17 @@ add_task(async function test_submit_cred
is(secondaryButton.disabled, true, "Not saving button should be disabled");
is(menuButton.disabled, true, "Never saving menu button should be disabled");
// Click the checkbox again to disable credit card sync.
cb.click();
is(SpecialPowers.getBoolPref(SYNC_CREDITCARDS_PREF), false,
"creditCards sync should be disabled after unchecked");
is(secondaryButton.disabled, false, "Not saving button should be enabled again");
is(menuButton.disabled, false, "Never saving menu button should be enabled again");
- await clickDoorhangerButton(MAIN_BUTTON);
+ await clickDoorhangerButton(SECONDARY_BUTTON);
}
);
});
add_task(async function test_submit_creditCard_with_synced_already() {
await SpecialPowers.pushPrefEnv({
"set": [
[SYNC_CREDITCARDS_PREF, true],
@@ -362,12 +379,45 @@ add_task(async function test_submit_cred
// Wait 500ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 500));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
let cb = getDoorhangerCheckbox();
ok(cb.hidden, "Sync checkbox should be hidden");
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+});
+
+add_task(async function test_submit_manual_mergeable_creditCard_form() {
+ await saveCreditCard(TEST_CREDIT_CARD_3);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+ await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
+ async function(browser) {
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown");
+ await ContentTask.spawn(browser, null, async function() {
+ let form = content.document.getElementById("form");
+ let name = form.querySelector("#cc-name");
+ name.focus();
+
+ name.setUserInput("User 3");
+ form.querySelector("#cc-number").setUserInput("9999888877776666");
+ form.querySelector("#cc-exp-month").setUserInput("1");
+ form.querySelector("#cc-exp-year").setUserInput("2000");
+
+ // Wait 1000ms before submission to make sure the input value applied
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ form.querySelector("input[type=submit]").click();
+ });
+ await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
}
);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card in storage");
+ is(creditCards[0]["cc-name"], "User 3", "Verify the name field");
+ await removeAllRecords();
});
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -1,17 +1,17 @@
/* exported MANAGE_ADDRESSES_DIALOG_URL, MANAGE_CREDIT_CARDS_DIALOG_URL, EDIT_ADDRESS_DIALOG_URL, EDIT_CREDIT_CARD_DIALOG_URL,
BASE_URL, TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3, TEST_ADDRESS_4, TEST_ADDRESS_5,
TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2, TEST_CREDIT_CARD_3, FORM_URL, CREDITCARD_FORM_URL,
FTU_PREF, ENABLED_AUTOFILL_ADDRESSES_PREF, AUTOFILL_CREDITCARDS_AVAILABLE_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF,
SYNC_USERNAME_PREF, SYNC_ADDRESSES_PREF, SYNC_CREDITCARDS_PREF, SYNC_CREDITCARDS_AVAILABLE_PREF,
sleep, expectPopupOpen, openPopupOn, expectPopupClose, closePopup, clickDoorhangerButton,
getAddresses, saveAddress, removeAddresses, saveCreditCard,
getDisplayedPopupItems, getDoorhangerCheckbox, waitForMasterPasswordDialog,
- getNotification, getDoorhangerButton */
+ getNotification, getDoorhangerButton, removeAllRecords */
"use strict";
Cu.import("resource://testing-common/LoginTestUtils.jsm", this);
const MANAGE_ADDRESSES_DIALOG_URL = "chrome://formautofill/content/manageAddresses.xhtml";
const MANAGE_CREDIT_CARDS_DIALOG_URL = "chrome://formautofill/content/manageCreditCards.xhtml";
const EDIT_ADDRESS_DIALOG_URL = "chrome://formautofill/content/editAddress.xhtml";
@@ -272,19 +272,21 @@ function waitForMasterPasswordDialog(log
dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
dialog.ui.button0.click();
} else {
dialog.ui.button1.click();
}
});
}
-registerCleanupFunction(async function() {
+async function removeAllRecords() {
let addresses = await getAddresses();
if (addresses.length) {
await removeAddresses(addresses.map(address => address.guid));
}
let creditCards = await getCreditCards();
if (creditCards.length) {
await removeCreditCards(creditCards.map(cc => cc.guid));
}
-});
+}
+
+registerCleanupFunction(removeAllRecords);
--- a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -23,16 +23,21 @@ const TEST_CREDIT_CARD_2 = {
};
const TEST_CREDIT_CARD_3 = {
"cc-number": "9999888877776666",
"cc-exp-month": 1,
"cc-exp-year": 2000,
};
+const TEST_CREDIT_CARD_4 = {
+ "cc-name": "Foo Bar",
+ "cc-number": "9999888877776666",
+};
+
const TEST_CREDIT_CARD_WITH_EMPTY_FIELD = {
"cc-name": "",
"cc-number": "1234123412341234",
"cc-exp-month": 1,
};
const TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR = {
"cc-number": "1234123412341234",
@@ -53,16 +58,78 @@ const TEST_CREDIT_CARD_WITH_INVALID_EXPI
"cc-exp-year": -3,
};
const TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS = {
"cc-name": "John Doe",
"cc-number": "1111 2222 3333 4444",
};
+const MERGE_TESTCASES = [
+ {
+ description: "Merge a superset",
+ creditCardInStorage: {
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ creditCardToMerge: {
+ "cc-name": "John Doe",
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ expectedCreditCard: {
+ "cc-name": "John Doe",
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ },
+ {
+ description: "Merge a subset",
+ creditCardInStorage: {
+ "cc-name": "John Doe",
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ creditCardToMerge: {
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ expectedCreditCard: {
+ "cc-name": "John Doe",
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ noNeedToUpdate: true,
+ },
+ {
+ description: "Merge an creditCard with partial overlaps",
+ creditCardInStorage: {
+ "cc-name": "John Doe",
+ "cc-number": "1234567812345678",
+ },
+ creditCardToMerge: {
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ expectedCreditCard: {
+ "cc-name": "John Doe",
+ "cc-number": "1234567812345678",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+ },
+ },
+];
+
let prepareTestCreditCards = async function(path) {
let profileStorage = new ProfileStorage(path);
await profileStorage.initialize();
let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
(subject, data) => data == "add");
do_check_true(profileStorage.creditCards.add(TEST_CREDIT_CARD_1));
await onChanged;
@@ -310,8 +377,86 @@ add_task(async function test_remove() {
await profileStorage.initialize();
creditCards = profileStorage.creditCards.getAll();
do_check_eq(creditCards.length, 1);
do_check_eq(profileStorage.creditCards.get(guid), null);
});
+
+MERGE_TESTCASES.forEach((testcase) => {
+ add_task(async function test_merge() {
+ do_print("Starting testcase: " + testcase.description);
+ let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
+ [testcase.creditCardInStorage],
+ "creditCards");
+ let creditCards = profileStorage.creditCards.getAll();
+ let guid = creditCards[0].guid;
+ let timeLastModified = creditCards[0].timeLastModified;
+ // Merge creditCard and verify the guid in notifyObservers subject
+ let onMerged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) =>
+ data == "update" && subject.QueryInterface(Ci.nsISupportsString).data == guid
+ );
+ // Force to create sync metadata.
+ profileStorage.creditCards.pullSyncChanges();
+ do_check_eq(getSyncChangeCounter(profileStorage.creditCards, guid), 1);
+ Assert.ok(profileStorage.creditCards.mergeIfPossible(guid, testcase.creditCardToMerge));
+ if (!testcase.noNeedToUpdate) {
+ await onMerged;
+ }
+ creditCards = profileStorage.creditCards.getAll();
+ Assert.equal(creditCards.length, 1);
+ do_check_credit_card_matches(creditCards[0], testcase.expectedCreditCard);
+ if (!testcase.noNeedToUpdate) {
+ // Record merging should update timeLastModified and bump the change counter.
+ Assert.notEqual(creditCards[0].timeLastModified, timeLastModified);
+ do_check_eq(getSyncChangeCounter(profileStorage.creditCards, guid), 2);
+ } else {
+ // Subset record merging should not update timeLastModified and the change
+ // counter is still the same.
+ Assert.equal(creditCards[0].timeLastModified, timeLastModified);
+ do_check_eq(getSyncChangeCounter(profileStorage.creditCards, guid), 1);
+ }
+ });
+});
+
+add_task(async function test_merge_unable_merge() {
+ let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
+ [TEST_CREDIT_CARD_1],
+ "creditCards");
+
+ let creditCards = profileStorage.creditCards.getAll();
+ let guid = creditCards[0].guid;
+ // Force to create sync metadata.
+ profileStorage.creditCards.pullSyncChanges();
+ do_check_eq(getSyncChangeCounter(profileStorage.creditCards, guid), 1);
+
+ // Unable to merge because of conflict
+ let anotherCreditCard = profileStorage.creditCards._clone(TEST_CREDIT_CARD_1);
+ anotherCreditCard["cc-name"] = "Foo Bar";
+ do_check_eq(profileStorage.creditCards.mergeIfPossible(guid, anotherCreditCard), false);
+ // The change counter is unchanged.
+ do_check_eq(getSyncChangeCounter(profileStorage.creditCards, guid), 1);
+
+ // Unable to merge because no credit card number
+ anotherCreditCard = profileStorage.creditCards._clone(TEST_CREDIT_CARD_1);
+ anotherCreditCard["cc-number"] = "";
+ do_check_eq(profileStorage.creditCards.mergeIfPossible(guid, anotherCreditCard), false);
+ // The change counter is still unchanged.
+ do_check_eq(getSyncChangeCounter(profileStorage.creditCards, guid), 1);
+});
+
+add_task(async function test_mergeToStorage() {
+ let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
+ [TEST_CREDIT_CARD_3, TEST_CREDIT_CARD_4],
+ "creditCards");
+ // Merge a creditCard to storage
+ let anotherCreditCard = profileStorage.creditCards._clone(TEST_CREDIT_CARD_3);
+ anotherCreditCard["cc-name"] = "Foo Bar";
+ do_check_eq(profileStorage.creditCards.mergeToStorage(anotherCreditCard).length, 2);
+ do_check_eq(profileStorage.creditCards.getAll()[0]["cc-name"], "Foo Bar");
+ do_check_eq(profileStorage.creditCards.getAll()[0]["cc-exp"], "2000-01");
+ do_check_eq(profileStorage.creditCards.getAll()[1]["cc-name"], "Foo Bar");
+ do_check_eq(profileStorage.creditCards.getAll()[1]["cc-exp"], "2000-01");
+});