зеркало из https://github.com/mozilla/gecko-dev.git
Merge autoland to mozilla-central a=merge
This commit is contained in:
Коммит
09659b9c3b
|
@ -56,7 +56,6 @@ module.exports = {
|
||||||
"browser/modules/test/browser/**",
|
"browser/modules/test/browser/**",
|
||||||
"browser/tools/mozscreenshots/browser_boundingbox.js",
|
"browser/tools/mozscreenshots/browser_boundingbox.js",
|
||||||
"devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js",
|
"devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js",
|
||||||
"services/**",
|
|
||||||
"storage/test/unit/**",
|
"storage/test/unit/**",
|
||||||
"testing/marionette/test/unit/**",
|
"testing/marionette/test/unit/**",
|
||||||
"toolkit/components/**",
|
"toolkit/components/**",
|
||||||
|
|
|
@ -201,6 +201,10 @@ var gIdentityHandler = {
|
||||||
delete this._popupExpander;
|
delete this._popupExpander;
|
||||||
return this._popupExpander = document.getElementById("identity-popup-security-expander");
|
return this._popupExpander = document.getElementById("identity-popup-security-expander");
|
||||||
},
|
},
|
||||||
|
get _clearSiteDataFooter() {
|
||||||
|
delete this._clearSiteDataFooter;
|
||||||
|
return this._clearSiteDataFooter = document.getElementById("identity-popup-clear-sitedata-footer");
|
||||||
|
},
|
||||||
get _permissionAnchors() {
|
get _permissionAnchors() {
|
||||||
delete this._permissionAnchors;
|
delete this._permissionAnchors;
|
||||||
let permissionAnchors = {};
|
let permissionAnchors = {};
|
||||||
|
@ -210,6 +214,39 @@ var gIdentityHandler = {
|
||||||
return this._permissionAnchors = permissionAnchors;
|
return this._permissionAnchors = permissionAnchors;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clicks on the "Clear Cookies and Site Data" button.
|
||||||
|
*/
|
||||||
|
async clearSiteData(event) {
|
||||||
|
if (!this._uriHasHost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let host = this._uri.host;
|
||||||
|
|
||||||
|
// Site data could have changed while the identity popup was open,
|
||||||
|
// reload again to be sure.
|
||||||
|
await SiteDataManager.updateSites();
|
||||||
|
|
||||||
|
let baseDomain = SiteDataManager.getBaseDomainFromHost(host);
|
||||||
|
let siteData = await SiteDataManager.getSites(baseDomain);
|
||||||
|
|
||||||
|
// Hide the popup before showing the removal prompt, to
|
||||||
|
// avoid a pretty ugly transition. Also hide it even
|
||||||
|
// if the update resulted in no site data, to keep the
|
||||||
|
// illusion that clicking the button had an effect.
|
||||||
|
PanelMultiView.hidePopup(this._identityPopup);
|
||||||
|
|
||||||
|
if (siteData && siteData.length) {
|
||||||
|
let hosts = siteData.map(site => site.host);
|
||||||
|
if (SiteDataManager.promptSiteDataRemoval(window, hosts)) {
|
||||||
|
SiteDataManager.remove(hosts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for mouseclicks on the "More Information" button in the
|
* Handler for mouseclicks on the "More Information" button in the
|
||||||
* "identity-popup" panel.
|
* "identity-popup" panel.
|
||||||
|
@ -578,6 +615,21 @@ var gIdentityHandler = {
|
||||||
* applicable
|
* applicable
|
||||||
*/
|
*/
|
||||||
refreshIdentityPopup() {
|
refreshIdentityPopup() {
|
||||||
|
// Update cookies and site data information and show the
|
||||||
|
// "Clear Site Data" button if the site is storing local data.
|
||||||
|
this._clearSiteDataFooter.hidden = true;
|
||||||
|
if (this._uriHasHost) {
|
||||||
|
let host = this._uri.host;
|
||||||
|
SiteDataManager.updateSites().then(async () => {
|
||||||
|
let baseDomain = SiteDataManager.getBaseDomainFromHost(host);
|
||||||
|
let siteData = await SiteDataManager.getSites(baseDomain);
|
||||||
|
|
||||||
|
if (siteData && siteData.length) {
|
||||||
|
this._clearSiteDataFooter.hidden = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update "Learn More" for Mixed Content Blocking and Insecure Login Forms.
|
// Update "Learn More" for Mixed Content Blocking and Insecure Login Forms.
|
||||||
let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
|
let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
|
||||||
this._identityPopupMixedContentLearnMore
|
this._identityPopupMixedContentLearnMore
|
||||||
|
|
|
@ -55,6 +55,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
||||||
SchedulePressure: "resource:///modules/SchedulePressure.jsm",
|
SchedulePressure: "resource:///modules/SchedulePressure.jsm",
|
||||||
ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
|
ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
|
||||||
SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
|
SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
|
||||||
|
SiteDataManager: "resource:///modules/SiteDataManager.jsm",
|
||||||
SitePermissions: "resource:///modules/SitePermissions.jsm",
|
SitePermissions: "resource:///modules/SitePermissions.jsm",
|
||||||
TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
|
TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
|
||||||
TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
|
TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
|
||||||
|
|
|
@ -46,6 +46,7 @@ support-files =
|
||||||
[browser_identity_UI.js]
|
[browser_identity_UI.js]
|
||||||
[browser_identityBlock_focus.js]
|
[browser_identityBlock_focus.js]
|
||||||
support-files = ../permissions/permissions.html
|
support-files = ../permissions/permissions.html
|
||||||
|
[browser_identityPopup_clearSiteData.js]
|
||||||
[browser_identityPopup_focus.js]
|
[browser_identityPopup_focus.js]
|
||||||
[browser_insecureLoginForms.js]
|
[browser_insecureLoginForms.js]
|
||||||
support-files =
|
support-files =
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
const TEST_ORIGIN = "https://example.com";
|
||||||
|
const TEST_SUB_ORIGIN = "https://test1.example.com";
|
||||||
|
const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
|
||||||
|
|
||||||
|
ChromeUtils.defineModuleGetter(this, "SiteDataTestUtils",
|
||||||
|
"resource://testing-common/SiteDataTestUtils.jsm");
|
||||||
|
|
||||||
|
async function testClearing(testQuota, testCookies) {
|
||||||
|
// Add some test quota storage.
|
||||||
|
if (testQuota) {
|
||||||
|
await SiteDataTestUtils.addToIndexedDB(TEST_ORIGIN);
|
||||||
|
await SiteDataTestUtils.addToIndexedDB(TEST_SUB_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some test cookies.
|
||||||
|
if (testCookies) {
|
||||||
|
SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test1", "1");
|
||||||
|
SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test2", "2");
|
||||||
|
SiteDataTestUtils.addToCookies(TEST_SUB_ORIGIN, "test3", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
|
||||||
|
// Verify we have added quota storage.
|
||||||
|
if (testQuota) {
|
||||||
|
let usage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
|
||||||
|
Assert.greater(usage, 0, "Should have data for the base origin.");
|
||||||
|
|
||||||
|
usage = await SiteDataTestUtils.getQuotaUsage(TEST_SUB_ORIGIN);
|
||||||
|
Assert.greater(usage, 0, "Should have data for the sub origin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the identity popup.
|
||||||
|
let { gIdentityHandler } = gBrowser.ownerGlobal;
|
||||||
|
let promisePanelOpen = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
|
||||||
|
let siteDataUpdated = TestUtils.topicObserved("sitedatamanager:sites-updated");
|
||||||
|
gIdentityHandler._identityBox.click();
|
||||||
|
await promisePanelOpen;
|
||||||
|
await siteDataUpdated;
|
||||||
|
|
||||||
|
let clearFooter = document.getElementById("identity-popup-clear-sitedata-footer");
|
||||||
|
let clearButton = document.getElementById("identity-popup-clear-sitedata-button");
|
||||||
|
ok(!clearFooter.hidden, "The clear data footer is not hidden.");
|
||||||
|
|
||||||
|
let cookiesCleared;
|
||||||
|
if (testCookies) {
|
||||||
|
cookiesCleared = Promise.all([
|
||||||
|
TestUtils.topicObserved("cookie-changed", (subj, data) => data == "deleted" && subj.name == "test1"),
|
||||||
|
TestUtils.topicObserved("cookie-changed", (subj, data) => data == "deleted" && subj.name == "test2"),
|
||||||
|
TestUtils.topicObserved("cookie-changed", (subj, data) => data == "deleted" && subj.name == "test3"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click the "Clear data" button.
|
||||||
|
siteDataUpdated = TestUtils.topicObserved("sitedatamanager:sites-updated");
|
||||||
|
let hideEvent = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
|
||||||
|
let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
|
||||||
|
clearButton.click();
|
||||||
|
await hideEvent;
|
||||||
|
await removeDialogPromise;
|
||||||
|
|
||||||
|
await siteDataUpdated;
|
||||||
|
|
||||||
|
// Check that cookies were deleted.
|
||||||
|
if (testCookies) {
|
||||||
|
await cookiesCleared;
|
||||||
|
let uri = Services.io.newURI(TEST_ORIGIN);
|
||||||
|
is(Services.cookies.countCookiesFromHost(uri.host), 0, "Cookies from the base domain should be cleared");
|
||||||
|
uri = Services.io.newURI(TEST_SUB_ORIGIN);
|
||||||
|
is(Services.cookies.countCookiesFromHost(uri.host), 0, "Cookies from the sub domain should be cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that quota storage was deleted.
|
||||||
|
if (testQuota) {
|
||||||
|
await TestUtils.waitForCondition(async () => {
|
||||||
|
let usage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
|
||||||
|
return usage == 0;
|
||||||
|
}, "Should have no data for the base origin.");
|
||||||
|
|
||||||
|
let usage = await SiteDataTestUtils.getQuotaUsage(TEST_SUB_ORIGIN);
|
||||||
|
is(usage, 0, "Should have no data for the sub origin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the site identity panel again to check that the button isn't shown anymore.
|
||||||
|
promisePanelOpen = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
|
||||||
|
siteDataUpdated = TestUtils.topicObserved("sitedatamanager:sites-updated");
|
||||||
|
gIdentityHandler._identityBox.click();
|
||||||
|
await promisePanelOpen;
|
||||||
|
await siteDataUpdated;
|
||||||
|
|
||||||
|
ok(clearFooter.hidden, "The clear data footer is hidden after clearing data.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test removing quota managed storage.
|
||||||
|
add_task(async function test_ClearSiteData() {
|
||||||
|
await testClearing(true, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test removing cookies.
|
||||||
|
add_task(async function test_ClearCookies() {
|
||||||
|
await testClearing(false, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test removing both.
|
||||||
|
add_task(async function test_ClearCookiesAndSiteData() {
|
||||||
|
await testClearing(true, true);
|
||||||
|
});
|
|
@ -95,6 +95,16 @@
|
||||||
<description id="identity-popup-permission-empty-hint">&identity.permissionsEmpty;</description>
|
<description id="identity-popup-permission-empty-hint">&identity.permissionsEmpty;</description>
|
||||||
</vbox>
|
</vbox>
|
||||||
</hbox>
|
</hbox>
|
||||||
|
|
||||||
|
<!-- Clear Site Data Button -->
|
||||||
|
<vbox hidden="true"
|
||||||
|
id="identity-popup-clear-sitedata-footer"
|
||||||
|
class="identity-popup-footer">
|
||||||
|
<button class="subviewkeynav"
|
||||||
|
id="identity-popup-clear-sitedata-button"
|
||||||
|
label="&identity.clearSiteData;"
|
||||||
|
oncommand="gIdentityHandler.clearSiteData(event);"/>
|
||||||
|
</vbox>
|
||||||
</panelview>
|
</panelview>
|
||||||
|
|
||||||
<!-- Security SubView -->
|
<!-- Security SubView -->
|
||||||
|
@ -178,7 +188,7 @@
|
||||||
oncommand="gIdentityHandler.enableMixedContentProtection()"/>
|
oncommand="gIdentityHandler.enableMixedContentProtection()"/>
|
||||||
</vbox>
|
</vbox>
|
||||||
|
|
||||||
<vbox id="identity-popup-securityView-footer">
|
<vbox id="identity-popup-more-info-footer" class="identity-popup-footer">
|
||||||
<!-- More Security Information -->
|
<!-- More Security Information -->
|
||||||
<button id="identity-popup-more-info" class="subviewkeynav"
|
<button id="identity-popup-more-info" class="subviewkeynav"
|
||||||
label="&identity.moreInfoLinkText2;"
|
label="&identity.moreInfoLinkText2;"
|
||||||
|
|
|
@ -577,9 +577,20 @@ var paymentDialogWrapper = {
|
||||||
|
|
||||||
// Select the new record
|
// Select the new record
|
||||||
if (selectedStateKey) {
|
if (selectedStateKey) {
|
||||||
Object.assign(successStateChange, {
|
if (selectedStateKey.length == 1) {
|
||||||
[selectedStateKey]: guid,
|
Object.assign(successStateChange, {
|
||||||
});
|
[selectedStateKey[0]]: guid,
|
||||||
|
});
|
||||||
|
} else if (selectedStateKey.length == 2) {
|
||||||
|
// Need to keep properties like preserveFieldValues from getting removed.
|
||||||
|
let subObj = Object.assign({}, successStateChange[selectedStateKey[0]]);
|
||||||
|
subObj[selectedStateKey[1]] = guid;
|
||||||
|
Object.assign(successStateChange, {
|
||||||
|
[selectedStateKey[0]]: subObj,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`selectedStateKey not supported: '${selectedStateKey}'`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendMessageToContent("updateState", successStateChange);
|
this.sendMessageToContent("updateState", successStateChange);
|
||||||
|
|
|
@ -86,6 +86,7 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
|
||||||
let record = {};
|
let record = {};
|
||||||
let {
|
let {
|
||||||
page,
|
page,
|
||||||
|
"address-page": addressPage,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
if (this.id && page && page.id !== this.id) {
|
if (this.id && page && page.id !== this.id) {
|
||||||
|
@ -101,23 +102,23 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
|
||||||
this.backButton.hidden = page.onboardingWizard;
|
this.backButton.hidden = page.onboardingWizard;
|
||||||
this.cancelButton.hidden = !page.onboardingWizard;
|
this.cancelButton.hidden = !page.onboardingWizard;
|
||||||
|
|
||||||
if (page.addressFields) {
|
if (addressPage.addressFields) {
|
||||||
this.setAttribute("address-fields", page.addressFields);
|
this.setAttribute("address-fields", addressPage.addressFields);
|
||||||
} else {
|
} else {
|
||||||
this.removeAttribute("address-fields");
|
this.removeAttribute("address-fields");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pageTitle.textContent = page.title;
|
this.pageTitle.textContent = addressPage.title;
|
||||||
this.genericErrorText.textContent = page.error;
|
this.genericErrorText.textContent = page.error;
|
||||||
|
|
||||||
let editing = !!page.guid;
|
let editing = !!addressPage.guid;
|
||||||
let addresses = paymentRequest.getAddresses(state);
|
let addresses = paymentRequest.getAddresses(state);
|
||||||
|
|
||||||
// If an address is selected we want to edit it.
|
// If an address is selected we want to edit it.
|
||||||
if (editing) {
|
if (editing) {
|
||||||
record = addresses[page.guid];
|
record = addresses[addressPage.guid];
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new Error("Trying to edit a non-existing address: " + page.guid);
|
throw new Error("Trying to edit a non-existing address: " + addressPage.guid);
|
||||||
}
|
}
|
||||||
// When editing an existing record, prevent changes to persistence
|
// When editing an existing record, prevent changes to persistence
|
||||||
this.persistCheckbox.hidden = true;
|
this.persistCheckbox.hidden = true;
|
||||||
|
@ -146,11 +147,19 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case this.backButton: {
|
case this.backButton: {
|
||||||
this.requestStore.setState({
|
let currentState = this.requestStore.getState();
|
||||||
|
const previousId = currentState.page.previousId;
|
||||||
|
let state = {
|
||||||
page: {
|
page: {
|
||||||
id: "payment-summary",
|
id: previousId || "payment-summary",
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
if (previousId) {
|
||||||
|
state[previousId] = Object.assign({}, currentState[previousId], {
|
||||||
|
preserveFieldValues: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.requestStore.setState(state);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case this.saveButton: {
|
case this.saveButton: {
|
||||||
|
@ -165,14 +174,16 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
|
||||||
|
|
||||||
saveRecord() {
|
saveRecord() {
|
||||||
let record = this.formHandler.buildFormObject();
|
let record = this.formHandler.buildFormObject();
|
||||||
|
let currentState = this.requestStore.getState();
|
||||||
let {
|
let {
|
||||||
page,
|
page,
|
||||||
tempAddresses,
|
tempAddresses,
|
||||||
savedBasicCards,
|
savedBasicCards,
|
||||||
} = this.requestStore.getState();
|
"address-page": addressPage,
|
||||||
let editing = !!page.guid;
|
} = currentState;
|
||||||
|
let editing = !!addressPage.guid;
|
||||||
|
|
||||||
if (editing ? (page.guid in tempAddresses) : !this.persistCheckbox.checked) {
|
if (editing ? (addressPage.guid in tempAddresses) : !this.persistCheckbox.checked) {
|
||||||
record.isTemporary = true;
|
record.isTemporary = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,28 +194,36 @@ export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement
|
||||||
onboardingWizard: page.onboardingWizard,
|
onboardingWizard: page.onboardingWizard,
|
||||||
error: this.dataset.errorGenericSave,
|
error: this.dataset.errorGenericSave,
|
||||||
},
|
},
|
||||||
|
"address-page": addressPage,
|
||||||
},
|
},
|
||||||
preserveOldProperties: true,
|
preserveOldProperties: true,
|
||||||
selectedStateKey: page.selectedStateKey,
|
selectedStateKey: page.selectedStateKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const previousId = page.previousId;
|
||||||
if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
|
if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
|
||||||
state.successStateChange = {
|
state.successStateChange = {
|
||||||
page: {
|
page: {
|
||||||
id: "basic-card-page",
|
id: "basic-card-page",
|
||||||
onboardingWizard: true,
|
previousId: "address-page",
|
||||||
guid: null,
|
onboardingWizard: page.onboardingWizard,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
state.successStateChange = {
|
state.successStateChange = {
|
||||||
page: {
|
page: {
|
||||||
id: "payment-summary",
|
id: previousId || "payment-summary",
|
||||||
|
onboardingWizard: page.onboardingWizard,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentRequest.updateAutofillRecord("addresses", record, page.guid, state);
|
if (previousId) {
|
||||||
|
state.successStateChange[previousId] = Object.assign({}, currentState[previousId]);
|
||||||
|
state.successStateChange[previousId].preserveFieldValues = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentRequest.updateAutofillRecord("addresses", record, addressPage.guid, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,22 +168,24 @@ export default class AddressPicker extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
let nextState = {
|
let nextState = {
|
||||||
page: {
|
page: {
|
||||||
id: "address-page",
|
id: "address-page",
|
||||||
selectedStateKey: this.selectedStateKey,
|
},
|
||||||
|
"address-page": {
|
||||||
addressFields: this.getAttribute("address-fields"),
|
addressFields: this.getAttribute("address-fields"),
|
||||||
|
selectedStateKey: this.selectedStateKey,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case this.addLink: {
|
case this.addLink: {
|
||||||
nextState.page.guid = null;
|
nextState["address-page"].guid = null;
|
||||||
nextState.page.title = this.dataset.addAddressTitle;
|
nextState["address-page"].title = this.dataset.addAddressTitle;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case this.editLink: {
|
case this.editLink: {
|
||||||
let state = this.requestStore.getState();
|
let state = this.requestStore.getState();
|
||||||
let selectedAddressGUID = state[this.selectedStateKey];
|
let selectedAddressGUID = state[this.selectedStateKey];
|
||||||
nextState.page.guid = selectedAddressGUID;
|
nextState["address-page"].guid = selectedAddressGUID;
|
||||||
nextState.page.title = this.dataset.editAddressTitle;
|
nextState["address-page"].title = this.dataset.editAddressTitle;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
|
@ -27,6 +27,15 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
this.cancelButton.className = "cancel-button";
|
this.cancelButton.className = "cancel-button";
|
||||||
this.cancelButton.addEventListener("click", this);
|
this.cancelButton.addEventListener("click", this);
|
||||||
|
|
||||||
|
this.addressAddLink = document.createElement("a");
|
||||||
|
this.addressAddLink.className = "add-link";
|
||||||
|
this.addressAddLink.href = "javascript:void(0)";
|
||||||
|
this.addressAddLink.addEventListener("click", this);
|
||||||
|
this.addressEditLink = document.createElement("a");
|
||||||
|
this.addressEditLink.className = "edit-link";
|
||||||
|
this.addressEditLink.href = "javascript:void(0)";
|
||||||
|
this.addressEditLink.addEventListener("click", this);
|
||||||
|
|
||||||
this.backButton = document.createElement("button");
|
this.backButton = document.createElement("button");
|
||||||
this.backButton.className = "back-button";
|
this.backButton.className = "back-button";
|
||||||
this.backButton.addEventListener("click", this);
|
this.backButton.addEventListener("click", this);
|
||||||
|
@ -72,6 +81,13 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
getAddressLabel: PaymentDialogUtils.getAddressLabel,
|
getAddressLabel: PaymentDialogUtils.getAddressLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let fragment = document.createDocumentFragment();
|
||||||
|
fragment.append(this.addressAddLink);
|
||||||
|
fragment.append(" ");
|
||||||
|
fragment.append(this.addressEditLink);
|
||||||
|
let billingAddressRow = this.form.querySelector(".billingAddressRow");
|
||||||
|
billingAddressRow.appendChild(fragment);
|
||||||
|
|
||||||
this.appendChild(this.persistCheckbox);
|
this.appendChild(this.persistCheckbox);
|
||||||
this.appendChild(this.genericErrorText);
|
this.appendChild(this.genericErrorText);
|
||||||
this.appendChild(this.cancelButton);
|
this.appendChild(this.cancelButton);
|
||||||
|
@ -87,6 +103,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
let {
|
let {
|
||||||
page,
|
page,
|
||||||
selectedShippingAddress,
|
selectedShippingAddress,
|
||||||
|
"basic-card-page": basicCardPage,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
if (this.id && page && page.id !== this.id) {
|
if (this.id && page && page.id !== this.id) {
|
||||||
|
@ -98,6 +115,8 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
this.backButton.textContent = this.dataset.backButtonLabel;
|
this.backButton.textContent = this.dataset.backButtonLabel;
|
||||||
this.saveButton.textContent = this.dataset.saveButtonLabel;
|
this.saveButton.textContent = this.dataset.saveButtonLabel;
|
||||||
this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
|
this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
|
||||||
|
this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
|
||||||
|
this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
|
||||||
|
|
||||||
// The back button is temporarily hidden(See Bug 1462461).
|
// The back button is temporarily hidden(See Bug 1462461).
|
||||||
this.backButton.hidden = !!page.onboardingWizard;
|
this.backButton.hidden = !!page.onboardingWizard;
|
||||||
|
@ -109,22 +128,22 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
|
|
||||||
this.genericErrorText.textContent = page.error;
|
this.genericErrorText.textContent = page.error;
|
||||||
|
|
||||||
let editing = !!page.guid;
|
let editing = !!basicCardPage.guid;
|
||||||
this.form.querySelector("#cc-number").disabled = editing;
|
this.form.querySelector("#cc-number").disabled = editing;
|
||||||
|
|
||||||
// If a card is selected we want to edit it.
|
// If a card is selected we want to edit it.
|
||||||
if (editing) {
|
if (editing) {
|
||||||
this.pageTitle.textContent = this.dataset.editBasicCardTitle;
|
this.pageTitle.textContent = this.dataset.editBasicCardTitle;
|
||||||
record = basicCards[page.guid];
|
record = basicCards[basicCardPage.guid];
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new Error("Trying to edit a non-existing card: " + page.guid);
|
throw new Error("Trying to edit a non-existing card: " + basicCardPage.guid);
|
||||||
}
|
}
|
||||||
// When editing an existing record, prevent changes to persistence
|
// When editing an existing record, prevent changes to persistence
|
||||||
this.persistCheckbox.hidden = true;
|
this.persistCheckbox.hidden = true;
|
||||||
} else {
|
} else {
|
||||||
this.pageTitle.textContent = this.dataset.addBasicCardTitle;
|
this.pageTitle.textContent = this.dataset.addBasicCardTitle;
|
||||||
// Use a currently selected shipping address as the default billing address
|
// Use a currently selected shipping address as the default billing address
|
||||||
if (selectedShippingAddress) {
|
if (!record.billingAddressGUID && selectedShippingAddress) {
|
||||||
record.billingAddressGUID = selectedShippingAddress;
|
record.billingAddressGUID = selectedShippingAddress;
|
||||||
}
|
}
|
||||||
// Adding a new record: default persistence to checked when in a not-private session
|
// Adding a new record: default persistence to checked when in a not-private session
|
||||||
|
@ -132,7 +151,15 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
this.persistCheckbox.checked = !state.isPrivate;
|
this.persistCheckbox.checked = !state.isPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formHandler.loadRecord(record, addresses);
|
this.formHandler.loadRecord(record, addresses, basicCardPage.preserveFieldValues);
|
||||||
|
|
||||||
|
this.form.querySelector(".billingAddressRow").hidden = false;
|
||||||
|
|
||||||
|
if (basicCardPage.billingAddressGUID) {
|
||||||
|
let addressGuid = basicCardPage.billingAddressGUID;
|
||||||
|
let billingAddressSelect = this.form.querySelector("#billingAddressGUID");
|
||||||
|
billingAddressSelect.value = addressGuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(event) {
|
handleEvent(event) {
|
||||||
|
@ -150,6 +177,36 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
paymentRequest.cancel();
|
paymentRequest.cancel();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case this.addressAddLink:
|
||||||
|
case this.addressEditLink: {
|
||||||
|
let {
|
||||||
|
"basic-card-page": basicCardPage,
|
||||||
|
} = this.requestStore.getState();
|
||||||
|
let nextState = {
|
||||||
|
page: {
|
||||||
|
id: "address-page",
|
||||||
|
previousId: "basic-card-page",
|
||||||
|
selectedStateKey: ["basic-card-page", "billingAddressGUID"],
|
||||||
|
},
|
||||||
|
"address-page": {
|
||||||
|
guid: null,
|
||||||
|
title: this.dataset.billingAddressTitleAdd,
|
||||||
|
},
|
||||||
|
"basic-card-page": {
|
||||||
|
preserveFieldValues: true,
|
||||||
|
guid: basicCardPage.guid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let billingAddressGUID = this.form.querySelector("#billingAddressGUID");
|
||||||
|
let selectedOption = billingAddressGUID.selectedOptions.length &&
|
||||||
|
billingAddressGUID.selectedOptions[0];
|
||||||
|
if (evt.target == this.addressEditLink && selectedOption && selectedOption.value) {
|
||||||
|
nextState["address-page"].title = this.dataset.billingAddressTitleEdit;
|
||||||
|
nextState["address-page"].guid = selectedOption.value;
|
||||||
|
}
|
||||||
|
this.requestStore.setState(nextState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case this.backButton: {
|
case this.backButton: {
|
||||||
this.requestStore.setState({
|
this.requestStore.setState({
|
||||||
page: {
|
page: {
|
||||||
|
@ -170,13 +227,15 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
|
|
||||||
saveRecord() {
|
saveRecord() {
|
||||||
let record = this.formHandler.buildFormObject();
|
let record = this.formHandler.buildFormObject();
|
||||||
|
let currentState = this.requestStore.getState();
|
||||||
let {
|
let {
|
||||||
page,
|
page,
|
||||||
tempBasicCards,
|
tempBasicCards,
|
||||||
} = this.requestStore.getState();
|
"basic-card-page": basicCardPage,
|
||||||
let editing = !!page.guid;
|
} = currentState;
|
||||||
|
let editing = !!basicCardPage.guid;
|
||||||
|
|
||||||
if (editing ? (page.guid in tempBasicCards) : !this.persistCheckbox.checked) {
|
if (editing ? (basicCardPage.guid in tempBasicCards) : !this.persistCheckbox.checked) {
|
||||||
record.isTemporary = true;
|
record.isTemporary = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +249,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
record["cc-number"] = record["cc-number"] || "";
|
record["cc-number"] = record["cc-number"] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentRequest.updateAutofillRecord("creditCards", record, page.guid, {
|
let state = {
|
||||||
errorStateChange: {
|
errorStateChange: {
|
||||||
page: {
|
page: {
|
||||||
id: "basic-card-page",
|
id: "basic-card-page",
|
||||||
|
@ -198,13 +257,20 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLEleme
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
preserveOldProperties: true,
|
preserveOldProperties: true,
|
||||||
selectedStateKey: "selectedPaymentCard",
|
selectedStateKey: ["selectedPaymentCard"],
|
||||||
successStateChange: {
|
successStateChange: {
|
||||||
page: {
|
page: {
|
||||||
id: "payment-summary",
|
id: "payment-summary",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const previousId = page.previousId;
|
||||||
|
if (previousId) {
|
||||||
|
state.successStateChange[previousId] = Object.assign({}, currentState[previousId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentRequest.updateAutofillRecord("creditCards", record, basicCardPage.guid, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,17 +133,18 @@ export default class PaymentMethodPicker extends PaymentStateSubscriberMixin(HTM
|
||||||
page: {
|
page: {
|
||||||
id: "basic-card-page",
|
id: "basic-card-page",
|
||||||
},
|
},
|
||||||
|
"basic-card-page": {},
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case this.addLink: {
|
case this.addLink: {
|
||||||
nextState.page.guid = null;
|
nextState["basic-card-page"].guid = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case this.editLink: {
|
case this.editLink: {
|
||||||
let state = this.requestStore.getState();
|
let state = this.requestStore.getState();
|
||||||
let selectedPaymentCardGUID = state[this.selectedStateKey];
|
let selectedPaymentCardGUID = state[this.selectedStateKey];
|
||||||
nextState.page.guid = selectedPaymentCardGUID;
|
nextState["basic-card-page"].guid = selectedPaymentCardGUID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
|
@ -15,10 +15,21 @@ export let requestStore = new PaymentsStore({
|
||||||
changesPrevented: false,
|
changesPrevented: false,
|
||||||
completionState: "initial",
|
completionState: "initial",
|
||||||
orderDetailsShowing: false,
|
orderDetailsShowing: false,
|
||||||
|
"basic-card-page": {
|
||||||
|
guid: null,
|
||||||
|
// preserveFieldValues: true,
|
||||||
|
},
|
||||||
|
"address-page": {
|
||||||
|
guid: null,
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
"payment-summary": {
|
||||||
|
},
|
||||||
page: {
|
page: {
|
||||||
id: "payment-summary",
|
id: "payment-summary",
|
||||||
|
previousId: null,
|
||||||
// onboardingWizard: true,
|
// onboardingWizard: true,
|
||||||
// error: "My error",
|
// error: "",
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
tabId: null,
|
tabId: null,
|
||||||
|
|
|
@ -134,7 +134,6 @@ var paymentRequest = {
|
||||||
state.page = {
|
state.page = {
|
||||||
id: "basic-card-page",
|
id: "basic-card-page",
|
||||||
onboardingWizard: true,
|
onboardingWizard: true,
|
||||||
guid: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
<!ENTITY deliveryAddress.editPage.title "Edit Delivery Address">
|
<!ENTITY deliveryAddress.editPage.title "Edit Delivery Address">
|
||||||
<!ENTITY pickupAddress.addPage.title "Add Pickup Address">
|
<!ENTITY pickupAddress.addPage.title "Add Pickup Address">
|
||||||
<!ENTITY pickupAddress.editPage.title "Edit Pickup Address">
|
<!ENTITY pickupAddress.editPage.title "Edit Pickup Address">
|
||||||
|
<!ENTITY billingAddress.addPage.title "Add Billing Address">
|
||||||
|
<!ENTITY billingAddress.editPage.title "Edit Billing Address">
|
||||||
<!ENTITY basicCard.addPage.title "Add Credit Card">
|
<!ENTITY basicCard.addPage.title "Add Credit Card">
|
||||||
<!ENTITY basicCard.editPage.title "Edit Credit Card">
|
<!ENTITY basicCard.editPage.title "Edit Credit Card">
|
||||||
<!ENTITY payer.addPage.title "Add Payer Contact">
|
<!ENTITY payer.addPage.title "Add Payer Contact">
|
||||||
|
@ -39,6 +41,8 @@
|
||||||
<!ENTITY orderDetailsLabel "Order Details">
|
<!ENTITY orderDetailsLabel "Order Details">
|
||||||
<!ENTITY orderTotalLabel "Total">
|
<!ENTITY orderTotalLabel "Total">
|
||||||
<!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card.">
|
<!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card.">
|
||||||
|
<!ENTITY basicCardPage.addressAddLink.label "Add">
|
||||||
|
<!ENTITY basicCardPage.addressEditLink.label "Edit">
|
||||||
<!ENTITY basicCardPage.backButton.label "Back">
|
<!ENTITY basicCardPage.backButton.label "Back">
|
||||||
<!ENTITY basicCardPage.saveButton.label "Save">
|
<!ENTITY basicCardPage.saveButton.label "Save">
|
||||||
<!ENTITY basicCardPage.persistCheckbox.label "Save credit card to Firefox (Security code will not be saved)">
|
<!ENTITY basicCardPage.persistCheckbox.label "Save credit card to Firefox (Security code will not be saved)">
|
||||||
|
@ -138,6 +142,10 @@
|
||||||
data-add-basic-card-title="&basicCard.addPage.title;"
|
data-add-basic-card-title="&basicCard.addPage.title;"
|
||||||
data-edit-basic-card-title="&basicCard.editPage.title;"
|
data-edit-basic-card-title="&basicCard.editPage.title;"
|
||||||
data-error-generic-save="&basicCardPage.error.genericSave;"
|
data-error-generic-save="&basicCardPage.error.genericSave;"
|
||||||
|
data-address-add-link-label="&basicCardPage.addressAddLink.label;"
|
||||||
|
data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
|
||||||
|
data-billing-address-title-add="&billingAddress.addPage.title;"
|
||||||
|
data-billing-address-title-edit="&billingAddress.editPage.title;"
|
||||||
data-back-button-label="&basicCardPage.backButton.label;"
|
data-back-button-label="&basicCardPage.backButton.label;"
|
||||||
data-save-button-label="&basicCardPage.saveButton.label;"
|
data-save-button-label="&basicCardPage.saveButton.label;"
|
||||||
data-cancel-button-label="&cancelPaymentButton.label;"
|
data-cancel-button-label="&cancelPaymentButton.label;"
|
||||||
|
|
|
@ -54,15 +54,15 @@ add_task(async function test_add_link() {
|
||||||
addLink.click();
|
addLink.click();
|
||||||
|
|
||||||
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "address-page" && !state.page.guid;
|
return state.page.id == "address-page" && !state["address-page"].guid;
|
||||||
}, "Check add page state");
|
}, "Check add page state");
|
||||||
|
|
||||||
let title = content.document.querySelector("address-form h1");
|
let title = content.document.querySelector("address-form h1");
|
||||||
is(title.textContent, "Add Shipping Address", "Page title should be set");
|
is(title.textContent, "Add Shipping Address", "Page title should be set");
|
||||||
|
|
||||||
let persistInput = content.document.querySelector("address-form labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
|
||||||
ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
|
ok(!persistCheckbox.hidden, "checkbox should be visible when adding a new address");
|
||||||
ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
|
ok(Cu.waiveXrays(persistCheckbox).checked, "persist checkbox should be checked by default");
|
||||||
|
|
||||||
info("filling fields");
|
info("filling fields");
|
||||||
for (let [key, val] of Object.entries(address)) {
|
for (let [key, val] of Object.entries(address)) {
|
||||||
|
@ -145,14 +145,14 @@ add_task(async function test_edit_link() {
|
||||||
editLink.click();
|
editLink.click();
|
||||||
|
|
||||||
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "address-page" && !!state.page.guid;
|
return state.page.id == "address-page" && !!state["address-page"].guid;
|
||||||
}, "Check edit page state");
|
}, "Check edit page state");
|
||||||
|
|
||||||
let title = content.document.querySelector("address-form h1");
|
let title = content.document.querySelector("address-form h1");
|
||||||
is(title.textContent, "Edit Shipping Address", "Page title should be set");
|
is(title.textContent, "Edit Shipping Address", "Page title should be set");
|
||||||
|
|
||||||
let persistInput = content.document.querySelector("address-form labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
|
||||||
ok(persistInput.hidden, "checkbox should be hidden when editing an address");
|
ok(persistCheckbox.hidden, "checkbox should be hidden when editing an address");
|
||||||
|
|
||||||
info("overwriting field values");
|
info("overwriting field values");
|
||||||
for (let [key, val] of Object.entries(address)) {
|
for (let [key, val] of Object.entries(address)) {
|
||||||
|
@ -230,15 +230,15 @@ add_task(async function test_add_payer_contact_name_email_link() {
|
||||||
addLink.click();
|
addLink.click();
|
||||||
|
|
||||||
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "address-page" && !state.page.guid;
|
return state.page.id == "address-page" && !state["address-page"].guid;
|
||||||
}, "Check add page state");
|
}, "Check add page state");
|
||||||
|
|
||||||
let title = content.document.querySelector("address-form h1");
|
let title = content.document.querySelector("address-form h1");
|
||||||
is(title.textContent, "Add Payer Contact", "Page title should be set");
|
is(title.textContent, "Add Payer Contact", "Page title should be set");
|
||||||
|
|
||||||
let persistInput = content.document.querySelector("address-form labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
|
||||||
ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
|
ok(!persistCheckbox.hidden, "checkbox should be visible when adding a new address");
|
||||||
ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
|
ok(Cu.waiveXrays(persistCheckbox).checked, "persist checkbox should be checked by default");
|
||||||
|
|
||||||
info("filling fields");
|
info("filling fields");
|
||||||
for (let [key, val] of Object.entries(address)) {
|
for (let [key, val] of Object.entries(address)) {
|
||||||
|
@ -317,15 +317,14 @@ add_task(async function test_edit_payer_contact_name_email_phone_link() {
|
||||||
editLink.click();
|
editLink.click();
|
||||||
|
|
||||||
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
info("state.page.id: " + state.page.id + "; state.page.guid: " + state.page.guid);
|
return state.page.id == "address-page" && !!state["address-page"].guid;
|
||||||
return state.page.id == "address-page" && !!state.page.guid;
|
|
||||||
}, "Check edit page state");
|
}, "Check edit page state");
|
||||||
|
|
||||||
let title = content.document.querySelector("address-form h1");
|
let title = content.document.querySelector("address-form h1");
|
||||||
is(title.textContent, "Edit Payer Contact", "Page title should be set");
|
is(title.textContent, "Edit Payer Contact", "Page title should be set");
|
||||||
|
|
||||||
let persistInput = content.document.querySelector("address-form labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
|
||||||
ok(persistInput.hidden, "checkbox should be hidden when editing an address");
|
ok(persistCheckbox.hidden, "checkbox should be hidden when editing an address");
|
||||||
|
|
||||||
info("overwriting field values");
|
info("overwriting field values");
|
||||||
for (let [key, val] of Object.entries(address)) {
|
for (let [key, val] of Object.entries(address)) {
|
||||||
|
@ -437,9 +436,10 @@ add_task(async function test_private_persist_addresses() {
|
||||||
PaymentTestUtils: PTU,
|
PaymentTestUtils: PTU,
|
||||||
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
|
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
|
||||||
|
|
||||||
let persistInput = content.document.querySelector("address-form labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
|
||||||
ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
|
ok(!persistCheckbox.hidden, "checkbox should be visible when adding a new address");
|
||||||
ok(!Cu.waiveXrays(persistInput).checked, "persist checkbox should be unchecked by default");
|
ok(!Cu.waiveXrays(persistCheckbox).checked,
|
||||||
|
"persist checkbox should be unchecked by default");
|
||||||
|
|
||||||
info("add the temp address");
|
info("add the temp address");
|
||||||
let addressToAdd = PTU.Addresses.Temp;
|
let addressToAdd = PTU.Addresses.Temp;
|
||||||
|
|
|
@ -18,16 +18,21 @@ add_task(async function test_add_link() {
|
||||||
addLink.click();
|
addLink.click();
|
||||||
|
|
||||||
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "basic-card-page" && !state.page.guid;
|
return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
|
||||||
}, "Check add page state");
|
}, "Check add page state");
|
||||||
|
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return Object.keys(state.savedBasicCards).length == 0 &&
|
||||||
|
Object.keys(state.savedAddresses).length == 0;
|
||||||
|
}, "Check no cards or addresses present at beginning of test");
|
||||||
|
|
||||||
let title = content.document.querySelector("basic-card-form h1");
|
let title = content.document.querySelector("basic-card-form h1");
|
||||||
is(title.textContent, "Add Credit Card", "Add title should be set");
|
is(title.textContent, "Add Credit Card", "Add title should be set");
|
||||||
|
|
||||||
ok(!state.isPrivate,
|
ok(!state.isPrivate,
|
||||||
"isPrivate flag is not set when paymentrequest is shown from a non-private session");
|
"isPrivate flag is not set when paymentrequest is shown from a non-private session");
|
||||||
let persistInput = content.document.querySelector("basic-card-form labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("basic-card-form labelled-checkbox");
|
||||||
ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
|
ok(Cu.waiveXrays(persistCheckbox).checked, "persist checkbox should be checked by default");
|
||||||
|
|
||||||
let year = (new Date()).getFullYear();
|
let year = (new Date()).getFullYear();
|
||||||
let card = {
|
let card = {
|
||||||
|
@ -44,11 +49,88 @@ add_task(async function test_add_link() {
|
||||||
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
|
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
|
||||||
|
isnot(billingAddressSelect.getBoundingClientRect().height, 0,
|
||||||
|
"The billing address selector should always be visible");
|
||||||
|
is(billingAddressSelect.childElementCount, 1,
|
||||||
|
"Only one child option should exist by default");
|
||||||
|
is(billingAddressSelect.children[0].value, "",
|
||||||
|
"The only option should be the blank/empty option");
|
||||||
|
|
||||||
|
let addressAddLink = content.document.querySelector(".billingAddressRow .add-link");
|
||||||
|
addressAddLink.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "address-page" && !state["address-page"].guid;
|
||||||
|
}, "Check address page state");
|
||||||
|
|
||||||
|
let addressTitle = content.document.querySelector("address-form h1");
|
||||||
|
is(addressTitle.textContent, "Add Billing Address",
|
||||||
|
"Address on add address page should be correct");
|
||||||
|
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return Object.keys(state.savedBasicCards).length == 0;
|
||||||
|
}, "Check card was not added when clicking the 'add' address button");
|
||||||
|
|
||||||
|
let addressBackButton = content.document.querySelector("address-form .back-button");
|
||||||
|
addressBackButton.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "basic-card-page" && !state["basic-card-page"].guid &&
|
||||||
|
Object.keys(state.savedAddresses).length == 0;
|
||||||
|
}, "Check basic-card page, but card should not be saved and no addresses present");
|
||||||
|
|
||||||
|
is(title.textContent, "Add Credit Card", "Add title should be still be on credit card page");
|
||||||
|
|
||||||
|
for (let [key, val] of Object.entries(card)) {
|
||||||
|
let field = content.document.getElementById(key);
|
||||||
|
is(field.value, val, "Field should still have previous value entered");
|
||||||
|
ok(!field.disabled, "Fields should still be enabled for editing");
|
||||||
|
}
|
||||||
|
|
||||||
|
addressAddLink.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "address-page" && !state["address-page"].guid;
|
||||||
|
}, "Check address page state");
|
||||||
|
|
||||||
|
info("filling address fields");
|
||||||
|
for (let [key, val] of Object.entries(PTU.Addresses.TimBL)) {
|
||||||
|
let field = content.document.getElementById(key);
|
||||||
|
if (!field) {
|
||||||
|
ok(false, `${key} field not found`);
|
||||||
|
}
|
||||||
|
field.value = val;
|
||||||
|
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.document.querySelector("address-form button:last-of-type").click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "basic-card-page" && !state["basic-card-page"].guid &&
|
||||||
|
Object.keys(state.savedAddresses).length == 1;
|
||||||
|
}, "Check address was added and we're back on basic-card page (add)");
|
||||||
|
|
||||||
|
ok(state["basic-card-page"].preserveFieldValues,
|
||||||
|
"preserveFieldValues should be set when coming back from address-page");
|
||||||
|
|
||||||
|
ok(state["basic-card-page"].billingAddressGUID,
|
||||||
|
"billingAddressGUID should be set when coming back from address-page");
|
||||||
|
|
||||||
|
is(billingAddressSelect.childElementCount, 2,
|
||||||
|
"Two options should exist in the billingAddressSelect");
|
||||||
|
let selectedOption =
|
||||||
|
billingAddressSelect.children[billingAddressSelect.selectedIndex];
|
||||||
|
let selectedAddressGuid = selectedOption.value;
|
||||||
|
is(selectedAddressGuid, Object.values(state.savedAddresses)[0].guid,
|
||||||
|
"The select should have the new address selected");
|
||||||
|
|
||||||
|
for (let [key, val] of Object.entries(card)) {
|
||||||
|
let field = content.document.getElementById(key);
|
||||||
|
is(field.value, val, `Field #${key} should have value`);
|
||||||
|
}
|
||||||
|
|
||||||
content.document.querySelector("basic-card-form button:last-of-type").click();
|
content.document.querySelector("basic-card-form button:last-of-type").click();
|
||||||
|
|
||||||
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return Object.keys(state.savedBasicCards).length == 1;
|
return Object.keys(state.savedBasicCards).length == 1;
|
||||||
}, "Check card was added");
|
}, "Check card was not added again");
|
||||||
|
|
||||||
let cardGUIDs = Object.keys(state.savedBasicCards);
|
let cardGUIDs = Object.keys(state.savedBasicCards);
|
||||||
is(cardGUIDs.length, 1, "Check there is one card");
|
is(cardGUIDs.length, 1, "Check there is one card");
|
||||||
|
@ -57,6 +139,8 @@ add_task(async function test_add_link() {
|
||||||
for (let [key, val] of Object.entries(card)) {
|
for (let [key, val] of Object.entries(card)) {
|
||||||
is(savedCard[key], val, "Check " + key);
|
is(savedCard[key], val, "Check " + key);
|
||||||
}
|
}
|
||||||
|
is(savedCard.billingAddressGUID, selectedAddressGuid,
|
||||||
|
"The saved card should be associated with the billing address");
|
||||||
|
|
||||||
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "payment-summary";
|
return state.page.id == "payment-summary";
|
||||||
|
@ -80,9 +164,14 @@ add_task(async function test_edit_link() {
|
||||||
editLink.click();
|
editLink.click();
|
||||||
|
|
||||||
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "basic-card-page" && !!state.page.guid;
|
return state.page.id == "basic-card-page" && state["basic-card-page"].guid;
|
||||||
}, "Check edit page state");
|
}, "Check edit page state");
|
||||||
|
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return Object.keys(state.savedBasicCards).length == 1 &&
|
||||||
|
Object.keys(state.savedAddresses).length == 1;
|
||||||
|
}, "Check card and address present at beginning of test");
|
||||||
|
|
||||||
let title = content.document.querySelector("basic-card-form h1");
|
let title = content.document.querySelector("basic-card-form h1");
|
||||||
is(title.textContent, "Edit Credit Card", "Edit title should be set");
|
is(title.textContent, "Edit Credit Card", "Edit title should be set");
|
||||||
|
|
||||||
|
@ -102,6 +191,90 @@ add_task(async function test_edit_link() {
|
||||||
}
|
}
|
||||||
ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled");
|
ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled");
|
||||||
|
|
||||||
|
let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
|
||||||
|
is(billingAddressSelect.childElementCount, 2,
|
||||||
|
"Two options should exist in the billingAddressSelect");
|
||||||
|
is(billingAddressSelect.selectedIndex, 1,
|
||||||
|
"The billing address set by the previous test should be selected by default");
|
||||||
|
|
||||||
|
info("Test clicking 'edit' on the empty option first");
|
||||||
|
billingAddressSelect.selectedIndex = 0;
|
||||||
|
|
||||||
|
let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link");
|
||||||
|
addressEditLink.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "address-page" && !state["address-page"].guid;
|
||||||
|
}, "Clicking edit button when the empty option is selected will go to 'add' page (no guid)");
|
||||||
|
|
||||||
|
let addressTitle = content.document.querySelector("address-form h1");
|
||||||
|
is(addressTitle.textContent, "Add Billing Address",
|
||||||
|
"Address on add address page should be correct");
|
||||||
|
|
||||||
|
let addressBackButton = content.document.querySelector("address-form .back-button");
|
||||||
|
addressBackButton.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
|
||||||
|
Object.keys(state.savedAddresses).length == 1;
|
||||||
|
}, "Check we're back at basic-card page with no state changed after adding");
|
||||||
|
|
||||||
|
info("Go back to previously selected option before clicking 'edit' now");
|
||||||
|
billingAddressSelect.selectedIndex = 1;
|
||||||
|
|
||||||
|
let selectedOption = billingAddressSelect.selectedOptions.length &&
|
||||||
|
billingAddressSelect.selectedOptions[0];
|
||||||
|
ok(selectedOption && selectedOption.value, "select should have a selected option value");
|
||||||
|
|
||||||
|
addressEditLink.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "address-page" && state["address-page"].guid;
|
||||||
|
}, "Check address page state (editing)");
|
||||||
|
|
||||||
|
is(addressTitle.textContent, "Edit Billing Address",
|
||||||
|
"Address on edit address page should be correct");
|
||||||
|
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return Object.keys(state.savedBasicCards).length == 1;
|
||||||
|
}, "Check card was not added again when clicking the 'edit' address button");
|
||||||
|
|
||||||
|
addressBackButton.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
|
||||||
|
Object.keys(state.savedAddresses).length == 1;
|
||||||
|
}, "Check we're back at basic-card page with no state changed after editing");
|
||||||
|
|
||||||
|
for (let [key, val] of Object.entries(card)) {
|
||||||
|
let field = content.document.getElementById(key);
|
||||||
|
is(field.value, val, "Field should still have previous value entered");
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedOption = billingAddressSelect.selectedOptions.length &&
|
||||||
|
billingAddressSelect.selectedOptions[0];
|
||||||
|
ok(selectedOption && selectedOption.value, "select should have a selected option value");
|
||||||
|
|
||||||
|
addressEditLink.click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "address-page" && state["address-page"].guid;
|
||||||
|
}, "Check address page state (editing)");
|
||||||
|
|
||||||
|
info("filling address fields");
|
||||||
|
for (let [key, val] of Object.entries(PTU.Addresses.TimBL)) {
|
||||||
|
let field = content.document.getElementById(key);
|
||||||
|
if (!field) {
|
||||||
|
ok(false, `${key} field not found`);
|
||||||
|
}
|
||||||
|
field.value = val + "1";
|
||||||
|
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.document.querySelector("address-form button:last-of-type").click();
|
||||||
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
|
||||||
|
Object.keys(state.savedAddresses).length == 1;
|
||||||
|
}, "Check still only one address and we're back on basic-card page");
|
||||||
|
|
||||||
|
is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel + "1",
|
||||||
|
"Check that address was edited and saved");
|
||||||
|
|
||||||
content.document.querySelector("basic-card-form button:last-of-type").click();
|
content.document.querySelector("basic-card-form button:last-of-type").click();
|
||||||
|
|
||||||
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
|
@ -140,14 +313,14 @@ add_task(async function test_private_persist_defaults() {
|
||||||
addLink.click();
|
addLink.click();
|
||||||
|
|
||||||
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "basic-card-page" && !state.page.guid;
|
return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
|
||||||
},
|
},
|
||||||
"Check add page state");
|
"Check add page state");
|
||||||
|
|
||||||
ok(!state.isPrivate,
|
ok(!state.isPrivate,
|
||||||
"isPrivate flag is not set when paymentrequest is shown from a non-private session");
|
"isPrivate flag is not set when paymentrequest is shown from a non-private session");
|
||||||
let persistInput = content.document.querySelector("basic-card-form labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("basic-card-form labelled-checkbox");
|
||||||
ok(Cu.waiveXrays(persistInput).checked,
|
ok(Cu.waiveXrays(persistCheckbox).checked,
|
||||||
"checkbox is checked by default from a non-private session");
|
"checkbox is checked by default from a non-private session");
|
||||||
}, args);
|
}, args);
|
||||||
|
|
||||||
|
@ -163,14 +336,14 @@ add_task(async function test_private_persist_defaults() {
|
||||||
addLink.click();
|
addLink.click();
|
||||||
|
|
||||||
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "basic-card-page" && !state.page.guid;
|
return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
|
||||||
},
|
},
|
||||||
"Check add page state");
|
"Check add page state");
|
||||||
|
|
||||||
ok(state.isPrivate,
|
ok(state.isPrivate,
|
||||||
"isPrivate flag is set when paymentrequest is shown from a private session");
|
"isPrivate flag is set when paymentrequest is shown from a private session");
|
||||||
let persistInput = content.document.querySelector("labelled-checkbox");
|
let persistCheckbox = content.document.querySelector("labelled-checkbox");
|
||||||
ok(!Cu.waiveXrays(persistInput).checked,
|
ok(!Cu.waiveXrays(persistCheckbox).checked,
|
||||||
"checkbox is not checked by default from a private session");
|
"checkbox is not checked by default from a private session");
|
||||||
}, args, {
|
}, args, {
|
||||||
browser: privateWin.gBrowser,
|
browser: privateWin.gBrowser,
|
||||||
|
@ -195,7 +368,7 @@ add_task(async function test_private_card_adding() {
|
||||||
addLink.click();
|
addLink.click();
|
||||||
|
|
||||||
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
|
||||||
return state.page.id == "basic-card-page" && !state.page.guid;
|
return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
|
||||||
},
|
},
|
||||||
"Check add page state");
|
"Check add page state");
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,9 @@ add_task(async function test_backButton() {
|
||||||
form.dataset.backButtonLabel = "Back";
|
form.dataset.backButtonLabel = "Back";
|
||||||
await form.requestStore.setState({
|
await form.requestStore.setState({
|
||||||
page: {
|
page: {
|
||||||
id: "test-page",
|
id: "address-page",
|
||||||
|
},
|
||||||
|
"address-page": {
|
||||||
title: "Sample page title",
|
title: "Sample page title",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -138,6 +140,9 @@ add_task(async function test_saveButton() {
|
||||||
error: "Generic error",
|
error: "Generic error",
|
||||||
onboardingWizard: undefined,
|
onboardingWizard: undefined,
|
||||||
},
|
},
|
||||||
|
"address-page": {
|
||||||
|
title: "Sample page title",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
guid: undefined,
|
guid: undefined,
|
||||||
messageType: "updateAutofillRecord",
|
messageType: "updateAutofillRecord",
|
||||||
|
@ -158,6 +163,7 @@ add_task(async function test_saveButton() {
|
||||||
successStateChange: {
|
successStateChange: {
|
||||||
page: {
|
page: {
|
||||||
id: "payment-summary",
|
id: "payment-summary",
|
||||||
|
onboardingWizard: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Check event details for the message to chrome");
|
}, "Check event details for the message to chrome");
|
||||||
|
@ -193,6 +199,8 @@ add_task(async function test_edit() {
|
||||||
await form.requestStore.setState({
|
await form.requestStore.setState({
|
||||||
page: {
|
page: {
|
||||||
id: "address-page",
|
id: "address-page",
|
||||||
|
},
|
||||||
|
"address-page": {
|
||||||
guid: address1.guid,
|
guid: address1.guid,
|
||||||
},
|
},
|
||||||
savedAddresses: {
|
savedAddresses: {
|
||||||
|
@ -210,6 +218,8 @@ add_task(async function test_edit() {
|
||||||
await form.requestStore.setState({
|
await form.requestStore.setState({
|
||||||
page: {
|
page: {
|
||||||
id: "address-page",
|
id: "address-page",
|
||||||
|
},
|
||||||
|
"address-page": {
|
||||||
guid: minimalAddress.guid,
|
guid: minimalAddress.guid,
|
||||||
},
|
},
|
||||||
savedAddresses: {
|
savedAddresses: {
|
||||||
|
@ -224,6 +234,7 @@ add_task(async function test_edit() {
|
||||||
page: {
|
page: {
|
||||||
id: "address-page",
|
id: "address-page",
|
||||||
},
|
},
|
||||||
|
"address-page": {},
|
||||||
});
|
});
|
||||||
await asyncElementRendered();
|
await asyncElementRendered();
|
||||||
checkAddressForm(form, {});
|
checkAddressForm(form, {});
|
||||||
|
|
|
@ -69,7 +69,9 @@ add_task(async function test_backButton() {
|
||||||
form.dataset.addBasicCardTitle = "Sample page title 2";
|
form.dataset.addBasicCardTitle = "Sample page title 2";
|
||||||
await form.requestStore.setState({
|
await form.requestStore.setState({
|
||||||
page: {
|
page: {
|
||||||
id: "test-page",
|
id: "basic-card-page",
|
||||||
|
},
|
||||||
|
"basic-card-page": {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await form.promiseReady;
|
await form.promiseReady;
|
||||||
|
@ -128,7 +130,7 @@ add_task(async function test_saveButton() {
|
||||||
"cc-name": "J. Smith",
|
"cc-name": "J. Smith",
|
||||||
"cc-number": "4111111111111111",
|
"cc-number": "4111111111111111",
|
||||||
},
|
},
|
||||||
selectedStateKey: "selectedPaymentCard",
|
selectedStateKey: ["selectedPaymentCard"],
|
||||||
successStateChange: {
|
successStateChange: {
|
||||||
page: {
|
page: {
|
||||||
id: "payment-summary",
|
id: "payment-summary",
|
||||||
|
@ -244,6 +246,8 @@ add_task(async function test_edit() {
|
||||||
await form.requestStore.setState({
|
await form.requestStore.setState({
|
||||||
page: {
|
page: {
|
||||||
id: "basic-card-page",
|
id: "basic-card-page",
|
||||||
|
},
|
||||||
|
"basic-card-page": {
|
||||||
guid: card1.guid,
|
guid: card1.guid,
|
||||||
},
|
},
|
||||||
savedBasicCards: {
|
savedBasicCards: {
|
||||||
|
@ -273,6 +277,8 @@ add_task(async function test_edit() {
|
||||||
await form.requestStore.setState({
|
await form.requestStore.setState({
|
||||||
page: {
|
page: {
|
||||||
id: "basic-card-page",
|
id: "basic-card-page",
|
||||||
|
},
|
||||||
|
"basic-card-page": {
|
||||||
guid: minimalCard.guid,
|
guid: minimalCard.guid,
|
||||||
},
|
},
|
||||||
savedBasicCards: {
|
savedBasicCards: {
|
||||||
|
@ -287,6 +293,9 @@ add_task(async function test_edit() {
|
||||||
page: {
|
page: {
|
||||||
id: "basic-card-page",
|
id: "basic-card-page",
|
||||||
},
|
},
|
||||||
|
"basic-card-page": {
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await asyncElementRendered();
|
await asyncElementRendered();
|
||||||
checkCCForm(form, {});
|
checkCCForm(form, {});
|
||||||
|
|
|
@ -197,13 +197,16 @@ class EditCreditCard extends EditAutofillForm {
|
||||||
this.attachEventListeners();
|
this.attachEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadRecord(record, addresses) {
|
loadRecord(record, addresses, preserveFieldValues) {
|
||||||
// _record must be updated before generateYears and generateBillingAddressOptions are called.
|
// _record must be updated before generateYears and generateBillingAddressOptions are called.
|
||||||
this._record = record;
|
this._record = record;
|
||||||
this._addresses = addresses;
|
this._addresses = addresses;
|
||||||
this.generateYears();
|
|
||||||
this.generateBillingAddressOptions();
|
this.generateBillingAddressOptions();
|
||||||
super.loadRecord(record);
|
if (!preserveFieldValues) {
|
||||||
|
// Re-generating the years will reset the selected option.
|
||||||
|
this.generateYears();
|
||||||
|
super.loadRecord(record);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateYears() {
|
generateYears() {
|
||||||
|
|
|
@ -792,6 +792,8 @@ you can use these alternative items. Otherwise, their values should be empty. -
|
||||||
|
|
||||||
<!ENTITY identity.moreInfoLinkText2 "More Information">
|
<!ENTITY identity.moreInfoLinkText2 "More Information">
|
||||||
|
|
||||||
|
<!ENTITY identity.clearSiteData "Clear Cookies and Site Data">
|
||||||
|
|
||||||
<!ENTITY identity.permissions "Permissions">
|
<!ENTITY identity.permissions "Permissions">
|
||||||
<!ENTITY identity.permissionsEmpty "You have not granted this site any special permissions.">
|
<!ENTITY identity.permissionsEmpty "You have not granted this site any special permissions.">
|
||||||
<!ENTITY identity.permissionsReloadHint "You may need to reload the page for changes to apply.">
|
<!ENTITY identity.permissionsReloadHint "You may need to reload the page for changes to apply.">
|
||||||
|
|
|
@ -239,12 +239,15 @@
|
||||||
padding-inline-end: 1em;
|
padding-inline-end: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#identity-popup-securityView-footer {
|
#identity-popup-more-info-footer {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity-popup-footer {
|
||||||
background-color: var(--arrowpanel-dimmed);
|
background-color: var(--arrowpanel-dimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#identity-popup-securityView-footer > button {
|
.identity-popup-footer > button {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -254,12 +257,12 @@
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#identity-popup-securityView-footer > button:hover,
|
.identity-popup-footer > button:hover,
|
||||||
#identity-popup-securityView-footer > button:focus {
|
.identity-popup-footer > button:focus {
|
||||||
background-color: var(--arrowpanel-dimmed);
|
background-color: var(--arrowpanel-dimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#identity-popup-securityView-footer > button:hover:active {
|
.identity-popup-footer > button:hover:active {
|
||||||
background-color: var(--arrowpanel-dimmed-further);
|
background-color: var(--arrowpanel-dimmed-further);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,27 @@ const TEST_DATA = [
|
||||||
{name: "p2", value: "v2", priority: "important", offsets: [21, 40]}
|
{name: "p2", value: "v2", priority: "important", offsets: [21, 40]}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// Test simple priority
|
||||||
|
{
|
||||||
|
input: "p1: v1 !/*comment*/important;",
|
||||||
|
expected: [
|
||||||
|
{name: "p1", value: "v1", priority: "important", offsets: [0, 29]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Test priority without terminating ";".
|
||||||
|
{
|
||||||
|
input: "p1: v1 !important",
|
||||||
|
expected: [
|
||||||
|
{name: "p1", value: "v1", priority: "important", offsets: [0, 17]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Test trailing "!" without terminating ";".
|
||||||
|
{
|
||||||
|
input: "p1: v1 !",
|
||||||
|
expected: [
|
||||||
|
{name: "p1", value: "v1 !", priority: "", offsets: [0, 8]},
|
||||||
|
]
|
||||||
|
},
|
||||||
// Test invalid priority
|
// Test invalid priority
|
||||||
{
|
{
|
||||||
input: "p1: v1 important;",
|
input: "p1: v1 important;",
|
||||||
|
@ -78,6 +99,32 @@ const TEST_DATA = [
|
||||||
{name: "p1", value: "v1 important", priority: "", offsets: [0, 17]}
|
{name: "p1", value: "v1 important", priority: "", offsets: [0, 17]}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// Test invalid priority (in the middle of the declaration).
|
||||||
|
// See bug 1462553.
|
||||||
|
{
|
||||||
|
input: "p1: v1 !important v2;",
|
||||||
|
expected: [
|
||||||
|
{name: "p1", value: "v1 !important v2", priority: "", offsets: [0, 21]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "p1: v1 ! important v2;",
|
||||||
|
expected: [
|
||||||
|
{name: "p1", value: "v1 ! important v2", priority: "", offsets: [0, 25]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "p1: v1 ! /*comment*/ important v2;",
|
||||||
|
expected: [
|
||||||
|
{name: "p1", value: "v1 ! important v2", priority: "", offsets: [0, 36]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "p1: v1 !/*hi*/important v2;",
|
||||||
|
expected: [
|
||||||
|
{name: "p1", value: "v1 ! important v2", priority: "", offsets: [0, 27]}
|
||||||
|
]
|
||||||
|
},
|
||||||
// Test various types of background-image urls
|
// Test various types of background-image urls
|
||||||
{
|
{
|
||||||
input: "background-image: url(../../relative/image.png)",
|
input: "background-image: url(../../relative/image.png)",
|
||||||
|
|
|
@ -22,7 +22,7 @@ const TEST_DATA = [
|
||||||
input: "blue ! important",
|
input: "blue ! important",
|
||||||
expected: {value: "blue", priority: "important"}
|
expected: {value: "blue", priority: "important"}
|
||||||
},
|
},
|
||||||
{input: "blue !", expected: {value: "blue", priority: ""}},
|
{input: "blue !", expected: {value: "blue !", priority: ""}},
|
||||||
{input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
|
{input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
|
||||||
{
|
{
|
||||||
input: " blue !important ",
|
input: " blue !important ",
|
||||||
|
|
|
@ -302,7 +302,16 @@ function parseDeclarationsInternal(isCssPropertyKnown, inputString,
|
||||||
let declarations = [getEmptyDeclaration()];
|
let declarations = [getEmptyDeclaration()];
|
||||||
let lastProp = declarations[0];
|
let lastProp = declarations[0];
|
||||||
|
|
||||||
let current = "", hasBang = false;
|
// This tracks the "!important" parsing state. The states are:
|
||||||
|
// 0 - haven't seen anything
|
||||||
|
// 1 - have seen "!", looking for "important" next (possibly after
|
||||||
|
// whitespace).
|
||||||
|
// 2 - have seen "!important"
|
||||||
|
let importantState = 0;
|
||||||
|
// This is true if we saw whitespace or comments between the "!" and
|
||||||
|
// the "important".
|
||||||
|
let importantWS = false;
|
||||||
|
let current = "";
|
||||||
while (true) {
|
while (true) {
|
||||||
let token = lexer.nextToken();
|
let token = lexer.nextToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -322,19 +331,23 @@ function parseDeclarationsInternal(isCssPropertyKnown, inputString,
|
||||||
lastProp.offsets[0] = token.startOffset;
|
lastProp.offsets[0] = token.startOffset;
|
||||||
}
|
}
|
||||||
lastProp.offsets[1] = token.endOffset;
|
lastProp.offsets[1] = token.endOffset;
|
||||||
} else if (lastProp.name && !current && !hasBang &&
|
} else if (lastProp.name && !current && !importantState &&
|
||||||
!lastProp.priority && lastProp.colonOffsets[1]) {
|
!lastProp.priority && lastProp.colonOffsets[1]) {
|
||||||
// Whitespace appearing after the ":" is attributed to it.
|
// Whitespace appearing after the ":" is attributed to it.
|
||||||
lastProp.colonOffsets[1] = token.endOffset;
|
lastProp.colonOffsets[1] = token.endOffset;
|
||||||
|
} else if (importantState === 1) {
|
||||||
|
importantWS = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.tokenType === "symbol" && token.text === ":") {
|
if (token.tokenType === "symbol" && token.text === ":") {
|
||||||
|
// Either way, a "!important" we've seen is no longer valid now.
|
||||||
|
importantState = 0;
|
||||||
|
importantWS = false;
|
||||||
if (!lastProp.name) {
|
if (!lastProp.name) {
|
||||||
// Set the current declaration name if there's no name yet
|
// Set the current declaration name if there's no name yet
|
||||||
lastProp.name = cssTrim(current);
|
lastProp.name = cssTrim(current);
|
||||||
lastProp.colonOffsets = [token.startOffset, token.endOffset];
|
lastProp.colonOffsets = [token.startOffset, token.endOffset];
|
||||||
current = "";
|
current = "";
|
||||||
hasBang = false;
|
|
||||||
|
|
||||||
// When parsing a comment body, if the left-hand-side is not a
|
// When parsing a comment body, if the left-hand-side is not a
|
||||||
// valid property name, then drop it and stop parsing.
|
// valid property name, then drop it and stop parsing.
|
||||||
|
@ -357,28 +370,44 @@ function parseDeclarationsInternal(isCssPropertyKnown, inputString,
|
||||||
current = "";
|
current = "";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (importantState === 2) {
|
||||||
|
lastProp.priority = "important";
|
||||||
|
} else if (importantState === 1) {
|
||||||
|
current += "!";
|
||||||
|
if (importantWS) {
|
||||||
|
current += " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
lastProp.value = cssTrim(current);
|
lastProp.value = cssTrim(current);
|
||||||
current = "";
|
current = "";
|
||||||
hasBang = false;
|
importantState = 0;
|
||||||
|
importantWS = false;
|
||||||
declarations.push(getEmptyDeclaration());
|
declarations.push(getEmptyDeclaration());
|
||||||
lastProp = declarations[declarations.length - 1];
|
lastProp = declarations[declarations.length - 1];
|
||||||
} else if (token.tokenType === "ident") {
|
} else if (token.tokenType === "ident") {
|
||||||
if (token.text === "important" && hasBang) {
|
if (token.text === "important" && importantState === 1) {
|
||||||
lastProp.priority = "important";
|
importantState = 2;
|
||||||
hasBang = false;
|
|
||||||
} else {
|
} else {
|
||||||
if (hasBang) {
|
if (importantState > 0) {
|
||||||
current += "!";
|
current += "!";
|
||||||
|
if (importantWS) {
|
||||||
|
current += " ";
|
||||||
|
}
|
||||||
|
if (importantState === 2) {
|
||||||
|
current += "important ";
|
||||||
|
}
|
||||||
|
importantState = 0;
|
||||||
|
importantWS = false;
|
||||||
}
|
}
|
||||||
// Re-escape the token to avoid dequoting problems.
|
// Re-escape the token to avoid dequoting problems.
|
||||||
// See bug 1287620.
|
// See bug 1287620.
|
||||||
current += CSS.escape(token.text);
|
current += CSS.escape(token.text);
|
||||||
}
|
}
|
||||||
} else if (token.tokenType === "symbol" && token.text === "!") {
|
} else if (token.tokenType === "symbol" && token.text === "!") {
|
||||||
hasBang = true;
|
importantState = 1;
|
||||||
} else if (token.tokenType === "whitespace") {
|
} else if (token.tokenType === "whitespace") {
|
||||||
if (current !== "") {
|
if (current !== "") {
|
||||||
current += " ";
|
current = current.trimRight() + " ";
|
||||||
}
|
}
|
||||||
} else if (token.tokenType === "comment") {
|
} else if (token.tokenType === "comment") {
|
||||||
if (parseComments && !lastProp.name && !lastProp.value) {
|
if (parseComments && !lastProp.name && !lastProp.value) {
|
||||||
|
@ -392,9 +421,20 @@ function parseDeclarationsInternal(isCssPropertyKnown, inputString,
|
||||||
let lastDecl = declarations.pop();
|
let lastDecl = declarations.pop();
|
||||||
declarations = [...declarations, ...newDecls, lastDecl];
|
declarations = [...declarations, ...newDecls, lastDecl];
|
||||||
} else {
|
} else {
|
||||||
current += " ";
|
current = current.trimRight() + " ";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (importantState > 0) {
|
||||||
|
current += "!";
|
||||||
|
if (importantWS) {
|
||||||
|
current += " ";
|
||||||
|
}
|
||||||
|
if (importantState === 2) {
|
||||||
|
current += "important ";
|
||||||
|
}
|
||||||
|
importantState = 0;
|
||||||
|
importantWS = false;
|
||||||
|
}
|
||||||
current += inputString.substring(token.startOffset, token.endOffset);
|
current += inputString.substring(token.startOffset, token.endOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -409,6 +449,11 @@ function parseDeclarationsInternal(isCssPropertyKnown, inputString,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Trailing value found, i.e. value without an ending ;
|
// Trailing value found, i.e. value without an ending ;
|
||||||
|
if (importantState === 2) {
|
||||||
|
lastProp.priority = "important";
|
||||||
|
} else if (importantState === 1) {
|
||||||
|
current += "!";
|
||||||
|
}
|
||||||
lastProp.value = cssTrim(current);
|
lastProp.value = cssTrim(current);
|
||||||
let terminator = lexer.performEOFFixup("", true);
|
let terminator = lexer.performEOFFixup("", true);
|
||||||
lastProp.terminator = terminator + ";";
|
lastProp.terminator = terminator + ";";
|
||||||
|
|
|
@ -489,11 +489,12 @@ EditorBase::GetDesiredSpellCheckState()
|
||||||
return element->Spellcheck();
|
return element->Spellcheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
void
|
||||||
EditorBase::PreDestroy(bool aDestroyingFrames)
|
EditorBase::PreDestroy(bool aDestroyingFrames)
|
||||||
{
|
{
|
||||||
if (mDidPreDestroy)
|
if (mDidPreDestroy) {
|
||||||
return NS_OK;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Selection* selection = GetSelection();
|
Selection* selection = GetSelection();
|
||||||
if (selection) {
|
if (selection) {
|
||||||
|
@ -537,7 +538,6 @@ EditorBase::PreDestroy(bool aDestroyingFrames)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDidPreDestroy = true;
|
mDidPreDestroy = true;
|
||||||
return NS_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
NS_IMETHODIMP
|
||||||
|
@ -1189,13 +1189,6 @@ EditorBase::CanPaste(int32_t aSelectionType, bool* aCanPaste)
|
||||||
return NS_ERROR_NOT_IMPLEMENTED;
|
return NS_ERROR_NOT_IMPLEMENTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
|
||||||
EditorBase::CanPasteTransferable(nsITransferable* aTransferable,
|
|
||||||
bool* aCanPaste)
|
|
||||||
{
|
|
||||||
return NS_ERROR_NOT_IMPLEMENTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
NS_IMETHODIMP
|
NS_IMETHODIMP
|
||||||
EditorBase::SetAttribute(Element* aElement,
|
EditorBase::SetAttribute(Element* aElement,
|
||||||
const nsAString& aAttribute,
|
const nsAString& aAttribute,
|
||||||
|
|
|
@ -969,6 +969,15 @@ public:
|
||||||
*/
|
*/
|
||||||
nsresult PostCreate();
|
nsresult PostCreate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreDestroy is called before the editor goes away, and gives the editor a
|
||||||
|
* chance to tell its documentStateObservers that the document is going away.
|
||||||
|
* @param aDestroyingFrames set to true when the frames being edited
|
||||||
|
* are being destroyed (so there is no need to modify any nsISelections,
|
||||||
|
* nor is it safe to do so)
|
||||||
|
*/
|
||||||
|
virtual void PreDestroy(bool aDestroyingFrames);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All editor operations which alter the doc should be prefaced
|
* All editor operations which alter the doc should be prefaced
|
||||||
* with a call to StartOperation, naming the action and direction.
|
* with a call to StartOperation, naming the action and direction.
|
||||||
|
|
|
@ -606,7 +606,8 @@ PasteTransferableCommand::IsCommandEnabled(const char* aCommandName,
|
||||||
if (!textEditor->IsSelectionEditable()) {
|
if (!textEditor->IsSelectionEditable()) {
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
return textEditor->CanPasteTransferable(nullptr, aIsEnabled);
|
*aIsEnabled = textEditor->CanPasteTransferable(nullptr);
|
||||||
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
NS_IMETHODIMP
|
||||||
|
@ -667,13 +668,8 @@ PasteTransferableCommand::GetCommandStateParams(const char* aCommandName,
|
||||||
TextEditor* textEditor = editor->AsTextEditor();
|
TextEditor* textEditor = editor->AsTextEditor();
|
||||||
MOZ_ASSERT(textEditor);
|
MOZ_ASSERT(textEditor);
|
||||||
|
|
||||||
bool canPaste;
|
return aParams->SetBooleanValue(STATE_ENABLED,
|
||||||
nsresult rv = textEditor->CanPasteTransferable(trans, &canPaste);
|
textEditor->CanPasteTransferable(trans));
|
||||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
return aParams->SetBooleanValue(STATE_ENABLED, canPaste);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************
|
/******************************************************************************
|
||||||
|
|
|
@ -332,11 +332,11 @@ HTMLEditor::Init(nsIDocument& aDoc,
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
void
|
||||||
HTMLEditor::PreDestroy(bool aDestroyingFrames)
|
HTMLEditor::PreDestroy(bool aDestroyingFrames)
|
||||||
{
|
{
|
||||||
if (mDidPreDestroy) {
|
if (mDidPreDestroy) {
|
||||||
return NS_OK;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nsCOMPtr<nsIDocument> document = GetDocument();
|
nsCOMPtr<nsIDocument> document = GetDocument();
|
||||||
|
@ -352,7 +352,7 @@ HTMLEditor::PreDestroy(bool aDestroyingFrames)
|
||||||
// stay around (which they would, since the frames have an owning reference).
|
// stay around (which they would, since the frames have an owning reference).
|
||||||
HideAnonymousEditingUIs();
|
HideAnonymousEditingUIs();
|
||||||
|
|
||||||
return TextEditor::PreDestroy(aDestroyingFrames);
|
EditorBase::PreDestroy(aDestroyingFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
NS_IMETHODIMP
|
||||||
|
|
|
@ -133,6 +133,7 @@ public:
|
||||||
bool aSuppressTransaction) override;
|
bool aSuppressTransaction) override;
|
||||||
using EditorBase::RemoveAttributeOrEquivalent;
|
using EditorBase::RemoveAttributeOrEquivalent;
|
||||||
using EditorBase::SetAttributeOrEquivalent;
|
using EditorBase::SetAttributeOrEquivalent;
|
||||||
|
virtual bool CanPasteTransferable(nsITransferable* aTransferable) override;
|
||||||
|
|
||||||
// nsStubMutationObserver overrides
|
// nsStubMutationObserver overrides
|
||||||
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
|
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
|
||||||
|
@ -212,7 +213,7 @@ public:
|
||||||
// Overrides of EditorBase interface methods
|
// Overrides of EditorBase interface methods
|
||||||
virtual nsresult EndUpdateViewBatch() override;
|
virtual nsresult EndUpdateViewBatch() override;
|
||||||
|
|
||||||
NS_IMETHOD PreDestroy(bool aDestroyingFrames) override;
|
virtual void PreDestroy(bool aDestroyingFrames) override;
|
||||||
|
|
||||||
virtual nsresult GetPreferredIMEState(widget::IMEState* aState) override;
|
virtual nsresult GetPreferredIMEState(widget::IMEState* aState) override;
|
||||||
|
|
||||||
|
@ -329,8 +330,6 @@ public:
|
||||||
NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override;
|
NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override;
|
||||||
|
|
||||||
NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override;
|
NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override;
|
||||||
NS_IMETHOD CanPasteTransferable(nsITransferable* aTransferable,
|
|
||||||
bool* aCanPaste) override;
|
|
||||||
|
|
||||||
NS_IMETHOD DebugUnitTests(int32_t* outNumTests,
|
NS_IMETHOD DebugUnitTests(int32_t* outNumTests,
|
||||||
int32_t* outNumTestsFailed) override;
|
int32_t* outNumTestsFailed) override;
|
||||||
|
|
|
@ -1429,7 +1429,7 @@ HTMLEditor::Paste(int32_t aSelectionType)
|
||||||
bHavePrivateHTMLFlavor, true);
|
bHavePrivateHTMLFlavor, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
nsresult
|
||||||
HTMLEditor::PasteTransferable(nsITransferable* aTransferable)
|
HTMLEditor::PasteTransferable(nsITransferable* aTransferable)
|
||||||
{
|
{
|
||||||
// Use an invalid value for the clipboard type as data comes from aTransferable
|
// Use an invalid value for the clipboard type as data comes from aTransferable
|
||||||
|
@ -1525,29 +1525,24 @@ HTMLEditor::CanPaste(int32_t aSelectionType,
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
NS_IMETHODIMP
|
bool
|
||||||
HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable,
|
HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable)
|
||||||
bool* aCanPaste)
|
|
||||||
{
|
{
|
||||||
NS_ENSURE_ARG_POINTER(aCanPaste);
|
|
||||||
|
|
||||||
// can't paste if readonly
|
// can't paste if readonly
|
||||||
if (!IsModifiable()) {
|
if (!IsModifiable()) {
|
||||||
*aCanPaste = false;
|
return false;
|
||||||
return NS_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If |aTransferable| is null, assume that a paste will succeed.
|
// If |aTransferable| is null, assume that a paste will succeed.
|
||||||
if (!aTransferable) {
|
if (!aTransferable) {
|
||||||
*aCanPaste = true;
|
return true;
|
||||||
return NS_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peek in |aTransferable| to see if it contains a supported MIME type.
|
// Peek in |aTransferable| to see if it contains a supported MIME type.
|
||||||
|
|
||||||
// Use the flavors depending on the current editor mask
|
// Use the flavors depending on the current editor mask
|
||||||
const char ** flavors;
|
const char ** flavors;
|
||||||
unsigned length;
|
size_t length;
|
||||||
if (IsPlaintextEditor()) {
|
if (IsPlaintextEditor()) {
|
||||||
flavors = textEditorFlavors;
|
flavors = textEditorFlavors;
|
||||||
length = ArrayLength(textEditorFlavors);
|
length = ArrayLength(textEditorFlavors);
|
||||||
|
@ -1556,20 +1551,18 @@ HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable,
|
||||||
length = ArrayLength(textHtmlEditorFlavors);
|
length = ArrayLength(textHtmlEditorFlavors);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (unsigned int i = 0; i < length; i++, flavors++) {
|
for (size_t i = 0; i < length; i++, flavors++) {
|
||||||
nsCOMPtr<nsISupports> data;
|
nsCOMPtr<nsISupports> data;
|
||||||
uint32_t dataLen;
|
uint32_t dataLen;
|
||||||
nsresult rv = aTransferable->GetTransferData(*flavors,
|
nsresult rv = aTransferable->GetTransferData(*flavors,
|
||||||
getter_AddRefs(data),
|
getter_AddRefs(data),
|
||||||
&dataLen);
|
&dataLen);
|
||||||
if (NS_SUCCEEDED(rv) && data) {
|
if (NS_SUCCEEDED(rv) && data) {
|
||||||
*aCanPaste = true;
|
return true;
|
||||||
return NS_OK;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*aCanPaste = false;
|
return false;
|
||||||
return NS_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1737,13 +1737,10 @@ TextEditor::OutputToString(const nsAString& aFormatType,
|
||||||
// Protect the edit rules object from dying
|
// Protect the edit rules object from dying
|
||||||
RefPtr<TextEditRules> rules(mRules);
|
RefPtr<TextEditRules> rules(mRules);
|
||||||
|
|
||||||
nsString resultString;
|
|
||||||
RulesInfo ruleInfo(EditAction::outputText);
|
RulesInfo ruleInfo(EditAction::outputText);
|
||||||
ruleInfo.outString = &resultString;
|
ruleInfo.outString = &aOutputString;
|
||||||
ruleInfo.flags = aFlags;
|
ruleInfo.flags = aFlags;
|
||||||
// XXX Struct should store a nsAReadable*
|
ruleInfo.outputFormat = &aFormatType;
|
||||||
nsAutoString str(aFormatType);
|
|
||||||
ruleInfo.outputFormat = &str;
|
|
||||||
Selection* selection = GetSelection();
|
Selection* selection = GetSelection();
|
||||||
if (NS_WARN_IF(!selection)) {
|
if (NS_WARN_IF(!selection)) {
|
||||||
return NS_ERROR_FAILURE;
|
return NS_ERROR_FAILURE;
|
||||||
|
@ -1754,8 +1751,7 @@ TextEditor::OutputToString(const nsAString& aFormatType,
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
if (handled) {
|
if (handled) {
|
||||||
// This case will get triggered by password fields.
|
// This case will get triggered by password fields or single text node only.
|
||||||
aOutputString.Assign(*(ruleInfo.outString));
|
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,13 +73,18 @@ public:
|
||||||
NS_IMETHOD Paste(int32_t aSelectionType) override;
|
NS_IMETHOD Paste(int32_t aSelectionType) override;
|
||||||
NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override;
|
NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override;
|
||||||
NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override;
|
NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override;
|
||||||
NS_IMETHOD CanPasteTransferable(nsITransferable* aTransferable,
|
|
||||||
bool* aCanPaste) override;
|
|
||||||
|
|
||||||
NS_IMETHOD OutputToString(const nsAString& aFormatType,
|
NS_IMETHOD OutputToString(const nsAString& aFormatType,
|
||||||
uint32_t aFlags,
|
uint32_t aFlags,
|
||||||
nsAString& aOutputString) override;
|
nsAString& aOutputString) override;
|
||||||
|
|
||||||
|
/** Can we paste |aTransferable| or, if |aTransferable| is null, will a call
|
||||||
|
* to pasteTransferable later possibly succeed if given an instance of
|
||||||
|
* nsITransferable then? True if the doc is modifiable, and, if
|
||||||
|
* |aTransfeable| is non-null, we have pasteable data in |aTransfeable|.
|
||||||
|
*/
|
||||||
|
virtual bool CanPasteTransferable(nsITransferable* aTransferable);
|
||||||
|
|
||||||
// Overrides of EditorBase
|
// Overrides of EditorBase
|
||||||
virtual nsresult RemoveAttributeOrEquivalent(
|
virtual nsresult RemoveAttributeOrEquivalent(
|
||||||
Element* aElement,
|
Element* aElement,
|
||||||
|
|
|
@ -374,23 +374,17 @@ TextEditor::CanPaste(int32_t aSelectionType,
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
NS_IMETHODIMP
|
TextEditor::CanPasteTransferable(nsITransferable* aTransferable)
|
||||||
TextEditor::CanPasteTransferable(nsITransferable* aTransferable,
|
|
||||||
bool* aCanPaste)
|
|
||||||
{
|
{
|
||||||
NS_ENSURE_ARG_POINTER(aCanPaste);
|
|
||||||
|
|
||||||
// can't paste if readonly
|
// can't paste if readonly
|
||||||
if (!IsModifiable()) {
|
if (!IsModifiable()) {
|
||||||
*aCanPaste = false;
|
return false;
|
||||||
return NS_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If |aTransferable| is null, assume that a paste will succeed.
|
// If |aTransferable| is null, assume that a paste will succeed.
|
||||||
if (!aTransferable) {
|
if (!aTransferable) {
|
||||||
*aCanPaste = true;
|
return true;
|
||||||
return NS_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nsCOMPtr<nsISupports> data;
|
nsCOMPtr<nsISupports> data;
|
||||||
|
@ -399,12 +393,10 @@ TextEditor::CanPasteTransferable(nsITransferable* aTransferable,
|
||||||
getter_AddRefs(data),
|
getter_AddRefs(data),
|
||||||
&dataLen);
|
&dataLen);
|
||||||
if (NS_SUCCEEDED(rv) && data) {
|
if (NS_SUCCEEDED(rv) && data) {
|
||||||
*aCanPaste = true;
|
return true;
|
||||||
} else {
|
|
||||||
*aCanPaste = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NS_OK;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
|
|
@ -59,15 +59,6 @@ interface nsIEditor : nsISupports
|
||||||
in DOMString sourceAttrName,
|
in DOMString sourceAttrName,
|
||||||
in boolean aSuppressTransaction);
|
in boolean aSuppressTransaction);
|
||||||
|
|
||||||
/**
|
|
||||||
* preDestroy is called before the editor goes away, and gives the editor a
|
|
||||||
* chance to tell its documentStateObservers that the document is going away.
|
|
||||||
* @param aDestroyingFrames set to true when the frames being edited
|
|
||||||
* are being destroyed (so there is no need to modify any nsISelections,
|
|
||||||
* nor is it safe to do so)
|
|
||||||
*/
|
|
||||||
void preDestroy(in boolean aDestroyingFrames);
|
|
||||||
|
|
||||||
/** edit flags for this editor. May be set at any time. */
|
/** edit flags for this editor. May be set at any time. */
|
||||||
attribute unsigned long flags;
|
attribute unsigned long flags;
|
||||||
|
|
||||||
|
@ -299,13 +290,6 @@ interface nsIEditor : nsISupports
|
||||||
*/
|
*/
|
||||||
boolean canPaste(in long aSelectionType);
|
boolean canPaste(in long aSelectionType);
|
||||||
|
|
||||||
/** Can we paste |aTransferable| or, if |aTransferable| is null, will a call
|
|
||||||
* to pasteTransferable later possibly succeed if given an instance of
|
|
||||||
* nsITransferable then? True if the doc is modifiable, and, if
|
|
||||||
* |aTransfeable| is non-null, we have pasteable data in |aTransfeable|.
|
|
||||||
*/
|
|
||||||
boolean canPasteTransferable([optional] in nsITransferable aTransferable);
|
|
||||||
|
|
||||||
/* ------------ Selection methods -------------- */
|
/* ------------ Selection methods -------------- */
|
||||||
|
|
||||||
/** sets the document selection to the entire contents of the document */
|
/** sets the document selection to the entire contents of the document */
|
||||||
|
|
|
@ -423,10 +423,27 @@ bool Channel::ChannelImpl::ProcessIncomingMessages() {
|
||||||
fds = wire_fds;
|
fds = wire_fds;
|
||||||
num_fds = num_wire_fds;
|
num_fds = num_wire_fds;
|
||||||
} else {
|
} else {
|
||||||
const size_t prev_size = input_overflow_fds_.size();
|
// This code may look like a no-op in the case where
|
||||||
input_overflow_fds_.resize(prev_size + num_wire_fds);
|
// num_wire_fds == 0, but in fact:
|
||||||
memcpy(&input_overflow_fds_[prev_size], wire_fds,
|
//
|
||||||
num_wire_fds * sizeof(int));
|
// 1. wire_fds will be nullptr, so passing it to memcpy is
|
||||||
|
// undefined behavior according to the C standard, even though
|
||||||
|
// the memcpy length is 0.
|
||||||
|
//
|
||||||
|
// 2. prev_size will be an out-of-bounds index for
|
||||||
|
// input_overflow_fds_; this is undefined behavior according to
|
||||||
|
// the C++ standard, even though the element only has its
|
||||||
|
// pointer taken and isn't accessed (and the corresponding
|
||||||
|
// operation on a C array would be defined).
|
||||||
|
//
|
||||||
|
// UBSan makes #1 a fatal error, and assertions in libstdc++ do
|
||||||
|
// the same for #2 if enabled.
|
||||||
|
if (num_wire_fds > 0) {
|
||||||
|
const size_t prev_size = input_overflow_fds_.size();
|
||||||
|
input_overflow_fds_.resize(prev_size + num_wire_fds);
|
||||||
|
memcpy(&input_overflow_fds_[prev_size], wire_fds,
|
||||||
|
num_wire_fds * sizeof(int));
|
||||||
|
}
|
||||||
fds = &input_overflow_fds_[0];
|
fds = &input_overflow_fds_[0];
|
||||||
num_fds = input_overflow_fds_.size();
|
num_fds = input_overflow_fds_.size();
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,8 +258,9 @@ MayHaveAnimationOfProperty(EffectSet* effects, nsCSSPropertyID aProperty)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
bool
|
||||||
MayHaveAnimationOfProperty(const nsIFrame* aFrame, nsCSSPropertyID aProperty)
|
nsLayoutUtils::MayHaveAnimationOfProperty(const nsIFrame* aFrame,
|
||||||
|
nsCSSPropertyID aProperty)
|
||||||
{
|
{
|
||||||
switch (aProperty) {
|
switch (aProperty) {
|
||||||
case eCSSProperty_transform:
|
case eCSSProperty_transform:
|
||||||
|
@ -276,7 +277,7 @@ bool
|
||||||
nsLayoutUtils::HasAnimationOfProperty(EffectSet* aEffectSet,
|
nsLayoutUtils::HasAnimationOfProperty(EffectSet* aEffectSet,
|
||||||
nsCSSPropertyID aProperty)
|
nsCSSPropertyID aProperty)
|
||||||
{
|
{
|
||||||
if (!aEffectSet || !MayHaveAnimationOfProperty(aEffectSet, aProperty)) {
|
if (!aEffectSet || !::MayHaveAnimationOfProperty(aEffectSet, aProperty)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,7 +313,7 @@ nsLayoutUtils::HasEffectiveAnimation(const nsIFrame* aFrame,
|
||||||
nsCSSPropertyID aProperty)
|
nsCSSPropertyID aProperty)
|
||||||
{
|
{
|
||||||
EffectSet* effects = EffectSet::GetEffectSet(aFrame);
|
EffectSet* effects = EffectSet::GetEffectSet(aFrame);
|
||||||
if (!effects || !MayHaveAnimationOfProperty(effects, aProperty)) {
|
if (!effects || !::MayHaveAnimationOfProperty(effects, aProperty)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,6 +327,17 @@ nsLayoutUtils::HasEffectiveAnimation(const nsIFrame* aFrame,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
nsLayoutUtils::MayHaveEffectiveAnimation(const nsIFrame* aFrame,
|
||||||
|
nsCSSPropertyID aProperty)
|
||||||
|
{
|
||||||
|
EffectSet* effects = EffectSet::GetEffectSet(aFrame);
|
||||||
|
if (!effects || !::MayHaveAnimationOfProperty(effects, aProperty)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static float
|
static float
|
||||||
GetSuitableScale(float aMaxScale, float aMinScale,
|
GetSuitableScale(float aMaxScale, float aMinScale,
|
||||||
nscoord aVisibleDimension, nscoord aDisplayDimension)
|
nscoord aVisibleDimension, nscoord aDisplayDimension)
|
||||||
|
|
|
@ -2332,6 +2332,8 @@ public:
|
||||||
*/
|
*/
|
||||||
static bool HasAnimationOfProperty(const nsIFrame* aFrame,
|
static bool HasAnimationOfProperty(const nsIFrame* aFrame,
|
||||||
nsCSSPropertyID aProperty);
|
nsCSSPropertyID aProperty);
|
||||||
|
static bool MayHaveAnimationOfProperty(const nsIFrame* aFrame,
|
||||||
|
nsCSSPropertyID aProperty);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if |aEffectSet| has an animation of |aProperty| regardless of
|
* Returns true if |aEffectSet| has an animation of |aProperty| regardless of
|
||||||
|
@ -2346,6 +2348,8 @@ public:
|
||||||
*/
|
*/
|
||||||
static bool HasEffectiveAnimation(const nsIFrame* aFrame,
|
static bool HasEffectiveAnimation(const nsIFrame* aFrame,
|
||||||
nsCSSPropertyID aProperty);
|
nsCSSPropertyID aProperty);
|
||||||
|
static bool MayHaveEffectiveAnimation(const nsIFrame* aFrame,
|
||||||
|
nsCSSPropertyID aProperty);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if off-main-thread animations are enabled.
|
* Checks if off-main-thread animations are enabled.
|
||||||
|
|
|
@ -2954,9 +2954,12 @@ nsIFrame::BuildDisplayListForStackingContext(nsDisplayListBuilder* aBuilder,
|
||||||
// layer (for async animations), see
|
// layer (for async animations), see
|
||||||
// nsSVGIntegrationsUtils::PaintMaskAndClipPath or
|
// nsSVGIntegrationsUtils::PaintMaskAndClipPath or
|
||||||
// nsSVGIntegrationsUtils::PaintFilter.
|
// nsSVGIntegrationsUtils::PaintFilter.
|
||||||
|
// Use MayNeedActiveLayer to decide, since we don't want to condition the wrapping
|
||||||
|
// display item on values that might change silently between paints (opacity activity
|
||||||
|
// can depend on the will-change budget).
|
||||||
bool useOpacity = HasVisualOpacity(effectSet) &&
|
bool useOpacity = HasVisualOpacity(effectSet) &&
|
||||||
!nsSVGUtils::CanOptimizeOpacity(this) &&
|
!nsSVGUtils::CanOptimizeOpacity(this) &&
|
||||||
(!usingSVGEffects || nsDisplayOpacity::NeedsActiveLayer(aBuilder, this));
|
(!usingSVGEffects || nsDisplayOpacity::MayNeedActiveLayer(this));
|
||||||
bool useBlendMode = effects->mMixBlendMode != NS_STYLE_BLEND_NORMAL;
|
bool useBlendMode = effects->mMixBlendMode != NS_STYLE_BLEND_NORMAL;
|
||||||
bool useStickyPosition = disp->mPosition == NS_STYLE_POSITION_STICKY &&
|
bool useStickyPosition = disp->mPosition == NS_STYLE_POSITION_STICKY &&
|
||||||
IsScrollFrameActive(aBuilder,
|
IsScrollFrameActive(aBuilder,
|
||||||
|
|
|
@ -462,6 +462,9 @@ nsSubDocumentFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
|
||||||
if (mPreviousCaret) {
|
if (mPreviousCaret) {
|
||||||
aBuilder->MarkFrameModifiedDuringBuilding(mPreviousCaret);
|
aBuilder->MarkFrameModifiedDuringBuilding(mPreviousCaret);
|
||||||
}
|
}
|
||||||
|
if (aBuilder->GetCaretFrame()) {
|
||||||
|
aBuilder->MarkFrameModifiedDuringBuilding(aBuilder->GetCaretFrame());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mPreviousCaret = aBuilder->GetCaretFrame();
|
mPreviousCaret = aBuilder->GetCaretFrame();
|
||||||
}
|
}
|
||||||
|
|
|
@ -436,7 +436,11 @@ ActiveLayerTracker::IsStyleAnimated(nsDisplayListBuilder* aBuilder,
|
||||||
if (aProperty == eCSSProperty_transform && aFrame->Combines3DTransformWithAncestors()) {
|
if (aProperty == eCSSProperty_transform && aFrame->Combines3DTransformWithAncestors()) {
|
||||||
return IsStyleAnimated(aBuilder, aFrame->GetParent(), aProperty);
|
return IsStyleAnimated(aBuilder, aFrame->GetParent(), aProperty);
|
||||||
}
|
}
|
||||||
return nsLayoutUtils::HasEffectiveAnimation(aFrame, aProperty);
|
if (aBuilder) {
|
||||||
|
return nsLayoutUtils::HasEffectiveAnimation(aFrame, aProperty);
|
||||||
|
} else {
|
||||||
|
return nsLayoutUtils::MayHaveEffectiveAnimation(aFrame, aProperty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* static */ bool
|
/* static */ bool
|
||||||
|
|
|
@ -335,8 +335,9 @@ public:
|
||||||
for (nsDisplayItem* i : *items) {
|
for (nsDisplayItem* i : *items) {
|
||||||
if (i != aItem && i->Frame() == aItem->Frame() &&
|
if (i != aItem && i->Frame() == aItem->Frame() &&
|
||||||
i->GetPerFrameKey() == aItem->GetPerFrameKey()) {
|
i->GetPerFrameKey() == aItem->GetPerFrameKey()) {
|
||||||
*aOutIndex = i->GetOldListIndex(mOldList, mOuterKey);
|
if (i->GetOldListIndex(mOldList, mOuterKey, aOutIndex)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -6526,6 +6526,12 @@ nsDisplayOpacity::NeedsActiveLayer(nsDisplayListBuilder* aBuilder, nsIFrame* aFr
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* static */ bool
|
||||||
|
nsDisplayOpacity::MayNeedActiveLayer(nsIFrame* aFrame)
|
||||||
|
{
|
||||||
|
return ActiveLayerTracker::IsStyleMaybeAnimated(aFrame, eCSSProperty_opacity);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
nsDisplayOpacity::ApplyOpacity(nsDisplayListBuilder* aBuilder,
|
nsDisplayOpacity::ApplyOpacity(nsDisplayListBuilder* aBuilder,
|
||||||
float aOpacity,
|
float aOpacity,
|
||||||
|
|
|
@ -2863,14 +2863,16 @@ public:
|
||||||
#endif
|
#endif
|
||||||
mOldListIndex = aIndex;
|
mOldListIndex = aIndex;
|
||||||
}
|
}
|
||||||
OldListIndex GetOldListIndex(nsDisplayList* aList, uint32_t aListKey)
|
bool GetOldListIndex(nsDisplayList* aList, uint32_t aListKey, OldListIndex* aOutIndex)
|
||||||
{
|
{
|
||||||
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
|
|
||||||
if (mOldList != reinterpret_cast<uintptr_t>(aList)) {
|
if (mOldList != reinterpret_cast<uintptr_t>(aList)) {
|
||||||
|
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
|
||||||
MOZ_CRASH_UNSAFE_PRINTF("Item found was in the wrong list! type %d (outer type was %d at depth %d, now is %d)", GetPerFrameKey(), mOldListKey, mOldNestingDepth, aListKey);
|
MOZ_CRASH_UNSAFE_PRINTF("Item found was in the wrong list! type %d (outer type was %d at depth %d, now is %d)", GetPerFrameKey(), mOldListKey, mOldNestingDepth, aListKey);
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
return mOldListIndex;
|
return false;
|
||||||
|
}
|
||||||
|
*aOutIndex = mOldListIndex;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nsRect& GetPaintRect() const {
|
const nsRect& GetPaintRect() const {
|
||||||
|
@ -2910,7 +2912,6 @@ protected:
|
||||||
|
|
||||||
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
|
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
|
||||||
public:
|
public:
|
||||||
uintptr_t mOldList = 0;
|
|
||||||
uint32_t mOldListKey = 0;
|
uint32_t mOldListKey = 0;
|
||||||
uint32_t mOldNestingDepth = 0;
|
uint32_t mOldNestingDepth = 0;
|
||||||
bool mMergedItem = false;
|
bool mMergedItem = false;
|
||||||
|
@ -2918,6 +2919,7 @@ public:
|
||||||
protected:
|
protected:
|
||||||
#endif
|
#endif
|
||||||
OldListIndex mOldListIndex;
|
OldListIndex mOldListIndex;
|
||||||
|
uintptr_t mOldList = 0;
|
||||||
|
|
||||||
bool mForceNotVisible;
|
bool mForceNotVisible;
|
||||||
bool mDisableSubpixelAA;
|
bool mDisableSubpixelAA;
|
||||||
|
@ -5384,6 +5386,7 @@ public:
|
||||||
bool OpacityAppliedToChildren() const { return mOpacityAppliedToChildren; }
|
bool OpacityAppliedToChildren() const { return mOpacityAppliedToChildren; }
|
||||||
|
|
||||||
static bool NeedsActiveLayer(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame);
|
static bool NeedsActiveLayer(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame);
|
||||||
|
static bool MayNeedActiveLayer(nsIFrame* aFrame);
|
||||||
NS_DISPLAY_DECL_NAME("Opacity", TYPE_OPACITY)
|
NS_DISPLAY_DECL_NAME("Opacity", TYPE_OPACITY)
|
||||||
virtual void WriteDebugInfo(std::stringstream& aStream) override;
|
virtual void WriteDebugInfo(std::stringstream& aStream) override;
|
||||||
|
|
||||||
|
|
|
@ -1295,7 +1295,7 @@ nsSVGUtils::CanOptimizeOpacity(nsIFrame *aFrame)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nsLayoutUtils::HasAnimationOfProperty(aFrame, eCSSProperty_opacity)) {
|
if (nsLayoutUtils::MayHaveAnimationOfProperty(aFrame, eCSSProperty_opacity)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1885,6 +1885,7 @@ nsHostResolver::Create(uint32_t maxCacheEntries,
|
||||||
void
|
void
|
||||||
nsHostResolver::GetDNSCacheEntries(nsTArray<DNSCacheEntries> *args)
|
nsHostResolver::GetDNSCacheEntries(nsTArray<DNSCacheEntries> *args)
|
||||||
{
|
{
|
||||||
|
MutexAutoLock lock(mLock);
|
||||||
for (auto iter = mRecordDB.Iter(); !iter.Done(); iter.Next()) {
|
for (auto iter = mRecordDB.Iter(); !iter.Done(); iter.Next()) {
|
||||||
// We don't pay attention to address literals, only resolved domains.
|
// We don't pay attention to address literals, only resolved domains.
|
||||||
// Also require a host.
|
// Also require a host.
|
||||||
|
|
|
@ -1053,6 +1053,14 @@ public:
|
||||||
case __NR_mremap:
|
case __NR_mremap:
|
||||||
return Allow();
|
return Allow();
|
||||||
|
|
||||||
|
// Bug 1462640: Mesa libEGL uses mincore to test whether values
|
||||||
|
// are pointers, for reasons.
|
||||||
|
case __NR_mincore: {
|
||||||
|
Arg<size_t> length(1);
|
||||||
|
return If(length == getpagesize(), Allow())
|
||||||
|
.Else(SandboxPolicyCommon::EvaluateSyscall(sysno));
|
||||||
|
}
|
||||||
|
|
||||||
case __NR_sigaltstack:
|
case __NR_sigaltstack:
|
||||||
return Allow();
|
return Allow();
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ var EXPORTED_SYMBOLS = [
|
||||||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm", {});
|
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm", {});
|
||||||
|
|
||||||
ChromeUtils.defineModuleGetter(this, "RemoteSettings",
|
ChromeUtils.defineModuleGetter(this, "RemoteSettings", "resource://services-common/remote-settings.js");
|
||||||
"resource://services-common/remote-settings.js");
|
ChromeUtils.defineModuleGetter(this, "jexlFilterFunc", "resource://services-common/remote-settings.js");
|
||||||
|
|
||||||
const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
|
const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
|
||||||
const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
|
const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
|
||||||
|
@ -132,17 +132,22 @@ async function updateJSONBlocklist(client, { data: { current: records } }) {
|
||||||
* This custom filter function is used to limit the entries returned
|
* This custom filter function is used to limit the entries returned
|
||||||
* by `RemoteSettings("...").get()` depending on the target app information
|
* by `RemoteSettings("...").get()` depending on the target app information
|
||||||
* defined on entries.
|
* defined on entries.
|
||||||
*
|
|
||||||
* When landing Bug 1451031, this function will have to check if the `entry`
|
|
||||||
* has a JEXL attribute and rely on the JEXL filter function in priority.
|
|
||||||
* The legacy target app mechanism will be kept in place for old entries.
|
|
||||||
*/
|
*/
|
||||||
async function targetAppFilter(entry, { appID, version: appVersion }) {
|
async function targetAppFilter(entry, environment) {
|
||||||
|
// If the entry has JEXL filters, they should prevail.
|
||||||
|
// The legacy target app mechanism will be kept in place for old entries.
|
||||||
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1463377
|
||||||
|
const { filters } = entry;
|
||||||
|
if (filters) {
|
||||||
|
return jexlFilterFunc(entry, environment);
|
||||||
|
}
|
||||||
|
|
||||||
// Keep entries without target information.
|
// Keep entries without target information.
|
||||||
if (!("versionRange" in entry)) {
|
if (!("versionRange" in entry)) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { appID, version: appVersion } = environment;
|
||||||
const { versionRange } = entry;
|
const { versionRange } = entry;
|
||||||
|
|
||||||
// Gfx blocklist has a specific versionRange object, which is not a list.
|
// Gfx blocklist has a specific versionRange object, which is not a list.
|
||||||
|
|
|
@ -103,6 +103,18 @@ It is possible to package a dump of the server records that will be loaded into
|
||||||
Now, when ``RemoteSettings("some-key").get()`` is called from an empty profile, the ``some-key.json`` file is going to be loaded before the results are returned.
|
Now, when ``RemoteSettings("some-key").get()`` is called from an empty profile, the ``some-key.json`` file is going to be loaded before the results are returned.
|
||||||
|
|
||||||
|
|
||||||
|
Targets and A/B testing
|
||||||
|
=======================
|
||||||
|
|
||||||
|
In order to deliver settings to subsets of the population, you can set targets on entries (platform, language, channel, version range, preferences values, samples, etc.) when editing records on the server.
|
||||||
|
|
||||||
|
From the client API standpoint, this is completely transparent: the ``.get()`` method — as well as the event data — will always filter the entries on which the target matches.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The remote settings targets follow the same approach as the :ref:`Normandy recipe client <components/normandy>` (ie. JEXL filters),
|
||||||
|
|
||||||
|
|
||||||
Uptake Telemetry
|
Uptake Telemetry
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var EXPORTED_SYMBOLS = ["RemoteSettings"];
|
var EXPORTED_SYMBOLS = [
|
||||||
|
"RemoteSettings",
|
||||||
|
"jexlFilterFunc"
|
||||||
|
];
|
||||||
|
|
||||||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
@ -21,6 +24,7 @@ ChromeUtils.defineModuleGetter(this, "UptakeTelemetry",
|
||||||
"resource://services-common/uptake-telemetry.js");
|
"resource://services-common/uptake-telemetry.js");
|
||||||
ChromeUtils.defineModuleGetter(this, "ClientEnvironmentBase",
|
ChromeUtils.defineModuleGetter(this, "ClientEnvironmentBase",
|
||||||
"resource://gre/modules/components-utils/ClientEnvironment.jsm");
|
"resource://gre/modules/components-utils/ClientEnvironment.jsm");
|
||||||
|
ChromeUtils.defineModuleGetter(this, "FilterExpressions", "resource://normandy/lib/FilterExpressions.jsm");
|
||||||
|
|
||||||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||||
const PREF_SETTINGS_DEFAULT_BUCKET = "services.settings.default_bucket";
|
const PREF_SETTINGS_DEFAULT_BUCKET = "services.settings.default_bucket";
|
||||||
|
@ -62,6 +66,27 @@ class ClientEnvironment extends ClientEnvironmentBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default entry filtering function, in charge of excluding remote settings entries
|
||||||
|
* where the JEXL expression evaluates into a falsy value.
|
||||||
|
*/
|
||||||
|
async function jexlFilterFunc(entry, environment) {
|
||||||
|
const { filters } = entry;
|
||||||
|
if (!filters) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
const context = {
|
||||||
|
environment
|
||||||
|
};
|
||||||
|
result = await FilterExpressions.eval(filters, context);
|
||||||
|
} catch (e) {
|
||||||
|
Cu.reportError(e);
|
||||||
|
}
|
||||||
|
return result ? entry : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function mergeChanges(collection, localRecords, changes) {
|
function mergeChanges(collection, localRecords, changes) {
|
||||||
const records = {};
|
const records = {};
|
||||||
|
@ -150,7 +175,7 @@ async function fetchLatestChanges(url, lastEtag) {
|
||||||
|
|
||||||
class RemoteSettingsClient {
|
class RemoteSettingsClient {
|
||||||
|
|
||||||
constructor(collectionName, { bucketName, signerName, filterFunc, lastCheckTimePref }) {
|
constructor(collectionName, { bucketName, signerName, filterFunc = jexlFilterFunc, lastCheckTimePref }) {
|
||||||
this.collectionName = collectionName;
|
this.collectionName = collectionName;
|
||||||
this.bucketName = bucketName;
|
this.bucketName = bucketName;
|
||||||
this.signerName = signerName;
|
this.signerName = signerName;
|
||||||
|
|
|
@ -245,6 +245,57 @@ add_task(async function test_sync_event_data_is_filtered_for_target() {
|
||||||
});
|
});
|
||||||
add_task(clear_state);
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_entries_are_filtered_when_jexl_filters_is_present() {
|
||||||
|
if (IS_ANDROID) {
|
||||||
|
// JEXL filters are not supported on Android.
|
||||||
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1463502
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = [{
|
||||||
|
willMatch: true,
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: null
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: "1 == 1"
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: "1 == 2"
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: "1 == 1",
|
||||||
|
versionRange: [{
|
||||||
|
targetApplication: [{
|
||||||
|
guid: "some-guid"
|
||||||
|
}],
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
willMatch: false, // jexl prevails over versionRange.
|
||||||
|
filters: "1 == 2",
|
||||||
|
versionRange: [{
|
||||||
|
targetApplication: [{
|
||||||
|
guid: "xpcshell@tests.mozilla.org",
|
||||||
|
minVersion: "0",
|
||||||
|
maxVersion: "*",
|
||||||
|
}],
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
for (let {client} of gBlocklistClients) {
|
||||||
|
const collection = await client.openCollection();
|
||||||
|
for (const record of records) {
|
||||||
|
await collection.create(record);
|
||||||
|
}
|
||||||
|
await collection.db.saveLastModified(42); // Prevent from loading JSON dump.
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 4);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
|
||||||
// get a response for a given request from sample data
|
// get a response for a given request from sample data
|
||||||
function getSampleResponse(req, port) {
|
function getSampleResponse(req, port) {
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
const { RemoteSettings } = ChromeUtils.import("resource://services-common/remote-settings.js", {});
|
||||||
|
|
||||||
|
let client;
|
||||||
|
|
||||||
|
async function createRecords(records) {
|
||||||
|
const collection = await client.openCollection();
|
||||||
|
await collection.clear();
|
||||||
|
for (const record of records) {
|
||||||
|
await collection.create(record);
|
||||||
|
}
|
||||||
|
await collection.db.saveLastModified(42); // Prevent from loading JSON dump.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function run_test() {
|
||||||
|
client = RemoteSettings("some-key");
|
||||||
|
|
||||||
|
run_next_test();
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_returns_all_without_target() {
|
||||||
|
await createRecords([{
|
||||||
|
passwordSelector: "#pass-signin"
|
||||||
|
}, {
|
||||||
|
filters: null,
|
||||||
|
}, {
|
||||||
|
filters: "",
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_filters_can_be_disabled() {
|
||||||
|
const c = RemoteSettings("no-jexl", { filterFunc: null });
|
||||||
|
const collection = await c.openCollection();
|
||||||
|
await collection.create({
|
||||||
|
filters: "1 == 2"
|
||||||
|
});
|
||||||
|
await collection.db.saveLastModified(42); // Prevent from loading JSON dump.
|
||||||
|
|
||||||
|
const list = await c.get();
|
||||||
|
equal(list.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_returns_entries_where_jexl_is_true() {
|
||||||
|
await createRecords([{
|
||||||
|
willMatch: true,
|
||||||
|
filters: "1"
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: "[42]"
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: "1 == 2 || 1 == 1"
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: 'environment.appID == "xpcshell@tests.mozilla.org"'
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: "environment.version == undefined"
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: "environment.unknown == undefined"
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: "1 == 2"
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 5);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ignores_entries_where_jexl_is_invalid() {
|
||||||
|
await createRecords([{
|
||||||
|
filters: "true === true" // JavaScript Error: "Invalid expression token: ="
|
||||||
|
}, {
|
||||||
|
filters: "Objects.keys({}) == []" // Token ( (openParen) unexpected in expression
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_date_filters() {
|
||||||
|
await createRecords([{
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"1982-05-08"|date < "2016-03-22"|date'
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: '"2000-01-01"|date < "1970-01-01"|date'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 1);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_preferences_filters() {
|
||||||
|
await createRecords([{
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"services.settings.last_etag"|preferenceValue == 42'
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"services.settings.changes.path"|preferenceExists == true'
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"services.settings.changes.path"|preferenceIsUserSet == false'
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"services.settings.last_etag"|preferenceIsUserSet == true'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Set a pref for the user.
|
||||||
|
Services.prefs.setIntPref("services.settings.last_etag", 42);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 4);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_intersect_operator() {
|
||||||
|
await createRecords([{
|
||||||
|
willMatch: true,
|
||||||
|
filters: '{foo: 1, bar: 2}|keys intersect ["foo"]'
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: '(["a", "b"] intersect ["a", 1, 4]) == "a"'
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: '(["a", "b"] intersect [3, 1, 4]) == "c"'
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: `
|
||||||
|
[1, 2, 3]
|
||||||
|
intersect
|
||||||
|
[3, 4, 5]
|
||||||
|
`
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 3);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_samples() {
|
||||||
|
await createRecords([{
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"always-true"|stableSample(1)'
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: '"always-false"|stableSample(0)'
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"turns-to-true-0"|stableSample(0.5)'
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: '"turns-to-false-1"|stableSample(0.5)'
|
||||||
|
}, {
|
||||||
|
willMatch: true,
|
||||||
|
filters: '"turns-to-true-0"|bucketSample(0, 50, 100)'
|
||||||
|
}, {
|
||||||
|
willMatch: false,
|
||||||
|
filters: '"turns-to-false-1"|bucketSample(0, 50, 100)'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 3);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
|
@ -648,7 +648,7 @@ add_task(async function test_abort() {
|
||||||
|
|
||||||
Assert.equal(request.status, request.ABORTED);
|
Assert.equal(request.status, request.ABORTED);
|
||||||
|
|
||||||
await Assert.rejects(responsePromise);
|
await Assert.rejects(responsePromise, /NS_BINDING_ABORTED/);
|
||||||
|
|
||||||
await promiseStopServer(server);
|
await promiseStopServer(server);
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,9 @@ tags = blocklist
|
||||||
tags = remote-settings blocklist
|
tags = remote-settings blocklist
|
||||||
[test_remote_settings_poll.js]
|
[test_remote_settings_poll.js]
|
||||||
tags = remote-settings blocklist
|
tags = remote-settings blocklist
|
||||||
|
[test_remote_settings_jexl_filters.js]
|
||||||
|
skip-if = os == "android"
|
||||||
|
tags = remote-settings
|
||||||
|
|
||||||
[test_kinto.js]
|
[test_kinto.js]
|
||||||
tags = blocklist
|
tags = blocklist
|
||||||
|
|
|
@ -287,13 +287,15 @@ add_task(async function test_update_account_data() {
|
||||||
uid: "another_uid",
|
uid: "another_uid",
|
||||||
assertion: "new_assertion",
|
assertion: "new_assertion",
|
||||||
};
|
};
|
||||||
await Assert.rejects(account.updateUserAccountData(newCreds));
|
await Assert.rejects(account.updateUserAccountData(newCreds),
|
||||||
|
/The specified credentials aren't for the current user/);
|
||||||
|
|
||||||
// should fail without the uid.
|
// should fail without the uid.
|
||||||
newCreds = {
|
newCreds = {
|
||||||
assertion: "new_assertion",
|
assertion: "new_assertion",
|
||||||
};
|
};
|
||||||
await Assert.rejects(account.updateUserAccountData(newCreds));
|
await Assert.rejects(account.updateUserAccountData(newCreds),
|
||||||
|
/The specified credentials aren't for the current user/);
|
||||||
|
|
||||||
// and should fail with a field name that's not known by storage.
|
// and should fail with a field name that's not known by storage.
|
||||||
newCreds = {
|
newCreds = {
|
||||||
|
@ -301,7 +303,8 @@ add_task(async function test_update_account_data() {
|
||||||
uid: "another_uid",
|
uid: "another_uid",
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
};
|
};
|
||||||
await Assert.rejects(account.updateUserAccountData(newCreds));
|
await Assert.rejects(account.updateUserAccountData(newCreds),
|
||||||
|
/The specified credentials aren't for the current user/);
|
||||||
});
|
});
|
||||||
|
|
||||||
add_task(async function test_getCertificateOffline() {
|
add_task(async function test_getCertificateOffline() {
|
||||||
|
|
|
@ -23,7 +23,7 @@ add_task(async function test_non_https_remote_server_uri() {
|
||||||
Services.prefs.setCharPref(
|
Services.prefs.setCharPref(
|
||||||
"identity.fxaccounts.remote.root",
|
"identity.fxaccounts.remote.root",
|
||||||
"http://example.com/");
|
"http://example.com/");
|
||||||
Assert.rejects(FxAccounts.config.promiseSignUpURI(), null, "Firefox Accounts server must use HTTPS");
|
await Assert.rejects(FxAccounts.config.promiseSignUpURI(), /Firefox Accounts server must use HTTPS/);
|
||||||
Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
|
Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ add_storage_task(async function checkInitializedEmpty(sm) {
|
||||||
}
|
}
|
||||||
await sm.initialize();
|
await sm.initialize();
|
||||||
Assert.strictEqual((await sm.getAccountData()), null);
|
Assert.strictEqual((await sm.getAccountData()), null);
|
||||||
Assert.rejects(sm.updateAccountData({kXCS: "kXCS"}), "No user is logged in");
|
await Assert.rejects(sm.updateAccountData({kXCS: "kXCS"}), /No user is logged in/);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialized with account data (ie, simulating a new user being logged in).
|
// Initialized with account data (ie, simulating a new user being logged in).
|
||||||
|
@ -185,13 +185,14 @@ add_storage_task(async function checkEverythingRead(sm) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
add_storage_task(function checkInvalidUpdates(sm) {
|
add_storage_task(async function checkInvalidUpdates(sm) {
|
||||||
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
|
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
|
||||||
if (sm.secureStorage) {
|
if (sm.secureStorage) {
|
||||||
sm.secureStorage = new MockedSecureStorage(null);
|
sm.secureStorage = new MockedSecureStorage(null);
|
||||||
}
|
}
|
||||||
Assert.rejects(sm.updateAccountData({uid: "another"}), "Can't change");
|
await sm.initialize();
|
||||||
Assert.rejects(sm.updateAccountData({email: "someoneelse"}), "Can't change");
|
|
||||||
|
await Assert.rejects(sm.updateAccountData({uid: "another"}), /Can't change uid/);
|
||||||
});
|
});
|
||||||
|
|
||||||
add_storage_task(async function checkNullUpdatesRemovedUnlocked(sm) {
|
add_storage_task(async function checkNullUpdatesRemovedUnlocked(sm) {
|
||||||
|
|
|
@ -26,6 +26,8 @@ ChromeUtils.defineModuleGetter(this, "TelemetryUtils",
|
||||||
"resource://gre/modules/TelemetryUtils.jsm");
|
"resource://gre/modules/TelemetryUtils.jsm");
|
||||||
ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
|
ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
|
||||||
"resource://gre/modules/TelemetryEnvironment.jsm");
|
"resource://gre/modules/TelemetryEnvironment.jsm");
|
||||||
|
ChromeUtils.defineModuleGetter(this, "ObjectUtils",
|
||||||
|
"resource://gre/modules/ObjectUtils.jsm");
|
||||||
ChromeUtils.defineModuleGetter(this, "OS",
|
ChromeUtils.defineModuleGetter(this, "OS",
|
||||||
"resource://gre/modules/osfile.jsm");
|
"resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
|
@ -86,6 +88,27 @@ function timeDeltaFrom(monotonicStartTime) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converts extra integer fields to strings, rounds floats to three
|
||||||
|
// decimal places (nanosecond precision for timings), and removes profile
|
||||||
|
// directory paths and URLs from potential error messages.
|
||||||
|
function normalizeExtraTelemetryFields(extra) {
|
||||||
|
let result = {};
|
||||||
|
for (let key in extra) {
|
||||||
|
let value = extra[key];
|
||||||
|
let type = typeof value;
|
||||||
|
if (type == "string") {
|
||||||
|
result[key] = cleanErrorMessage(value);
|
||||||
|
} else if (type == "number") {
|
||||||
|
result[key] = Number.isInteger(value) ? value.toString(10) :
|
||||||
|
value.toFixed(3);
|
||||||
|
} else if (type != "undefined") {
|
||||||
|
throw new TypeError(`Invalid type ${
|
||||||
|
type} for extra telemetry field ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ObjectUtils.isEmpty(result) ? undefined : result;
|
||||||
|
}
|
||||||
|
|
||||||
// This function validates the payload of a telemetry "event" - this can be
|
// This function validates the payload of a telemetry "event" - this can be
|
||||||
// removed once there are APIs available for the telemetry modules to collect
|
// removed once there are APIs available for the telemetry modules to collect
|
||||||
// these events (bug 1329530) - but for now we simulate that planned API as
|
// these events (bug 1329530) - but for now we simulate that planned API as
|
||||||
|
@ -577,13 +600,18 @@ class SyncTelemetryImpl {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let { object, method, value, extra } = eventDetails;
|
||||||
|
if (extra) {
|
||||||
|
extra = normalizeExtraTelemetryFields(extra);
|
||||||
|
eventDetails = { object, method, value, extra };
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateTelemetryEvent(eventDetails)) {
|
if (!validateTelemetryEvent(eventDetails)) {
|
||||||
// we've already logged what the problem is...
|
// we've already logged what the problem is...
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.debug("recording event", eventDetails);
|
log.debug("recording event", eventDetails);
|
||||||
|
|
||||||
let { object, method, value, extra } = eventDetails;
|
|
||||||
if (extra && Resource.serverTime && !extra.serverTime) {
|
if (extra && Resource.serverTime && !extra.serverTime) {
|
||||||
extra.serverTime = String(Resource.serverTime);
|
extra.serverTime = String(Resource.serverTime);
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,8 @@ async function promiseNoLocalItem(guid) {
|
||||||
let got = await bms.fetch({ guid });
|
let got = await bms.fetch({ guid });
|
||||||
ok(!got, `No record remains with GUID ${guid}`);
|
ok(!got, `No record remains with GUID ${guid}`);
|
||||||
// and while we are here ensure the places cache doesn't still have it.
|
// and while we are here ensure the places cache doesn't still have it.
|
||||||
await Assert.rejects(PlacesUtils.promiseItemId(guid));
|
await Assert.rejects(PlacesUtils.promiseItemId(guid),
|
||||||
|
/no item found for the given GUID/);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validate(collection, expectedFailures = []) {
|
async function validate(collection, expectedFailures = []) {
|
||||||
|
|
|
@ -116,7 +116,7 @@ add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
|
||||||
let mockFxAClient = new AuthErrorMockFxAClient();
|
let mockFxAClient = new AuthErrorMockFxAClient();
|
||||||
browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
|
browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
|
||||||
|
|
||||||
await Assert.rejects(browseridManager._ensureValidToken(),
|
await Assert.rejects(browseridManager._ensureValidToken(), AuthenticationError,
|
||||||
"should reject due to an auth error");
|
"should reject due to an auth error");
|
||||||
|
|
||||||
Assert.ok(signCertificateCalled);
|
Assert.ok(signCertificateCalled);
|
||||||
|
@ -256,7 +256,8 @@ add_task(async function test_ensureLoggedIn() {
|
||||||
let fxa = globalBrowseridManager._fxaService;
|
let fxa = globalBrowseridManager._fxaService;
|
||||||
let signedInUser = fxa.internal.currentAccountState.storageManager.accountData;
|
let signedInUser = fxa.internal.currentAccountState.storageManager.accountData;
|
||||||
fxa.internal.currentAccountState.storageManager.accountData = null;
|
fxa.internal.currentAccountState.storageManager.accountData = null;
|
||||||
await Assert.rejects(globalBrowseridManager._ensureValidToken(true), "expecting rejection due to no user");
|
await Assert.rejects(globalBrowseridManager._ensureValidToken(true),
|
||||||
|
/Can't possibly get keys; User is not signed in/, "expecting rejection due to no user");
|
||||||
// Restore the logged in user to what it was.
|
// Restore the logged in user to what it was.
|
||||||
fxa.internal.currentAccountState.storageManager.accountData = signedInUser;
|
fxa.internal.currentAccountState.storageManager.accountData = signedInUser;
|
||||||
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
|
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
|
||||||
|
@ -302,6 +303,7 @@ add_task(async function test_getTokenErrors() {
|
||||||
let browseridManager = Service.identity;
|
let browseridManager = Service.identity;
|
||||||
|
|
||||||
await Assert.rejects(browseridManager._ensureValidToken(),
|
await Assert.rejects(browseridManager._ensureValidToken(),
|
||||||
|
AuthenticationError,
|
||||||
"should reject due to 401");
|
"should reject due to 401");
|
||||||
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
|
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
|
||||||
|
|
||||||
|
@ -317,6 +319,7 @@ add_task(async function test_getTokenErrors() {
|
||||||
});
|
});
|
||||||
browseridManager = Service.identity;
|
browseridManager = Service.identity;
|
||||||
await Assert.rejects(browseridManager._ensureValidToken(),
|
await Assert.rejects(browseridManager._ensureValidToken(),
|
||||||
|
TokenServerClientServerError,
|
||||||
"should reject due to non-JSON response");
|
"should reject due to non-JSON response");
|
||||||
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
|
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
|
||||||
});
|
});
|
||||||
|
@ -401,6 +404,7 @@ add_task(async function test_getTokenErrorWithRetry() {
|
||||||
let browseridManager = Service.identity;
|
let browseridManager = Service.identity;
|
||||||
|
|
||||||
await Assert.rejects(browseridManager._ensureValidToken(),
|
await Assert.rejects(browseridManager._ensureValidToken(),
|
||||||
|
TokenServerClientServerError,
|
||||||
"should reject due to 503");
|
"should reject due to 503");
|
||||||
|
|
||||||
// The observer should have fired - check it got the value in the response.
|
// The observer should have fired - check it got the value in the response.
|
||||||
|
@ -419,6 +423,7 @@ add_task(async function test_getTokenErrorWithRetry() {
|
||||||
browseridManager = Service.identity;
|
browseridManager = Service.identity;
|
||||||
|
|
||||||
await Assert.rejects(browseridManager._ensureValidToken(),
|
await Assert.rejects(browseridManager._ensureValidToken(),
|
||||||
|
TokenServerClientServerError,
|
||||||
"should reject due to no token in response");
|
"should reject due to no token in response");
|
||||||
|
|
||||||
// The observer should have fired - check it got the value in the response.
|
// The observer should have fired - check it got the value in the response.
|
||||||
|
@ -453,6 +458,7 @@ add_task(async function test_getKeysErrorWithBackoff() {
|
||||||
|
|
||||||
let browseridManager = Service.identity;
|
let browseridManager = Service.identity;
|
||||||
await Assert.rejects(browseridManager._ensureValidToken(),
|
await Assert.rejects(browseridManager._ensureValidToken(),
|
||||||
|
TokenServerClientServerError,
|
||||||
"should reject due to 503");
|
"should reject due to 503");
|
||||||
|
|
||||||
// The observer should have fired - check it got the value in the response.
|
// The observer should have fired - check it got the value in the response.
|
||||||
|
@ -489,6 +495,7 @@ add_task(async function test_getKeysErrorWithRetry() {
|
||||||
|
|
||||||
let browseridManager = Service.identity;
|
let browseridManager = Service.identity;
|
||||||
await Assert.rejects(browseridManager._ensureValidToken(),
|
await Assert.rejects(browseridManager._ensureValidToken(),
|
||||||
|
TokenServerClientServerError,
|
||||||
"should reject due to 503");
|
"should reject due to 503");
|
||||||
|
|
||||||
// The observer should have fired - check it got the value in the response.
|
// The observer should have fired - check it got the value in the response.
|
||||||
|
@ -731,6 +738,8 @@ async function initializeIdentityWithHAWKResponseFactory(config, cbGetResponse)
|
||||||
globalBrowseridManager._fxaService = fxa;
|
globalBrowseridManager._fxaService = fxa;
|
||||||
globalBrowseridManager._signedInUser = await fxa.getSignedInUser();
|
globalBrowseridManager._signedInUser = await fxa.getSignedInUser();
|
||||||
await Assert.rejects(globalBrowseridManager._ensureValidToken(true),
|
await Assert.rejects(globalBrowseridManager._ensureValidToken(true),
|
||||||
|
// TODO: Ideally this should have a specific check for an error.
|
||||||
|
() => true,
|
||||||
"expecting rejection due to hawk error");
|
"expecting rejection due to hawk error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@ add_task(async function test_findCluster() {
|
||||||
body: "",
|
body: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.rejects(Service.identity._findCluster());
|
await Assert.rejects(Service.identity._findCluster(),
|
||||||
|
/TokenServerClientServerError/);
|
||||||
|
|
||||||
_("_findCluster() returns null on authentication errors.");
|
_("_findCluster() returns null on authentication errors.");
|
||||||
initializeIdentityWithTokenServerResponse({
|
initializeIdentityWithTokenServerResponse({
|
||||||
|
|
|
@ -16,7 +16,8 @@ add_task(async function test_findCluster() {
|
||||||
Service.identity._ensureValidToken = () => Promise.reject(new Error("Connection refused"));
|
Service.identity._ensureValidToken = () => Promise.reject(new Error("Connection refused"));
|
||||||
|
|
||||||
_("_findCluster() throws on network errors (e.g. connection refused).");
|
_("_findCluster() throws on network errors (e.g. connection refused).");
|
||||||
await Assert.rejects(Service.identity._findCluster());
|
await Assert.rejects(Service.identity._findCluster(),
|
||||||
|
/Connection refused/);
|
||||||
|
|
||||||
Service.identity._ensureValidToken = () => Promise.resolve({ endpoint: "http://weave.user.node" });
|
Service.identity._ensureValidToken = () => Promise.resolve({ endpoint: "http://weave.user.node" });
|
||||||
|
|
||||||
|
|
|
@ -3199,6 +3199,7 @@ impl<'a> StyleBuilder<'a> {
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
|
<% del property %>
|
||||||
|
|
||||||
/// Inherits style from the parent element, accounting for the default
|
/// Inherits style from the parent element, accounting for the default
|
||||||
/// computed values that need to be provided as well.
|
/// computed values that need to be provided as well.
|
||||||
|
@ -3256,7 +3257,7 @@ impl<'a> StyleBuilder<'a> {
|
||||||
|
|
||||||
/// Gets a mutable view of the current `${style_struct.name}` style.
|
/// Gets a mutable view of the current `${style_struct.name}` style.
|
||||||
pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} {
|
pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} {
|
||||||
% if not property.style_struct.inherited:
|
% if not style_struct.inherited:
|
||||||
self.modified_reset = true;
|
self.modified_reset = true;
|
||||||
% endif
|
% endif
|
||||||
self.${style_struct.ident}.mutate()
|
self.${style_struct.ident}.mutate()
|
||||||
|
@ -3264,7 +3265,7 @@ impl<'a> StyleBuilder<'a> {
|
||||||
|
|
||||||
/// Gets a mutable view of the current `${style_struct.name}` style.
|
/// Gets a mutable view of the current `${style_struct.name}` style.
|
||||||
pub fn take_${style_struct.name_lower}(&mut self) -> UniqueArc<style_structs::${style_struct.name}> {
|
pub fn take_${style_struct.name_lower}(&mut self) -> UniqueArc<style_structs::${style_struct.name}> {
|
||||||
% if not property.style_struct.inherited:
|
% if not style_struct.inherited:
|
||||||
self.modified_reset = true;
|
self.modified_reset = true;
|
||||||
% endif
|
% endif
|
||||||
self.${style_struct.ident}.take()
|
self.${style_struct.ident}.take()
|
||||||
|
@ -3288,6 +3289,7 @@ impl<'a> StyleBuilder<'a> {
|
||||||
StyleStructRef::Borrowed(self.reset_style.${style_struct.name_lower}_arc());
|
StyleStructRef::Borrowed(self.reset_style.${style_struct.name_lower}_arc());
|
||||||
}
|
}
|
||||||
% endfor
|
% endfor
|
||||||
|
<% del style_struct %>
|
||||||
|
|
||||||
/// Returns whether this computed style represents a floated object.
|
/// Returns whether this computed style represents a floated object.
|
||||||
pub fn floated(&self) -> bool {
|
pub fn floated(&self) -> bool {
|
||||||
|
|
|
@ -352,10 +352,7 @@ talos-svgr:
|
||||||
description: "Talos svgr"
|
description: "Talos svgr"
|
||||||
try-name: svgr
|
try-name: svgr
|
||||||
treeherder-symbol: T(s)
|
treeherder-symbol: T(s)
|
||||||
run-on-projects:
|
run-on-projects: ['mozilla-beta', 'mozilla-central', 'mozilla-inbound', 'autoland', 'try']
|
||||||
by-test-platform:
|
|
||||||
windows10-64-qr/.*: [] # bug 1451305
|
|
||||||
default: ['mozilla-beta', 'mozilla-central', 'mozilla-inbound', 'autoland', 'try']
|
|
||||||
max-run-time: 1800
|
max-run-time: 1800
|
||||||
mozharness:
|
mozharness:
|
||||||
extra-options:
|
extra-options:
|
||||||
|
|
|
@ -170,8 +170,9 @@ class DesktopPartnerRepacks(ReleaseMixin, BuildbotMixin, PurgeMixin,
|
||||||
for locale in self.config["limitLocales"]:
|
for locale in self.config["limitLocales"]:
|
||||||
repack_cmd.extend(["--limit-locale", locale])
|
repack_cmd.extend(["--limit-locale", locale])
|
||||||
|
|
||||||
return self.run_command(repack_cmd,
|
self.run_command(repack_cmd,
|
||||||
cwd=self.query_abs_dirs()['abs_scripts_dir'])
|
cwd=self.query_abs_dirs()['abs_scripts_dir'],
|
||||||
|
halt_on_failure=True)
|
||||||
|
|
||||||
|
|
||||||
# main {{{
|
# main {{{
|
||||||
|
|
|
@ -60,7 +60,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
||||||
Async: "resource://services-common/async.js",
|
Async: "resource://services-common/async.js",
|
||||||
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
|
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
|
||||||
Log: "resource://gre/modules/Log.jsm",
|
Log: "resource://gre/modules/Log.jsm",
|
||||||
ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
|
|
||||||
OS: "resource://gre/modules/osfile.jsm",
|
OS: "resource://gre/modules/osfile.jsm",
|
||||||
PlacesSyncUtils: "resource://gre/modules/PlacesSyncUtils.jsm",
|
PlacesSyncUtils: "resource://gre/modules/PlacesSyncUtils.jsm",
|
||||||
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
|
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
|
||||||
|
@ -224,8 +223,7 @@ class SyncedBookmarksMirror {
|
||||||
try {
|
try {
|
||||||
let info = await OS.File.stat(path);
|
let info = await OS.File.stat(path);
|
||||||
let size = Math.floor(info.size / 1024);
|
let size = Math.floor(info.size / 1024);
|
||||||
options.recordTelemetryEvent("mirror", "open", "success",
|
options.recordTelemetryEvent("mirror", "open", "success", { size });
|
||||||
normalizeExtraTelemetryFields({ size }));
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
MirrorLog.warn("Error recording stats for mirror database size", ex);
|
MirrorLog.warn("Error recording stats for mirror database size", ex);
|
||||||
}
|
}
|
||||||
|
@ -352,70 +350,42 @@ class SyncedBookmarksMirror {
|
||||||
*/
|
*/
|
||||||
async store(records, { needsMerge = true } = {}) {
|
async store(records, { needsMerge = true } = {}) {
|
||||||
let options = { needsMerge };
|
let options = { needsMerge };
|
||||||
let ignoreCounts = {
|
await this.db.executeBeforeShutdown(
|
||||||
bookmark: { id: 0, url: 0 },
|
"SyncedBookmarksMirror: store",
|
||||||
query: { id: 0, url: 0 },
|
db => db.executeTransaction(async () => {
|
||||||
folder: { id: 0, root: 0 },
|
for await (let record of yieldingIterator(records)) {
|
||||||
child: { id: 0, root: 0 },
|
MirrorLog.trace(`Storing in mirror: ${record.cleartextToString()}`);
|
||||||
livemark: { id: 0, feed: 0 },
|
switch (record.type) {
|
||||||
separator: { id: 0 },
|
case "bookmark":
|
||||||
tombstone: { id: 0, root: 0 },
|
await this.storeRemoteBookmark(record, options);
|
||||||
};
|
continue;
|
||||||
let extraTelemetryEvents = [];
|
|
||||||
try {
|
|
||||||
await this.db.executeBeforeShutdown(
|
|
||||||
"SyncedBookmarksMirror: store",
|
|
||||||
db => db.executeTransaction(async () => {
|
|
||||||
for await (let record of yieldingIterator(records)) {
|
|
||||||
MirrorLog.trace(`Storing in mirror: ${record.cleartextToString()}`);
|
|
||||||
switch (record.type) {
|
|
||||||
case "bookmark":
|
|
||||||
await this.storeRemoteBookmark(record, ignoreCounts, options);
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case "query":
|
case "query":
|
||||||
await this.storeRemoteQuery(record, ignoreCounts, options);
|
await this.storeRemoteQuery(record, options);
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
case "folder":
|
case "folder":
|
||||||
await this.storeRemoteFolder(record, ignoreCounts, options);
|
await this.storeRemoteFolder(record, options);
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
case "livemark":
|
case "livemark":
|
||||||
await this.storeRemoteLivemark(record, ignoreCounts, options);
|
await this.storeRemoteLivemark(record, options);
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
case "separator":
|
case "separator":
|
||||||
await this.storeRemoteSeparator(record, ignoreCounts, options);
|
await this.storeRemoteSeparator(record, options);
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (record.deleted) {
|
if (record.deleted) {
|
||||||
await this.storeRemoteTombstone(record, ignoreCounts,
|
await this.storeRemoteTombstone(record, options);
|
||||||
options);
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
MirrorLog.warn("Ignoring record with unknown type", record.type);
|
|
||||||
extraTelemetryEvents.push({
|
|
||||||
method: "ignore",
|
|
||||||
value: "unknown-kind",
|
|
||||||
extra: { kind: record.type },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
MirrorLog.warn("Ignoring record with unknown type", record.type);
|
||||||
));
|
|
||||||
} finally {
|
|
||||||
for (let { method, value, extra } of extraTelemetryEvents) {
|
|
||||||
this.recordTelemetryEvent("mirror", method, value, extra);
|
|
||||||
}
|
|
||||||
for (let kind in ignoreCounts) {
|
|
||||||
let extra = normalizeExtraTelemetryFields(ignoreCounts[kind]);
|
|
||||||
if (extra) {
|
|
||||||
this.recordTelemetryEvent("mirror", "ignore", kind, extra);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -443,61 +413,69 @@ class SyncedBookmarksMirror {
|
||||||
async apply({ localTimeSeconds = Date.now() / 1000,
|
async apply({ localTimeSeconds = Date.now() / 1000,
|
||||||
remoteTimeSeconds = 0,
|
remoteTimeSeconds = 0,
|
||||||
weakUpload = [] } = {}) {
|
weakUpload = [] } = {}) {
|
||||||
|
// We intentionally don't use `executeBeforeShutdown` in this function,
|
||||||
|
// since merging can take a while for large trees, and we don't want to
|
||||||
|
// block shutdown. Since all new items are in the mirror, we'll just try
|
||||||
|
// to merge again on the next sync.
|
||||||
|
|
||||||
let hasChanges = weakUpload.length > 0 || (await this.hasChanges());
|
let hasChanges = weakUpload.length > 0 || (await this.hasChanges());
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
MirrorLog.debug("No changes detected in both mirror and Places");
|
MirrorLog.debug("No changes detected in both mirror and Places");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
// We intentionally don't use `executeBeforeShutdown` in this function,
|
|
||||||
// since merging can take a while for large trees, and we don't want to
|
// The flow ID is used to correlate telemetry events for each sync.
|
||||||
// block shutdown. Since all new items are in the mirror, we'll just try
|
let flowID = PlacesUtils.history.makeGuid();
|
||||||
// to merge again on the next sync.
|
|
||||||
let { missingParents, missingChildren } = await this.fetchRemoteOrphans();
|
let { missingParents, missingChildren, parentsWithGaps } =
|
||||||
|
await this.fetchRemoteOrphans();
|
||||||
if (missingParents.length) {
|
if (missingParents.length) {
|
||||||
MirrorLog.warn("Temporarily reparenting remote items with missing " +
|
MirrorLog.warn("Temporarily reparenting remote items with missing " +
|
||||||
"parents to unfiled", missingParents);
|
"parents to unfiled", missingParents);
|
||||||
this.recordTelemetryEvent("mirror", "orphans", "parents",
|
|
||||||
normalizeExtraTelemetryFields({ count: missingParents.length }));
|
|
||||||
}
|
}
|
||||||
if (missingChildren.length) {
|
if (missingChildren.length) {
|
||||||
MirrorLog.warn("Remote tree missing items", missingChildren);
|
MirrorLog.warn("Remote tree missing items", missingChildren);
|
||||||
this.recordTelemetryEvent("mirror", "orphans", "children",
|
}
|
||||||
normalizeExtraTelemetryFields({ count: missingChildren.length }));
|
if (parentsWithGaps.length) {
|
||||||
|
MirrorLog.warn("Remote tree has parents with gaps in positions",
|
||||||
|
parentsWithGaps);
|
||||||
}
|
}
|
||||||
|
|
||||||
let { missingLocal, missingRemote, wrongSyncStatus } =
|
let { missingLocal, missingRemote, wrongSyncStatus } =
|
||||||
await this.fetchInconsistencies();
|
await this.fetchSyncStatusMismatches();
|
||||||
if (missingLocal.length) {
|
if (missingLocal.length) {
|
||||||
MirrorLog.warn("Remote tree has merged items that don't exist locally",
|
MirrorLog.warn("Remote tree has merged items that don't exist locally",
|
||||||
missingLocal);
|
missingLocal);
|
||||||
this.recordTelemetryEvent("mirror", "inconsistencies", "local",
|
|
||||||
normalizeExtraTelemetryFields({ count: missingLocal.length }));
|
|
||||||
}
|
}
|
||||||
if (missingRemote.length) {
|
if (missingRemote.length) {
|
||||||
MirrorLog.warn("Local tree has synced items that don't exist remotely",
|
MirrorLog.warn("Local tree has synced items that don't exist remotely",
|
||||||
missingRemote);
|
missingRemote);
|
||||||
this.recordTelemetryEvent("mirror", "inconsistencies", "remote",
|
|
||||||
normalizeExtraTelemetryFields({ count: missingRemote.length }));
|
|
||||||
}
|
}
|
||||||
if (wrongSyncStatus.length) {
|
if (wrongSyncStatus.length) {
|
||||||
MirrorLog.warn("Local tree has wrong sync statuses for items that " +
|
MirrorLog.warn("Local tree has wrong sync statuses for items that " +
|
||||||
"exist remotely", wrongSyncStatus);
|
"exist remotely", wrongSyncStatus);
|
||||||
this.recordTelemetryEvent("mirror", "inconsistencies", "syncStatus",
|
|
||||||
normalizeExtraTelemetryFields({ count: wrongSyncStatus.length }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let applyStats = {};
|
this.recordTelemetryEvent("mirror", "apply", "problems", {
|
||||||
|
flowID,
|
||||||
|
missingParents: missingParents.length,
|
||||||
|
missingChildren: missingChildren.length,
|
||||||
|
parentsWithGaps: parentsWithGaps.length,
|
||||||
|
missingLocal: missingLocal.length,
|
||||||
|
missingRemote: missingRemote.length,
|
||||||
|
wrongSyncStatus: wrongSyncStatus.length,
|
||||||
|
});
|
||||||
|
|
||||||
// It's safe to build the remote tree outside the transaction because
|
// It's safe to build the remote tree outside the transaction because
|
||||||
// `fetchRemoteTree` doesn't join to Places, only Sync writes to the
|
// `fetchRemoteTree` doesn't join to Places, only Sync writes to the
|
||||||
// mirror, and we're holding the Sync lock at this point.
|
// mirror, and we're holding the Sync lock at this point.
|
||||||
MirrorLog.debug("Building remote tree from mirror");
|
let remoteTree = await withTiming(
|
||||||
let { result: remoteTree, time: remoteTreeTiming } = await withTiming(
|
"Building remote tree from mirror",
|
||||||
"Fetch remote tree",
|
() => this.fetchRemoteTree(remoteTimeSeconds),
|
||||||
() => this.fetchRemoteTree(remoteTimeSeconds)
|
(time, tree) => this.recordTelemetryEvent("mirror", "apply",
|
||||||
|
"fetchRemoteTree", { flowID, time, deletions: tree.deletedGuids.size,
|
||||||
|
nodes: tree.byGuid.size })
|
||||||
);
|
);
|
||||||
applyStats.remoteTree = { time: remoteTreeTiming,
|
|
||||||
count: remoteTree.guidCount };
|
|
||||||
if (MirrorLog.level <= Log.Level.Debug) {
|
if (MirrorLog.level <= Log.Level.Debug) {
|
||||||
MirrorLog.debug("Built remote tree from mirror\n" +
|
MirrorLog.debug("Built remote tree from mirror\n" +
|
||||||
remoteTree.toASCIITreeString());
|
remoteTree.toASCIITreeString());
|
||||||
|
@ -505,122 +483,140 @@ class SyncedBookmarksMirror {
|
||||||
|
|
||||||
let observersToNotify = new BookmarkObserverRecorder(this.db);
|
let observersToNotify = new BookmarkObserverRecorder(this.db);
|
||||||
|
|
||||||
let changeRecords = await this.db.executeTransaction(async () => {
|
let changeRecords;
|
||||||
MirrorLog.debug("Building local tree from Places");
|
try {
|
||||||
let { result: localTree, time: localTreeTiming } = await withTiming(
|
changeRecords = await this.db.executeTransaction(async () => {
|
||||||
"Fetch local tree",
|
let localTree = await withTiming(
|
||||||
() => this.fetchLocalTree(localTimeSeconds)
|
"Building local tree from Places",
|
||||||
);
|
() => this.fetchLocalTree(localTimeSeconds),
|
||||||
applyStats.localTree = { time: localTreeTiming,
|
(time, tree) => this.recordTelemetryEvent("mirror", "apply",
|
||||||
count: localTree.guidCount };
|
"fetchLocalTree", { flowID, time, deletions: tree.deletedGuids.size,
|
||||||
if (MirrorLog.level <= Log.Level.Debug) {
|
nodes: tree.byGuid.size })
|
||||||
MirrorLog.debug("Built local tree from Places\n" +
|
);
|
||||||
localTree.toASCIITreeString());
|
if (MirrorLog.level <= Log.Level.Debug) {
|
||||||
}
|
MirrorLog.debug("Built local tree from Places\n" +
|
||||||
|
localTree.toASCIITreeString());
|
||||||
MirrorLog.debug("Fetching content info for new mirror items");
|
|
||||||
let {
|
|
||||||
result: newRemoteContents,
|
|
||||||
time: remoteContentsTiming,
|
|
||||||
} = await withTiming(
|
|
||||||
"Fetch new remote contents",
|
|
||||||
() => this.fetchNewRemoteContents()
|
|
||||||
);
|
|
||||||
applyStats.remoteContents = { time: remoteContentsTiming,
|
|
||||||
count: newRemoteContents.size };
|
|
||||||
|
|
||||||
MirrorLog.debug("Fetching content info for new Places items");
|
|
||||||
let {
|
|
||||||
result: newLocalContents,
|
|
||||||
time: localContentsTiming,
|
|
||||||
} = await withTiming(
|
|
||||||
"Fetch new local contents",
|
|
||||||
() => this.fetchNewLocalContents()
|
|
||||||
);
|
|
||||||
applyStats.localContents = { time: localContentsTiming,
|
|
||||||
count: newLocalContents.size };
|
|
||||||
|
|
||||||
MirrorLog.debug("Building complete merged tree");
|
|
||||||
let merger = new BookmarkMerger(localTree, newLocalContents,
|
|
||||||
remoteTree, newRemoteContents);
|
|
||||||
let mergedRoot;
|
|
||||||
try {
|
|
||||||
let time;
|
|
||||||
({ result: mergedRoot, time } = await withTiming(
|
|
||||||
"Build merged tree",
|
|
||||||
() => merger.merge()
|
|
||||||
));
|
|
||||||
applyStats.merge = { time };
|
|
||||||
} finally {
|
|
||||||
for (let { value, extra } of merger.summarizeTelemetryEvents()) {
|
|
||||||
this.recordTelemetryEvent("mirror", "merge", value, extra);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (MirrorLog.level <= Log.Level.Debug) {
|
let newRemoteContents = await withTiming(
|
||||||
MirrorLog.debug([
|
"Fetching content info for new mirror items",
|
||||||
"Built new merged tree",
|
() => this.fetchNewRemoteContents(),
|
||||||
mergedRoot.toASCIITreeString(),
|
(time, contents) => this.recordTelemetryEvent("mirror", "apply",
|
||||||
...merger.deletionsToStrings(),
|
"fetchNewRemoteContents", { flowID, time, count: contents.size })
|
||||||
].join("\n"));
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// The merged tree should know about all items mentioned in the local
|
let newLocalContents = await withTiming(
|
||||||
// and remote trees. Otherwise, it's incomplete, and we'll corrupt
|
"Fetching content info for new Places items",
|
||||||
// Places or lose data on the server if we try to apply it.
|
() => this.fetchNewLocalContents(),
|
||||||
if (!await merger.subsumes(localTree)) {
|
(time, contents) => this.recordTelemetryEvent("mirror", "apply",
|
||||||
throw new SyncedBookmarksMirror.ConsistencyError(
|
"fetchNewLocalContents", { flowID, time, count: contents.size })
|
||||||
"Merged tree doesn't mention all items from local tree");
|
);
|
||||||
}
|
|
||||||
if (!await merger.subsumes(remoteTree)) {
|
|
||||||
throw new SyncedBookmarksMirror.ConsistencyError(
|
|
||||||
"Merged tree doesn't mention all items from remote tree");
|
|
||||||
}
|
|
||||||
|
|
||||||
MirrorLog.debug("Applying merged tree");
|
let merger = new BookmarkMerger(localTree, newLocalContents,
|
||||||
let deletions = [];
|
remoteTree, newRemoteContents);
|
||||||
for await (let deletion of yieldingIterator(merger.deletions())) {
|
let mergedRoot = await withTiming(
|
||||||
deletions.push(deletion);
|
"Building complete merged tree",
|
||||||
}
|
() => merger.merge(),
|
||||||
let { time: updateTiming } = await withTiming(
|
time => {
|
||||||
"Apply merged tree",
|
this.recordTelemetryEvent("mirror", "apply", "merge",
|
||||||
() => this.updateLocalItemsInPlaces(mergedRoot, deletions)
|
{ flowID, time, nodes: merger.mergedGuids.size,
|
||||||
);
|
localDeletions: merger.deleteLocally.size,
|
||||||
applyStats.update = { time: updateTiming };
|
remoteDeletions: merger.deleteRemotely.size,
|
||||||
|
dupes: merger.dupeCount });
|
||||||
|
|
||||||
// At this point, the database is consistent, and we can fetch info to
|
this.recordTelemetryEvent("mirror", "merge", "structure",
|
||||||
// pass to observers. Note that we can't fetch observer info in the
|
merger.structureCounts);
|
||||||
// triggers above, because the structure might not be complete yet. An
|
}
|
||||||
// incomplete structure might cause us to miss or record wrong parents and
|
);
|
||||||
// positions.
|
|
||||||
|
|
||||||
MirrorLog.debug("Recording observer notifications");
|
if (MirrorLog.level <= Log.Level.Debug) {
|
||||||
await this.noteObserverChanges(observersToNotify);
|
MirrorLog.debug([
|
||||||
|
"Built new merged tree",
|
||||||
|
mergedRoot.toASCIITreeString(),
|
||||||
|
...merger.deletionsToStrings(),
|
||||||
|
].join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
let {
|
// The merged tree should know about all items mentioned in the local
|
||||||
result: changeRecords,
|
// and remote trees. Otherwise, it's incomplete, and we'll corrupt
|
||||||
time: stageTiming,
|
// Places or lose data on the server if we try to apply it.
|
||||||
} = await withTiming("Stage outgoing items", async () => {
|
if (!await merger.subsumes(localTree)) {
|
||||||
MirrorLog.debug("Staging locally changed items for upload");
|
throw new SyncedBookmarksMirror.ConsistencyError(
|
||||||
await this.stageItemsToUpload(weakUpload);
|
"Merged tree doesn't mention all items from local tree");
|
||||||
|
}
|
||||||
|
if (!await merger.subsumes(remoteTree)) {
|
||||||
|
throw new SyncedBookmarksMirror.ConsistencyError(
|
||||||
|
"Merged tree doesn't mention all items from remote tree");
|
||||||
|
}
|
||||||
|
|
||||||
MirrorLog.debug("Fetching records for local items to upload");
|
await withTiming(
|
||||||
return this.fetchLocalChangeRecords();
|
"Applying merged tree",
|
||||||
|
async () => {
|
||||||
|
let deletions = [];
|
||||||
|
for await (let deletion of yieldingIterator(merger.deletions())) {
|
||||||
|
deletions.push(deletion);
|
||||||
|
}
|
||||||
|
await this.updateLocalItemsInPlaces(mergedRoot, deletions);
|
||||||
|
},
|
||||||
|
time => this.recordTelemetryEvent("mirror", "apply",
|
||||||
|
"updateLocalItemsInPlaces", { flowID, time })
|
||||||
|
);
|
||||||
|
|
||||||
|
// At this point, the database is consistent, and we can fetch info to
|
||||||
|
// pass to observers. Note that we can't fetch observer info in the
|
||||||
|
// triggers above, because the structure might not be complete yet. An
|
||||||
|
// incomplete structure might cause us to miss or record wrong parents and
|
||||||
|
// positions.
|
||||||
|
|
||||||
|
await withTiming(
|
||||||
|
"Recording observer notifications",
|
||||||
|
() => this.noteObserverChanges(observersToNotify),
|
||||||
|
time => this.recordTelemetryEvent("mirror", "apply",
|
||||||
|
"noteObserverChanges", { flowID, time })
|
||||||
|
);
|
||||||
|
|
||||||
|
await withTiming(
|
||||||
|
"Staging locally changed items for upload",
|
||||||
|
() => this.stageItemsToUpload(weakUpload),
|
||||||
|
time => this.recordTelemetryEvent("mirror", "apply",
|
||||||
|
"stageItemsToUpload", { flowID, time })
|
||||||
|
);
|
||||||
|
|
||||||
|
let changeRecords = await withTiming(
|
||||||
|
"Fetching records for local items to upload",
|
||||||
|
() => this.fetchLocalChangeRecords(),
|
||||||
|
(time, records) => this.recordTelemetryEvent("mirror", "apply",
|
||||||
|
"fetchLocalChangeRecords", { flowID,
|
||||||
|
count: Object.keys(records).length })
|
||||||
|
);
|
||||||
|
|
||||||
|
await withTiming(
|
||||||
|
"Cleaning up merge tables",
|
||||||
|
async () => {
|
||||||
|
await this.db.execute(`DELETE FROM mergeStates`);
|
||||||
|
await this.db.execute(`DELETE FROM itemsAdded`);
|
||||||
|
await this.db.execute(`DELETE FROM guidsChanged`);
|
||||||
|
await this.db.execute(`DELETE FROM itemsChanged`);
|
||||||
|
await this.db.execute(`DELETE FROM itemsRemoved`);
|
||||||
|
await this.db.execute(`DELETE FROM itemsMoved`);
|
||||||
|
await this.db.execute(`DELETE FROM annosChanged`);
|
||||||
|
await this.db.execute(`DELETE FROM idsToWeaklyUpload`);
|
||||||
|
await this.db.execute(`DELETE FROM itemsToUpload`);
|
||||||
|
},
|
||||||
|
time => this.recordTelemetryEvent("mirror", "apply", "cleanup",
|
||||||
|
{ flowID, time })
|
||||||
|
);
|
||||||
|
|
||||||
|
return changeRecords;
|
||||||
});
|
});
|
||||||
applyStats.stage = { time: stageTiming };
|
} catch (ex) {
|
||||||
|
// Include the error message in the event payload, since we can't
|
||||||
await this.db.execute(`DELETE FROM mergeStates`);
|
// easily correlate event telemetry to engine errors in the Sync ping.
|
||||||
await this.db.execute(`DELETE FROM itemsAdded`);
|
let why = (typeof ex.message == "string" ? ex.message :
|
||||||
await this.db.execute(`DELETE FROM guidsChanged`);
|
String(ex)).slice(0, 85);
|
||||||
await this.db.execute(`DELETE FROM itemsChanged`);
|
this.recordTelemetryEvent("mirror", "apply", "error", { flowID, why });
|
||||||
await this.db.execute(`DELETE FROM itemsRemoved`);
|
throw ex;
|
||||||
await this.db.execute(`DELETE FROM itemsMoved`);
|
}
|
||||||
await this.db.execute(`DELETE FROM annosChanged`);
|
|
||||||
await this.db.execute(`DELETE FROM idsToWeaklyUpload`);
|
|
||||||
await this.db.execute(`DELETE FROM itemsToUpload`);
|
|
||||||
|
|
||||||
return changeRecords;
|
|
||||||
});
|
|
||||||
|
|
||||||
MirrorLog.debug("Replaying recorded observer notifications");
|
MirrorLog.debug("Replaying recorded observer notifications");
|
||||||
try {
|
try {
|
||||||
|
@ -629,13 +625,6 @@ class SyncedBookmarksMirror {
|
||||||
MirrorLog.warn("Error notifying Places observers", ex);
|
MirrorLog.warn("Error notifying Places observers", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let value in applyStats) {
|
|
||||||
let extra = normalizeExtraTelemetryFields(applyStats[value]);
|
|
||||||
if (extra) {
|
|
||||||
this.recordTelemetryEvent("mirror", "apply", value, extra);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changeRecords;
|
return changeRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -662,11 +651,10 @@ class SyncedBookmarksMirror {
|
||||||
return rows.map(row => row.getResultByName("guid"));
|
return rows.map(row => row.getResultByName("guid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeRemoteBookmark(record, ignoreCounts, { needsMerge }) {
|
async storeRemoteBookmark(record, { needsMerge }) {
|
||||||
let guid = validateGuid(record.id);
|
let guid = validateGuid(record.id);
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
MirrorLog.warn("Ignoring bookmark with invalid ID", record.id);
|
MirrorLog.warn("Ignoring bookmark with invalid ID", record.id);
|
||||||
ignoreCounts.bookmark.id++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -674,7 +662,6 @@ class SyncedBookmarksMirror {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
MirrorLog.warn("Ignoring bookmark ${guid} with invalid URL ${url}",
|
MirrorLog.warn("Ignoring bookmark ${guid} with invalid URL ${url}",
|
||||||
{ guid, url: record.bmkUri });
|
{ guid, url: record.bmkUri });
|
||||||
ignoreCounts.bookmark.url++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -717,11 +704,10 @@ class SyncedBookmarksMirror {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeRemoteQuery(record, ignoreCounts, { needsMerge }) {
|
async storeRemoteQuery(record, { needsMerge }) {
|
||||||
let guid = validateGuid(record.id);
|
let guid = validateGuid(record.id);
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
MirrorLog.warn("Ignoring query with invalid ID", record.id);
|
MirrorLog.warn("Ignoring query with invalid ID", record.id);
|
||||||
ignoreCounts.query.id++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -729,7 +715,6 @@ class SyncedBookmarksMirror {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
MirrorLog.warn("Ignoring query ${guid} with invalid URL ${url}",
|
MirrorLog.warn("Ignoring query ${guid} with invalid URL ${url}",
|
||||||
{ guid, url: record.bmkUri });
|
{ guid, url: record.bmkUri });
|
||||||
ignoreCounts.query.url++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -743,7 +728,6 @@ class SyncedBookmarksMirror {
|
||||||
if (!tagFolderName) {
|
if (!tagFolderName) {
|
||||||
MirrorLog.warn("Ignoring tag query ${guid} with invalid tag name " +
|
MirrorLog.warn("Ignoring tag query ${guid} with invalid tag name " +
|
||||||
"${tagFolderName}", { guid, tagFolderName });
|
"${tagFolderName}", { guid, tagFolderName });
|
||||||
ignoreCounts.query.url++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
url = new URL(`place:tag=${tagFolderName}`);
|
url = new URL(`place:tag=${tagFolderName}`);
|
||||||
|
@ -773,17 +757,15 @@ class SyncedBookmarksMirror {
|
||||||
url: url.href, description, smartBookmarkName });
|
url: url.href, description, smartBookmarkName });
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeRemoteFolder(record, ignoreCounts, { needsMerge }) {
|
async storeRemoteFolder(record, { needsMerge }) {
|
||||||
let guid = validateGuid(record.id);
|
let guid = validateGuid(record.id);
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
MirrorLog.warn("Ignoring folder with invalid ID", record.id);
|
MirrorLog.warn("Ignoring folder with invalid ID", record.id);
|
||||||
ignoreCounts.folder.id++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (guid == PlacesUtils.bookmarks.rootGuid) {
|
if (guid == PlacesUtils.bookmarks.rootGuid) {
|
||||||
// The Places root shouldn't be synced at all.
|
// The Places root shouldn't be synced at all.
|
||||||
MirrorLog.warn("Ignoring Places root record", record);
|
MirrorLog.warn("Ignoring Places root record", record);
|
||||||
ignoreCounts.folder.root++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverModified = determineServerModified(record);
|
let serverModified = determineServerModified(record);
|
||||||
|
@ -810,13 +792,11 @@ class SyncedBookmarksMirror {
|
||||||
MirrorLog.warn("Ignoring child of folder ${parentGuid} with " +
|
MirrorLog.warn("Ignoring child of folder ${parentGuid} with " +
|
||||||
"invalid ID ${childRecordId}", { parentGuid: guid,
|
"invalid ID ${childRecordId}", { parentGuid: guid,
|
||||||
childRecordId });
|
childRecordId });
|
||||||
ignoreCounts.child.id++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (childGuid == PlacesUtils.bookmarks.rootGuid ||
|
if (childGuid == PlacesUtils.bookmarks.rootGuid ||
|
||||||
PlacesUtils.bookmarks.userContentRoots.includes(childGuid)) {
|
PlacesUtils.bookmarks.userContentRoots.includes(childGuid)) {
|
||||||
MirrorLog.warn("Ignoring move for root", childGuid);
|
MirrorLog.warn("Ignoring move for root", childGuid);
|
||||||
ignoreCounts.child.root++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await this.db.executeCached(`
|
await this.db.executeCached(`
|
||||||
|
@ -841,11 +821,10 @@ class SyncedBookmarksMirror {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeRemoteLivemark(record, ignoreCounts, { needsMerge }) {
|
async storeRemoteLivemark(record, { needsMerge }) {
|
||||||
let guid = validateGuid(record.id);
|
let guid = validateGuid(record.id);
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
MirrorLog.warn("Ignoring livemark with invalid ID", record.id);
|
MirrorLog.warn("Ignoring livemark with invalid ID", record.id);
|
||||||
ignoreCounts.livemark.id++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -853,7 +832,6 @@ class SyncedBookmarksMirror {
|
||||||
if (!feedURL) {
|
if (!feedURL) {
|
||||||
MirrorLog.warn("Ignoring livemark ${guid} with invalid feed URL ${url}",
|
MirrorLog.warn("Ignoring livemark ${guid} with invalid feed URL ${url}",
|
||||||
{ guid, url: record.feedUri });
|
{ guid, url: record.feedUri });
|
||||||
ignoreCounts.livemark.feed++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -874,11 +852,10 @@ class SyncedBookmarksMirror {
|
||||||
siteURL: siteURL ? siteURL.href : null });
|
siteURL: siteURL ? siteURL.href : null });
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeRemoteSeparator(record, ignoreCounts, { needsMerge }) {
|
async storeRemoteSeparator(record, { needsMerge }) {
|
||||||
let guid = validateGuid(record.id);
|
let guid = validateGuid(record.id);
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
MirrorLog.warn("Ignoring separator with invalid ID", record.id);
|
MirrorLog.warn("Ignoring separator with invalid ID", record.id);
|
||||||
ignoreCounts.separator.id++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -894,18 +871,16 @@ class SyncedBookmarksMirror {
|
||||||
dateAdded });
|
dateAdded });
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeRemoteTombstone(record, ignoreCounts, { needsMerge }) {
|
async storeRemoteTombstone(record, { needsMerge }) {
|
||||||
let guid = validateGuid(record.id);
|
let guid = validateGuid(record.id);
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
MirrorLog.warn("Ignoring tombstone with invalid ID", record.id);
|
MirrorLog.warn("Ignoring tombstone with invalid ID", record.id);
|
||||||
ignoreCounts.tombstone.id++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (guid == PlacesUtils.bookmarks.rootGuid ||
|
if (guid == PlacesUtils.bookmarks.rootGuid ||
|
||||||
PlacesUtils.bookmarks.userContentRoots.includes(guid)) {
|
PlacesUtils.bookmarks.userContentRoots.includes(guid)) {
|
||||||
MirrorLog.warn("Ignoring tombstone for root", guid);
|
MirrorLog.warn("Ignoring tombstone for root", guid);
|
||||||
ignoreCounts.tombstone.root++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -929,19 +904,29 @@ class SyncedBookmarksMirror {
|
||||||
let infos = {
|
let infos = {
|
||||||
missingParents: [],
|
missingParents: [],
|
||||||
missingChildren: [],
|
missingChildren: [],
|
||||||
|
parentsWithGaps: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
let orphanRows = await this.db.execute(`
|
let orphanRows = await this.db.execute(`
|
||||||
SELECT v.guid AS guid, 1 AS missingParent, 0 AS missingChild
|
SELECT v.guid AS guid, 1 AS missingParent, 0 AS missingChild,
|
||||||
|
0 AS parentWithGaps
|
||||||
FROM items v
|
FROM items v
|
||||||
LEFT JOIN structure s ON s.guid = v.guid
|
LEFT JOIN structure s ON s.guid = v.guid
|
||||||
WHERE NOT v.isDeleted AND
|
WHERE NOT v.isDeleted AND
|
||||||
s.guid IS NULL
|
s.guid IS NULL
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT s.guid AS guid, 0 AS missingParent, 1 AS missingChild
|
SELECT s.guid AS guid, 0 AS missingParent, 1 AS missingChild,
|
||||||
|
0 AS parentsWithGaps
|
||||||
FROM structure s
|
FROM structure s
|
||||||
LEFT JOIN items v ON v.guid = s.guid
|
LEFT JOIN items v ON v.guid = s.guid
|
||||||
WHERE v.guid IS NULL`);
|
WHERE v.guid IS NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT s.parentGuid AS guid, 0 AS missingParent, 0 AS missingChild,
|
||||||
|
1 AS parentWithGaps
|
||||||
|
FROM structure s
|
||||||
|
GROUP BY s.parentGuid
|
||||||
|
HAVING (sum(DISTINCT position + 1) -
|
||||||
|
(count(*) * (count(*) + 1) / 2)) <> 0`);
|
||||||
|
|
||||||
for await (let row of yieldingIterator(orphanRows)) {
|
for await (let row of yieldingIterator(orphanRows)) {
|
||||||
let guid = row.getResultByName("guid");
|
let guid = row.getResultByName("guid");
|
||||||
|
@ -953,6 +938,10 @@ class SyncedBookmarksMirror {
|
||||||
if (missingChild) {
|
if (missingChild) {
|
||||||
infos.missingChildren.push(guid);
|
infos.missingChildren.push(guid);
|
||||||
}
|
}
|
||||||
|
let parentWithGaps = row.getResultByName("parentWithGaps");
|
||||||
|
if (parentWithGaps) {
|
||||||
|
infos.parentsWithGaps.push(guid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return infos;
|
return infos;
|
||||||
|
@ -971,7 +960,7 @@ class SyncedBookmarksMirror {
|
||||||
* - `missingRemote`: NORMAL items in the local tree that aren't
|
* - `missingRemote`: NORMAL items in the local tree that aren't
|
||||||
* mentioned in the remote tree.
|
* mentioned in the remote tree.
|
||||||
*/
|
*/
|
||||||
async fetchInconsistencies() {
|
async fetchSyncStatusMismatches() {
|
||||||
let infos = {
|
let infos = {
|
||||||
missingLocal: [],
|
missingLocal: [],
|
||||||
missingRemote: [],
|
missingRemote: [],
|
||||||
|
@ -1895,28 +1884,6 @@ class DatabaseCorruptError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts extra integer fields to strings, and rounds timings to nanosecond
|
|
||||||
// precision.
|
|
||||||
function normalizeExtraTelemetryFields(extra) {
|
|
||||||
let result = {};
|
|
||||||
for (let key in extra) {
|
|
||||||
let value = extra[key];
|
|
||||||
let type = typeof value;
|
|
||||||
if (type == "string") {
|
|
||||||
result[key] = value;
|
|
||||||
} else if (type == "number") {
|
|
||||||
if (value > 0) {
|
|
||||||
result[key] = Number.isInteger(value) ? value.toString(10) :
|
|
||||||
value.toFixed(3);
|
|
||||||
}
|
|
||||||
} else if (type != "undefined") {
|
|
||||||
throw new TypeError(`Invalid type ${
|
|
||||||
type} for extra telemetry field ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ObjectUtils.isEmpty(result) ? undefined : result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indicates if the mirror should be replaced because the database file is
|
// Indicates if the mirror should be replaced because the database file is
|
||||||
// corrupt.
|
// corrupt.
|
||||||
function isDatabaseCorrupt(error) {
|
function isDatabaseCorrupt(error) {
|
||||||
|
@ -2835,15 +2802,31 @@ async function inflateTree(tree, pseudoTree, parentNode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Executes a function and returns a `{ result, time }` tuple, where `result` is
|
/**
|
||||||
// the function's return value, and `time` is the time taken to execute the
|
* Measures and logs the time taken to execute a function, using a monotonic
|
||||||
// function.
|
* clock.
|
||||||
async function withTiming(name, func) {
|
*
|
||||||
|
* @param {String} name
|
||||||
|
* The name of the operation, used for logging.
|
||||||
|
* @param {Function} func
|
||||||
|
* The function to time.
|
||||||
|
* @param {Function} recordTiming
|
||||||
|
* A function with the signature `(time: Number, result: Object?)`,
|
||||||
|
* where `time` is the measured time, and `result` is the return
|
||||||
|
* value of the timed function.
|
||||||
|
* @return The return value of the timed function.
|
||||||
|
*/
|
||||||
|
async function withTiming(name, func, recordTiming) {
|
||||||
|
MirrorLog.debug(name);
|
||||||
|
|
||||||
let startTime = Cu.now();
|
let startTime = Cu.now();
|
||||||
let result = await func();
|
let result = await func();
|
||||||
let elapsedTime = Cu.now() - startTime;
|
let elapsedTime = Cu.now() - startTime;
|
||||||
|
|
||||||
MirrorLog.trace(`${name} took ${elapsedTime.toFixed(3)}ms`);
|
MirrorLog.trace(`${name} took ${elapsedTime.toFixed(3)}ms`);
|
||||||
return { result, time: elapsedTime };
|
recordTiming(elapsedTime, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3194,10 +3177,6 @@ class BookmarkTree {
|
||||||
this.deletedGuids = new Set();
|
this.deletedGuids = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
get guidCount() {
|
|
||||||
return this.byGuid.size + this.deletedGuids.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDeleted(guid) {
|
isDeleted(guid) {
|
||||||
return this.deletedGuids.has(guid);
|
return this.deletedGuids.has(guid);
|
||||||
}
|
}
|
||||||
|
@ -3382,22 +3361,6 @@ class BookmarkMerger {
|
||||||
remoteDeletes: 0, // Remote folder deletion wins over local change.
|
remoteDeletes: 0, // Remote folder deletion wins over local change.
|
||||||
};
|
};
|
||||||
this.dupeCount = 0;
|
this.dupeCount = 0;
|
||||||
this.extraTelemetryEvents = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
summarizeTelemetryEvents() {
|
|
||||||
let events = [...this.extraTelemetryEvents];
|
|
||||||
if (this.dupeCount > 0) {
|
|
||||||
events.push({
|
|
||||||
value: "dupes",
|
|
||||||
extra: normalizeExtraTelemetryFields({ count: this.dupeCount }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let structureExtra = normalizeExtraTelemetryFields(this.structureCounts);
|
|
||||||
if (structureExtra) {
|
|
||||||
events.push({ value: "structure", extra: structureExtra });
|
|
||||||
}
|
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async merge() {
|
async merge() {
|
||||||
|
@ -3548,11 +3511,6 @@ class BookmarkMerger {
|
||||||
if (!localNode.hasCompatibleKind(remoteNode)) {
|
if (!localNode.hasCompatibleKind(remoteNode)) {
|
||||||
MirrorLog.error("Merging local ${localNode} and remote ${remoteNode} " +
|
MirrorLog.error("Merging local ${localNode} and remote ${remoteNode} " +
|
||||||
"with different kinds", { localNode, remoteNode });
|
"with different kinds", { localNode, remoteNode });
|
||||||
this.extraTelemetryEvents.push({
|
|
||||||
value: "kind-mismatch",
|
|
||||||
extra: { local: localNode.kindToString().toLowerCase(),
|
|
||||||
remote: remoteNode.kindToString().toLowerCase() },
|
|
||||||
});
|
|
||||||
throw new SyncedBookmarksMirror.ConsistencyError(
|
throw new SyncedBookmarksMirror.ConsistencyError(
|
||||||
"Can't merge different item kinds");
|
"Can't merge different item kinds");
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,7 @@ async function getCountOfBookmarkRows(db) {
|
||||||
}
|
}
|
||||||
|
|
||||||
add_task(async function test_corrupt_roots() {
|
add_task(async function test_corrupt_roots() {
|
||||||
let telemetryEvents = [];
|
let buf = await openMirror("corrupt_roots");
|
||||||
let buf = await openMirror("corrupt_roots", {
|
|
||||||
recordTelemetryEvent(object, method, value, extra) {
|
|
||||||
if (object == "mirror" && ["open", "apply"].includes(method)) {
|
|
||||||
// Ignore timings, mirror database file, and tree sizes.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
telemetryEvents.push({ object, method, value, extra });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
info("Set up empty mirror");
|
info("Set up empty mirror");
|
||||||
await PlacesTestUtils.markBookmarksAsSynced();
|
await PlacesTestUtils.markBookmarksAsSynced();
|
||||||
|
@ -48,17 +39,6 @@ add_task(async function test_corrupt_roots() {
|
||||||
|
|
||||||
let changesToUpload = await buf.apply();
|
let changesToUpload = await buf.apply();
|
||||||
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
||||||
deepEqual(telemetryEvents, [{
|
|
||||||
object: "mirror",
|
|
||||||
method: "ignore",
|
|
||||||
value: "child",
|
|
||||||
extra: { root: "1" },
|
|
||||||
}, {
|
|
||||||
object: "mirror",
|
|
||||||
method: "ignore",
|
|
||||||
value: "tombstone",
|
|
||||||
extra: { root: "1" },
|
|
||||||
}], "Should record telemetry for ignored invalid roots");
|
|
||||||
|
|
||||||
deepEqual(changesToUpload, {}, "Should not reupload invalid roots");
|
deepEqual(changesToUpload, {}, "Should not reupload invalid roots");
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ add_task(async function test_duping_local_newer() {
|
||||||
|
|
||||||
// The mirror is out of sync because `bookmarkAAA5` is marked as merged,
|
// The mirror is out of sync because `bookmarkAAA5` is marked as merged,
|
||||||
// even though it's not in Places, but we should still recover.
|
// even though it's not in Places, but we should still recover.
|
||||||
deepEqual(await buf.fetchInconsistencies(), {
|
deepEqual(await buf.fetchSyncStatusMismatches(), {
|
||||||
missingLocal: ["bookmarkAAA5"],
|
missingLocal: ["bookmarkAAA5"],
|
||||||
missingRemote: [],
|
missingRemote: [],
|
||||||
wrongSyncStatus: [],
|
wrongSyncStatus: [],
|
||||||
|
@ -86,11 +86,9 @@ add_task(async function test_duping_local_newer() {
|
||||||
});
|
});
|
||||||
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
||||||
deepEqual(mergeTelemetryEvents, [{
|
deepEqual(mergeTelemetryEvents, [{
|
||||||
value: "dupes",
|
|
||||||
extra: { count: "2" },
|
|
||||||
}, {
|
|
||||||
value: "structure",
|
value: "structure",
|
||||||
extra: { new: "1" },
|
extra: { new: 1, remoteRevives: 0, localDeletes: 0, localRevives: 0,
|
||||||
|
remoteDeletes: 0 },
|
||||||
}], "Should record telemetry with dupe counts");
|
}], "Should record telemetry with dupe counts");
|
||||||
|
|
||||||
let menuInfo = await PlacesUtils.bookmarks.fetch(
|
let menuInfo = await PlacesUtils.bookmarks.fetch(
|
||||||
|
|
|
@ -114,7 +114,8 @@ add_task(async function test_complex_orphaning() {
|
||||||
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
||||||
deepEqual(mergeTelemetryEvents, [{
|
deepEqual(mergeTelemetryEvents, [{
|
||||||
value: "structure",
|
value: "structure",
|
||||||
extra: { new: "2", localDeletes: "1", remoteDeletes: "1" },
|
extra: { new: 2, remoteRevives: 0, localDeletes: 1, localRevives: 0,
|
||||||
|
remoteDeletes: 1 },
|
||||||
}], "Should record telemetry with structure change counts");
|
}], "Should record telemetry with structure change counts");
|
||||||
|
|
||||||
let idsToUpload = inspectChangeRecords(changesToUpload);
|
let idsToUpload = inspectChangeRecords(changesToUpload);
|
||||||
|
@ -306,7 +307,8 @@ add_task(async function test_locally_modified_remotely_deleted() {
|
||||||
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
||||||
deepEqual(mergeTelemetryEvents, [{
|
deepEqual(mergeTelemetryEvents, [{
|
||||||
value: "structure",
|
value: "structure",
|
||||||
extra: { new: "1", localRevives: "1", remoteDeletes: "2" },
|
extra: { new: 1, remoteRevives: 0, localDeletes: 0, localRevives: 1,
|
||||||
|
remoteDeletes: 2 },
|
||||||
}], "Should record telemetry for local item and remote folder deletions");
|
}], "Should record telemetry for local item and remote folder deletions");
|
||||||
|
|
||||||
let idsToUpload = inspectChangeRecords(changesToUpload);
|
let idsToUpload = inspectChangeRecords(changesToUpload);
|
||||||
|
@ -453,7 +455,8 @@ add_task(async function test_locally_deleted_remotely_modified() {
|
||||||
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
||||||
deepEqual(mergeTelemetryEvents, [{
|
deepEqual(mergeTelemetryEvents, [{
|
||||||
value: "structure",
|
value: "structure",
|
||||||
extra: { new: "1", remoteRevives: "1", localDeletes: "2" },
|
extra: { new: 1, remoteRevives: 1, localDeletes: 2, localRevives: 0,
|
||||||
|
remoteDeletes: 0 },
|
||||||
}], "Should record telemetry for remote item and local folder deletions");
|
}], "Should record telemetry for remote item and local folder deletions");
|
||||||
|
|
||||||
let idsToUpload = inspectChangeRecords(changesToUpload);
|
let idsToUpload = inspectChangeRecords(changesToUpload);
|
||||||
|
|
|
@ -418,15 +418,12 @@ add_task(async function test_mismatched_but_compatible_folder_types() {
|
||||||
});
|
});
|
||||||
|
|
||||||
add_task(async function test_mismatched_but_incompatible_folder_types() {
|
add_task(async function test_mismatched_but_incompatible_folder_types() {
|
||||||
let sawMismatchEvent = false;
|
let sawMismatchError = false;
|
||||||
let recordTelemetryEvent = (object, method, value, extra) => {
|
let recordTelemetryEvent = (object, method, value, extra) => {
|
||||||
// expecting to see a kind-mismatch event.
|
// expecting to see an error for kind mismatches.
|
||||||
if (value == "kind-mismatch" &&
|
if (method == "apply" && value == "error" &&
|
||||||
extra.local && typeof extra.local == "string" &&
|
extra && extra.why == "Can't merge different item kinds") {
|
||||||
extra.local == "livemark" &&
|
sawMismatchError = true;
|
||||||
extra.remote && typeof extra.remote == "string" &&
|
|
||||||
extra.remote == "folder") {
|
|
||||||
sawMismatchEvent = true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let buf = await openMirror("mismatched_incompatible_types",
|
let buf = await openMirror("mismatched_incompatible_types",
|
||||||
|
@ -458,7 +455,7 @@ add_task(async function test_mismatched_but_incompatible_folder_types() {
|
||||||
|
|
||||||
info("Apply remote, should fail");
|
info("Apply remote, should fail");
|
||||||
await Assert.rejects(buf.apply(), /Can't merge different item kinds/);
|
await Assert.rejects(buf.apply(), /Can't merge different item kinds/);
|
||||||
Assert.ok(sawMismatchEvent, "saw the correct mismatch event");
|
Assert.ok(sawMismatchError, "saw the correct mismatch event");
|
||||||
} finally {
|
} finally {
|
||||||
await buf.finalize();
|
await buf.finalize();
|
||||||
await PlacesUtils.bookmarks.eraseEverything();
|
await PlacesUtils.bookmarks.eraseEverything();
|
||||||
|
@ -539,15 +536,12 @@ add_task(async function test_different_but_compatible_bookmark_types() {
|
||||||
});
|
});
|
||||||
|
|
||||||
add_task(async function test_incompatible_types() {
|
add_task(async function test_incompatible_types() {
|
||||||
let sawMismatchEvent = false;
|
let sawMismatchError = false;
|
||||||
let recordTelemetryEvent = (object, method, value, extra) => {
|
let recordTelemetryEvent = (object, method, value, extra) => {
|
||||||
// expecting to see a kind-mismatch event.
|
// expecting to see an error for kind mismatches.
|
||||||
if (value == "kind-mismatch" &&
|
if (method == "apply" && value == "error" &&
|
||||||
extra.local && typeof extra.local == "string" &&
|
extra && extra.why == "Can't merge different item kinds") {
|
||||||
extra.local == "bookmark" &&
|
sawMismatchError = true;
|
||||||
extra.remote && typeof extra.remote == "string" &&
|
|
||||||
extra.remote == "folder") {
|
|
||||||
sawMismatchEvent = true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
@ -582,7 +576,7 @@ add_task(async function test_incompatible_types() {
|
||||||
await PlacesTestUtils.markBookmarksAsSynced();
|
await PlacesTestUtils.markBookmarksAsSynced();
|
||||||
|
|
||||||
await Assert.rejects(buf.apply(), /Can't merge different item kinds/);
|
await Assert.rejects(buf.apply(), /Can't merge different item kinds/);
|
||||||
Assert.ok(sawMismatchEvent, "saw expected mismatch event");
|
Assert.ok(sawMismatchError, "saw expected mismatch event");
|
||||||
} finally {
|
} finally {
|
||||||
await PlacesUtils.bookmarks.eraseEverything();
|
await PlacesUtils.bookmarks.eraseEverything();
|
||||||
await PlacesSyncUtils.bookmarks.reset();
|
await PlacesSyncUtils.bookmarks.reset();
|
||||||
|
|
|
@ -647,7 +647,8 @@ add_task(async function test_complex_move_with_additions() {
|
||||||
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
|
||||||
deepEqual(mergeTelemetryEvents, [{
|
deepEqual(mergeTelemetryEvents, [{
|
||||||
value: "structure",
|
value: "structure",
|
||||||
extra: { new: "1" },
|
extra: { new: 1, remoteRevives: 0, localDeletes: 0, localRevives: 0,
|
||||||
|
remoteDeletes: 0 },
|
||||||
}], "Should record telemetry with structure change counts");
|
}], "Should record telemetry with structure change counts");
|
||||||
|
|
||||||
let idsToUpload = inspectChangeRecords(changesToUpload);
|
let idsToUpload = inspectChangeRecords(changesToUpload);
|
||||||
|
|
|
@ -48,7 +48,7 @@ add_task(async function test_inconsistencies() {
|
||||||
await PlacesUtils.bookmarks.remove("bookmarkCCCC");
|
await PlacesUtils.bookmarks.remove("bookmarkCCCC");
|
||||||
await PlacesUtils.bookmarks.remove("bookmarkDDDD");
|
await PlacesUtils.bookmarks.remove("bookmarkDDDD");
|
||||||
|
|
||||||
deepEqual(await buf.fetchInconsistencies(), {
|
deepEqual(await buf.fetchSyncStatusMismatches(), {
|
||||||
missingLocal: [],
|
missingLocal: [],
|
||||||
missingRemote: [],
|
missingRemote: [],
|
||||||
wrongSyncStatus: [],
|
wrongSyncStatus: [],
|
||||||
|
@ -96,7 +96,7 @@ add_task(async function test_inconsistencies() {
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
let { missingLocal, missingRemote, wrongSyncStatus } =
|
let { missingLocal, missingRemote, wrongSyncStatus } =
|
||||||
await buf.fetchInconsistencies();
|
await buf.fetchSyncStatusMismatches();
|
||||||
deepEqual(missingLocal, ["bookmarkGGGG"],
|
deepEqual(missingLocal, ["bookmarkGGGG"],
|
||||||
"Should report merged remote items that don't exist locally");
|
"Should report merged remote items that don't exist locally");
|
||||||
deepEqual(missingRemote.sort(), ["bookmarkBBBB", "bookmarkCCCC"],
|
deepEqual(missingRemote.sort(), ["bookmarkBBBB", "bookmarkCCCC"],
|
||||||
|
|
Загрузка…
Ссылка в новой задаче