Update On Tue Sep 3 20:50:13 CEST 2024

This commit is contained in:
github-action[bot] 2024-09-03 20:50:14 +02:00
parent 8b9e162fce
commit fc2b848c21
816 changed files with 29842 additions and 5386 deletions

14
Cargo.lock generated
View file

@ -2323,6 +2323,7 @@ dependencies = [
"kvstore",
"l10nregistry",
"l10nregistry-ffi",
"libz-rs-sys",
"lmdb-rkv-sys",
"localization-ffi",
"log",
@ -3407,6 +3408,14 @@ dependencies = [
"libc",
]
[[package]]
name = "libz-rs-sys"
version = "0.2.1"
source = "git+https://github.com/memorysafety/zlib-rs?rev=4aa430ccb77537d0d60dab8db993ca51bb1194c5#4aa430ccb77537d0d60dab8db993ca51bb1194c5"
dependencies = [
"zlib-rs",
]
[[package]]
name = "line-wrap"
version = "0.1.1"
@ -7240,3 +7249,8 @@ dependencies = [
"memchr",
"thiserror",
]
[[package]]
name = "zlib-rs"
version = "0.2.1"
source = "git+https://github.com/memorysafety/zlib-rs?rev=4aa430ccb77537d0d60dab8db993ca51bb1194c5#4aa430ccb77537d0d60dab8db993ca51bb1194c5"

View file

@ -22,12 +22,7 @@
<browser id="sidebar" autoscroll="false" disablehistory="true" disablefullscreen="true" tooltip="aHTMLTooltip"/>
</vbox>
<splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" resizebefore="sibling" resizeafter="none" hidden="true"/>
<vbox id="appcontent" flex="1">
<!-- gNotificationBox will be added here lazily. -->
<tabbox id="tabbrowser-tabbox"
flex="1" tabcontainer="tabbrowser-tabs">
<tabpanels id="tabbrowser-tabpanels"
flex="1" selectedIndex="0"/>
</tabbox>
</vbox>
<tabbox id="tabbrowser-tabbox" flex="1" tabcontainer="tabbrowser-tabs">
<tabpanels id="tabbrowser-tabpanels" flex="1" selectedIndex="0"/>
</tabbox>
</hbox>

View file

@ -1108,12 +1108,13 @@ var PlacesToolbarHelper = {
);
if (toolbar.id == "PersonalToolbar") {
if (!toolbar.hasAttribute("initialized")) {
toolbar.setAttribute("initialized", "true");
}
// We just created a new view, thus we must check again the empty toolbar
// message, regardless of "initialized".
BookmarkingUI.updateEmptyToolbarMessage().catch(console.error);
BookmarkingUI.updateEmptyToolbarMessage()
.finally(() => {
toolbar.toggleAttribute("initialized", true);
})
.catch(console.error);
}
},
@ -1530,8 +1531,7 @@ var BookmarkingUI = {
* We hide it in customize mode, unless there's nothing on the toolbar.
*/
async updateEmptyToolbarMessage() {
let checkNumBookmarksOnToolbar = false;
let hasVisibleChildren = (() => {
let { initialHiddenState, checkHasBookmarks } = (() => {
// Do we have visible kids?
if (
this.toolbar.querySelector(
@ -1541,25 +1541,29 @@ var BookmarkingUI = {
:scope > toolbaritem:not([hidden], #personal-bookmarks)`
)
) {
return true;
return { initialHiddenState: true, checkHasBookmarks: false };
}
if (!this.toolbar.hasAttribute("initialized") && !this._isCustomizing) {
// If the bookmarks are here but it's early in startup, show the
// message. It'll get made visibility: hidden early in startup anyway -
// it's just to ensure the toolbar has height.
return false;
if (this._isCustomizing) {
return { initialHiddenState: true, checkHasBookmarks: false };
}
// Hmm, apparently not. Check for bookmarks or customize mode:
// If bookmarks have been moved out of the toolbar, we show the message.
let bookmarksToolbarItemsPlacement =
CustomizableUI.getPlacementOfWidget("personal-bookmarks");
let bookmarksItemInToolbar =
bookmarksToolbarItemsPlacement?.area == CustomizableUI.AREA_BOOKMARKS;
if (!bookmarksItemInToolbar) {
return false;
return { initialHiddenState: false, checkHasBookmarks: false };
}
if (this._isCustomizing) {
return true;
if (!this.toolbar.hasAttribute("initialized")) {
// If the toolbar has not been initialized yet, unhide the message, it
// will be made 0-width and visibility: hidden anyway, to keep the
// toolbar height stable.
return { initialHiddenState: false, checkHasBookmarks: true };
}
// Check visible bookmark nodes.
if (
this.toolbar.querySelector(
@ -1567,19 +1571,16 @@ var BookmarkingUI = {
#PlacesToolbarItems > toolbarbutton`
)
) {
return true;
return { initialHiddenState: true, checkHasBookmarks: false };
}
checkNumBookmarksOnToolbar = true;
return false;
return { initialHiddenState: true, checkHasBookmarks: true };
})();
if (checkNumBookmarksOnToolbar) {
hasVisibleChildren = !(await PlacesToolbarHelper.getIsEmpty());
}
let emptyMsg = document.getElementById("personal-toolbar-empty");
emptyMsg.hidden = hasVisibleChildren;
emptyMsg.toggleAttribute("nowidth", !hasVisibleChildren);
emptyMsg.hidden = initialHiddenState;
if (checkHasBookmarks) {
emptyMsg.hidden = !(await PlacesToolbarHelper.getIsEmpty());
}
},
openLibraryIfLinkClicked(event) {

View file

@ -97,8 +97,9 @@ add_task(async function () {
AppConstants.DEBUG &&
// In the content area
r.y1 >=
document.getElementById("appcontent").getBoundingClientRect()
.top,
document
.getElementById("tabbrowser-tabbox")
.getBoundingClientRect().top,
},
],
},

View file

@ -82,7 +82,8 @@ add_task(async function () {
condition: r =>
// In the content area
r.y1 >=
document.getElementById("appcontent").getBoundingClientRect().top,
document.getElementById("tabbrowser-tabbox").getBoundingClientRect()
.top,
},
],
};

View file

@ -115,8 +115,9 @@ add_task(async function () {
AppConstants.DEBUG &&
// In the content area
r.y1 >=
document.getElementById("appcontent").getBoundingClientRect()
.top,
document
.getElementById("tabbrowser-tabbox")
.getBoundingClientRect().top,
},
],
},

View file

@ -7,14 +7,14 @@ const EXPECTED_START_ORDINALS = [
["sidebar-main", 1],
["sidebar-box", 2],
["sidebar-splitter", 3],
["appcontent", 4],
["tabbrowser-tabbox", 4],
];
const EXPECTED_END_ORDINALS = [
["sidebar-main", 5],
["sidebar-box", 4],
["sidebar-splitter", 3],
["appcontent", 2],
["tabbrowser-tabbox", 2],
];
function getBrowserChildrenWithOrdinals() {

View file

@ -540,28 +540,31 @@ async function createAndRemoveDefaultFolder() {
}
async function showLibraryColumn(library, columnName) {
await SimpleTest.promiseFocus(library);
const viewMenu = library.document.getElementById("viewMenu");
const viewMenuPopup = library.document.getElementById("viewMenuPopup");
const onViewMenuPopup = new Promise(resolve => {
viewMenuPopup.addEventListener("popupshown", () => resolve(), {
once: true,
});
info("await viewMenuPopup");
await new Promise(resolve => {
library.document
.getElementById("viewMenuPopup")
.addEventListener("popupshown", resolve, {
once: true,
});
viewMenu.click();
});
EventUtils.synthesizeMouseAtCenter(viewMenu, {}, library);
await onViewMenuPopup;
const viewColumns = library.document.getElementById("viewColumns");
const viewColumnsPopup = viewColumns.querySelector("menupopup");
const onViewColumnsPopup = new Promise(resolve => {
viewColumnsPopup.addEventListener("popupshown", () => resolve(), {
once: true,
});
info("await viewColumnsPopup");
await new Promise(resolve => {
viewColumns
.querySelector("menupopup")
.addEventListener("popupshown", () => resolve(), {
once: true,
});
viewColumns.click();
});
EventUtils.synthesizeMouseAtCenter(viewColumns, {}, library);
await onViewColumnsPopup;
const columnMenu = library.document.getElementById(`menucol_${columnName}`);
EventUtils.synthesizeMouseAtCenter(columnMenu, {}, library);
library.document.getElementById(`menucol_${columnName}`).click();
}
registerCleanupFunction(async () => {

View file

@ -103,7 +103,10 @@ add_task(async function test_search_icon() {
await SpecialPowers.spawn(tab, [expectedIconURL], async function (iconURL) {
let computedStyle = content.window.getComputedStyle(content.document.body);
await ContentTaskUtils.waitForCondition(
() => computedStyle.getPropertyValue("--newtab-search-icon") != "null",
() =>
computedStyle
.getPropertyValue("--newtab-search-icon")
.startsWith("url"),
"Search Icon should get set."
);

View file

@ -366,9 +366,8 @@ add_task(async function test_createRegionWithKeyboard() {
} else {
const sidebar = document.querySelector("sidebar-main");
const sidebarWidth = sidebar.offsetWidth;
// Add 1 to account for #appcontent border
window100X =
(100 + window.mozInnerScreenX + sidebarWidth + 1) *
(100 + window.mozInnerScreenX + sidebarWidth) *
window.devicePixelRatio;
}
const contentTop =
@ -458,9 +457,8 @@ add_task(async function test_createRegionWithKeyboardWithShift() {
} else {
const sidebar = document.querySelector("sidebar-main");
const sidebarWidth = sidebar.offsetWidth;
// Add 1 to account for #appcontent border
window100X =
(100 + window.mozInnerScreenX + sidebarWidth + 1) *
(100 + window.mozInnerScreenX + sidebarWidth) *
window.devicePixelRatio;
}
const contentTop =

View file

@ -976,6 +976,9 @@ export class SearchOneOffs {
}
if (!this.textbox.value) {
if (event.shiftKey) {
this.popup.openSearchForm(event, engine);
}
return;
}
// Select the clicked button so that consumers can easily tell which
@ -1014,13 +1017,14 @@ export class SearchOneOffs {
}
if (target.classList.contains("search-one-offs-context-open-in-new-tab")) {
if (!this.textbox.value) {
return;
}
// Select the context-clicked button so that consumers can easily
// tell which button was acted on.
this.selectedButton = target.closest("menupopup")._triggerButton;
this.handleSearchCommand(event, this.selectedButton.engine, true);
if (this.textbox.value) {
this.handleSearchCommand(event, this.selectedButton.engine, true);
} else {
this.popup.openSearchForm(event, this.selectedButton.engine, true);
}
}
const isPrivateButton = target.classList.contains(

View file

@ -69,11 +69,11 @@
if (!engine) {
return;
}
// At this point, the click must have happened on the header.
if (!this.searchbar.value) {
return;
if (this.searchbar.value) {
this.oneOffButtons.handleSearchCommand(event, engine);
} else if (event.shiftKey) {
this.openSearchForm(event, engine);
}
this.oneOffButtons.handleSearchCommand(event, engine);
});
this._bundle = null;
@ -262,6 +262,14 @@
this.searchbar.handleSearchCommandWhere(event, engine, where, params);
}
openSearchForm(event, engine, forceNewTab = false) {
let { where, params } = this.oneOffButtons._whereToOpen(
event,
forceNewTab
);
this.searchbar.openSearchFormWhere(event, engine, where, params);
}
/**
* Passes DOM events for the popup to the _on_<event type> methods.
*

View file

@ -306,52 +306,14 @@
}
handleSearchCommand(aEvent, aEngine, aForceNewTab) {
let where = "current";
let params;
const newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
if (
aEvent &&
aEvent.originalTarget.classList.contains("search-go-button")
aEvent.originalTarget.classList.contains("search-go-button") &&
aEvent.button == 2
) {
if (aEvent.button == 2) {
return;
}
where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true);
if (
newTabPref &&
!aEvent.altKey &&
!aEvent.getModifierState("AltGraph") &&
where == "current" &&
!gBrowser.selectedTab.isEmpty
) {
where = "tab";
}
} else if (aForceNewTab) {
where = "tab";
if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
where += "-background";
}
} else {
if (
(KeyboardEvent.isInstance(aEvent) &&
(aEvent.altKey || aEvent.getModifierState("AltGraph"))) ^
newTabPref &&
!gBrowser.selectedTab.isEmpty
) {
where = "tab";
}
if (
MouseEvent.isInstance(aEvent) &&
(aEvent.button == 1 || aEvent.getModifierState("Accel"))
) {
where = "tab";
params = {
inBackground: true,
};
}
return;
}
let { where, params } = this._whereToOpen(aEvent, aForceNewTab);
this.handleSearchCommandWhere(aEvent, aEngine, where, params);
}
@ -449,6 +411,97 @@
openTrustedLinkIn(submission.uri.spec, aWhere, params);
}
/**
* Returns information on where a search results page should be loaded: in the
* current tab or a new tab.
*
* @param {event} aEvent
* The event that triggered the page load.
* @param {boolean} [aForceNewTab]
* True to force the load in a new tab.
* @returns {object} An object { where, params }. `where` is a string:
* "current" or "tab". `params` is an object further describing how
* the page should be loaded.
*/
_whereToOpen(aEvent, aForceNewTab = false) {
let where = "current";
let params = {};
const newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
if (aEvent?.originalTarget.classList.contains("search-go-button")) {
where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true);
if (
newTabPref &&
!aEvent.altKey &&
!aEvent.getModifierState("AltGraph") &&
where == "current" &&
!gBrowser.selectedTab.isEmpty
) {
where = "tab";
}
} else if (aForceNewTab) {
where = "tab";
if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
params = {
inBackground: true,
};
}
} else {
if (
(KeyboardEvent.isInstance(aEvent) &&
(aEvent.altKey || aEvent.getModifierState("AltGraph"))) ^
newTabPref &&
!gBrowser.selectedTab.isEmpty
) {
where = "tab";
}
if (
MouseEvent.isInstance(aEvent) &&
(aEvent.button == 1 || aEvent.getModifierState("Accel"))
) {
where = "tab";
params = {
inBackground: true,
};
}
}
return { where, params };
}
/**
* Opens the search form of the provided engine or the current engine
* if no engine was provided.
*
* @param {event} aEvent
* The event causing the searchForm to be opened.
* @param {nsISearchEngine} [aEngine]
* The search engine or undefined to use the current engine.
* @param {string} where
* Where the search form should be opened.
* @param {object} [params]
* Parameters for URILoadingHelper.openLinkIn.
*/
openSearchFormWhere(aEvent, aEngine, where, params = {}) {
let engine = aEngine || this.currentEngine;
let searchForm = engine.wrappedJSObject.searchForm;
if (where === "tab" && !!params.inBackground) {
// Keep the focus in the search bar.
params.avoidBrowserFocus = true;
} else if (
where !== "window" &&
aEvent.keyCode === KeyEvent.DOM_VK_RETURN
) {
// Move the focus to the selected browser when keyup the Enter.
params.avoidBrowserFocus = true;
this._needBrowserFocusAtEnterKeyUp = true;
}
openTrustedLinkIn(searchForm, where, params);
}
disconnectedCallback() {
this.destroy();
while (this.firstChild) {
@ -807,6 +860,11 @@
)
)
) {
if (event.shiftKey) {
let engine = this.textbox.selectedButton?.engine;
let { where, params } = this._whereToOpen(event);
this.openSearchFormWhere(event, engine, where, params);
}
return true;
}
// Otherwise, "call super": do what the autocomplete binding's

View file

@ -4,6 +4,8 @@ ChromeUtils.defineESModuleGetters(this, {
"resource://testing-common/FormHistoryTestUtils.sys.mjs",
});
const SEARCH_FORM = "http://mochi.test:8888/";
function expectedURL(aSearchTerms) {
const ENGINE_HTML_BASE =
"http://mochi.test:8888/browser/browser/components/search/test/browser/test.html";
@ -40,7 +42,7 @@ function simulateClick(aEvent, aTarget) {
// modified from toolkit/components/satchel/test/test_form_autocomplete.html
function checkMenuEntries(expectedValues) {
let actualValues = getMenuEntries();
let actualValues = getMenuEntries().toSorted((a, b) => a.localeCompare(b));
is(
actualValues.length,
expectedValues.length,
@ -61,11 +63,20 @@ function getMenuEntries() {
var searchBar;
var searchButton;
var searchEntries = ["test"];
var searchEntries = [
"testAltReturn",
"testAltGrReturn",
"testLeftClick",
"testMiddleClick",
"testReturn",
"testShiftMiddleClick",
].sort((a, b) => a.localeCompare(b));
var preSelectedBrowser;
var preTabNo;
async function prepareTest() {
async function prepareTest(searchBarValue = "test") {
searchBar.value = searchBarValue;
searchBar.updateGoButtonVisibility();
preSelectedBrowser = gBrowser.selectedBrowser;
preTabNo = gBrowser.tabs.length;
@ -92,7 +103,6 @@ add_setup(async function () {
});
searchBar = BrowserSearch.searchBar;
searchBar.value = "test";
searchButton = searchBar.querySelector(".search-go-button");
registerCleanupFunction(() => {
@ -114,7 +124,7 @@ add_setup(async function () {
});
add_task(async function testReturn() {
await prepareTest();
await prepareTest("testReturn");
EventUtils.synthesizeKey("KEY_Enter");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
@ -126,8 +136,32 @@ add_task(async function testReturn() {
);
});
add_task(async function testReturnEmpty() {
await prepareTest("");
EventUtils.synthesizeKey("KEY_Enter");
await TestUtils.waitForTick();
is(
gBrowser.selectedBrowser.ownerDocument.activeElement,
searchBar.textbox,
"Focus stays in the searchbar"
);
});
add_task(async function testShiftReturn() {
await prepareTest("");
let promise = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
SEARCH_FORM
);
EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true });
await promise;
info("testShiftReturn opened the search form page.");
});
add_task(async function testAltReturn() {
await prepareTest();
await prepareTest("testAltReturn");
await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
});
@ -140,8 +174,19 @@ add_task(async function testAltReturn() {
);
});
add_task(async function testAltReturnEmpty() {
await prepareTest("");
EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
await TestUtils.waitForTick();
is(
gBrowser.selectedBrowser.ownerDocument.activeElement,
searchBar.textbox,
"Focus stays in the searchbar"
);
});
add_task(async function testAltGrReturn() {
await prepareTest();
await prepareTest("testAltGrReturn");
await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true });
});
@ -154,24 +199,37 @@ add_task(async function testAltGrReturn() {
);
});
// Shift key has no effect for now, so skip it
add_task(async function testShiftAltReturn() {
/*
yield* prepareTest();
add_task(async function testAltGrReturnEmpty() {
await prepareTest("");
let url = expectedURL(searchBar.value);
EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true });
await TestUtils.waitForTick();
let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url);
EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true, altKey: true });
yield newTabPromise;
is(
gBrowser.selectedBrowser.ownerDocument.activeElement,
searchBar.textbox,
"Focus stays in the searchbar"
);
});
add_task(async function testShiftAltReturnEmpty() {
await prepareTest("");
let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, SEARCH_FORM);
EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true, altKey: true });
await newTabPromise;
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
is(gBrowser.tabs.length, preTabNo + 1, "Shift+Alt+Return key added new tab");
is(gBrowser.currentURI.spec, url, "testShiftAltReturn opened correct search page");
*/
is(
gBrowser.currentURI.spec,
SEARCH_FORM,
"testShiftAltReturn opened the search form page"
);
});
add_task(async function testLeftClick() {
await prepareTest();
await prepareTest("testLeftClick");
simulateClick({ button: 0 }, searchButton);
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
is(gBrowser.tabs.length, preTabNo, "LeftClick did not open new tab");
@ -183,7 +241,7 @@ add_task(async function testLeftClick() {
});
add_task(async function testMiddleClick() {
await prepareTest();
await prepareTest("testMiddleClick");
await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
simulateClick({ button: 1 }, searchButton);
});
@ -196,7 +254,7 @@ add_task(async function testMiddleClick() {
});
add_task(async function testShiftMiddleClick() {
await prepareTest();
await prepareTest("testShiftMiddleClick");
let url = expectedURL(searchBar.value);

View file

@ -53,28 +53,9 @@ add_setup(async function () {
});
add_task(async function nonEmptySearch() {
searchBar.focus();
searchBar.value = SEARCH_WORD;
await openPopup(SEARCH_WORD);
let shownPromise = promiseEvent(searchPopup, "popupshown");
let builtPromise = promiseEvent(oneOffInstance, "rebuild");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {}, win);
await Promise.all([shownPromise, builtPromise]);
// Get the one-off button for the test engine.
let oneOffButton;
for (let node of oneOffButtons.children) {
if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
oneOffButton = node;
break;
}
}
Assert.notEqual(
oneOffButton,
undefined,
"One-off for test engine should exist"
);
let oneOffButton = findOneOff(TEST_ENGINE_NAME);
let promise = BrowserTestUtils.browserLoaded(
win.gBrowser.selectedBrowser,
@ -87,29 +68,8 @@ add_task(async function nonEmptySearch() {
});
add_task(async function emptySearch() {
searchBar.focus();
searchBar.value = "";
let shownPromise = promiseEvent(searchPopup, "popupshown");
let builtPromise = promiseEvent(oneOffInstance, "rebuild");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {}, win);
await Promise.all([shownPromise, builtPromise]);
// Get the one-off button for the test engine.
let oneOffButton;
for (let node of oneOffButtons.children) {
if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
oneOffButton = node;
break;
}
}
Assert.notEqual(
oneOffButton,
undefined,
"One-off for test engine should exist"
);
await openPopup("");
let oneOffButton = findOneOff(TEST_ENGINE_NAME);
EventUtils.synthesizeMouseAtCenter(oneOffButton, {}, win);
await TestUtils.waitForTick();
@ -119,3 +79,44 @@ add_task(async function emptySearch() {
"Focus stays in the searchbar"
);
});
add_task(async function emptySearchShift() {
await openPopup("");
let oneOffButton = findOneOff(TEST_ENGINE_NAME);
let promise = BrowserTestUtils.browserLoaded(
win.gBrowser.selectedBrowser,
false,
`http://mochi.test:8888/`
);
EventUtils.synthesizeMouseAtCenter(oneOffButton, { shiftKey: true }, win);
await promise;
info("Opened search form page");
});
function findOneOff(engineName) {
let oneOffChildren = [...oneOffButtons.children];
let oneOffButton = oneOffChildren.find(
node => node.engine?.name == engineName
);
Assert.notEqual(
oneOffButton,
undefined,
`One-off for ${engineName} should exist`
);
return oneOffButton;
}
async function openPopup(searchBarValue) {
searchBar.focus();
searchBar.value = searchBarValue;
if (searchBar.textbox.popupOpen) {
info("searchPanel is already open");
return;
}
let shownPromise = promiseEvent(searchPopup, "popupshown");
let builtPromise = promiseEvent(oneOffInstance, "rebuild");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {}, win);
await Promise.all([shownPromise, builtPromise]);
}

View file

@ -5,6 +5,9 @@ const TEST_ENGINE_BASENAME = "testEngine.xml";
let searchbar;
let searchIcon;
let searchPopup;
let oneOffInstance;
let oneOffButtons;
add_setup(async function () {
searchbar = await gCUITestUtils.addSearchBar();
@ -12,6 +15,9 @@ add_setup(async function () {
gCUITestUtils.removeSearchBar();
});
searchIcon = searchbar.querySelector(".searchbar-search-button");
searchPopup = document.getElementById("PopupSearchAutoComplete");
oneOffInstance = searchPopup.oneOffButtons;
oneOffButtons = oneOffInstance.buttons;
// Set default engine so no external requests are made.
await SearchTestUtils.installSearchExtension(
@ -27,37 +33,66 @@ add_setup(async function () {
});
});
add_task(async function telemetry() {
searchbar.focus();
searchbar.value = "abc";
add_task(async function testNewtabEmpty() {
await openPopup("abc");
let oneOffButton = findOneOff(TEST_ENGINE_NAME);
let searchPopup = document.getElementById("PopupSearchAutoComplete");
let oneOffInstance = searchPopup.oneOffButtons;
let promise = BrowserTestUtils.waitForNewTab(gBrowser);
await activateContextMenuItem(
oneOffButton,
".search-one-offs-context-open-in-new-tab"
);
let tab = await promise;
let oneOffButtons = oneOffInstance.buttons;
// By default the search will open in the background and the popup will stay open
await closePopup();
// Open the popup.
let shownPromise = promiseEvent(searchPopup, "popupshown");
let builtPromise = promiseEvent(oneOffInstance, "rebuild");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {});
await Promise.all([shownPromise, builtPromise]);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
"http://mochi.test:8888/browser/browser/components/search/test/browser/?search&test=abc",
"Expected search tab should have loaded"
);
// Get the one-off button for the test engine.
let oneOffButton;
for (let node of oneOffButtons.children) {
if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
oneOffButton = node;
break;
}
}
BrowserTestUtils.removeTab(tab);
});
add_task(async function testNewtabNonempty() {
await openPopup("");
let oneOffButton = findOneOff(TEST_ENGINE_NAME);
let promise = BrowserTestUtils.waitForNewTab(gBrowser);
await activateContextMenuItem(
oneOffButton,
".search-one-offs-context-open-in-new-tab"
);
let tab = await promise;
// By default the search form will open in the background and the popup will stay open
await closePopup();
Assert.equal(
tab.linkedBrowser.currentURI.spec,
"http://mochi.test:8888/",
"Search form should have loaded in new tab"
);
BrowserTestUtils.removeTab(tab);
});
function findOneOff(engineName) {
let oneOffChildren = [...oneOffButtons.children];
let oneOffButton = oneOffChildren.find(
node => node.engine?.name == engineName
);
Assert.notEqual(
oneOffButton,
undefined,
"One-off for test engine should exist"
`One-off for ${engineName} should exist`
);
return oneOffButton;
}
// Open the context menu on the one-off.
async function activateContextMenuItem(oneOffButton, itemID) {
let contextMenu = oneOffInstance.querySelector(
".search-one-offs-context-menu"
);
@ -68,34 +103,27 @@ add_task(async function telemetry() {
});
await promise;
// Click the Search in New Tab menu item.
let searchInNewTabMenuItem = contextMenu.querySelector(
".search-one-offs-context-open-in-new-tab"
);
promise = BrowserTestUtils.waitForNewTab(gBrowser);
contextMenu.activateItem(searchInNewTabMenuItem);
let tab = await promise;
let menuItem = contextMenu.querySelector(itemID);
contextMenu.activateItem(menuItem);
}
// By default the search will open in the background and the popup will stay open:
promise = promiseEvent(searchPopup, "popuphidden");
async function openPopup(searchBarValue) {
searchbar.focus();
searchbar.value = searchBarValue;
if (searchbar.textbox.popupOpen) {
info("searchPanel is already open");
return;
}
let shownPromise = promiseEvent(searchPopup, "popupshown");
let builtPromise = promiseEvent(oneOffInstance, "rebuild");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {});
await Promise.all([shownPromise, builtPromise]);
}
async function closePopup() {
let promise = promiseEvent(searchPopup, "popuphidden");
info("Closing search panel");
EventUtils.synthesizeKey("KEY_Escape");
await promise;
// Check the loaded tab.
Assert.equal(
tab.linkedBrowser.currentURI.spec,
"http://mochi.test:8888/browser/browser/components/search/test/browser/?search&test=abc",
"Expected search tab should have loaded"
);
BrowserTestUtils.removeTab(tab);
// Move the cursor out of the panel area to avoid messing with other tests.
await EventUtils.promiseNativeMouseEvent({
type: "mousemove",
target: searchbar,
offsetX: 0,
offsetY: 0,
});
});
}

View file

@ -37,17 +37,11 @@ add_setup(async function () {
});
add_task(async function nonEmptySearch() {
searchBar.focus();
searchBar.value = SEARCH_WORD;
let promise = promiseEvent(searchPopup, "popupshown");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {}, win);
await promise;
await openPopup(SEARCH_WORD);
let engineNameBox = searchPopup.querySelector(".searchbar-engine-name");
promise = BrowserTestUtils.browserLoaded(
let promise = BrowserTestUtils.browserLoaded(
win.gBrowser.selectedBrowser,
false,
`http://mochi.test:8888/browser/browser/components/search/test/browser/?search&test=${SEARCH_WORD}`
@ -58,22 +52,44 @@ add_task(async function nonEmptySearch() {
});
add_task(async function emptySearch() {
searchBar.focus();
searchBar.value = "";
let promise = promiseEvent(searchPopup, "popupshown");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {}, win);
await promise;
await openPopup("");
let engineNameBox = searchPopup.querySelector(".searchbar-engine-name");
EventUtils.synthesizeMouseAtCenter(engineNameBox, {}, win);
await TestUtils.waitForTick();
Assert.equal(
win.gBrowser.selectedBrowser.ownerDocument.activeElement,
searchBar.textbox,
"Focus stays in the searchbar"
);
});
add_task(async function emptySearchShift() {
await openPopup("");
let engineNameBox = searchPopup.querySelector(".searchbar-engine-name");
let promise = BrowserTestUtils.browserLoaded(
win.gBrowser.selectedBrowser,
false,
"http://mochi.test:8888/"
);
EventUtils.synthesizeMouseAtCenter(engineNameBox, { shiftKey: true }, win);
await promise;
info("Opening search form successful");
});
async function openPopup(searchBarValue) {
searchBar.focus();
searchBar.value = searchBarValue;
if (searchBar.textbox.popupOpen) {
info("searchPanel is already open");
return;
}
let shownPromise = promiseEvent(searchPopup, "popupshown");
let builtPromise = promiseEvent(searchPopup.oneOffButtons, "rebuild");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchIcon, {}, win);
await Promise.all([shownPromise, builtPromise]);
}

View file

@ -184,10 +184,11 @@ add_task(async function openSettingsWithEnter() {
const searchButton = searchBar.querySelector(".searchbar-search-button");
let shownPromise = promiseEvent(searchPopup, "popupshown");
info("Clicking icon");
let builtPromise = promiseEvent(searchPopup.oneOffButtons, "rebuild");
info("Opening search panel");
EventUtils.synthesizeMouseAtCenter(searchButton, {}, win);
await shownPromise;
info("Popup shown");
await Promise.all([shownPromise, builtPromise]);
info("Search panel ready");
EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);

View file

@ -0,0 +1,52 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PrefUtils: "resource://normandy/lib/PrefUtils.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(lazy, "sidebarNimbus", "sidebar.nimbus");
export const SidebarManager = {
/**
* Handle startup tasks like telemetry, adding listeners.
*/
init() {
// Handle nimbus feature pref setting updates on init and enrollment
const featureId = "sidebar";
lazy.NimbusFeatures[featureId].onUpdate(() => {
// Set prefs only if we have an enrollment that's new
const feature = { featureId };
const enrollment =
lazy.ExperimentAPI.getExperimentMetaData(feature) ??
lazy.ExperimentAPI.getRolloutMetaData(feature);
if (!enrollment) {
return;
}
const slug = enrollment.slug + ":" + enrollment.branch.slug;
if (slug == lazy.sidebarNimbus) {
return;
}
// Set/override user prefs to persist after experiment end
const setPref = (pref, value) => {
// Only set prefs with a value (so no clearing)
if (value != null) {
lazy.PrefUtils.setPref("sidebar." + pref, value);
}
};
setPref("nimbus", slug);
["main.tools", "revamp", "verticalTabs"].forEach(pref =>
setPref(pref, lazy.NimbusFeatures[featureId].getVariable(pref))
);
});
},
};
// Initialize on first import
SidebarManager.init();

View file

@ -262,6 +262,9 @@ var SidebarController = {
},
async init() {
// Initialize with side effects
this.SidebarManager;
this._box = document.getElementById("sidebar-box");
this._splitter = document.getElementById("sidebar-splitter");
this._reversePositionButton = document.getElementById(
@ -528,14 +531,14 @@ var SidebarController = {
let sidebarContainer = document.getElementById("sidebar-main");
let sidebarMain = document.querySelector("sidebar-main");
if (!this._positionStart) {
// DOM ordering is: sidebar-main | sidebar-box | splitter | appcontent |
// Want to display as: | appcontent | splitter | sidebar-box | sidebar-main
// So we just swap box and appcontent ordering and move sidebar-main to the end
let appcontent = document.getElementById("appcontent");
// DOM ordering is: sidebar-main | sidebar-box | splitter | tabbrowser-tabbox |
// Want to display as: | tabbrowser-tabbox | splitter | sidebar-box | sidebar-main
// So we just swap box and tabbrowser-tabbox ordering and move sidebar-main to the end
let tabbox = document.getElementById("tabbrowser-tabbox");
let boxOrdinal = this._box.style.order;
this._box.style.order = appcontent.style.order;
this._box.style.order = tabbox.style.order;
appcontent.style.order = boxOrdinal;
tabbox.style.order = boxOrdinal;
// the launcher should be on the right of the sidebar-box
sidebarContainer.style.order = parseInt(this._box.style.order) + 1;
// Indicate we've switched ordering to the box
@ -1550,6 +1553,10 @@ var SidebarController = {
},
};
ChromeUtils.defineESModuleGetters(SidebarController, {
SidebarManager: "resource:///modules/SidebarManager.sys.mjs",
});
// Add getters related to the position here, since we will want them
// available for both startDelayedLoad and init.
XPCOMUtils.defineLazyPreferenceGetter(

View file

@ -7,3 +7,7 @@
JAR_MANIFESTS += ["jar.mn"]
BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
EXTRA_JS_MODULES += [
"SidebarManager.sys.mjs",
]

View file

@ -9,13 +9,6 @@
--sidebar-text-color: -moz-sidebartext;
--sidebar-border-color: -moz-sidebarborder;
/* stylelint-disable-next-line media-query-no-invalid */
@media ((-moz-bool-pref: "browser.theme.macos.native-theme") and (-moz-platform: macOS) and (not (prefers-contrast)) and (prefers-color-scheme: light)) {
&:not([lwtheme]) {
--toolbar-non-lwt-bgcolor: white;
}
}
background-color: transparent;
height: 100%;
}

View file

@ -26,6 +26,8 @@ run-if = ["os == 'mac'"] # Mac only feature
["browser_sidebar_max_width.js"]
["browser_sidebar_nimbus.js"]
["browser_sidebar_panel_header.js"]
["browser_sidebar_panel_switcher.js"]

View file

@ -0,0 +1,166 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
const { ExperimentFakes } = ChromeUtils.importESModule(
"resource://testing-common/NimbusTestUtils.sys.mjs"
);
/**
* Check that enrolling into sidebar experiments sets user prefs
*/
add_task(async function test_nimbus_user_prefs() {
const main = "sidebar.main.tools";
const nimbus = "sidebar.nimbus";
const vertical = "sidebar.verticalTabs";
Assert.ok(!Services.prefs.prefHasUserValue(main), "No user main pref yet");
Assert.ok(
!Services.prefs.prefHasUserValue(nimbus),
"No user nimbus pref yet"
);
let cleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: "sidebar",
value: {
"main.tools": "bar",
},
});
Assert.equal(
Services.prefs.getStringPref(main),
"bar",
"Set user pref with experiment"
);
Assert.ok(Services.prefs.prefHasUserValue(main), "main pref has user value");
const nimbusValue = Services.prefs.getStringPref(nimbus);
Assert.ok(nimbusValue, "Set some nimbus slug");
Assert.ok(
Services.prefs.prefHasUserValue(nimbus),
"nimbus pref has user value"
);
cleanup();
Assert.equal(
Services.prefs.getStringPref(main),
"bar",
"main pref still set"
);
Assert.equal(
Services.prefs.getStringPref(nimbus),
nimbusValue,
"nimbus pref still set"
);
Assert.ok(!Services.prefs.getBoolPref(vertical), "vertical is default value");
Assert.ok(
!Services.prefs.prefHasUserValue(vertical),
"vertical used default value"
);
cleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: "sidebar",
value: {
"main.tools": "aichat,syncedtabs,history",
verticalTabs: true,
},
});
Assert.ok(!Services.prefs.prefHasUserValue(main), "main pref no longer set");
Assert.notEqual(
Services.prefs.getStringPref(nimbus),
nimbusValue,
"nimbus pref changed"
);
Assert.ok(Services.prefs.getBoolPref(vertical), "vertical set to true");
Assert.ok(
Services.prefs.prefHasUserValue(vertical),
"vertical pref has user value"
);
cleanup();
Services.prefs.clearUserPref(nimbus);
Services.prefs.clearUserPref(vertical);
});
/**
* Check that rollout sets prefs then prefer experiment
*/
add_task(async function test_nimbus_rollout_experiment() {
const revamp = "sidebar.revamp";
const nimbus = "sidebar.nimbus";
await SpecialPowers.pushPrefEnv({ clear: [[revamp]] });
const cleanRollout = await ExperimentFakes.enrollWithFeatureConfig(
{
featureId: "sidebar",
value: { revamp: true },
},
{ isRollout: true }
);
Assert.ok(Services.prefs.getBoolPref(revamp), "Set user pref with rollout");
const nimbusValue = Services.prefs.getStringPref(nimbus);
Assert.ok(nimbusValue, "Set some nimbus slug");
const cleanExperiment = await ExperimentFakes.enrollWithFeatureConfig({
featureId: "sidebar",
value: { revamp: false },
});
Assert.ok(
!Services.prefs.getBoolPref(revamp),
"revamp pref flipped by experiment"
);
Assert.notEqual(
Services.prefs.getStringPref(nimbus),
nimbusValue,
"nimbus pref changed by experiment"
);
cleanRollout();
cleanExperiment();
Services.prefs.clearUserPref(nimbus);
});
/**
* Check that multi-feature sidebar and chatbot sets prefs
*/
add_task(async function test_nimbus_multi_feature() {
const chatbot = "browser.ml.chat.enabled";
const sidebar = "sidebar.main.tools";
Assert.ok(!Services.prefs.prefHasUserValue(chatbot), "chatbot is default");
Assert.ok(!Services.prefs.prefHasUserValue(sidebar), "sidebar is default");
const cleanup = await ExperimentFakes.enrollmentHelper(
ExperimentFakes.recipe("foo", {
branches: [
{
slug: "variant",
features: [
{
featureId: "sidebar",
value: { "main.tools": "syncedtabs,history" },
},
{
featureId: "chatbot",
value: { prefs: { enabled: { value: true } } },
},
],
},
],
})
);
Assert.ok(Services.prefs.prefHasUserValue(chatbot), "chatbot user pref set");
Assert.ok(Services.prefs.prefHasUserValue(sidebar), "sidebar user pref set");
cleanup();
Assert.ok(Services.prefs.prefHasUserValue(chatbot), "chatbot pref still set");
Assert.ok(Services.prefs.prefHasUserValue(sidebar), "sidebar pref still set");
Services.prefs.clearUserPref(chatbot);
Services.prefs.clearUserPref(sidebar);
Services.prefs.clearUserPref("browser.ml.chat.nimbus");
Services.prefs.clearUserPref("sidebar.nimbus");
});

View file

@ -3,11 +3,10 @@ Dynamic Result Types
This document discusses a special category of address bar results called dynamic
result types. Dynamic result types allow you to easily add new types of results
to the address bar and are especially useful for extensions.
to the address bar.
The intended audience for this document is developers who need to add new kinds
of address bar results, either internally in the address bar codebase or through
extensions.
of address bar results.
.. contents::
:depth: 2
@ -34,14 +33,6 @@ so that your card is drawn correctly; you may need to update the keyboard
selection behavior if your card contains elements that can be independently
selected such as different days of the week; and so on.
If you're implementing your weather card in an extension, as you might in an
add-on experiment, then you'd need to land your new result type in
mozilla-central so your extension can use it. Your new result type would ship
with Firefox even though the vast majority of users would never see it, and your
fellow address bar hackers would have to work around your code even though it
would remain inactive most of the time, at least until your experiment
graduated.
Dynamic Result Types
--------------------
@ -49,52 +40,27 @@ Dynamic Result Types
types. Instead of adding a new built-in type along with all that entails, you
add a new provider subclass and register a template that describes how the view
should draw your result type and indicates which elements are selectable. The
address bar takes care of everything else. (Or if you're implementing an
extension, you add a few event handlers instead of a provider subclass, although
we have a shim_ that abstracts away the differences between internal and
extension address bar code.)
address bar takes care of everything else.
Dynamic result types are essentially an abstraction layer: Support for them as a
general category of results is built into the address bar, and each
implementation of a specific dynamic result type fills in the details.
In addition, dynamic result types can be added at runtime. This is important for
extensions that implement new types of results like the weather forecast example
above.
.. _shim: https://github.com/0c0w3/dynamic-result-type-extension/blob/master/src/shim.js
In addition, dynamic result types can be added at runtime.
Getting Started
---------------
To get a feel for how dynamic result types are implemented, you can look at the
`example dynamic result type extension <exampleExtension_>`__. The extension
uses the recommended shim_ that makes writing address bar extension code very
similar to writing internal address bar code, and it's therefore a useful
example even if you intend to add a new dynamic result type internally in the
address bar codebase in mozilla-central.
:searchfox:`UrlbarProviderCalculator <browser/components/urlbar/UrlbarProviderCalculator.sys.mjs>`.
The next section describes the specific steps you need to take to add a new
dynamic result type.
.. _exampleExtension: https://github.com/0c0w3/dynamic-result-type-extension/blob/master/src/background.js
Implementation Steps
--------------------
This section describes how to add a new dynamic result type in either of the
following cases:
* You want to add a new dynamic result type in an extension using the
recommended shim_.
* You want to add a new dynamic result type internal to the address bar codebase
in mozilla-central.
The steps are mostly the same in both cases and are described next.
If you want to add a new dynamic result type in an extension but don't want to
use the shim, then skip ahead to `Appendix B: Using the WebExtensions API
Directly`_.
This section describes how to add a new dynamic result type.
1. Register the dynamic result type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -123,9 +89,7 @@ Next, add the view template for the new type:
``viewTemplate`` is an object called a view template. It describes in a
declarative manner the DOM that should be created in the view for all results of
the new type. For providers created in extensions, it also declares the
stylesheet that should be applied to results in the view. See `View Templates`_
for a description of this object.
the new type.
3. Add the provider
~~~~~~~~~~~~~~~~~~~
@ -161,9 +125,6 @@ For help on implementing providers in general, see the address bar's
If you are creating the provider in the internal address bar implementation in
mozilla-central, then don't forget to register it in ``UrlbarProvidersManager``.
If you are creating the provider in an extension, then it's registered
automatically, and there's nothing else you need to do.
__ https://firefox-source-docs.mozilla.org/browser/urlbar/overview.html#urlbarprovider
4. Implement the provider's getViewUpdate method
@ -220,11 +181,6 @@ mozilla-central, then add styling `urlbar-dynamic-results.css`_.
.. _urlbar-dynamic-results.css: https://searchfox.org/mozilla-central/source/browser/themes/shared/urlbar-dynamic-results.css
If you are creating the provider in an extension, then bundle a CSS file in your
extension and declare it in the top-level ``stylesheet`` property of your view
template, as described in `View Templates`_. Additionally, if any of your rules
override built-in rules, then you'll need to declare them as ``!important``.
The rest of this section will discuss the CSS rules you need to use to style
your results.
@ -276,11 +232,6 @@ build the DOM for a dynamic result type. When a result of a particular dynamic
result type is shown in the view, the type's view template is used to construct
the part of the view that represents the type in general.
The need for view templates arises from the fact that extensions run in a
separate process from the chrome process and can't directly access the chrome
DOM, where the address bar view lives. Since extensions are a primary use case
for dynamic result types, this is an important constraint on their design.
Properties
~~~~~~~~~~
@ -333,18 +284,6 @@ structure may include the following properties:
An optional list of classes. Each class will be added to the element created
for the object by calling ``element.classList.add()``.
``{string} [stylesheet]``
For dynamic result types created in extensions, this property should be set on
the root object in the view template structure, and its value should be a
stylesheet URL. The stylesheet will be loaded in all browser windows so that
the dynamic result type view may be styled. The specified URL will be resolved
against the extension's base URI. We recommend specifying a URL relative to
your extension's base directory.
For dynamic result types created internally in the address bar codebase, this
value should not be specified and instead styling should be added to
`urlbar-dynamic-results.css`_.
Example
~~~~~~~
@ -648,9 +587,6 @@ payload property in the following example:
}
);
``UrlbarUtils.HIGHLIGHT`` is defined in the extensions shim_ and is described
below.
Your view template must create an element corresponding to the payload
property. That is, it must include an object where the value of the ``name``
property is the name of the payload property, like this:
@ -693,16 +629,7 @@ Appendix A: Examples
This section lists some example and real-world consumers of dynamic result
types.
`Example Extension`__
This extension demonstrates a simple use of dynamic result types.
`Weather Quick Suggest Extension`__
A real-world Firefox extension experiment that shows weather forecasts and
alerts when the user performs relevant searches in the address bar.
`Tab-to-Search Provider`__
This is a built-in provider in mozilla-central that uses dynamic result types.
__ https://github.com/0c0w3/dynamic-result-type-extension
__ https://github.com/mozilla-extensions/firefox-quick-suggest-weather/blob/master/src/background.js
__ https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs

View file

@ -84,6 +84,7 @@ add_task(async function test_save_edited_fields() {
async function (browser) {
info(`Test ${TEST.description}`);
info(`Wait for save doorhanger shown`);
const onSavePopupShown = waitForPopupShown();
await focusUpdateSubmitForm(browser, {
focusSelector: "#given-name",
@ -91,10 +92,12 @@ add_task(async function test_save_edited_fields() {
});
await onSavePopupShown;
info(`Wait for edit doorhanger shown`);
const onEditPopupShown = waitForPopupShown();
await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON);
await onEditPopupShown;
info(`Fill edit doorhanger`);
fillEditDoorhanger(TEST.editedFields);
await clickAddressDoorhangerButton(MAIN_BUTTON);
}

View file

@ -8,6 +8,7 @@ support-files = [
"../fixtures/autocomplete_simple_basic.html",
"../fixtures/page_navigation.html",
"./empty.html",
"../fixtures/**",
]
["browser_autocomplete_footer.js"]
@ -25,6 +26,10 @@ skip-if = [
["browser_autofill_address_select_inexact.js"]
["browser_autofill_creditCard_name.js"]
["browser_autofill_creditCard_type.js"]
["browser_autofill_duplicate_fields.js"]
["browser_autofill_sandboxed_iframe.js"]
@ -46,6 +51,22 @@ skip-if = [
["browser_fathom_cc.js"]
["browser_form_changes.js"]
["browser_iframe_autofill_cc_number.js"]
["browser_iframe_autofill_sandbox.js"]
["browser_iframe_capture.js"]
["browser_iframe_cross_origin_autofill.js"]
["browser_iframe_cross_origin_field_detection.js"]
["browser_iframe_layout_telemetry.js"]
["browser_iframe_same_origin_field_detection.js"]
["browser_manageAddressesDialog.js"]
["browser_page_navigation_in_subtree.js"]

View file

@ -6,6 +6,7 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_PROFILE_US = {
email: "address_us@mozilla.org",
organization: "Mozilla",
"address-level1": "AZ",
country: "US",
};
@ -30,11 +31,21 @@ const MARKUP_SELECT_COUNTRY = `
`;
// Strip any attributes that could help identify select as country field
const MARKUP_SELECT_COUNTRY_WITHOUT_AUTOCOMPLETE =
MARKUP_SELECT_COUNTRY.replace(/<select[^>]*>/, "<select>");
const MARKUP_SELECT_COUNTRY_WITHOUT_AUTOCOMPLETE = `
<html><body>
<input id="email">
<input id="organization">
<select id="country">
<option value="">Select a country</option>
<option value="Canada">Canada</option>
<option value="United States">United States</option>
</select>
</body></html>
`;
add_autofill_heuristic_tests([
{
description: "Test autofill select with US profile",
fixtureData: MARKUP_SELECT_COUNTRY,
profile: TEST_PROFILE_US,
expectedResult: [
@ -51,6 +62,7 @@ add_autofill_heuristic_tests([
],
},
{
description: "Test autofill select with CA profile",
fixtureData: MARKUP_SELECT_COUNTRY,
profile: TEST_PROFILE_CA,
expectedResult: [
@ -67,21 +79,49 @@ add_autofill_heuristic_tests([
],
},
{
description: "Test autofill <select> without autocomplete attribute",
fixtureData: MARKUP_SELECT_COUNTRY_WITHOUT_AUTOCOMPLETE,
profile: TEST_PROFILE_CA,
expectedResult: [
{
default: {
reason: "regex-heuristic",
},
fields: [
{ fieldName: "email", autofill: TEST_PROFILE_CA.email },
{ fieldName: "organization", autofill: TEST_PROFILE_CA.organization },
{ fieldName: "country", autofill: "Canada" },
],
},
],
},
{
description:
"Address form without matching options in select for address-level1 and country",
fixtureData: `<form>
<input id="email" autocomplete="email">
<select id="address-level1" autocomplete="address-level1">
<option id=default value=""></option>
<option id="option-address-level1-dummy1" value="Dummy">Dummy</option>
<option id="option-address-level1-dummy2" value="Dummy 2">Dummy 2</option>
</select>
<select id="country" autocomplete="country">
<option id=default value=""></option>
<option id="option-country-dummy1" value="Dummy">Dummy</option>
<option id="option-country-dummy2" value="Dummy 2">Dummy 2</option>
</select>
</form>`,
profile: TEST_PROFILE_US,
expectedResult: [
{
default: {
reason: "autocomplete",
},
fields: [
{ fieldName: "email", autofill: TEST_PROFILE_CA.email },
{ fieldName: "organization", autofill: TEST_PROFILE_CA.organization },
{
fieldName: "country",
autofill: "Canada",
reason: "regex-heuristic",
},
{ fieldName: "email", autofill: TEST_PROFILE_US.email },
{ fieldName: "address-level1", autofill: "" },
{ fieldName: "country", autofill: "" },
],
},
],

View file

@ -8,13 +8,14 @@ const TEST_PROFILE = {
"cc-number": "4111111111111111",
// "cc-type" should be remove from proile after fixing Bug 1834768.
"cc-type": "visa",
"cc-exp-month": 4,
"cc-exp-month": "04",
"cc-exp-year": new Date().getFullYear(),
};
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["extensions.formautofill.loglevel", "Debug"],
["extensions.formautofill.creditCards.supported", "on"],
["extensions.formautofill.creditCards.enabled", true],
],
@ -85,6 +86,7 @@ add_autofill_heuristic_tests([
<input id="name" placeholder="given-name">
</form>`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
@ -96,7 +98,7 @@ add_autofill_heuristic_tests([
{
fieldName: "cc-exp",
reason: "autocomplete",
autofill: TEST_PROFILE["cc-exp"],
autofill: `${TEST_PROFILE["cc-exp-month"]}/${TEST_PROFILE["cc-exp-year"]}`,
},
{
fieldName: "cc-name",
@ -184,6 +186,7 @@ add_autofill_heuristic_tests([
<input id="country" placeholder="country">
</form>`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [

View file

@ -0,0 +1,189 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const MOCK_STORAGE = [
{
name: "John Doe",
organization: "Sesame Street",
"address-level2": "Austin",
tel: "+13453453456",
},
{
name: "Foo Bar",
organization: "Mozilla",
"address-level2": "San Francisco",
tel: "+16509030800",
},
];
function makeAddressComment({ primary, secondary, status }) {
return JSON.stringify({
primary,
secondary,
status,
ariaLabel: primary + " " + secondary + " " + status,
});
}
async function removeInputField(browser, selector) {
await SpecialPowers.spawn(browser, [{ selector }], async args => {
content.document.querySelector(args.selector).remove();
});
}
async function addInputField(browser, formId, className) {
await SpecialPowers.spawn(browser, [{ formId, className }], async args => {
const newElem = content.document.createElement("input");
newElem.name = args.className;
newElem.autocomplete = args.className;
newElem.type = "text";
const form = content.document.querySelector(`#${args.formId}`);
form.appendChild(newElem);
});
}
async function checkFieldsAutofilled(browser, formId, profile) {
await SpecialPowers.spawn(browser, [{ formId, profile }], async args => {
const elements = content.document.querySelectorAll(`#${args.formId} input`);
for (const element of elements) {
await ContentTaskUtils.waitForCondition(() => {
return element.value == args.profile[element.name];
});
await ContentTaskUtils.waitForCondition(
() => element.matches(":autofill"),
`Checking #${element.id} highlight style`
);
}
});
}
// Compare the comment on the autocomplete menu items to the expected comment.
// The profile field is not compared.
async function checkMenuEntries(
browser,
expectedValues,
extraRows = 1,
{ checkComment = false } = {}
) {
const expectedLength = expectedValues.length + extraRows;
let actualValues;
await BrowserTestUtils.waitForCondition(() => {
actualValues = browser.autoCompletePopup.view.results;
return actualValues.length == expectedLength;
});
is(actualValues.length, expectedLength, " Checking length of expected menu");
for (let i = 0; i < expectedValues.length; i++) {
if (checkComment) {
const expectedValue = JSON.parse(expectedValues[i]);
const actualValue = JSON.parse(actualValues[i].comment);
for (const [key, value] of Object.entries(expectedValue)) {
is(
actualValue[key],
value,
`Checking menu entry #${i}, ${key} should be the same`
);
}
} else {
is(actualValues[i].label, expectedValues[i], "Checking menu entry #" + i);
}
}
}
const TEST_PAGE = `
<form id="form1">
<p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
<p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
<p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
</form>
<div id="form2">
<p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
<p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
<p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
</div>`;
async function checkFormChangeHappened(formId) {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: `https://example.com/document-builder.sjs?html=${encodeURIComponent(
TEST_PAGE
)}`,
},
async browser => {
await openPopupOn(browser, `#${formId} input[name=tel]`);
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await checkMenuEntries(
browser,
MOCK_STORAGE.map(address =>
makeAddressComment({
primary: address.tel,
secondary: address.name,
status: "Also autofills name, organization",
})
),
2,
{ checkComment: true }
);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
// Click the first entry of the autocomplete popup and make sure all fields are autofilled
await checkFieldsAutofilled(browser, formId, MOCK_STORAGE[0]);
// This is for checking the changes of element count.
addInputField(browser, formId, "address-level2");
await openPopupOn(browser, `#${formId} input[name=name]`);
// Click on an autofilled field would show an autocomplete popup with "clear form" entry
await checkMenuEntries(
browser,
[
"Clear Autofill Form", // Clear Autofill Form
"Manage addresses", // FormAutofill Preferemce
],
0
);
// This is for checking the changes of element removed and added then.
removeInputField(browser, `#${formId} input[name=address-level2]`);
addInputField(browser, formId, "address-level2");
await openPopupOn(browser, `#${formId} input[name=address-level2]`);
await checkMenuEntries(
browser,
MOCK_STORAGE.map(address =>
makeAddressComment({
primary: address["address-level2"],
secondary: address.name,
status: "Also autofills name, organization, phone",
})
),
2,
{ checkComment: true }
);
// Make sure everything is autofilled in the end
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
await checkFieldsAutofilled(browser, formId, MOCK_STORAGE[0]);
}
);
}
add_setup(async function () {
await setStorage(MOCK_STORAGE[0]);
await setStorage(MOCK_STORAGE[1]);
});
add_task(async function check_change_happened_in_form() {
await checkFormChangeHappened("form1");
});
add_task(async function check_change_happened_in_body() {
await checkFormChangeHappened("form2");
});

View file

@ -0,0 +1,285 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global add_autofill_heuristic_tests */
const TEST_PROFILE = {
"cc-name": "John Doe",
"cc-number": "4111111111111111",
// "cc-type" should be remove from proile after fixing Bug 1834768.
"cc-type": "visa",
"cc-exp-month": "04",
"cc-exp-year": new Date().getFullYear(),
};
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["extensions.formautofill.creditCards.supported", "on"],
["extensions.formautofill.creditCards.enabled", true],
],
});
});
add_autofill_heuristic_tests([
/**
* Test credit card number in the main-frame
*/
{
description:
"Fill cc-number in a main-frame when the autofill is triggered in a first-party-origin iframe",
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Do not fill cc-number in a main-frame when the autofill is triggered in a third-party-origin iframe",
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
/**
* Test credit card number in a first-party-origin iframe
*/
{
description:
"Fill cc-number in a first-party-origin iframe when the autofill is triggered in another first-party-origin iframe",
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Do not fill cc-number in a first-party-origin iframe when autofill is triggered in a third-party iframe",
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Fill cc-number in a first-party-origin iframe when the autofill is triggered in the main-frame",
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
/**
* Test credit card number is in a third-party iframe
*/
{
description:
"Do not fill cc-number in a third-party-origin iframe when the autofill is triggered in a first-party-origin iframe",
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Do not fill cc-number in a third-party-origin iframe when the autofill is triggered in cross-origin third-party-origin iframe",
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_2_CC_NAME}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Do not fill cc-number in a third-party-origin iframe when the autofill is triggered in the main-iframe",
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Fill cc-number in a third-party-origin iframe when the autofill is triggered in a same-origin third-party-origin iframe",
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
/**
* Test cases when relaxed restriction is applied
*/
{
description:
"Do not fill cc-number in a main-frame when the autofill is triggered in a third-party-origin iframe even when the relaxed restriction is applied",
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Do not fill cc-number in a first-party-origin iframe when autofill is triggered in a third-party iframe even when the relaxed restriction is applied",
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Do not fill cc-number in a third-party-origin iframe when the autofill is triggered in cross-origin third-party-origin iframe even when the relaxed restriction is applied",
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_2_CC_NAME}\"></iframe>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
{
description:
"Do not fill cc-number in a third-party-origin iframe when the autofill is triggered in the main-iframe even when the relaxed restriction is applied",
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
],
},
],
},
]);

View file

@ -0,0 +1,164 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global add_autofill_heuristic_tests */
const TEST_PROFILE = {
"cc-name": "John Doe",
"cc-number": "4111111111111111",
// "cc-type" should be remove from proile after fixing Bug 1834768.
"cc-type": "visa",
"cc-exp-month": "04",
"cc-exp-year": new Date().getFullYear(),
};
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["ui.popup.disable_autohide", true],
["extensions.formautofill.creditCards.supported", "on"],
["extensions.formautofill.creditCards.enabled", true],
],
});
});
add_autofill_heuristic_tests([
{
description:
"Trigger autofill in the main-frame, do not autofill into sandboxed iframe",
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe sandbox src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe sandbox src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: "" },
{ fieldName: "cc-exp", autofill: "" },
],
},
],
},
{
description:
"Trigger autofill in a first-party-origin iframe, do not autofill into sandboxed iframe",
fixtureData: `
<iframe sandbox src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<iframe sandbox src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
<iframe sandbox src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: "" },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
{
description:
"Trigger autofill in a sandboxed first-party-origin iframe, do not autofill into iframe",
fixtureData: `
<iframe sandbox src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: "" },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
{
description:
"Trigger autofill in a third-party-origin iframe, do not autofill into sandboxed other iframes",
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<iframe sandbox src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
<iframe sandbox src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: "" },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
{
description:
"Trigger autofill in a sandboxed third-party-origin iframe, do not autofill into other iframes",
fixtureData: `
<iframe sandbox src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: "" },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
{
description:
"Relaxed restriction applied - Trigger autofill in a sandboxed third-party-origin iframe, do not autofill into other iframes",
fixtureData: `
<iframe sandbox src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
{
fieldName: "cc-exp",
autofill: `${TEST_PROFILE["cc-exp-month"]}/${TEST_PROFILE["cc-exp-year"]}`,
},
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
]);

View file

@ -0,0 +1,252 @@
/* global add_capture_heuristic_tests */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["extensions.formautofill.creditCards.supported", "on"],
["extensions.formautofill.creditCards.enabled", true],
["extensions.formautofill.addresses.capture.requiredFields", ""],
["extensions.formautofill.loglevel", "Debug"],
],
});
});
const CAPTURE_FILL_VALUE = {
"#cc-number": "4111111111111111",
"#cc-name": "John Doe",
"#cc-exp": `04/${new Date().getFullYear()}`,
};
const CAPTURE_EXPECTED_RECORD = {
"cc-number": "************1111",
"cc-name": "John Doe",
"cc-exp-month": 4,
"cc-exp-year": new Date().getFullYear(),
"cc-type": "visa",
};
const CAPTURE_FILL_VALUE_1 = {
"#form1 #cc-number": "378282246310005",
"#form1 #cc-name": "Timothy Berners-Lee",
"#form1 #cc-exp": `07/${new Date().getFullYear() - 1}`,
};
const CAPTURE_EXPECTED_RECORD_1 = {
"cc-number": "***********0005",
"cc-name": "Timothy Berners-Lee",
"cc-exp-month": 7,
"cc-exp-year": new Date().getFullYear() - 1,
"cc-type": "amex",
};
const CAPTURE_FILL_VALUE_2 = {
"#form2 #cc-number": "5555555555554444",
"#form2 #cc-name": "Jane Doe",
"#form2 #cc-exp": `12/${new Date().getFullYear() + 1}`,
};
const CAPTURE_EXPECTED_RECORD_2 = {
"cc-number": "************4444",
"cc-name": "Jane Doe",
"cc-exp-month": 12,
"cc-exp-year": new Date().getFullYear() + 1,
"cc-type": "mastercard",
};
add_capture_heuristic_tests([
{
description: `All fields are in the same same-origin iframe`,
fixtureData: `
<form id="form1">
<iframe src=${SAME_ORIGIN_ALL_FIELDS}></iframe>
<input id="submit" type="submit">
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
captureFillValue: CAPTURE_FILL_VALUE,
captureExpectedRecord: CAPTURE_EXPECTED_RECORD,
},
{
description: `All fields are in the same cross-origin iframe`,
fixtureData: `
<form id="form1">
<iframe src=${CROSS_ORIGIN_ALL_FIELDS}></iframe>
<input id="submit" type="submit">
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
captureFillValue: CAPTURE_FILL_VALUE,
captureExpectedRecord: CAPTURE_EXPECTED_RECORD,
},
{
description:
"One main-frame, one same-origin iframe and one cross-origin iframe",
fixtureData: `
<form id="form1">
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<input id="submit" type="submit">
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
captureFillValue: CAPTURE_FILL_VALUE,
captureExpectedRecord: CAPTURE_EXPECTED_RECORD,
},
{
description: `Every field is in its own same-origin iframe`,
fixtureData: `
<form id="form1">
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
<input id="submit" type="submit">
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
captureFillValue: CAPTURE_FILL_VALUE,
captureExpectedRecord: CAPTURE_EXPECTED_RECORD,
},
{
description: `Every field is in its own corss-origin iframe`,
fixtureData: `
<form id="form1">
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<input id="submit" type="submit">
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
captureFillValue: CAPTURE_FILL_VALUE,
captureExpectedRecord: CAPTURE_EXPECTED_RECORD,
},
{
description: `Two forms, submit the cross-origin form`,
fixtureData: `
<form>
<iframe src=${SAME_ORIGIN_ALL_FIELDS}?formId=form1></iframe>
</form>
<form>
<iframe src=${CROSS_ORIGIN_ALL_FIELDS}?formId=form2></iframe>
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
submitButtonSelector: "#form2 input[type=submit]",
captureFillValue: { ...CAPTURE_FILL_VALUE_1, ...CAPTURE_FILL_VALUE_2 },
captureExpectedRecord: CAPTURE_EXPECTED_RECORD_2,
},
{
description: `Two forms, submit the same-origin form`,
fixtureData: `
<form>
<iframe src=${SAME_ORIGIN_ALL_FIELDS}?formId=form1></iframe>
</form>
<form>
<iframe src=${CROSS_ORIGIN_ALL_FIELDS}?formId=form2></iframe>
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
submitButtonSelector: "#form1 input[type=submit]",
captureFillValue: { ...CAPTURE_FILL_VALUE_1, ...CAPTURE_FILL_VALUE_2 },
captureExpectedRecord: CAPTURE_EXPECTED_RECORD_1,
},
{
description:
"One main-frame, one same-origin sandbox iframe and one cross-origin sandbox iframe",
fixtureData: `
<form id="form1">
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\" sandbox></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\" sandbox></iframe>
<input id="submit" type="submit">
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
captureFillValue: CAPTURE_FILL_VALUE,
captureExpectedRecord: CAPTURE_EXPECTED_RECORD,
},
]);

View file

@ -0,0 +1,204 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global add_heuristic_tests */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_PROFILE = {
"cc-name": "John Doe",
"cc-number": "4111111111111111",
// "cc-type" should be remove from proile after fixing Bug 1834768.
"cc-type": "visa",
"cc-exp-month": "04",
"cc-exp-year": new Date().getFullYear(),
};
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["extensions.formautofill.creditCards.supported", "on"],
["extensions.formautofill.creditCards.enabled", true],
],
});
});
add_autofill_heuristic_tests([
{
description: `Trigger autofill in the main-frame`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<p><label>Card Type: <select id="cc-type" autocomplete="cc-type">
<option></option>
<option value="discover">Discover</option>
<option value="jcb">JCB</option>
<option value="visa">Visa</option>
<option value="mastercard">MasterCard</option>
<option value="gringotts">Unknown card network</option>
</select></label></p>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "visa" },
],
},
],
},
{
description: `Trigger autofill in a fist-party-origin iframe`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_TYPE}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-name",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "visa" },
],
},
],
},
{
description: `Trigger autofill in a third-party-origin iframe`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-exp",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: "" },
{
fieldName: "cc-exp",
autofill: `${TEST_PROFILE["cc-exp-month"]}/${TEST_PROFILE["cc-exp-year"]}`,
},
{ fieldName: "cc-type", autofill: "visa" },
],
},
],
},
{
description: `Trigger autofill in a third-party-origin iframe, cc-type is in another third-party-origin iframe`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_2_CC_TYPE}\"></iframe>
`,
profile: TEST_PROFILE,
autofillTrigger: "#cc-exp",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: "" },
{
fieldName: "cc-exp",
autofill: `${TEST_PROFILE["cc-exp-month"]}/${TEST_PROFILE["cc-exp-year"]}`,
},
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
{
description: `Relaxed autofill restriction - trigger autofill in a third-party-origin iframe`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-exp",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: "" },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
{
fieldName: "cc-exp",
autofill: `${TEST_PROFILE["cc-exp-month"]}/${TEST_PROFILE["cc-exp-year"]}`,
},
{ fieldName: "cc-type", autofill: "visa" },
],
},
],
},
{
description: `Relaxed autofill restriction - Do not apply autofill to same-site iframes when autofill is triggered in a main frame`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${SAME_SITE_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
{
description: `Relaxed autofill restriction - Do not apply autofill to same-site iframes when autofill is triggered in a same-origin iframe`,
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${SAME_SITE_CC_EXP}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
prefs: [
["extensions.formautofill.heuristics.autofillSameOriginWithTop", true],
],
profile: TEST_PROFILE,
autofillTrigger: "#cc-number",
expectedResult: [
{
fields: [
{ fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
{ fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
{ fieldName: "cc-exp", autofill: "" },
{ fieldName: "cc-type", autofill: "" },
],
},
],
},
]);

View file

@ -0,0 +1,184 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global add_heuristic_tests */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["extensions.formautofill.creditCards.supported", "on"],
["extensions.formautofill.creditCards.enabled", true],
],
});
});
add_heuristic_tests([
{
description: `All fields are in the same cross-origin iframe`,
fixtureData: `<iframe src=${CROSS_ORIGIN_ALL_FIELDS}></iframe>`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Mix cross-origin and same-origin iframe`,
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Mix main-frame and cross-origin iframe`,
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Mix main-frame, same-origin iframe, and cross-origin iframe`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Every fields is in its own cross-origin iframe`,
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `One same-origin iframe and one cross-origin iframe`,
fixtureData: `
<iframe src=${SAME_ORIGIN_ALL_FIELDS}></iframe>
<iframe src=${CROSS_ORIGIN_ALL_FIELDS}></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Mutliple cross-origin iframes`,
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_2_CC_EXP}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Two cross-origin iframes, one of the iframe is sandboxed`,
fixtureData: `
<iframe src=${CROSS_ORIGIN_ALL_FIELDS}></iframe>
<iframe src=${CROSS_ORIGIN_ALL_FIELDS} sandbox></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Every field is in its own sandboxed cross-origin iframe`,
fixtureData: `
<iframe src=\"${CROSS_ORIGIN_CC_NUMBER}\" sandbox></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_NAME}\" sandbox></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\" sandbox></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
]);

View file

@ -0,0 +1,206 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function assertGleanTelemetry(events, expected_number_of_flowid = 1) {
let flow_ids = new Set();
events.forEach(({ event_name, expected_extra, event_count = 1 }) => {
const actual_events = Glean.formautofill[event_name].testGetValue() ?? [];
Assert.equal(
actual_events.length,
event_count,
`Expected to have ${event_count} event/s with the name "${event_name}"`
);
expected_extra = Array.isArray(expected_extra)
? expected_extra
: [expected_extra];
for (let idx = 0; idx < actual_events.length; idx++) {
const actual = actual_events[idx];
const expected = expected_extra[idx];
if (expected) {
flow_ids.add(actual.extra.flow_id);
delete actual.extra.flow_id; // We don't want to test the specific flow_id value yet
Assert.deepEqual(actual.extra, expected);
}
}
});
Assert.equal(
flow_ids.size,
expected_number_of_flowid,
`All events from the same user interaction session have the same flow id`
);
}
add_setup(async function () {
Services.telemetry.setEventRecordingEnabled("creditcard", true);
registerCleanupFunction(async function () {
Services.telemetry.setEventRecordingEnabled("creditcard", false);
});
await clearGleanTelemetry();
});
add_heuristic_tests([
{
description: `All fields are in the main frame`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<p><label>Card Expiry: <input id="cc-exp" autocomplete="cc-exp"></label></p>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
async onTestComplete() {
assertGleanTelemetry([
{
event_name: "iframeLayoutDetection",
expected_extra: {
category: "creditcard",
iframe_count: 0,
main_frame: "cc-number,cc-name,cc-exp",
iframe: "",
cross_origin: "",
sandboxed: "",
},
},
]);
await clearGleanTelemetry();
},
},
{
description: `All fields are in the same same-origin iframe`,
fixtureData: `<iframe src=${SAME_ORIGIN_ALL_FIELDS}></iframe>`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
async onTestComplete() {
assertGleanTelemetry([
{
event_name: "iframeLayoutDetection",
expected_extra: {
category: "creditcard",
iframe_count: 1,
main_frame: "",
iframe: "cc-number,cc-name,cc-exp",
cross_origin: "",
sandboxed: "",
},
},
]);
await clearGleanTelemetry();
},
},
{
description: `Mix main-frame, same-origin iframe, and cross-origin iframe`,
fixtureData: `
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${CROSS_ORIGIN_CC_EXP}\"></iframe>
<iframe sandbox src=\"${CROSS_ORIGIN_CC_TYPE}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
{ fieldName: "cc-type" },
],
},
],
async onTestComplete() {
assertGleanTelemetry([
{
event_name: "iframeLayoutDetection",
expected_extra: {
category: "creditcard",
iframe_count: 3,
main_frame: "cc-number",
iframe: "cc-name,cc-exp,cc-type",
cross_origin: "cc-exp,cc-type",
sandboxed: "cc-type",
},
},
]);
await clearGleanTelemetry();
},
},
{
description: "Test category",
fixtureData: `
<form>
<input id="name" autocomplete="name" />
<input id="country" autocomplete="country"/>
<input id="street-address" autocomplete="street-address" />
</form>
<form>
<input id="cc-name" autocomplete="cc-name" />
<input id="cc-number" autocomplete="cc-number"/>
<input id="cc-exp" autocomplete="cc-exp" />
</form>
`,
expectedResult: [
{
fields: [
{ fieldName: "name" },
{ fieldName: "country" },
{ fieldName: "street-address" },
],
},
{
fields: [
{ fieldName: "cc-name" },
{ fieldName: "cc-number" },
{ fieldName: "cc-exp" },
],
},
],
async onTestComplete() {
assertGleanTelemetry(
[
{
event_name: "iframeLayoutDetection",
event_count: 2,
expected_extra: [
{
category: "address",
iframe_count: 0,
main_frame: "name,country,street-address",
iframe: "",
cross_origin: "",
sandboxed: "",
},
{
category: "creditcard",
iframe_count: 0,
main_frame: "cc-name,cc-number,cc-exp",
iframe: "",
cross_origin: "",
sandboxed: "",
},
],
},
],
2
);
await clearGleanTelemetry();
},
},
]);

View file

@ -0,0 +1,181 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global add_heuristic_tests */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["extensions.formautofill.creditCards.supported", "on"],
["extensions.formautofill.creditCards.enabled", true],
],
});
});
add_heuristic_tests([
{
description: `All fields are in the same same-origin iframe`,
fixtureData: `<iframe src=${SAME_ORIGIN_ALL_FIELDS}></iframe>`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `cc-name in the main frame, others in its own iframes`,
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<iframe src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `cc-name & cc-exp in the main frame, cc-number in an iframe`,
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<p><label>Card Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<p><label>Card Expiration Date: <input id="cc-exp" autocomplete="cc-exp"></label></p>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Every field is in its own same-origin iframe`,
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\"></iframe>
<iframe src=\"${SAME_ORIGIN_CC_EXP}\"></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Two same-origin iframes`,
fixtureData: `
<iframe src=${SAME_ORIGIN_ALL_FIELDS}></iframe>
<iframe src=${SAME_ORIGIN_ALL_FIELDS}></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Two same-origin iframes, one of the iframe is sandboxed`,
fixtureData: `
<iframe src=${SAME_ORIGIN_ALL_FIELDS}></iframe>
<iframe src=${SAME_ORIGIN_ALL_FIELDS} sandbox></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Every field is in its own sandboxed same-origin iframe`,
fixtureData: `
<iframe src=\"${SAME_ORIGIN_CC_NUMBER}\" sandbox></iframe>
<iframe src=\"${SAME_ORIGIN_CC_NAME}\" sandbox></iframe>
<iframe src=\"${SAME_ORIGIN_CC_EXP}\" sandbox></iframe>
`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
],
},
{
description: `Every field is in its own sandboxed same-origin iframe`,
fixtureData: `
<iframe src="https://example.com/document-builder.sjs?html=
${encodeURIComponent(`
<input id="cc-number" autocomplete="cc-number">
<input id="cc-name" autocomplete="cc-name">
<input id="cc-exp" autocomplete="cc-exp">
<input id="cc-number" autocomplete="cc-number">
<input id="cc-name" autocomplete="cc-name">
<input id="cc-exp-month" autocomplete="cc-exp-month">
<input id="cc-exp-year" autocomplete="cc-exp-year">
`)}"></iframe>`,
expectedResult: [
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp" },
],
},
{
fields: [
{ fieldName: "cc-number" },
{ fieldName: "cc-name" },
{ fieldName: "cc-exp-month" },
{ fieldName: "cc-exp-year" },
],
},
],
},
]);

View file

@ -18,6 +18,7 @@ const ADDRESS_VALUES = {
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["extensions.formautofill.loglevel", "Debug"],
["extensions.formautofill.addresses.capture.enabled", true],
["extensions.formautofill.addresses.supported", "on"],
["extensions.formautofill.heuristics.captureOnPageNavigation", true],

View file

@ -96,12 +96,6 @@ skip-if = [
["browser_creditCard_heuristics.js"]
skip-if = ["apple_silicon && !debug"] # Bug 1714221
["browser_creditCard_heuristics_autofill_name.js"]
skip-if = ["apple_silicon && !debug"] # Bug 1714221
["browser_creditCard_heuristics_cc_type.js"]
skip-if = ["apple_silicon && !debug"] # Bug 1714221
["browser_creditCard_osAuth.js"]
skip-if = ["os == 'linux'"]

View file

@ -46,7 +46,7 @@ add_task(async function test_active_delay() {
// gets opened and listen for it in this test before we check if the item
// is disabled.
await SpecialPowers.pushPrefEnv({
set: [["security.notification_enable_delay", 1000]],
set: [["security.notification_enable_delay", 2000]],
});
await disableOSAuthForThisTest();
@ -86,17 +86,19 @@ add_task(async function test_active_delay() {
info(`Popup was disabled for ${delta} ms`);
Assert.greaterOrEqual(
delta,
1000,
"Popup was disabled for at least 1000 ms"
2000,
"Popup was disabled for at least 2000 ms"
);
// Check the clicking on the menu works now
const promise = TestUtils.topicObserved("formautofill-autofill-complete");
firstItem.click();
is(
browser.autoCompletePopup.selectedIndex,
0,
"First item selected after clicking on enabled item"
);
await promise;
// Clean up
await closePopup(browser);
@ -106,7 +108,7 @@ add_task(async function test_active_delay() {
add_task(async function test_no_delay() {
await SpecialPowers.pushPrefEnv({
set: [["security.notification_enable_delay", 1000]],
set: [["security.notification_enable_delay", 2000]],
});
await BrowserTestUtils.withNewTab(
{ gBrowser, url: ADDRESS_URL },

View file

@ -24,6 +24,11 @@ const { FormAutofillNameUtils } = ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs"
);
const { VALID_ADDRESS_FIELDS, VALID_CREDIT_CARD_FIELDS } =
ChromeUtils.importESModule(
"resource://autofill/FormAutofillStorageBase.sys.mjs"
);
const { FormAutofillUtils } = ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
);
@ -57,6 +62,9 @@ const PRIVACY_PREF_URL = "about:preferences#privacy";
const HTTP_TEST_PATH = "/browser/browser/extensions/formautofill/test/browser/";
const BASE_URL = "http://mochi.test:8888" + HTTP_TEST_PATH;
const BASE_URL_HTTPS = "https://mochi.test" + HTTP_TEST_PATH;
const CROSS_ORIGIN_BASE_URL = "https://example.org" + HTTP_TEST_PATH;
const CROSS_ORIGIN_2_BASE_URL = "https://example.com" + HTTP_TEST_PATH;
const FORM_URL = BASE_URL + "autocomplete_basic.html";
const ADDRESS_FORM_URL =
"https://example.org" +
@ -94,6 +102,12 @@ const CREDITCARD_FORM_WITH_PAGE_NAVIGATION_BUTTONS =
"creditCard/capture_creditCard_on_page_navigation.html";
const EMPTY_URL = "https://example.org" + HTTP_TEST_PATH + "empty.html";
const TOP_LEVEL_HOST = "https://example.com";
const TOP_LEVEL_URL = TOP_LEVEL_HOST + HTTP_TEST_PATH;
const SAME_SITE_URL = "https://test1.example.com" + HTTP_TEST_PATH;
const CROSS_ORIGIN_URL = "https://example.net" + HTTP_TEST_PATH;
const CROSS_ORIGIN_2_URL = "https://example.org" + HTTP_TEST_PATH;
const ENABLED_AUTOFILL_ADDRESSES_PREF =
"extensions.formautofill.addresses.enabled";
const ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF =
@ -113,6 +127,54 @@ const SYNC_CREDITCARDS_PREF = "services.sync.engine.creditcards";
const SYNC_CREDITCARDS_AVAILABLE_PREF =
"services.sync.engine.creditcards.available";
// For iframe autofill tests
const SAME_ORIGIN_ALL_FIELDS =
TOP_LEVEL_URL + "../fixtures/autocomplete_cc_mandatory_embeded.html";
const SAME_ORIGIN_CC_NUMBER =
TOP_LEVEL_URL + "../fixtures/autocomplete_cc_number_embeded.html";
const SAME_ORIGIN_CC_NAME =
TOP_LEVEL_URL + "../fixtures/autocomplete_cc_name_embeded.html";
const SAME_ORIGIN_CC_EXP =
TOP_LEVEL_URL + "../fixtures/autocomplete_cc_exp_embeded.html";
const SAME_ORIGIN_CC_TYPE =
TOP_LEVEL_URL + "../fixtures/autocomplete_cc_type_embeded.html";
const SAME_SITE_ALL_FIELDS =
SAME_SITE_URL + "../fixtures/autocomplete_cc_mandatory_embeded.html";
const SAME_SITE_CC_NUMBER =
SAME_SITE_URL + "../fixtures/autocomplete_cc_number_embeded.html";
const SAME_SITE_CC_NAME =
SAME_SITE_URL + "../fixtures/autocomplete_cc_name_embeded.html";
const SAME_SITE_CC_EXP =
SAME_SITE_URL + "../fixtures/autocomplete_cc_exp_embeded.html";
const SAME_SITE_CC_TYPE =
SAME_SITE_URL + "../fixtures/autocomplete_cc_type_embeded.html";
const CROSS_ORIGIN_ALL_FIELDS =
CROSS_ORIGIN_URL + "../fixtures/autocomplete_cc_mandatory_embeded.html";
const CROSS_ORIGIN_CC_NUMBER =
CROSS_ORIGIN_URL + "../fixtures/autocomplete_cc_number_embeded.html";
const CROSS_ORIGIN_CC_NAME =
CROSS_ORIGIN_URL + "../fixtures/autocomplete_cc_name_embeded.html";
const CROSS_ORIGIN_CC_EXP =
CROSS_ORIGIN_URL + "../fixtures/autocomplete_cc_exp_embeded.html";
const CROSS_ORIGIN_CC_TYPE =
CROSS_ORIGIN_URL + "../fixtures/autocomplete_cc_type_embeded.html";
const CROSS_ORIGIN_2_ALL_FIELDS =
CROSS_ORIGIN_2_URL +
"../fixtures/" +
"autocomplete_cc_mandatory_embeded.html";
const CROSS_ORIGIN_2_CC_NUMBER =
CROSS_ORIGIN_2_URL + "../fixtures/autocomplete_cc_number_embeded.html";
const CROSS_ORIGIN_2_CC_NAME =
CROSS_ORIGIN_2_URL + "../fixtures/autocomplete_cc_name_embeded.html";
const CROSS_ORIGIN_2_CC_EXP =
CROSS_ORIGIN_2_URL + "../fixtures/autocomplete_cc_exp_embeded.html";
const CROSS_ORIGIN_2_CC_TYPE =
CROSS_ORIGIN_2_URL + "../fixtures/autocomplete_cc_type_embeded.html";
// Test profiles
const TEST_ADDRESS_1 = {
"given-name": "John",
"additional-name": "R.",
@ -410,6 +472,8 @@ async function focusUpdateSubmitForm(target, args, submit = true) {
} else {
form = content.document.getElementById(obj.formId ?? "form");
}
form ||= content.document;
let element = form.querySelector(obj.focusSelector);
if (element != content.document.activeElement) {
info(`focus on element (id=${element.id})`);
@ -455,6 +519,7 @@ async function focusUpdateSubmitForm(target, args, submit = true) {
} else {
form = content.document.getElementById(obj.formId ?? "form");
}
form ||= content.document;
info(`submit form (id=${form.id})`);
form.querySelector("input[type=submit]").click();
});
@ -524,7 +589,7 @@ async function focusAndWaitForFieldsIdentified(browserOrContext, selector) {
if (previouslyIdentified) {
info("previouslyIdentified");
FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver);
return;
return previouslyFocused;
}
// Wait 500ms to ensure that "markAsAutofillField" is completely finished.
@ -541,6 +606,8 @@ async function focusAndWaitForFieldsIdentified(browserOrContext, selector) {
content.document.activeElement
).setAttribute("test-formautofill-identified", "true");
});
return previouslyFocused;
}
/**
@ -589,34 +656,37 @@ async function waitForAutoCompletePopupOpen(browser, taskFunction) {
}
async function openPopupOn(browser, selector) {
const popupOpenPromise = waitForAutoCompletePopupOpen(browser);
await SimpleTest.promiseFocus(browser);
await runAndWaitForAutocompletePopupOpen(browser, async () => {
await focusAndWaitForFieldsIdentified(browser, selector);
if (!selector.includes("cc-")) {
const previouslyFocused = await focusAndWaitForFieldsIdentified(
browser,
selector
);
// If the field is already focused, we need to send a key event to
// open the popup
if (previouslyFocused || !selector.includes("cc-")) {
info(`openPopupOn: before VK_DOWN on ${selector}`);
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
}
});
await popupOpenPromise;
}
async function openPopupOnSubframe(browser, frameBrowsingContext, selector) {
const popupOpenPromise = waitForAutoCompletePopupOpen(browser);
await SimpleTest.promiseFocus(browser);
await runAndWaitForAutocompletePopupOpen(browser, async () => {
await focusAndWaitForFieldsIdentified(frameBrowsingContext, selector);
if (!selector.includes("cc-")) {
const previouslyFocused = await focusAndWaitForFieldsIdentified(
frameBrowsingContext,
selector
);
// If the field is already focused, we need to send a key event to
// open the popup
if (previouslyFocused || !selector.includes("cc-")) {
info(`openPopupOnSubframe: before VK_DOWN on ${selector}`);
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, frameBrowsingContext);
}
});
await popupOpenPromise;
}
async function closePopup(browser) {
@ -638,6 +708,11 @@ async function closePopup(browser) {
}
async function closePopupForSubframe(browser, frameBrowsingContext) {
// Return if the popup isn't open.
if (!browser.autoCompletePopup.popupOpen) {
return;
}
const popupClosePromise = BrowserTestUtils.waitForPopupEvent(
browser.autoCompletePopup,
"hidden"
@ -893,6 +968,228 @@ function verifySectionAutofillResult(section, result, expectedSection) {
});
}
function getSelectorFromFieldDetail(fieldDetail) {
// identifier is set with `${element.id}/${element.name}`;
return `#${fieldDetail.identifier.split("/")[0]}`;
}
/**
* Discards all recorded Glean telemetry in parent and child processes
* and resets FOG and the Glean SDK.
*
* @param {boolean} onlyInParent Whether we only discard the metric data in the parent process
*
* Since the current method Services.fog.testResetFOG only discards metrics recorded in the parent process,
* we would like to keep this option in our method as well.
*/
async function clearGleanTelemetry(onlyInParent = false) {
if (!onlyInParent) {
await Services.fog.testFlushAllChildren();
}
Services.fog.testResetFOG();
}
function fillEditDoorhanger(record) {
const notification = getNotification();
for (const [key, value] of Object.entries(record)) {
const id = AddressEditDoorhanger.getInputId(key);
const element = notification.querySelector(`#${id}`);
element.value = value;
}
}
// TODO: This function should be removed. We should make normalizeFields in
// FormAutofillStorageBase.sys.mjs static and using it directly
function normalizeAddressFields(record) {
let normalized = { ...record };
if (normalized.name != undefined) {
let nameParts = FormAutofillNameUtils.splitName(normalized.name);
normalized["given-name"] = nameParts.given;
normalized["additional-name"] = nameParts.middle;
normalized["family-name"] = nameParts.family;
delete normalized.name;
}
return normalized;
}
async function verifyConfirmationHint(
browser,
forceClose,
anchorID = "identity-icon-box"
) {
let hintElem = browser.ownerGlobal.ConfirmationHint._panel;
let popupshown = BrowserTestUtils.waitForPopupEvent(hintElem, "shown");
let popuphidden;
if (!forceClose) {
popuphidden = BrowserTestUtils.waitForPopupEvent(hintElem, "hidden");
}
await popupshown;
try {
Assert.equal(hintElem.state, "open", "hint popup is open");
Assert.ok(
BrowserTestUtils.isVisible(hintElem.anchorNode),
"hint anchorNode is visible"
);
Assert.equal(
hintElem.anchorNode.id,
anchorID,
"Hint should be anchored on the expected notification icon"
);
info("verifyConfirmationHint, hint is shown and has its anchorNode");
if (forceClose) {
await closePopup(hintElem);
} else {
info("verifyConfirmationHint, assertion ok, wait for poopuphidden");
await popuphidden;
info("verifyConfirmationHint, hintElem popup is hidden");
}
} catch (ex) {
Assert.ok(false, "Confirmation hint not shown: " + ex.message);
} finally {
info("verifyConfirmationHint promise finalized");
}
}
async function showAddressDoorhanger(browser, values = null) {
const defaultValues = {
"#given-name": "John",
"#family-name": "Doe",
"#organization": "Mozilla",
"#street-address": "123 Sesame Street",
};
const onPopupShown = waitForPopupShown();
const promise = BrowserTestUtils.browserLoaded(browser);
await focusUpdateSubmitForm(browser, {
focusSelector: "#given-name",
newValues: values ?? defaultValues,
});
await promise;
await onPopupShown;
}
async function findContext(browser, selector) {
const contexts =
browser.browsingContext.top.getAllBrowsingContextsInSubtree();
for (const context of contexts) {
const find = await SpecialPowers.spawn(
context,
[selector],
async selector => !!content.document.querySelector(selector)
);
if (find) {
return context;
}
}
return null;
}
async function verifyCaptureRecord(guid, expectedRecord) {
let fields;
let record = (await getAddresses()).find(addr => addr.guid == guid);
if (record) {
fields = VALID_ADDRESS_FIELDS;
} else {
record = (await getCreditCards()).find(cc => cc.guid == guid);
if (record) {
fields = VALID_CREDIT_CARD_FIELDS;
} else {
Assert.ok(false, "Cannot find record by guid");
}
}
for (const field of fields) {
Assert.equal(record[field], expectedRecord[field], `${field} is the same`);
}
}
async function verifyPreviewResult(browser, section, expectedSection) {
info(`Verify preview result`);
const fieldDetails = section.fieldDetails;
const expectedFieldDetails = expectedSection.fields;
for (let i = 0; i < fieldDetails.length; i++) {
const selector = getSelectorFromFieldDetail(fieldDetails[i]);
const context = await findContext(browser, selector);
let expected = expectedFieldDetails[i].autofill ?? "";
if (fieldDetails[i].fieldName == "cc-number" && expected.length) {
expected = "•".repeat(expected.length - 4) + expected.slice(-4);
}
await SpecialPowers.spawn(context, [{ expected, selector }], async obj => {
const element = content.document.querySelector(obj.selector);
if (content.HTMLSelectElement.isInstance(element)) {
if (obj.expected) {
for (let idx = 0; idx < element.options.length; idx++) {
if (element.options[idx].value == obj.expected) {
obj.expected = element.options[idx].text;
break;
}
}
} else {
obj.expected = "";
}
}
Assert.equal(
element.previewValue,
obj.expected,
`element ${obj.selector} previewValue is the same ${element.previewValue}`
);
});
}
}
async function verifyAutofillResult(browser, section, expectedSection) {
info(`Verify autofill result`);
const fieldDetails = section.fieldDetails;
const expectedFieldDetails = expectedSection.fields;
for (let i = 0; i < fieldDetails.length; i++) {
const selector = getSelectorFromFieldDetail(fieldDetails[i]);
const context = await findContext(browser, selector);
const expected = expectedFieldDetails[i].autofill ?? "";
await SpecialPowers.spawn(context, [{ expected, selector }], async obj => {
const element = content.document.querySelector(obj.selector);
if (content.HTMLSelectElement.isInstance(element)) {
if (!obj.expected) {
obj.expected = element.options[0].value;
}
}
Assert.equal(
element.value,
obj.expected,
`element ${obj.selector} value is the same ${element.value}`
);
});
}
}
async function verifyClearResult(browser, section) {
info(`Verify clear form result`);
const fieldDetails = section.fieldDetails;
for (let i = 0; i < fieldDetails.length; i++) {
const selector = getSelectorFromFieldDetail(fieldDetails[i]);
const context = await findContext(browser, selector);
const expected = "";
await SpecialPowers.spawn(context, [{ expected, selector }], async obj => {
const element = content.document.querySelector(obj.selector);
if (content.HTMLSelectElement.isInstance(element)) {
obj.expected = element.options[0].value;
}
Assert.equal(
element.value,
obj.expected,
`element ${obj.selector} value is the same ${element.value}`
);
});
}
}
function verifySectionFieldDetails(sections, expectedSectionsInfo) {
sections.forEach((section, index) => {
const expectedSection = expectedSectionsInfo[index];
@ -956,35 +1253,149 @@ function verifySectionFieldDetails(sections, expectedSectionsInfo) {
});
}
/**
* Discards all recorded Glean telemetry in parent and child processes
* and resets FOG and the Glean SDK.
*
* @param {boolean} onlyInParent Whether we only discard the metric data in the parent process
*
* Since the current method Services.fog.testResetFOG only discards metrics recorded in the parent process,
* we would like to keep this option in our method as well.
*/
async function clearGleanTelemetry(onlyInParent = false) {
if (!onlyInParent) {
await Services.fog.testFlushAllChildren();
async function triggerAutofillAndPreview(
browser,
selector,
previewCallback,
autofillCallback,
clearCallback
) {
const focusedContext = await findContext(browser, selector);
if (focusedContext == focusedContext.top) {
info(`Open the popup`);
await openPopupOn(browser, selector);
} else {
info(`Open the popup on subframe`);
await openPopupOnSubframe(browser, focusedContext, selector);
}
Services.fog.testResetFOG();
// Preview
info(`Send key down to trigger preview`);
let promise = TestUtils.topicObserved("formautofill-preview-complete");
const firstItem = getDisplayedPopupItems(browser)[0];
if (!firstItem.selected) {
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, focusedContext);
}
await promise;
await previewCallback();
// Autofill
info(`Send key return to trigger autofill`);
promise = TestUtils.topicObserved("formautofill-autofill-complete");
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, focusedContext);
await promise;
await autofillCallback();
// Clear Form
if (focusedContext == focusedContext.top) {
info(`Open the popup again for clearing form`);
await openPopupOn(browser, selector);
} else {
info(`Open the popup on subframe again for clearing form`);
await openPopupOnSubframe(browser, focusedContext, selector);
}
info(`Send key down and return to clear form`);
promise = TestUtils.topicObserved("formautofill-clear-form-complete");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, focusedContext);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, focusedContext);
await promise;
await clearCallback();
}
async function triggerCapture(browser, submitButtonSelector, fillSelectors) {
for (const [selector, value] of Object.entries(fillSelectors)) {
const context = await findContext(browser, selector);
await SpecialPowers.spawn(context, [{ selector, value }], obj => {
const element = content.document.querySelector(obj.selector);
if (content.HTMLInputElement.isInstance(element)) {
element.setUserInput(obj.value);
} else if (
content.HTMLSelectElement.isInstance(element) &&
Array.isArray(obj.value)
) {
element.multiple = true;
[...element.options].forEach(option => {
option.selected = obj.value.includes(option.value);
});
} else {
element.value = obj.value;
}
});
}
const onAdded = waitForStorageChangedEvents("add");
const onPopupShown = waitForPopupShown();
submitButtonSelector ||= "input[type=submit]";
const context = await findContext(browser, submitButtonSelector);
await SpecialPowers.spawn(context, [submitButtonSelector], selector => {
content.document.querySelector(selector).click();
});
await onPopupShown;
await clickDoorhangerButton(MAIN_BUTTON);
const [subject] = (await onAdded)[0];
return subject.wrappedJSObject.guid;
}
/**
* Runs heuristics test for form autofill on given patterns.
*
* @param {Array<object>} patterns - An array of test patterns to run the heuristics test on.
* @param {string} pattern.description - Description of this heuristic test
* @param {string} pattern.fixurePath - The path of the test document
* @param {string} pattern.fixureData - Test document by string. Use either fixurePath or fixtureData.
* @param {object} pattern.profile - The profile to autofill. This is required only when running autofill test
* @param {Array} pattern.expectedResult - The expected result of this heuristic test. See below for detailed explanation
* @param {Array<object>} patterns
* An array of test patterns to run the heuristics test on.
* @param {string} patterns.description
* Description of this heuristic test
* @param {string} patterns.fixurePath
* The path of the test document
* @param {string} patterns.fixureData
* Test document by string. Use either fixurePath or fixtureData.
* @param {Array} patterns.prefs
* Array of preferences to be set before running the test.
* @param {object} patterns.profile
* The profile to autofill. This is required only when running autofill test
* @param {Array} patterns.expectedResult
* The expected result of this heuristic test. See below for detailed explanation
* @param {Function} patterns.onTestComplete
* Function that is executed when the test is complete. This can be used by the test
* to verify the status after running the test.
*
* @param {string} patterns.autofillTrigger
* The selector to find the element to trigger the autocomplete popup.
* Currently we only supports id selector so the value must start with `#`.
* This parameter is only used when `options.testAutofill` is set.
*
* @param {string} patterns.submitButtonSelector
* The selector to find the submit button for capture test. This parameter
* is only used when `options.testCapture` is set.
* @param {object} patterns.captureFillValue
* An object that is keyed by selector, and the value to be set for the element
* that is found by matching selector before submitting the form. This parameter
* is only used when `options.testCapture` is set.
* @param {object} patterns.captureExpectedRecord
* The expected saved record after capturing the form. Keyed by field name. This
* parameter is only used when `options.testCapture` is set.
* @param {object} patterns.only
* This parameter is used solely for debugging purposes. When set to true,
* it restricts the execution to only the specified testcase.
*
* @param {string} [fixturePathPrefix=""]
* The prefix to the path of fixture files.
* @param {object} [options={ testAutofill: false, testCapture: false }]
* An options object containing additional configuration for running the test.
* @param {boolean} [options.testAutofill]
* When set to true, the following tests will be run:
* 1. Trigger preview and verify the preview result
* 2. Trigger autofill and verify the autofill result
* 3. Trigger clear form and verify the clear result
* @param {boolean} [options.testCapture]
* When set to true, the test submits the form after autofilling test finishes.
* Before submitting the form, the test first filles value if `captureFillValue`
* is set then submits the form. This test then verifies that the capture
* doorhanger appears, and the doorhanger captures the expected value (captureExpectedRecord).
*
* @param {string} [fixturePathPrefix=""] - The prefix to the path of fixture files.
* @param {object} [options={ testAutofill: false }] - An options object containing additional configuration for running the test.
* @param {boolean} [options.testAutofill=false] - A boolean indicating whether to run the test for autofill or not.
* @returns {Promise} A promise that resolves when all the tests are completed.
*
* The `patterns.expectedResult` array contains test data for different address or credit card sections.
@ -1037,29 +1448,26 @@ async function clearGleanTelemetry(onlyInParent = false) {
* }
* ],
* "/fixturepath",
* {testAutofill: true} // test options
* {
* testAutofill: true,
* testCapture: true,
* } // test options
* )
*/
async function add_heuristic_tests(
patterns,
fixturePathPrefix = "",
options = { testAutofill: false }
options = { testAutofill: false, testCapture: false }
) {
async function runTest(testPattern) {
const TEST_URL = testPattern.fixtureData
? `data:text/html,${testPattern.fixtureData}`
? TOP_LEVEL_HOST +
`/document-builder.sjs?html=${encodeURIComponent(
testPattern.fixtureData
)}`
: `${BASE_URL}../${fixturePathPrefix}${testPattern.fixturePath}`;
if (testPattern.fixtureData) {
info(`Starting test with fixture data`);
} else {
info(`Starting test fixture: ${testPattern.fixturePath ?? ""}`);
}
if (testPattern.description) {
info(`Test "${testPattern.description}"`);
}
info(`Test "${testPattern.description}"`);
if (testPattern.prefs) {
await SpecialPowers.pushPrefEnv({
@ -1067,139 +1475,154 @@ async function add_heuristic_tests(
});
}
await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
await SpecialPowers.spawn(browser, [], async function () {
const elements = Array.from(
content.document.querySelectorAll("input, select")
);
// Focus on each field in the test document to trigger autofill field detection
// on all the fields.
elements.forEach(element => element.focus());
});
if (testPattern.profile) {
await setStorage(testPattern.profile);
}
await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
await SimpleTest.promiseFocus(browser);
info(`Focus on each field in the test document`);
const contexts =
browser.browsingContext.getAllBrowsingContextsInSubtree();
for (const context of contexts) {
await SpecialPowers.spawn(context, [], async function () {
const elements = Array.from(
content.document.querySelectorAll("input, select")
);
// Focus on each field in the test document to trigger autofill field detection
// on all the fields.
elements.forEach(element => {
element.focus();
});
});
await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, context);
}
// This is a workaround for when we set focus on elements across iframes (in the previous step).
// The popup is not refreshed, and consequently, it does not receive key events needed to trigger
// the autocomplete popup.
if (contexts.length > 1) {
await sleep();
}
info(`Waiting for expected section count`);
const actor =
browser.browsingContext.currentWindowGlobal.getActor("FormAutofill");
await BrowserTestUtils.waitForCondition(() => {
return actor.getSections().length == testPattern.expectedResult.length;
const sections = Array.from(actor.sectionsByRootId.values()).flat();
return sections.length == testPattern.expectedResult.length;
}, "Expected section count.");
// Verify the identified fields in each section.
const sections = actor.getSections();
info(`Verify the identified fields in each section`);
const sections = Array.from(actor.sectionsByRootId.values()).flat();
verifySectionFieldDetails(sections, testPattern.expectedResult);
// Verify the autofilled value.
if (options.testAutofill) {
for (let index = 0; index < sections.length; index++) {
const section = sections[index];
// We do not autofill for sections that are invalid.
if (!section.isValidSection()) {
continue;
info(`test preview, autofill, and clear form`);
let section;
let autofillTrigger = testPattern.autofillTrigger;
if (autofillTrigger) {
if (!autofillTrigger.startsWith("#")) {
Assert.ok(false, `autofillTrigger must start with #`);
}
// Trigger autofill from the first element of this section
const elementId = section.fieldDetails[0].elementId;
const result = await actor.autofillFields(
elementId,
testPattern.profile
);
verifySectionAutofillResult(
section,
result,
testPattern.expectedResult[index]
section = sections.find(s =>
s.fieldDetails.some(f =>
f.identifier.startsWith(autofillTrigger.substr(1))
)
);
} else {
section = sections[0];
autofillTrigger = getSelectorFromFieldDetail(section.fieldDetails[0]);
}
const expected = testPattern.expectedResult[sections.indexOf(section)];
await triggerAutofillAndPreview(
browser,
autofillTrigger,
async () => verifyPreviewResult(browser, section, expected),
async () => verifyAutofillResult(browser, section, expected),
async () => verifyClearResult(browser, section)
);
}
if (options.testCapture) {
info(`test capture`);
const guid = await triggerCapture(
browser,
testPattern.submitButtonSelector,
testPattern.captureFillValue
);
verifyCaptureRecord(guid, testPattern.captureExpectedRecord);
await removeAllRecords();
}
});
if (testPattern.onTestComplete) {
await testPattern.onTestComplete();
}
if (testPattern.profile) {
await removeAllRecords();
}
if (testPattern.prefs) {
await SpecialPowers.popPrefEnv();
}
}
const only = patterns.find(pattern => !!pattern.only);
if (only) {
add_task(() => runTest(only));
return;
}
patterns.forEach(testPattern => {
add_task(() => runTest(testPattern));
});
}
async function add_autofill_heuristic_tests(patterns, fixturePathPrefix = "") {
add_heuristic_tests(patterns, fixturePathPrefix, { testAutofill: true });
}
async function add_capture_heuristic_tests(patterns, fixturePathPrefix = "") {
const oldValue = FormAutofillUtils.getOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
);
function fillEditDoorhanger(record) {
const notification = getNotification();
FormAutofillUtils.setOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
false
);
for (const [key, value] of Object.entries(record)) {
const id = AddressEditDoorhanger.getInputId(key);
const element = notification.querySelector(`#${id}`);
element.value = value;
}
}
// TODO: This function should be removed. We should make normalizeFields in
// FormAutofillStorageBase.sys.mjs static and using it directly
function normalizeAddressFields(record) {
let normalized = { ...record };
if (normalized.name != undefined) {
let nameParts = FormAutofillNameUtils.splitName(normalized.name);
normalized["given-name"] = nameParts.given;
normalized["additional-name"] = nameParts.middle;
normalized["family-name"] = nameParts.family;
delete normalized.name;
}
return normalized;
}
async function verifyConfirmationHint(
browser,
forceClose,
anchorID = "identity-icon-box"
) {
let hintElem = browser.ownerGlobal.ConfirmationHint._panel;
await BrowserTestUtils.waitForPopupEvent(hintElem, "shown");
try {
Assert.equal(hintElem.state, "open", "hint popup is open");
Assert.ok(
BrowserTestUtils.isVisible(hintElem.anchorNode),
"hint anchorNode is visible"
registerCleanupFunction(() => {
FormAutofillUtils.setOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
oldValue
);
Assert.equal(
hintElem.anchorNode.id,
anchorID,
"Hint should be anchored on the expected notification icon"
);
info("verifyConfirmationHint, hint is shown and has its anchorNode");
if (forceClose) {
await closePopup(hintElem);
} else {
info("verifyConfirmationHint, assertion ok, wait for poopuphidden");
await BrowserTestUtils.waitForPopupEvent(hintElem, "hidden");
info("verifyConfirmationHint, hintElem popup is hidden");
}
} catch (ex) {
Assert.ok(false, "Confirmation hint not shown: " + ex.message);
} finally {
info("verifyConfirmationHint promise finalized");
}
}
async function showAddressDoorhanger(browser, values = null) {
const defaultValues = {
"#given-name": "John",
"#family-name": "Doe",
"#organization": "Mozilla",
"#street-address": "123 Sesame Street",
};
const onPopupShown = waitForPopupShown();
const promise = BrowserTestUtils.browserLoaded(browser);
await focusUpdateSubmitForm(browser, {
focusSelector: "#given-name",
newValues: values ?? defaultValues,
});
await promise;
await onPopupShown;
add_heuristic_tests(patterns, fixturePathPrefix, { testCapture: true });
}
async function add_autofill_heuristic_tests(patterns, fixturePathPrefix = "") {
const oldValue = FormAutofillUtils.getOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
);
FormAutofillUtils.setOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
false
);
registerCleanupFunction(() => {
FormAutofillUtils.setOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
oldValue
);
});
add_heuristic_tests(patterns, fixturePathPrefix, { testAutofill: true });
}
add_setup(function () {

View file

@ -132,7 +132,6 @@ add_heuristic_tests(
},
{
fieldName: "address-level1",
autofill: "--",
reason: "autocomplete",
},
{

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<script>
const params = new URLSearchParams(document.location.search);
const formId = params.get('formId');
if (formId) {
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('form');
form.id = formId;
});
}
</script>
<body>
<form id="form">
<p><label>Expiration Date: <input id="cc-exp" autocomplete="cc-exp"></label></p>
</form>
</body>
</html>

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<script>
const params = new URLSearchParams(document.location.search);
const formId = params.get('formId');
if (formId) {
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('form');
form.id = formId;
});
}
</script>
<body>
<form id="form">
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
<p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
<p><label>Expiration Data: <input id="cc-exp" autocomplete="cc-exp"></label></p>
<input id="submit" type="submit">
</form>
</body>
</html>

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<body>
<form id="form">
<p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
</form>
</body>
</html>

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<body>
<form id="form">
<p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
</form>
</body>
</html>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<body>
<form id="form">
<p><label>Card Type:
<select id="cc-type" autocomplete="cc-type">
<option></option>
<option value="discover">Discover</option>
<option value="jcb">JCB</option>
<option value="visa">Visa</option>
<option value="mastercard">MasterCard</option>
<option value="gringotts">Unknown card network</option>
</select>
</label></p>
</form>
</body>
</html>

View file

@ -493,9 +493,10 @@ function initPopupListener() {
}
async function triggerPopupAndHoverItem(fieldSelector, selectIndex) {
const promise = expectPopup();
await focusAndWaitForFieldsIdentified(fieldSelector);
synthesizeKey("KEY_ArrowDown");
await expectPopup();
await await promise;
for (let i = 0; i <= selectIndex; i++) {
synthesizeKey("KEY_ArrowDown");
}

View file

@ -20,8 +20,6 @@ support-files = [
["test_basic_autocomplete_form.html"]
skip-if = [ "apple_catalina && !debug" ] # Bug 1789194
["test_form_changes.html"]
["test_formautofill_preview_highlight.html"]
skip-if = ["verify"]

View file

@ -1,136 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test basic autofill</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="formautofill_common.js"></script>
<script type="text/javascript" src="satchel_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
Form autofill test: autocomplete on an autofocus form
<script>
/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
"use strict";
let MOCK_STORAGE = [{
name: "John Doe",
organization: "Sesame Street",
"address-level2": "Austin",
tel: "+13453453456",
}, {
name: "Foo Bar",
organization: "Mozilla",
"address-level2": "San Francisco",
tel: "+16509030800",
}];
initPopupListener();
async function setupAddressStorage() {
await addAddress(MOCK_STORAGE[0]);
await addAddress(MOCK_STORAGE[1]);
}
function addInputField(form, className) {
let newElem = document.createElement("input");
newElem.name = className;
newElem.autocomplete = className;
newElem.type = "text";
form.appendChild(newElem);
}
async function checkFieldsAutofilled(formId, profile) {
const elements = document.querySelectorAll(`#${formId} input`);
for (const element of elements) {
await SimpleTest.promiseWaitForCondition(() => {
return element.value == profile[element.name];
});
await checkFieldHighlighted(element, true);
}
}
async function checkFormChangeHappened(formId) {
info("expecting form changed");
await focusAndWaitForFieldsIdentified(`#${formId} input[name=tel]`);
synthesizeKey("KEY_ArrowDown");
await expectPopup();
synthesizeKey("KEY_ArrowDown");
checkMenuEntriesComment(MOCK_STORAGE.map(address =>
makeAddressComment({
primary: address.tel,
secondary: address.name,
status: "Also autofills name, organization"
})
), 2);
// Click the first entry of the autocomplete popup and make sure all fields are autofilled
synthesizeKey("KEY_Enter");
await checkFieldsAutofilled(formId, MOCK_STORAGE[0]);
// This is for checking the changes of element count.
addInputField(document.querySelector(`#${formId}`), "address-level2");
await focusAndWaitForFieldsIdentified(`#${formId} input[name=name]`);
synthesizeKey("KEY_ArrowDown");
await expectPopup();
// Click on an autofilled field would show an autocomplete popup with "clear form" entry
checkMenuEntries([
"Clear Autofill Form", // Clear Autofill Form
"Manage addresses" // FormAutofill Preferemce
], 0);
// This is for checking the changes of element removed and added then.
document.querySelector(`#${formId} input[name=address-level2]`).remove();
addInputField(document.querySelector(`#${formId}`), "address-level2");
await focusAndWaitForFieldsIdentified(`#${formId} input[name=address-level2]`, true);
synthesizeKey("KEY_ArrowDown");
await expectPopup();
checkMenuEntriesComment(MOCK_STORAGE.map(address =>
makeAddressComment({
primary: address["address-level2"],
secondary: address.name,
status: "Also autofills name, organization, phone"
})
), 2);
// Make sure everything is autofilled in the end
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
await checkFieldsAutofilled(formId, MOCK_STORAGE[0]);
}
add_task(async function init_storage() {
await setupAddressStorage();
});
add_task(async function check_change_happened_in_form() {
await checkFormChangeHappened("form1");
});
add_task(async function check_change_happened_in_body() {
await checkFormChangeHappened("form2");
});
</script>
<p id="display"></p>
<div id="content">
<form id="form1">
<p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
<p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
<p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
</form>
<div id="form2">
<p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
<p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
<p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
</div>
</div>
<pre id="test"></pre>
</body>
</html>

View file

@ -348,34 +348,6 @@ const TESTCASES = [
},
],
},
{
description:
"Address form without matching options in select for address-level1 and country",
document: `<form>
<input autocomplete="given-name">
<select autocomplete="address-level1">
<option id="option-address-level1-dummy1" value="">Dummy</option>
<option id="option-address-level1-dummy2" value="">Dummy 2</option>
</select>
<select autocomplete="country">
<option id="option-country-dummy1" value="">Dummy</option>
<option id="option-country-dummy2" value="">Dummy 2</option>
</select>
</form>`,
profileData: [{ ...DEFAULT_ADDRESS_RECORD }],
expectedResult: [
{
guid: "123",
"street-address": "2 Harrison St\nline2\nline3",
"-moz-street-address-one-line": "2 Harrison St line2 line3",
"address-line1": "2 Harrison St",
"address-line2": "line2",
"address-line3": "line3",
tel: "+19876543210",
"tel-national": "9876543210",
},
],
},
{
description:
"Change the tel value of a profile to tel-national for a field without pattern and maxlength.",
@ -1311,9 +1283,11 @@ for (let testcase of TESTCASES) {
let expectedOption = doc.getElementById(expectedOptionElement[field]);
Assert.notEqual(expectedOption, null);
let value = testcase.profileData[i][field];
let cache = handler.matchingSelectOption.get(select);
let targetOption = cache[value] && cache[value].deref();
let targetOption =
handler.matchSelectOptions(
{ element: select, fieldName: field },
testcase.profileData[i]
) ?? null;
Assert.notEqual(targetOption, null);
Assert.equal(targetOption, expectedOption);

View file

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { FormAutofillChild } = ChromeUtils.importESModule(
"resource://autofill/FormAutofillChild.sys.mjs"
);
add_task(async function test_inspectFields() {
const doc = MockDocument.createTestDocument(
"http://localhost:8080/test/",
`<form>
<input id="cc-number" autocomplete="cc-number">
<input id="cc-name" autocomplete="cc-name">
<input id="cc-exp" autocomplete="cc-exp">
</form>
<select id="cc-type" autocomplete="cc-type">
<option/>
<option value="visa">VISA</option>
</select>
<form>
<input id="name" autocomplete="name">
<select id="country" autocomplete="country">
<option/>
<option value="US">United States</option>
</select>
</form>
<input id="email" autocomplete="email">
`
);
const fac = new FormAutofillChild();
Object.defineProperty(fac, "document", {
value: doc,
});
const fields = fac.inspectFields();
const expectedElements = Array.from(doc.querySelectorAll("input, select"));
const inspectedElements = fields.map(field => field.element);
Assert.deepEqual(
expectedElements,
inspectedElements,
"inspectedElements should return all the eligible fields"
);
});

View file

@ -84,6 +84,10 @@ let addressTestCases = [
description: "Focus on an `organization` field",
options: {},
matchingProfiles,
filledCategories: [
["address", "name", "tel"],
["address", "name", "tel"],
],
allFieldNames,
searchString: "",
fieldDetail: { fieldName: "organization" },
@ -122,6 +126,11 @@ let addressTestCases = [
description: "Focus on an `tel` field",
options: {},
matchingProfiles,
filledCategories: [
["address", "name", "tel", "organization"],
["address", "name", "tel", "organization"],
["address", "tel"],
],
allFieldNames,
searchString: "",
fieldDetail: { fieldName: "tel" },
@ -172,6 +181,11 @@ let addressTestCases = [
description: "Focus on an `street-address` field",
options: {},
matchingProfiles,
filledCategories: [
["address", "name", "tel", "organization"],
["address", "name", "tel", "organization"],
["address", "tel"],
],
allFieldNames,
searchString: "",
fieldDetail: { fieldName: "street-address" },
@ -222,6 +236,11 @@ let addressTestCases = [
description: "Focus on an `address-line1` field",
options: {},
matchingProfiles,
filledCategories: [
["address", "name", "tel", "organization"],
["address", "name", "tel", "organization"],
["address", "tel"],
],
allFieldNames,
searchString: "",
fieldDetail: { fieldName: "address-line1" },
@ -465,6 +484,7 @@ add_task(async function test_all_patterns() {
testCase.fieldDetail,
testCase.allFieldNames,
testCase.matchingProfiles,
testCase.filledCategories,
testCase.options
);
let expectedValue = testCase.expected;

View file

@ -84,6 +84,8 @@ skip-if = [
"apple_silicon", # bug 1729554
]
["test_inspectFields.js"]
["test_isAddressAutofillAvailable.js"]
["test_isCJKName.js"]

View file

@ -16,7 +16,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"af": {
"pin": false,
@ -35,7 +35,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"an": {
"pin": false,
@ -54,7 +54,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ar": {
"pin": false,
@ -73,7 +73,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ast": {
"pin": false,
@ -92,7 +92,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"az": {
"pin": false,
@ -111,7 +111,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"be": {
"pin": false,
@ -130,7 +130,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"bg": {
"pin": false,
@ -149,7 +149,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"bn": {
"pin": false,
@ -168,7 +168,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"bo": {
"pin": false,
@ -187,7 +187,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"br": {
"pin": false,
@ -206,7 +206,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"brx": {
"pin": false,
@ -225,7 +225,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"bs": {
"pin": false,
@ -244,7 +244,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ca": {
"pin": false,
@ -263,7 +263,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ca-valencia": {
"pin": false,
@ -282,7 +282,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"cak": {
"pin": false,
@ -301,7 +301,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ckb": {
"pin": false,
@ -320,7 +320,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"cs": {
"pin": false,
@ -339,7 +339,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"cy": {
"pin": false,
@ -358,7 +358,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"da": {
"pin": false,
@ -377,7 +377,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"de": {
"pin": false,
@ -396,7 +396,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"dsb": {
"pin": false,
@ -415,7 +415,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"el": {
"pin": false,
@ -434,7 +434,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"en-CA": {
"pin": false,
@ -453,7 +453,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"en-GB": {
"pin": false,
@ -472,7 +472,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"eo": {
"pin": false,
@ -491,7 +491,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"es-AR": {
"pin": false,
@ -510,7 +510,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"es-CL": {
"pin": false,
@ -529,7 +529,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"es-ES": {
"pin": false,
@ -548,7 +548,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"es-MX": {
"pin": false,
@ -567,7 +567,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"et": {
"pin": false,
@ -586,7 +586,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"eu": {
"pin": false,
@ -605,7 +605,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"fa": {
"pin": false,
@ -624,7 +624,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ff": {
"pin": false,
@ -643,7 +643,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"fi": {
"pin": false,
@ -662,7 +662,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"fr": {
"pin": false,
@ -681,7 +681,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"fur": {
"pin": false,
@ -700,7 +700,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"fy-NL": {
"pin": false,
@ -719,7 +719,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ga-IE": {
"pin": false,
@ -738,7 +738,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"gd": {
"pin": false,
@ -757,7 +757,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"gl": {
"pin": false,
@ -776,7 +776,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"gn": {
"pin": false,
@ -795,7 +795,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"gu-IN": {
"pin": false,
@ -814,7 +814,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"he": {
"pin": false,
@ -833,7 +833,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"hi-IN": {
"pin": false,
@ -852,7 +852,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"hr": {
"pin": false,
@ -871,7 +871,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"hsb": {
"pin": false,
@ -890,7 +890,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"hu": {
"pin": false,
@ -909,7 +909,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"hy-AM": {
"pin": false,
@ -928,7 +928,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"hye": {
"pin": false,
@ -947,7 +947,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ia": {
"pin": false,
@ -966,7 +966,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"id": {
"pin": false,
@ -985,7 +985,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"is": {
"pin": false,
@ -1004,7 +1004,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"it": {
"pin": false,
@ -1023,7 +1023,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ja": {
"pin": false,
@ -1040,7 +1040,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ja-JP-mac": {
"pin": false,
@ -1048,7 +1048,7 @@
"macosx64",
"macosx64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ka": {
"pin": false,
@ -1067,7 +1067,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"kab": {
"pin": false,
@ -1086,7 +1086,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"kk": {
"pin": false,
@ -1105,7 +1105,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"km": {
"pin": false,
@ -1124,7 +1124,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"kn": {
"pin": false,
@ -1143,7 +1143,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ko": {
"pin": false,
@ -1162,7 +1162,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"lij": {
"pin": false,
@ -1181,7 +1181,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"lo": {
"pin": false,
@ -1200,7 +1200,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"lt": {
"pin": false,
@ -1219,7 +1219,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ltg": {
"pin": false,
@ -1238,7 +1238,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"lv": {
"pin": false,
@ -1257,7 +1257,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"meh": {
"pin": false,
@ -1276,7 +1276,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"mk": {
"pin": false,
@ -1295,7 +1295,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"mr": {
"pin": false,
@ -1314,7 +1314,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ms": {
"pin": false,
@ -1333,7 +1333,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"my": {
"pin": false,
@ -1352,7 +1352,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"nb-NO": {
"pin": false,
@ -1371,7 +1371,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ne-NP": {
"pin": false,
@ -1390,7 +1390,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"nl": {
"pin": false,
@ -1409,7 +1409,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"nn-NO": {
"pin": false,
@ -1428,7 +1428,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"oc": {
"pin": false,
@ -1447,7 +1447,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"pa-IN": {
"pin": false,
@ -1466,7 +1466,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"pl": {
"pin": false,
@ -1485,7 +1485,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"pt-BR": {
"pin": false,
@ -1504,7 +1504,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"pt-PT": {
"pin": false,
@ -1523,7 +1523,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"rm": {
"pin": false,
@ -1542,7 +1542,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ro": {
"pin": false,
@ -1561,7 +1561,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ru": {
"pin": false,
@ -1580,7 +1580,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sat": {
"pin": false,
@ -1599,7 +1599,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sc": {
"pin": false,
@ -1618,7 +1618,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"scn": {
"pin": false,
@ -1637,7 +1637,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sco": {
"pin": false,
@ -1656,7 +1656,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"si": {
"pin": false,
@ -1675,7 +1675,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sk": {
"pin": false,
@ -1694,7 +1694,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"skr": {
"pin": false,
@ -1713,7 +1713,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sl": {
"pin": false,
@ -1732,7 +1732,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"son": {
"pin": false,
@ -1751,7 +1751,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sq": {
"pin": false,
@ -1770,7 +1770,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sr": {
"pin": false,
@ -1789,7 +1789,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"sv-SE": {
"pin": false,
@ -1808,7 +1808,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"szl": {
"pin": false,
@ -1827,7 +1827,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ta": {
"pin": false,
@ -1846,7 +1846,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"te": {
"pin": false,
@ -1865,7 +1865,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"tg": {
"pin": false,
@ -1884,7 +1884,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"th": {
"pin": false,
@ -1903,7 +1903,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"tl": {
"pin": false,
@ -1922,7 +1922,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"tr": {
"pin": false,
@ -1941,7 +1941,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"trs": {
"pin": false,
@ -1960,7 +1960,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"uk": {
"pin": false,
@ -1979,7 +1979,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"ur": {
"pin": false,
@ -1998,7 +1998,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"uz": {
"pin": false,
@ -2017,7 +2017,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"vi": {
"pin": false,
@ -2036,7 +2036,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"wo": {
"pin": false,
@ -2055,7 +2055,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"xh": {
"pin": false,
@ -2074,7 +2074,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"zh-CN": {
"pin": false,
@ -2093,7 +2093,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
},
"zh-TW": {
"pin": false,
@ -2112,6 +2112,6 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b05219d455a438c501765992929bf2c3e83996de"
"revision": "c628e0bed7361ec547babde7cc6bd68e620f08cb"
}
}

View file

@ -143,7 +143,7 @@ add_task(async function toolbarButtons() {
() => {
return (
bookmarksToolbar.getAttribute("collapsed") != "true" &&
bookmarksToolbar.getAttribute("initialized") == "true"
bookmarksToolbar.hasAttribute("initialized")
);
}
);

View file

@ -5,8 +5,6 @@
@import url("chrome://browser/skin/browser-shared.css");
@import url("chrome://browser/skin/contextmenu.css");
@namespace html url("http://www.w3.org/1999/xhtml");
/**
* We intentionally do not include browser-custom-colors.css, instead choosing to
* fall back to system colors and transparencies in order to accommodate the
@ -17,24 +15,6 @@
*/
@media not (prefers-contrast) {
:root:not([lwtheme]) {
--toolbar-field-border-color: transparent;
/* These colors don't exactly match the default dark color that buttons and
* textfields have in Adwaita[1][2], which would end up computing to
* rgba(0, 0, 0, .8 * .1) and rgba(255, 255, 255, 1 * .1) for light and
* dark, respectively.
*
* Instead, for the light theme we use .05 alpha, to increase the contrast
* of the text. For the dark theme, we use a darker background, which works
* because the toolbar background applies some white unconditionally, and
* matches what our default themes do in other platforms too.
*
* [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/blob/42c04e038f19b2123560da662692d65480a67931/src/stylesheet/widgets/_buttons.scss#L1
* [2]: https://gitlab.gnome.org/GNOME/libadwaita/-/blob/42c04e038f19b2123560da662692d65480a67931/src/stylesheet/widgets/_entries.scss#L9
*/
--toolbar-field-background-color: light-dark(rgba(0, 0, 0, .05), rgba(0, 0, 0, .3));
--toolbar-field-color: inherit;
@media (-moz-gtk-theme-family) {
--tabs-navbar-separator-style: none;
@media (prefers-color-scheme: light) {

View file

@ -12,14 +12,9 @@
/* stylelint-disable-next-line media-query-no-invalid */
@media (-moz-bool-pref: "browser.theme.macos.native-theme") {
/* TODO: Share this with Linux, which effectively does ~the same */
@media not (prefers-contrast) {
:root:not([lwtheme]) {
--toolbar-field-border-color: transparent;
--toolbar-field-background-color: light-dark(rgba(0, 0, 0, .05), rgba(0, 0, 0, .3));
--toolbar-field-color: inherit;
@media (prefers-color-scheme: light) {
--toolbar-non-lwt-bgcolor: white;
--urlbar-box-bgcolor: #fafafa;
}
@media (prefers-color-scheme: dark) {

View file

@ -2,8 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@namespace html url("http://www.w3.org/1999/xhtml");
@media not (prefers-contrast) {
:root:not([lwtheme]) {
--button-primary-bgcolor: light-dark(rgb(0, 97, 224), rgb(0, 221, 255));

View file

@ -192,9 +192,6 @@ body {
#navigator-toolbox {
appearance: none;
/* Toolbar / content area border */
border-bottom: 0.01px solid var(--chrome-content-separator-color);
background-color: var(--toolbox-non-lwt-bgcolor);
color: var(--toolbox-non-lwt-textcolor);
@ -206,11 +203,6 @@ body {
color: var(--toolbox-non-lwt-textcolor-inactive);
}
/* stylelint-disable-next-line media-query-no-invalid */
@media (-moz-bool-pref: "sidebar.revamp") {
border-bottom: none;
}
&[fullscreenShouldAnimate] {
transition: 0.8s margin-top ease-out;
}
@ -318,6 +310,23 @@ body {
}
}
/* Chrome/content separation */
#navigator-toolbox {
/* This reserves space for the tabbox shadow */
border-bottom: 0.5px solid var(--toolbar-bgcolor);
}
#tabbrowser-tabbox {
position: relative;
box-shadow: 0 0 0 0.5px var(--chrome-content-separator-color);
/* stylelint-disable-next-line media-query-no-invalid */
@media (-moz-bool-pref: "sidebar.revamp") {
box-shadow: 0 0 0 0.5px var(--chrome-content-separator-color), 0 2px 6px 0 light-dark(rgb(0, 0, 0, 0.2), rgb(0, 0, 0, 0.8));
}
}
/* Hide the TabsToolbar titlebar controls if the menubar is permanently shown.
* (That is, if the menu bar doesn't autohide, and we're not in a fullscreen or
* popup window.) */
@ -437,28 +446,11 @@ body {
/* Bookmarks toolbar empty message */
/* If the toolbar is not initialized set a zero width, but retain height. */
&[collapsed=false]:not([initialized]) > #personal-toolbar-empty {
visibility: hidden;
}
/*
* Make the empty bookmarks toolbar message take up no horizontal space.
* This avoids two issues:
* 1) drag/drop of urls/bookmarks to the toolbar not working, because they
* land on the personal-toolbar-empty element.
* 2) buttons in the toolbar moving horizontally while the window opens,
* because the element is first shown at full width and then completely
* hidden.
* TODO(emilio): The comment above was never quite true (the message did take
* horizontal space, see bug 1812636). Figure out how much of this rule is
* really needed.
*/
&[collapsed=false] > #personal-toolbar-empty[nowidth] > #personal-toolbar-empty-description {
margin-inline: 0;
min-width: 0;
white-space: nowrap;
position: relative;
z-index: 1;
width: 0;
overflow-x: hidden;
}
/* Bookmarks toolbar in customize mode */
@ -471,6 +463,10 @@ body {
}
}
#personal-toolbar-empty-description {
white-space: nowrap;
}
#personal-bookmarks {
position: relative;
-moz-window-dragging: inherit;

View file

@ -25,7 +25,6 @@
}
}
#appcontent,
#browser,
#tabbrowser-tabbox,
#tabbrowser-tabpanels,
@ -35,22 +34,6 @@
min-height: 0;
}
#appcontent {
/* stylelint-disable-next-line media-query-no-invalid */
@media (-moz-bool-pref: "sidebar.revamp") {
border-top: 0.5px solid var(--chrome-content-separator-color);
border-inline-start: 0.5px solid var(--chrome-content-separator-color);
box-shadow: 0 2px 6px 0 light-dark(rgb(0, 0, 0, 0.2), rgb(0, 0, 0, 0.8));
position: relative;
}
/* stylelint-disable-next-line media-query-no-invalid */
@media (not (-moz-bool-pref: "sidebar.position_start")) and (-moz-bool-pref: "sidebar.revamp") {
border-inline-end: 0.5px solid var(--chrome-content-separator-color);
border-inline-start: none;
}
}
/* We set large flex on both containers to allow the devtools toolbox to
* set a flex value itself. We don't want the toolbox to actually take up free
* space, but we do want it to collapse when the window shrinks, and with

View file

@ -174,6 +174,17 @@
fun:_ZN5style5bloom19StyleBloom$LT$E$GT$3new*
...
}
{
Same as above, but for rustc >= 1.80
Memcheck:Leak
match-leak-kinds: definite
fun:calloc
...
fun:_ZN3std3sys12thread_local10fast_local4lazy20Storage*10initialize*
...
fun:_ZN5style5bloom19StyleBloom$LT$E$GT$3new*
...
}
{
We intentionally leak sharing cache TLS data in the global servo thread-pool until we can free it consistently (https://github.com/rayon-rs/rayon/issues/688)
Memcheck:Leak
@ -185,6 +196,17 @@
fun:_ZN5style7sharing26StyleSharingCache$LT$E$GT$3new*
...
}
{
Same as above, but for rustc >= 1.80
Memcheck:Leak
match-leak-kinds: definite
fun:malloc
...
fun:_ZN3std3sys12thread_local10fast_local4lazy20Storage*10initialize*
...
fun:_ZN5style7sharing26StyleSharingCache$LT$E$GT$3new*
...
}
{
Leak in libfontconfig1 in Debian 8 and 9. See bug 1636003.
Memcheck:Leak

View file

@ -261,6 +261,9 @@ export IPHONEOS_SDK_DIR
PATH := $(topsrcdir)/build/macosx:$(PATH)
endif
endif
# Use the same prefix as set through modules/zlib/src/mozzconf.h
# for libz-rs-sys, since we still use the headers from there.
export LIBZ_RS_SYS_PREFIX=MOZ_Z_
ifndef RUSTC_BOOTSTRAP
RUSTC_BOOTSTRAP := mozglue_static,qcms

View file

@ -13,9 +13,14 @@ import {
getSelectedFrame,
getCurrentThread,
getSelectedException,
getSelectedTraceIndex,
getAllTraces,
} from "../selectors/index";
import { getMappedExpression } from "./expressions";
const {
TRACER_FIELDS_INDEXES,
} = require("resource://devtools/server/actors/tracer.js");
async function findExpressionMatch(state, parserWorker, editor, tokenPos) {
const location = getSelectedLocation(state);
@ -35,7 +40,97 @@ async function findExpressionMatch(state, parserWorker, editor, tokenPos) {
return editor.getExpressionFromCoords(tokenPos);
}
export function getPreview(target, tokenPos, editor) {
/**
* Get a preview object for the currently selected frame in the JS Tracer.
*
* @param {Object} target
* The hovered DOM Element within CodeMirror rendering.
* @param {Object} tokenPos
* The CodeMirror position object for the hovered token.
* @param {Object} editor
* The CodeMirror editor object.
*/
export function getTracerPreview(target, tokenPos, editor) {
return async thunkArgs => {
const { getState, parserWorker } = thunkArgs;
const selectedTraceIndex = getSelectedTraceIndex(getState());
if (selectedTraceIndex == null) {
return null;
}
const trace = getAllTraces(getState())[selectedTraceIndex];
// We may be selecting a mutation trace, which doesn't expose any value,
// so only consider method calls.
if (trace[TRACER_FIELDS_INDEXES.TYPE] != "enter") {
return null;
}
const match = await findExpressionMatch(
getState(),
parserWorker,
editor,
tokenPos
);
let { expression, location } = match;
const source = getSelectedSource(getState());
if (location && source.isOriginal) {
const thread = getCurrentThread(getState());
const mapResult = await getMappedExpression(
expression,
thread,
thunkArgs
);
if (mapResult) {
expression = mapResult.expression;
}
}
const argumentValues = trace[TRACER_FIELDS_INDEXES.ENTER_ARGS];
const argumentNames = trace[TRACER_FIELDS_INDEXES.ENTER_ARG_NAMES];
if (!argumentNames || !argumentValues) {
return null;
}
const argumentIndex = argumentNames.indexOf(expression);
if (argumentIndex == -1) {
return null;
}
const result = argumentValues[argumentIndex];
// Values are either primitives, or an Object Front
const resultGrip = result?.getGrip ? result?.getGrip() : result;
const root = {
path: expression,
contents: {
value: resultGrip,
front: getFront(result),
},
};
return {
previewType: "tracer",
target,
tokenPos,
cursorPos: target.getBoundingClientRect(),
expression,
root,
resultGrip,
};
};
}
/**
* Get a preview object for the currently paused frame, if paused.
*
* @param {Object} target
* The hovered DOM Element within CodeMirror rendering.
* @param {Object} tokenPos
* The CodeMirror position object for the hovered token.
* @param {Object} editor
* The CodeMirror editor object.
*/
export function getPausedPreview(target, tokenPos, editor) {
return async thunkArgs => {
const { getState, client, parserWorker } = thunkArgs;
if (
@ -127,6 +222,7 @@ export function getPreview(target, tokenPos, editor) {
};
return {
previewType: "pause",
target,
tokenPos,
cursorPos: target.getBoundingClientRect(),

View file

@ -6,6 +6,7 @@ import {
getAllTraces,
getTraceFrames,
getIsCurrentlyTracing,
getCurrentThread,
} from "../selectors/index";
import { selectSourceBySourceActorID } from "./sources/select.js";
const {
@ -44,9 +45,13 @@ export function addTraces(traces) {
export function selectTrace(traceIndex) {
return async function ({ dispatch, getState }) {
// For now, the tracer only consider the top level thread
const thread = getCurrentThread(getState());
dispatch({
type: "SELECT_TRACE",
traceIndex,
thread,
});
const traces = getAllTraces(getState());
const trace = traces[traceIndex];

View file

@ -41,7 +41,7 @@ breakpointButton.appendChild(svg);
class ColumnBreakpoints extends Component {
static get propTypes() {
return {
columnBreakpoints: PropTypes.array.isRequired,
columnBreakpoints: PropTypes.array,
editor: PropTypes.object.isRequired,
selectedSource: PropTypes.object,
addBreakpoint: PropTypes.func,

View file

@ -17,6 +17,12 @@
border-bottom: 1px solid var(--theme-splitter-color);
}
.conditional-breakpoint-panel:focus-within {
outline: var(--theme-focus-outline);
outline-offset: -2px;
box-shadow: var(--theme-outline-box-shadow);
}
.conditional-breakpoint-panel .prompt {
font-size: 1.8em;
color: var(--theme-graphs-orange);

View file

@ -24,12 +24,6 @@ import {
const classnames = require("resource://devtools/client/shared/classnames.js");
function addNewLine(doc) {
const cursor = doc.getCursor();
const pos = { line: cursor.line, ch: cursor.ch };
doc.replaceRange("\n", pos);
}
export class ConditionalPanel extends PureComponent {
cbPanel;
input;
@ -70,12 +64,8 @@ export class ConditionalPanel extends PureComponent {
};
onKey = e => {
if (e.key === "Enter") {
if (this.codeMirror && e.altKey) {
addNewLine(this.codeMirror.doc);
} else {
this.saveAndClose();
}
if (e.key === "Enter" && !e.shiftKey) {
this.saveAndClose();
} else if (e.key === "Escape") {
this.props.closeConditionalPanel();
}

View file

@ -46,14 +46,14 @@ class SourceFooter extends PureComponent {
static get propTypes() {
return {
canPrettyPrint: PropTypes.bool.isRequired,
prettyPrintMessage: PropTypes.string.isRequired,
prettyPrintMessage: PropTypes.string,
endPanelCollapsed: PropTypes.bool.isRequired,
horizontal: PropTypes.bool.isRequired,
jumpToMappedLocation: PropTypes.func.isRequired,
mappedSource: PropTypes.object,
selectedSource: PropTypes.object,
selectedLocation: PropTypes.object,
isSelectedSourceBlackBoxed: PropTypes.bool.isRequired,
isSelectedSourceBlackBoxed: PropTypes.bool,
sourceLoaded: PropTypes.bool.isRequired,
toggleBlackBox: PropTypes.func.isRequired,
togglePaneCollapse: PropTypes.func.isRequired,
@ -277,6 +277,7 @@ class SourceFooter extends PureComponent {
MenuButton,
{
menuId: "debugger-source-map-button",
key: "debugger-source-map-button",
toolboxDoc,
className: classnames("devtools-button", "debugger-source-map-button", {
error: !!this.props.sourceMapError,

View file

@ -59,7 +59,7 @@ export class HighlightLine extends Component {
selectedFrame: PropTypes.object,
selectedLocation: PropTypes.object.isRequired,
selectedSourceTextContent: PropTypes.object.isRequired,
shouldHighlightSelectedLocation: PropTypes.func.isRequired,
shouldHighlightSelectedLocation: PropTypes.bool.isRequired,
editor: PropTypes.object,
};
}

View file

@ -69,6 +69,7 @@ class InlinePreviewRow extends PureComponent {
React.createElement(InlinePreview, {
line,
key: `${line}-${preview.name}`,
type: preview.type,
variable: preview.name,
value: preview.value,
openElementInInspector,

View file

@ -1,111 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
.popover .preview {
background: var(--theme-body-background);
width: 350px;
border: 1px solid var(--theme-splitter-color);
padding: 10px;
height: auto;
min-height: inherit;
max-height: 200px;
overflow: auto;
box-shadow: 1px 2px 3px var(--popup-shadow-color);
}
.theme-dark .popover .preview {
box-shadow: 1px 2px 3px var(--popup-shadow-color);
}
.popover .preview .header {
width: 100%;
line-height: 20px;
border-bottom: 1px solid #cccccc;
display: flex;
flex-direction: column;
}
.popover .preview .header .link {
align-self: flex-end;
color: var(--theme-link-color);
text-decoration: underline;
}
.selection,
.debug-expression.selection {
background-color: var(--theme-highlight-yellow);
}
.theme-dark .selection,
.theme-dark .debug-expression.selection {
background-color: #743884;
}
.theme-dark .cm-s-mozilla .selection,
.theme-dark .cm-s-mozilla .debug-expression.selection {
color: #e7ebee;
}
.popover .preview .function-signature {
padding-top: 10px;
}
.theme-dark .popover .preview {
border-color: var(--theme-body-color);
}
.tooltip {
position: fixed;
z-index: 100;
}
.tooltip .preview {
background: var(--theme-toolbar-background);
max-width: inherit;
border: 1px solid var(--theme-splitter-color);
box-shadow: 1px 2px 4px 1px var(--theme-toolbar-background-alt);
padding: 5px;
height: auto;
min-height: inherit;
max-height: 200px;
overflow: auto;
}
.theme-dark .tooltip .preview {
border-color: var(--theme-body-color);
}
.tooltip .gap {
height: 4px;
padding-top: 4px;
}
.add-to-expression-bar {
border: 1px solid var(--theme-splitter-color);
border-top: none;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 14px;
line-height: 30px;
background: var(--theme-toolbar-background);
color: var(--theme-text-color-inactive);
padding: 0 4px;
}
.add-to-expression-bar .prompt {
width: 1em;
}
.add-to-expression-bar .expression-to-save-label {
width: calc(100% - 4em);
}
.add-to-expression-bar .expression-to-save-button {
font-size: 14px;
color: var(--theme-comment);
}

View file

@ -10,17 +10,91 @@
box-shadow: 1px 1px 3px var(--popup-shadow-color);
}
/* Popover is used when previewing objects */
.popover .preview-popup {
padding: 5px 10px;
max-width: 450px;
min-width: 200px;
&.preview-type-pause {
padding: 5px 10px;
}
/* Because of tracer header, we can't put a padding on the popup */
/* Nor can we put a margin on the .tree or .objectBox which causes overflows */
&.preview-type-tracer {
padding-bottom: 5px;
.tree {
padding: 0 5px;
}
}
}
/* Tooltip is used when previewing primitives */
.tooltip .preview-popup {
max-width: inherit;
padding: 5px;
min-height: inherit;
max-height: 200px;
&.preview-type-pause {
padding: 5px;
}
&.preview-type-tracer {
padding-bottom: 5px;
.preview-tracer-header {
margin-bottom: 5px;
}
.objectBox {
padding: 0 5px;
}
}
}
.preview-tracer-header {
--icon-url: url("chrome://devtools/content/debugger/images/trace.svg");
--icon-color: var(--theme-inline-preview-label-trace-color);
color: var(--theme-inline-preview-label-trace-color);
background-color: var(--theme-inline-preview-label-trace-background);
border-block-end: 1px solid oklch(from var(--theme-inline-preview-label-trace-color) l c h / 0.25);
/* Make sure the header is always visible */
position: sticky;
top: 0;
z-index: 1;
/* Add a bit more padding on the end to balance the icon, especially when have a small primitive value in preview */
padding-inline-end: 10px !important;
}
.preview-tracer-warning {
--icon-url: url("chrome://devtools/skin/images/info.svg");
--icon-color: currentColor;
background-color: var(--theme-body-alternate-emphasized-background);
border-bottom: 1px solid var(--theme-splitter-color);
margin-bottom: 5px;
}
.preview-tracer-header, .preview-tracer-warning {
display: flex;
gap: 5px;
padding: 5px;
align-items: center;
&::before {
flex-shrink: 0;
content: "";
display: inline-block;
width: 12px;
height: 12px;
background-image: var(--icon-url);
background-size: contain;
background-repeat: no-repeat;
background-position: center;
-moz-context-properties: fill;
fill: var(--icon-color);
}
}
.preview-popup .tree {
@ -80,35 +154,6 @@
padding-top: 0px;
}
.add-to-expression-bar {
border: 1px solid var(--theme-splitter-color);
border-top: none;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 14px;
line-height: 30px;
background: var(--theme-toolbar-background);
color: var(--theme-text-color-inactive);
padding: 0 4px;
}
.add-to-expression-bar .prompt {
width: 1em;
}
.add-to-expression-bar .expression-to-save-label {
width: calc(100% - 4em);
}
.add-to-expression-bar .expression-to-save-button {
font-size: 14px;
color: var(--theme-comment);
}
/* Exception popup */
.exception-popup .exception-text {
color: var(--red-70);

View file

@ -107,7 +107,7 @@ export class Popup extends Component {
renderPreview() {
const {
preview: { root, exception, resultGrip },
preview: { root, exception, resultGrip, previewType },
} = this.props;
const usesCustomFormatter =
@ -119,11 +119,21 @@ export class Popup extends Component {
return div(
{
className: "preview-popup",
className: `preview-popup preview-type-${previewType}`,
style: {
maxHeight: this.calculateMaxHeight(),
},
},
// Bug 1915610 - JS Tracer isn't localized yet
previewType == "tracer"
? div({ className: "preview-tracer-header" }, "Tracer preview")
: null,
previewType == "tracer" && !nodeIsPrimitive(root)
? div(
{ className: "preview-tracer-warning" },
"Attribute previews on traced objects are showing the current values and not the value at execution of the selected frame."
)
: null,
React.createElement(ObjectInspector, {
roots: [root],
autoExpandDepth: 1,

View file

@ -8,7 +8,10 @@ import { connect } from "devtools/client/shared/vendor/react-redux";
import Popup from "./Popup";
import { getIsCurrentThreadPaused } from "../../../selectors/index";
import {
getIsCurrentThreadPaused,
getSelectedTraceIndex,
} from "../../../selectors/index";
import actions from "../../../actions/index";
import { features } from "../../../utils/prefs";
@ -26,6 +29,7 @@ class Preview extends PureComponent {
editor: PropTypes.object.isRequired,
editorRef: PropTypes.object.isRequired,
isPaused: PropTypes.bool.isRequired,
hasSelectedTrace: PropTypes.bool.isRequired,
getExceptionPreview: PropTypes.func.isRequired,
getPreview: PropTypes.func,
};
@ -75,7 +79,14 @@ class Preview extends PureComponent {
const tokenId = {};
this.currentTokenId = tokenId;
const { editor, getPreview, getExceptionPreview } = this.props;
const {
editor,
getPausedPreview,
getTracerPreview,
getExceptionPreview,
isPaused,
hasSelectedTrace,
} = this.props;
const isTargetException = target.closest(`.${EXCEPTION_MARKER}`);
let preview;
@ -83,8 +94,13 @@ class Preview extends PureComponent {
preview = await getExceptionPreview(target, tokenPos, editor);
}
if (!preview && this.props.isPaused && !this.state.selecting) {
preview = await getPreview(target, tokenPos, editor);
if (!preview && (hasSelectedTrace || isPaused) && !this.state.selecting) {
if (hasSelectedTrace) {
preview = await getTracerPreview(target, tokenPos, editor);
}
if (!preview && isPaused) {
preview = await getPausedPreview(target, tokenPos, editor);
}
}
// Prevent modifying state and showing this preview if we started hovering another token
@ -95,19 +111,19 @@ class Preview extends PureComponent {
};
onMouseUp = () => {
if (this.props.isPaused) {
if (this.props.isPaused || this.props.hasSelectedTrace) {
this.setState({ selecting: false });
}
};
onMouseDown = () => {
if (this.props.isPaused) {
if (this.props.isPaused || this.props.hasSelectedTrace) {
this.setState({ selecting: true });
}
};
onScroll = () => {
if (this.props.isPaused) {
if (this.props.isPaused || this.props.hasSelectedTrace) {
this.clearPreview();
}
};
@ -133,11 +149,13 @@ class Preview extends PureComponent {
const mapStateToProps = state => {
return {
isPaused: getIsCurrentThreadPaused(state),
hasSelectedTrace: getSelectedTraceIndex(state) != null,
};
};
export default connect(mapStateToProps, {
addExpression: actions.addExpression,
getPreview: actions.getPreview,
getPausedPreview: actions.getPausedPreview,
getTracerPreview: actions.getTracerPreview,
getExceptionPreview: actions.getExceptionPreview,
})(Preview);

View file

@ -58,7 +58,7 @@ class SearchInFileBar extends Component {
editor: PropTypes.object,
modifiers: PropTypes.object.isRequired,
searchInFileEnabled: PropTypes.bool.isRequired,
selectedSourceTextContent: PropTypes.bool.isRequired,
selectedSourceTextContent: PropTypes.object,
selectedSource: PropTypes.object.isRequired,
setActiveSearch: PropTypes.func.isRequired,
querySearchWorker: PropTypes.func.isRequired,

View file

@ -876,7 +876,7 @@ class Editor extends PureComponent {
React.Fragment,
null,
React.createElement(Breakpoints, { editor }),
isPaused &&
(isPaused || isTraceSelected) &&
selectedSource.isOriginal &&
!selectedSource.isPrettyPrinted &&
!mapScopesEnabled
@ -929,7 +929,7 @@ class Editor extends PureComponent {
React.createElement(Breakpoints, {
editor,
}),
isPaused &&
(isPaused || isTraceSelected) &&
selectedSource.isOriginal &&
!selectedSource.isPrettyPrinted &&
!mapScopesEnabled

View file

@ -34,6 +34,7 @@ exports[`SourceFooter Component default case should render 1`] = `
<MenuButton
className="devtools-button debugger-source-map-button disabled not-mapped"
icon={true}
key="debugger-source-map-button"
menuId="debugger-source-map-button"
menuOffset={-5}
menuPosition="bottom"
@ -90,6 +91,7 @@ exports[`SourceFooter Component move cursor should render new cursor position 1`
<MenuButton
className="devtools-button debugger-source-map-button disabled not-mapped"
icon={true}
key="debugger-source-map-button"
menuId="debugger-source-map-button"
menuOffset={-5}
menuPosition="bottom"

View file

@ -76,10 +76,9 @@ export class Outline extends Component {
return {
alphabetizeOutline: PropTypes.bool.isRequired,
cursorPosition: PropTypes.object,
flashLineRange: PropTypes.func.isRequired,
onAlphabetizeClick: PropTypes.func.isRequired,
selectLocation: PropTypes.func.isRequired,
selectedLocation: PropTypes.object.isRequired,
selectedLocation: PropTypes.object,
getFunctionSymbols: PropTypes.func.isRequired,
getClassSymbols: PropTypes.func.isRequired,
selectedSourceTextContent: PropTypes.object,

View file

@ -77,18 +77,8 @@ export class ProjectSearch extends Component {
return {
doSearchForHighlight: PropTypes.func.isRequired,
query: PropTypes.string.isRequired,
results: PropTypes.array.isRequired,
searchSources: PropTypes.func.isRequired,
selectSpecificLocationOrSameUrl: PropTypes.func.isRequired,
status: PropTypes.oneOf([
"INITIAL",
"FETCHING",
"CANCELED",
"DONE",
"ERROR",
]).isRequired,
modifiers: PropTypes.object,
toggleProjectSearchModifier: PropTypes.func,
};
}

View file

@ -52,14 +52,14 @@ class SourcesTree extends Component {
static get propTypes() {
return {
mainThreadHost: PropTypes.string.isRequired,
mainThreadHost: PropTypes.string,
expanded: PropTypes.object.isRequired,
focusItem: PropTypes.func.isRequired,
focused: PropTypes.object,
projectRoot: PropTypes.string.isRequired,
selectSource: PropTypes.func.isRequired,
setExpandedState: PropTypes.func.isRequired,
rootItems: PropTypes.object.isRequired,
rootItems: PropTypes.array.isRequired,
clearProjectDirectoryRoot: PropTypes.func.isRequired,
projectRootName: PropTypes.string.isRequired,
setHideOrShowIgnoredSources: PropTypes.func.isRequired,

View file

@ -26,11 +26,11 @@ class SourceTreeItemContents extends Component {
static get propTypes() {
return {
autoExpand: PropTypes.bool.isRequired,
depth: PropTypes.bool.isRequired,
depth: PropTypes.number.isRequired,
expanded: PropTypes.bool.isRequired,
focusItem: PropTypes.func.isRequired,
focused: PropTypes.bool.isRequired,
hasMatchingGeneratedSource: PropTypes.bool.isRequired,
hasMatchingGeneratedSource: PropTypes.bool,
item: PropTypes.object.isRequired,
selectSourceItem: PropTypes.func.isRequired,
setExpanded: PropTypes.func.isRequired,

View file

@ -400,9 +400,11 @@ export class Tracer extends Component {
const yInSlider = event.clientY - top;
const mousePositionRatio = yInSlider / height;
const index =
this.state.startIndex +
Math.floor(mousePositionRatio * this.state.renderedTraceCount);
// Indexes and ratios are floating number whereas
// we expect to pass an array index to `selectTrace`.
const index = Math.round(
this.state.startIndex + mousePositionRatio * this.state.renderedTraceCount
);
this.props.selectTrace(index);
}

View file

@ -43,7 +43,6 @@ class PrimaryPanes extends Component {
static get propTypes() {
return {
projectRootName: PropTypes.string.isRequired,
selectedTab: PropTypes.oneOf(tabs).isRequired,
setPrimaryPaneTab: PropTypes.func.isRequired,
setActiveSearch: PropTypes.func.isRequired,

View file

@ -93,7 +93,6 @@ class CommandBar extends Component {
isPaused: PropTypes.bool.isRequired,
isWaitingOnBreak: PropTypes.bool.isRequired,
javascriptEnabled: PropTypes.bool.isRequired,
trace: PropTypes.func.isRequired,
resume: PropTypes.func.isRequired,
skipPausing: PropTypes.bool.isRequired,
stepIn: PropTypes.func.isRequired,
@ -105,9 +104,6 @@ class CommandBar extends Component {
toggleSkipPausing: PropTypes.any.isRequired,
toggleSourceMapsEnabled: PropTypes.func.isRequired,
topFrameSelected: PropTypes.bool.isRequired,
logMethod: PropTypes.string.isRequired,
logValues: PropTypes.bool.isRequired,
traceOnNextInteraction: PropTypes.bool.isRequired,
setHideOrShowIgnoredSources: PropTypes.func.isRequired,
toggleSourceMapIgnoreList: PropTypes.func.isRequired,
};

View file

@ -26,7 +26,6 @@ FrameTitle.propTypes = {
frame: PropTypes.object.isRequired,
options: PropTypes.object.isRequired,
l10n: PropTypes.object.isRequired,
showFrameContextMenu: PropTypes.func.isRequired,
};
function getFrameLocation(frame, shouldDisplayOriginalLocation) {
@ -102,6 +101,7 @@ export default class FrameComponent extends Component {
panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
selectFrame: PropTypes.func.isRequired,
selectedFrame: PropTypes.object,
isTracerFrameSelected: PropTypes.bool.isRequired,
shouldMapDisplayName: PropTypes.bool.isRequired,
shouldDisplayOriginalLocation: PropTypes.bool.isRequired,
showFrameContextMenu: PropTypes.func.isRequired,
@ -144,6 +144,7 @@ export default class FrameComponent extends Component {
const {
frame,
selectedFrame,
isTracerFrameSelected,
hideLocation,
shouldMapDisplayName,
displayFullUrl,
@ -155,7 +156,15 @@ export default class FrameComponent extends Component {
const { l10n } = this.context;
const className = classnames("frame", {
selected: selectedFrame && selectedFrame.id === frame.id,
selected:
!isTracerFrameSelected &&
selectedFrame &&
selectedFrame.id === frame.id,
// When a JS Tracer frame is selected, the frame will still be considered as selected,
// and switch from a blue to a grey background. It will still be considered as selected
// from the point of view of stepping buttons.
inactive:
isTracerFrameSelected && selectedFrame && selectedFrame.id === frame.id,
});
const location = getFrameLocation(frame, shouldDisplayOriginalLocation);

View file

@ -99,6 +99,12 @@
color: white;
}
.frames [role="list"] [role="listitem"].inactive,
.frames [role="list"] [role="listitem"].inactive.async-label {
background-color: light-dark(var(--theme-toolbar-background-alt), var(--theme-body-alternate-emphasized-background));
}
.frames [role="list"] [role="listitem"].selected i.annotation-logo svg path {
fill: white;
}

View file

@ -66,6 +66,7 @@ export default class Group extends Component {
selectFrame: PropTypes.func.isRequired,
selectLocation: PropTypes.func,
selectedFrame: PropTypes.object,
isTracerFrameSelected: PropTypes.bool.isRequired,
showFrameContextMenu: PropTypes.func.isRequired,
};
}
@ -91,6 +92,7 @@ export default class Group extends Component {
selectFrame,
selectLocation,
selectedFrame,
isTracerFrameSelected,
displayFullUrl,
getFrameTitle,
disableContextMenu,
@ -115,6 +117,7 @@ export default class Group extends Component {
hideLocation: true,
key: frame.id,
selectedFrame,
isTracerFrameSelected,
selectFrame,
selectLocation,
shouldMapDisplayName: false,

View file

@ -18,6 +18,7 @@ import {
getCurrentThreadFrames,
getCurrentThread,
getShouldSelectOriginalLocation,
getSelectedTraceIndex,
} from "../../../selectors/index";
const NUM_FRAMES_SHOWN = 7;
@ -43,6 +44,7 @@ class Frames extends Component {
selectFrame: PropTypes.func.isRequired,
selectLocation: PropTypes.func,
selectedFrame: PropTypes.object,
isTracerFrameSelected: PropTypes.bool.isRequired,
showFrameContextMenu: PropTypes.func,
shouldDisplayOriginalLocation: PropTypes.bool,
};
@ -52,6 +54,7 @@ class Frames extends Component {
const {
frames,
selectedFrame,
isTracerFrameSelected,
frameworkGroupingOn,
shouldDisplayOriginalLocation,
} = this.props;
@ -59,6 +62,7 @@ class Frames extends Component {
return (
frames !== nextProps.frames ||
selectedFrame !== nextProps.selectedFrame ||
isTracerFrameSelected !== nextProps.isTracerFrameSelected ||
showAllFrames !== nextState.showAllFrames ||
frameworkGroupingOn !== nextProps.frameworkGroupingOn ||
shouldDisplayOriginalLocation !== nextProps.shouldDisplayOriginalLocation
@ -93,6 +97,7 @@ class Frames extends Component {
selectFrame,
selectLocation,
selectedFrame,
isTracerFrameSelected,
displayFullUrl,
getFrameTitle,
disableContextMenu,
@ -119,6 +124,7 @@ class Frames extends Component {
selectFrame,
selectLocation,
selectedFrame,
isTracerFrameSelected,
shouldDisplayOriginalLocation,
key: String(frameOrGroup.id),
displayFullUrl,
@ -132,6 +138,7 @@ class Frames extends Component {
selectFrame,
selectLocation,
selectedFrame,
isTracerFrameSelected,
key: frameOrGroup[0].id,
displayFullUrl,
getFrameTitle,
@ -203,6 +210,7 @@ const mapStateToProps = state => ({
frames: getCurrentThreadFrames(state),
frameworkGroupingOn: getFrameworkGroupingState(state),
selectedFrame: getSelectedFrame(state, getCurrentThread(state)),
isTracerFrameSelected: getSelectedTraceIndex(state) != null,
shouldDisplayOriginalLocation: getShouldSelectOriginalLocation(state),
disableFrameTruncate: false,
disableContextMenu: false,

View file

@ -360,7 +360,7 @@ const mapStateToProps = state => {
let originalFrameScopes;
let generatedFrameScopes;
let isLoading;
let mapScopesEnabled;
let mapScopesEnabled = false;
if (
selectedSource?.isOriginal &&

View file

@ -88,7 +88,7 @@ class SecondaryPanes extends Component {
mapScopesEnabled: PropTypes.bool.isRequired,
pauseReason: PropTypes.string.isRequired,
shouldBreakpointsPaneOpenOnPause: PropTypes.bool.isRequired,
thread: PropTypes.string.isRequired,
thread: PropTypes.string,
renderWhyPauseDelay: PropTypes.number.isRequired,
selectedFrame: PropTypes.object,
skipPausing: PropTypes.bool.isRequired,
@ -184,6 +184,7 @@ class SecondaryPanes extends Component {
return {
header: L10N.getStr("scopes.header"),
className: "scopes-pane",
id: "scopes-pane",
component: React.createElement(Scopes, null),
opened: prefs.scopesVisible,
buttons: this.getScopesButtons(),

View file

@ -26,7 +26,3 @@
.popover:not(.orientation-right) .preview-popup {
margin-left: var(--left-offset);
}
.popover .add-to-expression-bar {
margin-left: var(--left-offset);
}

View file

@ -29,7 +29,6 @@
@import url("chrome://devtools/content/debugger/src/components/Editor/Editor.css");
@import url("chrome://devtools/content/debugger/src/components/Editor/Footer.css");
@import url("chrome://devtools/content/debugger/src/components/Editor/InlinePreview.css");
@import url("chrome://devtools/content/debugger/src/components/Editor/Preview.css");
@import url("chrome://devtools/content/debugger/src/components/Editor/Preview/Popup.css");
@import url("chrome://devtools/content/debugger/src/components/Editor/SearchInFileBar.css");
@import url("chrome://devtools/content/debugger/src/components/Editor/Tabs.css");

View file

@ -58,6 +58,7 @@ const createInitialPauseState = () => ({
previousLocation: null,
expandedScopes: new Set(),
lastExpandedScopes: [],
shouldBreakpointsPaneOpenOnPause: false,
});
export function getThreadPauseState(state, thread) {

View file

@ -238,6 +238,11 @@ export function getSelectedFrameId(state, thread) {
export function isTopFrameSelected(state, thread) {
const selectedFrameId = getSelectedFrameId(state, thread);
// Consider that the top frame is selected when none is specified,
// which happens when a JS Tracer frame is selected.
if (!selectedFrameId) {
return true;
}
const topFrame = getTopFrame(state, thread);
return selectedFrameId == topFrame?.id;
}

View file

@ -40,18 +40,21 @@ add_task(async function () {
bp = findBreakpoint(dbg, "simple2.js", 5);
is(bp.options.condition, "12", "Hit 'Enter' doesn't add a new line");
info("Hit 'Alt+Enter' when the cursor is in the conditional statement");
info("Hit 'Shift+Enter' when the cursor is in the conditional statement");
rightClickElement(dbg, "gutter", 5);
await waitForContextMenu(dbg);
selectContextMenuItem(dbg, `${selectors.editConditionItem}`);
await waitForConditionalPanelFocus(dbg);
// Move one char left to put the cursor between 1 and 2
pressKey(dbg, "Left");
pressKey(dbg, "AltEnter");
// Insert a new line
pressKey(dbg, "ShiftEnter");
// And validate
pressKey(dbg, "Enter");
await waitForCondition(dbg, "1\n2");
bp = findBreakpoint(dbg, "simple2.js", 5);
is(bp.options.condition, "1\n2", "Hit 'Alt+Enter' adds a new line");
is(bp.options.condition, "1\n2", "Hit 'Shift+Enter' adds a new line");
clickElement(dbg, "gutter", 5);
await waitForDispatch(dbg.store, "REMOVE_BREAKPOINT");

View file

@ -42,11 +42,14 @@ add_task(async function () {
invokeInTab("main");
info("Wait for the call tree to appear in the tracer panel");
const tree = await waitForElementWithSelector(dbg, "#tracer-tab-panel .tree");
const tracerTree = await waitForElementWithSelector(
dbg,
"#tracer-tab-panel .tree"
);
info("Wait for the expected traces to appear in the call tree");
const traces = await waitFor(() => {
const elements = tree.querySelectorAll(".trace-line");
let traces = await waitFor(() => {
const elements = tracerTree.querySelectorAll(".trace-line");
if (elements.length == 3) {
return elements;
}
@ -59,6 +62,10 @@ add_task(async function () {
info("Select the trace for the call to `foo`");
EventUtils.synthesizeMouseAtCenter(traces[1], {}, dbg.win);
let focusedTrace = tracerTree.querySelector(".tree-node.focused .trace-line");
is(focusedTrace, traces[1], "The clicked trace is now focused");
// Naive sanity checks for inlines previews
const inlinePreviews = [
{
identifier: "x:",
@ -89,6 +96,76 @@ add_task(async function () {
index++;
}
// Naive sanity checks for popup previews on hovering
{
const { element: popupEl, tokenEl } = await tryHovering(
dbg,
1,
14,
"previewPopup"
);
is(popupEl.querySelector(".objectBox")?.textContent, "1");
await closePreviewForToken(dbg, tokenEl, "previewPopup");
}
{
const { element: popupEl, tokenEl } = await tryHovering(
dbg,
1,
17,
"previewPopup"
);
is(popupEl.querySelector(".objectBox")?.textContent, "2");
await closePreviewForToken(dbg, tokenEl, "previewPopup");
}
let focusedPausedFrame = findElementWithSelector(
dbg,
".frames .frame.selected"
);
ok(!focusedPausedFrame, "Before pausing, there is no selected paused frame");
info("Trigger a breakpoint");
const onResumed = SpecialPowers.spawn(
gBrowser.selectedBrowser,
[],
async function () {
content.eval("debugger;");
}
);
await waitForPaused(dbg);
await waitForSelectedLocation(dbg, 1, 0);
focusedPausedFrame = findElementWithSelector(dbg, ".frames .frame.selected");
ok(
!!focusedPausedFrame,
"When paused, a frame is selected in the call stack panel"
);
focusedTrace = tracerTree.querySelector(".tree-node.focused .trace-line");
is(focusedTrace, null, "When pausing, there is no trace selected anymore");
info("Re select the tracer frame while being paused");
EventUtils.synthesizeMouseAtCenter(traces[1], {}, dbg.win);
await waitForSelectedLocation(dbg, 1, 12);
focusedPausedFrame = findElementWithSelector(dbg, ".frames .frame.selected");
ok(
!focusedPausedFrame,
"While paused, if we select a tracer frame, the paused frame is no longer highlighted in the call stack panel"
);
const highlightedPausedFrame = findElementWithSelector(
dbg,
".frames .frame.inactive"
);
ok(
!!highlightedPausedFrame,
"But it is still highlighted as inactive with a grey background"
);
await resume(dbg);
await onResumed;
// Trigger a click in the content page to verify we do trace DOM events
BrowserTestUtils.synthesizeMouseAtCenter(
"button",
@ -97,13 +174,18 @@ add_task(async function () {
);
const clickTrace = await waitFor(() =>
tree.querySelector(".tracer-dom-event")
tracerTree.querySelector(".tracer-dom-event")
);
is(clickTrace.textContent, "DOM | click");
is(
tracerTree.querySelectorAll(".trace-line").length,
6,
"The click event adds two elements in the tree. The DOM Event and its top frame"
);
await BrowserTestUtils.synthesizeKey("x", {}, gBrowser.selectedBrowser);
const keyTrace = await waitFor(() => {
const elts = tree.querySelectorAll(".tracer-dom-event");
const elts = tracerTree.querySelectorAll(".tracer-dom-event");
if (elts.length == 2) {
return elts[1];
}
@ -111,9 +193,52 @@ add_task(async function () {
});
is(keyTrace.textContent, "DOM | keypress");
// Assert the final content of the tree before stopping
const finalTreeSize = 7;
is(tree.querySelectorAll(".trace-line").length, finalTreeSize);
is(
tracerTree.querySelectorAll(".trace-line").length,
8,
"The key event adds two elements in the tree. The DOM Event and its top frame"
);
info("Trigger a DOM Mutation");
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
content.eval(`
window.doMutation = () => {
const div = document.createElement("div");
document.body.appendChild(div);
//# sourceURL=foo.js
};
`);
content.wrappedJSObject.doMutation();
});
// Wait for the `eval` and the `doMutation` calls to be rendered
traces = await waitFor(() => {
// Scroll to bottom to ensure rendering the last elements (otherwise they are not because of VirtualizedTree)
tracerTree.scrollTop = tracerTree.scrollHeight;
const elements = tracerTree.querySelectorAll(".trace-line");
// Wait for the expected element to be rendered
if (
elements[elements.length - 1].textContent.includes("window.doMutation")
) {
return elements;
}
return false;
});
const doMutationTrace = traces[traces.length - 1];
is(doMutationTrace.textContent, "λ window.doMutation eval:2:32");
// Expand the call to doMutation in order to show the DOM Mutation in the tree
doMutationTrace.querySelector(".arrow").click();
const mutationTrace = await waitFor(() =>
tracerTree.querySelector(".tracer-dom-mutation")
);
is(mutationTrace.textContent, "DOM Mutation | add");
// Click on the mutation trace to open its source
mutationTrace.click();
await waitForSelectedSource(dbg, "foo.js");
// Test Disabling tracing
info("Disable the tracing");

View file

@ -136,9 +136,9 @@ var gDevToolsBrowser = (exports.gDevToolsBrowser = {
},
/**
* This function makes sure that the "devtoolstheme" attribute is set on the browser
* window to make it possible to change colors on elements in the browser (like the
* splitter between the toolbox and web content).
* This function makes sure that the "devtoolstheme" attribute is set on the
* browser window to make it possible to change colors on elements in the
* browser (like the splitter between the toolbox and web content).
*/
updateDevtoolsThemeAttribute(win) {
// Set an attribute on root element of each window to make it possible
@ -147,13 +147,7 @@ var gDevToolsBrowser = (exports.gDevToolsBrowser = {
if (devtoolsTheme != "dark") {
devtoolsTheme = "light";
}
// Style the splitter between the toolbox and page content. This used to
// set the attribute on the browser's root node but that regressed tpaint:
// bug 1331449.
win.document
.getElementById("appcontent")
.setAttribute("devtoolstheme", devtoolsTheme);
win.document.documentElement.setAttribute("devtoolstheme", devtoolsTheme);
},
observe(subject, topic, prefName) {

View file

@ -12,21 +12,21 @@ add_task(async function testDevtoolsTheme() {
info("Checking stylesheet and :root attributes based on devtools theme.");
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
is(
document.getElementById("appcontent").getAttribute("devtoolstheme"),
document.documentElement.getAttribute("devtoolstheme"),
"light",
"The element has an attribute based on devtools theme."
);
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
is(
document.getElementById("appcontent").getAttribute("devtoolstheme"),
document.documentElement.getAttribute("devtoolstheme"),
"dark",
"The element has an attribute based on devtools theme."
);
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "unknown");
is(
document.getElementById("appcontent").getAttribute("devtoolstheme"),
document.documentElement.getAttribute("devtoolstheme"),
"light",
"The element has 'light' as a default for the devtoolstheme attribute."
);

Some files were not shown because too many files have changed in this diff Show more