Backed out 3 changesets (bug 1492797) for perma failures on browser_editor_autocomplete_events.js. CLOSED TREE

Backed out changeset 3bfad9588f0b (bug 1492797)
Backed out changeset 3ecf0fc44704 (bug 1492797)
Backed out changeset a7f6906f69de (bug 1492797)
This commit is contained in:
Razvan Maries 2020-07-21 14:26:49 +03:00
Родитель 754c06d4eb
Коммит 657d7e2c6b
12 изменённых файлов: 64 добавлений и 1017 удалений

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

@ -24,15 +24,9 @@ loader.lazyRequireGetter(
* PageStyleFront, the front object for the PageStyleActor
*/
class PageStyleFront extends FrontClassWithSpec(pageStyleSpec) {
_attributesCache = new Map();
constructor(conn, targetFront, parentFront) {
super(conn, targetFront, parentFront);
this.inspector = this.getParent();
this._clearAttributesCache = this._clearAttributesCache.bind(this);
this.on("stylesheet-updated", this._clearAttributesCache);
this.walker.on("new-mutations", this._clearAttributesCache);
}
form(form) {
@ -82,56 +76,6 @@ class PageStyleFront extends FrontClassWithSpec(pageStyleSpec) {
return ret.entries[0];
});
}
/**
* Get an array of existing attribute values in a node document, given an attribute type.
*
* @param {String} search: A string to filter attribute value on.
* @param {String} attributeType: The type of attribute we want to retrieve the values.
* @param {Element} node: The element we want to get possible attributes for. This will
* be used to get the document where the search is happening.
* @returns {Array<String>} An array of strings
*/
async getAttributesInOwnerDocument(search, attributeType, node) {
if (!attributeType) {
throw new Error("`type` should not be empty");
}
if (!this._form.traits.getAttributesInOwnerDocument || !search) {
return [];
}
const lcFilter = search.toLowerCase();
// If the new filter includes the string that was used on our last trip to the server,
// we can filter the cached results instead of calling the server again.
if (
this._attributesCache &&
this._attributesCache.has(attributeType) &&
search.startsWith(this._attributesCache.get(attributeType).search)
) {
const cachedResults = this._attributesCache
.get(attributeType)
.results.filter(item => item.toLowerCase().startsWith(lcFilter));
this.emitForTests(
"getAttributesInOwnerDocument-cache-hit",
cachedResults
);
return cachedResults;
}
const results = await super.getAttributesInOwnerDocument(
search,
attributeType,
node
);
this._attributesCache.set(attributeType, { search, results });
return results;
}
_clearAttributesCache() {
this._attributesCache.clear();
}
}
exports.PageStyleFront = PageStyleFront;

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

@ -193,24 +193,6 @@ class ClassList {
}
}
}
/**
* Get the available classNames in the document where the current selected node lives:
* - the one already used on elements of the document
* - the one defined in Stylesheets of the document
*
* @param {String} filter: A string the classNames should start with (an insensitive
* case matching will be done).
* @returns {Promise<Array<String>>} A promise that resolves with an array of strings
* matching the passed filter.
*/
getClassNames(filter) {
return this.currentNode.inspectorFront.pageStyle.getAttributesInOwnerDocument(
filter,
"class",
this.currentNode
);
}
}
module.exports = ClassList;

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

@ -5,8 +5,6 @@ support-files =
doc_blob_stylesheet.html
doc_copystyles.css
doc_copystyles.html
doc_class_panel_autocomplete_stylesheet.css
doc_class_panel_autocomplete.html
doc_cssom.html
doc_custom.html
doc_edit_imported_selector.html
@ -43,7 +41,6 @@ skip-if = !debug && ((os == 'linux' && bits == 64 && os_version == '18.04') || o
[browser_rules_authored_override.js]
[browser_rules_blob_stylesheet.js]
[browser_rules_class_panel_add.js]
[browser_rules_class_panel_autocomplete.js]
[browser_rules_class_panel_content.js]
[browser_rules_class_panel_edit.js]
[browser_rules_class_panel_invalid_nodes.js]

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

@ -1,226 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the autocomplete for the class panel input behaves as expected. The test also
// checks that we're using the cache to retrieve the data when we can do so, and that the
// cache gets cleared, and we're getting data from the server, when there's mutation on
// the page.
const TEST_URI = `${URL_ROOT}doc_class_panel_autocomplete.html`;
add_task(async function() {
await addTab(TEST_URI);
const { inspector, view } = await openRuleView();
const { addEl: textInput } = view.classListPreviewer;
info("Open the class panel");
view.showClassPanel();
textInput.focus();
info("Type a letter and check that the popup has the expected items");
const allClasses = [
"auto-body-class-1",
"auto-body-class-2",
"auto-bold",
"auto-cssom-primary-color",
"auto-div-class-1",
"auto-div-class-2",
"auto-html-class-1",
"auto-html-class-2",
"auto-inline-class-1",
"auto-inline-class-2",
"auto-inline-class-3",
"auto-inline-class-4",
"auto-inline-class-5",
"auto-stylesheet-class-1",
"auto-stylesheet-class-2",
"auto-stylesheet-class-3",
"auto-stylesheet-class-4",
"auto-stylesheet-class-5",
];
const { autocompletePopup } = view.classListPreviewer;
let onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("a", {}, view.styleWindow);
await onPopupOpened;
await checkAutocompleteItems(
autocompletePopup,
allClasses,
"The autocomplete popup has all the classes used in the DOM and in stylesheets"
);
info(
"Test that typing more letters filters the autocomplete popup and uses the cache mechanism"
);
const onCacheHit = inspector.inspectorFront.pageStyle.once(
"getAttributesInOwnerDocument-cache-hit"
);
EventUtils.sendString("uto-b", view.styleWindow);
await checkAutocompleteItems(
autocompletePopup,
allClasses.filter(cls => cls.startsWith("auto-b")),
"The autocomplete popup was filtered with the content of the input"
);
await onCacheHit;
ok(true, "The results were retrieved from the cache mechanism");
info("Test that autocomplete shows up-to-date results");
// Modify the content page and assert that the new class is displayed in the
// autocomplete if the user types a new letter.
const onNewMutation = inspector.inspectorFront.walker.once("new-mutations");
await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
content.document.body.classList.add("auto-body-added-by-script");
});
await onNewMutation;
// input is now auto-body
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.sendString("ody", view.styleWindow);
await onPopupOpened;
await checkAutocompleteItems(
autocompletePopup,
[
...allClasses.filter(cls => cls.startsWith("auto-body")),
"auto-body-added-by-script",
].sort(),
"The autocomplete popup was filtered with the content of the input"
);
info(
"Test that typing a letter that won't match any of the item closes the popup"
);
// input is now auto-bodyy
let onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("y", {}, view.styleWindow);
await onPopupClosed;
ok(true, "The popup was closed as expected");
await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared");
info("Clear the input and try to autocomplete again");
textInput.select();
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
// Wait a bit so the debounced function can be executed
await wait(200);
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("a", {}, view.styleWindow);
await onPopupOpened;
await checkAutocompleteItems(
autocompletePopup,
[...allClasses, "auto-body-added-by-script"].sort(),
"The autocomplete popup was updated with the new class added to the DOM"
);
info("Test keyboard shortcut when the popup is displayed");
// Escape to hide
onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
await onPopupClosed;
ok(true, "The popup was closed when hitting escape");
// Ctrl + space to show again
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
await onPopupOpened;
ok(true, "Popup was opened again with Ctrl+Space");
await checkAutocompleteItems(
autocompletePopup,
[...allClasses, "auto-body-added-by-script"].sort()
);
// Arrow left to hide
onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_ArrowLeft", {}, view.styleWindow);
await onPopupClosed;
ok(true, "The popup was closed as when hitting ArrowLeft");
// Arrow right and Ctrl + space to show again, and Arrow Right to accept
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
await onPopupOpened;
onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
await onPopupClosed;
is(
textInput.value,
"auto-body-added-by-script",
"ArrowRight puts the selected item in the input and closes the popup"
);
// Backspace to show the list again
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
await onPopupOpened;
is(
textInput.value,
"auto-body-added-by-scrip",
"ArrowRight puts the selected item in the input and closes the popup"
);
await checkAutocompleteItems(
autocompletePopup,
["auto-body-added-by-script"],
"The autocomplete does show the matching items after hitting backspace"
);
// Enter to accept
onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_Enter", {}, view.styleWindow);
await onPopupClosed;
is(
textInput.value,
"auto-body-added-by-script",
"Enter puts the selected item in the input and closes the popup"
);
// Backspace to show again
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
await onPopupOpened;
is(
textInput.value,
"auto-body-added-by-scrip",
"ArrowRight puts the selected item in the input and closes the popup"
);
await checkAutocompleteItems(
autocompletePopup,
["auto-body-added-by-script"],
"The autocomplete does show the matching items after hitting backspace"
);
// Tab to accept
onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_Tab", {}, view.styleWindow);
await onPopupClosed;
is(
textInput.value,
"auto-body-added-by-script",
"Tab puts the selected item in the input and closes the popup"
);
});
async function checkAutocompleteItems(
autocompletePopup,
expectedItems,
assertionMessage
) {
await waitForSuccess(
() =>
getAutocompleteItems(autocompletePopup).length === expectedItems.length
);
const items = getAutocompleteItems(autocompletePopup);
const formatList = list => `\n${list.join("\n")}\n`;
is(formatList(items), formatList(expectedItems), assertionMessage);
}
function getAutocompleteItems(autocompletePopup) {
return Array.from(autocompletePopup._panel.querySelectorAll("li")).map(
el => el.textContent
);
}

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

@ -1,40 +0,0 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<html class="auto-html-class-1 auto-html-class-2 auto-bold">
<head>
<title>Class panel autocomplete test</title>
<link href="./doc_class_panel_autocomplete_stylesheet.css" rel="stylesheet" type="text/css">
<style>
.auto-inline-class-1 {
padding: 1em;
}
.auto-inline-class-2 {
padding: 2em;
}
.auto-inline-class-3 {
padding: 3em;
}
.auto-inline-class-1,
div.auto-inline-class-2,
p:first-of-type.auto-inline-class-3,
.auto-inline-class-4 {
background-color: blue;
}
:root .auto-bold .auto-inline-class-5 {
font-size: bold;
}
</style>
<script defer>
"use strict";
const x = document.styleSheets[0];
x.insertRule(".auto-cssom-primary-color { color: tomato; }", 1);
</script>
</head>
<body class="auto-body-class-1 auto-body-class-2 auto-bold">
<div id="auto-div-id-1" class="auto-div-class-1 auto-div-class-2 auto-bold"> the ocean </div>
<div id="auto-div-id-2" class="auto-div-class-1 auto-div-class-2 auto-bold"> roaring </div>
</body>
</html>

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

@ -1,20 +0,0 @@
.auto-stylesheet-class-1 {
padding: 1em;
}
.auto-stylesheet-class-2 {
padding: 2em;
}
.auto-stylesheet-class-3 {
padding: 3em;
}
.auto-stylesheet-class-1,
div.auto-stylesheet-class-2,
p:first-of-type.auto-stylesheet-class-3,
.auto-stylesheet-class-4 {
background-color: blue;
}
:root .auto-bold .auto-stylesheet-class-5 {
font-size: bold;
}

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

@ -5,89 +5,61 @@
"use strict";
const ClassList = require("devtools/client/inspector/rules/models/class-list");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N = new LocalizationHelper(
"devtools/client/locales/inspector.properties"
);
const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
const { debounce } = require("devtools/shared/debounce");
/**
* This UI widget shows a textfield and a series of checkboxes in the rule-view. It is
* used to toggle classes on the current node selection, and add new classes.
*
* @param {Inspector} inspector
* The current inspector instance.
* @param {DomNode} containerEl
* The element in the rule-view where the widget should go.
*/
class ClassListPreviewer {
/*
* @param {Inspector} inspector
* The current inspector instance.
* @param {DomNode} containerEl
* The element in the rule-view where the widget should go.
*/
constructor(inspector, containerEl) {
this.inspector = inspector;
this.containerEl = containerEl;
this.model = new ClassList(inspector);
function ClassListPreviewer(inspector, containerEl) {
this.inspector = inspector;
this.containerEl = containerEl;
this.model = new ClassList(inspector);
this.onNewSelection = this.onNewSelection.bind(this);
this.onCheckBoxChanged = this.onCheckBoxChanged.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onAddElementInputModified = debounce(
this.onAddElementInputModified,
75,
this
);
this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
this.onNewSelection = this.onNewSelection.bind(this);
this.onCheckBoxChanged = this.onCheckBoxChanged.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
// Create the add class text field.
this.addEl = this.doc.createElement("input");
this.addEl.classList.add("devtools-textinput");
this.addEl.classList.add("add-class");
this.addEl.setAttribute(
"placeholder",
L10N.getStr("inspector.classPanel.newClass.placeholder")
);
this.addEl.addEventListener("keypress", this.onKeyPress);
this.addEl.addEventListener("input", this.onAddElementInputModified);
this.containerEl.appendChild(this.addEl);
// Create the add class text field.
this.addEl = this.doc.createElement("input");
this.addEl.classList.add("devtools-textinput");
this.addEl.classList.add("add-class");
this.addEl.setAttribute(
"placeholder",
L10N.getStr("inspector.classPanel.newClass.placeholder")
);
this.addEl.addEventListener("keypress", this.onKeyPress);
this.containerEl.appendChild(this.addEl);
// Create the class checkboxes container.
this.classesEl = this.doc.createElement("div");
this.classesEl.classList.add("classes");
this.containerEl.appendChild(this.classesEl);
// Create the class checkboxes container.
this.classesEl = this.doc.createElement("div");
this.classesEl.classList.add("classes");
this.containerEl.appendChild(this.classesEl);
// Create the autocomplete popup
this.autocompletePopup = new AutocompletePopup(this.inspector.toolbox.doc, {
listId: "inspector_classListPreviewer_autocompletePopupListBox",
position: "bottom",
autoSelect: false,
useXulWrapper: true,
input: this.addEl,
onClick: (e, item) => {
if (item) {
this.addEl.value = item.label;
this.autocompletePopup.hidePopup();
this.autocompletePopup.clearItems();
}
},
});
// Start listening for interesting events.
this.inspector.selection.on("new-node-front", this.onNewSelection);
this.containerEl.addEventListener("input", this.onCheckBoxChanged);
this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
// Start listening for interesting events.
this.inspector.selection.on("new-node-front", this.onNewSelection);
this.containerEl.addEventListener("input", this.onCheckBoxChanged);
this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
this.onNewSelection();
}
this.onNewSelection();
}
ClassListPreviewer.prototype = {
destroy() {
this.inspector.selection.off("new-node-front", this.onNewSelection);
this.addEl.removeEventListener("keypress", this.onKeyPress);
this.addEl.removeEventListener("input", this.onAddElementInputModified);
this.containerEl.removeEventListener("input", this.onCheckBoxChanged);
this.autocompletePopup.destroy();
this.containerEl.innerHTML = "";
this.model.destroy();
@ -95,11 +67,11 @@ class ClassListPreviewer {
this.inspector = null;
this.addEl = null;
this.classesEl = null;
}
},
get doc() {
return this.containerEl.ownerDocument;
}
},
/**
* Render the content of the panel. You typically don't need to call this as the panel
@ -116,7 +88,7 @@ class ClassListPreviewer {
if (!this.model.currentClasses.length) {
this.classesEl.appendChild(this.renderNoClassesMessage());
}
}
},
/**
* Render a single checkbox for a given classname.
@ -145,7 +117,7 @@ class ClassListPreviewer {
labelWrapper.appendChild(label);
return labelWrapper;
}
},
/**
* Render the message displayed in the panel when the current element has no classes.
@ -157,7 +129,7 @@ class ClassListPreviewer {
msg.classList.add("no-classes");
msg.textContent = L10N.getStr("inspector.classPanel.noClasses");
return msg;
}
},
/**
* Focus the add-class text field.
@ -166,7 +138,7 @@ class ClassListPreviewer {
if (this.addEl) {
this.addEl.focus();
}
}
},
onCheckBoxChanged({ target }) {
if (!target.dataset.name) {
@ -179,88 +151,34 @@ class ClassListPreviewer {
console.error(e);
}
});
}
},
onKeyPress(event) {
// If the popup is already open, all the keyboard interaction are handled
// directly by the popup component.
if (this.autocompletePopup.isOpen) {
if (event.key !== "Enter" || this.addEl.value === "") {
return;
}
// Open the autocomplete popup on Ctrl+Space / ArrowDown (when the input isn't empty)
if (
(this.addEl.value && event.key === " " && event.ctrlKey) ||
event.key === "ArrowDown"
) {
this.onAddElementInputModified();
return;
}
if (this.addEl.value !== "" && event.key === "Enter") {
this.addClassName(this.addEl.value);
}
}
async onAddElementInputModified() {
const newValue = this.addEl.value;
// if the input is empty, let's close the popup, if it was open.
if (newValue === "") {
if (this.autocompletePopup.isOpen) {
this.autocompletePopup.hidePopup();
this.autocompletePopup.clearItems();
}
return;
}
// Otherwise, we need to update the popup items to match the new input.
let items = [];
try {
const classNames = await this.model.getClassNames(newValue);
items = classNames.map(className => {
return {
preLabel: className.substring(0, newValue.length),
label: className,
};
this.model
.addClassName(this.addEl.value)
.then(() => {
this.render();
this.addEl.value = "";
})
.catch(e => {
// Only log the error if the panel wasn't destroyed in the meantime.
if (this.containerEl) {
console.error(e);
}
});
} catch (e) {
// If there was an error while retrieving the classNames, we silently bail out;
// we'll simply NOT show the popup, which is okay.
}
if (
items.length == 0 ||
(items.length == 1 && items[0].label === newValue)
) {
this.autocompletePopup.clearItems();
this.autocompletePopup.hidePopup();
} else {
this.autocompletePopup.setItems(items);
this.autocompletePopup.openPopup();
}
}
async addClassName(className) {
try {
await this.model.addClassName(className);
this.render();
this.addEl.value = "";
} catch (e) {
// Only log the error if the panel wasn't destroyed in the meantime.
if (this.containerEl) {
console.error(e);
}
}
}
},
onNewSelection() {
this.render();
}
},
onCurrentNodeClassChanged() {
this.render();
}
}
},
};
module.exports = ClassListPreviewer;

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

@ -25,7 +25,7 @@ let itemIdCounter = 0;
* The toolbox document to attach the autocomplete popup panel.
* @param {Object} options
* An object consiting any of the following options:
* - listId {String} The id for the list <UL> element.
* - listId {String} The id for the list <LI> element.
* - position {String} The position for the tooltip ("top" or "bottom").
* - useXulWrapper {Boolean} If the tooltip is hosted in a XUL document, use a
* XUL panel in order to use all the screen viewport available (defaults to false).
@ -34,9 +34,6 @@ let itemIdCounter = 0;
* - onSelect {String} Callback called when the selected index is updated.
* - onClick {String} Callback called when the autocomplete popup receives a click
* event. The selectedIndex will already be updated if need be.
* - input {Element} Optional input element the popup will be bound to. If provided
* the event listeners for navigating the autocomplete list are going to be
* automatically added.
*/
function AutocompletePopup(toolboxDoc, options = {}) {
EventEmitter.decorate(this);
@ -58,14 +55,6 @@ function AutocompletePopup(toolboxDoc, options = {}) {
this.selectedIndex = -1;
this.onClick = this.onClick.bind(this);
this.onInputKeyDown = this.onInputKeyDown.bind(this);
this.onInputBlur = this.onInputBlur.bind(this);
if (options.input) {
this.input = options.input;
options.input.addEventListener("keydown", this.onInputKeyDown);
options.input.addEventListener("blur", this.onInputBlur);
}
}
AutocompletePopup.prototype = {
@ -126,66 +115,6 @@ AutocompletePopup.prototype = {
return this._tooltip;
},
onInputKeyDown: function(event) {
// Only handle the even if the popup is opened.
if (!this.isOpen) {
return;
}
if (
this.selectedItem &&
this.onClickCallback &&
(event.key === "Enter" ||
(event.key === "ArrowRight" && !event.shiftKey) ||
(event.key === "Tab" && !event.shiftKey))
) {
this.onClickCallback(event, this.selectedItem);
// Prevent the associated keypress to be triggered.
event.preventDefault();
event.stopPropagation();
return;
}
// Close the popup when the user hit Left Arrow, but let the keypress be triggered
// so the cursor moves as the user wanted.
if (event.key === "ArrowLeft" && !event.shiftKey) {
this.clearItems();
this.hidePopup();
return;
}
// Close the popup when the user hit Escape.
if (event.key === "Escape") {
this.clearItems();
this.hidePopup();
// Prevent the associated keypress to be triggered.
event.preventDefault();
event.stopPropagation();
return;
}
if (event.key === "ArrowDown") {
this.selectNextItem();
event.preventDefault();
event.stopPropagation();
return;
}
if (event.key === "ArrowUp") {
this.selectPreviousItem();
event.preventDefault();
event.stopPropagation();
}
},
onInputBlur: function(event) {
if (this.isOpen) {
this.clearItems();
this.hidePopup();
}
},
onSelect: function(e) {
if (this.onSelectCallback) {
this.onSelectCallback(e);
@ -193,22 +122,14 @@ AutocompletePopup.prototype = {
},
onClick: function(e) {
const itemEl = e.target.closest(".autocomplete-item");
const index = itemEl?.dataset?.index;
if (typeof index !== "undefined") {
this.selectItemAtIndex(index);
const item = e.target.closest(".autocomplete-item");
if (item && typeof item.dataset.index !== "undefined") {
this.selectItemAtIndex(parseInt(item.dataset.index, 10));
}
this.emit("popup-click");
const item =
typeof index !== "undefined"
? this.items[parseInt(itemEl.dataset.index, 10)]
: null;
if (this.onClickCallback) {
this.onClickCallback(e, item);
this.onClickCallback(e);
}
},
@ -216,7 +137,7 @@ AutocompletePopup.prototype = {
* Open the autocomplete popup panel.
*
* @param {Node} anchor
* Optional node to anchor the panel to. Will default to this.input if it exists.
* Optional node to anchor the panel to.
* @param {Number} xOffset
* Horizontal offset in pixels from the left of the node to the left
* of the popup.
@ -228,10 +149,6 @@ AutocompletePopup.prototype = {
* @param {Object} options: Check `selectItemAtIndex` for more information.
*/
openPopup: async function(anchor, xOffset = 0, yOffset = 0, index, options) {
if (!anchor && this.input) {
anchor = this.input;
}
// Retrieve the anchor's document active element to add accessibility metadata.
this._activeElement = anchor.ownerDocument.activeElement;
@ -355,12 +272,6 @@ AutocompletePopup.prototype = {
this._tooltip = null;
}
if (this.input) {
this.input.addEventListener("keydown", this.onInputKeyDown);
this.input.addEventListener("blur", this.onInputBlur);
this.input = null;
}
this._document = null;
},

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

@ -67,7 +67,6 @@ support-files =
!/gfx/layers/apz/test/mochitest/apz_test_utils.js
[browser_autocomplete_popup_consecutive-show.js]
[browser_autocomplete_popup_input.js]
[browser_autocomplete_popup.js]
[browser_browserloader_mocks.js]
[browser_css_angle.js]

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

@ -1,241 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function() {
const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
info("Create an autocompletion popup and an input that will be bound to it");
const { doc } = await createHost();
const input = doc.createElement("input");
doc.body.appendChild(input);
const onSelectCalled = [];
const onClickCalled = [];
const popup = new AutocompletePopup(doc, {
input,
position: "top",
autoSelect: true,
onSelect: item => onSelectCalled.push(item),
onClick: (e, item) => onClickCalled.push(item),
});
input.focus();
info(
"Check that Tab moves the focus out of the input when the popup isn't opened"
);
EventUtils.synthesizeKey("KEY_Tab");
is(onClickCalled.length, 0, "onClick wasn't called");
is(hasFocus(input), false, "input does not have the focus anymore");
info("Set the focus back to the input and open the popup");
input.focus();
await populateAndOpenPopup(popup);
const checkSelectedItem = (expected, info) =>
checkPopupSelectedItem(popup, input, expected, info);
checkSelectedItem(popupItems[0], "First item from top is selected");
is(
onSelectCalled[0],
popupItems[0],
"onSelect was called with expected param"
);
info("Check that arrow down/up navigates into the list");
EventUtils.synthesizeKey("KEY_ArrowDown");
checkSelectedItem(popupItems[1], "item-1 is selected");
is(
onSelectCalled[1],
popupItems[1],
"onSelect was called with expected param"
);
EventUtils.synthesizeKey("KEY_ArrowDown");
checkSelectedItem(popupItems[2], "item-2 is selected");
is(
onSelectCalled[2],
popupItems[2],
"onSelect was called with expected param"
);
EventUtils.synthesizeKey("KEY_ArrowDown");
checkSelectedItem(popupItems[0], "item-0 is selected");
is(
onSelectCalled[3],
popupItems[0],
"onSelect was called with expected param"
);
EventUtils.synthesizeKey("KEY_ArrowUp");
checkSelectedItem(popupItems[2], "item-2 is selected");
is(
onSelectCalled[4],
popupItems[2],
"onSelect was called with expected param"
);
EventUtils.synthesizeKey("KEY_ArrowUp");
checkSelectedItem(popupItems[1], "item-2 is selected");
is(
onSelectCalled[5],
popupItems[1],
"onSelect was called with expected param"
);
info("Check that Escape closes the popup");
let onPopupClosed = popup.once("popup-closed");
EventUtils.synthesizeKey("KEY_Escape");
await onPopupClosed;
ok(true, "popup was closed with Escape key");
ok(hasFocus(input), "input still has the focus");
is(onClickCalled.length, 0, "onClick wasn't called");
info("Fill the input");
const value = "item";
EventUtils.sendString(value);
is(input.value, value, "input has the expected value");
is(
input.selectionStart,
value.length,
"input cursor is at expected position"
);
info("Open the popup again");
await populateAndOpenPopup(popup);
info("Check that Arrow Left + Shift does not close the popup");
const timeoutRes = "TIMED_OUT";
const onRaceEnded = Promise.race([
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(res => setTimeout(() => res(timeoutRes), 500)),
popup.once("popup-closed"),
]);
EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
const raceResult = await onRaceEnded;
is(raceResult, timeoutRes, "popup wasn't closed");
ok(popup.isOpen, "popup is still open");
is(input.selectionEnd - input.selectionStart, 1, "text was selected");
ok(hasFocus(input), "input still has the focus");
info("Check that Arrow Left closes the popup");
onPopupClosed = popup.once("popup-closed");
EventUtils.synthesizeKey("KEY_ArrowLeft");
await onPopupClosed;
is(
input.selectionStart,
value.length - 1,
"input cursor was moved one char back"
);
is(input.selectionEnd, input.selectionStart, "selection was removed");
is(onClickCalled.length, 0, "onClick wasn't called");
ok(hasFocus(input), "input still has the focus");
info("Open the popup again");
await populateAndOpenPopup(popup);
info("Check that Arrow Right + Shift does not trigger onClick");
EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
is(onClickCalled.length, 0, "onClick wasn't called");
is(input.selectionEnd - input.selectionStart, 1, "input text was selected");
ok(hasFocus(input), "input still has the focus");
info("Check that Arrow Right triggers onClick");
EventUtils.synthesizeKey("KEY_ArrowRight");
is(onClickCalled.length, 1, "onClick was called");
is(
onClickCalled[0],
popupItems[0],
"onClick was called with the selected item"
);
ok(hasFocus(input), "input still has the focus");
info("Check that Enter triggers onClick");
EventUtils.synthesizeKey("KEY_Enter");
is(onClickCalled.length, 2, "onClick was called");
is(
onClickCalled[1],
popupItems[0],
"onClick was called with the selected item"
);
ok(hasFocus(input), "input still has the focus");
info("Check that Tab triggers onClick");
EventUtils.synthesizeKey("KEY_Tab");
is(onClickCalled.length, 3, "onClick was called");
is(
onClickCalled[2],
popupItems[0],
"onClick was called with the selected item"
);
ok(hasFocus(input), "input still has the focus");
info(
"Check that Shift+Tab does not trigger onClick and move the focus out of the input"
);
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
is(onClickCalled.length, 3, "onClick wasn't called");
is(hasFocus(input), false, "input does not have the focus anymore");
const onPopupClose = popup.once("popup-closed");
popup.hidePopup();
await onPopupClose;
});
const popupItems = [
{ label: "item-0", value: "value-0" },
{ label: "item-1", value: "value-1" },
{ label: "item-2", value: "value-2" },
];
function populateAndOpenPopup(popup) {
popup.setItems(popupItems);
info("Open popup");
const onPopupOpen = popup.once("popup-opened");
popup.openPopup();
return onPopupOpen;
}
/**
* Returns true if the give node is currently focused.
*/
function hasFocus(node) {
return (
node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
);
}
/**
*
* @param {*} popup
* @param {*} input
* @param {*} expectedSelectedItem
* @param {*} info
*/
function checkPopupSelectedItem(popup, input, expectedSelectedItem, info) {
is(popup.selectedItem, expectedSelectedItem, info);
checkActiveDescendant(popup, input);
ok(hasFocus(input), "input still has the focus");
}
function checkActiveDescendant(popup, input) {
const activeElement = input.ownerDocument.activeElement;
const descendantId = activeElement.getAttribute("aria-activedescendant");
const popupItem = popup._tooltip.panel.querySelector("#" + descendantId);
const cloneItem = input.ownerDocument.querySelector("#" + descendantId);
ok(popupItem, "Active descendant is found in the popup list");
ok(cloneItem, "Active descendant is found in the list clone");
is(
stripNS(popupItem.outerHTML),
cloneItem.outerHTML,
"Cloned item has the same HTML as the original element"
);
}
function stripNS(text) {
return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");
}

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

@ -202,8 +202,6 @@ var PageStyleActor = protocol.ActorClassWithSpec(pageStyleSpec, {
// expected support of font-stretch at CSS Fonts Level 4.
fontWeightLevel4:
CSS.supports("font-weight: 1") && CSS.supports("font-stretch: 100%"),
// Introduced in Firefox 80.
getAttributesInOwnerDocument: true,
},
};
},
@ -1202,171 +1200,6 @@ var PageStyleActor = protocol.ActorClassWithSpec(pageStyleSpec, {
rule.refresh();
}
},
/**
* Get an array of existing attribute values in a node document.
*
* @param {String} search: A string to filter attribute value on.
* @param {String} attributeType: The type of attribute we want to retrieve the values.
* @param {Element} node: The element we want to get possible attributes for. This will
* be used to get the document where the search is happening.
* @returns {Array<String>} An array of strings
*/
getAttributesInOwnerDocument(search, attributeType, node) {
if (!search) {
throw new Error("search is mandatory");
}
// In a non-fission world, a node from an iframe shares the same `rootNode` as a node
// in the top-level document. So here we need to retrieve the document from the node
// in parameter in order to retrieve the right document.
// This may change once we have a dedicated walker for every target in a tab, as we'll
// be able to directly talk to the "right" walker actor.
const targetDocument = node.rawNode.ownerDocument;
// We store the result in a Set which will contain the attribute value
const result = new Set();
const lcSearch = search.toLowerCase();
this._collectAttributesFromDocumentDOM(
result,
lcSearch,
attributeType,
targetDocument
);
this._collectAttributesFromDocumentStyleSheets(
result,
lcSearch,
attributeType,
targetDocument
);
return Array.from(result).sort();
},
/**
* Collect attribute values from the document DOM tree, matching the passed filter and
* type, to the result Set.
*
* @param {Set<String>} result: A Set to which the results will be added.
* @param {String} search: A string to filter attribute value on.
* @param {String} attributeType: The type of attribute we want to retrieve the values.
* @param {Document} targetDocument: The document the search occurs in.
*/
_collectAttributesFromDocumentDOM(
result,
search,
attributeType,
targetDocument
) {
// In order to retrieve attributes from DOM elements in the document, we're going to
// do a query on the root node using attributes selector, to directly get the elements
// matching the attributes we're looking for.
// For classes, we need something a bit different as the className we're looking
// for might not be the first in the attribute value, meaning we can't use the
// "attribute starts with X" selector.
const attributeSelectorPositionChar = attributeType === "class" ? "*" : "^";
const selector = `[${attributeType}${attributeSelectorPositionChar}=${search} i]`;
const matchingElements = targetDocument.querySelectorAll(selector);
for (const element of matchingElements) {
// For class attribute, we need to add the elements of the classList that match
// the filter string.
if (attributeType === "class") {
for (const cls of element.classList) {
if (!result.has(cls) && cls.toLowerCase().startsWith(search)) {
result.add(cls);
}
}
} else {
const { value } = element.attributes[attributeType];
// For other attributes, we can directly use the attribute value.
result.add(value);
}
}
},
/**
* Collect attribute values from the document stylesheets, matching the passed filter
* and type, to the result Set.
*
* @param {Set<String>} result: A Set to which the results will be added.
* @param {String} search: A string to filter attribute value on.
* @param {String} attributeType: The type of attribute we want to retrieve the values.
* It only supports "class" and "id" at the moment.
* @param {Document} targetDocument: The document the search occurs in.
*/
_collectAttributesFromDocumentStyleSheets(
result,
search,
attributeType,
targetDocument
) {
if (attributeType !== "class" && attributeType !== "id") {
return;
}
// We loop through all the stylesheets and their rules, and then use the lexer to only
// get the attributes we're looking for.
for (const styleSheet of targetDocument.styleSheets) {
for (const rule of styleSheet.rules) {
this._collectAttributesFromRule(result, rule, search, attributeType);
}
}
},
/**
* Collect attribute values from the rule, matching the passed filter and type, to the
* result Set.
*
* @param {Set<String>} result: A Set to which the results will be added.
* @param {Rule} rule: The rule the search occurs in.
* @param {String} search: A string to filter attribute value on.
* @param {String} attributeType: The type of attribute we want to retrieve the values.
* It only supports "class" and "id" at the moment.
*/
_collectAttributesFromRule(result, rule, search, attributeType) {
const shouldRetrieveClasses = attributeType === "class";
const shouldRetrieveIds = attributeType === "id";
const { selectorText } = rule;
// If there's no selectorText, or if the selectorText does not include the
// filter, we can bail out.
if (!selectorText || !selectorText.toLowerCase().includes(search)) {
return;
}
// Check if we should parse the selectorText (do we need to check for class/id and
// if so, does the selector contains class/id related chars).
const parseForClasses =
shouldRetrieveClasses &&
selectorText.toLowerCase().includes(`.${search}`);
const parseForIds =
shouldRetrieveIds && selectorText.toLowerCase().includes(`#${search}`);
if (!parseForClasses && !parseForIds) {
return;
}
const lexer = getCSSLexer(selectorText);
let token;
while ((token = lexer.nextToken())) {
if (
token.tokenType === "symbol" &&
((shouldRetrieveClasses && token.text === ".") ||
(shouldRetrieveIds && token.text === "#"))
) {
token = lexer.nextToken();
if (
token.tokenType === "ident" &&
token.text.toLowerCase().startsWith(search)
) {
result.add(token.text);
}
}
}
},
});
exports.PageStyleActor = PageStyleActor;

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

@ -188,16 +188,6 @@ const pageStyleSpec = generateActorSpec({
},
response: RetVal("appliedStylesReturn"),
},
getAttributesInOwnerDocument: {
request: {
search: Arg(0, "string"),
attributeType: Arg(1, "string"),
node: Arg(2, "nullable:domnode"),
},
response: {
attributes: RetVal("array:string"),
},
},
},
});