diff options
Diffstat (limited to 'devtools/client/inspector/test')
231 files changed, 21305 insertions, 0 deletions
diff --git a/devtools/client/inspector/test/browser.toml b/devtools/client/inspector/test/browser.toml new file mode 100644 index 0000000000..eaa54c31f8 --- /dev/null +++ b/devtools/client/inspector/test/browser.toml @@ -0,0 +1,445 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "doc_inspector_add_node.html", + "doc_inspector_breadcrumbs.html", + "doc_inspector_breadcrumbs_visibility.html", + "doc_inspector_csp.html", + "doc_inspector_csp.html^headers^", + "doc_inspector_delete-selected-node-01.html", + "doc_inspector_delete-selected-node-02.html", + "doc_inspector_embed.html", + "doc_inspector_eyedropper_disabled.xhtml", + "doc_inspector_fission_frame_navigation.html", + "doc_inspector_highlight_after_transition.html", + "doc_inspector_highlighter-comments.html", + "doc_inspector_highlighter-geometry_01.html", + "doc_inspector_highlighter-geometry_02.html", + "doc_inspector_highlighter_cssshapes.html", + "doc_inspector_highlighter_cssshapes-percent.html", + "doc_inspector_highlighter_cssshapes_iframe.html", + "doc_inspector_highlighter_csstransform.html", + "doc_inspector_highlighter_dom.html", + "doc_inspector_highlighter_inline.html", + "doc_inspector_highlighter.html", + "doc_inspector_highlighter_rect.html", + "doc_inspector_highlighter_rect_iframe.html", + "doc_inspector_highlighter_scroll.html", + "doc_inspector_highlighter_custom_element.xhtml", + "doc_inspector_infobar_01.html", + "doc_inspector_infobar_02.html", + "doc_inspector_infobar_03.html", + "doc_inspector_infobar_04.html", + "doc_inspector_infobar_textnode.html", + "doc_inspector_long-divs.html", + "doc_inspector_menu.html", + "doc_inspector_outerhtml.html", + "doc_inspector_pane-toggle-layout-invariant.html", + "doc_inspector_reload_xul.xhtml", + "doc_inspector_remove-iframe-during-load.html", + "doc_inspector_search.html", + "doc_inspector_search-iframes.html", + "doc_inspector_search-reserved.html", + "doc_inspector_search-suggestions.html", + "doc_inspector_search-svg.html", + "doc_inspector_select-last-selected-01.html", + "doc_inspector_select-last-selected-02.html", + "doc_inspector_svg.svg", + "head.js", + "img_browser_inspector_highlighter-eyedropper-image.png", + "shared-head.js", + "sjs_slow-loading-image.sjs", + "style_inspector_eyedropper_ruleview.css", + "style_inspector_csp.css", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/debugger/test/mochitest/shared-head.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_inspector_addNode_01.js"] + +["browser_inspector_addNode_02.js"] + +["browser_inspector_addNode_03.js"] + +["browser_inspector_addSidebarTab.js"] + +["browser_inspector_breadcrumbs.js"] + +["browser_inspector_breadcrumbs_highlight_hover.js"] + +["browser_inspector_breadcrumbs_keybinding.js"] + +["browser_inspector_breadcrumbs_keyboard_trap.js"] +skip-if = ["os == 'mac'"] # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences + +["browser_inspector_breadcrumbs_mutations.js"] + +["browser_inspector_breadcrumbs_namespaced.js"] + +["browser_inspector_breadcrumbs_shadowdom.js"] + +["browser_inspector_breadcrumbs_visibility.js"] + +["browser_inspector_delete-selected-node-01.js"] + +["browser_inspector_delete-selected-node-02.js"] + +["browser_inspector_delete-selected-node-03.js"] + +["browser_inspector_delete_node_in_frame.js"] + +["browser_inspector_destroy-after-navigation.js"] + +["browser_inspector_destroy-before-ready.js"] + +["browser_inspector_expand-collapse.js"] + +["browser_inspector_eyedropper_ruleview.js"] + +["browser_inspector_fission_frame.js"] + +["browser_inspector_fission_frame_navigation.js"] + +["browser_inspector_fission_switch_target.js"] + +["browser_inspector_highlighter-01.js"] + +["browser_inspector_highlighter-02.js"] + +["browser_inspector_highlighter-03.js"] + +["browser_inspector_highlighter-04.js"] + +["browser_inspector_highlighter-05.js"] + +["browser_inspector_highlighter-06.js"] + +["browser_inspector_highlighter-07.js"] + +["browser_inspector_highlighter-08.js"] + +["browser_inspector_highlighter-autohide-config_01.js"] + +["browser_inspector_highlighter-autohide-config_02.js"] + +["browser_inspector_highlighter-autohide-config_03.js"] + +["browser_inspector_highlighter-autohide.js"] + +["browser_inspector_highlighter-by-type.js"] + +["browser_inspector_highlighter-cancel.js"] + +["browser_inspector_highlighter-comments.js"] + +["browser_inspector_highlighter-cssgrid_01.js"] + +["browser_inspector_highlighter-cssgrid_02.js"] + +["browser_inspector_highlighter-cssshape_01.js"] + +["browser_inspector_highlighter-cssshape_02.js"] + +["browser_inspector_highlighter-cssshape_03.js"] + +["browser_inspector_highlighter-cssshape_04.js"] +skip-if = [ + "os == 'win' && asan", # Bug 1453214 + "os == 'win' && debug", # Bug 1453214 + "os == 'linux'", # Bug 1453214 + "os == 'mac' && debug", # high frequency intermittent, bug 1453214 +] + +["browser_inspector_highlighter-cssshape_05.js"] +fail-if = ["a11y_checks"] # bug 1687723 ruleview-shapeswatch is not accessible + +["browser_inspector_highlighter-cssshape_06-scale.js"] +fail-if = ["a11y_checks"] # bug 1687723 ruleview-shapeswatch is not accessible + +["browser_inspector_highlighter-cssshape_06-translate.js"] +fail-if = ["a11y_checks"] # bug 1687723 ruleview-shapeswatch is not accessible + +["browser_inspector_highlighter-cssshape_07.js"] +fail-if = ["a11y_checks"] # bug 1687723 ruleview-shapeswatch is not accessible + +["browser_inspector_highlighter-cssshape_iframe_01.js"] +skip-if = ["verify && debug"] +fail-if = ["a11y_checks"] # bug 1687723 ruleview-shapeswatch is not accessible + +["browser_inspector_highlighter-cssshape_offset-path.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_inspector_highlighter-cssshape_percent.js"] + +["browser_inspector_highlighter-csstransform_01.js"] + +["browser_inspector_highlighter-csstransform_02.js"] + +["browser_inspector_highlighter-custom-element.js"] + +["browser_inspector_highlighter-embed.js"] + +["browser_inspector_highlighter-eyedropper-clipboard.js"] + +["browser_inspector_highlighter-eyedropper-csp.js"] + +["browser_inspector_highlighter-eyedropper-events.js"] +skip-if = ["os == 'win'"] # bug 1413442 + +["browser_inspector_highlighter-eyedropper-frames.js"] + +["browser_inspector_highlighter-eyedropper-image.js"] + +["browser_inspector_highlighter-eyedropper-label.js"] + +["browser_inspector_highlighter-eyedropper-show-hide.js"] + +["browser_inspector_highlighter-eyedropper-xul.js"] + +["browser_inspector_highlighter-eyedropper-zoom.js"] + +["browser_inspector_highlighter-geometry_01.js"] + +["browser_inspector_highlighter-geometry_02.js"] + +["browser_inspector_highlighter-geometry_03.js"] + +["browser_inspector_highlighter-geometry_04.js"] + +["browser_inspector_highlighter-geometry_05.js"] + +["browser_inspector_highlighter-geometry_06.js"] + +["browser_inspector_highlighter-geometry_07.js"] + +["browser_inspector_highlighter-geometry_hide_on_interaction.js"] + +["browser_inspector_highlighter-geometry_iframe.js"] + +["browser_inspector_highlighter-hover_01.js"] + +["browser_inspector_highlighter-hover_02.js"] + +["browser_inspector_highlighter-hover_03.js"] + +["browser_inspector_highlighter-iframes_01.js"] + +["browser_inspector_highlighter-iframes_02.js"] + +["browser_inspector_highlighter-inline.js"] + +["browser_inspector_highlighter-keybinding_01.js"] + +["browser_inspector_highlighter-keybinding_02.js"] + +["browser_inspector_highlighter-keybinding_03.js"] + +["browser_inspector_highlighter-keybinding_04.js"] + +["browser_inspector_highlighter-keybinding_separate-window.js"] + +["browser_inspector_highlighter-measure-keybinding.js"] + +["browser_inspector_highlighter-measure_01.js"] + +["browser_inspector_highlighter-measure_02.js"] + +["browser_inspector_highlighter-measure_03.js"] + +["browser_inspector_highlighter-measure_04.js"] + +["browser_inspector_highlighter-options.js"] + +["browser_inspector_highlighter-preview.js"] + +["browser_inspector_highlighter-reduced-motion-message.js"] + +["browser_inspector_highlighter-reduced-motion.js"] + +["browser_inspector_highlighter-reload.js"] + +["browser_inspector_highlighter-rulers_01.js"] + +["browser_inspector_highlighter-rulers_02.js"] + +["browser_inspector_highlighter-rulers_03.js"] +skip-if = ["os == 'win' && !debug"] # Bug 1449754 + +["browser_inspector_highlighter-selector_01.js"] + +["browser_inspector_highlighter-selector_02.js"] + +["browser_inspector_highlighter-zoom.js"] + +["browser_inspector_iframe-navigation.js"] + +["browser_inspector_iframe-picker-bfcache-navigation.js"] + +["browser_inspector_iframe-picker.js"] + +["browser_inspector_infobar_01.js"] + +["browser_inspector_infobar_02.js"] + +["browser_inspector_infobar_03.js"] + +["browser_inspector_infobar_04.js"] + +["browser_inspector_infobar_05.js"] + +["browser_inspector_infobar_textnode.js"] + +["browser_inspector_initialization.js"] +skip-if = ["debug"] # Bug 1250058 - Docshell leak on debug + +["browser_inspector_inspect-object-element.js"] + +["browser_inspector_inspect_loading_document.js"] + +["browser_inspector_inspect_mutated_node.js"] + +["browser_inspector_inspect_node_contextmenu.js"] + +["browser_inspector_inspect_node_contextmenu_nested.js"] + +["browser_inspector_inspect_parent_process_page.js"] + +["browser_inspector_invalidate.js"] + +["browser_inspector_keyboard-shortcuts-copy-outerhtml.js"] + +["browser_inspector_keyboard-shortcuts.js"] + +["browser_inspector_menu-01-sensitivity.js"] + +["browser_inspector_menu-03-paste-items-svg.js"] + +["browser_inspector_menu-03-paste-items.js"] + +["browser_inspector_menu-04-use-in-console.js"] + +["browser_inspector_menu-05-attribute-items.js"] + +["browser_inspector_menu-06-other.js"] + +["browser_inspector_navigate_to_errors.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_inspector_navigation.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_inspector_open_on_neterror.js"] + +["browser_inspector_pane-toggle-01.js"] + +["browser_inspector_pane-toggle-02.js"] + +["browser_inspector_pane-toggle-03.js"] + +["browser_inspector_pane-toggle-04.js"] + +["browser_inspector_pane-toggle-05.js"] + +["browser_inspector_pane-toggle-layout-invariant.js"] + +["browser_inspector_pane_state_restore.js"] + +["browser_inspector_picker-reset-reference.js"] + +["browser_inspector_picker-shift-key.js"] + +["browser_inspector_picker-stop-on-eyedropper.js"] + +["browser_inspector_picker-stop-on-tool-change.js"] + +["browser_inspector_picker-useragent-widget.js"] + +["browser_inspector_portrait_mode.js"] + +["browser_inspector_pseudoclass-lock.js"] + +["browser_inspector_pseudoclass-menu.js"] + +["browser_inspector_reload-01.js"] + +["browser_inspector_reload-02.js"] + +["browser_inspector_reload_iframe.js"] + +["browser_inspector_reload_invalid_iframe.js"] + +["browser_inspector_reload_missing-iframe-node.js"] + +["browser_inspector_reload_nested_iframe.js"] + +["browser_inspector_reload_shadow_dom.js"] + +["browser_inspector_reload_xul.js"] + +["browser_inspector_remove-iframe-during-load.js"] + +["browser_inspector_search-01.js"] + +["browser_inspector_search-02.js"] + +["browser_inspector_search-03.js"] + +["browser_inspector_search-04.js"] + +["browser_inspector_search-05.js"] + +["browser_inspector_search-06.js"] + +["browser_inspector_search-07.js"] + +["browser_inspector_search-08.js"] + +["browser_inspector_search-09.js"] + +["browser_inspector_search-10.js"] + +["browser_inspector_search-clear.js"] + +["browser_inspector_search-filter_context-menu.js"] + +["browser_inspector_search-label.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_inspector_search-navigation.js"] + +["browser_inspector_search-reserved.js"] + +["browser_inspector_search-selection.js"] + +["browser_inspector_search-sidebar.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_inspector_search-suggests-ids-and-classes.js"] + +["browser_inspector_search_keyboard_shortcut_conflict.js"] + +["browser_inspector_search_keyboard_trap.js"] + +["browser_inspector_select-last-selected.js"] + +["browser_inspector_sidebarstate.js"] + +["browser_inspector_startup.js"] + +["browser_inspector_switch-to-inspector-on-pick.js"] +skip-if = ["apple_catalina && !debug"] # Bug 1713158 + +["browser_inspector_textbox-menu.js"] + +["browser_inspector_textbox-menu_reopen_toolbox.js"] + +["browser_inspector_use-in-console-conflict.js"] diff --git a/devtools/client/inspector/test/browser_inspector_addNode_01.js b/devtools/client/inspector/test/browser_inspector_addNode_01.js new file mode 100644 index 0000000000..ef89950b9b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addNode_01.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the add node button and context menu items are present in the UI. + +const TEST_URL = "data:text/html;charset=utf-8,<h1>Add node</h1>"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { panelDoc } = inspector; + + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const menuItem = allMenuItems.find(item => item.id === "node-menu-add"); + ok(menuItem, "The item is in the menu"); + + const toolbarButton = panelDoc.querySelector( + "#inspector-toolbar #inspector-element-add-button" + ); + ok(toolbarButton, "The add button is in the toolbar"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_addNode_02.js b/devtools/client/inspector/test/browser_inspector_addNode_02.js new file mode 100644 index 0000000000..fcd9b00f18 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addNode_02.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the add node button and context menu items have the right state +// depending on the current selection. + +const TEST_URL = URL_ROOT + "doc_inspector_add_node.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Select the DOCTYPE element"); + let { nodes } = await inspector.walker.children(inspector.walker.rootNode); + await selectNode(nodes[0], inspector); + assertState(false, inspector, "The button and item are disabled on DOCTYPE"); + + info("Select the ::before pseudo-element"); + const body = await getNodeFront("body", inspector); + ({ nodes } = await inspector.walker.children(body)); + await selectNode(nodes[0], inspector); + assertState( + false, + inspector, + "The button and item are disabled on a pseudo-element" + ); + + info("Select the svg element"); + await selectNode("svg", inspector); + assertState( + false, + inspector, + "The button and item are disabled on a SVG element" + ); + + info("Select the div#foo element"); + await selectNode("#foo", inspector); + assertState( + true, + inspector, + "The button and item are enabled on a DIV element" + ); + + info("Select the documentElement element (html)"); + await selectNode("html", inspector); + assertState( + false, + inspector, + "The button and item are disabled on the documentElement" + ); + + info("Select the iframe element"); + await selectNode("iframe", inspector); + assertState( + false, + inspector, + "The button and item are disabled on an IFRAME element" + ); +}); + +function assertState(isEnabled, inspector, desc) { + const doc = inspector.panelDoc; + const btn = doc.querySelector("#inspector-element-add-button"); + + // Force an update of the context menu to make sure menu items are updated + // according to the current selection. This normally happens when the menu is + // opened, but for the sake of this test's simplicity, we directly call the + // private update function instead. + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const menuItem = allMenuItems.find(item => item.id === "node-menu-add"); + ok(menuItem, "The item is in the menu"); + is(!menuItem.disabled, isEnabled, desc); + + is(!btn.hasAttribute("disabled"), isEnabled, desc); +} diff --git a/devtools/client/inspector/test/browser_inspector_addNode_03.js b/devtools/client/inspector/test/browser_inspector_addNode_03.js new file mode 100644 index 0000000000..abc50d2969 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addNode_03.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that adding nodes does work as expected: the parent node remains selected and the +// new node is created inside the parent. + +const TEST_URL = URL_ROOT + "doc_inspector_add_node.html"; +const PARENT_TREE_LEVEL = 3; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Adding a node in an element that has no children and is collapsed"); + let parentNode = await getNodeFront("#foo", inspector); + await selectNode(parentNode, inspector); + await testAddNode(parentNode, inspector); + + info( + "Adding a node in an element with children but that has not been expanded yet" + ); + parentNode = await getNodeFront("#bar", inspector); + await selectNode(parentNode, inspector); + await testAddNode(parentNode, inspector); + + info( + "Adding a node in an element with children that has been expanded then collapsed" + ); + // Select again #bar and collapse it. + parentNode = await getNodeFront("#bar", inspector); + await selectNode(parentNode, inspector); + collapseNode(parentNode, inspector); + await testAddNode(parentNode, inspector); + + info("Adding a node in an element with children that is expanded"); + parentNode = await getNodeFront("#bar", inspector); + await selectNode(parentNode, inspector); + await testAddNode(parentNode, inspector); +}); + +async function testAddNode(parentNode, inspector) { + const btn = inspector.panelDoc.querySelector("#inspector-element-add-button"); + const parentContainer = inspector.markup.getContainer(parentNode); + + is( + parseInt(parentContainer.tagLine.getAttribute("aria-level"), 10), + PARENT_TREE_LEVEL, + "The parent aria-level is up to date." + ); + + info( + "Clicking 'add node' and expecting a markup mutation and a new container" + ); + const onMutation = inspector.once("markupmutation"); + const onNewContainer = inspector.once("container-created"); + btn.click(); + let mutations = await onMutation; + await onNewContainer; + + // We are only interested in childList mutations here. Filter everything else out as + // there may be unrelated mutations (e.g. "events") grouped in. + mutations = mutations.filter(({ type }) => type === "childList"); + + is(mutations.length, 1, "There is one mutation only"); + is(mutations[0].added.length, 1, "There is one new node only"); + + const newNode = mutations[0].added[0]; + + is( + parentNode, + inspector.selection.nodeFront, + "The parent node is still selected" + ); + is( + newNode.parentNode(), + parentNode, + "The new node is inside the right parent" + ); + + const newNodeContainer = inspector.markup.getContainer(newNode); + + is( + parseInt(newNodeContainer.tagLine.getAttribute("aria-level"), 10), + PARENT_TREE_LEVEL + 1, + "The child aria-level is up to date." + ); +} + +function collapseNode(node, inspector) { + const container = inspector.markup.getContainer(node); + container.setExpanded(false); +} diff --git a/devtools/client/inspector/test/browser_inspector_addSidebarTab.js b/devtools/client/inspector/test/browser_inspector_addSidebarTab.js new file mode 100644 index 0000000000..d7c810d3c6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addSidebarTab.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_URI = + "data:text/html;charset=UTF-8," + "<h1>browser_inspector_addtabbar.js</h1>"; + +const CONTENT_TEXT = "Hello World!"; + +/** + * Verify InspectorPanel.addSidebarTab() API that can be consumed + * by DevTools extensions as well as DevTools code base. + */ +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + const { Component, createFactory } = inspector.React; + const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + const { div } = dom; + + info("Adding custom panel."); + + // Define custom side-panel. + class myTabPanel extends Component { + render() { + return div({ className: "my-tab-panel" }, CONTENT_TEXT); + } + } + let tabPanel = createFactory(myTabPanel); + + // Append custom panel (tab) into the Inspector panel and + // make sure it's selected by default (the last arg = true). + inspector.addSidebarTab("myPanel", "My Panel", tabPanel, true); + is( + inspector.sidebar.getCurrentTabID(), + "myPanel", + "My Panel is selected by default" + ); + + // Define another custom side-panel. + class myTabPanel2 extends Component { + render() { + return div({ className: "my-tab-panel2" }, "Another Content"); + } + } + tabPanel = createFactory(myTabPanel2); + + // Append second panel, but don't select it by default. + inspector.addSidebarTab("myPanel", "My Panel", tabPanel, false); + is( + inspector.sidebar.getCurrentTabID(), + "myPanel", + "My Panel is selected by default" + ); + + // Check the the panel content is properly rendered. + const tabPanelNode = inspector.panelDoc.querySelector(".my-tab-panel"); + is( + tabPanelNode.textContent, + CONTENT_TEXT, + "Side panel content has been rendered." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs.js new file mode 100644 index 0000000000..01d53820ac --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs.js @@ -0,0 +1,191 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the breadcrumbs widget content is correct. + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html"; +const NODES = [ + { + selector: "#i1111", + ids: "i1 i11 i111 i1111", + nodeName: "div", + title: "div#i1111", + }, + { selector: "#i22", ids: "i2 i22", nodeName: "div", title: "div#i22" }, + { + selector: "#i2111", + ids: "i2 i21 i211 i2111", + nodeName: "div", + title: "div#i2111", + }, + { + selector: "#i21", + ids: "i2 i21 i211 i2111", + nodeName: "div", + title: "div#i21", + }, + { + selector: "#i22211", + ids: "i2 i22 i222 i2221 i22211", + nodeName: "div", + title: "div#i22211", + }, + { + selector: "#i22", + ids: "i2 i22 i222 i2221 i22211", + nodeName: "div", + title: "div#i22", + }, + { selector: "#i3", ids: "i3", nodeName: "article", title: "article#i3" }, + { + selector: "clipPath", + ids: "vector clip", + nodeName: "clipPath", + title: "clipPath#clip", + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + const breadcrumbs = inspector.panelDoc.getElementById( + "inspector-breadcrumbs" + ); + const container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); + + for (const node of NODES) { + info("Testing node " + node.selector); + + info("Selecting node and waiting for breadcrumbs to update"); + const breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + await selectNode(node.selector, inspector); + await breadcrumbsUpdated; + + info("Performing checks for node " + node.selector); + const buttonsLabelIds = node.ids.split(" "); + + // html > body > … + is( + container.childNodes.length, + buttonsLabelIds.length + 2, + "Node " + node.selector + ": Items count" + ); + + for (let i = 2; i < container.childNodes.length; i++) { + const expectedId = "#" + buttonsLabelIds[i - 2]; + const button = container.childNodes[i]; + const labelId = button.querySelector(".breadcrumbs-widget-item-id"); + is( + labelId.textContent, + expectedId, + "Node " + node.selector + ": button " + i + " matches" + ); + } + + const checkedButton = container.querySelector("button[checked]"); + const labelId = checkedButton.querySelector(".breadcrumbs-widget-item-id"); + const id = inspector.selection.nodeFront.id; + is( + labelId.textContent, + "#" + id, + "Node " + node.selector + ": selection matches" + ); + + const labelTag = checkedButton.querySelector( + ".breadcrumbs-widget-item-tag" + ); + is( + labelTag.textContent, + node.nodeName, + "Node " + node.selector + " has the expected tag name" + ); + + is( + checkedButton.getAttribute("title"), + node.title, + "Node " + node.selector + " has the expected tooltip" + ); + } + + await testPseudoElements(inspector, container); + await testComments(inspector, container); +}); + +async function testPseudoElements(inspector, container) { + info("Checking for pseudo elements"); + + const pseudoParent = await getNodeFront("#pseudo-container", inspector); + const children = await inspector.walker.children(pseudoParent); + is(children.nodes.length, 2, "Pseudo children returned from walker"); + + const beforeElement = children.nodes[0]; + let breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + await selectNode(beforeElement, inspector); + await breadcrumbsUpdated; + is( + container.childNodes[3].textContent, + "::before", + "::before shows up in breadcrumb" + ); + + const afterElement = children.nodes[1]; + breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + await selectNode(afterElement, inspector); + await breadcrumbsUpdated; + is( + container.childNodes[3].textContent, + "::after", + "::before shows up in breadcrumb" + ); +} + +async function testComments(inspector, container) { + info("Checking for comment elements"); + + const breadcrumbs = inspector.breadcrumbs; + const checkedButtonIndex = 2; + const button = container.childNodes[checkedButtonIndex]; + + let onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + button.click(); + await onBreadcrumbsUpdated; + + is(breadcrumbs.currentIndex, checkedButtonIndex, "New button is selected"); + ok( + breadcrumbs.outer.hasAttribute("aria-activedescendant"), + "Active descendant must be set" + ); + + const comment = [...inspector.markup._containers].find( + ([node]) => node.nodeType === Node.COMMENT_NODE + )[0]; + + let onInspectorUpdated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(comment); + await onInspectorUpdated; + + is( + breadcrumbs.currentIndex, + -1, + "When comment is selected no breadcrumb should be checked" + ); + ok( + !breadcrumbs.outer.hasAttribute("aria-activedescendant"), + "Active descendant must not be set" + ); + + onInspectorUpdated = inspector.once("inspector-updated"); + onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + button.click(); + await Promise.all([onInspectorUpdated, onBreadcrumbsUpdated]); + + is( + breadcrumbs.currentIndex, + checkedButtonIndex, + "Same button is selected again" + ); + ok( + breadcrumbs.outer.hasAttribute("aria-activedescendant"), + "Active descendant must be set again" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js new file mode 100644 index 0000000000..d94b640b6f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js @@ -0,0 +1,69 @@ +/* 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/. */ + +"use strict"; + +// Test that hovering over nodes on the breadcrumb buttons in the inspector +// shows the highlighter over those nodes +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>" + ); + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + info("Selecting the test node"); + await selectNode("span", inspector); + const bcButtons = inspector.breadcrumbs.container; + + let onNodeHighlighted = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + let button = bcButtons.childNodes[1]; + EventUtils.synthesizeMouseAtCenter( + button, + { type: "mousemove" }, + button.ownerDocument.defaultView + ); + await onNodeHighlighted; + + let isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "The highlighter is shown on a markup container hover"); + + ok( + await highlighterTestFront.assertHighlightedNode("body"), + "The highlighter highlights the right node" + ); + + const onNodeUnhighlighted = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + // move outside of the breadcrumb trail to trigger unhighlight + EventUtils.synthesizeMouseAtCenter( + inspector.addNodeButton, + { type: "mousemove" }, + inspector.addNodeButton.ownerDocument.defaultView + ); + await onNodeUnhighlighted; + + onNodeHighlighted = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + button = bcButtons.childNodes[2]; + EventUtils.synthesizeMouseAtCenter( + button, + { type: "mousemove" }, + button.ownerDocument.defaultView + ); + await onNodeHighlighted; + + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "The highlighter is shown on a markup container hover"); + + ok( + await highlighterTestFront.assertHighlightedNode("span"), + "The highlighter highlights the right node" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js new file mode 100644 index 0000000000..5d795afdca --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the breadcrumbs keybindings work. + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html"; +const TEST_DATA = [ + { + desc: "Pressing left should select the parent <body>", + key: "KEY_ArrowLeft", + newSelection: "body", + }, + { + desc: "Pressing left again should select the parent <html>", + key: "KEY_ArrowLeft", + newSelection: "html", + }, + { + desc: "Pressing left again should stay on <html>, it's the first element", + key: "KEY_ArrowLeft", + newSelection: "html", + }, + { + desc: "Pressing right should go to <body>", + key: "KEY_ArrowRight", + newSelection: "body", + }, + { + desc: "Pressing right again should go to #i2", + key: "KEY_ArrowRight", + newSelection: "#i2", + }, + { + desc: "Pressing right again should stay on #i2, it's the last element", + key: "KEY_ArrowRight", + newSelection: "#i2", + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + info("Selecting the test node"); + await selectNode("#i2", inspector); + + info("Clicking on the corresponding breadcrumbs node to focus it"); + const container = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + + const button = container.querySelector("button[checked]"); + button.click(); + + let currentSelection = "#id2"; + for (const { desc, key, newSelection } of TEST_DATA) { + info(desc); + + // If the selection will change, wait for the breadcrumb to update, + // otherwise continue. + let onUpdated = null; + if (newSelection !== currentSelection) { + info("Expecting a new node to be selected"); + onUpdated = inspector.once("breadcrumbs-updated"); + } + + EventUtils.synthesizeKey(key); + await onUpdated; + + const newNodeFront = await getNodeFront(newSelection, inspector); + is( + newNodeFront, + inspector.selection.nodeFront, + "The current selection is correct" + ); + is( + container.getAttribute("aria-activedescendant"), + container.querySelector("button[checked]").id, + "aria-activedescendant is set correctly" + ); + + currentSelection = newSelection; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js new file mode 100644 index 0000000000..e3046ce30e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test ability to tab to and away from breadcrumbs using keyboard. + +const TEST_URL = URL_ROOT + "doc_inspector_breadcrumbs.html"; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * focused {Boolean} flag, indicating if breadcrumbs contain focus + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * } + */ +const TEST_DATA = [ + { + desc: "Move the focus away from breadcrumbs to a next focusable element", + focused: false, + key: "VK_TAB", + options: {}, + }, + { + desc: "Move the focus back to the breadcrumbs", + focused: true, + key: "VK_TAB", + options: { shiftKey: true }, + }, + { + desc: + "Move the focus back away from breadcrumbs to a previous focusable " + + "element", + focused: false, + key: "VK_TAB", + options: { shiftKey: true }, + }, + { + desc: "Move the focus back to the breadcrumbs", + focused: true, + key: "VK_TAB", + options: {}, + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const doc = inspector.panelDoc; + const { breadcrumbs } = inspector; + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + await selectNode("#i2", inspector); + + info("Clicking on the corresponding breadcrumbs node to focus it"); + const container = doc.getElementById("inspector-breadcrumbs"); + + const button = container.querySelector("button[checked]"); + const onHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + button.click(); + await onHighlight; + + // Ensure a breadcrumb is focused. + is(doc.activeElement, container, "Focus is on selected breadcrumb"); + is( + container.getAttribute("aria-activedescendant"), + button.id, + "aria-activedescendant is set correctly" + ); + + for (const { desc, focused, key, options } of TEST_DATA) { + info(desc); + + EventUtils.synthesizeKey(key, options); + // Wait until the keyPromise promise resolves. + await breadcrumbs.keyPromise; + + if (focused) { + is(doc.activeElement, container, "Focus is on selected breadcrumb"); + } else { + ok(!containsFocus(doc, container), "Focus is outside of breadcrumbs"); + } + is( + container.getAttribute("aria-activedescendant"), + button.id, + "aria-activedescendant is set correctly" + ); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js new file mode 100644 index 0000000000..91e3976907 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js @@ -0,0 +1,282 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the breadcrumbs widget refreshes correctly when there are markup +// mutations (and that it doesn't refresh when those mutations don't change its +// output). + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html"; + +// Each item in the TEST_DATA array is a test case that should contain the +// following properties: +// - desc {String} A description of this test case (will be logged). +// - setup {Function*} A generator function (can yield promises) that sets up +// the test case. Useful for selecting a node before starting the test. +// - run {Function*} A generator function (can yield promises) that runs the +// actual test case, i.e, mutates the content DOM to cause the breadcrumbs +// to refresh, or not. +// - shouldRefresh {Boolean} Once the `run` function has completed, and the test +// has detected that the page has changed, this boolean instructs the test to +// verify if the breadcrumbs has refreshed or not. +// - output {Array} A list of strings for the text that should be found in each +// button after the test has run. +const TEST_DATA = [ + { + desc: "Adding a child at the end of the chain shouldn't change anything", + async setup(inspector) { + await selectNode("#i1111", inspector); + }, + async run({ walker, selection }) { + await walker.setInnerHTML(selection.nodeFront, "<b>test</b>"); + }, + shouldRefresh: false, + output: ["html", "body", "article#i1", "div#i11", "div#i111", "div#i1111"], + }, + { + desc: "Updating an ID to an displayed element should refresh", + setup() {}, + async run({ walker }) { + const node = await walker.querySelector(walker.rootNode, "#i1"); + await node.modifyAttributes([ + { + attributeName: "id", + newValue: "i1-changed", + }, + ]); + }, + shouldRefresh: true, + output: [ + "html", + "body", + "article#i1-changed", + "div#i11", + "div#i111", + "div#i1111", + ], + }, + { + desc: "Updating an class to a displayed element should refresh", + setup() {}, + async run({ walker }) { + const node = await walker.querySelector(walker.rootNode, "body"); + await node.modifyAttributes([ + { + attributeName: "class", + newValue: "test-class", + }, + ]); + }, + shouldRefresh: true, + output: [ + "html", + "body.test-class", + "article#i1-changed", + "div#i11", + "div#i111", + "div#i1111", + ], + }, + { + desc: + "Updating a non id/class attribute to a displayed element should not " + + "refresh", + setup() {}, + async run({ walker }) { + const node = await walker.querySelector(walker.rootNode, "#i11"); + await node.modifyAttributes([ + { + attributeName: "name", + newValue: "value", + }, + ]); + }, + shouldRefresh: false, + output: [ + "html", + "body.test-class", + "article#i1-changed", + "div#i11", + "div#i111", + "div#i1111", + ], + }, + { + desc: "Moving a child in an element that's not displayed should not refresh", + setup() {}, + async run({ walker }) { + // Re-append #i1211 as a last child of #i2. + const parent = await walker.querySelector(walker.rootNode, "#i2"); + const child = await walker.querySelector(walker.rootNode, "#i211"); + await walker.insertBefore(child, parent); + }, + shouldRefresh: false, + output: [ + "html", + "body.test-class", + "article#i1-changed", + "div#i11", + "div#i111", + "div#i1111", + ], + }, + { + desc: "Moving an undisplayed child in a displayed element should not refresh", + setup() {}, + async run({ walker }) { + // Re-append #i2 in body (move it to the end). + const parent = await walker.querySelector(walker.rootNode, "body"); + const child = await walker.querySelector(walker.rootNode, "#i2"); + await walker.insertBefore(child, parent); + }, + shouldRefresh: false, + output: [ + "html", + "body.test-class", + "article#i1-changed", + "div#i11", + "div#i111", + "div#i1111", + ], + }, + { + desc: + "Updating attributes on an element that's not displayed should not " + + "refresh", + setup() {}, + async run({ walker }) { + const node = await walker.querySelector(walker.rootNode, "#i2"); + await node.modifyAttributes([ + { + attributeName: "id", + newValue: "i2-changed", + }, + { + attributeName: "class", + newValue: "test-class", + }, + ]); + }, + shouldRefresh: false, + output: [ + "html", + "body.test-class", + "article#i1-changed", + "div#i11", + "div#i111", + "div#i1111", + ], + }, + { + desc: "Removing the currently selected node should refresh", + async setup(inspector) { + await selectNode("#i2-changed", inspector); + }, + async run({ walker, selection }) { + await walker.removeNode(selection.nodeFront); + }, + shouldRefresh: true, + output: ["html", "body.test-class"], + }, + { + desc: "Changing the class of the currently selected node should refresh", + setup() {}, + async run({ selection }) { + await selection.nodeFront.modifyAttributes([ + { + attributeName: "class", + newValue: "test-class-changed", + }, + ]); + }, + shouldRefresh: true, + output: ["html", "body.test-class-changed"], + }, + { + desc: "Changing the id of the currently selected node should refresh", + setup() {}, + async run({ selection }) { + await selection.nodeFront.modifyAttributes([ + { + attributeName: "id", + newValue: "new-id", + }, + ]); + }, + shouldRefresh: true, + output: ["html", "body#new-id.test-class-changed"], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + const breadcrumbs = inspector.panelDoc.getElementById( + "inspector-breadcrumbs" + ); + const container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); + const win = container.ownerDocument.defaultView; + + for (const { desc, setup, run, shouldRefresh, output } of TEST_DATA) { + info("Running test case: " + desc); + + info( + "Listen to markupmutation events from the inspector to know when a " + + "test case has completed" + ); + const onContentMutation = inspector.once("markupmutation"); + + info("Running setup"); + await setup(inspector); + + info("Listen to mutations on the breadcrumbs container"); + let hasBreadcrumbsMutated = false; + const observer = new win.MutationObserver(mutations => { + // Only consider childList changes or tooltiptext/checked attributes + // changes. The rest may be mutations caused by the overflowing arrowbox. + for (const { type, attributeName } of mutations) { + const isChildList = type === "childList"; + const isAttributes = + type === "attributes" && + (attributeName === "checked" || attributeName === "tooltiptext"); + if (isChildList || isAttributes) { + hasBreadcrumbsMutated = true; + break; + } + } + }); + observer.observe(container, { + attributes: true, + childList: true, + subtree: true, + }); + + info("Running the test case"); + await run(inspector); + + info("Wait until the page has mutated"); + await onContentMutation; + + if (shouldRefresh) { + info("The breadcrumbs is expected to refresh, so wait for it"); + await inspector.once("inspector-updated"); + } else { + ok( + !inspector._updateProgress, + "The breadcrumbs widget is not currently updating" + ); + } + + is(shouldRefresh, hasBreadcrumbsMutated, "Has the breadcrumbs refreshed?"); + observer.disconnect(); + + info("Check the output of the breadcrumbs widget"); + is(container.childNodes.length, output.length, "Correct number of buttons"); + for (let i = 0; i < container.childNodes.length; i++) { + is( + output[i], + container.childNodes[i].textContent, + "Text content for button " + i + " is correct" + ); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js new file mode 100644 index 0000000000..77b25a0ffa --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the breadcrumbs widget content for namespaced elements is correct. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath id="clip"> + <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +const NODES = [ + { + selector: "clipPath", + nodes: ["svg:svg", "svg:clipPath"], + nodeName: "svg:clipPath", + title: "svg:clipPath#clip", + }, + { + selector: "circle", + nodes: ["svg:svg", "svg:circle"], + nodeName: "svg:circle", + title: "svg:circle", + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + const container = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + + for (const node of NODES) { + info("Testing node " + node.selector); + + info("Selecting node and waiting for breadcrumbs to update"); + const breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + await selectNode(node.selector, inspector); + await breadcrumbsUpdated; + + info("Performing checks for node " + node.selector); + + const checkedButton = container.querySelector("button[checked]"); + + const labelTag = checkedButton.querySelector( + ".breadcrumbs-widget-item-tag" + ); + is( + labelTag.textContent, + node.nodeName, + "Node " + node.selector + " has the expected tag name" + ); + + is( + checkedButton.getAttribute("title"), + node.title, + "Node " + node.selector + " has the expected tooltip" + ); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js new file mode 100644 index 0000000000..80540a528d --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the breadcrumbs widget refreshes correctly when there are markup +// mutations, even if the currently selected node is a slotted node in the shadow DOM. + +const TEST_URL = `data:text/html;charset=utf-8, + <test-component> + <div slot="slot1" id="el1">content</div> + </test-component> + + <script> + 'use strict'; + customElements.define('test-component', class extends HTMLElement { + constructor() { + super(); + let shadowRoot = this.attachShadow({mode: 'open'}); + shadowRoot.innerHTML = '<slot class="slot-class" name="slot1"></slot>'; + } + }); + </script>`; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { markup } = inspector; + const breadcrumbs = inspector.panelDoc.getElementById( + "inspector-breadcrumbs" + ); + + info("Find and expand the test-component shadow DOM host."); + const hostFront = await getNodeFront("test-component", inspector); + const hostContainer = markup.getContainer(hostFront); + await expandContainer(inspector, hostContainer); + + info("Expand the shadow root"); + const shadowRootContainer = hostContainer.getChildContainers()[0]; + await expandContainer(inspector, shadowRootContainer); + + const slotContainer = shadowRootContainer.getChildContainers()[0]; + + info("Select the slot node and wait for the breadcrumbs update"); + const slotNodeFront = slotContainer.node; + let onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + inspector.selection.setNodeFront(slotNodeFront); + await onBreadcrumbsUpdated; + + checkBreadcrumbsContent(breadcrumbs, [ + "html", + "body", + "test-component", + "#shadow-root", + "slot.slot-class", + ]); + + info("Expand the slot"); + await expandContainer(inspector, slotContainer); + + const slotChildContainers = slotContainer.getChildContainers(); + is(slotChildContainers.length, 1, "Expecting 1 slotted child"); + + info("Select the slotted node and wait for the breadcrumbs update"); + const slottedNodeFront = slotChildContainers[0].node; + onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + inspector.selection.setNodeFront(slottedNodeFront); + await onBreadcrumbsUpdated; + + checkBreadcrumbsContent(breadcrumbs, [ + "html", + "body", + "test-component", + "div#el1", + ]); + + info( + "Update the classname of the real element and wait for the breadcrumbs update" + ); + onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.document.getElementById("el1").setAttribute("class", "test"); + }); + await onBreadcrumbsUpdated; + + checkBreadcrumbsContent(breadcrumbs, [ + "html", + "body", + "test-component", + "div#el1.test", + ]); +}); + +function checkBreadcrumbsContent(breadcrumbs, selectors) { + info("Check the output of the breadcrumbs widget"); + const container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); + is( + container.childNodes.length, + selectors.length, + "Correct number of buttons" + ); + for (let i = 0; i < container.childNodes.length; i++) { + is( + container.childNodes[i].textContent, + selectors[i], + "Text content for button " + i + " is correct" + ); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js new file mode 100644 index 0000000000..1267d23646 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the start and end buttons on the breadcrumb trail bring the right +// crumbs into the visible area, for both LTR and RTL + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs_visibility.html"; +const NODE_ONE = "div#aVeryLongIdToExceedTheBreadcrumbTruncationLimit"; +const NODE_TWO = "div#anotherVeryLongIdToExceedTheBreadcrumbTruncationLimit"; +const NODE_THREE = "div#aThirdVeryLongIdToExceedTheTruncationLimit"; +const NODE_FOUR = "div#aFourthOneToExceedTheTruncationLimit"; +const NODE_FIVE = "div#aFifthOneToExceedTheTruncationLimit"; +const NODE_SIX = "div#aSixthOneToExceedTheTruncationLimit"; +const NODE_SEVEN = "div#aSeventhOneToExceedTheTruncationLimit"; + +const NODES = [ + { action: "start", title: NODE_SIX }, + { action: "start", title: NODE_FIVE }, + { action: "start", title: NODE_FOUR }, + { action: "start", title: NODE_THREE }, + { action: "start", title: NODE_TWO }, + { action: "start", title: NODE_ONE }, + { action: "end", title: NODE_TWO }, + { action: "end", title: NODE_THREE }, + { action: "end", title: NODE_FOUR }, + { action: "end", title: NODE_FIVE }, + { action: "end", title: NODE_SIX }, +]; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URI); + + // No way to wait for scrolling to end (Bug 1172171) + // Rather than wait a max time; limit test to instant scroll behavior + inspector.breadcrumbs.arrowScrollBox.scrollBehavior = "instant"; + + await toolbox.switchHost(Toolbox.HostType.WINDOW); + const hostWindow = toolbox.win.parent; + const originalWidth = hostWindow.outerWidth; + const originalHeight = hostWindow.outerHeight; + const inspectorResized = inspector.once("inspector-resize"); + hostWindow.resizeTo(640, 300); + await inspectorResized; + + info("Testing transitions ltr"); + await pushPref("intl.l10n.pseudo", ""); + await testBreadcrumbTransitions(hostWindow, inspector); + + info("Testing transitions rtl"); + await pushPref("intl.l10n.pseudo", "bidi"); + await testBreadcrumbTransitions(hostWindow, inspector); + + hostWindow.resizeTo(originalWidth, originalHeight); +}); + +async function testBreadcrumbTransitions(hostWindow, inspector) { + const breadcrumbs = inspector.panelDoc.getElementById( + "inspector-breadcrumbs" + ); + const startBtn = breadcrumbs.querySelector(".scrollbutton-up"); + const endBtn = breadcrumbs.querySelector(".scrollbutton-down"); + const container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); + const breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + + info("Selecting initial node"); + await selectNode(NODE_SEVEN, inspector); + + // So just need to wait for a duration + await breadcrumbsUpdated; + const initialCrumb = container.querySelector("button[checked]"); + is( + isElementInViewport(hostWindow, initialCrumb), + true, + "initial element was visible" + ); + + for (const node of NODES) { + info("Checking for visibility of crumb " + node.title); + if (node.action === "end") { + info("Simulating click of end button"); + EventUtils.synthesizeMouseAtCenter(endBtn, {}, inspector.panelWin); + } else if (node.action === "start") { + info("Simulating click of start button"); + EventUtils.synthesizeMouseAtCenter(startBtn, {}, inspector.panelWin); + } + await breadcrumbsUpdated; + const selector = 'button[title="' + node.title + '"]'; + const relevantCrumb = container.querySelector(selector); + is( + isElementInViewport(hostWindow, relevantCrumb), + true, + node.title + " crumb is visible" + ); + } +} + +function isElementInViewport(window, el) { + const rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); +} + +registerCleanupFunction(function () { + // Restore the host type for other tests. + Services.prefs.clearUserPref("devtools.toolbox.host"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js new file mode 100644 index 0000000000..8d7b3a1cfc --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test to ensure inspector handles deletion of selected node correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_delete-selected-node-01.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + const spanNodeFront = await getNodeFrontInFrames( + ["iframe", "span"], + inspector + ); + await selectNode(spanNodeFront, inspector); + + info("Removing selected <span> element."); + const parentNode = spanNodeFront.parentNode(); + await spanNodeFront.inspectorFront.walker.removeNode(spanNodeFront); + + // Wait for the inspector to process the mutation + await inspector.once("inspector-updated"); + is( + inspector.selection.nodeFront, + parentNode, + "Parent node of selected <span> got selected." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js new file mode 100644 index 0000000000..6fc8c1031f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js @@ -0,0 +1,145 @@ +/* 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/. */ +"use strict"; + +// Test that when nodes are being deleted in the page, the current selection +// and therefore the markup view, css rule view, computed view, font view, +// box model view, and breadcrumbs, reset accordingly to show the right node + +const TEST_PAGE = URL_ROOT + "doc_inspector_delete-selected-node-02.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_PAGE); + + await testManuallyDeleteSelectedNode(); + await testAutomaticallyDeleteSelectedNode(); + await testDeleteSelectedNodeContainerFrame(); + await testDeleteWithNonElementNode(); + + async function testManuallyDeleteSelectedNode() { + info( + "Selecting a node, deleting it via context menu and checking that " + + "its parent node is selected and breadcrumbs are updated." + ); + + await selectNode("#deleteManually", inspector); + const nodeToBeDeleted = inspector.selection.nodeFront; + await deleteNodeWithContextMenu(nodeToBeDeleted, inspector); + + info("Performing checks."); + await assertNodeSelectedAndPanelsUpdated( + "#selectedAfterDelete", + "li#selectedAfterDelete" + ); + } + + async function testAutomaticallyDeleteSelectedNode() { + info( + "Selecting a node, deleting it via javascript and checking that " + + "its parent node is selected and breadcrumbs are updated." + ); + + const div = await getNodeFront("#deleteAutomatically", inspector); + await selectNode(div, inspector); + + info("Deleting selected node via javascript."); + await inspector.walker.removeNode(div); + + info("Waiting for inspector to update."); + await inspector.once("inspector-updated"); + + info("Inspector updated, performing checks."); + await assertNodeSelectedAndPanelsUpdated( + "#deleteChildren", + "ul#deleteChildren" + ); + } + + async function testDeleteSelectedNodeContainerFrame() { + info( + "Selecting a node inside iframe, deleting the iframe via javascript " + + "and checking the parent node of the iframe is selected and " + + "breadcrumbs are updated." + ); + + info("Selecting an element inside iframe."); + await selectNodeInFrames(["#deleteIframe", "#deleteInIframe"], inspector); + + info("Deleting parent iframe node via javascript."); + const onInspectorUpdated = inspector.once("inspector-updated"); + + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.document.querySelector("iframe#deleteIframe").remove(); + }); + + info("Waiting for inspector to update."); + await onInspectorUpdated; + + info("Inspector updated, performing checks."); + await assertNodeSelectedAndPanelsUpdated("body", "body"); + } + + async function testDeleteWithNonElementNode() { + info( + "Selecting a node, deleting it via context menu and checking that " + + "its parent node is selected and breadcrumbs are updated " + + "when the node is followed by a non-element node" + ); + + await selectNode("#deleteWithNonElement", inspector); + const nodeToBeDeleted = inspector.selection.nodeFront; + await deleteNodeWithContextMenu(nodeToBeDeleted, inspector); + + let expectedCrumbs = ["html", "body", "div#deleteToMakeSingleTextNode"]; + await assertNodeSelectedAndCrumbsUpdated(expectedCrumbs, Node.TEXT_NODE); + + // Delete node with key, as cannot delete text node with + // context menu at this time. + inspector.markup._frame.focus(); + EventUtils.synthesizeKey("KEY_Delete"); + await inspector.once("inspector-updated"); + + expectedCrumbs = ["html", "body", "div#deleteToMakeSingleTextNode"]; + await assertNodeSelectedAndCrumbsUpdated(expectedCrumbs, Node.ELEMENT_NODE); + } + + function assertNodeSelectedAndCrumbsUpdated( + expectedCrumbs, + expectedNodeType + ) { + info("Performing checks"); + const actualNodeType = inspector.selection.nodeFront.nodeType; + is(actualNodeType, expectedNodeType, "The node has the right type"); + + const breadcrumbs = inspector.panelDoc.querySelectorAll( + "#inspector-breadcrumbs .html-arrowscrollbox-inner > *" + ); + is( + breadcrumbs.length, + expectedCrumbs.length, + "Have the correct number of breadcrumbs" + ); + for (let i = 0; i < breadcrumbs.length; i++) { + is( + breadcrumbs[i].textContent, + expectedCrumbs[i], + "Text content for button " + i + " is correct" + ); + } + } + + async function assertNodeSelectedAndPanelsUpdated(selector, crumbLabel) { + const nodeFront = await getNodeFront(selector, inspector); + is(inspector.selection.nodeFront, nodeFront, "The right node is selected"); + + const breadcrumbs = inspector.panelDoc.querySelector( + "#inspector-breadcrumbs .html-arrowscrollbox-inner" + ); + is( + breadcrumbs.querySelector("button[checked=true]").textContent, + crumbLabel, + "The right breadcrumb is selected" + ); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js new file mode 100644 index 0000000000..e8d1c2bb9e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test to ensure inspector can handle destruction of selected node inside an iframe. + +const TEST_URL = URL_ROOT + "doc_inspector_delete-selected-node-01.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Select a node inside the iframe"); + await selectNodeInFrames(["iframe", "span"], inspector); + + info("Removing iframe."); + const onInspectorUpdated = inspector.once("inspector-updated"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.document.querySelector("iframe").remove(); + }); + await onInspectorUpdated; + + const body = await getNodeFront("body", inspector); + is(inspector.selection.nodeFront, body, "Selection is now the body node"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_delete_node_in_frame.js b/devtools/client/inspector/test/browser_inspector_delete_node_in_frame.js new file mode 100644 index 0000000000..53b4b11277 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_delete_node_in_frame.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_URL_1 = `https://example.com/document-builder.sjs?html= +<meta charset=utf8><iframe srcdoc="<div>"></iframe><body>`; +const TEST_URL_2 = `https://example.com/document-builder.sjs?html=<meta charset=utf8><div id=url2>`; + +// Test that deleting a node in a same-process iframe and doing a navigation +// does not freeze the browser or break the toolbox. +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL_1); + + info("Select a node in a same-process iframe"); + const node = await selectNodeInFrames(["iframe", "div"], inspector); + const parentNode = node.parentNode(); + + info("Removing selected element with the context menu."); + await deleteNodeWithContextMenu(node, inspector); + + is( + inspector.selection.nodeFront, + parentNode, + "The parent node of the deleted node was selected." + ); + + const onInspectorReloaded = inspector.once("reloaded"); + await navigateTo(TEST_URL_2); + await onInspectorReloaded; + + const url2NodeFront = await getNodeFront("#url2", inspector); + ok(url2NodeFront, "Can retrieve a node front after the navigation"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js b/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js new file mode 100644 index 0000000000..2040bccae4 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing that closing the inspector after navigating to a page doesn't fail. + +const URL_1 = "data:text/plain;charset=UTF-8,abcde"; +const URL_2 = "data:text/plain;charset=UTF-8,12345"; + +add_task(async function () { + const { toolbox } = await openInspectorForURL(URL_1); + + await navigateTo(URL_2); + + info("Destroying toolbox"); + try { + await toolbox.destroy(); + ok(true, "Toolbox destroyed"); + } catch (e) { + ok(false, "An exception occured while destroying toolbox"); + console.error(e); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js b/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js new file mode 100644 index 0000000000..4df83e3a46 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that switching to the inspector panel and not waiting for it to be fully +// loaded doesn't fail the test with unhandled rejected promises. + +add_task(async function () { + // At least one assertion is needed to avoid failing the test, but really, + // what we're interested in is just having the test pass when switching to the + // inspector. + ok(true); + + await addTab("data:text/html;charset=utf-8,test inspector destroy"); + + info("Open the toolbox on the debugger panel"); + const toolbox = await gDevTools.showToolboxForTab(gBrowser.selectedTab, { + toolId: "jsdebugger", + }); + + info("Switch to the inspector panel and immediately end the test"); + const onInspectorSelected = toolbox.once("inspector-selected"); + toolbox.selectTool("inspector"); + await onInspectorSelected; +}); diff --git a/devtools/client/inspector/test/browser_inspector_expand-collapse.js b/devtools/client/inspector/test/browser_inspector_expand-collapse.js new file mode 100644 index 0000000000..dd39d5898f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_expand-collapse.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that context menu items exapnd all and collapse are shown properly. + +const TEST_URL = + "data:text/html;charset=utf-8," + + "<div id='parent-node'><div id='child-node'></div></div>"; + +add_task(async function () { + // Test is often exceeding time-out threshold, similar to Bug 1137765 + requestLongerTimeout(2); + + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Selecting the parent node"); + + const front = await getNodeFrontForSelector("#parent-node", inspector); + + await selectNode(front, inspector); + + info("Simulating context menu click on the selected node container."); + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(front, inspector).tagLine, + }); + let nodeMenuCollapseElement = allMenuItems.find( + item => item.id === "node-menu-collapse" + ); + let nodeMenuExpandElement = allMenuItems.find( + item => item.id === "node-menu-expand" + ); + + ok(nodeMenuCollapseElement.disabled, "Collapse option is disabled"); + ok(!nodeMenuExpandElement.disabled, "ExpandAll option is enabled"); + + info("Testing whether expansion works properly"); + nodeMenuExpandElement.click(); + + info("Waiting for expansion to occur"); + await waitForMultipleChildrenUpdates(inspector); + const markUpContainer = getContainerForNodeFront(front, inspector); + ok(markUpContainer.expanded, "node has been successfully expanded"); + + // reselecting node after expansion + await selectNode(front, inspector); + + info("Testing whether collapse works properly"); + info("Simulating context menu click on the selected node container."); + allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(front, inspector).tagLine, + }); + nodeMenuCollapseElement = allMenuItems.find( + item => item.id === "node-menu-collapse" + ); + nodeMenuExpandElement = allMenuItems.find( + item => item.id === "node-menu-expand" + ); + + ok(!nodeMenuCollapseElement.disabled, "Collapse option is enabled"); + ok(!nodeMenuExpandElement.disabled, "ExpandAll option is enabled"); + nodeMenuCollapseElement.click(); + + info("Waiting for collapse to occur"); + await waitForMultipleChildrenUpdates(inspector); + ok(!markUpContainer.expanded, "node has been successfully collapsed"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_eyedropper_ruleview.js b/devtools/client/inspector/test/browser_inspector_eyedropper_ruleview.js new file mode 100644 index 0000000000..ba749fbf10 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_eyedropper_ruleview.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CSS_URI = URL_ROOT + "style_inspector_eyedropper_ruleview.css"; + +const TEST_URI = ` + <link href="${CSS_URI}" rel="stylesheet" type="text/css"/> +`; + +// Test that opening the eyedropper before opening devtools doesn't break links +// in the ruleview. +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + const onPickerCommandHandled = new Promise(r => { + const listener = subject => { + Services.obs.removeObserver(listener, "color-picker-command-handled"); + r(subject.wrappedJSObject); + }; + Services.obs.addObserver(listener, "color-picker-command-handled"); + }); + + info("Trigger the eyedropper command"); + const menu = document.getElementById("menu_eyedropper"); + menu.doCommand(); + + info("Wait for the color-picker-command-handled observable"); + const targetFront = await onPickerCommandHandled; + + info("Wait for the eye dropper to be visible"); + const highlighterTestFront = await targetFront.getFront("highlighterTest"); + await asyncWaitUntil(() => highlighterTestFront.isEyeDropperVisible()); + + info("Cancel the eye dropper and wait for the target to be destroyed"); + EventUtils.synthesizeKey("KEY_Escape"); + await waitFor(() => targetFront.isDestroyed()); + + const { inspector, view } = await openRuleView(); + + await selectNode("body", inspector); + + const linkText = getRuleViewLinkTextByIndex(view, 1); + is( + linkText, + "style_inspector_eyedropper_ruleview.css:1", + "link text at index 1 has the correct link." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_fission_frame.js b/devtools/client/inspector/test/browser_inspector_fission_frame.js new file mode 100644 index 0000000000..fda1471248 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_fission_frame.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * bug 1673627 - Test that remote iframes with short inline content, + * get their inner remote document displayed instead of the inlineTextChild content. + */ +const TEST_URI = `data:text/html,<div id="root"><iframe src="https://example.com/document-builder.sjs?html=<div id=com>com"><br></iframe></div>`; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + const tree = ` + id="root" + iframe + #document + html + head + body + id="com"`; + await assertMarkupViewAsTree(tree, "#root", inspector); +}); + +// Test regular remote frames +const FRAME_URL = `https://example.com/document-builder.sjs?html=<div>com`; +const TEST_REMOTE_FRAME = `https://example.org/document-builder.sjs?html=<div id="org-root"><iframe src="${FRAME_URL}"></iframe></div>`; +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_REMOTE_FRAME); + const tree = ` + id="org-root" + iframe + #document + html + head + body + com`; + await assertMarkupViewAsTree(tree, "#org-root", inspector); +}); diff --git a/devtools/client/inspector/test/browser_inspector_fission_frame_navigation.js b/devtools/client/inspector/test/browser_inspector_fission_frame_navigation.js new file mode 100644 index 0000000000..7ac3b51586 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_fission_frame_navigation.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const EXAMPLE_COM_URI = + "https://example.com/document-builder.sjs?html=<div id=com>com"; +const EXAMPLE_NET_URI = + "https://example.net/document-builder.sjs?html=<div id=net>net"; + +const ORG_URL_ROOT = URL_ROOT.replace("example.com", "example.org"); +const TEST_ORG_URI = + ORG_URL_ROOT + "doc_inspector_fission_frame_navigation.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_ORG_URI); + const tree = ` + id="root" + iframe + #document + html + head + body + id="org"`; + // Note: the assertMarkupViewAsTree uses very high level APIs and is similar + // to what should happen when a user interacts with the markup view. + // It is important to avoid explicitly fetching walkers or node fronts during + // the test, as it might cause reparenting of remote frames and make the test + // succeed without actually testing that the feature works correctly. + await assertMarkupViewAsTree(tree, "#root", inspector); + + await navigateIframeTo(inspector, EXAMPLE_COM_URI); + const treeAfterLoadingCom = ` + id="root" + iframe + #document + html + head + body + id="com"`; + await assertMarkupViewAsTree(treeAfterLoadingCom, "#root", inspector); + + await navigateIframeTo(inspector, EXAMPLE_NET_URI); + const treeAfterLoadingNet = ` + id="root" + iframe + #document + html + head + body + id="net"`; + await assertMarkupViewAsTree(treeAfterLoadingNet, "#root", inspector); +}); + +/** + * This test will check the behavior when navigating a frame which is not + * visible in the markup view, because its parentNode has not been expanded yet. + * + * We expect a root-node resource to be emitted, but ideally we should not + * initialize the walker and inspector actors for the target of this root-node + * resource. + */ +add_task(async function navigateFrameNotExpandedInMarkupView() { + if (!isFissionEnabled()) { + // This test only makes sense with Fission and remote frames, otherwise the + // root-node resource will not be emitted for a frame navigation. + return; + } + + const { inspector } = await openInspectorForURL(TEST_ORG_URI); + const resourceCommand = inspector.toolbox.resourceCommand; + + // At this stage the expected layout of the markup view is + // v html (expanded) + // v body (expanded) + // > p (collapsed) + // > div (collapsed) + // + // The iframe we are about to navigate is therefore hidden and we are not + // watching it - ie, it is not in the list of known NodeFronts/Actors. + const resource = await navigateIframeTo(inspector, EXAMPLE_COM_URI); + + is( + resource.resourceType, + resourceCommand.TYPES.ROOT_NODE, + "A resource with resourceType ROOT_NODE was received when navigating" + ); + + // This highlights what doesn't work with the current approach. + // Since the root-node resource is a NodeFront watched via the WalkerFront, + // watching it for a new remote frame target implies initializing the + // inspector & walker fronts. + // + // If the resource was not a front (eg ContentDOMreference?) and was emitted + // by the target actor instead of the walker actor, the client can decide if + // the inspector and walker fronts should be initialized or not. + // + // In this test scenario, the children of the iframe were not known by the + // inspector before this navigation. Per the explanation above, the new target + // should not have an already instantiated inspector front. + // + // This should be fixed when implementing the RootNode resource on the server + // in https://bugzilla.mozilla.org/show_bug.cgi?id=1644190 + todo( + !resource.targetFront.getCachedFront("inspector"), + "The inspector front for the new target should not be initialized" + ); +}); + +async function navigateIframeTo(inspector, url) { + info("Navigate the test iframe to " + url); + + const { commands } = inspector; + const { resourceCommand } = inspector.toolbox; + const onTargetProcessed = waitForTargetProcessed(commands, url); + + const { onResource: onNewRoot } = await resourceCommand.waitForNextResource( + resourceCommand.TYPES.ROOT_NODE, + { + ignoreExistingResources: true, + } + ); + + info("Update the src attribute of the iframe tag"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [url], function (_url) { + content.document.querySelector("iframe").setAttribute("src", _url); + }); + + info("Wait for frameLoad/newRoot to resolve"); + const newRootResult = await onNewRoot; + + info("Wait for pending children updates"); + await inspector.markup._waitForChildren(); + + if (isFissionEnabled()) { + info("Wait until the new target has been processed by TargetCommand"); + await onTargetProcessed; + } + + // Note: the newRootResult changes when the test runs with or without fission. + return newRootResult; +} + +/** + * Returns a promise that waits until the provided commands's TargetCommand has fully + * processed a target with the provided URL. + * This will avoid navigating again before the new resource command have fully + * attached to the new target. + */ +function waitForTargetProcessed(commands, url) { + return new Promise(resolve => { + const onTargetProcessed = targetFront => { + if (targetFront.url !== encodeURI(url)) { + return; + } + commands.targetCommand.off( + "processed-available-target", + onTargetProcessed + ); + resolve(); + }; + commands.targetCommand.on("processed-available-target", onTargetProcessed); + }); +} diff --git a/devtools/client/inspector/test/browser_inspector_fission_switch_target.js b/devtools/client/inspector/test/browser_inspector_fission_switch_target.js new file mode 100644 index 0000000000..f8c7edc5dd --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_fission_switch_target.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test target-switching with the inspector. +// This test should check both fission and non-fission target switching. + +const PARENT_PROCESS_URI = "about:robots"; +const EXAMPLE_COM_URI = + "https://example.com/document-builder.sjs?html=<div id=com>com"; +const EXAMPLE_ORG_URI = + "https://example.org/document-builder.sjs?html=<div id=org>org"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(PARENT_PROCESS_URI); + const aboutRobotsNodeFront = await getNodeFront(".title-text", inspector); + ok(!!aboutRobotsNodeFront, "Can retrieve a node front from about:robots"); + + info("Navigate to " + EXAMPLE_COM_URI); + await navigateTo(EXAMPLE_COM_URI); + const comNodeFront = await getNodeFront("#com", inspector); + ok(!!comNodeFront, "Can retrieve a node front from example.com"); + + await navigateTo(EXAMPLE_ORG_URI); + const orgNodeFront = await getNodeFront("#org", inspector); + ok(!!orgNodeFront, "Can retrieve a node front from example.org"); + + await navigateTo(PARENT_PROCESS_URI); + const aboutRobotsNodeFront2 = await getNodeFront(".title-text", inspector); + ok(!!aboutRobotsNodeFront2, "Can retrieve a node front from about:robots"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-01.js b/devtools/client/inspector/test/browser_inspector_highlighter-01.js new file mode 100644 index 0000000000..2cdf530073 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-01.js @@ -0,0 +1,43 @@ +/* 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/. */ + +"use strict"; + +// Test that hovering over nodes in the markup-view shows the highlighter over +// those nodes +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>" + ); + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + let isVisible = !!inspector.highlighters.getActiveHighlighter( + inspector.highlighters.TYPES.BOXMODEL + ); + ok(!isVisible, "The highlighter is hidden by default"); + + info("Selecting the test node"); + await selectNode("span", inspector); + const container = await getContainerForSelector("h1", inspector); + + const onHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + + EventUtils.synthesizeMouseAtCenter( + container.tagLine, + { type: "mousemove" }, + inspector.markup.doc.defaultView + ); + await onHighlight; + + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "The highlighter is shown on a markup container hover"); + + ok( + await highlighterTestFront.assertHighlightedNode("h1"), + "The highlighter highlights the right node" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-02.js b/devtools/client/inspector/test/browser_inspector_highlighter-02.js new file mode 100644 index 0000000000..d112dcb290 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-02.js @@ -0,0 +1,49 @@ +/* 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/. */ + +"use strict"; + +// Test that the highlighter is correctly displayed over a variety of elements + +const TEST_URI = URL_ROOT + "doc_inspector_highlighter.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + info("Selecting the simple, non-transformed DIV"); + await selectAndHighlightNode("#simple-div", inspector); + + let isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "The highlighter is shown"); + ok( + await highlighterTestFront.assertHighlightedNode("#simple-div"), + "The highlighter's outline corresponds to the simple div" + ); + await isNodeCorrectlyHighlighted(highlighterTestFront, "#simple-div"); + + info("Selecting the rotated DIV"); + await selectAndHighlightNode("#rotated-div", inspector); + + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "The highlighter is shown"); + info( + "Check that the highlighter is displayed at the expected position for rotated div" + ); + await isNodeCorrectlyHighlighted(highlighterTestFront, "#rotated-div"); + + info("Selecting the zero width height DIV"); + await selectAndHighlightNode("#widthHeightZero-div", inspector); + + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "The highlighter is shown"); + info( + "Check that the highlighter is displayed at the expected position for a zero width height div" + ); + await isNodeCorrectlyHighlighted( + highlighterTestFront, + "#widthHeightZero-div" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-03.js b/devtools/client/inspector/test/browser_inspector_highlighter-03.js new file mode 100644 index 0000000000..8be820248b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-03.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that iframes are correctly highlighted. + +const IFRAME_SRC = `<style> + body { + margin:0; + height:100%; + background-color:tomato; + } + </style> + <body class=remote>hello from iframe</body>`; + +const DOCUMENT_SRC = `<style> + iframe { + height:200px; + border: 11px solid black; + padding: 13px; + } + body,iframe,h1 { + margin:0; + padding: 0; + } + </style> + <body> + <iframe src='https://example.com/document-builder.sjs?html=${encodeURIComponent( + IFRAME_SRC + )}'></iframe> + </body>`; + +const TEST_URI = "data:text/html;charset=utf-8," + DOCUMENT_SRC; + +add_task(async function () { + const { inspector, toolbox, highlighterTestFront } = + await openInspectorForURL(TEST_URI); + + info("Waiting for box mode to show."); + const topLevelBodyNodeFront = await getNodeFront("body", inspector); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + topLevelBodyNodeFront + ); + + info("Waiting for element picker to become active."); + await startPicker(toolbox); + + info("Check that hovering iframe padding does highlight the iframe element"); + // the iframe has 13px of padding, so hovering at [1,1] should be enough. + await hoverElement(inspector, "iframe", 1, 1); + + await isNodeCorrectlyHighlighted(highlighterTestFront, "iframe"); + + info("Scrolling the document"); + await setContentPageElementProperty( + "iframe", + "style", + "margin-bottom: 2000px" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.scrollBy(0, 40) + ); + + // target the body within the iframe + const iframeBodySelector = ["iframe", "body"]; + let iframeHighlighterTestFront = highlighterTestFront; + let bodySelectorWithinHighlighterEnv = iframeBodySelector; + + if (isFissionEnabled() || isEveryFrameTargetEnabled()) { + const target = toolbox.commands.targetCommand + .getAllTargets([toolbox.commands.targetCommand.TYPES.FRAME]) + .find(t => t.url.startsWith("https://example.com")); + + // We need to retrieve the highlighterTestFront for the frame target. + iframeHighlighterTestFront = await getHighlighterTestFront(toolbox, { + target, + }); + + bodySelectorWithinHighlighterEnv = ["body"]; + } + + info("Check that hovering the iframe <body> highlights the expected element"); + await hoverElement(inspector, iframeBodySelector, 40, 40); + + ok( + await iframeHighlighterTestFront.assertHighlightedNode( + bodySelectorWithinHighlighterEnv + ), + "highlighter is shown on the iframe body" + ); + await isNodeCorrectlyHighlighted( + iframeHighlighterTestFront, + iframeBodySelector + ); + + info("Scrolling the document up"); + // scroll up so we can inspect the top level document again + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.scrollTo(0, 0) + ); + + info("Check that hovering iframe padding again does work"); + // the iframe has 13px of padding, so hovering at [1,1] should be enough. + await hoverElement(inspector, "iframe", 1, 1); + await isNodeCorrectlyHighlighted(highlighterTestFront, "iframe"); + + info("And finally check that hovering the iframe <body> again does work"); + info("Check that hovering the iframe <body> highlights the expected element"); + await hoverElement(inspector, iframeBodySelector, 40, 40); + + ok( + await iframeHighlighterTestFront.assertHighlightedNode( + bodySelectorWithinHighlighterEnv + ), + "highlighter is shown on the iframe body" + ); + await isNodeCorrectlyHighlighted( + iframeHighlighterTestFront, + iframeBodySelector + ); + + info("Stop the element picker."); + await toolbox.nodePicker.stop({ canceled: true }); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-04.js b/devtools/client/inspector/test/browser_inspector_highlighter-04.js new file mode 100644 index 0000000000..eb8e2b3deb --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-04.js @@ -0,0 +1,55 @@ +/* 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/. */ + +"use strict"; + +// Check that various highlighter elements exist. + +const TEST_URL = "data:text/html;charset=utf-8,<div>test</div>"; + +// IDs of all highlighter elements that we expect to find in the canvasFrame. +const ELEMENTS = [ + "box-model-root", + "box-model-elements", + "box-model-margin", + "box-model-border", + "box-model-padding", + "box-model-content", + "box-model-guide-top", + "box-model-guide-right", + "box-model-guide-bottom", + "box-model-guide-left", + "box-model-infobar-container", + "box-model-infobar-tagname", + "box-model-infobar-id", + "box-model-infobar-classes", + "box-model-infobar-pseudo-classes", + "box-model-infobar-dimensions", +]; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + + info("Show the box-model highlighter"); + const divFront = await getNodeFront("div", inspector); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + divFront + ); + + for (const id of ELEMENTS) { + const foundId = await highlighterTestFront.getHighlighterNodeAttribute( + id, + "id" + ); + is(foundId, id, "Element " + id + " found"); + } + + info("Hide the box-model highlighter"); + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-05.js b/devtools/client/inspector/test/browser_inspector_highlighter-05.js new file mode 100644 index 0000000000..4c4fd4781c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-05.js @@ -0,0 +1,72 @@ +/* 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/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +// This is testing that the Anonymous Content is properly inserted into the document. +// Usually that is happening during the "interactive" state of the document, to have them +// ready as soon as possible. +// However, in some conditions, that's not possible since we don't have access yet to +// the `CustomContentContainer`, that is used to add the Anonymous Content. +// That can happen if the page has some external resource, as <link>, that takes time +// to load and / or returns the wrong content. This is not happening, for instance, with +// images. +// +// In those case, we want to be sure that if we're not able to insert the Anonymous +// Content at the "interactive" state, we're doing so when the document is loaded. +// +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1365075 + +const server = createTestHTTPServer(); +const filepath = "/slow.css"; +const cssuri = `http://localhost:${server.identity.primaryPort}${filepath}`; + +// Register a slow css file handler so we can simulate a long loading time. +server.registerContentType("css", "text/css"); +server.registerPathHandler(filepath, (metadata, response) => { + info("CSS has been requested"); + response.processAsync(); + setTimeout(() => { + info("CSS is responding"); + response.finish(); + }, 2000); +}); + +const TEST_URL = + "data:text/html," + + encodeURIComponent(` + <!DOCTYPE html> + <html> + <head> + <link href="${cssuri}" rel="stylesheet" /> + </head> + <body> + <p>Slow page</p> + </body> + </html> +`); + +add_task(async function () { + info("Open the inspector to a blank page."); + const { inspector } = await openInspectorForURL("about:blank"); + + info("Navigate to the test url and waiting for the page to be loaded."); + await navigateTo(TEST_URL); + + info("Shows the box model highligher for the <p> node."); + const nodeFront = await getNodeFront("p", inspector); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront + ); + + info("Check the node is highlighted."); + const highlighterTestFront = await getHighlighterTestFront(inspector.toolbox); + is( + await highlighterTestFront.isHighlighting(), + true, + "Box Model highlighter is working as expected." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-06.js b/devtools/client/inspector/test/browser_inspector_highlighter-06.js new file mode 100644 index 0000000000..11f1cee1a1 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-06.js @@ -0,0 +1,39 @@ +/* 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/. */ + +"use strict"; + +// Test that the browser remembers the previous scroll position after reload, even with +// Inspector opened - Bug 1382341. +// NOTE: Using a real file instead data: URL since the bug won't happen on data: URL +const TEST_URI = URL_ROOT + "doc_inspector_highlighter_scroll.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + await scrollContentPageNodeIntoView(gBrowser.selectedBrowser, "a"); + await selectAndHighlightNode("a", inspector); + + const markupLoaded = inspector.once("markuploaded"); + + const y = await getPageYOffset(); + isnot(y, 0, "window scrolled vertically."); + + info("Reloading page."); + await reloadBrowser(); + + info("Waiting for markupview to load after reload."); + await markupLoaded; + + const newY = await getPageYOffset(); + is(y, newY, "window remember the previous scroll position."); +}); + +async function getPageYOffset() { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.pageYOffset + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-07.js b/devtools/client/inspector/test/browser_inspector_highlighter-07.js new file mode 100644 index 0000000000..cb8c11cdd0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-07.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js", + this +); + +// Test that the highlighter works when the debugger is paused. + +function debuggerIsPaused(dbg) { + return !!dbg.selectors.getIsPaused(dbg.selectors.getCurrentThread()); +} + +function waitForPaused(dbg) { + return new Promise(resolve => { + if (debuggerIsPaused(dbg)) { + resolve(); + return; + } + + const unsubscribe = dbg.store.subscribe(() => { + if (debuggerIsPaused(dbg)) { + unsubscribe(); + resolve(); + } + }); + }); +} + +const IFRAME_SRC = + "<style>" + + "body {" + + "margin:0;" + + "height:100%;" + + "background-color:red" + + "}" + + "</style><body>hello from iframe</body>"; + +const DOCUMENT_SRC = + "<style>" + + "iframe {" + + "height:200px;" + + "border: 11px solid black;" + + "padding: 13px;" + + "}" + + "body,iframe {" + + "margin:0" + + "}" + + "</style>" + + "<body>" + + "<script>setInterval('debugger', 100)</script>" + + "<iframe src='data:text/html;charset=utf-8," + + IFRAME_SRC + + "'></iframe>" + + "</body>"; + +const TEST_URI = "data:text/html;charset=utf-8," + DOCUMENT_SRC; + +add_task(async function () { + const { inspector, toolbox, highlighterTestFront, tab } = + await openInspectorForURL(TEST_URI); + + await gDevTools.showToolboxForTab(tab, { toolId: "jsdebugger" }); + const dbg = await createDebuggerContext(toolbox); + + await waitForPaused(dbg); + + await gDevTools.showToolboxForTab(tab, { toolId: "inspector" }); + + // Needed to get this test to pass consistently :( + await waitForTime(1000); + + info("Waiting for box mode to show."); + const body = await getNodeFront("body", inspector); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + body + ); + + info("Waiting for element picker to become active."); + await startPicker(toolbox); + + info("Moving mouse over iframe padding."); + await hoverElement(inspector, "iframe", 1, 1); + + info("Performing checks"); + await isNodeCorrectlyHighlighted(highlighterTestFront, "iframe"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-08.js b/devtools/client/inspector/test/browser_inspector_highlighter-08.js new file mode 100644 index 0000000000..49b544a325 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-08.js @@ -0,0 +1,67 @@ +/* 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/. */ + +"use strict"; + +// Test that performing multiple requests to highlight nodes or to hide the highlighter, +// without waiting for the former ones to complete, still works well. +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html," + ); + const html = await getNodeFront("html", inspector); + const body = await getNodeFront("body", inspector); + const type = inspector.highlighters.TYPES.BOXMODEL; + const getActiveHighlighter = () => { + return inspector.highlighters.getActiveHighlighter(type); + }; + is(getActiveHighlighter(), null, "The highlighter is hidden by default"); + + info("Highlight <body>, and hide the highlighter immediately after"); + await Promise.all([ + inspector.highlighters.showHighlighterTypeForNode(type, body), + inspector.highlighters.hideHighlighterType(type), + ]); + is(getActiveHighlighter(), null, "The highlighter is hidden"); + + info("Highlight <body>, then <html>, then <body> again, synchronously"); + await Promise.all([ + inspector.highlighters.showHighlighterTypeForNode(type, body), + inspector.highlighters.showHighlighterTypeForNode(type, html), + inspector.highlighters.showHighlighterTypeForNode(type, body), + ]); + ok( + await highlighterTestFront.assertHighlightedNode("body"), + "The highlighter highlights <body>" + ); + + info("Highlight <html>, then <body>, then <html> again, synchronously"); + await Promise.all([ + inspector.highlighters.showHighlighterTypeForNode(type, html), + inspector.highlighters.showHighlighterTypeForNode(type, body), + inspector.highlighters.showHighlighterTypeForNode(type, html), + ]); + ok( + await highlighterTestFront.assertHighlightedNode("html"), + "The highlighter highlights <html>" + ); + + info("Hide the highlighter, and highlight <html> immediately after"); + await Promise.all([ + inspector.highlighters.hideHighlighterType(type), + inspector.highlighters.showHighlighterTypeForNode(type, body), + ]); + ok( + await highlighterTestFront.assertHighlightedNode("body"), + "The highlighter highlights <body>" + ); + + info("Highlight <html>, and hide the highlighter immediately after"); + await Promise.all([ + inspector.highlighters.showHighlighterTypeForNode(type, html), + inspector.highlighters.hideHighlighterType(type), + ]); + is(getActiveHighlighter(), null, "The highlighter is hidden"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_01.js new file mode 100644 index 0000000000..f0daf4bbe8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_01.js @@ -0,0 +1,36 @@ +/* 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/. */ + +"use strict"; + +// Test that highlighters can be configured to automatically hide after a delay. +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8,<p id='one'>TEST 1</p>" + ); + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + const HALF_SECOND = 500; + const nodeFront = await getNodeFront("#one", inspector); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + const onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + + info("Show Box Model Highlighter, then hide after half a second"); + inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + { duration: HALF_SECOND } + ); + + info("Wait for Box Model Highlighter shown"); + await onHighlighterShown; + info("Wait for Box Model Highlighter hidden"); + await onHighlighterHidden; +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_02.js new file mode 100644 index 0000000000..bebc87f593 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_02.js @@ -0,0 +1,45 @@ +/* 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/. */ + +"use strict"; + +// Test that configuring two different highlighters to autohide +// will not overwrite each other's timers. +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8,<p id='one'>TEST 1</p>" + ); + + const HALF_SECOND = 500; + const nodeFront = await getNodeFront("#one", inspector); + + const waitForShowEvents = waitForNEvents( + inspector.highlighters, + "highlighter-shown", + 2 + ); + const waitForHideEvents = waitForNEvents( + inspector.highlighters, + "highlighter-hidden", + 2 + ); + + info("Show Box Model Highlighter, then hide after half a second"); + inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + { duration: HALF_SECOND } + ); + + info("Show Selector Highlighter, then hide after half a second"); + inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.SELECTOR, + nodeFront, + { selector: "#one", duration: HALF_SECOND } + ); + + info("Waiting for 2 highlighter-shown and 2 highlighter-hidden events"); + await Promise.all([waitForShowEvents, waitForHideEvents]); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_03.js new file mode 100644 index 0000000000..188b99a7cb --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-autohide-config_03.js @@ -0,0 +1,79 @@ +/* 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/. */ + +"use strict"; + +const { getTimeoutMultiplier } = ChromeUtils.importESModule( + "chrome://remote/content/shared/AppInfo.sys.mjs" +); + +// Test that configuring a highlighter to autohide twice +// will replace the first timer and hide just once. +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8,<p id='one'>TEST 1</p>" + ); + + // On opt builds, use 500ms and 1000ms for popup timeouts. + // On debug builds, multiply the first timeout by the platform multiplier to avoid + // an extra `hidden` event between the `showHighlighterTypeForNode` commands. + const FIRST_POPUP_TIMEOUT = getTimeoutMultiplier() * 500; + const SECOND_POPUP_TIMEOUT = 1000; + + const nodeFront = await getNodeFront("#one", inspector); + + const waitForShowEvents = waitForNEvents( + inspector.highlighters, + "highlighter-shown", + 2 + ); + const waitForHideEvents = waitForNEvents( + inspector.highlighters, + "highlighter-hidden", + 1 + ); + + info("Show Box Model Highlighter, then hide after half a second"); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + { duration: FIRST_POPUP_TIMEOUT } + ); + + info("Show Box Model Highlighter again, then hide after one second"); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + { duration: SECOND_POPUP_TIMEOUT } + ); + + info("Waiting for 2 highlighter-shown and 1 highlighter-hidden event"); + await Promise.all([waitForShowEvents, waitForHideEvents]); + + /* + Since the second duration passed is longer than the first and is supposed to overwrite + the first, it is reasonable to expect that the "highlighter-hidden" event was emitted + after the second (longer) duration expired. As an added check, we naively wait for an + additional time amounting to the sum of both durations to check if the first timer was + somehow not overwritten and fires another "highlighter-hidden" event. + */ + let wasEmitted = false; + const waitForExtraEvent = new Promise((resolve, reject) => { + const _handler = () => { + wasEmitted = true; + resolve(); + }; + + inspector.highlighters.on("highlighter-hidden", _handler, { once: true }); + }); + + info("Wait to see if another highlighter-hidden event is emitted"); + await Promise.race([ + waitForExtraEvent, + wait(FIRST_POPUP_TIMEOUT + SECOND_POPUP_TIMEOUT), + ]); + + is(wasEmitted, false, "An extra highlighter-hidden event was not emitted"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-autohide.js b/devtools/client/inspector/test/browser_inspector_highlighter-autohide.js new file mode 100644 index 0000000000..4d658e77ad --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-autohide.js @@ -0,0 +1,77 @@ +/* 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/. */ + +"use strict"; + +// Test that clicking on a node in the markup view or picking a node with the node picker +// shows a highlighter which is automatically hidden after a delay. +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector, toolbox, highlighterTestFront } = + await openInspectorForURL( + "data:text/html;charset=utf-8,<p id='one'>TEST 1</p><p id='two'>TEST 2</p>" + ); + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + + // While in test mode, the configuration to automatically hide Box Model Highlighters + // after a delay is ignored to prevent intermittent test failures from race conditions. + // Restore this behavior just for this test because it is explicitly checked. + const HIGHLIGHTER_AUTOHIDE_TIMER = inspector.HIGHLIGHTER_AUTOHIDE_TIMER; + inspector.HIGHLIGHTER_AUTOHIDE_TIMER = 1000; + + registerCleanupFunction(() => { + // Restore the value to avoid impacting other tests. + inspector.HIGHLIGHTER_AUTOHIDE_TIMER = HIGHLIGHTER_AUTOHIDE_TIMER; + }); + + info( + "Check that selecting an element in the markup-view shows the highlighter and auto-hides it" + ); + let onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + const onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + + let delay; + const start = Date.now(); + onHighlighterHidden.then(() => { + delay = Date.now() - start; + }); + + await clickContainer("#one", inspector); + + info("Wait for Box Model Highlighter shown"); + await onHighlighterShown; + info("Wait for Box Model Highlighter hidden"); + await onHighlighterHidden; + + ok(true, "Highlighter was shown and hidden"); + Assert.greaterOrEqual( + delay, + inspector.HIGHLIGHTER_AUTOHIDE_TIMER, + `Highlighter was hidden after expected delay (${delay}ms)` + ); + + info("Check that picking a node hides the highlighter right away"); + onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + await startPicker(toolbox); + await hoverElement(inspector, "#two", 0, 0); + await onHighlighterShown; + ok( + await highlighterTestFront.isHighlighting(), + "Highlighter was shown when hovering the node" + ); + + await pickElement(inspector, "#two", 0, 0); + is( + await highlighterTestFront.isHighlighting(), + false, + "Highlighter gets hidden without delay after picking a node" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js b/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js new file mode 100644 index 0000000000..4687448ec9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js @@ -0,0 +1,73 @@ +/* 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/. */ + +"use strict"; + +// Check that custom highlighters can be retrieved by type and that they expose +// the expected API. + +const TEST_URL = "data:text/html;charset=utf-8,custom highlighters"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + await manyInstancesOfCustomHighlighters(inspector); + await showHideMethodsAreAvailable(inspector); + await unknownHighlighterTypeShouldntBeAccepted(inspector); +}); + +async function manyInstancesOfCustomHighlighters({ inspectorFront }) { + const h1 = await inspectorFront.getHighlighterByType("BoxModelHighlighter"); + const h2 = await inspectorFront.getHighlighterByType("BoxModelHighlighter"); + Assert.notStrictEqual( + h1, + h2, + "getHighlighterByType returns new instances every time (1)" + ); + + const h3 = await inspectorFront.getHighlighterByType( + "CssTransformHighlighter" + ); + const h4 = await inspectorFront.getHighlighterByType( + "CssTransformHighlighter" + ); + Assert.notStrictEqual( + h3, + h4, + "getHighlighterByType returns new instances every time (2)" + ); + ok( + h3 !== h1 && h3 !== h2, + "getHighlighterByType returns new instances every time (3)" + ); + ok( + h4 !== h1 && h4 !== h2, + "getHighlighterByType returns new instances every time (4)" + ); + + await h1.finalize(); + await h2.finalize(); + await h3.finalize(); + await h4.finalize(); +} + +async function showHideMethodsAreAvailable({ inspectorFront }) { + const h1 = await inspectorFront.getHighlighterByType("BoxModelHighlighter"); + const h2 = await inspectorFront.getHighlighterByType( + "CssTransformHighlighter" + ); + + ok("show" in h1, "Show method is present on the front API"); + ok("show" in h2, "Show method is present on the front API"); + ok("hide" in h1, "Hide method is present on the front API"); + ok("hide" in h2, "Hide method is present on the front API"); + + await h1.finalize(); + await h2.finalize(); +} + +async function unknownHighlighterTypeShouldntBeAccepted({ inspectorFront }) { + const h = await inspectorFront.getHighlighterByType("whatever"); + ok(!h, "No highlighter was returned for the invalid type"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js b/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js new file mode 100644 index 0000000000..4502870863 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js @@ -0,0 +1,94 @@ +/* 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/. */ + +"use strict"; + +// Test that canceling the element picker zooms back on the focused element. Bug 1224304. + +const TEST_URL = URL_ROOT + "doc_inspector_long-divs.html"; + +const isMac = Services.appinfo.OS === "Darwin"; +const TESTS = [ + { + key: "VK_ESCAPE", + options: {}, + // Escape would open the split console if the toolbox was focused + focusToolbox: false, + }, + { + key: "C", + options: { + metaKey: isMac, + ctrlKey: !isMac, + shiftKey: true, + }, + // Test with the toolbox focused to check the client side event listener. + focusToolbox: true, + }, + { + key: "C", + options: { + metaKey: isMac, + ctrlKey: !isMac, + shiftKey: true, + }, + // Test with the content page focused to check the actor's event listener. + focusToolbox: false, + }, +]; + +for (const { key, options, focusToolbox } of TESTS) { + add_task(async function () { + info(`Testing cancel shortcut: ${key} with toolbox focus: ${focusToolbox}`); + + const { inspector, toolbox, highlighterTestFront } = + await openInspectorForURL(TEST_URL); + await selectAndHighlightNode("#focus-here", inspector); + ok( + await highlighterTestFront.assertHighlightedNode("#focus-here"), + "The highlighter focuses on div#focus-here" + ); + ok( + isSelectedMarkupNodeInView(inspector), + "The currently selected node is on the screen." + ); + + await startPicker(toolbox); + + await hoverElement(inspector, "#zoom-here"); + ok( + !isSelectedMarkupNodeInView(inspector), + "The currently selected node is off the screen." + ); + + if (focusToolbox) { + toolbox.win.focus(); + } + + await cancelPickerByShortcut(toolbox, key, options); + ok( + isSelectedMarkupNodeInView(inspector), + "The currently selected node is focused back on the screen." + ); + + is( + await highlighterTestFront.isHighlighting(), + false, + "The highlighter was hidden" + ); + }); +} + +async function cancelPickerByShortcut(toolbox, key, options) { + info("Key pressed. Waiting for picker to be canceled."); + const onStopped = toolbox.nodePicker.once("picker-node-canceled"); + EventUtils.synthesizeKey(key, options, toolbox.win); + return onStopped; +} + +function isSelectedMarkupNodeInView(inspector) { + const selectedNodeContainer = inspector.markup._selectedContainer.elt; + const bounds = selectedNodeContainer.getBoundingClientRect(); + return bounds.top > 0 && bounds.bottom > 0; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-comments.js b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js new file mode 100644 index 0000000000..615e110d89 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js @@ -0,0 +1,119 @@ +/* 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/. */ +"use strict"; + +// Test that hovering over the markup-view's containers doesn't always show the +// highlighter, depending on the type of node hovered over. + +const TEST_PAGE = URL_ROOT + "doc_inspector_highlighter-comments.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_PAGE + ); + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + const markupView = inspector.markup; + await selectNode("p", inspector); + + info("Hovering over #id1 and waiting for highlighter to appear."); + await hoverElement("#id1"); + await assertHighlighterShownOn("#id1"); + + info("Hovering over comment node and ensuring highlighter doesn't appear."); + await hoverComment(); + await assertHighlighterHidden(); + + info("Hovering over #id1 again and waiting for highlighter to appear."); + await hoverElement("#id1"); + await assertHighlighterShownOn("#id1"); + + info("Hovering over #id2 and waiting for highlighter to appear."); + await hoverElement("#id2"); + await assertHighlighterShownOn("#id2"); + + info("Hovering over <script> and ensuring highlighter doesn't appear."); + await hoverElement("script"); + await assertHighlighterHidden(); + + info("Hovering over #id3 and waiting for highlighter to appear."); + await hoverElement("#id3"); + await assertHighlighterShownOn("#id3"); + + info("Hovering over hidden #id4 and ensuring highlighter doesn't appear."); + await hoverElement("#id4"); + await assertHighlighterHidden(); + + info("Hovering over a text node and waiting for highlighter to appear."); + await hoverTextNode("Visible text node"); + await assertHighlighterShownOnTextNode("body", 14); + + function hoverContainer(container) { + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + + container.tagLine.scrollIntoView(); + EventUtils.synthesizeMouse( + container.tagLine, + 2, + 2, + { type: "mousemove" }, + markupView.doc.defaultView + ); + + return onHighlighterShown; + } + + async function hoverElement(selector) { + info(`Hovering node ${selector} in the markup view`); + const container = await getContainerForSelector(selector, inspector); + return hoverContainer(container); + } + + function hoverComment() { + info("Hovering the comment node in the markup view"); + for (const [node, container] of markupView._containers) { + if (node.nodeType === Node.COMMENT_NODE) { + return hoverContainer(container); + } + } + return null; + } + + function hoverTextNode(text) { + info(`Hovering the text node "${text}" in the markup view`); + const container = [...markupView._containers].filter(([nodeFront]) => { + return ( + nodeFront.nodeType === Node.TEXT_NODE && + nodeFront._form.nodeValue.trim() === text.trim() + ); + })[0][1]; + return hoverContainer(container); + } + + async function assertHighlighterShownOn(selector) { + ok( + await highlighterTestFront.assertHighlightedNode(selector), + "Highlighter is shown on the right node: " + selector + ); + } + + async function assertHighlighterShownOnTextNode( + parentSelector, + childNodeIndex + ) { + ok( + await highlighterTestFront.assertHighlightedTextNode( + parentSelector, + childNodeIndex + ), + "Highlighter is shown on the right text node" + ); + } + + async function assertHighlighterHidden() { + const isVisible = await highlighterTestFront.isHighlighting(); + ok(!isVisible, "Highlighter is hidden"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js new file mode 100644 index 0000000000..cbc10c8843 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js @@ -0,0 +1,91 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the canvas highlighter element of the css grid highlighter. + +const TEST_URL = ` + <style type='text/css'> + #grid { + display: grid; + } + #cell1 { + grid-column: 1; + grid-row: 1; + } + #cell2 { + grid-column: 2; + grid-row: 1; + } + #cell3 { + grid-column: 1; + grid-row: 2; + } + #cell4 { + grid-column: 2; + grid-row: 2; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + <div id="cell3">cell3</div> + <div id="cell4">cell4</div> + </div> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URL) + ); + const front = inspector.inspectorFront; + const highlighter = await front.getHighlighterByType(HIGHLIGHTER_TYPE); + + await isHiddenByDefault(highlighterTestFront, highlighter); + await isVisibleWhenShown(highlighterTestFront, inspector, highlighter); + + await highlighter.finalize(); +}); + +async function isHiddenByDefault(highlighterTestFront, highlighterFront) { + info("Checking that the highlighter is hidden by default"); + + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-grid-canvas", + "hidden", + highlighterFront + ); + ok(hidden, "The highlighter is hidden by default"); +} + +async function isVisibleWhenShown( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Asking to show the highlighter on the test node"); + + const node = await getNodeFront("#grid", inspector); + await highlighterFront.show(node); + + let hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-grid-canvas", + "hidden", + highlighterFront + ); + ok(!hidden, "The highlighter is visible"); + + info("Hiding the highlighter"); + await highlighterFront.hide(); + + hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-grid-canvas", + "hidden", + highlighterFront + ); + ok(hidden, "The highlighter is hidden"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_02.js new file mode 100644 index 0000000000..2fabf9b34b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_02.js @@ -0,0 +1,51 @@ +/* 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/. */ + +"use strict"; + +// Test that grid layouts without items don't cause grid highlighter errors. + +const TEST_URL = ` + <style type='text/css'> + .grid { + display: grid; + grid-template-columns: 20px 20px; + grid-gap: 15px; + } + </style> + <div class="grid"></div> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URL) + ); + const front = inspector.inspectorFront; + const highlighter = await front.getHighlighterByType(HIGHLIGHTER_TYPE); + + info("Try to show the highlighter on the grid container"); + const node = await getNodeFront(".grid", inspector); + await highlighter.show(node); + + let hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-grid-canvas", + "hidden", + highlighter + ); + ok(!hidden, "The highlighter is visible"); + + info("Hiding the highlighter"); + await highlighter.hide(); + + hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-grid-canvas", + "hidden", + highlighter + ); + ok(hidden, "The highlighter is hidden"); + + await highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js new file mode 100644 index 0000000000..ae4e968263 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js @@ -0,0 +1,95 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the CSS shapes highlighter. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; +const SHAPE_IDS = ["polygon", "ellipse", "rect"]; +const SHAPE_TYPES = [ + { + shapeName: "polygon", + highlighter: "polygon", + }, + { + shapeName: "circle", + highlighter: "ellipse", + }, + { + shapeName: "ellipse", + highlighter: "ellipse", + }, + { + shapeName: "inset", + highlighter: "rect", + }, +]; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + const highlighter = await front.getHighlighterByType(HIGHLIGHTER_TYPE); + + await isHiddenByDefault(highlighterTestFront, highlighter); + await isVisibleWhenShown(highlighterTestFront, inspector, highlighter); + + await highlighter.finalize(); +}); + +async function getShapeHidden(highlighterTestFront, highlighterFront) { + const hidden = {}; + for (const shape of SHAPE_IDS) { + hidden[shape] = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-" + shape, + "hidden", + highlighterFront + ); + } + return hidden; +} + +async function isHiddenByDefault(highlighterTestFront, highlighterFront) { + info("Checking that highlighter is hidden by default"); + + const polygonHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-polygon", + "hidden", + highlighterFront + ); + const ellipseHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "hidden", + highlighterFront + ); + ok(polygonHidden && ellipseHidden, "The highlighter is hidden by default"); +} + +async function isVisibleWhenShown( + highlighterTestFront, + inspector, + highlighterFront +) { + for (const { shapeName, highlighter } of SHAPE_TYPES) { + info(`Asking to show the highlighter on the ${shapeName} node`); + + const node = await getNodeFront(`#${shapeName}`, inspector); + await highlighterFront.show(node, { mode: "cssClipPath" }); + + const hidden = await getShapeHidden(highlighterTestFront, highlighterFront); + ok(!hidden[highlighter], `The ${shapeName} highlighter is visible`); + } + + info("Hiding the highlighter"); + await highlighterFront.hide(); + + const hidden = await getShapeHidden(highlighterTestFront, highlighterFront); + ok( + hidden.polygon && hidden.ellipse && hidden.rect, + "The highlighter is hidden" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js new file mode 100644 index 0000000000..1cb72c7b4d --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js @@ -0,0 +1,157 @@ +/* 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/. */ + +"use strict"; + +// Make sure that the CSS shapes highlighters have the correct attributes. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + const highlighter = await front.getHighlighterByType(HIGHLIGHTER_TYPE); + + await polygonHasCorrectAttrs(highlighterTestFront, inspector, highlighter); + await circleHasCorrectAttrs(highlighterTestFront, inspector, highlighter); + await ellipseHasCorrectAttrs(highlighterTestFront, inspector, highlighter); + await insetHasCorrectAttrs(highlighterTestFront, inspector, highlighter); + + await highlighter.finalize(); +}); + +async function polygonHasCorrectAttrs( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Checking polygon highlighter has correct points"); + + const polygonNode = await getNodeFront("#polygon", inspector); + await highlighterFront.show(polygonNode, { mode: "cssClipPath" }); + + const points = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-polygon", + "points", + highlighterFront + ); + const realPoints = + "0,0 12.5,50 25,0 37.5,50 50,0 62.5,50 " + + "75,0 87.5,50 100,0 90,100 50,60 10,100"; + is(points, realPoints, "Polygon highlighter has correct points"); +} + +async function circleHasCorrectAttrs( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Checking circle highlighter has correct attributes"); + + const circleNode = await getNodeFront("#circle", inspector); + await highlighterFront.show(circleNode, { mode: "cssClipPath" }); + + const rx = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "rx", + highlighterFront + ); + const ry = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "ry", + highlighterFront + ); + const cx = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cx", + highlighterFront + ); + const cy = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cy", + highlighterFront + ); + + is(rx, "25", "Circle highlighter has correct rx"); + is(ry, "25", "Circle highlighter has correct ry"); + is(cx, "30", "Circle highlighter has correct cx"); + is(cy, "40", "Circle highlighter has correct cy"); +} + +async function ellipseHasCorrectAttrs( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Checking ellipse highlighter has correct attributes"); + + const ellipseNode = await getNodeFront("#ellipse", inspector); + await highlighterFront.show(ellipseNode, { mode: "cssClipPath" }); + + const rx = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "rx", + highlighterFront + ); + const ry = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "ry", + highlighterFront + ); + const cx = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cx", + highlighterFront + ); + const cy = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cy", + highlighterFront + ); + + is(rx, "40", "Ellipse highlighter has correct rx"); + is(ry, "30", "Ellipse highlighter has correct ry"); + is(cx, "25", "Ellipse highlighter has correct cx"); + is(cy, "30", "Ellipse highlighter has correct cy"); +} + +async function insetHasCorrectAttrs( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Checking rect highlighter has correct attributes"); + + const insetNode = await getNodeFront("#inset", inspector); + await highlighterFront.show(insetNode, { mode: "cssClipPath" }); + + const x = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "x", + highlighterFront + ); + const y = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "y", + highlighterFront + ); + const width = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "width", + highlighterFront + ); + const height = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "height", + highlighterFront + ); + + is(x, "15", "Rect highlighter has correct x"); + is(y, "25", "Rect highlighter has correct y"); + is(width, "72.5", "Rect highlighter has correct width"); + is(height, "45", "Rect highlighter has correct height"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_03.js new file mode 100644 index 0000000000..7d1b7d0ca6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_03.js @@ -0,0 +1,116 @@ +/* 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/. */ + +"use strict"; + +// Make sure that the CSS shapes highlighters have the correct size +// in different zoom levels and with different geometry-box. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; +const TEST_LEVELS = [0.5, 1, 2]; + +add_task(async function () { + const inspector = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector); + const { highlighterTestFront } = inspector; + + await testZoomSize(highlighterTestFront, helper); + await testGeometryBox(helper); + await testStrokeBox(helper); + + await helper.finalize(); +}); + +async function testZoomSize(highlighterTestFront, helper) { + await helper.show("#polygon", { mode: "cssClipPath" }); + const quads = await getAllAdjustedQuadsForContentPageElement("#polygon"); + const { top, left, width, height } = quads.border[0].bounds; + const expectedStyle = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`; + + // The top/left/width/height of the highlighter should not change at any zoom level. + // It should always match the element being highlighted. + for (const zoom of TEST_LEVELS) { + info(`Setting zoom level to ${zoom}.`); + + const onHighlighterUpdated = highlighterTestFront.once( + "highlighter-updated" + ); + // we need to await here to ensure the event listener was registered. + await highlighterTestFront.registerOneTimeHighlighterUpdate(helper.actorID); + + setContentPageZoomLevel(zoom); + await onHighlighterUpdated; + const style = await helper.getElementAttribute( + "shapes-shape-container", + "style" + ); + + is( + style, + expectedStyle, + `Highlighter has correct quads at zoom level ${zoom}` + ); + } + // reset zoom + setContentPageZoomLevel(1); +} + +async function testGeometryBox(helper) { + await helper.show("#ellipse", { mode: "cssClipPath" }); + let quads = await getAllAdjustedQuadsForContentPageElement("#ellipse"); + const { + top: cTop, + left: cLeft, + width: cWidth, + height: cHeight, + } = quads.content[0].bounds; + let expectedStyle = + `top:${cTop}px;left:${cLeft}px;` + `width:${cWidth}px;height:${cHeight}px;`; + let style = await helper.getElementAttribute( + "shapes-shape-container", + "style" + ); + is(style, expectedStyle, "Highlighter has correct quads for content-box"); + + await helper.show("#ellipse-padding-box", { mode: "cssClipPath" }); + quads = await getAllAdjustedQuadsForContentPageElement( + "#ellipse-padding-box" + ); + const { + top: pTop, + left: pLeft, + width: pWidth, + height: pHeight, + } = quads.padding[0].bounds; + expectedStyle = + `top:${pTop}px;left:${pLeft}px;` + `width:${pWidth}px;height:${pHeight}px;`; + style = await helper.getElementAttribute("shapes-shape-container", "style"); + is(style, expectedStyle, "Highlighter has correct quads for padding-box"); +} + +async function testStrokeBox(helper) { + // #rect has a stroke and doesn't have the clip-path option stroke-box, + // so we must adjust the quads to reflect the object bounding box. + await helper.show("#rect", { mode: "cssClipPath" }); + const quads = await getAllAdjustedQuadsForContentPageElement("#rect"); + const { top, left, width, height } = quads.border[0].bounds; + const { highlightedNode } = helper; + const computedStyle = await highlightedNode.getComputedStyle(); + const strokeWidth = computedStyle["stroke-width"].value; + const delta = parseFloat(strokeWidth) / 2; + + const expectedStyle = + `top:${top + delta}px;left:${left + delta}px;` + + `width:${width - 2 * delta}px;height:${height - 2 * delta}px;`; + const style = await helper.getElementAttribute( + "shapes-shape-container", + "style" + ); + is( + style, + expectedStyle, + "Highlighter has correct quads for SVG rect with stroke and stroke-box" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js new file mode 100644 index 0000000000..26fa1bda82 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js @@ -0,0 +1,539 @@ +/* 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/. */ + +"use strict"; + +// Test that shapes are updated correctly on mouse events. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(async function () { + const env = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + const { highlighterTestFront, inspector } = env; + const view = selectRuleView(inspector); + const highlighters = view.highlighters; + + const config = { + inspector, + view, + highlighters, + highlighterTestFront, + helper, + }; + + await testPolygonMovePoint(config); + await testPolygonAddPoint(config); + await testPolygonRemovePoint(config); + await testCircleMoveCenter(config); + await testCircleWithoutPosition(config); + await testEllipseMoveRadius(config); + await testInsetMoveEdges(config); + + helper.finalize(); +}); + +async function getComputedPropertyValue(selector, property, inspector) { + const highlightedNode = await getNodeFront(selector, inspector); + const computedStyle = + await highlightedNode.inspectorFront.pageStyle.getComputed(highlightedNode); + return computedStyle[property].value; +} + +async function setup(config) { + const { view, selector, property, inspector } = config; + info(`Turn on shapes highlighter for ${selector}`); + await selectNode(selector, inspector); + await toggleShapesHighlighter(view, selector, property, true); +} + +async function teardown(config) { + const { view, selector, property } = config; + info(`Turn off shapes highlighter for ${selector}`); + await toggleShapesHighlighter(view, selector, property, false); +} + +async function testPolygonMovePoint(config) { + const { inspector, view, highlighterTestFront, helper } = config; + const selector = "#polygon"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + const points = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-polygon", + "points", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ); + let [x, y] = points.split(" ")[0].split(","); + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { top, left, width, height } = quads.border[0].bounds; + x = left + (width * x) / 100; + y = top + (height * y) / 100; + const dx = width / 10; + const dyPercent = 10; + const dy = height / dyPercent; + + const onRuleViewChanged = view.once("ruleview-changed"); + info("Moving first polygon point"); + const { mouse } = helper; + await mouse.down(x, y); + await mouse.move(x + dx, y + dy); + await mouse.up(); + await reflowContentPage(); + info("Waiting for rule view changed from shape change"); + await onRuleViewChanged; + + const definition = await getComputedPropertyValue( + selector, + property, + inspector + ); + ok( + definition.includes(`${dx}px ${dyPercent}%`), + `Point moved to ${dx}px ${dyPercent}%` + ); + + await teardown({ selector, property, ...config }); +} + +async function testPolygonAddPoint(config) { + const { inspector, view, highlighterTestFront, helper } = config; + const selector = "#polygon"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + // Move first point to have same x as second point, then double click between + // the two points to add a new one. + const points = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-polygon", + "points", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ); + const pointsArray = points.split(" "); + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { top, left, width, height } = quads.border[0].bounds; + let [x1, y1] = pointsArray[0].split(","); + let [x2, y2] = pointsArray[1].split(","); + x1 = left + (width * x1) / 100; + x2 = left + (width * x2) / 100; + y1 = top + (height * y1) / 100; + y2 = top + (height * y2) / 100; + + const { mouse } = helper; + await mouse.down(x1, y1); + await mouse.move(x2, y1); + await mouse.up(); + await reflowContentPage(); + + let newPointX = x2; + let newPointY = (y1 + y2) / 2; + + const onRuleViewChanged = view.once("ruleview-changed"); + info("Adding new polygon point"); + BrowserTestUtils.synthesizeMouse( + ":root", + newPointX, + newPointY, + { clickCount: 2 }, + gBrowser.selectedTab.linkedBrowser + ); + + await reflowContentPage(); + info("Waiting for rule view changed from shape change"); + await onRuleViewChanged; + + // Decimal precision for coordinates with percentage units is 2 + const precision = 2; + // Round to the desired decimal precision and cast to Number to remove trailing zeroes. + newPointX = Number(((newPointX * 100) / width).toFixed(precision)); + newPointY = Number(((newPointY * 100) / height).toFixed(precision)); + const definition = await getComputedPropertyValue( + selector, + property, + inspector + ); + ok( + definition.includes(`${newPointX}% ${newPointY}%`), + "Point successfuly added" + ); + + await teardown({ selector, property, ...config }); +} + +async function testPolygonRemovePoint(config) { + const { inspector, highlighters, highlighterTestFront, helper } = config; + const selector = "#polygon"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + const points = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-polygon", + "points", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ); + const [x, y] = points.split(" ")[0].split(","); + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { top, left, width, height } = quads.border[0].bounds; + + const adjustedX = left + (width * x) / 100; + const adjustedY = top + (height * y) / 100; + + info("Move mouse over first point in highlighter"); + const onEventHandled = highlighters.once("highlighter-event-handled"); + const { mouse } = helper; + await mouse.move(adjustedX, adjustedY); + await onEventHandled; + const markerHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-marker-hover", + "hidden", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ); + ok(!markerHidden, "Marker on highlighter is visible"); + + info("Double click on first point in highlighter"); + const onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + BrowserTestUtils.synthesizeMouse( + ":root", + adjustedX, + adjustedY, + { clickCount: 2 }, + gBrowser.selectedTab.linkedBrowser + ); + + info("Waiting for shape changes to apply"); + await onShapeChangeApplied; + const definition = await getComputedPropertyValue( + selector, + property, + inspector + ); + ok(!definition.includes(`${x}% ${y}%`), "Point successfully removed"); + + await teardown({ selector, property, ...config }); +} + +async function testCircleMoveCenter(config) { + const { inspector, highlighters, highlighterTestFront, helper } = config; + const selector = "#circle"; + const property = "clip-path"; + + const onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await setup({ selector, property, ...config }); + + const cx = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cx", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const cy = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cy", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { width, height } = quads.border[0].bounds; + const cxPixel = (width * cx) / 100; + const cyPixel = (height * cy) / 100; + const dx = width / 10; + const dy = height / 10; + + info("Moving circle center"); + const { mouse } = helper; + await mouse.down(cxPixel, cyPixel, selector); + await mouse.move(cxPixel + dx, cyPixel + dy, selector); + await mouse.up(cxPixel + dx, cyPixel + dy, selector); + await reflowContentPage(); + info("Waiting for shape changes to apply"); + await onShapeChangeApplied; + + const definition = await getComputedPropertyValue( + selector, + property, + inspector + ); + ok( + definition.includes(`at ${cx + 10}% ${cy + 10}%`), + "Circle center successfully moved" + ); + + await teardown({ selector, property, ...config }); +} + +async function testCircleWithoutPosition(config) { + const { inspector, highlighters, highlighterTestFront, helper } = config; + const selector = "#circle-without-position"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + const rx = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "rx", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + + const cx = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cx", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const cy = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cy", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { width, height } = quads.content[0].bounds; + const highlightedNode = await getNodeFront(selector, inspector); + const computedStyle = + await highlightedNode.inspectorFront.pageStyle.getComputed(highlightedNode); + const paddingTop = parseFloat(computedStyle["padding-top"].value); + const paddingLeft = parseFloat(computedStyle["padding-left"].value); + const cxPixel = paddingLeft + (width * cx) / 100; + const cyPixel = paddingTop + (height * cy) / 100; + const rxPixel = cxPixel + (width * rx) / 100; + const dx = width / 10; + const dy = height / 10; + + const { mouse } = helper; + info("Moving circle rx"); + let onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(rxPixel, cyPixel, selector); + await mouse.move(rxPixel + dx, cyPixel, selector); + await mouse.up(rxPixel + dx, cyPixel, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + let definition = await getComputedPropertyValue( + selector, + property, + inspector + ); + is( + definition, + `circle(${rx + 10}%)`, + "Circle without position radiuses successfully changed" + ); + + info("Moving circle center"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(cxPixel, cyPixel, selector); + await mouse.move(cxPixel + dx, cyPixel, selector); + await mouse.up(cxPixel + dx, cyPixel, selector); + await reflowContentPage(); + info("Waiting for shape changes to apply"); + await onShapeChangeApplied; + + definition = await getComputedPropertyValue(selector, property, inspector); + is( + definition, + `circle(${rx + 10}% at ${cxPixel + dx}px ${cyPixel}px)`, + `Circle without position center (${cxPixel},${cyPixel}) successfully moved (${dx},${dy})` + ); + + await teardown({ selector, property, ...config }); +} + +async function testEllipseMoveRadius(config) { + const { inspector, highlighters, highlighterTestFront, helper } = config; + const selector = "#ellipse"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + const rx = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "rx", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const ry = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "ry", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const cx = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cx", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const cy = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-ellipse", + "cy", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const quads = await getAllAdjustedQuadsForContentPageElement("#ellipse"); + const { width, height } = quads.content[0].bounds; + const highlightedNode = await getNodeFront(selector, inspector); + const computedStyle = + await highlightedNode.inspectorFront.pageStyle.getComputed(highlightedNode); + const paddingTop = parseFloat(computedStyle["padding-top"].value); + const paddingLeft = parseFloat(computedStyle["padding-left"].value); + const cxPixel = paddingLeft + (width * cx) / 100; + const cyPixel = paddingTop + (height * cy) / 100; + const rxPixel = cxPixel + (width * rx) / 100; + const ryPixel = cyPixel + (height * ry) / 100; + const dx = width / 10; + const dy = height / 10; + + const { mouse } = helper; + info("Moving ellipse rx"); + await mouse.down(rxPixel, cyPixel, selector); + await mouse.move(rxPixel + dx, cyPixel, selector); + await mouse.up(rxPixel + dx, cyPixel, selector); + await reflowContentPage(); + + info("Moving ellipse ry"); + const onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(cxPixel, ryPixel, selector); + await mouse.move(cxPixel, ryPixel - dy, selector); + await mouse.up(cxPixel, ryPixel - dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const definition = await getComputedPropertyValue( + selector, + property, + inspector + ); + ok( + definition.includes(`${rx + 10}% ${ry - 10}%`), + "Ellipse radiuses successfully moved" + ); + + await teardown({ selector, property, ...config }); +} + +async function testInsetMoveEdges(config) { + const { inspector, highlighters, highlighterTestFront, helper } = config; + const selector = "#inset"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + const x = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "x", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const y = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "y", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const width = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "width", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const height = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "height", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { width: elemWidth, height: elemHeight } = quads.content[0].bounds; + + const left = (elemWidth * x) / 100; + const top = (elemHeight * y) / 100; + const right = left + (elemWidth * width) / 100; + const bottom = top + (elemHeight * height) / 100; + const xCenter = (left + right) / 2; + const yCenter = (top + bottom) / 2; + const dx = elemWidth / 10; + const dy = elemHeight / 10; + const { mouse } = helper; + + info("Moving inset top"); + let onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(xCenter, top, selector); + await mouse.move(xCenter, top + dy, selector); + await mouse.up(xCenter, top + dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + // TODO: Test bottom inset marker after Bug 1456777 is fixed. + // Bug 1456777 - https://bugzilla.mozilla.org/show_bug.cgi?id=1456777 + // The test element is larger than the viewport when tests run in headless mode. + // When moved, the bottom marker value is getting clamped to the viewport. + + info("Moving inset left"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(left, yCenter, selector); + await mouse.move(left + dx, yCenter, selector); + await mouse.up(left + dx, yCenter, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + info("Moving inset right"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(right, yCenter, selector); + await mouse.move(right + dx, yCenter, selector); + await mouse.up(right + dx, yCenter, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const definition = await getComputedPropertyValue( + selector, + property, + inspector + ); + + // NOTE: No change to bottom inset until Bug 1456777 is fixed. + ok( + definition.includes( + `${top + dy}px ${elemWidth - right - dx}px ${100 - y - height}% ${ + x + 10 + }%` + ), + "Inset edges successfully moved" + ); + + await teardown({ selector, property, ...config }); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js new file mode 100644 index 0000000000..194fc76cc4 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test hovering over shape points in the rule-view and shapes highlighter. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(async function () { + const env = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + const { highlighterTestFront, inspector } = env; + const view = selectRuleView(inspector); + const highlighters = view.highlighters; + const config = { + inspector, + view, + highlighters, + highlighterTestFront, + helper, + }; + + await highlightFromRuleView(config); + await highlightFromHighlighter(config); +}); + +async function setup(config) { + const { view, selector, property, inspector } = config; + info(`Turn on shapes highlighter for ${selector}`); + await selectNode(selector, inspector); + await toggleShapesHighlighter(view, selector, property, true); +} + +async function teardown(config) { + const { view, selector, property } = config; + info(`Turn off shapes highlighter for ${selector}`); + await toggleShapesHighlighter(view, selector, property, false); +} +/* + * Test that points hovered in the rule view will highlight corresponding points + * in the shapes highlighter on the page. + */ +async function highlightFromRuleView(config) { + const { view, highlighters, highlighterTestFront, inspector } = config; + const selector = "#polygon"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + const container = getRuleViewProperty(view, selector, property).valueSpan; + const shapesToggle = container.querySelector(".ruleview-shapeswatch"); + + const highlighterFront = + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE); + + let markerHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-marker-hover", + "hidden", + highlighterFront + ); + ok(markerHidden, "Hover marker on highlighter is not visible"); + + info("Hover over point 0 in rule view"); + const pointSpan = container.querySelector( + ".ruleview-shape-point[data-point='0']" + ); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + EventUtils.synthesizeMouseAtCenter( + pointSpan, + { type: "mousemove" }, + view.styleWindow + ); + await onHighlighterShown; + + info( + "Point in shapes highlighter is marked when same point in rule view is hovered" + ); + markerHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-marker-hover", + "hidden", + highlighterFront + ); + ok(!markerHidden, "Marker on highlighter is visible"); + + info("Move mouse off point"); + onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + EventUtils.synthesizeMouseAtCenter( + shapesToggle, + { type: "mousemove" }, + view.styleWindow + ); + await onHighlighterShown; + + markerHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-marker-hover", + "hidden", + highlighterFront + ); + ok(markerHidden, "Marker on highlighter is not visible"); + + await teardown({ selector, property, ...config }); +} + +/* + * Test that points hovered in the shapes highlighter on the page will highlight + * corresponding points in the rule view. + */ +async function highlightFromHighlighter(config) { + const { view, highlighters, highlighterTestFront, helper, inspector } = + config; + const selector = "#polygon"; + const property = "clip-path"; + + await setup({ selector, property, ...config }); + + const highlighterFront = + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE); + const { mouse } = helper; + const container = getRuleViewProperty(view, selector, property).valueSpan; + + info("Hover over first point in highlighter"); + let onEventHandled = highlighters.once("highlighter-event-handled"); + await mouse.move(0, 0); + await onEventHandled; + let markerHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-marker-hover", + "hidden", + highlighterFront + ); + ok(!markerHidden, "Marker on highlighter is visible"); + + info( + "Point in rule view is marked when same point in shapes highlighter is hovered" + ); + const pointSpan = container.querySelector( + ".ruleview-shape-point[data-point='0']" + ); + ok(pointSpan.classList.contains("active"), "Span for point 0 is active"); + + info("Move mouse off point"); + onEventHandled = highlighters.once("highlighter-event-handled"); + await mouse.move(100, 100); + await onEventHandled; + markerHidden = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-marker-hover", + "hidden", + highlighterFront + ); + ok(markerHidden, "Marker on highlighter is no longer visible"); + ok( + !pointSpan.classList.contains("active"), + "Span for point 0 is no longer active" + ); + + await teardown({ selector, property, ...config }); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-scale.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-scale.js new file mode 100644 index 0000000000..60f645943a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-scale.js @@ -0,0 +1,187 @@ +/* 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/. */ + +"use strict"; + +// Test that shapes are updated correctly on mouse events in transform mode. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; +const SHAPE_SELECTORS = ["#polygon-transform", "#circle", "#ellipse", "#inset"]; + +add_task(async function () { + const env = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + const { highlighterTestFront, inspector } = env; + const view = selectRuleView(inspector); + const highlighters = view.highlighters; + const config = { + inspector, + view, + highlighters, + highlighterTestFront, + helper, + }; + + await testScale(config); +}); + +async function setup(config) { + const { inspector, view, selector, property, options } = config; + await selectNode(selector, inspector); + await toggleShapesHighlighter(view, selector, property, true, options); +} + +async function teardown(config) { + const { view, selector, property } = config; + info(`Turn off shapes highlighter for ${selector}`); + await toggleShapesHighlighter(view, selector, property, false); +} + +async function testScale(config) { + const { helper, highlighters } = config; + const options = { transformMode: true }; + const property = "clip-path"; + + for (const selector of SHAPE_SELECTORS) { + await setup({ selector, property, options, ...config }); + const { mouse } = helper; + + const { nw, width, height, center } = await getBoundingBoxInPx({ + selector, + ...config, + }); + + // if the top or left edges are not visible, move the shape so it is. + if (nw[0] < 0 || nw[1] < 0) { + const [x, y] = center; + const dx = Math.max(0, -nw[0]); + const dy = Math.max(0, -nw[1]); + await mouse.down(x, y, selector); + await mouse.move(x + dx, y + dy, selector); + await mouse.up(x + dx, y + dy, selector); + await reflowContentPage(); + nw[0] += dx; + nw[1] += dy; + } + const dx = width / 10; + const dy = height / 10; + let onShapeChangeApplied; + + info("Scaling from nw"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(nw[0], nw[1], selector); + await mouse.move(nw[0] + dx, nw[1] + dy, selector); + await mouse.up(nw[0] + dx, nw[1] + dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const nwBB = await getBoundingBoxInPx({ selector, ...config }); + isnot(nwBB.nw[0], nw[0], `${selector} nw moved right after nw scale`); + isnot(nwBB.nw[1], nw[1], `${selector} nw moved down after nw scale`); + isnot(nwBB.width, width, `${selector} width reduced after nw scale`); + isnot(nwBB.height, height, `${selector} height reduced after nw scale`); + + info("Scaling from ne"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(nwBB.ne[0], nwBB.ne[1], selector); + await mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector); + await mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const neBB = await getBoundingBoxInPx({ selector, ...config }); + isnot(neBB.ne[0], nwBB.ne[0], `${selector} ne moved right after ne scale`); + isnot(neBB.ne[1], nwBB.ne[1], `${selector} ne moved down after ne scale`); + isnot(neBB.width, nwBB.width, `${selector} width reduced after ne scale`); + isnot( + neBB.height, + nwBB.height, + `${selector} height reduced after ne scale` + ); + + info("Scaling from sw"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(neBB.sw[0], neBB.sw[1], selector); + await mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, selector); + await mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const swBB = await getBoundingBoxInPx({ selector, ...config }); + isnot(swBB.sw[0], neBB.sw[0], `${selector} sw moved right after sw scale`); + isnot(swBB.sw[1], neBB.sw[1], `${selector} sw moved down after sw scale`); + isnot(swBB.width, neBB.width, `${selector} width reduced after sw scale`); + isnot( + swBB.height, + neBB.height, + `${selector} height reduced after sw scale` + ); + + info("Scaling from se"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(swBB.se[0], swBB.se[1], selector); + await mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, selector); + await mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const seBB = await getBoundingBoxInPx({ selector, ...config }); + isnot(seBB.se[0], swBB.se[0], `${selector} se moved right after se scale`); + isnot(seBB.se[1], swBB.se[1], `${selector} se moved down after se scale`); + isnot(seBB.width, swBB.width, `${selector} width reduced after se scale`); + isnot( + seBB.height, + swBB.height, + `${selector} height reduced after se scale` + ); + + await teardown({ selector, property, ...config }); + } +} + +async function getBoundingBoxInPx(config) { + const { highlighterTestFront, selector, inspector } = config; + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { width, height } = quads.content[0].bounds; + const highlightedNode = await getNodeFront(selector, inspector); + const highlighterFront = + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE); + const computedStyle = + await highlightedNode.inspectorFront.pageStyle.getComputed(highlightedNode); + const paddingTop = parseFloat(computedStyle["padding-top"].value); + const paddingLeft = parseFloat(computedStyle["padding-left"].value); + // path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers + const path = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-bounding-box", + "d", + highlighterFront + ); + const coords = path + .replace(/[MLZ]/g, "") + .split(" ") + .map((n, i) => { + return i % 2 === 0 + ? paddingLeft + (width * n) / 100 + : paddingTop + (height * n) / 100; + }); + + const nw = [coords[0], coords[1]]; + const ne = [coords[2], coords[3]]; + const se = [coords[4], coords[5]]; + const sw = [coords[6], coords[7]]; + const center = [(nw[0] + se[0]) / 2, (nw[1] + se[1]) / 2]; + const shapeWidth = Math.sqrt((ne[0] - nw[0]) ** 2 + (ne[1] - nw[1]) ** 2); + const shapeHeight = Math.sqrt((sw[0] - nw[0]) ** 2 + (sw[1] - nw[1]) ** 2); + + return { nw, ne, se, sw, center, width: shapeWidth, height: shapeHeight }; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-translate.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-translate.js new file mode 100644 index 0000000000..11a683188b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06-translate.js @@ -0,0 +1,127 @@ +/* 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/. */ + +"use strict"; + +// Test that shapes are updated correctly on mouse events in transform mode. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; +const SHAPE_SELECTORS = ["#polygon-transform", "#circle", "#ellipse", "#inset"]; + +add_task(async function () { + const env = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + const { highlighterTestFront, inspector } = env; + const view = selectRuleView(inspector); + const highlighters = view.highlighters; + const config = { + inspector, + view, + highlighters, + highlighterTestFront, + helper, + }; + + await testTranslate(config); +}); + +async function setup(config) { + const { inspector, view, selector, property, options } = config; + await selectNode(selector, inspector); + await toggleShapesHighlighter(view, selector, property, true, options); +} + +async function teardown(config) { + const { view, selector, property } = config; + info(`Turn off shapes highlighter for ${selector}`); + await toggleShapesHighlighter(view, selector, property, false); +} + +async function testTranslate(config) { + const { helper, highlighters } = config; + const options = { transformMode: true }; + const property = "clip-path"; + + for (const selector of SHAPE_SELECTORS) { + await setup({ selector, property, options, ...config }); + const { mouse } = helper; + + const { center, width, height } = await getBoundingBoxInPx({ + selector, + ...config, + }); + const [x, y] = center; + const dx = width / 10; + const dy = height / 10; + let onShapeChangeApplied; + + info(`Translating ${selector}`); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(x, y, selector); + await mouse.move(x + dx, y + dy, selector); + await mouse.up(x + dx, y + dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + let newBB = await getBoundingBoxInPx({ selector, ...config }); + isnot(newBB.center[0], x, `${selector} translated on y axis`); + isnot(newBB.center[1], y, `${selector} translated on x axis`); + + info(`Translating ${selector} back`); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(x + dx, y + dy, selector); + await mouse.move(x, y, selector); + await mouse.up(x, y, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + newBB = await getBoundingBoxInPx({ selector, ...config }); + is(newBB.center[0], x, `${selector} translated back on x axis`); + is(newBB.center[1], y, `${selector} translated back on y axis`); + + await teardown({ selector, property, ...config }); + } +} + +async function getBoundingBoxInPx(config) { + const { highlighterTestFront, selector, inspector } = config; + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { width, height } = quads.content[0].bounds; + const highlightedNode = await getNodeFront(selector, inspector); + const highlighterFront = + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE); + const computedStyle = + await highlightedNode.inspectorFront.pageStyle.getComputed(highlightedNode); + const paddingTop = parseFloat(computedStyle["padding-top"].value); + const paddingLeft = parseFloat(computedStyle["padding-left"].value); + // path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers + const path = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-bounding-box", + "d", + highlighterFront + ); + const coords = path + .replace(/[MLZ]/g, "") + .split(" ") + .map((n, i) => { + return i % 2 === 0 + ? paddingLeft + (width * n) / 100 + : paddingTop + (height * n) / 100; + }); + + const nw = [coords[0], coords[1]]; + const ne = [coords[2], coords[3]]; + const se = [coords[4], coords[5]]; + const sw = [coords[6], coords[7]]; + const center = [(nw[0] + se[0]) / 2, (nw[1] + se[1]) / 2]; + const shapeWidth = Math.sqrt((ne[0] - nw[0]) ** 2 + (ne[1] - nw[1]) ** 2); + const shapeHeight = Math.sqrt((sw[0] - nw[0]) ** 2 + (sw[1] - nw[1]) ** 2); + + return { nw, ne, se, sw, center, width: shapeWidth, height: shapeHeight }; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js new file mode 100644 index 0000000000..07df792c23 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js @@ -0,0 +1,179 @@ +/* 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/. */ + +"use strict"; + +// Test that shapes are updated correctly for scaling on one axis in transform mode. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; +const SHAPE_SELECTORS = ["#polygon-transform", "#ellipse"]; + +add_task(async function () { + const env = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + const { highlighterTestFront, inspector } = env; + const view = selectRuleView(inspector); + const highlighters = view.highlighters; + const config = { + inspector, + view, + highlighters, + highlighterTestFront, + helper, + }; + + await testOneDimScale(config); +}); + +async function setup(config) { + const { inspector, view, selector, property, options } = config; + await selectNode(selector, inspector); + await toggleShapesHighlighter(view, selector, property, true, options); +} + +async function teardown(config) { + const { view, selector, property } = config; + info(`Turn off shapes highlighter for ${selector}`); + await toggleShapesHighlighter(view, selector, property, false); +} + +async function testOneDimScale(config) { + const { helper, highlighters } = config; + const options = { transformMode: true }; + const property = "clip-path"; + + for (const selector of SHAPE_SELECTORS) { + await setup({ selector, property, options, ...config }); + const { mouse } = helper; + + const { nw, width, height, center } = await getBoundingBoxInPx({ + selector, + ...config, + }); + + // if the top or left edges are not visible, move the shape so it is. + if (nw[0] < 0 || nw[1] < 0) { + const [x, y] = center; + const dx = Math.max(0, -nw[0]); + const dy = Math.max(0, -nw[1]); + await mouse.down(x, y, selector); + await mouse.move(x + dx, y + dy, selector); + await mouse.up(x + dx, y + dy, selector); + await reflowContentPage(); + nw[0] += dx; + nw[1] += dy; + } + const dx = width / 10; + const dy = height / 10; + let onShapeChangeApplied; + + info("Scaling from w"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(nw[0], center[1], selector); + await mouse.move(nw[0] + dx, center[1], selector); + await mouse.up(nw[0] + dx, center[1], selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const wBB = await getBoundingBoxInPx({ selector, ...config }); + isnot(wBB.nw[0], nw[0], `${selector} nw moved right after w scale`); + is(wBB.nw[1], nw[1], `${selector} nw not moved down after w scale`); + isnot(wBB.width, width, `${selector} width reduced after w scale`); + is(wBB.height, height, `${selector} height not reduced after w scale`); + + info("Scaling from e"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(wBB.ne[0], center[1], selector); + await mouse.move(wBB.ne[0] - dx, center[1], selector); + await mouse.up(wBB.ne[0] - dx, center[1], selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const eBB = await getBoundingBoxInPx({ selector, ...config }); + isnot(eBB.ne[0], wBB.ne[0], `${selector} ne moved left after e scale`); + is(eBB.ne[1], wBB.ne[1], `${selector} ne not moved down after e scale`); + isnot(eBB.width, wBB.width, `${selector} width reduced after e scale`); + is(eBB.height, wBB.height, `${selector} height not reduced after e scale`); + + info("Scaling from s"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(eBB.center[0], eBB.sw[1], selector); + await mouse.move(eBB.center[0], eBB.sw[1] - dy, selector); + await mouse.up(eBB.center[0], eBB.sw[1] - dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const sBB = await getBoundingBoxInPx({ selector, ...config }); + is(sBB.sw[0], eBB.sw[0], `${selector} sw not moved right after w scale`); + isnot(sBB.sw[1], eBB.sw[1], `${selector} sw moved down after w scale`); + is(sBB.width, eBB.width, `${selector} width not reduced after w scale`); + isnot(sBB.height, eBB.height, `${selector} height reduced after w scale`); + + info("Scaling from n"); + onShapeChangeApplied = highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(sBB.center[0], sBB.nw[1], selector); + await mouse.move(sBB.center[0], sBB.nw[1] + dy, selector); + await mouse.up(sBB.center[0], sBB.nw[1] + dy, selector); + await reflowContentPage(); + await onShapeChangeApplied; + + const nBB = await getBoundingBoxInPx({ selector, ...config }); + is(nBB.nw[0], sBB.nw[0], `${selector} nw not moved right after n scale`); + isnot(nBB.nw[1], sBB.nw[1], `${selector} nw moved down after n scale`); + is(nBB.width, sBB.width, `${selector} width reduced after n scale`); + isnot( + nBB.height, + sBB.height, + `${selector} height not reduced after n scale` + ); + + await teardown({ selector, property, ...config }); + } +} + +async function getBoundingBoxInPx(config) { + const { highlighterTestFront, selector, inspector } = config; + const quads = await getAllAdjustedQuadsForContentPageElement(selector); + const { width, height } = quads.content[0].bounds; + const highlightedNode = await getNodeFront(selector, inspector); + const highlighterFront = + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE); + const computedStyle = + await highlightedNode.inspectorFront.pageStyle.getComputed(highlightedNode); + const paddingTop = parseFloat(computedStyle["padding-top"].value); + const paddingLeft = parseFloat(computedStyle["padding-left"].value); + // path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers + const path = await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-bounding-box", + "d", + highlighterFront + ); + const coords = path + .replace(/[MLZ]/g, "") + .split(" ") + .map((n, i) => { + return i % 2 === 0 + ? paddingLeft + (width * n) / 100 + : paddingTop + (height * n) / 100; + }); + + const nw = [coords[0], coords[1]]; + const ne = [coords[2], coords[3]]; + const se = [coords[4], coords[5]]; + const sw = [coords[6], coords[7]]; + const center = [(nw[0] + se[0]) / 2, (nw[1] + se[1]) / 2]; + const shapeWidth = Math.sqrt((ne[0] - nw[0]) ** 2 + (ne[1] - nw[1]) ** 2); + const shapeHeight = Math.sqrt((sw[0] - nw[0]) ** 2 + (sw[1] - nw[1]) ** 2); + + return { nw, ne, se, sw, center, width: shapeWidth, height: shapeHeight }; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js new file mode 100644 index 0000000000..3598939d27 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js @@ -0,0 +1,97 @@ +/* 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/. */ + +"use strict"; + +// Test that shapes in iframes are updated correctly on mouse events. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes_iframe.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(async function () { + const env = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + const { inspector } = env; + const view = selectRuleView(inspector); + const highlighters = view.highlighters; + const config = { inspector, view, highlighters, helper }; + + await testPolygonIframeMovePoint(config); +}); + +async function testPolygonIframeMovePoint(config) { + const { inspector, view, helper } = config; + const selector = "#polygon"; + const property = "clip-path"; + + info(`Turn on shapes highlighter for ${selector}`); + // Get a reference to the highlighter's target node inside the iframe. + const highlightedNode = await getNodeFrontInFrames( + ["#frame", selector], + inspector + ); + // Select the nested node so toggling of the shapes highlighter works from the rule view + await selectNode(highlightedNode, inspector); + await toggleShapesHighlighter(view, selector, property, true); + const { mouse } = helper; + + // We expect two ruleview-changed events: + // - one for previewing the change (DomRule::previewPropertyValue) + // - one for applying the change (DomRule::applyProperties) + let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + + info("Moving polygon point visible in iframe"); + // Iframe has 10px margin. Element in iframe is 800px by 800px. First point is at 0 0% + is( + await getClipPathPoint(highlightedNode, 0), + "0px 0%", + `First point is at 0 0%` + ); + + await mouse.down(10, 10); + await mouse.move(20, 20); + await mouse.up(); + await reflowContentPage(); + await onRuleViewChanged; + + // point moved from y 0 to 10px, 10/800 (iframe height) = 1,25% + is( + await getClipPathPoint(highlightedNode, 0), + "10px 1.25%", + `Point moved to 10px 1.25%` + ); + + onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + + info("Dragging mouse out of the iframe"); + // Iframe has 10px margin. Element in iframe is 800px by 800px. Second point is at 100px 50% + is( + await getClipPathPoint(highlightedNode, 1), + "100px 50%", + `Second point is at 100px 50%` + ); + + await mouse.down(110, 410); + await mouse.move(120, 510); + await mouse.up(); + await reflowContentPage(); + await onRuleViewChanged; + + // The point can't be moved out of the iframe boundary, so we can't really assert the + // y point here (as it depends on the horizontal scrollbar size + the shape control point size). + ok( + (await getClipPathPoint(highlightedNode, 1)).startsWith("110px"), + `Point moved to 110px` + ); + + info(`Turn off shapes highlighter for ${selector}`); + await toggleShapesHighlighter(view, selector, property, false); +} + +async function getClipPathPoint(node, pointIndex) { + const computedStyle = await node.inspectorFront.pageStyle.getComputed(node); + const definition = computedStyle["clip-path"].value; + const points = definition.replaceAll(/(^polygon\()|(\)$)/g, "").split(", "); + return points[pointIndex]; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_offset-path.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_offset-path.js new file mode 100644 index 0000000000..004d7e945f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_offset-path.js @@ -0,0 +1,204 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the CSS shapes highlighter for offset-path. + +const TEST_URL = `data:text/html,<meta charset=utf8>${encodeURIComponent(` + <style> + html, body {margin: 0; padding: 0; } + + .wrapper { + width: 100px; + height: 100px; + background: tomato; + } + + .target { + width: 20px; + height: 20px; + display: block; + background: gold; + offset-path: inset(10% 20%); + } + + #abs { + position: absolute; + background: cyan; + offset-path: inset(10% 20%); + } + </style> + <div class=wrapper> + <span id=regular class=target></span> + <span id=abs class=target></span> + </div>`)}`; + +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(async function () { + await pushPref("layout.css.motion-path-basic-shapes.enabled", true); + const env = await openInspectorForURL(TEST_URL); + const { highlighterTestFront, inspector } = env; + const view = selectRuleView(inspector); + + await selectNode("#regular", inspector); + + info(`Show offset-path shape highlighter for regular case`); + await toggleShapesHighlighter(view, ".target", "offset-path", true); + + info( + "Check that highlighter is drawn relatively to the selected node parent node" + ); + + const wrapperQuads = await getAllAdjustedQuadsForContentPageElement( + ".wrapper" + ); + const { + width: wrapperWidth, + height: wrapperHeight, + x: wrapperX, + y: wrapperY, + } = wrapperQuads.content[0].bounds; + + const rect = await getShapeHighlighterRect(highlighterTestFront, inspector); + // SVG stroke seems to impact boundingClientRect differently depending on platform/hardware. + // Let's assert that the delta is okay, and use it for the different assertions. + const delta = wrapperX + wrapperWidth * 0.2 - rect.x; + Assert.lessOrEqual(Math.abs(delta), 1, `delta is <=1 (${Math.abs(delta)})`); + + // Coming from inset(10% 20%) + let inlineOffset = 0.2 * wrapperWidth - delta; + let blockOffset = 0.1 * wrapperHeight - delta; + + is(rect.x, wrapperX + inlineOffset, "Rect has expected x"); + is(rect.y, wrapperY + blockOffset, "Rect has expected y"); + is(rect.width, wrapperWidth - inlineOffset * 2, "Rect has expected width"); + is(rect.height, wrapperHeight - blockOffset * 2, "Rect has expected height"); + + const x = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "x", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const y = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "y", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + const width = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "width", + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE) + ) + ); + + const left = (wrapperWidth * x) / 100; + const top = (wrapperHeight * y) / 100; + const right = left + (wrapperWidth * width) / 100; + const xCenter = (left + right) / 2; + const dy = wrapperHeight / 10; + + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + const { mouse } = helper; + + info("Moving inset top"); + const onShapeChangeApplied = view.highlighters.once( + "shapes-highlighter-changes-applied" + ); + await mouse.down(xCenter, top, ".wrapper"); + await mouse.move(xCenter, top + dy, ".wrapper"); + await mouse.up(xCenter, top + dy, ".wrapper"); + await reflowContentPage(); + await onShapeChangeApplied; + + const offsetPathAfterUpdate = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => + content.getComputedStyle(content.document.getElementById("regular")) + .offsetPath + ); + + is( + offsetPathAfterUpdate, + `inset(${top + dy}% 20% 10%)`, + "Inset edges successfully moved" + ); + + info(`Hide offset-path shape highlighter`); + await toggleShapesHighlighter(view, ".target", "offset-path", false); + + info(`Show offset-path shape highlighter for absolutely positioned element`); + await selectNode("#abs", inspector); + await toggleShapesHighlighter(view, ".target", "offset-path", true); + + const viewportClientRect = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const [quad] = content.document.getBoxQuads(); + return { + x: quad.p1.x, + y: quad.p1.y, + width: quad.p2.x - quad.p1.x, + height: quad.p3.y - quad.p2.y, + }; + } + ); + + const absRect = await getShapeHighlighterRect( + highlighterTestFront, + inspector + ); + + inlineOffset = 0.2 * viewportClientRect.width - delta; + blockOffset = 0.1 * viewportClientRect.height - delta; + + Assert.less( + Math.abs(absRect.x - (viewportClientRect.x + inlineOffset)), + 1, + `Rect approximately has expected x (got ${absRect.x}, expected ${ + viewportClientRect.x + inlineOffset + })` + ); + Assert.less( + Math.abs(absRect.y - (viewportClientRect.y + blockOffset)), + 1, + `Rect approximately has expected y (got ${absRect.y}, expected ${ + viewportClientRect.y + blockOffset + })` + ); + Assert.less( + Math.abs(absRect.width - (viewportClientRect.width - inlineOffset * 2)), + 1, + `Rect approximately has expected width (got ${absRect.width}, expected ${ + viewportClientRect.width - inlineOffset * 2 + })` + ); + Assert.less( + Math.abs(absRect.height - (viewportClientRect.height - blockOffset * 2)), + 1, + `Rect approximately has expected height (got ${absRect.height}, expected ${ + viewportClientRect.height - blockOffset * 2 + })` + ); + + info(`Hide offset-path shape highlighter for absolutely positioned element`); + await toggleShapesHighlighter(view, ".target", "offset-path", false); +}); + +async function getShapeHighlighterRect(highlighterTestFront, inspector) { + const highlighterFront = + inspector.inspectorFront.getKnownHighlighter(HIGHLIGHTER_TYPE); + return highlighterTestFront.getHighlighterBoundingClientRect( + "shapes-rect", + highlighterFront.actorID + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_percent.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_percent.js new file mode 100644 index 0000000000..f1a7b07fe3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_percent.js @@ -0,0 +1,121 @@ +/* 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/. */ + +"use strict"; + +// Make sure that the inset() highlighter displays correctly when using pixels +// on top of screen %. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes-percent.html"; +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + const highlighter = await front.getHighlighterByType(HIGHLIGHTER_TYPE); + + await insetHasCorrectAttrs(highlighterTestFront, inspector, highlighter); + + await highlighter.finalize(); +}); + +async function insetHasCorrectAttrs( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Testing inset() editor using pixels on page %"); + + const top = 10; + const right = 20; + const bottom = 30; + const left = 40; + + // Set the clip-path property + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [top, right, bottom, left], + (t, r, b, l) => { + content.document.querySelector( + "#inset" + ).style.clipPath = `inset(${t}px ${r}px ${b}px ${l}px)`; + } + ); + + // Get width and height of page + const { innerWidth, innerHeight } = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + innerWidth: content.innerWidth, + innerHeight: content.innerHeight, + }; + } + ); + + const insetNode = await getNodeFront("#inset", inspector); + await highlighterFront.show(insetNode, { mode: "cssClipPath" }); + + const x = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "x", + highlighterFront + ) + ); + const y = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "y", + highlighterFront + ) + ); + const width = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "width", + highlighterFront + ) + ); + const height = parseFloat( + await highlighterTestFront.getHighlighterNodeAttribute( + "shapes-rect", + "height", + highlighterFront + ) + ); + + // Convert pixels to screen percentage + const expectedX = (left / innerWidth) * 100; + const expectedY = (top / innerHeight) * 100; + const expectedWidth = ((innerWidth - (left + right)) / innerWidth) * 100; + const expectedHeight = ((innerHeight - (top + bottom)) / innerHeight) * 100; + + ok( + floatEq(x, expectedX), + `Rect highlighter has correct x (got ${x}, expected ${expectedX})` + ); + ok( + floatEq(y, expectedY), + `Rect highlighter has correct y (got ${y}, expected ${expectedY})` + ); + ok( + floatEq(width, expectedWidth), + `Rect highlighter has correct width (got ${width}, expected ${expectedWidth})` + ); + ok( + floatEq(height, expectedHeight), + `Rect highlighter has correct height (got ${height}, expected ${expectedHeight})` + ); +} + +/** + * Compare two floats with a tolerance of 0.1 + */ +function floatEq(f1, f2) { + return Math.abs(f1 - f2) < 0.1; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js new file mode 100644 index 0000000000..934625867b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js @@ -0,0 +1,229 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the SVG highlighter elements of the css transform +// highlighter. + +const TEST_URL = ` + <div id="transformed" + style="border:1px solid red;width:100px;height:100px;transform:skew(13deg);"> + </div> + <div id="untransformed" + style="border:1px solid blue;width:100px;height:100px;"> + </div> + <span id="inline" + style="transform:rotate(90deg);">this is an inline transformed element + </span> +`; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURI(TEST_URL) + ); + const front = inspector.inspectorFront; + + const highlighter = await front.getHighlighterByType( + "CssTransformHighlighter" + ); + + await isHiddenByDefault(highlighterTestFront, highlighter); + await has2PolygonsAnd4Lines(highlighterTestFront, highlighter); + await isNotShownForUntransformed( + highlighterTestFront, + inspector, + highlighter + ); + await isNotShownForInline(highlighterTestFront, inspector, highlighter); + await isVisibleWhenShown(highlighterTestFront, inspector, highlighter); + await linesLinkThePolygons(highlighterTestFront, inspector, highlighter); + + await highlighter.finalize(); +}); + +async function isHiddenByDefault(highlighterTestFront, highlighterFront) { + info("Checking that the highlighter is hidden by default"); + + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-elements", + "hidden", + highlighterFront + ); + ok(hidden, "The highlighter is hidden by default"); +} + +async function has2PolygonsAnd4Lines(highlighterTestFront, highlighterFront) { + info("Checking that the highlighter is made up of 4 lines and 2 polygons"); + + let value = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-untransformed", + "class", + highlighterFront + ); + is(value, "css-transform-untransformed", "The untransformed polygon exists"); + + value = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-transformed", + "class", + highlighterFront + ); + is(value, "css-transform-transformed", "The transformed polygon exists"); + + for (const nb of ["1", "2", "3", "4"]) { + value = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-line" + nb, + "class", + highlighterFront + ); + is(value, "css-transform-line", "The line " + nb + " exists"); + } +} + +async function isNotShownForUntransformed( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Asking to show the highlighter on the untransformed test node"); + + const node = await getNodeFront("#untransformed", inspector); + await highlighterFront.show(node); + + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-elements", + "hidden", + highlighterFront + ); + ok(hidden, "The highlighter is still hidden"); +} + +async function isNotShownForInline( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Asking to show the highlighter on the inline test node"); + + const node = await getNodeFront("#inline", inspector); + await highlighterFront.show(node); + + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-elements", + "hidden", + highlighterFront + ); + ok(hidden, "The highlighter is still hidden"); +} + +async function isVisibleWhenShown( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Asking to show the highlighter on the test node"); + + const node = await getNodeFront("#transformed", inspector); + await highlighterFront.show(node); + + let hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-elements", + "hidden", + highlighterFront + ); + ok(!hidden, "The highlighter is visible"); + + info("Hiding the highlighter"); + await highlighterFront.hide(); + + hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-elements", + "hidden", + highlighterFront + ); + ok(hidden, "The highlighter is hidden"); +} + +async function linesLinkThePolygons( + highlighterTestFront, + inspector, + highlighterFront +) { + info("Showing the highlighter on the transformed node"); + + const node = await getNodeFront("#transformed", inspector); + await highlighterFront.show(node); + + info("Checking that the 4 lines do link the 2 shape's corners"); + + const lines = []; + for (const nb of ["1", "2", "3", "4"]) { + const x1 = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-line" + nb, + "x1", + highlighterFront + ); + const y1 = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-line" + nb, + "y1", + highlighterFront + ); + const x2 = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-line" + nb, + "x2", + highlighterFront + ); + const y2 = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-line" + nb, + "y2", + highlighterFront + ); + lines.push({ x1, y1, x2, y2 }); + } + + let points1 = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-untransformed", + "points", + highlighterFront + ); + points1 = points1.split(" "); + + let points2 = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-transformed", + "points", + highlighterFront + ); + points2 = points2.split(" "); + + for (let i = 0; i < lines.length; i++) { + info("Checking line nb " + i); + const line = lines[i]; + + const p1 = points1[i].split(","); + is( + p1[0], + line.x1, + "line " + i + "'s first point matches the untransformed x coordinate" + ); + is( + p1[1], + line.y1, + "line " + i + "'s first point matches the untransformed y coordinate" + ); + + const p2 = points2[i].split(","); + is( + p2[0], + line.x2, + "line " + i + "'s first point matches the transformed x coordinate" + ); + is( + p2[1], + line.y2, + "line " + i + "'s first point matches the transformed y coordinate" + ); + } + + await highlighterFront.hide(); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js new file mode 100644 index 0000000000..9903a40da0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js @@ -0,0 +1,69 @@ +/* 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/. */ + +"use strict"; + +/* +Bug 1014547 - CSS transforms highlighter +Test that the highlighter elements created have the right size and coordinates. + +Note that instead of hard-coding values here, the assertions are made by +comparing with the result of getAdjustedQuads. + +There's a separate test for checking that getAdjustedQuads actually returns +sensible values +(devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js), +so the present test doesn't care about that, it just verifies that the css +transform highlighter applies those values correctly to the SVG elements +*/ + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_csstransform.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + + const highlighter = await front.getHighlighterByType( + "CssTransformHighlighter" + ); + + const nodeFront = await getNodeFront("#test-node", inspector); + + info("Displaying the transform highlighter on test node"); + await highlighter.show(nodeFront); + + const data = await getAllAdjustedQuadsForContentPageElement("#test-node"); + const [expected] = data.border; + + const points = await highlighterTestFront.getHighlighterNodeAttribute( + "css-transform-transformed", + "points", + highlighter + ); + const polygonPoints = points.split(" ").map(p => { + return { + x: +p.substring(0, p.indexOf(",")), + y: +p.substring(p.indexOf(",") + 1), + }; + }); + + for (let i = 1; i < 5; i++) { + is( + polygonPoints[i - 1].x, + expected["p" + i].x, + "p" + i + " x coordinate is correct" + ); + is( + polygonPoints[i - 1].y, + expected["p" + i].y, + "p" + i + " y coordinate is correct" + ); + } + + info("Hiding the transform highlighter"); + await highlighter.hide(); + await highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-custom-element.js b/devtools/client/inspector/test/browser_inspector_highlighter-custom-element.js new file mode 100644 index 0000000000..8d24505565 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-custom-element.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the picker works correctly with XBL anonymous nodes + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_custom_element.xhtml"; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + await startPicker(toolbox); + + info("Selecting the custom element"); + await hoverElement(inspector, "#custom-element"); + + info("Key pressed. Waiting for element to be picked"); + BrowserTestUtils.synthesizeKey("VK_RETURN", {}, gBrowser.selectedBrowser); + await Promise.all([ + inspector.selection.once("new-node-front"), + inspector.once("inspector-updated"), + ]); + + is( + inspector.selection.nodeFront.className, + "custom-element-anon", + "The .custom-element-anon inside the div was selected" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-embed.js b/devtools/client/inspector/test/browser_inspector_highlighter-embed.js new file mode 100644 index 0000000000..5da6d0ac08 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-embed.js @@ -0,0 +1,32 @@ +/* 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/. */ + +"use strict"; + +// Test that the highlighter can go inside <embed> elements + +const TEST_URL = URL_ROOT + "doc_inspector_embed.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Get a node inside the <embed> element and select/highlight it"); + const body = await getEmbeddedBody(inspector); + await selectAndHighlightNode(body, inspector); + + const selectedNode = inspector.selection.nodeFront; + is(selectedNode.tagName.toLowerCase(), "body", "The selected node is <body>"); + ok( + selectedNode.baseURI.endsWith("doc_inspector_menu.html"), + "The selected node is the <body> node inside the <embed> element" + ); +}); + +async function getEmbeddedBody({ walker }) { + const embed = await walker.querySelector(walker.rootNode, "embed"); + const { nodes } = await walker.children(embed); + const contentDoc = nodes[0]; + const body = await walker.querySelector(contentDoc, "body"); + return body; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js new file mode 100644 index 0000000000..8d1b355e9f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js @@ -0,0 +1,63 @@ +/* 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/. */ +"use strict"; + +// Test that the eyedropper can copy colors to the clipboard + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; +const TEST_URI = + "data:text/html;charset=utf-8,<style>html{background:red}</style>"; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URI).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + helper.prefix = ID; + + const { + show, + finalize, + waitForElementAttributeSet, + waitForElementAttributeRemoved, + } = helper; + + info("Show the eyedropper with the copyOnSelect option"); + await show("html", { copyOnSelect: true }); + + info( + "Make sure to wait until the eyedropper is done taking a screenshot of the page" + ); + await waitForElementAttributeSet("root", "drawn", helper); + + await waitForClipboardPromise(() => { + info("Activate the eyedropper so the background color is copied"); + EventUtils.synthesizeKey("KEY_Enter"); + }, "#ff0000"); + + ok(true, "The clipboard contains the right value"); + + await waitForElementAttributeRemoved("root", "drawn", helper); + await waitForElementAttributeSet("root", "hidden", helper); + ok(true, "The eyedropper is now hidden"); + + info("Check that the clipboard still contains the copied color"); + is(SpecialPowers.getClipboardData("text/plain"), "#ff0000"); + + info("Replace the clipboard content with another text"); + SpecialPowers.clipboardCopyString("not-a-color"); + is(SpecialPowers.getClipboardData("text/plain"), "not-a-color"); + + info("Click on the page again, check the clipboard was not updated"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "body", + {}, + gBrowser.selectedBrowser + ); + // Wait 500ms because nothing is observable when the test is successful. + await wait(500); + is(SpecialPowers.getClipboardData("text/plain"), "not-a-color"); + + finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js new file mode 100644 index 0000000000..0435aee919 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js @@ -0,0 +1,39 @@ +/* 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/. */ +"use strict"; + +// Test that the eyedropper opens correctly even when the page defines CSP headers. + +const TEST_URI = URL_ROOT + "doc_inspector_csp.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + const toggleButton = inspector.panelDoc.querySelector( + "#inspector-eyedropper-toggle" + ); + toggleButton.click(); + await TestUtils.waitForCondition(() => + highlighterTestFront.isEyeDropperVisible() + ); + + ok(true, "Eye dropper is visible"); + + await checkEyeDropperColorAt( + highlighterTestFront, + 5, + 5, + "#ff0000", + "The eyedropper holds the expected color for the top-level element" + ); + + info("Hide the eyedropper"); + toggleButton.click(); + await TestUtils.waitForCondition(async () => { + const visible = await highlighterTestFront.isEyeDropperVisible(); + return !visible; + }); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js new file mode 100644 index 0000000000..0c05aa219f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js @@ -0,0 +1,184 @@ +/* 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/. */ +"use strict"; + +// Test the eyedropper mouse and keyboard handling. + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; +const TEST_URI = ` +<style> + html{width:100%;height:100%;} +</style> +<body>eye-dropper test</body>`; + +const MOVE_EVENTS_DATA = [ + { type: "mouse", x: 200, y: 100, expected: { x: 200, y: 100 } }, + { type: "mouse", x: 100, y: 200, expected: { x: 100, y: 200 } }, + { type: "keyboard", key: "VK_LEFT", expected: { x: 99, y: 200 } }, + { + type: "keyboard", + key: "VK_LEFT", + shift: true, + expected: { x: 89, y: 200 }, + }, + { type: "keyboard", key: "VK_RIGHT", expected: { x: 90, y: 200 } }, + { + type: "keyboard", + key: "VK_RIGHT", + shift: true, + expected: { x: 100, y: 200 }, + }, + { type: "keyboard", key: "VK_DOWN", expected: { x: 100, y: 201 } }, + { + type: "keyboard", + key: "VK_DOWN", + shift: true, + expected: { x: 100, y: 211 }, + }, + { type: "keyboard", key: "VK_UP", expected: { x: 100, y: 210 } }, + { type: "keyboard", key: "VK_UP", shift: true, expected: { x: 100, y: 200 } }, + // Mouse initialization for left and top snapping + { type: "mouse", x: 7, y: 7, expected: { x: 7, y: 7 } }, + // Left Snapping + { + type: "keyboard", + key: "VK_LEFT", + shift: true, + expected: { x: 0, y: 7 }, + desc: "Left Snapping to x=0", + }, + // Top Snapping + { + type: "keyboard", + key: "VK_UP", + shift: true, + expected: { x: 0, y: 0 }, + desc: "Top Snapping to y=0", + }, + // Mouse initialization for right snapping + { + type: "mouse", + x: (width, height) => width - 5, + y: 0, + expected: { + x: (width, height) => width - 5, + y: 0, + }, + }, + // Right snapping + { + type: "keyboard", + key: "VK_RIGHT", + shift: true, + expected: { + x: (width, height) => width, + y: 0, + }, + desc: "Right snapping to x=max window width available", + }, + // Mouse initialization for bottom snapping + { + type: "mouse", + x: 0, + y: (width, height) => height - 5, + expected: { + x: 0, + y: (width, height) => height - 5, + }, + }, + // Bottom snapping + { + type: "keyboard", + key: "VK_DOWN", + shift: true, + expected: { + x: 0, + y: (width, height) => height, + }, + desc: "Bottom snapping to y=max window height available", + }, +]; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI) + ); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)({ + inspector, + highlighterTestFront, + }); + + helper.prefix = ID; + + await helper.show("html"); + await respondsToMoveEvents(helper); + await respondsToReturnAndEscape(helper); + + helper.finalize(); +}); + +async function respondsToMoveEvents(helper) { + info( + "Checking that the eyedropper responds to events from the mouse and keyboard" + ); + const { mouse } = helper; + const { width, height } = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const rect = content.document + .querySelector("html") + .getBoundingClientRect(); + return { width: rect.width, height: rect.height }; + } + ); + + for (let { type, x, y, key, shift, expected, desc } of MOVE_EVENTS_DATA) { + x = typeof x === "function" ? x(width, height) : x; + y = typeof y === "function" ? y(width, height) : y; + expected.x = + typeof expected.x === "function" ? expected.x(width, height) : expected.x; + expected.y = + typeof expected.y === "function" ? expected.y(width, height) : expected.y; + + if (typeof desc === "undefined") { + info(`Simulating a ${type} event to move to ${expected.x} ${expected.y}`); + } else { + info(`Simulating ${type} event: ${desc}`); + } + + if (type === "mouse") { + await mouse.move(x, y); + } else if (type === "keyboard") { + const options = shift ? { shiftKey: true } : {}; + await EventUtils.synthesizeAndWaitKey(key, options); + } + await checkPosition(expected, helper); + } +} + +async function checkPosition({ x, y }, { getElementAttribute }) { + const style = await getElementAttribute("root", "style"); + is( + style, + `top:${y}px;left:${x}px;`, + `The eyedropper is at the expected ${x} ${y} position` + ); +} + +async function respondsToReturnAndEscape({ isElementHidden, show }) { + info("Simulating return to select the color and hide the eyedropper"); + + await EventUtils.synthesizeAndWaitKey("VK_RETURN", {}); + let hidden = await isElementHidden("root"); + ok(hidden, "The eyedropper has been hidden"); + + info("Showing the eyedropper again and simulating escape to hide it"); + + await show("html"); + await EventUtils.synthesizeAndWaitKey("VK_ESCAPE", {}); + hidden = await isElementHidden("root"); + ok(hidden, "The eyedropper has been hidden again"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-frames.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-frames.js new file mode 100644 index 0000000000..926a97a80b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-frames.js @@ -0,0 +1,94 @@ +/* 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/. */ +"use strict"; + +// Test that the eyedropper works on a page with iframes. + +const TOP_LEVEL_BACKGROUND_COLOR = "#ff0000"; +const SAME_ORIGIN_FRAME_BACKGROUND_COLOR = "#00ee00"; +const REMOTE_FRAME_BACKGROUND_COLOR = "#0000dd"; + +const HTML = ` +<style> + body { + height: 100vh; + background: ${TOP_LEVEL_BACKGROUND_COLOR}; + margin: 0; + } + + div, iframe { + border: none; + display: block; + height: 100px; + text-align: center; + } +</style> +<div>top-level element</div> +<iframe src="https://example.com/document-builder.sjs?html=<style>body {background:${encodeURIComponent( + SAME_ORIGIN_FRAME_BACKGROUND_COLOR +)};text-align: center;}</style><body>same origin iframe</body>"></iframe> +<iframe src="https://example.org/document-builder.sjs?html=<style>body {background:${encodeURIComponent( + REMOTE_FRAME_BACKGROUND_COLOR +)};text-align: center;}</style><body>remote iframe</body>"></iframe> +`; +const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent( + HTML +)}`; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + const toggleButton = inspector.panelDoc.querySelector( + "#inspector-eyedropper-toggle" + ); + toggleButton.click(); + await TestUtils.waitForCondition(() => + highlighterTestFront.isEyeDropperVisible() + ); + + ok(true, "Eye dropper is visible"); + + const checkColorAt = (...args) => + checkEyeDropperColorAt(highlighterTestFront, ...args); + + // The content page has the following layout: + // + // +------------------------------------+ + // | top level div (#ff0000) | 100px + // +------------------------------------+ + // | same origin iframe (#00ee00) | 100px + // +------------------------------------+ + // | remote iframe (#0000dd) | 100px + // +------------------------------------+ + + await checkColorAt( + 50, + 50, + TOP_LEVEL_BACKGROUND_COLOR, + "The eyedropper holds the expected color for the top-level element" + ); + + await checkColorAt( + 50, + 150, + SAME_ORIGIN_FRAME_BACKGROUND_COLOR, + "The eyedropper holds the expected color for the same-origin iframe" + ); + + await checkColorAt( + 50, + 250, + REMOTE_FRAME_BACKGROUND_COLOR, + "The eyedropper holds the expected color for the remote iframe" + ); + + info("Hide the eyedropper"); + toggleButton.click(); + await TestUtils.waitForCondition(async () => { + const visible = await highlighterTestFront.isEyeDropperVisible(); + return !visible; + }); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-image.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-image.js new file mode 100644 index 0000000000..eb38767bf8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-image.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the eyedropper icon in the toolbar is enabled when viewing an image. + +const TEST_URL = + URL_ROOT + "img_browser_inspector_highlighter-eyedropper-image.png"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + info("Check the inspector toolbar when viewing an image"); + const button = inspector.panelDoc.querySelector( + "#inspector-eyedropper-toggle" + ); + ok(!button.disabled, "The button is enabled in the toolbar"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js new file mode 100644 index 0000000000..d9413e5d39 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js @@ -0,0 +1,145 @@ +/* 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/. */ +"use strict"; + +// Test the position of the eyedropper label. +// It should move around when the eyedropper is close to the edges of the viewport so as +// to always stay visible. + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; + +const HTML = ` +<style> +html, body {height: 100%; margin: 0;} +body {background: linear-gradient(red, gold); display: flex; justify-content: center; + align-items: center;} +</style> +Eyedropper label position test +`; +const TEST_PAGE = "data:text/html;charset=utf-8," + encodeURI(HTML); + +const TEST_DATA = [ + { + desc: "Move the mouse to the center of the screen", + getCoordinates: (width, height) => { + return { x: width / 2, y: height / 2 }; + }, + expectedPositions: { top: false, right: false, left: false }, + }, + { + desc: "Move the mouse to the center left", + getCoordinates: (width, height) => { + return { x: 0, y: height / 2 }; + }, + expectedPositions: { top: false, right: true, left: false }, + }, + { + desc: "Move the mouse to the center right", + getCoordinates: (width, height) => { + return { x: width, y: height / 2 }; + }, + expectedPositions: { top: false, right: false, left: true }, + }, + { + desc: "Move the mouse to the bottom center", + getCoordinates: (width, height) => { + return { x: width / 2, y: height }; + }, + expectedPositions: { top: true, right: false, left: false }, + }, + { + desc: "Move the mouse to the bottom left", + getCoordinates: (width, height) => { + return { x: 0, y: height }; + }, + expectedPositions: { top: true, right: true, left: false }, + }, + { + desc: "Move the mouse to the bottom right", + getCoordinates: (width, height) => { + return { x: width, y: height }; + }, + expectedPositions: { top: true, right: false, left: true }, + }, + { + desc: "Move the mouse to the top left", + getCoordinates: (width, height) => { + return { x: 0, y: 0 }; + }, + expectedPositions: { top: false, right: true, left: false }, + }, + { + desc: "Move the mouse to the top right", + getCoordinates: (width, height) => { + return { x: width, y: 0 }; + }, + expectedPositions: { top: false, right: false, left: true }, + }, +]; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_PAGE + ); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)({ + inspector, + highlighterTestFront, + }); + helper.prefix = ID; + + const { mouse, show, hide, finalize } = helper; + let { width, height } = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const rect = content.document + .querySelector("html") + .getBoundingClientRect(); + return { width: rect.width, height: rect.height }; + } + ); + + // This test fails in non-e10s windows if we use width and height. For some reasons, the + // mouse events can't be dispatched/handled properly when we try to move the eyedropper + // to the far right and/or bottom of the screen. So just removing 10px from each side + // fixes it. + width -= 10; + height -= 10; + + info("Show the eyedropper on the page"); + await show("html"); + + info( + "Move the eyedropper around and check that the label appears at the right place" + ); + for (const { desc, getCoordinates, expectedPositions } of TEST_DATA) { + info(desc); + const { x, y } = getCoordinates(width, height); + info(`Moving the mouse to ${x} ${y}`); + await mouse.move(x, y); + await checkLabelPositionAttributes(helper, expectedPositions); + } + + info("Hide the eyedropper"); + await hide(); + finalize(); +}); + +async function checkLabelPositionAttributes(helper, positions) { + for (const position in positions) { + is( + await hasAttribute(helper, position), + positions[position], + `The label was ${ + positions[position] ? "" : "not " + }moved to the ${position}` + ); + } +} + +async function hasAttribute({ getElementAttribute }, name) { + const value = await getElementAttribute("root", name); + return value !== null; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js new file mode 100644 index 0000000000..7ccc87bf88 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js @@ -0,0 +1,41 @@ +/* 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/. */ +"use strict"; + +// Test the basic structure of the eye-dropper highlighter. + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8,eye-dropper test" + ); + + info("Checking that the eyedropper is hidden by default"); + const eyeDropperVisible = await highlighterTestFront.isEyeDropperVisible(); + is(eyeDropperVisible, false, "The eyedropper is hidden by default"); + + const toggleButton = inspector.panelDoc.querySelector( + "#inspector-eyedropper-toggle" + ); + + info("Display the eyedropper by clicking on the inspector toolbar button"); + toggleButton.click(); + await TestUtils.waitForCondition(() => + highlighterTestFront.isEyeDropperVisible() + ); + ok(true, "Eye dropper is visible after clicking the button in the inspector"); + + const style = await highlighterTestFront.getEyeDropperElementAttribute( + "root", + "style" + ); + is(style, "top:100px;left:100px;", "The eyedropper is correctly positioned"); + + info("Hide the eyedropper by clicking on the inspector toolbar button again"); + toggleButton.click(); + await TestUtils.waitForCondition(async () => { + const visible = await highlighterTestFront.isEyeDropperVisible(); + return !visible; + }); + ok(true, "Eye dropper is not visible anymore"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js new file mode 100644 index 0000000000..07d6708440 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the eyedropper icons in the toolbar and in the color picker aren't displayed +// when the page isn't an HTML one. + +const TEST_URL = URL_ROOT_SSL + "doc_inspector_eyedropper_disabled.xhtml"; +const TEST_URL_2 = + "data:text/html;charset=utf-8,<h1 style='color:red'>HTML test page</h1>"; + +add_task(async function () { + await SpecialPowers.pushPermissions([ + { type: "allowXULXBL", allow: true, context: URL_ROOT_SSL }, + ]); + + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Check the inspector toolbar"); + let button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle"); + ok(isDisabled(button), "The button is hidden in the toolbar"); + + info("Check the color picker"); + await selectNode("#box", inspector); + + // Find the color swatch in the rule-view. + let ruleView = inspector.getPanel("ruleview").view; + let ruleViewDocument = ruleView.styleDocument; + let swatchEl = ruleViewDocument.querySelector(".ruleview-colorswatch"); + + info("Open the color picker"); + let cPicker = ruleView.tooltips.getTooltip("colorPicker"); + let onColorPickerReady = cPicker.once("ready"); + swatchEl.click(); + await onColorPickerReady; + + button = cPicker.tooltip.container.querySelector("#eyedropper-button"); + ok(isDisabled(button), "The button is disabled in the color picker"); + + // Close the picker to avoid pending Promise when the connection closes because of + // the navigation to the HTML document (See Bug 1721369). + cPicker.hide(); + + info("Navigate to a HTML document"); + const toolbarUpdated = inspector.once("inspector-toolbar-updated"); + await navigateTo(TEST_URL_2); + await toolbarUpdated; + + info("Check the inspector toolbar in HTML document"); + button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle"); + ok(!isDisabled(button), "The button is enabled in the toolbar"); + + info("Check the color picker in HTML document"); + // Find the color swatch in the rule-view. + await selectNode("h1", inspector); + + ruleView = inspector.getPanel("ruleview").view; + ruleViewDocument = ruleView.styleDocument; + swatchEl = ruleViewDocument.querySelector(".ruleview-colorswatch"); + + info("Open the color picker in HTML document"); + cPicker = ruleView.tooltips.getTooltip("colorPicker"); + onColorPickerReady = cPicker.once("ready"); + swatchEl.click(); + await onColorPickerReady; + + button = cPicker.tooltip.container.querySelector("#eyedropper-button"); + ok(!isDisabled(button), "The button is enabled in the color picker"); +}); + +function isDisabled(button) { + return button.disabled; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-zoom.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-zoom.js new file mode 100644 index 0000000000..ea8b3ef444 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-zoom.js @@ -0,0 +1,89 @@ +/* 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/. */ +"use strict"; + +// Test that the eye dropper works as expected when the page is zoomed-in. + +const COLOR_1 = "#aa0000"; +const COLOR_2 = "#0bb000"; +const COLOR_3 = "#00cc00"; +const COLOR_4 = "#000dd0"; + +const HTML = ` +<style> + body { + margin: 0; + } + div { + height: 50px; + width: 50px; + } +</style> +<div style="background-color: ${COLOR_1};"></div> +<div style="background-color: ${COLOR_2};"></div> +<div style="background-color: ${COLOR_3};"></div> +<div style="background-color: ${COLOR_4};"></div>`; +const TEST_URI = `http://example.com/document-builder.sjs?html=${encodeURIComponent( + HTML +)}`; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + info("Zoom in the page"); + setContentPageZoomLevel(2); + + const toggleButton = inspector.panelDoc.querySelector( + "#inspector-eyedropper-toggle" + ); + toggleButton.click(); + await TestUtils.waitForCondition(() => + highlighterTestFront.isEyeDropperVisible() + ); + + ok(true, "Eye dropper is visible"); + + const checkColorAt = (...args) => + checkEyeDropperColorAt(highlighterTestFront, ...args); + + // ⚠️ Note that we need to check the regular position, not the zoomed-in ones. + + await checkColorAt( + 25, + 10, + COLOR_1, + "The eyedropper holds the expected color for the first div" + ); + + await checkColorAt( + 25, + 60, + COLOR_2, + "The eyedropper holds the expected color for the second div" + ); + + await checkColorAt( + 25, + 110, + COLOR_3, + "The eyedropper holds the expected color for the third div" + ); + + await checkColorAt( + 25, + 160, + COLOR_4, + "The eyedropper holds the expected color for the fourth div" + ); + + info("Hide the eyedropper"); + toggleButton.click(); + await TestUtils.waitForCondition(async () => { + const visible = await highlighterTestFront.isEyeDropperVisible(); + return !visible; + }); + setContentPageZoomLevel(1); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js new file mode 100644 index 0000000000..c50dee30b0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js @@ -0,0 +1,96 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the geometry highlighter elements. + +const TEST_URL = `data:text/html;charset=utf-8, + <span id='inline'></span> + <div id='positioned' style=' + background:yellow; + position:absolute; + left:5rem; + top:30px; + right:300px; + bottom:10em;'></div> + <div id='sized' style=' + background:red; + width:5em; + height:50%;'></div>`; + +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const ID = "geometry-editor-"; +const SIDES = ["left", "right", "top", "bottom"]; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + const { finalize } = helper; + + helper.prefix = ID; + + await hasArrowsAndLabelsAndHandlers(helper); + await isHiddenForNonPositionedNonSizedElement(helper); + await sideArrowsAreDisplayedForPositionedNode(helper); + + finalize(); +}); + +async function hasArrowsAndLabelsAndHandlers({ getElementAttribute }) { + info("Checking that the highlighter has the expected arrows and labels"); + + for (const name of [...SIDES]) { + let value = await getElementAttribute("arrow-" + name, "class"); + is(value, ID + "arrow " + name, "The " + name + " arrow exists"); + + value = await getElementAttribute("label-text-" + name, "class"); + is(value, ID + "label-text", "The " + name + " label exists"); + + value = await getElementAttribute("handler-" + name, "class"); + is(value, ID + "handler-" + name, "The " + name + " handler exists"); + } +} + +async function isHiddenForNonPositionedNonSizedElement({ + show, + hide, + isElementHidden, +}) { + info("Asking to show the highlighter on an inline, non p ositioned element"); + + await show("#inline"); + + for (const name of [...SIDES]) { + let hidden = await isElementHidden("arrow-" + name); + ok(hidden, "The " + name + " arrow is hidden"); + + hidden = await isElementHidden("handler-" + name); + ok(hidden, "The " + name + " handler is hidden"); + } +} + +async function sideArrowsAreDisplayedForPositionedNode({ + show, + hide, + isElementHidden, +}) { + info("Asking to show the highlighter on the positioned node"); + + await show("#positioned"); + + for (const name of SIDES) { + let hidden = await isElementHidden("arrow-" + name); + ok(!hidden, "The " + name + " arrow is visible for the positioned node"); + + hidden = await isElementHidden("handler-" + name); + ok(!hidden, "The " + name + " handler is visible for the positioned node"); + } + + info("Hiding the highlighter"); + await hide(); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js new file mode 100644 index 0000000000..8875b95efb --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js @@ -0,0 +1,125 @@ +/* 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/. */ + +/* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the geometry highlighter labels are correct. + +const TEST_URL = `data:text/html;charset=utf-8, + <div id='positioned' style=' + background:yellow; + position:absolute; + left:5rem; + top:30px; + right:300px; + bottom:10em;'></div> + <div id='positioned2' style=' + background:blue; + position:absolute; + right:10%; + top:5vmin;'>test element</div> + <div id='relative' style=' + background:green; + position:relative; + top:10px; + left:20px; + bottom:30px; + right:40px; + width:100px; + height:100px;'></div> + <div id='relative2' style=' + background:grey; + position:relative; + top:0;bottom:-50px; + height:3em;'>relative</div>`; + +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const POSITIONED_ELEMENT_TESTS = [ + { + selector: "#positioned", + expectedLabels: [ + { side: "left", visible: true, label: "5rem" }, + { side: "top", visible: true, label: "30px" }, + { side: "right", visible: true, label: "300px" }, + { side: "bottom", visible: true, label: "10em" }, + ], + }, + { + selector: "#positioned2", + expectedLabels: [ + { side: "left", visible: false }, + { side: "top", visible: true, label: "5vmin" }, + { side: "right", visible: true, label: "10%" }, + { side: "bottom", visible: false }, + ], + }, + { + selector: "#relative", + expectedLabels: [ + { side: "left", visible: true, label: "20px" }, + { side: "top", visible: true, label: "10px" }, + { side: "right", visible: false }, + { side: "bottom", visible: false }, + ], + }, + { + selector: "#relative2", + expectedLabels: [ + { side: "left", visible: false }, + { side: "top", visible: true, label: "0px" }, + { side: "right", visible: false }, + { side: "bottom", visible: false }, + ], + }, +]; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + helper.prefix = ID; + + const { finalize } = helper; + + await positionLabelsAreCorrect(helper); + + await finalize(); +}); + +async function positionLabelsAreCorrect({ + show, + hide, + isElementHidden, + getElementTextContent, +}) { + info("Highlight nodes and check position labels"); + + for (const { selector, expectedLabels } of POSITIONED_ELEMENT_TESTS) { + info("Testing node " + selector); + + await show(selector); + + for (const { side, visible, label } of expectedLabels) { + const id = "label-" + side; + + const hidden = await isElementHidden(id); + if (visible) { + ok(!hidden, "The " + side + " label is visible"); + + const value = await getElementTextContent(id); + is(value, label, "The " + side + " label textcontent is correct"); + } else { + ok(hidden, "The " + side + " label is hidden"); + } + } + + info("Hiding the highlighter"); + await hide(); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js new file mode 100644 index 0000000000..e11b9484db --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js @@ -0,0 +1,72 @@ +/* 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/. */ + +/* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the right arrows/labels are shown even when the css properties are +// in several different css rules. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; +const PROPS = ["left", "right", "top", "bottom"]; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + helper.prefix = ID; + + const { finalize } = helper; + + await checkArrowsLabelsAndHandlers( + "#node2", + ["top", "left", "bottom", "right"], + helper + ); + + await checkArrowsLabelsAndHandlers("#node3", ["top", "left"], helper); + + await finalize(); +}); + +async function checkArrowsLabelsAndHandlers( + selector, + expectedProperties, + { show, hide, isElementHidden } +) { + info("Getting node " + selector + " from the page"); + + await show(selector); + + for (const name of expectedProperties) { + const hidden = + (await isElementHidden("arrow-" + name)) && + (await isElementHidden("handler-" + name)); + ok( + !hidden, + "The " + name + " label/arrow & handler is visible for node " + selector + ); + } + + // Testing that the other arrows are hidden + for (const name of PROPS) { + if (expectedProperties.includes(name)) { + continue; + } + const hidden = + (await isElementHidden("arrow-" + name)) && + (await isElementHidden("handler-" + name)); + ok( + hidden, + "The " + name + " arrow & handler is hidden for node " + selector + ); + } + + info("Hiding the highlighter"); + await hide(); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js new file mode 100644 index 0000000000..9fef4c98dd --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js @@ -0,0 +1,103 @@ +/* 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/. */ + +/* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the arrows and handlers are positioned correctly and have the right +// size. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const handlerMap = { + top: { cx: "x2", cy: "y2" }, + bottom: { cx: "x2", cy: "y2" }, + left: { cx: "x2", cy: "y2" }, + right: { cx: "x2", cy: "y2" }, +}; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + helper.prefix = ID; + + const { hide, finalize } = helper; + + await checkArrowsAndHandlers(helper, ".absolute-all-4", { + top: { x1: 506, y1: 51, x2: 506, y2: 61 }, + bottom: { x1: 506, y1: 451, x2: 506, y2: 251 }, + left: { x1: 401, y1: 156, x2: 411, y2: 156 }, + right: { x1: 901, y1: 156, x2: 601, y2: 156 }, + }); + + await checkArrowsAndHandlers(helper, ".relative", { + top: { x1: 901, y1: 51, x2: 901, y2: 91 }, + left: { x1: 401, y1: 97, x2: 651, y2: 97 }, + }); + + await checkArrowsAndHandlers(helper, ".fixed", { + top: { x1: 25, y1: 0, x2: 25, y2: 400 }, + left: { x1: 0, y1: 425, x2: 0, y2: 425 }, + }); + + info("Hiding the highlighter"); + await hide(); + await finalize(); +}); + +async function checkArrowsAndHandlers(helper, selector, arrows) { + info("Highlighting the test node " + selector); + + await helper.show(selector); + + for (const side in arrows) { + await checkArrowAndHandler(helper, side, arrows[side]); + } +} + +async function checkArrowAndHandler( + { getElementAttribute }, + name, + expectedCoords +) { + info("Checking " + name + "arrow and handler coordinates are correct"); + + const handlerX = await getElementAttribute("handler-" + name, "cx"); + const handlerY = await getElementAttribute("handler-" + name, "cy"); + + const expectedHandlerX = await getElementAttribute( + "arrow-" + name, + handlerMap[name].cx + ); + const expectedHandlerY = await getElementAttribute( + "arrow-" + name, + handlerMap[name].cy + ); + + is( + handlerX, + expectedHandlerX, + "coordinate X for handler " + name + " is correct." + ); + is( + handlerY, + expectedHandlerY, + "coordinate Y for handler " + name + " is correct." + ); + + for (const coordinate in expectedCoords) { + const value = await getElementAttribute("arrow-" + name, coordinate); + + is( + Math.floor(value), + expectedCoords[coordinate], + coordinate + " coordinate for arrow " + name + " is correct" + ); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js new file mode 100644 index 0000000000..7b68fd1042 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js @@ -0,0 +1,140 @@ +/* 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/. */ + +/* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the arrows/handlers and offsetparent and currentnode elements of +// the geometry highlighter only appear when needed. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_02.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const TEST_DATA = [ + { + selector: "body", + isOffsetParentVisible: false, + isCurrentNodeVisible: false, + hasVisibleArrowsAndHandlers: false, + }, + { + selector: "h1", + isOffsetParentVisible: false, + isCurrentNodeVisible: false, + hasVisibleArrowsAndHandlers: false, + }, + { + selector: ".absolute", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true, + }, + { + selector: "#absolute-container", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: false, + }, + { + selector: ".absolute-bottom-right", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true, + }, + { + selector: ".absolute-width-margin", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true, + }, + { + selector: ".absolute-all-4", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true, + }, + { + selector: ".relative", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true, + }, + { + selector: ".static", + isOffsetParentVisible: false, + isCurrentNodeVisible: false, + hasVisibleArrowsAndHandlers: false, + }, + { + selector: ".static-size", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: false, + }, + { + selector: ".fixed", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true, + }, +]; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + helper.prefix = ID; + + const { hide, finalize } = helper; + + for (const data of TEST_DATA) { + await testNode(helper, data); + } + + info("Hiding the highlighter"); + await hide(); + await finalize(); +}); + +async function testNode(helper, data) { + const { selector } = data; + await helper.show(data.selector); + + is( + await isOffsetParentVisible(helper), + data.isOffsetParentVisible, + "The offset-parent highlighter visibility is correct for node " + selector + ); + is( + await isCurrentNodeVisible(helper), + data.isCurrentNodeVisible, + "The current-node highlighter visibility is correct for node " + selector + ); + is( + await hasVisibleArrowsAndHandlers(helper), + data.hasVisibleArrowsAndHandlers, + "The arrows visibility is correct for node " + selector + ); +} + +async function isOffsetParentVisible({ isElementHidden }) { + return !(await isElementHidden("offset-parent")); +} + +async function isCurrentNodeVisible({ isElementHidden }) { + return !(await isElementHidden("current-node")); +} + +async function hasVisibleArrowsAndHandlers({ isElementHidden }) { + for (const side of ["top", "left", "bottom", "right"]) { + const hidden = await isElementHidden("arrow-" + side); + if (!hidden) { + return !(await isElementHidden("handler-" + side)); + } + } + return false; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js new file mode 100644 index 0000000000..c4064792cd --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js @@ -0,0 +1,166 @@ +/* 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/. */ + +"use strict"; + +// Test that the geometry editor resizes properly an element on all sides, +// with different unit measures, and that arrow/handlers are updated correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const SIDES = ["top", "right", "bottom", "left"]; + +// The object below contains all the tests for this unit test. +// The property's name is the test's description, that points to an +// object contains the steps (what side of the geometry editor to drag, +// the amount of pixels) and the expectation. +const TESTS = { + "Drag top's handler along x and y, south-east direction": { + expects: "Only y axis is used to updated the top's element value", + drag: "top", + by: { x: 10, y: 10 }, + }, + "Drag right's handler along x and y, south-east direction": { + expects: "Only x axis is used to updated the right's element value", + drag: "right", + by: { x: 10, y: 10 }, + }, + "Drag bottom's handler along x and y, south-east direction": { + expects: "Only y axis is used to updated the bottom's element value", + drag: "bottom", + by: { x: 10, y: 10 }, + }, + "Drag left's handler along x and y, south-east direction": { + expects: "Only y axis is used to updated the left's element value", + drag: "left", + by: { x: 10, y: 10 }, + }, + "Drag top's handler along x and y, north-west direction": { + expects: "Only y axis is used to updated the top's element value", + drag: "top", + by: { x: -20, y: -20 }, + }, + "Drag right's handler along x and y, north-west direction": { + expects: "Only x axis is used to updated the right's element value", + drag: "right", + by: { x: -20, y: -20 }, + }, + "Drag bottom's handler along x and y, north-west direction": { + expects: "Only y axis is used to updated the bottom's element value", + drag: "bottom", + by: { x: -20, y: -20 }, + }, + "Drag left's handler along x and y, north-west direction": { + expects: "Only y axis is used to updated the left's element value", + drag: "left", + by: { x: -20, y: -20 }, + }, +}; + +add_task(async function () { + const inspector = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector); + + helper.prefix = ID; + + const { show, hide, finalize } = helper; + + info("Showing the highlighter"); + await show("#node2"); + + for (const desc in TESTS) { + await executeTest(helper, desc, TESTS[desc]); + } + + info("Hiding the highlighter"); + await hide(); + await finalize(); +}); + +async function executeTest(helper, desc, data) { + info(desc); + + ok( + await areElementAndHighlighterMovedCorrectly(helper, data.drag, data.by), + data.expects + ); +} + +async function areElementAndHighlighterMovedCorrectly(helper, side, by) { + const { mouse, highlightedNode } = helper; + + const { x, y } = await getHandlerCoords(helper, side); + + const dx = x + by.x; + const dy = y + by.y; + + const beforeDragStyle = await highlightedNode.getComputedStyle(); + + // simulate drag & drop + await mouse.down(x, y); + await mouse.move(dx, dy); + await mouse.up(); + + await reflowContentPage(); + + info(`Checking ${side} handler is moved correctly`); + await isHandlerPositionUpdated(helper, side, x, y, by); + + let delta = side === "left" || side === "right" ? by.x : by.y; + delta = delta * (side === "right" || side === "bottom" ? -1 : 1); + + info("Checking element's sides are correct after drag & drop"); + return areElementSideValuesCorrect( + highlightedNode, + beforeDragStyle, + side, + delta + ); +} + +async function isHandlerPositionUpdated(helper, name, x, y, by) { + const { x: afterDragX, y: afterDragY } = await getHandlerCoords(helper, name); + + if (name === "left" || name === "right") { + is(afterDragX, x + by.x, `${name} handler's x axis updated.`); + is(afterDragY, y, `${name} handler's y axis unchanged.`); + } else { + is(afterDragX, x, `${name} handler's x axis unchanged.`); + is(afterDragY, y + by.y, `${name} handler's y axis updated.`); + } +} + +async function areElementSideValuesCorrect(node, beforeDragStyle, name, delta) { + const afterDragStyle = await node.getComputedStyle(); + let isSideCorrect = true; + + for (const side of SIDES) { + const afterValue = Math.round(parseFloat(afterDragStyle[side].value)); + const beforeValue = Math.round(parseFloat(beforeDragStyle[side].value)); + + if (side === name) { + // `isSideCorrect` is used only as test's return value, not to perform + // the actual test, because with `is` instead of `ok` we gather more + // information in case of failure + isSideCorrect = isSideCorrect && afterValue === beforeValue + delta; + + is(afterValue, beforeValue + delta, `${side} is updated.`); + } else { + isSideCorrect = isSideCorrect && afterValue === beforeValue; + + is(afterValue, beforeValue, `${side} is unchaged.`); + } + } + + return isSideCorrect; +} + +async function getHandlerCoords({ getElementAttribute }, side) { + return { + x: Math.round(await getElementAttribute("handler-" + side, "cx")), + y: Math.round(await getElementAttribute("handler-" + side, "cy")), + }; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_07.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_07.js new file mode 100644 index 0000000000..e371ed4ca9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_07.js @@ -0,0 +1,146 @@ +/* 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/. */ + +"use strict"; + +// Test that the original position of a relative positioned node is correct +// even when zooming. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const TESTS = { + ".absolute-all-4": { + desc: "Check absolute positioned element's parentOffset highlight", + sides: ["top", "right", "bottom", "left"], + }, + ".relative": { + desc: "Check relative positioned element original offset highlight", + sides: ["top", "left"], + }, +}; + +add_task(async function () { + const inspector = await openInspectorForURL(TEST_URL); + const helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector); + + helper.prefix = ID; + + const { finalize } = helper; + + for (const selector in TESTS) { + await executeTest(inspector, helper, selector, TESTS[selector]); + } + + await finalize(); +}); + +async function executeTest(inspector, helper, selector, data) { + const { show, hide } = helper; + + info("Showing the highlighter"); + await show(selector); + info(data.desc); + + await checkOffsetParentSame(helper, selector, data.sides); + + setContentPageZoomLevel(1.2); + await reflowContentPage(); + + await checkOffsetParentSame(helper, selector, data.sides); + + info("Hiding the highlighter and resetting zoom"); + setContentPageZoomLevel(1); + await hide(); +} + +// Check that the offsetParent element does not move around, +// when the target element itself is being moved. +async function checkOffsetParentSame({ getElementAttribute }, selector, sides) { + const expectedOffsetParent = splitPointsIntoCoordinates( + await getElementAttribute("offset-parent", "points") + ); + const expectedArrowStartPos = await checkArrowStartingPos( + getElementAttribute, + expectedOffsetParent, + sides + ); + + // Setting both to 0px would disable the offsetParent highlighter and not update points. + await setContentPageElementAttribute(selector, "style", "top:1px; left:1px;"); + await reflowContentPage(); + + const actualOffsetParent = splitPointsIntoCoordinates( + await getElementAttribute("offset-parent", "points") + ); + const actualArrowStartPos = await checkArrowStartingPos( + getElementAttribute, + actualOffsetParent, + sides + ); + + await removeContentPageElementAttribute(selector, "style"); + + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 2; j++) { + is( + actualOffsetParent[i][j], + expectedOffsetParent[i][j], + `Coordinate ${j + 1}/2 of point ${ + i + 1 + }/4 is the same after repositioning.` + ); + } + } + + for (let i = 0; i < expectedArrowStartPos.length; i++) { + is( + actualArrowStartPos[i], + expectedArrowStartPos[i], + `Start position of arrow-${sides[i]} is the same after repositioning.` + ); + } +} + +// Check that the arrow starts at the boundary of the offsetParent. +// This also returns the start coordinate along the relevant axis. +async function checkArrowStartingPos( + getElementAttribute, + offsetParentPoints, + sides +) { + // offsetParentPoints is a number[][]. + offsetParentPoints = offsetParentPoints.flat(); + const result = []; + + for (const side of sides) { + // The only attribute that must not change is the start position of the arrow. + // Cross-axis and end position could change when the target element is moved. + const axis = side === "top" || side === "bottom" ? "y" : "x"; + // We have to round because floating point arithmetics are not consistent. + // (They are consistent enough, they just vary in their 10^(-5)) + const arrowPos = Math.round( + parseFloat(await getElementAttribute("arrow-" + side, axis + "1")) + ); + ok( + offsetParentPoints.includes(arrowPos), + `arrow-${side} starts at offset parent` + ); + result.push(arrowPos); + } + + return result; +} + +// Splits the value of the points attribute into coordinates. +function splitPointsIntoCoordinates(points) { + const result = []; + for (const coord of points.split(" ")) { + // We have to round (see above). + result.push(coord.split(",").map(s => Math.round(parseFloat(s)))); + } + + return result; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_hide_on_interaction.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_hide_on_interaction.js new file mode 100644 index 0000000000..4b44561b61 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_hide_on_interaction.js @@ -0,0 +1,80 @@ +/* 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/. */ + +"use strict"; + +// Test that the geometry highlighter gets hidden when the user performs other actions. + +const TEST_URL = `data:text/html;charset=utf-8, + <h1 style='background:yellow;position:absolute;left:5rem;'>Hello</h1>`; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + info("Select the absolute positioned element"); + await selectNode("h1", inspector); + const boxModelPanel = inspector.getPanel("boxmodel"); + + info("Click on the button enabling the highlighter"); + const button = await waitFor(() => + boxModelPanel.document.querySelector(".layout-geometry-editor") + ); + + let onHighlighterShown = getOnceHighlighterShown(inspector); + button.click(); + + await onHighlighterShown; + ok(true, "Highlighter is displayed"); + + info("Check that hovering a node in the markup view hides the highlighter"); + const h1Nodecontainer = await getContainerForSelector("h1", inspector); + + let onHighlighterHidden = getOnceHighlighterHidden(inspector); + EventUtils.synthesizeMouseAtCenter( + h1Nodecontainer.tagLine, + { type: "mousemove" }, + inspector.markup.doc.defaultView + ); + await onHighlighterHidden; + ok("Highlighter was hidden when hovering a node in the markup view"); + + info("Check that leaving the markupview shows the highlighter again"); + onHighlighterShown = getOnceHighlighterShown(inspector); + EventUtils.synthesizeMouseAtCenter( + button, + { type: "mousemove" }, + button.ownerDocument.defaultView + ); + await onHighlighterShown; + ok(true, "Highlighter is shown again when leaving the markup view"); + + info("Check that starting the node picker disable the highlighter"); + onHighlighterHidden = getOnceHighlighterHidden(inspector); + await startPicker(toolbox); + await onHighlighterHidden; + ok("Highlighter was hidden when using the node picker"); + + // stop the node picker + await toolbox.nodePicker.stop({ canceled: true }); + + info("Check that selecting another node does hide the highlighter"); + onHighlighterShown = onHighlighterShown = getOnceHighlighterShown(inspector); + button.click(); + await onHighlighterShown; + ok(true, "highlighter is displayed again"); + + onHighlighterHidden = onHighlighterHidden = + getOnceHighlighterHidden(inspector); + await selectNode("body", inspector); + await onHighlighterHidden; + ok(true, "Selecting another node hides the highlighter"); +}); + +function getOnceHighlighterShown(inspector) { + return inspector.highlighters.once("geometry-editor-highlighter-shown"); +} + +function getOnceHighlighterHidden(inspector) { + return inspector.highlighters.once("geometry-editor-highlighter-hidden"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_iframe.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_iframe.js new file mode 100644 index 0000000000..a5f7b22a00 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_iframe.js @@ -0,0 +1,48 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the geometry highlighter elements on remote frame. + +const TEST_URL = `data:text/html;charset=utf-8, + <iframe src="https://example.org/document-builder.sjs?html=${encodeURIComponent(` + <div id='positioned' style=' + background:yellow; + position:absolute; + left:5rem; + top:30px;' + > + Hello from iframe + </div>`)}"> + </iframe>`; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Select the absolute positioned node in the iframe"); + await selectNodeInFrames(["iframe", "#positioned"], inspector); + + info("Click on the button enabling the highlighter"); + const onHighlighterShown = inspector.highlighters.once( + "geometry-editor-highlighter-shown" + ); + const boxModelPanel = inspector.getPanel("boxmodel"); + const button = await waitFor(() => + boxModelPanel.document.querySelector(".layout-geometry-editor") + ); + button.click(); + + await onHighlighterShown; + ok(true, "Highlighter is displayed"); + + info("Click on the button again to disable the highlighter"); + const onHighlighterHidden = inspector.highlighters.once( + "geometry-editor-highlighter-hidden" + ); + + button.click(); + await onHighlighterHidden; + ok("Highlighter is hidden"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js new file mode 100644 index 0000000000..1f80694e70 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +// Test that when first hovering over a node and immediately after selecting it +// by clicking on it, the highlighter stays visible + +const TEST_URL = + "data:text/html;charset=utf-8," + "<p>It's going to be legen....</p>"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + + info("hovering over the <p> line in the markup-view"); + await hoverContainer("p", inspector); + let isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "the highlighter is still visible"); + + info("selecting the <p> line by clicking in the markup-view"); + await clickContainer("p", inspector); + + info( + "wait and see if the highlighter stays visible even after the node " + + "was selected" + ); + + await setContentPageElementProperty("p", "textContent", "dary!!!!"); + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "the highlighter is still visible"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js new file mode 100644 index 0000000000..e67f63c7ff --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that when after an element is selected and highlighted on hover, if the +// mouse leaves the markup-view and comes back again on the same element, that +// the highlighter is shown again on the node + +const TEST_URL = "data:text/html;charset=utf-8,<p>Select me!</p>"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const { waitForHighlighterTypeHidden } = getHighlighterTestHelpers(inspector); + + info( + "hover over the <p> line in the markup-view so that it's the " + + "currently hovered node" + ); + await hoverContainer("p", inspector); + + info("select the <p> markup-container line by clicking"); + await clickContainer("p", inspector); + let isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "the highlighter is shown"); + + const onHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + info("mouse-leave the markup-view"); + await mouseLeaveMarkupView(inspector); + info("listen to the highlighter's hidden event"); + await onHidden; + info("check that the highlighter is no longer visible"); + isVisible = await highlighterTestFront.isHighlighting(); + ok(!isVisible, "the highlighter is hidden after mouseleave"); + + info("hover over the <p> line again, which is still selected"); + await hoverContainer("p", inspector); + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "the highlighter is visible again"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js new file mode 100644 index 0000000000..0cd7461bea --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that once a node has been hovered over and marked as such, if it is +// navigated away using the keyboard, the highlighter moves to the new node, and +// if it is then navigated back to, it is briefly highlighted again + +const TEST_PAGE = + "data:text/html;charset=utf-8," + '<p id="one">one</p><p id="two">two</p>'; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_PAGE); + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + info("Making sure the markup-view frame is focused"); + inspector.markup._frame.focus(); + + let highlightedNode = null; + + async function isHighlighting(selector, desc) { + const nodeFront = await getNodeFront(selector, inspector); + is(highlightedNode, nodeFront, desc); + } + + async function waitForHighlighterShown() { + const onShownData = await waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + highlightedNode = onShownData.nodeFront; + } + + async function waitForInspectorUpdated() { + await inspector.once("inspector-updated"); + } + + info("Hover over <p#one> line in the markup-view"); + let onShown = waitForHighlighterShown(); + await hoverContainer("#one", inspector); + await onShown; + await isHighlighting("#one", "<p#one> is highlighted"); + + info("Navigate to <p#two> with the keyboard"); + let onUpdated = waitForInspectorUpdated(); + EventUtils.synthesizeKey("VK_DOWN", {}, inspector.panelWin); + await onUpdated; + onUpdated = waitForInspectorUpdated(); + onShown = waitForHighlighterShown(); + EventUtils.synthesizeKey("VK_DOWN", {}, inspector.panelWin); + await Promise.all([onShown, onUpdated]); + await isHighlighting("#two", "<p#two> is highlighted"); + + info("Navigate back to <p#one> with the keyboard"); + onUpdated = waitForInspectorUpdated(); + onShown = waitForHighlighterShown(); + EventUtils.synthesizeKey("VK_UP", {}, inspector.panelWin); + await Promise.all([onShown, onUpdated]); + await isHighlighting("#one", "<p#one> is highlighted again"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js new file mode 100644 index 0000000000..bfc6905b2d --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js @@ -0,0 +1,90 @@ +/* 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/. */ +"use strict"; + +// Testing that moving the mouse over the document with the element picker +// started highlights nodes + +const NESTED_FRAME_SRC = + "data:text/html;charset=utf-8," + + "nested iframe<section>nested div</section>"; + +const OUTER_FRAME_SRC = + "data:text/html;charset=utf-8," + + "little frame<main>little div</main>" + + "<iframe src='" + + NESTED_FRAME_SRC + + "' />"; + +const TEST_URI = + "data:text/html;charset=utf-8," + + "iframe tests for inspector" + + '<iframe src="' + + OUTER_FRAME_SRC + + '" />'; + +add_task(async function () { + const { toolbox, inspector } = await openInspectorForURL(TEST_URI); + const outerFrameMainSelector = ["iframe", "main"]; + const innerFrameSectionSelector = ["iframe", "iframe", "section"]; + + const outerFrameMainNodeFront = await getNodeFrontInFrames( + outerFrameMainSelector, + inspector + ); + const outerFrameHighlighterTestFront = await getHighlighterTestFront( + toolbox, + { target: outerFrameMainNodeFront.targetFront } + ); + + const innerFrameSectionNodeFront = await getNodeFrontInFrames( + innerFrameSectionSelector, + inspector + ); + const innerFrameHighlighterTestFront = await getHighlighterTestFront( + toolbox, + { target: innerFrameSectionNodeFront.targetFront } + ); + + info("Waiting for element picker to activate."); + await startPicker(inspector.toolbox); + + info("Moving mouse over outerFrameDiv"); + await hoverElement(inspector, outerFrameMainSelector); + + ok( + await outerFrameHighlighterTestFront.assertHighlightedNode( + isEveryFrameTargetEnabled() + ? outerFrameMainSelector.at(-1) + : outerFrameMainSelector + ), + "outerFrameDiv is highlighted." + ); + + info("Moving mouse over innerFrameDiv"); + await hoverElement(inspector, innerFrameSectionSelector); + ok( + await innerFrameHighlighterTestFront.assertHighlightedNode( + isEveryFrameTargetEnabled() + ? innerFrameSectionSelector.at(-1) + : innerFrameSectionSelector + ), + "innerFrameDiv is highlighted." + ); + + info("Selecting root node"); + await selectNode(inspector.walker.rootNode, inspector); + + info("Selecting an element from the nested iframe directly"); + await selectNodeInFrames(innerFrameSectionSelector, inspector); + + is( + inspector.breadcrumbs.nodeHierarchy.length, + 9, + "Breadcrumbs have 9 items." + ); + + info("Waiting for element picker to deactivate."); + await toolbox.nodePicker.stop({ canceled: true }); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js new file mode 100644 index 0000000000..5f50e0eb0c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js @@ -0,0 +1,80 @@ +/* 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/. */ +"use strict"; + +// Test that the highlighter is correctly positioned when switching context +// to an iframe that has an offset from the parent viewport (eg. 100px margin) + +const TEST_URI = + "data:text/html;charset=utf-8," + + '<div id="outer"></div>' + + "<iframe style='margin:100px' src='data:text/html," + + '<div id="inner">Look I am here!</div>\'>'; + +add_task(async function () { + info("Enable command-button-frames preference setting"); + Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true); + const { inspector, toolbox } = await openInspectorForURL(TEST_URI); + + await assertMarkupViewAsTree( + ` + body + div id="outer" + iframe!ignore-children`, + "body", + inspector + ); + + info("Switch to the iframe context."); + await switchToFrameContext(1, toolbox, inspector); + + info("Check the markup view is rendered correctly after switching frames"); + await assertMarkupViewAsTree( + ` + body + div id="inner"`, + "body", + inspector + ); + + info("Check highlighting is correct after switching iframe context"); + await selectAndHighlightNode("#inner", inspector); + + const nodeFront = await getNodeFront("#inner", inspector); + const iframeHighlighterTestFront = await getHighlighterTestFront(toolbox, { + target: nodeFront.targetFront, + }); + const isHighlightCorrect = + await iframeHighlighterTestFront.assertHighlightedNode("#inner"); + ok(isHighlightCorrect, "The selected node is properly highlighted."); + + info("Cleanup command-button-frames preferences."); + Services.prefs.clearUserPref("devtools.command-button-frames.enabled"); +}); + +/** + * Helper designed to switch context to another frame at the provided index. + * Returns a promise that will resolve when the navigation is complete. + * @return {Promise} + */ +async function switchToFrameContext(frameIndex, toolbox, inspector) { + // Open frame menu and wait till it's available on the screen. + const btn = toolbox.doc.getElementById("command-button-frames"); + const panel = toolbox.doc.getElementById("command-button-frames-panel"); + btn.click(); + ok(panel, "popup panel has created."); + await waitUntil(() => panel.classList.contains("tooltip-visible")); + + info("Select the iframe in the frame list."); + const menuList = toolbox.doc.getElementById("toolbox-frame-menu"); + const firstButton = menuList.querySelectorAll(".command")[frameIndex]; + const newRoot = inspector.once("new-root"); + + firstButton.click(); + + await newRoot; + await inspector.once("inspector-updated"); + + info("Navigation to the iframe is done."); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-inline.js b/devtools/client/inspector/test/browser_inspector_highlighter-inline.js new file mode 100644 index 0000000000..158c0b9ce8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-inline.js @@ -0,0 +1,95 @@ +/* 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/. */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that highlighting various inline boxes displays the right number of +// polygons in the page. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_inline.html"; +const TEST_DATA = [ + "body", + "h1", + "h2", + "h2 em", + "p", + "p span", + // The following test case used to fail. See bug 1139925. + "[dir=rtl] > span", +]; + +add_task(async function () { + info("Loading the test document and opening the inspector"); + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + + for (const selector of TEST_DATA) { + info("Selecting and highlighting node " + selector); + await selectAndHighlightNode(selector, inspector); + + info("Get all quads for this node"); + const data = await getAllAdjustedQuadsForContentPageElement(selector); + + info( + "Iterate over the box-model regions and verify that the highlighter " + + "is correct" + ); + for (const region of ["margin", "border", "padding", "content"]) { + const { points } = await highlighterTestFront.getHighlighterRegionPath( + region + ); + is( + points.length, + data[region].length, + "The highlighter's " + + region + + " path defines the correct number of boxes" + ); + } + + info( + "Verify that the guides define a rectangle that contains all " + + "content boxes" + ); + + const expectedContentRect = { + p1: { x: Infinity, y: Infinity }, + p2: { x: -Infinity, y: Infinity }, + p3: { x: -Infinity, y: -Infinity }, + p4: { x: Infinity, y: -Infinity }, + }; + for (const { p1, p2, p3, p4 } of data.content) { + expectedContentRect.p1.x = Math.min(expectedContentRect.p1.x, p1.x); + expectedContentRect.p1.y = Math.min(expectedContentRect.p1.y, p1.y); + expectedContentRect.p2.x = Math.max(expectedContentRect.p2.x, p2.x); + expectedContentRect.p2.y = Math.min(expectedContentRect.p2.y, p2.y); + expectedContentRect.p3.x = Math.max(expectedContentRect.p3.x, p3.x); + expectedContentRect.p3.y = Math.max(expectedContentRect.p3.y, p3.y); + expectedContentRect.p4.x = Math.min(expectedContentRect.p4.x, p4.x); + expectedContentRect.p4.y = Math.max(expectedContentRect.p4.y, p4.y); + } + + const contentRect = await highlighterTestFront.getGuidesRectangle(); + + for (const point of ["p1", "p2", "p3", "p4"]) { + is( + Math.round(contentRect[point].x), + Math.round(expectedContentRect[point].x), + "x coordinate of point " + + point + + " of the content rectangle defined by the outer guides is correct" + ); + is( + Math.round(contentRect[point].y), + Math.round(expectedContentRect[point].y), + "y coordinate of point " + + point + + " of the content rectangle defined by the outer guides is correct" + ); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js new file mode 100644 index 0000000000..7e5a09ded3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js @@ -0,0 +1,71 @@ +/* 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/. */ + +"use strict"; + +// Test that the keybindings for Picker work alright + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html"; + +add_task(async function () { + const { inspector, toolbox, highlighterTestFront } = + await openInspectorForURL(TEST_URL); + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + await startPicker(toolbox); + + info("Selecting the simple-div1 DIV"); + await hoverElement(inspector, "#simple-div1"); + + ok( + await highlighterTestFront.assertHighlightedNode("#simple-div1"), + "The highlighter shows #simple-div1. OK." + ); + + // First Child selection + info("Testing first-child selection."); + + await doKeyHover("VK_RIGHT"); + ok( + await highlighterTestFront.assertHighlightedNode("#useless-para"), + "The highlighter shows #useless-para. OK." + ); + + info("Selecting the useful-para paragraph DIV"); + await hoverElement(inspector, "#useful-para"); + ok( + await highlighterTestFront.assertHighlightedNode("#useful-para"), + "The highlighter shows #useful-para. OK." + ); + + await doKeyHover("VK_RIGHT"); + ok( + await highlighterTestFront.assertHighlightedNode("#bold"), + "The highlighter shows #bold. OK." + ); + + info("Going back up to the simple-div1 DIV"); + await doKeyHover("VK_LEFT"); + await doKeyHover("VK_LEFT"); + ok( + await highlighterTestFront.assertHighlightedNode("#simple-div1"), + "The highlighter shows #simple-div1. OK." + ); + + info("First child selection test Passed."); + + info("Stopping the picker"); + await toolbox.nodePicker.stop({ canceled: true }); + + function doKeyHover(key) { + info("Key pressed. Waiting for element to be highlighted/hovered"); + const onPickerHovered = toolbox.nodePicker.once("picker-node-hovered"); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + BrowserTestUtils.synthesizeKey(key, {}, gBrowser.selectedBrowser); + + return Promise.all([onPickerHovered, onHighlighterShown]); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js new file mode 100644 index 0000000000..cb8f995699 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js @@ -0,0 +1,64 @@ +/* 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/. */ + +"use strict"; + +// Test that the keybindings for Picker work alright + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html"; + +add_task(async function () { + const { inspector, toolbox, highlighterTestFront } = + await openInspectorForURL(TEST_URL); + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + await startPicker(toolbox); + + // Previously chosen child memory + info("Testing whether previously chosen child is remembered"); + + info("Selecting the ahoy paragraph DIV"); + await hoverElement(inspector, "#ahoy"); + + await doKeyHover("VK_LEFT"); + ok( + await highlighterTestFront.assertHighlightedNode("#simple-div2"), + "The highlighter shows #simple-div2. OK." + ); + + await doKeyHover("VK_RIGHT"); + ok( + await highlighterTestFront.assertHighlightedNode("#ahoy"), + "The highlighter shows #ahoy. OK." + ); + + info("Going back up to the complex-div DIV"); + await doKeyHover("VK_LEFT"); + await doKeyHover("VK_LEFT"); + ok( + await highlighterTestFront.assertHighlightedNode("#complex-div"), + "The highlighter shows #complex-div. OK." + ); + + await doKeyHover("VK_RIGHT"); + ok( + await highlighterTestFront.assertHighlightedNode("#simple-div2"), + "The highlighter shows #simple-div2. OK." + ); + + info("Previously chosen child is remembered. Passed."); + + info("Stopping the picker"); + await toolbox.nodePicker.stop({ canceled: true }); + + function doKeyHover(key) { + info("Key pressed. Waiting for element to be highlighted/hovered"); + const onPickerHovered = toolbox.nodePicker.once("picker-node-hovered"); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + BrowserTestUtils.synthesizeKey(key, {}, gBrowser.selectedBrowser); + return Promise.all([onPickerHovered, onHighlighterShown]); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js new file mode 100644 index 0000000000..86a427c6e7 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js @@ -0,0 +1,69 @@ +/* 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/. */ + +"use strict"; + +// Test that the keybindings for Picker work alright + +const IS_OSX = Services.appinfo.OS === "Darwin"; +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html"; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + await startPicker(toolbox); + await hoverElement(inspector, "#another"); + + info("Testing enter/return key as pick-node command"); + await doKeyPick("VK_RETURN"); + is( + inspector.selection.nodeFront.id, + "another", + "The #another node was selected. Passed." + ); + + info("Testing escape key as cancel-picker command"); + await startPicker(toolbox); + await hoverElement(inspector, "#ahoy"); + await doKeyStop("VK_ESCAPE"); + is( + inspector.selection.nodeFront.id, + "another", + "The #another DIV is still selected. Passed." + ); + + info("Testing Ctrl+Shift+C shortcut as cancel-picker command"); + await startPicker(toolbox); + await hoverElement(inspector, "#ahoy"); + const eventOptions = { key: "VK_C" }; + if (IS_OSX) { + eventOptions.metaKey = true; + eventOptions.altKey = true; + } else { + eventOptions.ctrlKey = true; + eventOptions.shiftKey = true; + } + await doKeyStop("VK_C", eventOptions); + is( + inspector.selection.nodeFront.id, + "another", + "The #another DIV is still selected. Passed." + ); + + function doKeyPick(key, options = {}) { + info("Key pressed. Waiting for element to be picked"); + BrowserTestUtils.synthesizeKey(key, options, gBrowser.selectedBrowser); + return Promise.all([ + inspector.selection.once("new-node-front"), + inspector.once("inspector-updated"), + toolbox.nodePicker.once("picker-stopped"), + ]); + } + + function doKeyStop(key, options = {}) { + info("Key pressed. Waiting for picker to be canceled"); + BrowserTestUtils.synthesizeKey(key, options, gBrowser.selectedBrowser); + return toolbox.nodePicker.once("picker-stopped"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js new file mode 100644 index 0000000000..5351ae32de --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that pressing ESC twice while in picker mode first stops the picker and +// then opens the split-console (see bug 988278). + +const TEST_URL = "data:text/html;charset=utf8,<div></div>"; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + await startPicker(toolbox); + + info("Start using the picker by hovering over nodes"); + const onHover = toolbox.nodePicker.once("picker-node-hovered"); + await safeSynthesizeMouseEventAtCenterInContentPage("div", { + type: "mousemove", + }); + + await onHover; + + info("Press escape and wait for the picker to stop"); + await stopPickerWithEscapeKey(toolbox); + + info("Press escape again and wait for the split console to open"); + const onSplitConsole = toolbox.once("split-console"); + const onConsoleReady = toolbox.once("webconsole-ready"); + // The escape key is synthesized in the main process, which is where the focus + // should be after the picker was stopped. + EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin); + await onSplitConsole; + await onConsoleReady; + ok(toolbox.splitConsole, "The split console is shown."); + + // Hide the split console. + await toolbox.toggleSplitConsole(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_separate-window.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_separate-window.js new file mode 100644 index 0000000000..80984bf96e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_separate-window.js @@ -0,0 +1,120 @@ +/* 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/. */ + +"use strict"; + +// Test the keybindings for element picker with separate window. + +const { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +const IS_OSX = Services.appinfo.OS === "Darwin"; +const TEST_URL = "data:text/html;charset=utf8,<div></div>"; + +const TEST_MODIFIERS = [ + { + isOSX: true, + description: "OSX and altKey+metaKey", + modifier: { altKey: true, metaKey: true }, + }, + { + isOSX: true, + description: "OSX and metaKey+shiftKey", + modifier: { metaKey: true, shiftKey: true }, + }, + { + description: "ctrlKey+shiftKey", + modifier: { ctrlKey: true, shiftKey: true }, + }, +]; + +add_task(async () => { + info("In order to open the inspector in separate window"); + await pushPref("devtools.toolbox.host", "window"); + + info("Open an inspected tab"); + await addTab(TEST_URL); + + for (const { description, modifier, isOSX } of TEST_MODIFIERS) { + if (!!isOSX !== IS_OSX) { + continue; + } + + info(`Start the test for ${description}`); + + info("Open the toolbox and the inspecor"); + const onToolboxReady = gDevTools.once("toolbox-ready"); + EventUtils.synthesizeKey("c", modifier, window); + + info("Check the state of the inspector"); + const toolbox = await onToolboxReady; + is( + toolbox.hostType, + Toolbox.HostType.WINDOW, + "The toolbox opens in a separate window" + ); + is(toolbox.currentToolId, "inspector", "The inspector selects"); + await assertStatuses(toolbox, true, true); + + info("Toggle the picker mode by the shortcut key on the toolbox"); + EventUtils.synthesizeKey("c", modifier, toolbox.win); + await assertStatuses(toolbox, false, true); + + info("Focus on main window"); + window.focus(); + await waitForFocusChanged(document, true); + ok(true, "The main window has focus"); + + info("Toggle the picker mode by the shortcut key on the main window"); + EventUtils.synthesizeKey("c", modifier, window); + await assertStatuses(toolbox, true, false); + + info("Toggle again by the shortcut key on the main window"); + EventUtils.synthesizeKey("c", modifier, window); + await assertStatuses(toolbox, false, false); + + info("Select a tool other than the inspector"); + const onConsoleLoaded = toolbox.once("webconsole-ready"); + await toolbox.selectTool("webconsole"); + await onConsoleLoaded; + await waitForFocusChanged(toolbox.doc, true); + + info("Focus on main window"); + window.focus(); + await waitForFocusChanged(document, true); + + info("Type the shortcut key again after selecting other tool"); + const onInspectorSelected = toolbox.once("inspector-selected"); + EventUtils.synthesizeKey("c", modifier, window); + await onInspectorSelected; + await assertStatuses(toolbox, true, false); + + info("Cancel the picker mode"); + EventUtils.synthesizeKey("c", modifier, window); + await waitUntil(() => toolbox.pickerButton.isChecked === false); + + info("Close the toolbox"); + await toolbox.closeToolbox(); + } +}); + +async function assertStatuses(toolbox, isPickerMode, isToolboxHavingFocus) { + info("Check the state of the picker mode"); + await waitUntil(() => toolbox.pickerButton.isChecked === isPickerMode); + is( + toolbox.pickerButton.isChecked, + isPickerMode, + "The picker mode is correct" + ); + + info("Check whether the toolbox has the focus"); + await waitForFocusChanged(toolbox.doc, isToolboxHavingFocus); + ok(true, "The focus state of the toolbox is correct"); + + await waitForFocusChanged(document, !isToolboxHavingFocus); + ok(true, "The focus state of the main window is correct"); +} + +async function waitForFocusChanged(doc, expected) { + return waitUntil(() => doc.hasFocus() === expected); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure-keybinding.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure-keybinding.js new file mode 100644 index 0000000000..0831b90bf4 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure-keybinding.js @@ -0,0 +1,194 @@ +/* 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/. */ + +"use strict"; + +const IS_OSX = Services.appinfo.OS === "Darwin"; +const VK_MOD = IS_OSX ? "VK_META" : "VK_CONTROL"; +const TEST_URL = "data:text/html;charset=utf-8,measuring tool test"; + +const PREFIX = "measuring-tool-"; +const HANDLER_PREFIX = "handler-"; +const HIGHLIGHTED_HANDLER_CLASSNAME = "highlight"; +const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter"; + +const SMALL_DELTA = 1; +const LARGE_DELTA = 10; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + const { show, finalize } = helper; + + helper.prefix = PREFIX; + + info("Showing the highlighter"); + await show(); + + info("Creating the area"); + const { mouse } = helper; + await mouse.down(32, 20); + await mouse.move(32 + 160, 20 + 100); + await mouse.up(); + + const arrow_tests = [ + { + key: "VK_LEFT", + shift: false, + delta: -SMALL_DELTA, + move: "dx", + resize: "dw", + }, + { + key: "VK_LEFT", + shift: true, + delta: -LARGE_DELTA, + move: "dx", + resize: "dw", + }, + { + key: "VK_RIGHT", + shift: false, + delta: SMALL_DELTA, + move: "dx", + resize: "dw", + }, + { + key: "VK_RIGHT", + shift: true, + delta: LARGE_DELTA, + move: "dx", + resize: "dw", + }, + { + key: "VK_UP", + shift: false, + delta: -SMALL_DELTA, + move: "dy", + resize: "dh", + }, + { + key: "VK_UP", + shift: true, + delta: -LARGE_DELTA, + move: "dy", + resize: "dh", + }, + { + key: "VK_DOWN", + shift: false, + delta: SMALL_DELTA, + move: "dy", + resize: "dh", + }, + { + key: "VK_DOWN", + shift: true, + delta: LARGE_DELTA, + move: "dy", + resize: "dh", + }, + ]; + + for (const { key, shift, delta, move, resize } of arrow_tests) { + await canMoveAreaViaKeybindings(helper, key, shift, { [move]: delta }); + await canResizeAreaViaKeybindings(helper, key, shift, { [resize]: delta }); + } + + // Test handler highlighting on Ctrl/Command hold + await handlerShouldNotBeHighlighted(helper); + BrowserTestUtils.synthesizeKey( + VK_MOD, + { type: "keydown" }, + gBrowser.selectedBrowser + ); + await handlerShouldBeHighlighted(helper); + BrowserTestUtils.synthesizeKey("VK_LEFT", {}, gBrowser.selectedBrowser); + await handlerShouldBeHighlighted(helper); + BrowserTestUtils.synthesizeKey( + VK_MOD, + { type: "keyup" }, + gBrowser.selectedBrowser + ); + await handlerShouldNotBeHighlighted(helper); + + info("Hiding the highlighter"); + await finalize(); +}); + +async function canMoveAreaViaKeybindings(helper, key, shiftHeld, deltas) { + const { dx = 0, dy = 0 } = deltas; + + const { + x: origAreaX, + y: origAreaY, + width: origAreaWidth, + height: origAreaHeight, + } = await getAreaRect(helper); + + const eventOptions = shiftHeld ? { shiftKey: true } : {}; + BrowserTestUtils.synthesizeKey(key, eventOptions, gBrowser.selectedBrowser); + + const { + x: areaX, + y: areaY, + width: areaWidth, + height: areaHeight, + } = await getAreaRect(helper); + + is(areaX, origAreaX + dx, "X coordinate correct after moving"); + is(areaY, origAreaY + dy, "Y coordinate correct after moving"); + is(areaWidth, origAreaWidth, "Width unchanged after moving"); + is(areaHeight, origAreaHeight, "Height unchanged after moving"); +} + +async function canResizeAreaViaKeybindings(helper, key, shiftHeld, deltas) { + const { dw = 0, dh = 0 } = deltas; + + const { + x: origAreaX, + y: origAreaY, + width: origAreaWidth, + height: origAreaHeight, + } = await getAreaRect(helper); + + const eventOptions = IS_OSX ? { metaKey: true } : { ctrlKey: true }; + if (shiftHeld) { + eventOptions.shiftKey = true; + } + BrowserTestUtils.synthesizeKey(key, eventOptions, gBrowser.selectedBrowser); + + const { + x: areaX, + y: areaY, + width: areaWidth, + height: areaHeight, + } = await getAreaRect(helper); + + is(areaX, origAreaX, "X coordinate unchanged after resizing"); + is(areaY, origAreaY, "Y coordinate unchanged after resizing"); + is(areaWidth, origAreaWidth + dw, "Width correct after resizing"); + is(areaHeight, origAreaHeight + dh, "Height correct after resizing"); +} + +async function handlerIsHighlighted(helper) { + const klass = await helper.getElementAttribute( + `${HANDLER_PREFIX}topleft`, + "class" + ); + return klass.includes(HIGHLIGHTED_HANDLER_CLASSNAME); +} + +async function handlerShouldBeHighlighted(helper) { + ok(await handlerIsHighlighted(helper), "Origin handler is highlighted"); +} + +async function handlerShouldNotBeHighlighted(helper) { + ok( + !(await handlerIsHighlighted(helper)), + "Origin handler is not highlighted" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js new file mode 100644 index 0000000000..6155cec2d7 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js @@ -0,0 +1,92 @@ +/* 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/. */ + +"use strict"; + +const TEST_URL = `data:text/html;charset=utf-8, + <div style=' + position:absolute; + left: 0; + top: 0; + width: 40000px; + height: 8000px'> + </div>`; + +const PREFIX = "measuring-tool-"; +const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter"; + +const X = 32; +const Y = 20; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + const { finalize } = helper; + + helper.prefix = PREFIX; + + await isHiddenByDefault(helper); + await areLabelsHiddenByDefaultWhenShows(helper); + await areLabelsProperlyDisplayedWhenMouseMoved(helper); + + await finalize(); +}); + +async function isHiddenByDefault({ isElementHidden }) { + info("Checking the highlighter is hidden by default"); + + let hidden = await isElementHidden("root"); + ok(hidden, "highlighter's root is hidden by default"); + + hidden = await isElementHidden("label-size"); + ok(hidden, "highlighter's label size is hidden by default"); + + hidden = await isElementHidden("label-position"); + ok(hidden, "highlighter's label position is hidden by default"); +} + +async function areLabelsHiddenByDefaultWhenShows({ isElementHidden, show }) { + info("Checking the highlighter is displayed when asked"); + + await show(); + + let hidden = await isElementHidden("elements"); + is(hidden, false, "highlighter is visible after show"); + + hidden = await isElementHidden("label-size"); + ok(hidden, "label's size still hidden"); + + hidden = await isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); +} + +async function areLabelsProperlyDisplayedWhenMouseMoved({ + isElementHidden, + synthesizeMouse, + getElementTextContent, +}) { + info("Checking labels are properly displayed when mouse moved"); + + await synthesizeMouse({ + selector: ":root", + options: { type: "mousemove" }, + x: X, + y: Y, + }); + + let hidden = await isElementHidden("label-position"); + is(hidden, false, "label's position is displayed after the mouse is moved"); + + hidden = await isElementHidden("label-size"); + ok(hidden, "label's size still hidden"); + + const text = await getElementTextContent("label-position"); + + const [x, y] = text.replace(/ /g, "").split(/\n/); + + is(+x, X, "label's position shows the proper X coord"); + is(+y, Y, "label's position shows the proper Y coord"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js new file mode 100644 index 0000000000..b8df488afa --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js @@ -0,0 +1,138 @@ +/* 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/. */ + +"use strict"; + +const TEST_URL = `data:text/html;charset=utf-8, + <div style=' + position:absolute; + left: 0; + top: 0; + width: 40000px; + height: 8000px'> + </div>`; + +const PREFIX = "measuring-tool-"; +const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter"; + +const SIDES = ["top", "right", "bottom", "left"]; + +const X = 32; +const Y = 20; +const WIDTH = 160; +const HEIGHT = 100; +const HYPOTENUSE = Math.hypot(WIDTH, HEIGHT).toFixed(2); + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + const { show, finalize } = helper; + + helper.prefix = PREFIX; + + await show(); + + await hasNoLabelsWhenStarts(helper); + await hasSizeLabelWhenMoved(helper); + await hasCorrectSizeLabelValue(helper); + await hasSizeLabelAndGuidesWhenStops(helper); + await hasCorrectSizeLabelValue(helper); + + await finalize(); +}); + +async function hasNoLabelsWhenStarts({ isElementHidden, synthesizeMouse }) { + info("Checking highlighter has no labels when we start to select"); + + await synthesizeMouse({ + selector: ":root", + options: { type: "mousedown" }, + x: X, + y: Y, + }); + + let hidden = await isElementHidden("label-size"); + ok(hidden, "label's size still hidden"); + + hidden = await isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); + + info("Checking highlighter has no guides when we start to select"); + + let guidesHidden = true; + for (const side of SIDES) { + guidesHidden = guidesHidden && (await isElementHidden("guide-" + side)); + } + + ok(guidesHidden, "guides are hidden during dragging"); +} + +async function hasSizeLabelWhenMoved({ isElementHidden, synthesizeMouse }) { + info("Checking highlighter has size label when we select the area"); + + await synthesizeMouse({ + selector: ":root", + options: { type: "mousemove" }, + x: X + WIDTH, + y: Y + HEIGHT, + }); + + let hidden = await isElementHidden("label-size"); + is(hidden, false, "label's size is visible during selection"); + + hidden = await isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); + + info("Checking highlighter has no guides when we select the area"); + + let guidesHidden = true; + for (const side of SIDES) { + guidesHidden = guidesHidden && (await isElementHidden("guide-" + side)); + } + + ok(guidesHidden, "guides are hidden during selection"); +} + +async function hasSizeLabelAndGuidesWhenStops({ + isElementHidden, + synthesizeMouse, +}) { + info("Checking highlighter has size label and guides when we stop"); + + await synthesizeMouse({ + selector: ":root", + options: { type: "mouseup" }, + x: X + WIDTH, + y: Y + HEIGHT, + }); + + let hidden = await isElementHidden("label-size"); + is(hidden, false, "label's size is visible when the selection is done"); + + hidden = await isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); + + let guidesVisible = true; + for (const side of SIDES) { + guidesVisible = guidesVisible && !(await isElementHidden("guide-" + side)); + } + + ok(guidesVisible, "guides are visible when the selection is done"); +} + +async function hasCorrectSizeLabelValue({ getElementTextContent }) { + const text = await getElementTextContent("label-size"); + + const [width, height, hypot] = text.match(/\d.*px/g); + + is(parseFloat(width), WIDTH, "width on label's size is correct"); + is(parseFloat(height), HEIGHT, "height on label's size is correct"); + is( + parseFloat(hypot), + parseFloat(HYPOTENUSE), + "hypotenuse on label's size is correct" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_03.js new file mode 100644 index 0000000000..906284ba86 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_03.js @@ -0,0 +1,114 @@ +/* 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/. */ + +"use strict"; + +const TEST_URL = "data:text/html;charset=utf-8,measuring tool test"; + +const PREFIX = "measuring-tool-"; +const HANDLER_PREFIX = "handler-"; +const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter"; + +const X = 32; +const Y = 20; +const WIDTH = 160; +const HEIGHT = 100; + +const HANDLER_MAP = { + top(areaWidth, areaHeight) { + return { x: Math.round(areaWidth / 2), y: 0 }; + }, + topright(areaWidth, areaHeight) { + return { x: areaWidth, y: 0 }; + }, + right(areaWidth, areaHeight) { + return { x: areaWidth, y: Math.round(areaHeight / 2) }; + }, + bottomright(areaWidth, areaHeight) { + return { x: areaWidth, y: areaHeight }; + }, + bottom(areaWidth, areaHeight) { + return { x: Math.round(areaWidth / 2), y: areaHeight }; + }, + bottomleft(areaWidth, areaHeight) { + return { x: 0, y: areaHeight }; + }, + left(areaWidth, areaHeight) { + return { x: 0, y: Math.round(areaHeight / 2) }; + }, + topleft(areaWidth, areaHeight) { + return { x: 0, y: 0 }; + }, +}; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + const { show, finalize } = helper; + + helper.prefix = PREFIX; + + info("Showing the highlighter"); + await show(); + + await areHandlersHiddenByDefault(helper); + await areHandlersHiddenOnAreaCreation(helper); + await areHandlersCorrectlyShownAfterAreaCreation(helper); + + info("Hiding the highlighter"); + await finalize(); +}); + +async function areHandlersHiddenByDefault({ isElementHidden, mouse }) { + info("Checking that highlighter's handlers are hidden by default"); + + await mouse.down(X, Y); + + for (const handler of Object.keys(HANDLER_MAP)) { + const hidden = await isElementHidden(`${HANDLER_PREFIX}${handler}`); + ok(hidden, `${handler} handler is hidden by default`); + } +} + +async function areHandlersHiddenOnAreaCreation({ isElementHidden, mouse }) { + info("Checking that highlighter's handlers are hidden while area creation"); + + await mouse.move(X + WIDTH, Y + HEIGHT); + + for (const handler of Object.keys(HANDLER_MAP)) { + const hidden = await isElementHidden(`${HANDLER_PREFIX}${handler}`); + ok(hidden, `${handler} handler is still hidden on area creation`); + } +} + +async function areHandlersCorrectlyShownAfterAreaCreation(helper) { + info("Checking that highlighter's handlers are shown after area creation"); + + const { isElementHidden, mouse } = helper; + + await mouse.up(); + + for (const handler of Object.keys(HANDLER_MAP)) { + const hidden = await isElementHidden(`${HANDLER_PREFIX}${handler}`); + ok(!hidden, `${handler} handler is shown after area creation`); + + const { x: handlerX, y: handlerY } = await getHandlerCoords( + helper, + handler + ); + const { x: expectedX, y: expectedY } = HANDLER_MAP[handler](WIDTH, HEIGHT); + is(handlerX, expectedX, `x coordinate of ${handler} handler is correct`); + is(handlerY, expectedY, `y coordinate of ${handler} handler is correct`); + } +} + +async function getHandlerCoords({ getElementAttribute }, handler) { + const handlerId = `${HANDLER_PREFIX}${handler}`; + return { + x: Math.round(await getElementAttribute(handlerId, "cx")), + y: Math.round(await getElementAttribute(handlerId, "cy")), + }; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_04.js new file mode 100644 index 0000000000..d9dcd5d89f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_04.js @@ -0,0 +1,163 @@ +/* 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/. */ + +"use strict"; + +const TEST_URL = "data:text/html;charset=utf-8,measuring tool test"; + +const PREFIX = "measuring-tool-"; +const HANDLER_PREFIX = "handler-"; +const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter"; + +const X = 32; +const Y = 20; +const WIDTH = 160; +const HEIGHT = 100; +const X_OFFSET = 15; +const Y_OFFSET = 10; + +const HANDLER_MAP = { + top(areaWidth, areaHeight) { + return { x: Math.round(areaWidth / 2), y: 0 }; + }, + topright(areaWidth, areaHeight) { + return { x: areaWidth, y: 0 }; + }, + right(areaWidth, areaHeight) { + return { x: areaWidth, y: Math.round(areaHeight / 2) }; + }, + bottomright(areaWidth, areaHeight) { + return { x: areaWidth, y: areaHeight }; + }, + bottom(areaWidth, areaHeight) { + return { x: Math.round(areaWidth / 2), y: areaHeight }; + }, + bottomleft(areaWidth, areaHeight) { + return { x: 0, y: areaHeight }; + }, + left(areaWidth, areaHeight) { + return { x: 0, y: Math.round(areaHeight / 2) }; + }, + topleft(areaWidth, areaHeight) { + return { x: 0, y: 0 }; + }, +}; + +add_task(async function () { + const helper = await openInspectorForURL(TEST_URL).then( + getHighlighterHelperFor(HIGHLIGHTER_TYPE) + ); + + const { show, finalize } = helper; + + helper.prefix = PREFIX; + + info("Showing the highlighter"); + await show(); + + info("Creating the area"); + const { mouse } = helper; + await mouse.down(X, Y); + await mouse.move(X + WIDTH, Y + HEIGHT); + await mouse.up(); + + await canResizeAreaViaHandlers(helper); + + info("Hiding the highlighter"); + await finalize(); +}); + +async function canResizeAreaViaHandlers(helper) { + const { mouse } = helper; + + for (const handler of Object.keys(HANDLER_MAP)) { + const { x: origHandlerX, y: origHandlerY } = await getHandlerCoords( + helper, + handler + ); + const { + x: origAreaX, + y: origAreaY, + width: origAreaWidth, + height: origAreaHeight, + } = await getAreaRect(helper); + const absOrigHandlerX = origHandlerX + origAreaX; + const absOrigHandlerY = origHandlerY + origAreaY; + + const delta = { + x: handler.includes("left") ? X_OFFSET : 0, + y: handler.includes("top") ? Y_OFFSET : 0, + width: + handler.includes("right") || handler.includes("left") ? X_OFFSET : 0, + height: + handler.includes("bottom") || handler.includes("top") ? Y_OFFSET : 0, + }; + + if (handler.includes("left")) { + delta.width *= -1; + } + if (handler.includes("top")) { + delta.height *= -1; + } + + // Simulate drag & drop of handler + await mouse.down(absOrigHandlerX, absOrigHandlerY); + await mouse.move(absOrigHandlerX + X_OFFSET, absOrigHandlerY + Y_OFFSET); + await mouse.up(); + + const { + x: areaX, + y: areaY, + width: areaWidth, + height: areaHeight, + } = await getAreaRect(helper); + is( + areaX, + origAreaX + delta.x, + `X coordinate of area correct after resizing using ${handler} handler` + ); + is( + areaY, + origAreaY + delta.y, + `Y coordinate of area correct after resizing using ${handler} handler` + ); + is( + areaWidth, + origAreaWidth + delta.width, + `Width of area correct after resizing using ${handler} handler` + ); + is( + areaHeight, + origAreaHeight + delta.height, + `Height of area correct after resizing using ${handler} handler` + ); + + const { x: handlerX, y: handlerY } = await getHandlerCoords( + helper, + handler + ); + const { x: expectedX, y: expectedY } = HANDLER_MAP[handler]( + areaWidth, + areaHeight + ); + is( + handlerX, + expectedX, + `X coordinate of ${handler} handler correct after resizing` + ); + is( + handlerY, + expectedY, + `Y coordinate of ${handler} handler correct after resizing` + ); + } +} + +async function getHandlerCoords({ getElementAttribute }, handler) { + const handlerId = `${HANDLER_PREFIX}${handler}`; + return { + x: Math.round(await getElementAttribute(handlerId, "cx")), + y: Math.round(await getElementAttribute(handlerId, "cy")), + }; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-options.js b/devtools/client/inspector/test/browser_inspector_highlighter-options.js new file mode 100644 index 0000000000..558b67059a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-options.js @@ -0,0 +1,267 @@ +/* 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/. */ + +"use strict"; + +// Check that the box-model highlighter supports configuration options + +const TEST_URL = ` + <body style="padding:2em;"> + <div style="width:100px;height:100px;padding:2em; + border:.5em solid black;margin:1em;">test</div> + </body> +`; + +// Test data format: +// - desc: a string that will be output to the console. +// - options: json object to be passed as options to the highlighter. +// - checkHighlighter: a generator (async) function that should check the +// highlighter is correct. +const TEST_DATA = [ + { + desc: "Guides and infobar should be shown by default", + options: {}, + async checkHighlighter(highlighterTestFront) { + let hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-infobar-container", + "hidden" + ); + ok(!hidden, "Node infobar is visible"); + + hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-elements", + "hidden" + ); + ok(!hidden, "SVG container is visible"); + + for (const side of ["top", "right", "bottom", "left"]) { + hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-" + side, + "hidden" + ); + ok(!hidden, side + " guide is visible"); + } + }, + }, + { + desc: "All regions should be shown by default", + options: {}, + async checkHighlighter(highlighterTestFront) { + for (const region of ["margin", "border", "padding", "content"]) { + const { d } = await highlighterTestFront.getHighlighterRegionPath( + region + ); + ok(d, "Region " + region + " has set coordinates"); + } + }, + }, + { + desc: "Guides can be hidden", + options: { hideGuides: true }, + async checkHighlighter(highlighterTestFront) { + for (const side of ["top", "right", "bottom", "left"]) { + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-" + side, + "hidden" + ); + is(hidden, "true", side + " guide has been hidden"); + } + }, + }, + { + desc: "Infobar can be hidden", + options: { hideInfoBar: true }, + async checkHighlighter(highlighterTestFront) { + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-infobar-container", + "hidden" + ); + is(hidden, "true", "infobar has been hidden"); + }, + }, + { + desc: "One region only can be shown (1)", + options: { showOnly: "content" }, + async checkHighlighter(highlighterTestFront) { + let { d } = await highlighterTestFront.getHighlighterRegionPath("margin"); + ok(!d, "margin region is hidden"); + + ({ d } = await highlighterTestFront.getHighlighterRegionPath("border")); + ok(!d, "border region is hidden"); + + ({ d } = await highlighterTestFront.getHighlighterRegionPath("padding")); + ok(!d, "padding region is hidden"); + + ({ d } = await highlighterTestFront.getHighlighterRegionPath("content")); + ok(d, "content region is shown"); + }, + }, + { + desc: "One region only can be shown (2)", + options: { showOnly: "margin" }, + async checkHighlighter(highlighterTestFront) { + let { d } = await highlighterTestFront.getHighlighterRegionPath("margin"); + ok(d, "margin region is shown"); + + ({ d } = await highlighterTestFront.getHighlighterRegionPath("border")); + ok(!d, "border region is hidden"); + + ({ d } = await highlighterTestFront.getHighlighterRegionPath("padding")); + ok(!d, "padding region is hidden"); + + ({ d } = await highlighterTestFront.getHighlighterRegionPath("content")); + ok(!d, "content region is hidden"); + }, + }, + { + desc: "Guides can be drawn around a given region (1)", + options: { region: "padding" }, + async checkHighlighter(highlighterTestFront) { + const topY1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-top", + "y1" + ); + const rightX1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-right", + "x1" + ); + const bottomY1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-bottom", + "y1" + ); + const leftX1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-left", + "x1" + ); + + let { points } = await highlighterTestFront.getHighlighterRegionPath( + "padding" + ); + points = points[0]; + + is(topY1, points[0][1], "Top guide's y1 is correct"); + is( + parseInt(rightX1, 10), + points[1][0] - 1, + "Right guide's x1 is correct" + ); + is( + parseInt(bottomY1, 10), + points[2][1] - 1, + "Bottom guide's y1 is correct" + ); + is(leftX1, points[3][0], "Left guide's x1 is correct"); + }, + }, + { + desc: "Guides can be drawn around a given region (2)", + options: { region: "margin" }, + async checkHighlighter(highlighterTestFront) { + const topY1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-top", + "y1" + ); + const rightX1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-right", + "x1" + ); + const bottomY1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-bottom", + "y1" + ); + const leftX1 = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-guide-left", + "x1" + ); + + let { points } = await highlighterTestFront.getHighlighterRegionPath( + "margin" + ); + points = points[0]; + + is(topY1, points[0][1], "Top guide's y1 is correct"); + is( + parseInt(rightX1, 10), + points[1][0] - 1, + "Right guide's x1 is correct" + ); + is( + parseInt(bottomY1, 10), + points[2][1] - 1, + "Bottom guide's y1 is correct" + ); + is(leftX1, points[3][0], "Left guide's x1 is correct"); + }, + }, + { + desc: "When showOnly is used, other regions can be faded", + options: { showOnly: "margin", onlyRegionArea: true }, + async checkHighlighter(highlighterTestFront) { + for (const region of ["margin", "border", "padding", "content"]) { + const { d } = await highlighterTestFront.getHighlighterRegionPath( + region + ); + ok(d, "Region " + region + " is shown (it has a d attribute)"); + + const faded = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-" + region, + "faded" + ); + if (region === "margin") { + ok(!faded, "The margin region is not faded"); + } else { + is(faded, "true", "Region " + region + " is faded"); + } + } + }, + }, + { + desc: "When showOnly is used, other regions can be faded (2)", + options: { showOnly: "padding", onlyRegionArea: true }, + async checkHighlighter(highlighterTestFront) { + for (const region of ["margin", "border", "padding", "content"]) { + const { d } = await highlighterTestFront.getHighlighterRegionPath( + region + ); + ok(d, "Region " + region + " is shown (it has a d attribute)"); + + const faded = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-" + region, + "faded" + ); + if (region === "padding") { + ok(!faded, "The padding region is not faded"); + } else { + is(faded, "true", "Region " + region + " is faded"); + } + } + }, + }, +]; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURI(TEST_URL) + ); + + const divFront = await getNodeFront("div", inspector); + + for (const { desc, options, checkHighlighter } of TEST_DATA) { + info("Running test: " + desc); + + info("Show the box-model highlighter with options " + options); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + divFront, + options + ); + + await checkHighlighter(highlighterTestFront); + + info("Hide the box-model highlighter"); + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-preview.js b/devtools/client/inspector/test/browser_inspector_highlighter-preview.js new file mode 100644 index 0000000000..256f57c521 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-preview.js @@ -0,0 +1,72 @@ +/* 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/. */ + +"use strict"; + +// Test that the highlighter is correctly displayed and picker mode is not stopped after +// a Ctrl-click (Cmd-click on OSX). + +const TEST_URI = `data:text/html;charset=utf-8, + <p id="one">one</p><p id="two">two</p><p id="three">three</p>`; +const IS_OSX = Services.appinfo.OS === "Darwin"; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URI); + + const body = await getNodeFront("body", inspector); + is( + inspector.selection.nodeFront, + body, + "By default the body node is selected" + ); + + info("Start the element picker"); + await startPicker(toolbox); + + info("Shift-clicking element #one should select it but keep the picker ON"); + await clickElement("#one", inspector, true); + await checkElementSelected("#one", inspector); + checkPickerMode(toolbox, true); + + info("Shift-clicking element #two should select it but keep the picker ON"); + await clickElement("#two", inspector, true); + await checkElementSelected("#two", inspector); + checkPickerMode(toolbox, true); + + info("Clicking element #three should select it and turn the picker OFF"); + await clickElement("#three", inspector, false); + await checkElementSelected("#three", inspector); + checkPickerMode(toolbox, false); +}); + +async function clickElement(selector, inspector, preview) { + const onSelectionChanged = inspector.once("inspector-updated"); + await safeSynthesizeMouseEventAtCenterInContentPage(selector, { + [IS_OSX ? "metaKey" : "ctrlKey"]: preview, + }); + await onSelectionChanged; +} + +async function checkElementSelected(selector, inspector) { + const el = await getNodeFront(selector, inspector); + is( + inspector.selection.nodeFront, + el, + `The element ${selector} is now selected` + ); +} + +function checkPickerMode(toolbox, isOn) { + const pickerButton = toolbox.doc.querySelector("#command-button-pick"); + is( + pickerButton.classList.contains("checked"), + isOn, + "The picker mode is correct" + ); + is( + pickerButton.getAttribute("aria-pressed"), + isOn ? "true" : "false", + "The picker mode is correct" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion-message.js b/devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion-message.js new file mode 100644 index 0000000000..2d134392d3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion-message.js @@ -0,0 +1,104 @@ +/* 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/. */ + +"use strict"; + +const REDUCED_MOTION_PREF = "ui.prefersReducedMotion"; +const DISMISS_MESSAGE_PREF = + "devtools.inspector.simple-highlighters.message-dismissed"; + +add_task(async function testMessageHiddenWhenPrefersReducedMotionDisabled() { + info("Disable ui.prefersReducedMotion"); + await pushPref(REDUCED_MOTION_PREF, 0); + + await pushPref(DISMISS_MESSAGE_PREF, false); + + const tab = await addTab("data:text/html,test"); + const toolbox = await gDevTools.showToolboxForTab(tab); + + await wait(1000); + ok( + !getSimpleHighlightersMessage(toolbox), + "The simple highlighters notification is not displayed" + ); +}); + +add_task(async function testMessageHiddenWhenAlreadyDismissed() { + info("Enable ui.prefersReducedMotion"); + await pushPref(REDUCED_MOTION_PREF, 1); + + info("Simulate already dismissed message"); + await pushPref(DISMISS_MESSAGE_PREF, true); + + const tab = await addTab("data:text/html,test"); + const toolbox = await gDevTools.showToolboxForTab(tab); + + await wait(1000); + ok( + !getSimpleHighlightersMessage(toolbox), + "The simple highlighters notification is not displayed" + ); +}); + +// Check that the message is displayed under the expected conditions, that the +// settings button successfully opens the corresponding panel and that after +// dismissing the message once, it is no longer displayed. +add_task(async function () { + info("Enable ui.prefersReducedMotion"); + await pushPref(REDUCED_MOTION_PREF, 1); + + info("Simulate already dismissed message"); + await pushPref(DISMISS_MESSAGE_PREF, false); + + const tab = await addTab("data:text/html,test"); + let toolbox = await gDevTools.showToolboxForTab(tab); + + info("Check the simple-highlighters message is displayed"); + let notification = await waitFor(() => getSimpleHighlightersMessage(toolbox)); + ok(notification, "A notification was displayed"); + + info("Click on the settings button from the notification"); + const onSettingsCallbackDone = toolbox.once( + "test-highlighters-settings-opened" + ); + const settingsButton = notification.querySelector(".notificationButton"); + settingsButton.click(); + + info("Wait until the open settings button callback is done"); + await onSettingsCallbackDone; + is(toolbox.currentToolId, "options", "The options panel was selected"); + + info("Close and reopen the toolbox"); + await toolbox.destroy(); + toolbox = await gDevTools.showToolboxForTab(tab); + + info("Check the notification is displayed again"); + notification = await waitFor(() => getSimpleHighlightersMessage(toolbox)); + ok(notification, "A notification was displayed after reopening the toolbox"); + + info("Close the notification"); + const closeButton = notification.querySelector(".messageCloseButton"); + closeButton.click(); + + info("Wait for the notification to be removed"); + await waitFor(() => !getSimpleHighlightersMessage(toolbox)); + + info("Close and reopen the toolbox"); + await toolbox.destroy(); + toolbox = await gDevTools.showToolboxForTab(tab); + + await wait(1000); + ok(!getSimpleHighlightersMessage(toolbox)); + is( + Services.prefs.getBoolPref(DISMISS_MESSAGE_PREF), + true, + "The dismiss simple-highlighters-message preference was set to true" + ); +}); + +function getSimpleHighlightersMessage(toolbox) { + return toolbox.doc.querySelector( + '.notification[data-key="simple-highlighters-message"]' + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion.js b/devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion.js new file mode 100644 index 0000000000..8b6f576182 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-reduced-motion.js @@ -0,0 +1,89 @@ +/* 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/. */ + +"use strict"; + +// Check that the boxmodel highlighter is styled differently when +// ui.prefersReducedMotion is enabled. + +const TEST_URL = + "data:text/html;charset=utf-8,<h1>test1</h1><h2>test2</h2><h3>test3</h3>"; + +add_task(async function () { + info("Disable ui.prefersReducedMotion"); + await pushPref("ui.prefersReducedMotion", 0); + + info("Enable simple highlighters"); + await pushPref("devtools.inspector.simple-highlighters-reduced-motion", true); + + const { highlighterTestFront, inspector } = await openInspectorForURL( + TEST_URL + ); + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.BOXMODEL; + + const front = inspector.inspectorFront; + const highlighterFront = await front.getHighlighterByType(HIGHLIGHTER_TYPE); + + // Small helper to retrieve the computed style of a specific highlighter + // element. + const getElementComputedStyle = async (id, property) => { + info(`Retrieve computed style for property ${property} on element ${id}`); + return highlighterTestFront.getHighlighterComputedStyle( + id, + property, + highlighterFront + ); + }; + + info("Highlight a node and check the highlighter is filled"); + await selectAndHighlightNode("h1", inspector); + let stroke = await getElementComputedStyle("box-model-content", "stroke"); + let fill = await getElementComputedStyle("box-model-content", "fill"); + is( + stroke, + "none", + "If prefersReducedMotion is disabled, stroke style is none" + ); + ok( + InspectorUtils.isValidCSSColor(fill), + "If prefersReducedMotion is disabled, fill style is a valid color" + ); + await inspector.highlighters.hideHighlighterType(HIGHLIGHTER_TYPE); + + info("Enable ui.prefersReducedMotion"); + await pushPref("ui.prefersReducedMotion", 1); + + info("Highlight a node and check the highlighter uses stroke and not fill"); + await selectAndHighlightNode("h2", inspector); + stroke = await getElementComputedStyle("box-model-content", "stroke"); + fill = await getElementComputedStyle("box-model-content", "fill"); + ok( + InspectorUtils.isValidCSSColor(stroke), + "If prefersReducedMotion is enabled, stroke style is a valid color" + ); + is(fill, "none", "If prefersReducedMotion is enabled, fill style is none"); + + await inspector.highlighters.hideHighlighterType(HIGHLIGHTER_TYPE); + + info("Disable simple highlighters"); + await pushPref( + "devtools.inspector.simple-highlighters-reduced-motion", + false + ); + + info("Highlight a node and check the highlighter is filled again"); + await selectAndHighlightNode("h3", inspector); + stroke = await getElementComputedStyle("box-model-content", "stroke"); + fill = await getElementComputedStyle("box-model-content", "fill"); + is( + stroke, + "none", + "If simple highlighters are disabled, stroke style is none" + ); + ok( + InspectorUtils.isValidCSSColor(fill), + "If simple highlighters are disabled, fill style is a valid color" + ); + await inspector.highlighters.hideHighlighterType(HIGHLIGHTER_TYPE); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-reload.js b/devtools/client/inspector/test/browser_inspector_highlighter-reload.js new file mode 100644 index 0000000000..ac3dac4e2c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-reload.js @@ -0,0 +1,36 @@ +/* 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/. */ + +"use strict"; + +// Test that the node picker continues to work after page reload + +const TEST_URL = URL_ROOT_SSL + "doc_inspector_highlighter_dom.html"; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + await startPicker(toolbox); + + info("Selecting the simple-div1 DIV"); + await hoverElement(inspector, "#simple-div1"); + + // Reload the current page (navigate to the same URL) + await navigateTo(TEST_URL); + + // hoverElement() resolves after both the "picker-node-hovered" event + // and the "highlighter-shown" event are triggered. If this test doesn't timeout, + // it means node picking and node highlighting continue to work as expected. + info("Selecting the simple-div2 DIV after reload"); + await hoverElement(inspector, "#simple-div2"); + + info("Picking the simple-div2 DIV after reload"); + await pickElement(inspector, "#simple-div2", 0, 0); + + is( + inspector.selection.nodeFront.id, + "simple-div2", + "The simple-div2 DIV has been picked after reload" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js new file mode 100644 index 0000000000..a0ee23a324 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js @@ -0,0 +1,116 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the geometry highlighter elements. + +const TEST_URL = + "data:text/html;charset=utf-8," + + "<div style='position:absolute;left: 0; top: 0; " + + "width: 40000px; height: 8000px'></div>"; + +const ID = "rulers-highlighter-"; + +// Maximum size, in pixel, for the horizontal ruler and vertical ruler +// used by RulersHighlighter +const RULERS_MAX_X_AXIS = 10000; +const RULERS_MAX_Y_AXIS = 15000; +// Number of steps after we add a text in RulersHighliter; +// currently the unit is in pixel. +const RULERS_TEXT_STEP = 100; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + + const highlighter = await front.getHighlighterByType("RulersHighlighter"); + + await isHiddenByDefault(highlighter, inspector, highlighterTestFront); + await isVisibleAfterShow(highlighter, inspector, highlighterTestFront); + await hasRightLabelsContent(highlighter, inspector, highlighterTestFront); + await isHiddenAfterHide(highlighter, inspector, highlighterTestFront); + + await highlighter.finalize(); +}); + +async function isHiddenByDefault( + highlighterFront, + inspector, + highlighterTestFront +) { + info("Checking the highlighter is hidden by default"); + + const hidden = await isRulerHidden(highlighterFront, highlighterTestFront); + ok(hidden, "highlighter is hidden by default"); +} + +async function isVisibleAfterShow( + highlighterFront, + inspector, + highlighterTestFront +) { + info("Checking the highlighter is displayed when asked"); + // the rulers doesn't need any node, but as highligher it seems mandatory + // ones, so the body is given + const body = await getNodeFront("body", inspector); + await highlighterFront.show(body); + + const hidden = await isRulerHidden(highlighterFront, highlighterTestFront); + ok(!hidden, "highlighter is visible after show"); +} + +async function isHiddenAfterHide( + highlighterFront, + inspector, + highlighterTestFront +) { + info("Checking that the highlighter is hidden after disabling it"); + await highlighterFront.hide(); + + const hidden = await isRulerHidden(highlighterFront, highlighterTestFront); + ok(hidden, "highlighter is hidden"); +} + +async function hasRightLabelsContent( + highlighterFront, + inspector, + highlighterTestFront +) { + info("Checking the rulers have the proper text, based on rulers' size"); + + const contentX = await highlighterTestFront.getHighlighterNodeTextContent( + `${ID}x-axis-text`, + highlighterFront + ); + const contentY = await highlighterTestFront.getHighlighterNodeTextContent( + `${ID}y-axis-text`, + highlighterFront + ); + + let expectedX = ""; + for (let i = RULERS_TEXT_STEP; i < RULERS_MAX_X_AXIS; i += RULERS_TEXT_STEP) { + expectedX += i; + } + + is(contentX, expectedX, "x axis text content is correct"); + + let expectedY = ""; + for (let i = RULERS_TEXT_STEP; i < RULERS_MAX_Y_AXIS; i += RULERS_TEXT_STEP) { + expectedY += i; + } + + is(contentY, expectedY, "y axis text content is correct"); +} + +async function isRulerHidden(highlighterFront, highlighterTestFront) { + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + ID + "elements", + "hidden", + highlighterFront + ); + return hidden === "true"; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js new file mode 100644 index 0000000000..44bd1514b3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js @@ -0,0 +1,201 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the geometry highlighter elements. + +const TEST_URL = + "data:text/html;charset=utf-8," + + "<div style='position:absolute;left: 0; top: 0; " + + "width: 40000px; height: 8000px'></div>"; + +const ID = "rulers-highlighter-"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + + const highlighter = await front.getHighlighterByType("RulersHighlighter"); + + // the rulers doesn't need any node, but as highligher it seems mandatory + // ones, so the body is given + const body = await getNodeFront("body", inspector); + await highlighter.show(body); + + await isUpdatedAfterScroll(highlighter, inspector, highlighterTestFront); + + await highlighter.finalize(); +}); + +async function isUpdatedAfterScroll( + highlighterFront, + inspector, + highlighterTestFront +) { + info("Check the rulers' position by default"); + + let xAxisRulerTransform = + await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}x-axis-ruler`, + "transform", + highlighterFront + ); + let xAxisTextTransform = + await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}x-axis-text`, + "transform", + highlighterFront + ); + let yAxisRulerTransform = + await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}y-axis-ruler`, + "transform", + highlighterFront + ); + let yAxisTextTransform = + await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}y-axis-text`, + "transform", + highlighterFront + ); + + is(xAxisRulerTransform, null, "x axis ruler is positioned properly"); + is(xAxisTextTransform, null, "x axis text are positioned properly"); + is(yAxisRulerTransform, null, "y axis ruler is positioned properly"); + is(yAxisTextTransform, null, "y axis text are positioned properly"); + + info("Ask the content window to scroll to specific coords"); + + const x = 200, + y = 300; + + let data = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [x, y], + (_x, _y) => { + return new Promise(resolve => { + content.addEventListener( + "scroll", + () => resolve({ x: content.scrollX, y: content.scrollY }), + { once: true } + ); + content.scrollTo(_x, _y); + }); + } + ); + + is(data.x, x, "window scrolled properly horizontally"); + is(data.y, y, "window scrolled properly vertically"); + + info("Check the rulers are properly positioned after the scrolling"); + + xAxisRulerTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}x-axis-ruler`, + "transform", + highlighterFront + ); + xAxisTextTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}x-axis-text`, + "transform", + highlighterFront + ); + yAxisRulerTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}y-axis-ruler`, + "transform", + highlighterFront + ); + yAxisTextTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}y-axis-text`, + "transform", + highlighterFront + ); + + is( + xAxisRulerTransform, + `translate(-${x})`, + "x axis ruler is positioned properly" + ); + is( + xAxisTextTransform, + `translate(-${x})`, + "x axis text are positioned properly" + ); + is( + yAxisRulerTransform, + `translate(0, -${y})`, + "y axis ruler is positioned properly" + ); + is( + yAxisTextTransform, + `translate(0, -${y})`, + "y axis text are positioned properly" + ); + + info("Ask the content window to scroll relative to the current position"); + + data = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [-50, -60], + (_deltaX, _deltaY) => { + return new Promise(resolve => { + content.addEventListener( + "scroll", + () => resolve({ x: content.scrollX, y: content.scrollY }), + { once: true } + ); + content.scrollBy(_deltaX, _deltaY); + }); + } + ); + + is(data.x, x - 50, "window scrolled properly horizontally"); + is(data.y, y - 60, "window scrolled properly vertically"); + + info("Check the rulers are properly positioned after the relative scrolling"); + + xAxisRulerTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}x-axis-ruler`, + "transform", + highlighterFront + ); + xAxisTextTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}x-axis-text`, + "transform", + highlighterFront + ); + yAxisRulerTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}y-axis-ruler`, + "transform", + highlighterFront + ); + yAxisTextTransform = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}y-axis-text`, + "transform", + highlighterFront + ); + + is( + xAxisRulerTransform, + `translate(-${x - 50})`, + "x axis ruler is positioned properly" + ); + is( + xAxisTextTransform, + `translate(-${x - 50})`, + "x axis text are positioned properly" + ); + is( + yAxisRulerTransform, + `translate(0, -${y - 60})`, + "y axis ruler is positioned properly" + ); + is( + yAxisTextTransform, + `translate(0, -${y - 60})`, + "y axis text are positioned properly" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rulers_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_03.js new file mode 100644 index 0000000000..9644758341 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_03.js @@ -0,0 +1,117 @@ +/* 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/. */ + +"use strict"; + +// Test the creation of the viewport infobar and makes sure if resizes correctly + +const TEST_URL = + "data:text/html;charset=utf-8," + + "<div style='position:absolute;left: 0; top: 0; " + + "width: 20px; height: 50px'></div>"; + +const ID = "viewport-size-highlighter-"; + +var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + + const highlighter = await front.getHighlighterByType( + "ViewportSizeHighlighter" + ); + + await isVisibleAfterShow(highlighter, inspector, highlighterTestFront); + await hasRightLabelsContent(highlighter, highlighterTestFront); + await resizeInspector(inspector); + await hasRightLabelsContent(highlighter, highlighterTestFront); + await isHiddenAfterHide(highlighter, inspector, highlighterTestFront); + + await highlighter.finalize(); +}); + +async function isVisibleAfterShow( + highlighterFront, + inspector, + highlighterTestFront +) { + info("Checking that the viewport infobar is displayed"); + // the rulers doesn't need any node, but as highligher it seems mandatory + // ones, so the body is given + const body = await getNodeFront("body", inspector); + await highlighterFront.show(body); + + const hidden = await isViewportInfobarHidden( + highlighterFront, + highlighterTestFront + ); + ok(!hidden, "viewport infobar is visible after show"); +} + +async function isHiddenAfterHide( + highlighterFront, + inspector, + highlighterTestFront +) { + info("Checking that the viewport infobar is hidden after disabling"); + await highlighterFront.hide(); + + const hidden = await isViewportInfobarHidden( + highlighterFront, + highlighterTestFront + ); + ok(hidden, "viewport infobar is hidden after hide"); +} + +async function hasRightLabelsContent(highlighterFront, highlighterTestFront) { + const windowDimensions = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { + getWindowDimensions, + } = require("resource://devtools/shared/layout/utils.js"); + return getWindowDimensions(content); + } + ); + const windowHeight = Math.round(windowDimensions.height); + const windowWidth = Math.round(windowDimensions.width); + const windowText = windowWidth + "px \u00D7 " + windowHeight + "px"; + + info("Wait until the rulers dimension tooltip have the proper text"); + await asyncWaitUntil(async () => { + const dimensionText = + await highlighterTestFront.getHighlighterNodeTextContent( + `${ID}viewport-infobar-container`, + highlighterFront + ); + return dimensionText == windowText; + }, 100); +} + +async function resizeInspector(inspector) { + info( + "Docking the toolbox to the side of the browser to change the window size" + ); + const toolbox = inspector.toolbox; + await toolbox.switchHost(Toolbox.HostType.RIGHT); + + // Wait for some time to avoid measuring outdated window dimensions. + await wait(100); +} + +async function isViewportInfobarHidden(highlighterFront, highlighterTestFront) { + const hidden = await highlighterTestFront.getHighlighterNodeAttribute( + `${ID}viewport-infobar-container`, + "hidden", + highlighterFront + ); + return hidden === "true"; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js new file mode 100644 index 0000000000..6dcbae3383 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js @@ -0,0 +1,80 @@ +/* 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/. */ + +"use strict"; + +// Test that the custom selector highlighter creates as many box-model +// highlighters as there are nodes that match the given selector + +const TEST_URL = + "data:text/html;charset=utf-8," + + "<div id='test-node'>test node</div>" + + "<ul>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + "</ul>"; + +const TEST_DATA = [ + { + selector: "#test-node", + containerCount: 1, + }, + { + selector: null, + containerCount: 0, + }, + { + selector: undefined, + containerCount: 0, + }, + { + selector: ".invalid-class", + containerCount: 0, + }, + { + selector: ".item", + containerCount: 5, + }, + { + selector: "#test-node, ul, .item", + containerCount: 7, + }, +]; + +requestLongerTimeout(5); + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + const front = inspector.inspectorFront; + const highlighter = await front.getHighlighterByType("SelectorHighlighter"); + + const contextNode = await getNodeFront("body", inspector); + + for (const { selector, containerCount } of TEST_DATA) { + info( + "Showing the highlighter on " + + selector + + ". Expecting " + + containerCount + + " highlighter containers" + ); + + await highlighter.show(contextNode, { selector }); + + const nb = await highlighterTestFront.getSelectorHighlighterBoxNb( + highlighter.actorID + ); + Assert.notStrictEqual(nb, null, "The number of highlighters was retrieved"); + + is(nb, containerCount, "The correct number of highlighers were created"); + await highlighter.hide(); + } + + await highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js new file mode 100644 index 0000000000..94ccd624e0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js @@ -0,0 +1,84 @@ +/* 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/. */ + +"use strict"; + +// Test that the custom selector highlighter creates highlighters for nodes in +// the right frame. + +const FRAME_SRC = + "data:text/html;charset=utf-8," + "<div class=sub-level-node></div>"; + +const TEST_URL = + "data:text/html;charset=utf-8," + + "<div class=root-level-node></div>" + + '<iframe src="' + + FRAME_SRC + + '" />'; + +const TEST_DATA = [ + { + selector: ".root-level-node", + containerCount: 1, + }, + { + selector: ".sub-level-node", + containerCount: 0, + }, + { + inIframe: true, + selector: ".root-level-node", + containerCount: 0, + }, + { + inIframe: true, + selector: ".sub-level-node", + containerCount: 1, + }, +]; + +requestLongerTimeout(5); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + for (const { inIframe, selector, containerCount } of TEST_DATA) { + info( + "Showing the highlighter on " + + selector + + ". Expecting " + + containerCount + + " highlighter containers" + ); + + let contextNode; + if (inIframe) { + contextNode = await getNodeFrontInFrames(["iframe", "body"], inspector); + } else { + contextNode = await getNodeFront("body", inspector); + } + + const inspectorFront = await contextNode.targetFront.getFront("inspector"); + const highlighter = await inspectorFront.getHighlighterByType( + "SelectorHighlighter" + ); + const highlighterTestFront = await getHighlighterTestFront( + inspector.toolbox, + { + target: contextNode.targetFront, + } + ); + + await highlighter.show(contextNode, { selector }); + + const nb = await highlighterTestFront.getSelectorHighlighterBoxNb( + highlighter.actorID + ); + Assert.notStrictEqual(nb, null, "The number of highlighters was retrieved"); + + is(nb, containerCount, "The correct number of highlighers were created"); + await highlighter.hide(); + await highlighter.finalize(); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js new file mode 100644 index 0000000000..ba81fde1b8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js @@ -0,0 +1,83 @@ +/* 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/. */ + +"use strict"; + +// Test that the highlighter stays correctly positioned and has the right aspect +// ratio even when the page is zoomed in or out. + +const TEST_URL = "data:text/html;charset=utf-8,<div>zoom me</div>"; + +// TEST_LEVELS entries should contain the zoom level to test. +const TEST_LEVELS = [2, 1, 0.5]; + +// Returns the expected style attribute value to check for on the highlighter's elements +// node, for the values given. +const expectedStyle = (w, h, z) => + (z !== 1 + ? `transform-origin: left top 0px; transform: scale(${1 / z}); ` + : "") + + `position: absolute; width: ${w * z}px; height: ${h * z}px; ` + + "overflow: hidden;"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URL + ); + + const div = await getNodeFront("div", inspector); + + for (const level of TEST_LEVELS) { + info(`Zoom to level ${level}`); + setContentPageZoomLevel(level); + + info("Highlight the test node"); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + div + ); + + const isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, `The highlighter is visible at zoom level ${level}`); + + await isNodeCorrectlyHighlighted(highlighterTestFront, "div"); + + info("Check that the highlighter root wrapper node was scaled down"); + + const style = await getElementsNodeStyle(highlighterTestFront); + + const { width, height } = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { + getWindowDimensions, + } = require("resource://devtools/shared/layout/utils.js"); + return getWindowDimensions(content); + } + ); + + is( + style, + expectedStyle(width, height, level), + "The style attribute of the root element is correct" + ); + + info("Unhighlight the node"); + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); + } +}); + +async function getElementsNodeStyle(highlighterTestFront) { + const value = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-elements", + "style" + ); + return value; +} diff --git a/devtools/client/inspector/test/browser_inspector_iframe-navigation.js b/devtools/client/inspector/test/browser_inspector_iframe-navigation.js new file mode 100644 index 0000000000..de97b1e020 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_iframe-navigation.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the highlighter element picker still works through iframe +// navigations. + +const TEST_URI = + "data:text/html;charset=utf-8," + + "<p>bug 699308 - test iframe navigation</p>" + + "<iframe src='data:text/html;charset=utf-8,hello world'></iframe>"; + +add_task(async function () { + const { inspector, toolbox, highlighterTestFront } = + await openInspectorForURL(TEST_URI); + + info("Starting element picker."); + await startPicker(toolbox); + + info("Mouse over for body element."); + await hoverElement(inspector, "body"); + + let isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "Inspector is highlighting."); + + await reloadIframe(inspector); + info("Frame reloaded. Reloading again."); + + await reloadIframe(inspector); + info("Frame reloaded twice."); + + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "Inspector is highlighting after iframe nav."); + + info("Stopping element picker."); + await toolbox.nodePicker.stop({ canceled: true }); +}); + +async function reloadIframe(inspector) { + const { resourceCommand } = inspector.commands; + + const { onResource: onNewRoot } = await resourceCommand.waitForNextResource( + resourceCommand.TYPES.ROOT_NODE, + { + ignoreExistingResources: true, + } + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const iframeEl = content.document.querySelector("iframe"); + await new Promise(resolve => { + iframeEl.addEventListener("load", () => resolve(), { once: true }); + iframeEl.contentWindow.location.reload(); + }); + }); + + await onNewRoot; +} diff --git a/devtools/client/inspector/test/browser_inspector_iframe-picker-bfcache-navigation.js b/devtools/client/inspector/test/browser_inspector_iframe-picker-bfcache-navigation.js new file mode 100644 index 0000000000..37ca935c2e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_iframe-picker-bfcache-navigation.js @@ -0,0 +1,121 @@ +/* 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/. */ + +"use strict"; + +// Test that using the iframe picker usage + bfcache navigations don't break the toolbox + +const IFRAME_URL = `https://example.com/document-builder.sjs?html=<meta charset=utf8><div id=in-iframe>frame</div>`; + +const URL = + "https://example.com/document-builder.sjs?html=" + + `<meta charset=utf8><iframe src='${IFRAME_URL}'></iframe><div id=top>top</div>`; + +add_task(async function () { + await pushPref("devtools.command-button-frames.enabled", true); + + // Don't show the third panel to limit the logs and activity. + await pushPref("devtools.inspector.three-pane-enabled", false); + await pushPref("devtools.inspector.activeSidebar", "ruleview"); + + const { inspector, toolbox } = await openInspectorForURL(URL); + + info("Verify we are on the top level document"); + await assertMarkupViewAsTree( + ` + body + iframe!ignore-children + div id="top"`, + "body", + inspector + ); + + info("Navigate to a different page to put the current page in the bfcache"); + let onMarkupLoaded = getOnInspectorReadyAfterNavigation(inspector); + await navigateTo( + "https://example.org/document-builder.sjs?html=<meta charset=utf8>example.org page" + ); + await onMarkupLoaded; + + info("Navigate back to the example.com page"); + onMarkupLoaded = getOnInspectorReadyAfterNavigation(inspector); + gBrowser.goBack(); + await onMarkupLoaded; + + // Verify that the frame map button is empty at the moment. + const btn = toolbox.doc.getElementById("command-button-frames"); + ok(!btn.firstChild, "The frame list button doesn't have any children"); + + // Open frame menu and wait till it's available on the screen. + const frameMenu = toolbox.doc.getElementById("toolbox-frame-menu"); + info("Wait for the frame to be populated before opening the tooltip"); + await waitUntil(() => frameMenu.childNodes.length == 2); + const panel = toolbox.doc.getElementById("command-button-frames-panel"); + btn.click(); + ok(panel, "popup panel has created."); + await waitUntil(() => panel.classList.contains("tooltip-visible")); + + // Verify that the menu is populated. + const menuList = toolbox.doc.getElementById("toolbox-frame-menu"); + const frames = Array.from(menuList.querySelectorAll(".command")).sort( + (a, b) => a.textContent < b.textContent + ); + is(frames.length, 2, "We have both frames in the menu"); + const [topLevelFrameItem, iframeItem] = frames; + + is( + topLevelFrameItem.querySelector(".label").textContent, + URL, + "Got top-level document in the list" + ); + is( + iframeItem.querySelector(".label").textContent, + IFRAME_URL, + "Got iframe document in the list" + ); + + info("Select the iframe in the iframe picker"); + const newRoot = inspector.once("new-root"); + iframeItem.click(); + await newRoot; + + info("Check that the markup view was updated"); + await assertMarkupViewAsTree( + ` + body + div id="in-iframe"`, + "body", + inspector + ); + + info("Go forward (to example.org page)"); + onMarkupLoaded = getOnInspectorReadyAfterNavigation(inspector); + gBrowser.goForward(); + await onMarkupLoaded; + + info("And go back again to example.com page"); + onMarkupLoaded = getOnInspectorReadyAfterNavigation(inspector); + gBrowser.goBack(); + await onMarkupLoaded; + + info("Check that the document markup is displayed as expected"); + await assertMarkupViewAsTree( + ` + body + iframe!ignore-children + div id="top"`, + "body", + inspector + ); +}); + +function getOnInspectorReadyAfterNavigation(inspector) { + return Promise.all([ + inspector.once("reloaded"), + // the inspector is initializing the accessibility front in onTargetAvailable, so we + // need to wait for the target to be processed, otherwise we may end up with pending + // promises failures. + inspector.toolbox.commands.targetCommand.once("processed-available-target"), + ]); +} diff --git a/devtools/client/inspector/test/browser_inspector_iframe-picker.js b/devtools/client/inspector/test/browser_inspector_iframe-picker.js new file mode 100644 index 0000000000..fab3e4577b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_iframe-picker.js @@ -0,0 +1,131 @@ +/* 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/. */ + +"use strict"; + +// Test frame selection switching at toolbox level when using the inspector + +const FrameURL = + "data:text/html;charset=UTF-8," + + encodeURI('<div id="in-iframe">frame</div>'); +const URL = + "data:text/html;charset=UTF-8," + + encodeURI('<iframe src="' + FrameURL + '"></iframe><div id="top">top</div>'); + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(URL); + + // Verify we are on the top level document + await assertMarkupViewAsTree( + ` + body + iframe!ignore-children + div id="top"`, + "body", + inspector + ); + + assertMarkupViewIsLoaded(inspector); + + // Verify that the frame map button is empty at the moment. + const btn = toolbox.doc.getElementById("command-button-frames"); + ok(!btn.firstChild, "The frame list button doesn't have any children"); + + // Open frame menu and wait till it's available on the screen. + const panel = toolbox.doc.getElementById("command-button-frames-panel"); + btn.click(); + ok(panel, "popup panel has created."); + await waitUntil(() => panel.classList.contains("tooltip-visible")); + + // Verify that the menu is populated. + const menuList = toolbox.doc.getElementById("toolbox-frame-menu"); + const frames = Array.from(menuList.querySelectorAll(".command")); + is(frames.length, 2, "We have both frames in the menu"); + + frames.sort(function (a, b) { + return a.children[0].innerHTML.localeCompare(b.children[0].innerHTML); + }); + + is( + frames[0].querySelector(".label").textContent, + FrameURL, + "Got top level document in the list" + ); + is( + frames[1].querySelector(".label").textContent, + URL, + "Got iframe document in the list" + ); + + // Listen to will-navigate to check if the view is empty + const { resourceCommand } = toolbox.commands; + const { onResource: willNavigate } = + await resourceCommand.waitForNextResource( + resourceCommand.TYPES.DOCUMENT_EVENT, + { + ignoreExistingResources: true, + predicate(resource) { + return resource.name == "will-navigate"; + }, + } + ); + willNavigate.then(() => { + info("Navigation to the iframe has started, the inspector should be empty"); + assertMarkupViewIsEmpty(inspector); + }); + + // Only select the iframe after we are able to select an element from the top + // level document. + let newRoot = inspector.once("new-root"); + await selectNode("#top", inspector); + info("Select the iframe"); + frames[0].click(); + + if (!isEveryFrameTargetEnabled()) { + await willNavigate; + } + await newRoot; + + info("The iframe is selected, check that the markup view was updated"); + await assertMarkupViewAsTree( + ` + body + div id="in-iframe"`, + "body", + inspector + ); + assertMarkupViewIsLoaded(inspector); + + info( + "Remove the iframe and check that the inspector gets updated to show the top level frame markup" + ); + newRoot = inspector.once("new-root"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.document.querySelector("iframe").remove(); + }); + await newRoot; + + await assertMarkupViewAsTree( + ` + body + div id="top"`, + "body", + inspector + ); + assertMarkupViewIsLoaded(inspector); +}); + +function assertMarkupViewIsLoaded(inspector) { + const markupViewBox = inspector.panelDoc.getElementById("markup-box"); + is(markupViewBox.childNodes.length, 1, "The markup-view is loaded"); +} + +function assertMarkupViewIsEmpty(inspector) { + const markupFrame = inspector._markupFrame; + is( + markupFrame.contentDocument.getElementById("root").childNodes.length, + 0, + "The markup-view is unloaded" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_01.js b/devtools/client/inspector/test/browser_inspector_infobar_01.js new file mode 100644 index 0000000000..cc10addb66 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_01.js @@ -0,0 +1,113 @@ +/* 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/. */ + +"use strict"; + +// Check the position and text content of the highlighter nodeinfo bar. + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_01.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + const testData = [ + { + selector: "#top", + position: "bottom", + tag: "div", + id: "top", + classes: ".class1.class2", + dims: "500" + " \u00D7 " + "100", + arrowed: true, + }, + { + selector: "#vertical", + position: "top", + tag: "div", + id: "vertical", + classes: "", + arrowed: false, + // No dims as they will vary between computers + }, + { + selector: "#bottom", + position: "top", + tag: "div", + id: "bottom", + classes: "", + dims: "500" + " \u00D7 " + "100", + arrowed: true, + }, + { + selector: "body", + position: "bottom", + tag: "body", + classes: "", + arrowed: true, + // No dims as they will vary between computers + }, + { + selector: "clipPath", + position: "bottom", + tag: "clipPath", + id: "clip", + classes: "", + arrowed: false, + // No dims as element is not displayed and we just want to test tag name + }, + ]; + + for (const currTest of testData) { + await testPosition(currTest, inspector, highlighterTestFront); + } +}); + +async function testPosition(test, inspector, highlighterTestFront) { + info("Testing " + test.selector); + + await selectAndHighlightNode(test.selector, inspector); + + const position = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-infobar-container", + "position" + ); + is(position, test.position, "Node " + test.selector + ": position matches"); + + const tag = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-tagname" + ); + is(tag, test.tag, "node " + test.selector + ": tagName matches."); + + if (test.id) { + const id = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-id" + ); + is(id, "#" + test.id, "node " + test.selector + ": id matches."); + } + + const classes = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-classes" + ); + is(classes, test.classes, "node " + test.selector + ": classes match."); + + const arrowed = !(await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-infobar-container", + "hide-arrow" + )); + + is( + arrowed, + test.arrowed, + "node " + test.selector + ": arrow visibility match." + ); + + if (test.dims) { + const dims = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-dimensions" + ); + is(dims, test.dims, "node " + test.selector + ": dims match."); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_02.js b/devtools/client/inspector/test/browser_inspector_infobar_02.js new file mode 100644 index 0000000000..594629a40a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_02.js @@ -0,0 +1,53 @@ +/* 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/. */ + +"use strict"; + +// Check the text content of the highlighter info bar for namespaced elements. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + const testData = [ + { + selector: "svg", + tag: "svg:svg", + }, + { + selector: "circle", + tag: "svg:circle", + }, + ]; + + for (const currTest of testData) { + await testNode(currTest, inspector, highlighterTestFront); + } +}); + +async function testNode(test, inspector, highlighterTestFront) { + info("Testing " + test.selector); + + await selectAndHighlightNode(test.selector, inspector); + + const tag = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-tagname" + ); + is(tag, test.tag, "node " + test.selector + ": tagName matches."); +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_03.js b/devtools/client/inspector/test/browser_inspector_infobar_03.js new file mode 100644 index 0000000000..afbc06dcec --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_03.js @@ -0,0 +1,59 @@ +/* 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/. */ + +"use strict"; + +// Bug 1102269 - Make sure info-bar never gets outside of visible area after scrolling + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_03.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + const testData = { + selector: "body", + position: "overlap", + style: "position:fixed", + }; + + await testPositionAndStyle(testData, inspector, highlighterTestFront); +}); + +async function testPositionAndStyle(test, inspector, highlighterTestFront) { + info("Testing " + test.selector); + + await selectAndHighlightNode(test.selector, inspector); + + let style = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-infobar-container", + "style" + ); + + is( + style.split(";")[0].trim(), + test.style, + "Infobar shows on top of the page when page isn't scrolled" + ); + + info("Scroll down"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return new Promise(resolve => { + content.addEventListener("scroll", () => resolve(), { once: true }); + content.scrollTo({ top: 500 }); + }); + }); + + style = await highlighterTestFront.getHighlighterNodeAttribute( + "box-model-infobar-container", + "style" + ); + + is( + style.split(";")[0].trim(), + test.style, + "Infobar shows on top of the page even if the page is scrolled" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_04.js b/devtools/client/inspector/test/browser_inspector_infobar_04.js new file mode 100644 index 0000000000..5569dc6176 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_04.js @@ -0,0 +1,45 @@ +/* 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/. */ + +"use strict"; + +// Check the position and text content of the highlighter nodeinfo bar under page zoom. + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_01.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + const testData = { + selector: "#top", + dims: "500" + " \u00D7 " + "100", + }; + + await testInfobar(testData, inspector, highlighterTestFront); + info("Change zoom page to level 2."); + setContentPageZoomLevel(2); + info("Testing again the infobar after zoom."); + await testInfobar(testData, inspector, highlighterTestFront); +}); + +async function testInfobar(test, inspector, highlighterTestFront) { + info(`Testing ${test.selector}`); + // First, hide any existing box model highlighter. Duplicate calls to show are ignored. + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); + await selectAndHighlightNode(test.selector, inspector); + + // Ensure the node is the correct one. + const id = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-id" + ); + is(id, test.selector, `Node ${test.selector} selected.`); + + const dims = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-dimensions" + ); + is(dims, test.dims, "Node's infobar displays the right dimensions."); +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_05.js b/devtools/client/inspector/test/browser_inspector_infobar_05.js new file mode 100644 index 0000000000..3a1b9a978c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_05.js @@ -0,0 +1,119 @@ +/* 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/. */ + +"use strict"; + +// Bug 1521188 - Indicate grid/flex container/item in infobar +// Check the text content of the highlighter nodeinfo bar. +const HighlightersBundle = new Localization( + ["devtools/shared/highlighters.ftl"], + true +); + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_04.html"; + +const CLASS_GRID_TYPE = "box-model-infobar-grid-type"; +const CLASS_FLEX_TYPE = "box-model-infobar-flex-type"; + +const FLEX_CONTAINER_TEXT = + HighlightersBundle.formatValueSync("flextype-container"); +const FLEX_ITEM_TEXT = HighlightersBundle.formatValueSync("flextype-item"); +const FLEX_DUAL_TEXT = HighlightersBundle.formatValueSync("flextype-dual"); +const GRID_CONTAINER_TEXT = + HighlightersBundle.formatValueSync("gridtype-container"); +const GRID_ITEM_TEXT = HighlightersBundle.formatValueSync("gridtype-item"); +const GRID_DUAL_TEXT = HighlightersBundle.formatValueSync("gridtype-dual"); + +const TEST_DATA = [ + { + selector: "#flex-container", + flexText: FLEX_CONTAINER_TEXT, + gridText: "", + }, + { + selector: "#flex-item", + flexText: FLEX_ITEM_TEXT, + gridText: "", + }, + { + selector: "#flex-container-item", + flexText: FLEX_DUAL_TEXT, + gridText: "", + }, + { + selector: "#grid-container", + flexText: "", + gridText: GRID_CONTAINER_TEXT, + }, + { + selector: "#grid-item", + flexText: "", + gridText: GRID_ITEM_TEXT, + }, + { + selector: "#grid-container-item", + flexText: "", + gridText: GRID_DUAL_TEXT, + }, + { + selector: "#flex-item-grid-container", + flexText: FLEX_ITEM_TEXT, + gridText: GRID_CONTAINER_TEXT, + }, +]; + +const TEST_TEXT_DATA = [ + { + selector: "#flex-text-container", + flexText: FLEX_ITEM_TEXT, + gridText: "", + }, + { + selector: "#grid-text-container", + flexText: "", + gridText: GRID_ITEM_TEXT, + }, +]; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + + for (const currentTest of TEST_DATA) { + info("Testing " + currentTest.selector); + await testTextContent(currentTest, inspector, highlighterTestFront); + } + + for (const currentTest of TEST_TEXT_DATA) { + info("Testing " + currentTest.selector); + await testTextNodeTextContent(currentTest, inspector, highlighterTestFront); + } +}); + +async function testTextContent( + { selector, gridText, flexText }, + inspector, + highlighterTestFront +) { + await selectAndHighlightNode(selector, inspector); + + const gridType = await highlighterTestFront.getHighlighterNodeTextContent( + CLASS_GRID_TYPE + ); + const flexType = await highlighterTestFront.getHighlighterNodeTextContent( + CLASS_FLEX_TYPE + ); + + is(gridType, gridText, "node " + selector + ": grid type matches."); + is(flexType, flexText, "node " + selector + ": flex type matches."); +} + +async function testTextNodeTextContent(test, inspector, highlighterTestFront) { + const { walker } = inspector; + const div = await walker.querySelector(walker.rootNode, test.selector); + const { nodes } = await walker.children(div); + test.selector = nodes[0]; + await testTextContent(test, inspector, highlighterTestFront); +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_textnode.js b/devtools/client/inspector/test/browser_inspector_infobar_textnode.js new file mode 100644 index 0000000000..b02cee6819 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_textnode.js @@ -0,0 +1,53 @@ +/* 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/. */ + +"use strict"; + +// Bug 1309212 - Make sure info-bar is displayed with dimensions for text nodes. + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_textnode.html"; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + const { walker } = inspector; + + info("Retrieve the children of #textnode-container"); + const div = await walker.querySelector( + walker.rootNode, + "#textnode-container" + ); + const { nodes } = await inspector.walker.children(div); + + // Children 0, 2 and 4 are text nodes, for which we expect to see an infobar containing + // dimensions. + + // Regular text node. + info("Select the first text node"); + await selectNode(nodes[0], inspector, "test-highlight"); + await checkTextNodeInfoBar(highlighterTestFront); + + // Whitespace-only text node. + info("Select the second text node"); + await selectNode(nodes[2], inspector, "test-highlight"); + await checkTextNodeInfoBar(highlighterTestFront); + + // Regular text node. + info("Select the third text node"); + await selectNode(nodes[4], inspector, "test-highlight"); + await checkTextNodeInfoBar(highlighterTestFront); +}); + +async function checkTextNodeInfoBar(highlighterTestFront) { + const tag = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-tagname" + ); + is(tag, "#text", "node display name is #text"); + const dims = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-dimensions" + ); + // Do not assert dimensions as they might be platform specific. + ok(!!dims, "node has dims"); +} diff --git a/devtools/client/inspector/test/browser_inspector_initialization.js b/devtools/client/inspector/test/browser_inspector_initialization.js new file mode 100644 index 0000000000..a01cf5bbe3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_initialization.js @@ -0,0 +1,114 @@ +/* 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/. */ +"use strict"; + +// Tests for different ways to initialize the inspector. + +const HTML = ` + <div id="first" style="margin: 10em; font-size: 14pt; + font-family: helvetica, sans-serif; color: gray"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to test the inspector initialization.</p> + <p>If you are reading this, you should go do something else instead. Maybe + read a book. Or better yet, write some test-cases for another bit of code. + <span style="font-style: italic">Inspector's!</span> + </p> + <p id="closing">end transmission</p> + </div> +`; + +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); + +add_task(async function () { + const tab = await addTab(TEST_URI); + await testToolboxInitialization(tab); + await testContextMenuInitialization(); + await testContextMenuInspectorAlreadyOpen(); +}); + +async function testToolboxInitialization(tab) { + info("Opening inspector with gDevTools."); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "inspector", + }); + const inspector = toolbox.getCurrentPanel(); + + ok(true, "Inspector started, and notification received."); + ok(inspector, "Inspector instance is accessible."); + + await selectNode("p", inspector); + await testMarkupView("p", inspector); + await testBreadcrumbs("p", inspector); + + await scrollContentPageNodeIntoView(gBrowser.selectedBrowser, "span"); + + await selectNode("span", inspector); + await testMarkupView("span", inspector); + await testBreadcrumbs("span", inspector); + + info("Destroying toolbox"); + await toolbox.destroy(); + + ok(true, "'destroyed' notification received."); + const toolboxForTab = gDevTools.getToolboxForTab(tab); + ok(!toolboxForTab, "Toolbox destroyed."); +} + +async function testContextMenuInitialization() { + info("Opening inspector by clicking on 'Inspect Element' context menu item"); + await clickOnInspectMenuItem("#salutation"); + + info("Checking inspector state."); + await testMarkupView("#salutation"); + await testBreadcrumbs("#salutation"); +} + +async function testContextMenuInspectorAlreadyOpen() { + info("Changing node by clicking on 'Inspect Element' context menu item"); + + const inspector = getActiveInspector(); + ok(inspector, "Inspector is active"); + + await clickOnInspectMenuItem("#closing"); + + ok(true, "Inspector was updated when 'Inspect Element' was clicked."); + await testMarkupView("#closing", inspector); + await testBreadcrumbs("#closing", inspector); +} + +async function testMarkupView(selector, inspector) { + if (!inspector) { + inspector = getActiveInspector(); + } + const nodeFront = await getNodeFront(selector, inspector); + try { + is( + inspector.selection.nodeFront, + nodeFront, + "Right node is selected in the markup view" + ); + } catch (ex) { + ok(false, "Got exception while resolving selected node of markup view."); + console.error(ex); + } +} + +async function testBreadcrumbs(selector, inspector) { + if (!inspector) { + inspector = getActiveInspector(); + } + const nodeFront = await getNodeFront(selector, inspector); + + const b = inspector.breadcrumbs; + const expectedText = b.prettyPrintNodeAsText(nodeFront); + const button = b.container.querySelector("button[checked=true]"); + ok(button, "A crumbs is checked=true"); + is( + button.getAttribute("title"), + expectedText, + "Crumb refers to the right node" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_inspect-object-element.js b/devtools/client/inspector/test/browser_inspector_inspect-object-element.js new file mode 100644 index 0000000000..9ae870185d --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_inspect-object-element.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// A regression test for bug 665880 to make sure elements inside <object> can +// be inspected without exceptions. + +const TEST_URI = + "data:text/html;charset=utf-8," + + "<object><p>browser_inspector_inspect-object-element.js</p></object>"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + await selectNode("object", inspector); + + ok(true, "Selected <object> without throwing"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_inspect_loading_document.js b/devtools/client/inspector/test/browser_inspector_inspect_loading_document.js new file mode 100644 index 0000000000..6798c85394 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_inspect_loading_document.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Fix blank inspector bug when opening DevTools on a webpage with a document +// where document.write is called without ever calling document.close. +// Such a document remains forever in the "loading" readyState. See Bug 1765760. + +const TEST_URL = + `data:text/html;charset=utf-8,` + + encodeURIComponent(` + <!DOCTYPE html> + <html lang="en"> + <body> + <div id=outsideframe></div> + <script type="text/javascript"> + const iframe = document.createElement("iframe"); + iframe.src = "about:blank"; + iframe.addEventListener('load', () => { + iframe.contentDocument.write('<div id=inframe>inframe</div>'); + }, true); + document.body.appendChild(iframe); + </script> + </body> + </html> +`); + +add_task(async function testSlowLoadingFrame() { + const loadingTab = BrowserTestUtils.addTab(gBrowser, TEST_URL); + gBrowser.selectedTab = loadingTab; + + // Note: we cannot use `await addTab` here because the iframe never finishes + // loading. But we still want to wait for the frame to reach the loading state + // and to display a test element so that we can reproduce the initial issue + // fixed by Bug 1765760. + info("Wait for the loading iframe to be ready to test"); + await TestUtils.waitForCondition(async () => { + try { + return await ContentTask.spawn(gBrowser.selectedBrowser, {}, () => { + const iframe = content.document.querySelector("iframe"); + return ( + iframe?.contentDocument.readyState === "loading" && + iframe.contentDocument.getElementById("inframe") + ); + }); + } catch (e) { + return false; + } + }); + + const { inspector } = await openInspector(); + + info("Check the markup view displays the loading iframe successfully"); + await assertMarkupViewAsTree( + ` + body + div id="outsideframe" + script!ignore-children + iframe + #document + html + head + body + div id="inframe"`, + "body", + inspector + ); +}); + +add_task(async function testSlowLoadingDocument() { + info("Create a test server serving a slow document"); + const httpServer = createTestHTTPServer(); + httpServer.registerContentType("html", "text/html"); + + // This promise allows to block serving the complete document from the test. + let unblockRequest; + const onRequestUnblocked = new Promise(r => (unblockRequest = r)); + + httpServer.registerPathHandler(`/`, async function (request, response) { + response.processAsync(); + response.setStatusLine(request.httpVersion, 200, "OK"); + + // Split the page content in 2 parts: + // - opening body tag and the "#start" div will be returned immediately + // - "#end" div and closing body tag are blocked on a promise. + const page_start = "<body><div id='start'>start</div>"; + const page_end = "<div id='end'>end</div></body>"; + const page = page_start + page_end; + + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.setHeader("Content-Length", page.length + "", false); + response.write(page_start); + + await onRequestUnblocked; + + response.write(page_end); + response.finish(); + }); + + const port = httpServer.identity.primaryPort; + const TEST_URL_2 = `http://localhost:${port}/`; + + // Same as in the other task, we cannot wait for the full load. + info("Open a new tab on TEST_URL_2 and select it"); + const loadingTab = BrowserTestUtils.addTab(gBrowser, TEST_URL_2); + gBrowser.selectedTab = loadingTab; + + info("Wait for the #start div to be available in the document"); + await TestUtils.waitForCondition(async () => { + try { + return await ContentTask.spawn(gBrowser.selectedBrowser, {}, () => + content.document.getElementById("start") + ); + } catch (e) { + return false; + } + }); + + const { inspector } = await openInspector(); + + info("Check that the inspector is not blank and only shows the #start div"); + await assertMarkupViewAsTree( + ` + body + div id="start"`, + "body", + inspector + ); + + // Navigate to about:blank to clean the state. + await navigateTo("about:blank"); + + await navigateTo(TEST_URL_2, { waitForLoad: false }); + info("Wait for the #start div to be available as a markupview container"); + await TestUtils.waitForCondition(async () => { + const nodeFront = await getNodeFront("#start", inspector); + return nodeFront && getContainerForNodeFront(nodeFront, inspector); + }); + + info("Check that the inspector is not blank and only shows the #start div"); + await assertMarkupViewAsTree( + ` + body + div id="start"`, + "body", + inspector + ); + + info("Unblock the document request"); + unblockRequest(); + + info("Wait for the #end div to be available as a markupview container"); + await TestUtils.waitForCondition(async () => { + const nodeFront = await getNodeFront("#end", inspector); + return nodeFront && getContainerForNodeFront(nodeFront, inspector); + }); + + info("Check that the inspector will ultimately show the #end div"); + await assertMarkupViewAsTree( + ` + body + div id="start" + div id="end"`, + "body", + inspector + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_inspect_mutated_node.js b/devtools/client/inspector/test/browser_inspector_inspect_mutated_node.js new file mode 100644 index 0000000000..f43fc0bf6f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_inspect_mutated_node.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that Inspect Element works even if the selector of the inspected +// element changes after opening the context menu. +// For this test, we explicitly move the element when opening the context menu. + +const TEST_URL = + `data:text/html;charset=utf-8,` + + encodeURIComponent(` + + <div id="parent-1"> + <span>Inspect me</span> + </div> + <div id="parent-2"></div> + <div id="changing-id">My ID will change</div> + + <script type="text/javascript"> + document.querySelector("span").addEventListener("contextmenu", () => { + // Right after opening the context menu, move the target element in the + // tree to change its selector. + window.setTimeout(() => { + const span = document.querySelector("span"); + document.getElementById("parent-2").appendChild(span); + }, 0) + }); + + document.querySelector("#changing-id").addEventListener("contextmenu", () => { + // Right after opening the context menu, update the id of #changing-id + // to make sure we are not relying on outdated selectors to find the node + window.setTimeout(() => { + document.querySelector("#changing-id").id= "new-id"; + }, 0) + }); + </script> +`); + +add_task(async function () { + await addTab(TEST_URL); + await testNodeWithChangingPath(); + await testNodeWithChangingId(); +}); + +async function testNodeWithChangingPath() { + info("Test that inspect element works if the CSS path changes"); + const inspector = await clickOnInspectMenuItem("span"); + + const selectedNode = inspector.selection.nodeFront; + ok(selectedNode, "A node is selected in the inspector"); + is( + selectedNode.tagName.toLowerCase(), + "span", + "The selected node is correct" + ); + const parentNode = await selectedNode.parentNode(); + is( + parentNode.id, + "parent-2", + "The selected node is under the expected parent node" + ); +} + +async function testNodeWithChangingId() { + info("Test that inspect element works if the id changes"); + const inspector = await clickOnInspectMenuItem("#changing-id"); + + const selectedNode = inspector.selection.nodeFront; + ok(selectedNode, "A node is selected in the inspector"); + is(selectedNode.id.toLowerCase(), "new-id", "The selected node is correct"); +} diff --git a/devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu.js b/devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu.js new file mode 100644 index 0000000000..b21141b10a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu.js @@ -0,0 +1,140 @@ +/* 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/. */ + +"use strict"; + +// Tests for inspecting iframes and frames in browser context menu +const IFRAME_URI = `data:text/html;charset=utf-8,${encodeURI( + `<div id="in-iframe">div in the iframe</div>` +)}`; +const TEST_IFRAME_DOC_URI = `data:text/html;charset=utf-8,${encodeURI(` + <div id="salutation">Salution in top document</div> + <iframe src="${IFRAME_URI}"></iframe>`)}`; + +// <frameset> acts as the body element, so we can't use them in a document with other elements +// and have to set a dedicated document so we can test them. +const SAME_ORIGIN_FRAME_URI = `https://example.com/document-builder.sjs?html=<h2 id=in-same-origin-frame>h2 in the same origin frame</h2>`; +const REMOTE_ORIGIN_FRAME_URI = `https://example.org/document-builder.sjs?html=<h3 id=in-remote-frame>h3 in the remote frame</h3>`; +const TEST_FRAME_DOC_URI = `https://example.com/document-builder.sjs?html=${encodeURI(` + <frameset cols="50%,50%"> + <frame class=same-origin src="${SAME_ORIGIN_FRAME_URI}"></frame> + <frame class=remote src="${REMOTE_ORIGIN_FRAME_URI}"></frame> + </frameset>`)}`; + +add_task(async function () { + await pushPref("devtools.command-button-frames.enabled", true); + await addTab(TEST_IFRAME_DOC_URI); + info( + "Test inspecting element in <iframe> with top document selected in the frame picker" + ); + await testContextMenuWithinFrame({ + selector: ["iframe", "#in-iframe"], + nodeFrontGetter: inspector => + getNodeFrontInFrames(["iframe", "#in-iframe"], inspector), + }); + + info( + "Test inspecting element in <iframe> with iframe document selected in the frame picker" + ); + await changeToolboxToFrame(IFRAME_URI, 2); + await testContextMenuWithinFrame({ + selector: ["iframe", "#in-iframe"], + nodeFrontGetter: inspector => getNodeFront("#in-iframe", inspector), + }); + await changeToolboxToFrame(TEST_IFRAME_DOC_URI, 2); + + await navigateTo(TEST_FRAME_DOC_URI); + + info( + "Test inspecting element in same origin <frame> with top document selected in the frame picker" + ); + await testContextMenuWithinFrame({ + selector: ["frame.same-origin", "#in-same-origin-frame"], + nodeFrontGetter: inspector => + getNodeFrontInFrames( + ["frame.same-origin", "#in-same-origin-frame"], + inspector + ), + }); + + info( + "Test inspecting element in remote <frame> with top document selected in the frame picker" + ); + await testContextMenuWithinFrame({ + selector: ["frame.remote", "#in-remote-frame"], + nodeFrontGetter: inspector => + getNodeFrontInFrames(["frame.remote", "#in-remote-frame"], inspector), + }); + + info( + "Test inspecting element in <frame> with frame document selected in the frame picker" + ); + await changeToolboxToFrame(SAME_ORIGIN_FRAME_URI, 3); + await testContextMenuWithinFrame({ + selector: ["frame.same-origin", "#in-same-origin-frame"], + nodeFrontGetter: inspector => + getNodeFront("#in-same-origin-frame", inspector), + }); +}); + +/** + * Pick a given element on the page with the 'Inspect Element' context menu entry and check + * that the expected node is selected in the markup view. + * + * @param {Object} options + * @param {Array<String>} options.selector: The selector of the element in the frame we + * want to select + * @param {Function} options.nodeFrontGetter: A function that will be executed to retrieve + * the nodeFront that should be selected as a result of the 'Inspect Element' action. + */ +async function testContextMenuWithinFrame({ selector, nodeFrontGetter }) { + info( + `Opening inspector via 'Inspect Element' context menu on ${JSON.stringify( + selector + )}` + ); + await clickOnInspectMenuItem(selector); + + info("Checking inspector state."); + const inspector = getActiveInspector(); + const nodeFront = await nodeFrontGetter(inspector); + + is( + inspector.selection.nodeFront, + nodeFront, + "Right node is selected in the markup view" + ); +} + +/** + * Select a specific document in the toolbox frame picker + * + * @param {String} frameUrl: The frame URL to select + * @param {Number} expectedFramesCount: The number of frames that should be displayed in + * the frame picker + */ +async function changeToolboxToFrame(frameUrl, expectedFramesCount) { + const { toolbox } = getActiveInspector(); + + const btn = toolbox.doc.getElementById("command-button-frames"); + const panel = toolbox.doc.getElementById("command-button-frames-panel"); + btn.click(); + ok(panel, "popup panel has created."); + await waitUntil(() => panel.classList.contains("tooltip-visible")); + + info("Select the iframe in the frame list."); + const menuList = toolbox.doc.getElementById("toolbox-frame-menu"); + const frames = Array.from(menuList.querySelectorAll(".command")); + is(frames.length, expectedFramesCount, "Two frames shown in the switcher"); + + const innerFrameButton = frames.find( + frame => frame.querySelector(".label").textContent === frameUrl + ); + ok(innerFrameButton, `Found frame button for inner frame "${frameUrl}"`); + + const newRoot = toolbox.getPanel("inspector").once("new-root"); + info(`Switch toolbox to inner frame "${frameUrl}"`); + innerFrameButton.click(); + await newRoot; +} diff --git a/devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu_nested.js b/devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu_nested.js new file mode 100644 index 0000000000..d172d03e10 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_inspect_node_contextmenu_nested.js @@ -0,0 +1,152 @@ +/* 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/. */ + +"use strict"; + +// Test Inspect Element feature with nested iframes of different origins. +// root (example.net) +// remote_frame1 (example.com) +// nested_same_process_frame (example.com, same as parent) +// same_process_frame (example.net, same as root) +// remote_frame2 (example.org) +// nested_remote_frame (example.com) + +// Build the remote_frame1 hierarchy +const NESTED_SAME_PROCESS_FRAME_URI = + "https://example.com/document-builder.sjs?html=" + + encodeURI( + `<div id="in-nested_same_process_frame">in-nested_same_process_frame` + ); +const REMOTE_FRAME1_HTML = `<iframe id="nested_same_process_frame" src="${NESTED_SAME_PROCESS_FRAME_URI}"></iframe>`; +const REMOTE_FRAME1_URI = + "https://example.com/document-builder.sjs?html=" + + encodeURI(REMOTE_FRAME1_HTML); + +// Build the same_process_frame hierarchy +const SAME_PROCESS_FRAME_URI = + "https://example.net/document-builder.sjs?html=" + + encodeURI(`<div id="in-same_process_frame">in-same_process_frame`); + +// Build the remote_frame2 hierarchy +const NESTED_REMOTE_FRAME_URI = + "https://example.com/document-builder.sjs?html=" + + encodeURI(`<div id="in-nested_remote_frame">in-nested_remote_frame`); +const REMOTE_FRAME2_HTML = `<iframe id="nested_remote_frame" src="${NESTED_REMOTE_FRAME_URI}"></iframe>`; +const REMOTE_FRAME2_URI = + "https://example.org/document-builder.sjs?html=" + + encodeURI(REMOTE_FRAME2_HTML); + +// Assemble all frames in a single test page. +const HTML = ` + <iframe id="remote_frame1" src="${REMOTE_FRAME1_URI}"></iframe> + <iframe id="same_process_frame" src="${SAME_PROCESS_FRAME_URI}"></iframe> + <iframe id="remote_frame2" src="${REMOTE_FRAME2_URI}"></iframe> +`; +const TEST_URI = + "https://example.net/document-builder.sjs?html=" + encodeURI(HTML); + +add_task(async function () { + const tab = await addTab(TEST_URI); + + info("Retrieve the browsing context for nested_same_process_frame"); + const nestedSameProcessFrameBC = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + function () { + const remote_frame1 = content.document.getElementById("remote_frame1"); + return SpecialPowers.spawn(remote_frame1, [], function () { + return content.document.getElementById( + "nested_same_process_frame" + ).browsingContext; + }); + } + ); + await inspectElementInBrowsingContext( + nestedSameProcessFrameBC, + "#in-nested_same_process_frame" + ); + checkSelectedNode("in-nested_same_process_frame"); + + info("Retrieve the browsing context for same_process_frame"); + const sameProcessFrameBC = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + function () { + return content.document.getElementById("same_process_frame") + .browsingContext; + } + ); + await inspectElementInBrowsingContext( + sameProcessFrameBC, + "#in-same_process_frame" + ); + checkSelectedNode("in-same_process_frame"); + + info("Retrieve the browsing context for nested_remote_frame"); + const nestedRemoteFrameBC = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + function () { + const remote_frame2 = content.document.getElementById("remote_frame2"); + return SpecialPowers.spawn(remote_frame2, [], function () { + return content.document.getElementById( + "nested_remote_frame" + ).browsingContext; + }); + } + ); + await inspectElementInBrowsingContext( + nestedRemoteFrameBC, + "#in-nested_remote_frame" + ); + checkSelectedNode("in-nested_remote_frame"); +}); + +/** + * Check the id of currently selected node front in the inspector. + */ +function checkSelectedNode(id) { + const inspector = getActiveInspector(); + is( + inspector.selection.nodeFront.id, + id, + "The correct node is selected in the markup view" + ); +} + +/** + * Use inspect element on the element matching the provided selector in a given + * browsing context. + * + * Note: adapted from head.js `clickOnInspectMenuItem` in order to work with a + * browsingContext instead of a test actor. + */ +async function inspectElementInBrowsingContext(browsingContext, selector) { + info( + `Show the context menu for selector "${selector}" in browsing context ${browsingContext.id}` + ); + const contentAreaContextMenu = document.querySelector( + "#contentAreaContextMenu" + ); + const contextOpened = once(contentAreaContextMenu, "popupshown"); + + const options = { type: "contextmenu", button: 2 }; + await BrowserTestUtils.synthesizeMouse( + selector, + 0, + 0, + options, + browsingContext + ); + + await contextOpened; + + info("Triggering the inspect action"); + await gContextMenu.inspectNode(); + + info("Hiding the menu"); + const contextClosed = once(contentAreaContextMenu, "popuphidden"); + contentAreaContextMenu.hidePopup(); + await contextClosed; +} diff --git a/devtools/client/inspector/test/browser_inspector_inspect_parent_process_page.js b/devtools/client/inspector/test/browser_inspector_inspect_parent_process_page.js new file mode 100644 index 0000000000..79e1907152 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_inspect_parent_process_page.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the "inspect element" context menu item works for parent process +// chrome pages. +add_task(async function () { + const tab = await addTab("about:debugging"); + + const browser = tab.linkedBrowser; + const document = browser.contentDocument; + + info("Wait until Connect page is displayed"); + await waitUntil(() => document.querySelector(".qa-connect-page")); + const inspector = await clickOnInspectMenuItem(".qa-network-form-input"); + + const selectedNode = inspector.selection.nodeFront; + ok(selectedNode, "A node is selected in the inspector"); + is( + selectedNode.tagName.toLowerCase(), + "input", + "The selected node has the correct tagName" + ); + ok( + selectedNode.className.includes("qa-network-form-input"), + "The selected node has the expected className" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_invalidate.js b/devtools/client/inspector/test/browser_inspector_invalidate.js new file mode 100644 index 0000000000..8d48d2bfc1 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_invalidate.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that highlighter handles geometry changes correctly. + +const TEST_URI = + "data:text/html;charset=utf-8," + + "browser_inspector_invalidate.js\n" + + '<div style="width: 100px; height: 100px; background:yellow;"></div>'; + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + const divFront = await getNodeFront("div", inspector); + + info("Waiting for highlighter to activate"); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + divFront + ); + + let rect = await highlighterTestFront.getSimpleBorderRect(); + is(rect.width, 100, "The highlighter has the right width."); + + info( + "Changing the test element's size and waiting for the highlighter " + + "to update" + ); + await highlighterTestFront.changeHighlightedNodeWaitForUpdate( + "style", + "width: 200px; height: 100px; background:yellow;" + ); + + rect = await highlighterTestFront.getSimpleBorderRect(); + is(rect.width, 200, "The highlighter has the right width after update"); + + info("Waiting for highlighter to hide"); + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js new file mode 100644 index 0000000000..6e64579269 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test copy outer HTML from the keyboard/copy event + +const TEST_URL = URL_ROOT + "doc_inspector_outerhtml.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const root = inspector.markup._elt; + + info("Test copy outerHTML for COMMENT node"); + const comment = getElementByType(inspector, Node.COMMENT_NODE); + await setSelectionNodeFront(comment, inspector); + await checkClipboard("<!-- Comment -->", root); + + info("Test copy outerHTML for DOCTYPE node"); + const doctype = getElementByType(inspector, Node.DOCUMENT_TYPE_NODE); + await setSelectionNodeFront(doctype, inspector); + await checkClipboard("<!DOCTYPE html>", root); + + info("Test copy outerHTML for ELEMENT node"); + await selectAndHighlightNode("div", inspector); + await checkClipboard("<div><p>Test copy OuterHTML</p></div>", root); +}); + +async function setSelectionNodeFront(node, inspector) { + const updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(node); + await updated; +} + +async function checkClipboard(expectedText, node) { + try { + await waitForClipboardPromise(() => fireCopyEvent(node), expectedText); + ok(true, "Clipboard successfully filled with : " + expectedText); + } catch (e) { + ok( + false, + "Clipboard could not be filled with the expected text : " + expectedText + ); + } +} + +function getElementByType(inspector, type) { + for (const [node] of inspector.markup._containers) { + if (node.nodeType === type) { + return node; + } + } + return null; +} diff --git a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js new file mode 100644 index 0000000000..d4e1ee5631 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests that the keybindings for highlighting different elements work as +// intended. + +const TEST_URI = + "data:text/html;charset=utf-8," + + "<html><head><title>Test for the highlighter keybindings</title></head>" + + "<body><p><strong>Greetings, earthlings!</strong>" + + " I come in peace.</p></body></html>"; + +const TEST_DATA = [ + { key: "KEY_ArrowLeft", selectedNode: "p" }, + { key: "KEY_ArrowLeft", selectedNode: "body" }, + { key: "KEY_ArrowLeft", selectedNode: "html" }, + { key: "KEY_ArrowRight", selectedNode: "body" }, + { key: "KEY_ArrowRight", selectedNode: "p" }, + { key: "KEY_ArrowRight", selectedNode: "strong" }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + info("Selecting the deepest element to start with"); + await selectNode("strong", inspector); + + const nodeFront = await getNodeFront("strong", inspector); + is( + inspector.selection.nodeFront, + nodeFront, + "<strong> should be selected initially" + ); + + info("Focusing the currently active breadcrumb button"); + const bc = inspector.breadcrumbs; + bc.nodeHierarchy[bc.currentIndex].button.focus(); + + for (const { key, selectedNode } of TEST_DATA) { + info("Pressing " + key + " to select " + selectedNode); + + const updated = inspector.once("inspector-updated"); + EventUtils.synthesizeKey(key); + await updated; + + const selectedNodeFront = await getNodeFront(selectedNode, inspector); + is( + inspector.selection.nodeFront, + selectedNodeFront, + selectedNode + " is selected." + ); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js new file mode 100644 index 0000000000..9ac925db6e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js @@ -0,0 +1,388 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that context menu items are enabled / disabled correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; + +const PASTE_MENU_ITEMS = [ + "node-menu-pasteinnerhtml", + "node-menu-pasteouterhtml", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-pastefirstchild", + "node-menu-pastelastchild", +]; + +const ACTIVE_ON_DOCTYPE_ITEMS = [ + "node-menu-showdomproperties", + "node-menu-useinconsole", +]; + +const ACTIVE_ON_SHADOW_ROOT_ITEMS = [ + "node-menu-pasteinnerhtml", + "node-menu-copyinner", + "node-menu-edithtml", +].concat(ACTIVE_ON_DOCTYPE_ITEMS); + +const ALL_MENU_ITEMS = [ + "node-menu-edithtml", + "node-menu-copyinner", + "node-menu-copyouter", + "node-menu-copyuniqueselector", + "node-menu-copycsspath", + "node-menu-copyxpath", + "node-menu-copyimagedatauri", + "node-menu-delete", + "node-menu-pseudo-hover", + "node-menu-pseudo-active", + "node-menu-pseudo-focus", + "node-menu-pseudo-focus-within", + "node-menu-scrollnodeintoview", + "node-menu-screenshotnode", + "node-menu-add-attribute", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", +].concat(PASTE_MENU_ITEMS, ACTIVE_ON_DOCTYPE_ITEMS); + +const INACTIVE_ON_DOCTYPE_ITEMS = ALL_MENU_ITEMS.filter( + item => !ACTIVE_ON_DOCTYPE_ITEMS.includes(item) +); + +const INACTIVE_ON_DOCUMENT_ITEMS = INACTIVE_ON_DOCTYPE_ITEMS; + +const INACTIVE_ON_SHADOW_ROOT_ITEMS = ALL_MENU_ITEMS.filter( + item => !ACTIVE_ON_SHADOW_ROOT_ITEMS.includes(item) +); + +/** + * Test cases, each item of this array may define the following properties: + * desc: string that will be logged + * selector: selector of the node to be selected + * disabled: items that should have disabled state + * clipboardData: clipboard content + * clipboardDataType: clipboard content type + * attributeTrigger: attribute that will be used as context menu trigger + * shadowRoot: if true, selects the shadow root from the node, rather than + * the node itself. + */ +const TEST_CASES = [ + { + desc: "doctype node with empty clipboard", + selector: null, + disabled: INACTIVE_ON_DOCTYPE_ITEMS, + }, + { + desc: "doctype node with html on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "text", + selector: null, + disabled: INACTIVE_ON_DOCTYPE_ITEMS, + }, + { + desc: "element node HTML on the clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "text", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ], + selector: "#sensitivity", + }, + { + desc: "<html> element", + clipboardData: "<p>some text</p>", + clipboardDataType: "text", + selector: "html", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-pastefirstchild", + "node-menu-pastelastchild", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + "node-menu-delete", + ], + }, + { + desc: "<body> with HTML on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "text", + selector: "body", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ], + }, + { + desc: "<img> with HTML on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "text", + selector: "img", + disabled: [ + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ], + }, + { + desc: "<head> with HTML on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "text", + selector: "head", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-screenshotnode", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ], + }, + { + desc: "<head> with no html on clipboard", + selector: "head", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-screenshotnode", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ]), + }, + { + desc: "<element> with text on clipboard", + clipboardData: "some text", + clipboardDataType: "text", + selector: "#paste-area", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ], + }, + { + desc: "<element> with base64 encoded image data uri on clipboard", + clipboardData: + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" + + "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==", + clipboardDataType: "image", + selector: "#paste-area", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ]), + }, + { + desc: "<element> with empty string on clipboard", + clipboardData: "", + clipboardDataType: "text", + selector: "#paste-area", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ]), + }, + { + desc: "<element> with whitespace only on clipboard", + clipboardData: " \n\n\t\n\n \n", + clipboardDataType: "text", + selector: "#paste-area", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ]), + }, + { + desc: "<element> that isn't visible on the page, empty clipboard", + selector: "#hiddenElement", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-screenshotnode", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ]), + }, + { + desc: "<element> nested in another hidden element, empty clipboard", + selector: "#nestedHiddenElement", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-screenshotnode", + "node-menu-copy-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute", + ]), + }, + { + desc: "<element> with context menu triggered on attribute, empty clipboard", + selector: "#attributes", + disabled: PASTE_MENU_ITEMS.concat(["node-menu-copyimagedatauri"]), + attributeTrigger: "data-edit", + }, + { + desc: "Shadow Root", + clipboardData: "<p>some text</p>", + clipboardDataType: "text", + disabled: INACTIVE_ON_SHADOW_ROOT_ITEMS, + selector: "#host", + shadowRoot: true, + }, + { + desc: "Document node in iFrame", + disabled: INACTIVE_ON_DOCUMENT_ITEMS, + selector: "iframe", + documentNode: true, + }, +]; + +var clipboard = require("resource://devtools/shared/platform/clipboard.js"); +registerCleanupFunction(() => { + clipboard.copyString(""); + clipboard = null; +}); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + for (const test of TEST_CASES) { + const { + desc, + disabled, + selector, + attributeTrigger, + documentNode = false, + shadowRoot = false, + } = test; + + info(`Test ${desc}`); + setupClipboard(test.clipboardData, test.clipboardDataType); + + const front = await getNodeFrontForSelector( + selector, + inspector, + documentNode, + shadowRoot + ); + + info("Selecting the specified node."); + await selectNode(front, inspector); + + info("Simulating context menu click on the selected node container."); + const nodeFrontContainer = getContainerForNodeFront(front, inspector); + const contextMenuTrigger = attributeTrigger + ? nodeFrontContainer.tagLine.querySelector( + `[data-attr="${attributeTrigger}"]` + ) + : nodeFrontContainer.tagLine; + + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: contextMenuTrigger, + }); + + for (const id of ALL_MENU_ITEMS) { + const menuItem = allMenuItems.find(item => item.id === id); + const shouldBeDisabled = disabled.includes(id); + const shouldBeDisabledText = shouldBeDisabled ? "disabled" : "enabled"; + is( + menuItem.disabled, + shouldBeDisabled, + `#${id} should be ${shouldBeDisabledText} for test case ${desc}` + ); + } + } +}); + +/** + * A helper that fetches a front for a node that matches the given selector or + * doctype node if the selector is falsy. + */ +async function getNodeFrontForSelector( + selector, + inspector, + documentNode, + shadowRoot +) { + if (selector) { + info("Retrieving front for selector " + selector); + const node = await getNodeFront(selector, inspector); + if (shadowRoot) { + return getShadowRoot(node, inspector); + } + if (documentNode) { + return getFrameDocument(node, inspector); + } + return node; + } + + info("Retrieving front for doctype node"); + const { nodes } = await inspector.walker.children(inspector.walker.rootNode); + return nodes[0]; +} + +/** + * A helper that populates the clipboard with data of given type. Clears the + * clipboard if data is falsy. + */ +function setupClipboard(data, type) { + if (!data) { + info("Clearing the clipboard."); + clipboard.copyString(""); + } else if (type === "text") { + info("Populating clipboard with text."); + clipboard.copyString(data); + } else if (type === "image") { + info("Populating clipboard with image content"); + copyImageToClipboard(data); + } +} + +/** + * The code below is a simplified version of the sdk/clipboard helper set() method. + */ +function copyImageToClipboard(data) { + const imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools); + + // Image data is stored as base64 in the test. + const image = atob(data); + + const imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance( + Ci.nsISupportsInterfacePointer + ); + imgPtr.data = imageTools.decodeImageFromBuffer( + image, + image.length, + "image/png" + ); + + const xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + xferable.init(null); + xferable.addDataFlavor("image/png"); + xferable.setTransferData("image/png", imgPtr); + + Services.clipboard.setData( + xferable, + null, + Services.clipboard.kGlobalClipboard + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js new file mode 100644 index 0000000000..7a69230aef --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that HTML can be pasted in SVG elements. + +const TEST_URL = URL_ROOT + "doc_inspector_svg.svg"; +const PASTE_AS_FIRST_CHILD = + '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="5"/>'; +const PASTE_AS_LAST_CHILD = + '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="15"/>'; + +add_task(async function () { + const clipboard = require("resource://devtools/shared/platform/clipboard.js"); + + const { inspector } = await openInspectorForURL(TEST_URL); + + const refSelector = "svg"; + const oldHTML = await getContentPageElementProperty(refSelector, "innerHTML"); + await selectNode(refSelector, inspector); + const markupTagLine = getContainerForSelector(refSelector, inspector).tagLine; + + await pasteContent("node-menu-pastefirstchild", PASTE_AS_FIRST_CHILD); + await pasteContent("node-menu-pastelastchild", PASTE_AS_LAST_CHILD); + + const html = await getContentPageElementProperty(refSelector, "innerHTML"); + const expectedHtml = PASTE_AS_FIRST_CHILD + oldHTML + PASTE_AS_LAST_CHILD; + is(html, expectedHtml, "The innerHTML of the SVG node is correct"); + + // Helpers + async function pasteContent(menuId, clipboardData) { + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: markupTagLine, + }); + info(`Testing ${menuId} for ${clipboardData}`); + + await SimpleTest.promiseClipboardChange(clipboardData, () => { + clipboard.copyString(clipboardData); + }); + + const onMutation = inspector.once("markupmutation"); + allMenuItems.find(item => item.id === menuId).click(); + info("Waiting for mutation to occur"); + await onMutation; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js new file mode 100644 index 0000000000..ae59a1c532 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that different paste items work in the context menu + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; +const PASTE_ADJACENT_HTML_DATA = [ + { + desc: "As First Child", + clipboardData: "2", + menuId: "node-menu-pastefirstchild", + }, + { + desc: "As Last Child", + clipboardData: "4", + menuId: "node-menu-pastelastchild", + }, + { + desc: "Before", + clipboardData: "1", + menuId: "node-menu-pastebefore", + }, + { + desc: "After", + clipboardData: "<span>5</span>", + menuId: "node-menu-pasteafter", + }, +]; + +var clipboard = require("resource://devtools/shared/platform/clipboard.js"); +registerCleanupFunction(() => { + clipboard = null; +}); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + await testPasteOuterHTMLMenu(); + await testPasteInnerHTMLMenu(); + await testPasteAdjacentHTMLMenu(); + + async function testPasteOuterHTMLMenu() { + info("Testing that 'Paste Outer HTML' menu item works."); + + await SimpleTest.promiseClipboardChange( + "this was pasted (outerHTML)", + () => { + clipboard.copyString("this was pasted (outerHTML)"); + } + ); + + const outerHTMLSelector = "#paste-area h1"; + + const nodeFront = await getNodeFront(outerHTMLSelector, inspector); + await selectNode(nodeFront, inspector); + + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(nodeFront, inspector).tagLine, + }); + + const onNodeReselected = inspector.markup.once("reselectedonremoved"); + allMenuItems.find(item => item.id === "node-menu-pasteouterhtml").click(); + + info("Waiting for inspector selection to update"); + await onNodeReselected; + + const outerHTML = await getContentPageElementProperty("body", "outerHTML"); + ok( + outerHTML.includes(clipboard.getText()), + "Clipboard content was pasted into the node's outer HTML." + ); + ok( + !(await hasMatchingElementInContentPage(outerHTMLSelector)), + "The original node was removed." + ); + } + + async function testPasteInnerHTMLMenu() { + info("Testing that 'Paste Inner HTML' menu item works."); + + await SimpleTest.promiseClipboardChange( + "this was pasted (innerHTML)", + () => { + clipboard.copyString("this was pasted (innerHTML)"); + } + ); + const innerHTMLSelector = "#paste-area .inner"; + const getInnerHTML = () => + getContentPageElementProperty(innerHTMLSelector, "innerHTML"); + const origInnerHTML = await getInnerHTML(); + + const nodeFront = await getNodeFront(innerHTMLSelector, inspector); + await selectNode(nodeFront, inspector); + + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(nodeFront, inspector).tagLine, + }); + + const onMutation = inspector.once("markupmutation"); + allMenuItems.find(item => item.id === "node-menu-pasteinnerhtml").click(); + info("Waiting for mutation to occur"); + await onMutation; + + Assert.strictEqual( + await getInnerHTML(), + clipboard.getText(), + "Clipboard content was pasted into the node's inner HTML." + ); + ok( + await hasMatchingElementInContentPage(innerHTMLSelector), + "The original node has been preserved." + ); + await undoChange(inspector); + Assert.strictEqual( + await getInnerHTML(), + origInnerHTML, + "Previous innerHTML has been restored after undo" + ); + } + + async function testPasteAdjacentHTMLMenu() { + const refSelector = "#paste-area .adjacent .ref"; + const adjacentNodeSelector = "#paste-area .adjacent"; + const nodeFront = await getNodeFront(refSelector, inspector); + await selectNode(nodeFront, inspector); + const markupTagLine = getContainerForNodeFront( + nodeFront, + inspector + ).tagLine; + + for (const { clipboardData, menuId } of PASTE_ADJACENT_HTML_DATA) { + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: markupTagLine, + }); + info(`Testing ${menuId} for ${clipboardData}`); + + await SimpleTest.promiseClipboardChange(clipboardData, () => { + clipboard.copyString(clipboardData); + }); + + const onMutation = inspector.once("markupmutation"); + allMenuItems.find(item => item.id === menuId).click(); + info("Waiting for mutation to occur"); + await onMutation; + } + + let html = await getContentPageElementProperty( + adjacentNodeSelector, + "innerHTML" + ); + Assert.strictEqual( + html.trim(), + '1<span class="ref">234</span><span>5</span>', + "The Paste as Last Child / as First Child / Before / After worked as " + + "expected" + ); + await undoChange(inspector); + + html = await getContentPageElementProperty( + adjacentNodeSelector, + "innerHTML" + ); + Assert.strictEqual( + html.trim(), + '1<span class="ref">234</span>', + "Undo works for paste adjacent HTML" + ); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js b/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js new file mode 100644 index 0000000000..ac6ae35ad1 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests "Use in Console" menu item + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; + +add_task(async function () { + // Disable eager evaluation to avoid intermittent failures due to pending + // requests to evaluateJSAsync. + await pushPref("devtools.webconsole.input.eagerEvaluation", false); + + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + info("Testing 'Use in Console' menu item."); + + await selectNode("#console-var", inspector); + const container = await getContainerForSelector("#console-var", inspector); + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: container.tagLine, + }); + const menuItem = allMenuItems.find(i => i.id === "node-menu-useinconsole"); + menuItem.click(); + + await inspector.once("console-var-ready"); + + const hud = toolbox.getPanel("webconsole").hud; + + const getConsoleResults = () => hud.ui.outputNode.querySelectorAll(".result"); + + is(hud.getInputValue(), "temp0", "first console variable is named temp0"); + hud.ui.wrapper.dispatchEvaluateExpression(); + + await waitUntil(() => getConsoleResults().length === 1); + let result = getConsoleResults()[0]; + ok( + result.textContent.includes('<p id="console-var">'), + "variable temp0 references correct node" + ); + + await selectNode("#console-var-multi", inspector); + menuItem.click(); + await inspector.once("console-var-ready"); + + is(hud.getInputValue(), "temp1", "second console variable is named temp1"); + hud.ui.wrapper.dispatchEvaluateExpression(); + + await waitUntil(() => getConsoleResults().length === 2); + result = getConsoleResults()[1]; + ok( + result.textContent.includes('<p id="console-var-multi">'), + "variable temp1 references correct node" + ); + + hud.ui.wrapper.dispatchClearHistory(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js new file mode 100644 index 0000000000..4e2122a24a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that attribute items work in the context menu + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + await selectNode("#attributes", inspector); + + await testAddAttribute(); + await testCopyAttributeValue(); + await testCopyLongAttributeValue(); + await testEditAttribute(); + await testRemoveAttribute(); + + async function testAddAttribute() { + info("Triggering 'Add Attribute' and waiting for mutation to occur"); + const addAttribute = getMenuItem("node-menu-add-attribute"); + addAttribute.click(); + + EventUtils.sendString('class="u-hidden"'); + const onMutation = inspector.once("markupmutation"); + EventUtils.synthesizeKey("KEY_Enter"); + await onMutation; + + const hasAttribute = await hasMatchingElementInContentPage( + "#attributes.u-hidden" + ); + ok(hasAttribute, "attribute was successfully added"); + } + + async function testCopyAttributeValue() { + info( + "Testing 'Copy Attribute Value' and waiting for clipboard promise to resolve" + ); + const copyAttributeValue = getMenuItem("node-menu-copy-attribute"); + + info( + "Triggering 'Copy Attribute Value' and waiting for clipboard to copy the value" + ); + inspector.markup.contextMenu.nodeMenuTriggerInfo = { + type: "attribute", + name: "data-edit", + value: "the", + }; + + await waitForClipboardPromise(() => copyAttributeValue.click(), "the"); + } + + async function testCopyLongAttributeValue() { + info("Testing 'Copy Attribute Value' copies very long attribute values"); + const copyAttributeValue = getMenuItem("node-menu-copy-attribute"); + const longAttribute = + "#01234567890123456789012345678901234567890123456789" + + "12345678901234567890123456789012345678901234567890123456789012345678901" + + "23456789012345678901234567890123456789012345678901234567890123456789012" + + "34567890123456789012345678901234567890123456789012345678901234567890123"; + + inspector.markup.contextMenu.nodeMenuTriggerInfo = { + type: "attribute", + name: "data-edit", + value: longAttribute, + }; + + await waitForClipboardPromise( + () => copyAttributeValue.click(), + longAttribute + ); + } + + async function testEditAttribute() { + info("Testing 'Edit Attribute' menu item"); + const editAttribute = getMenuItem("node-menu-edit-attribute"); + + info("Triggering 'Edit Attribute' and waiting for mutation to occur"); + inspector.markup.contextMenu.nodeMenuTriggerInfo = { + type: "attribute", + name: "data-edit", + }; + editAttribute.click(); + EventUtils.sendString("data-edit='edited'"); + const onMutation = inspector.once("markupmutation"); + EventUtils.synthesizeKey("KEY_Enter"); + await onMutation; + + const isAttributeChanged = await hasMatchingElementInContentPage( + "#attributes[data-edit='edited']" + ); + ok(isAttributeChanged, "attribute was successfully edited"); + } + + async function testRemoveAttribute() { + info("Testing 'Remove Attribute' menu item"); + const removeAttribute = getMenuItem("node-menu-remove-attribute"); + + info("Triggering 'Remove Attribute' and waiting for mutation to occur"); + inspector.markup.contextMenu.nodeMenuTriggerInfo = { + type: "attribute", + name: "data-remove", + }; + const onMutation = inspector.once("markupmutation"); + removeAttribute.click(); + await onMutation; + + const hasAttribute = await hasMatchingElementInContentPage( + "#attributes[data-remove]" + ); + ok(!hasAttribute, "attribute was successfully removed"); + } + + function getMenuItem(id) { + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForSelector("#attributes", inspector).tagLine, + }); + const menuItem = allMenuItems.find(i => i.id === id); + ok(menuItem, "Menu item '" + id + "' found"); + // Close the menu so synthesizing future keys won't select menu items. + EventUtils.synthesizeKey("KEY_Escape"); + return menuItem; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-06-other.js b/devtools/client/inspector/test/browser_inspector_menu-06-other.js new file mode 100644 index 0000000000..34572cb934 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-06-other.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests for menuitem functionality that doesn't fit into any specific category +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + await testShowDOMProperties(); + await testDuplicateNode(); + await testDeleteNode(); + await testDeleteTextNode(); + await testDeleteRootNode(); + await testScrollIntoView(); + + async function testDuplicateNode() { + info("Testing 'Duplicate Node' menu item for normal elements."); + + await selectNode(".duplicate", inspector); + is( + await getNumberOfMatchingElementsInContentPage(".duplicate"), + 1, + "There should initially be 1 .duplicate node" + ); + + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const menuItem = allMenuItems.find( + item => item.id === "node-menu-duplicatenode" + ); + ok(menuItem, "'Duplicate node' menu item should exist"); + + info("Triggering 'Duplicate Node' and waiting for inspector to update"); + const updated = inspector.once("markupmutation"); + menuItem.click(); + await updated; + + is( + await getNumberOfMatchingElementsInContentPage(".duplicate"), + 2, + "The duplicated node should be in the markup." + ); + + const container = await getContainerForSelector( + ".duplicate + .duplicate", + inspector + ); + ok(container, "A MarkupContainer should be created for the new node"); + } + + async function testDeleteNode() { + info("Testing 'Delete Node' menu item for normal elements."); + await selectNode("#delete", inspector); + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const deleteNode = allMenuItems.find( + item => item.id === "node-menu-delete" + ); + ok(deleteNode, "the popup menu has a delete menu item"); + const updated = inspector.once("inspector-updated"); + + info("Triggering 'Delete Node' and waiting for inspector to update"); + deleteNode.click(); + await updated; + + ok(!(await hasMatchingElementInContentPage("#delete")), "Node deleted"); + } + + async function testDeleteTextNode() { + info("Testing 'Delete Node' menu item for text elements."); + const { walker } = inspector; + const divBefore = await walker.querySelector( + walker.rootNode, + "#nestedHiddenElement" + ); + const { nodes } = await walker.children(divBefore); + await selectNode(nodes[0], inspector, "test-highlight"); + + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const deleteNode = allMenuItems.find( + item => item.id === "node-menu-delete" + ); + ok(deleteNode, "the popup menu has a delete menu item"); + ok(!deleteNode.disabled, "the delete menu item is not disabled"); + const updated = inspector.once("inspector-updated"); + + info("Triggering 'Delete Node' and waiting for inspector to update"); + deleteNode.click(); + await updated; + + const divAfter = await walker.querySelector( + walker.rootNode, + "#nestedHiddenElement" + ); + const nodesAfter = (await walker.children(divAfter)).nodes; + ok(!nodesAfter.length, "the node still had children"); + } + + async function testDeleteRootNode() { + info("Testing 'Delete Node' menu item does not delete root node."); + await selectNode("html", inspector); + + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const deleteNode = allMenuItems.find( + item => item.id === "node-menu-delete" + ); + deleteNode.click(); + + await new Promise(resolve => { + executeSoon(resolve); + }); + + const hasDocumentElement = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => !!content.document.documentElement + ); + ok(hasDocumentElement, "Document element still alive."); + } + + async function testShowDOMProperties() { + info("Testing 'Show DOM Properties' menu item."); + const allMenuItems = openContextMenuAndGetAllItems(inspector); + const showDOMPropertiesNode = allMenuItems.find( + item => item.id === "node-menu-showdomproperties" + ); + ok(showDOMPropertiesNode, "the popup menu has a show dom properties item"); + + const consoleOpened = toolbox.once("webconsole-ready"); + + info("Triggering 'Show DOM Properties' and waiting for inspector open"); + showDOMPropertiesNode.click(); + await consoleOpened; + + const webconsoleUI = toolbox.getPanel("webconsole").hud.ui; + + await poll( + () => { + const messages = [ + ...webconsoleUI.outputNode.querySelectorAll(".message"), + ]; + const nodeMessage = messages.find(m => m.textContent.includes("body")); + // wait for the object to be expanded + return ( + nodeMessage && + nodeMessage.querySelectorAll(".object-inspector .node").length > 10 + ); + }, + "Waiting for the element node to be expanded", + 10, + 1000 + ); + + info("Close split console"); + await toolbox.toggleSplitConsole(); + } + + function testScrollIntoView() { + // Follow up bug to add this test - https://bugzilla.mozilla.org/show_bug.cgi?id=1154107 + todo(false, "Verify that node is scrolled into the viewport."); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js b/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js new file mode 100644 index 0000000000..7e43c68e17 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js @@ -0,0 +1,69 @@ +/* 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/. */ +"use strict"; + +// Test that inspector works when navigating to error pages. + +const TEST_URL_1 = + 'data:text/html,<html><body id="test-doc-1">page</body></html>'; +const TEST_URL_2 = "http://127.0.0.1:36325/"; +const TEST_URL_3 = "https://www.wronguri.wronguri/"; +const TEST_URL_4 = "data:text/html,<html><body>test-doc-4</body></html>"; + +add_task(async function () { + // Open the inspector on a valid URL + const { inspector } = await openInspectorForURL(TEST_URL_1); + + info("Navigate to closed port"); + await navigateTo(TEST_URL_2, { isErrorPage: true }); + + const documentURI = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return content.document.documentURI; + } + ); + ok(documentURI.startsWith("about:neterror"), "content is correct."); + + const hasPage = await getNodeFront("#test-doc-1", inspector); + ok( + !hasPage, + "Inspector actor is no longer able to reach previous page DOM node" + ); + + const hasNetErrorNode = await getNodeFront("#errorShortDesc", inspector); + ok(hasNetErrorNode, "Inspector actor is able to reach error page DOM node"); + + const bundle = Services.strings.createBundle( + "chrome://global/locale/appstrings.properties" + ); + let domain = TEST_URL_2.match(/^http:\/\/(.*)\/$/)[1]; + let errorMsg = bundle.formatStringFromName("connectionFailure", [domain]); + is( + await getDisplayedNodeTextContent("#errorShortDesc", inspector), + errorMsg, + "Inpector really inspects the error page" + ); + + info("Navigate to unknown domain"); + await navigateTo(TEST_URL_3, { isErrorPage: true }); + + domain = TEST_URL_3.match(/^https:\/\/(.*)\/$/)[1]; + errorMsg = bundle.formatStringFromName("dnsNotFound2", [domain]); + is( + await getDisplayedNodeTextContent("#errorShortDesc", inspector), + errorMsg, + "Inspector really inspects the new error page" + ); + + info("Navigate to a valid url"); + await navigateTo(TEST_URL_4); + + is( + await getDisplayedNodeTextContent("body", inspector), + "test-doc-4", + "Inspector really inspects the valid url" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_navigation.js b/devtools/client/inspector/test/browser_inspector_navigation.js new file mode 100644 index 0000000000..68f1edc4ca --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_navigation.js @@ -0,0 +1,88 @@ +/* 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/. */ +"use strict"; + +// Test that inspector updates when page is navigated. + +const TEST_URL_FILE = + "browser/devtools/client/inspector/test/doc_inspector_breadcrumbs.html"; + +const TEST_URL_1 = "https://test1.example.org/" + TEST_URL_FILE; +const TEST_URL_2 = "https://test2.example.org/" + TEST_URL_FILE; + +// Bug 1340592: "srcset" attribute causes bfcache events (pageshow/pagehide) +// with buggy "persisted" values. +const TEST_URL_3 = + "data:text/html;charset=utf-8," + + encodeURIComponent('<img src="foo.png" srcset="foo.png 1.5x" />'); +const TEST_URL_4 = + "data:text/html;charset=utf-8," + encodeURIComponent("<h1>bar</h1>"); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL_1); + + await selectNode("#i1", inspector); + + info("Navigating to a different page."); + await navigateTo(TEST_URL_2); + + ok(true, "New page loaded"); + await selectNode("#i1", inspector); + + const markuploaded = inspector.once("markuploaded"); + const onUpdated = inspector.once("inspector-updated"); + + info("Going back in history"); + gBrowser.goBack(); + + info("Waiting for markup view to load after going back in history."); + await markuploaded; + + info("Check that the inspector updates"); + await onUpdated; + + ok(true, "Old page loaded"); + const url = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.location.href + ); + is(url, TEST_URL_1, "URL is correct."); + + await selectNode("#i1", inspector); +}); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL_3); + + await selectNode("img", inspector); + + info("Navigating to a different page."); + await navigateTo(TEST_URL_4); + + ok(true, "New page loaded"); + await selectNode("#h1", inspector); + + const markuploaded = inspector.once("markuploaded"); + const onUpdated = inspector.once("inspector-updated"); + + info("Going back in history"); + gBrowser.goBack(); + + info("Waiting for markup view to load after going back in history."); + await markuploaded; + + info("Check that the inspector updates"); + await onUpdated; + + ok(true, "Old page loaded"); + const url = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.location.href + ); + is(url, TEST_URL_3, "URL is correct."); + + await selectNode("img", inspector); +}); diff --git a/devtools/client/inspector/test/browser_inspector_open_on_neterror.js b/devtools/client/inspector/test/browser_inspector_open_on_neterror.js new file mode 100644 index 0000000000..96354d3fab --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_open_on_neterror.js @@ -0,0 +1,41 @@ +/* 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/. */ +"use strict"; + +// Test that inspector works correctly when opened against a net error page + +const TEST_URL_1 = "http://127.0.0.1:36325/"; +const TEST_URL_2 = "data:text/html,<html><body>test-doc-2</body></html>"; + +add_task(async function () { + // We cannot directly use addTab here as waiting for error pages requires + // a specific code path. + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TEST_URL_1); + await BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + + const { inspector } = await openInspector(); + ok(true, "Inspector loaded on the already opened net error"); + + const documentURI = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => content.document.documentURI + ); + ok( + documentURI.startsWith("about:neterror"), + "content is really a net error page." + ); + + const netErrorNode = await getNodeFront(".container", inspector); + ok(netErrorNode, "The inspector can get a node front from the neterror page"); + + info("Navigate to a valid url"); + await navigateTo(TEST_URL_2); + + is( + await getDisplayedNodeTextContent("body", inspector), + "test-doc-2", + "Inspector really inspects the valid url" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js new file mode 100644 index 0000000000..e5335c3e93 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the inspector panel has a 3 pane toggle button, and that +// this button is visible both in BOTTOM and SIDE hosts. + +add_task(async function () { + info("Switch to 2 pane inspector to test the 3 pane toggle button behavior"); + await pushPref("devtools.inspector.three-pane-enabled", false); + + info("Open the inspector in a bottom toolbox host"); + const { inspector, toolbox } = await openInspectorForURL( + "about:blank", + "bottom" + ); + + const button = inspector.panelDoc.querySelector(".sidebar-toggle"); + ok(button, "The toggle button exists in the DOM"); + ok(button.getAttribute("title"), "The title tooltip has initial state"); + ok( + button.classList.contains("pane-collapsed"), + "The button is in collapsed state" + ); + ok(!!button.getClientRects().length, "The button is visible"); + + info("Switch the host to the right"); + await toolbox.switchHost("right"); + + ok(!!button.getClientRects().length, "The button is still visible"); + ok( + button.classList.contains("pane-collapsed"), + "The button is still in collapsed state" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js new file mode 100644 index 0000000000..132f0feb7f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the 3 pane toggle button can toggle on and off the inspector's 3 pane mode, +// and the 3 panes rendered are all of equal widths in the BOTTOM host. + +add_task(async function () { + info("Switch to 2 pane inspector to test the 3 pane toggle button behavior"); + await pushPref("devtools.inspector.three-pane-enabled", false); + + const { inspector } = await openInspectorForURL("about:blank"); + const { panelDoc: doc } = inspector; + const button = doc.querySelector(".sidebar-toggle"); + const ruleViewSidebar = + inspector.sidebarSplitBoxRef.current.startPanelContainer; + const toolboxWidth = doc.getElementById("inspector-splitter-box").clientWidth; + + ok( + button.classList.contains("pane-collapsed"), + "The button is in collapsed state" + ); + + info("Click on the toggle button to toggle ON 3 pane inspector"); + let onRuleViewAdded = inspector.once("ruleview-added"); + EventUtils.synthesizeMouseAtCenter( + button, + {}, + inspector.panelDoc.defaultView + ); + await onRuleViewAdded; + + info("Checking the state of the 3 pane inspector"); + let sidebarWidth = inspector.splitBox.state.width; + const sidebarSplitBoxWidth = inspector.sidebarSplitBoxRef.current.state.width; + ok( + !button.classList.contains("pane-collapsed"), + "The button is in expanded state" + ); + ok(doc.getElementById("ruleview-panel"), "The rule view panel exist"); + is( + inspector.sidebar.getCurrentTabID(), + "layoutview", + "Layout view is shown in the sidebar" + ); + is( + ruleViewSidebar.style.display, + "block", + "The split rule view sidebar is displayed" + ); + is(sidebarWidth, (toolboxWidth * 2) / 3, "Got correct main split box width"); + is( + sidebarSplitBoxWidth, + toolboxWidth / 3, + "Got correct sidebar split box width" + ); + + info("Click on the toggle button to toggle OFF the 3 pane inspector"); + onRuleViewAdded = inspector.once("ruleview-added"); + EventUtils.synthesizeMouseAtCenter( + button, + {}, + inspector.panelDoc.defaultView + ); + await onRuleViewAdded; + + info("Checking the state of the 2 pane inspector"); + sidebarWidth = inspector.splitBox.state.width; + ok( + button.classList.contains("pane-collapsed"), + "The button is in collapsed state" + ); + is( + inspector.sidebar.getCurrentTabID(), + "ruleview", + "Rule view is shown in the sidebar" + ); + is( + ruleViewSidebar.style.display, + "none", + "The split rule view sidebar is hidden" + ); + is(sidebarWidth, sidebarSplitBoxWidth, "Got correct sidebar width"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js new file mode 100644 index 0000000000..1522781832 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the 3 pane inspector toggle can render the middle and right panels of equal +// sizes when the original sidebar can be doubled in width and be smaller than half the +// toolbox's width in the BOTTOM host. + +const SIDEBAR_WIDTH = 200; + +add_task(async function () { + info("Switch to 2 pane inspector to test the 3 pane toggle button behavior"); + await pushPref("devtools.inspector.three-pane-enabled", false); + + const { inspector } = await openInspectorForURL("about:blank"); + const { panelDoc: doc } = inspector; + const button = doc.querySelector(".sidebar-toggle"); + const toolboxWidth = doc.getElementById("inspector-splitter-box").clientWidth; + + if (toolboxWidth < 600) { + ok(true, "Can't run the full test because the toolbox width is too small."); + } else { + info("Set the sidebar width to 200px"); + inspector.splitBox.setState({ width: SIDEBAR_WIDTH }); + + info("Click on the toggle button to toggle ON 3 pane inspector"); + let onRuleViewAdded = inspector.once("ruleview-added"); + EventUtils.synthesizeMouseAtCenter( + button, + {}, + inspector.panelDoc.defaultView + ); + await onRuleViewAdded; + + info("Checking the sizes of the 3 pane inspector"); + let sidebarWidth = inspector.splitBox.state.width; + const sidebarSplitBoxWidth = + inspector.sidebarSplitBoxRef.current.state.width; + is(sidebarWidth, SIDEBAR_WIDTH * 2, "Got correct main split box width"); + is( + sidebarSplitBoxWidth, + SIDEBAR_WIDTH, + "Got correct sidebar split box width" + ); + + info("Click on the toggle button to toggle OFF the 3 pane inspector"); + onRuleViewAdded = inspector.once("ruleview-added"); + EventUtils.synthesizeMouseAtCenter( + button, + {}, + inspector.panelDoc.defaultView + ); + await onRuleViewAdded; + + info("Checking the sidebar size of the 2 pane inspector"); + sidebarWidth = inspector.splitBox.state.width; + is(sidebarWidth, SIDEBAR_WIDTH, "Got correct sidebar width"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js new file mode 100644 index 0000000000..024fca816b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the 3 pane inspector toggle button can render the bottom-left and +// bottom-right panels of equal sizes in the SIDE host. + +add_task(async function () { + info("Switch to 2 pane inspector to test the 3 pane toggle button behavior"); + await pushPref("devtools.inspector.three-pane-enabled", false); + + const { inspector, toolbox } = await openInspectorForURL("about:blank"); + const { panelDoc: doc } = inspector; + + info("Switch the host to the right"); + await toolbox.switchHost("right"); + + // Switching hosts is not correctly waiting when DevTools run in content frame + // See Bug 1571421. + await wait(1000); + + const button = doc.querySelector(".sidebar-toggle"); + const toolboxWidth = doc.getElementById("inspector-splitter-box").clientWidth; + + info("Click on the toggle button to toggle ON 3 pane inspector"); + let onRuleViewAdded = inspector.once("ruleview-added"); + EventUtils.synthesizeMouseAtCenter( + button, + {}, + inspector.panelDoc.defaultView + ); + await onRuleViewAdded; + + info("Checking the sizes of the 3 pane inspector"); + const sidebarSplitBoxWidth = inspector.sidebarSplitBoxRef.current.state.width; + is( + sidebarSplitBoxWidth, + toolboxWidth / 2, + "Got correct sidebar split box width" + ); + + info("Click on the toggle button to toggle OFF the 3 pane inspector"); + onRuleViewAdded = inspector.once("ruleview-added"); + EventUtils.synthesizeMouseAtCenter( + button, + {}, + inspector.panelDoc.defaultView + ); + await onRuleViewAdded; + + info("Checking the sidebar size of the 2 pane inspector"); + const sidebarWidth = inspector.splitBox.state.width; + is(sidebarWidth, toolboxWidth, "Got correct sidebar width"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js new file mode 100644 index 0000000000..d2653639a0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js @@ -0,0 +1,106 @@ +/* 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/. */ + +"use strict"; + +// Tests whether the initial selected tab is displayed when switched from 3 pane to 2 pane +// and back to 3 pane when no other tab is selected explicitly. Rule view is displayed +// immediately on toggling to 2 pane. +add_task(async function () { + info( + "Switch to 2 pane inspector and back to 3 pane to test whether the selected tab is used" + ); + await pushPref("devtools.inspector.three-pane-enabled", true); + + const { inspector } = await openInspectorForURL("about:blank"); + + inspector.sidebar.select("changesview"); + + is( + inspector.sidebar.getCurrentTabID(), + "changesview", + "Changes view should be the active sidebar" + ); + + info("Click on the toggle button to toggle OFF 3 pane inspector"); + await toggleSidebar(inspector); + + is( + inspector.sidebar.getCurrentTabID(), + "ruleview", + "Rules view should be the active sidebar on toggle to 2 pane" + ); + + info("Click on the toggle button to toggle ON 3 pane inspector"); + await toggleSidebar(inspector); + + is( + inspector.sidebar.getCurrentTabID(), + "changesview", + "Changes view should be the active sidebar again" + ); +}); + +// Tests whether the selected pane in 2 pane view is also used after toggling to 3 pane view. +add_task(async function () { + info("Switch to 3 pane to test whether the selected pane is preserved"); + await pushPref("devtools.inspector.three-pane-enabled", false); + + const { inspector } = await openInspectorForURL("about:blank"); + + inspector.sidebar.select("changesview"); + + is( + inspector.sidebar.getCurrentTabID(), + "changesview", + "Changes view should be the active sidebar" + ); + + info("Click on the toggle button to toggle ON 3 pane inspector"); + await toggleSidebar(inspector); + + is( + inspector.sidebar.getCurrentTabID(), + "changesview", + "Changes view should still be the active sidebar" + ); +}); + +// Tests whether the selected pane is layout view, if rule view is selected before toggling to 3 pane. +add_task(async function () { + info("Switch to 3 pane to test whether the selected pane is layout view"); + await pushPref("devtools.inspector.three-pane-enabled", false); + + const { inspector } = await openInspectorForURL("about:blank"); + + inspector.sidebar.select("ruleview"); + + is( + inspector.sidebar.getCurrentTabID(), + "ruleview", + "Rules view should be the active sidebar in 2 pane" + ); + + info("Click on the toggle button to toggle ON 3 pane inspector"); + await toggleSidebar(inspector); + + is( + inspector.sidebar.getCurrentTabID(), + "layoutview", + "Layout view should be the active sidebar in 3 pane" + ); +}); + +const toggleSidebar = async inspector => { + const { panelDoc: doc } = inspector; + const button = doc.querySelector(".sidebar-toggle"); + + const onRuleViewAdded = inspector.once("ruleview-added"); + EventUtils.synthesizeMouseAtCenter( + button, + {}, + inspector.panelDoc.defaultView + ); + await onRuleViewAdded; +}; diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-layout-invariant.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-layout-invariant.js new file mode 100644 index 0000000000..1abc7db975 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-layout-invariant.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that documents with suppressed whitespace nodes are unaffected by the +// activation of the inspector panel. + +const TEST_URL = URL_ROOT + "doc_inspector_pane-toggle-layout-invariant.html"; + +async function getInvariantRect() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + const invariant = content.document.getElementById("invariant"); + return invariant.getBoundingClientRect(); + }); +} + +add_task(async function () { + await addTab(TEST_URL); + + // Get the initial position of the "invariant" element. We'll later check it + // again and we'll expect to get the same value. + const beforeRect = await getInvariantRect(); + + // Open the inspector. + await openInspector(); + + const afterRect = await getInvariantRect(); + + is(afterRect.x, beforeRect.x, "invariant x should be same as initial value."); + is(afterRect.y, beforeRect.y, "invariant y should be same as initial value."); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane_state_restore.js b/devtools/client/inspector/test/browser_inspector_pane_state_restore.js new file mode 100644 index 0000000000..8eb345e3de --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane_state_restore.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the previous inspector split box sizes are restored when reopening the +// inspector. + +const EXPECTED_INITIAL_WIDTH = 101; +const EXPECTED_INITIAL_HEIGHT = 102; +const EXPECTED_INITIAL_SIDEBAR_WIDTH = 103; + +const EXPECTED_NEW_WIDTH = 150; +const EXPECTED_NEW_HEIGHT = 100; +const EXPECTED_NEW_SIDEBAR_WIDTH = 250; + +add_task(async function () { + // Simulate that the user has already stored their preferred split boxes widths. + await pushPref( + "devtools.toolsidebar-width.inspector", + EXPECTED_INITIAL_WIDTH + ); + await pushPref( + "devtools.toolsidebar-height.inspector", + EXPECTED_INITIAL_HEIGHT + ); + await pushPref( + "devtools.toolsidebar-width.inspector.splitsidebar", + EXPECTED_INITIAL_SIDEBAR_WIDTH + ); + + const { inspector } = await openInspectorForURL("about:blank"); + + info("Check the initial size of the inspector."); + const { width, height, splitSidebarWidth } = inspector.getSidebarSize(); + is(width, EXPECTED_INITIAL_WIDTH, "Got correct initial width."); + is(height, EXPECTED_INITIAL_HEIGHT, "Got correct initial height."); + is( + splitSidebarWidth, + EXPECTED_INITIAL_SIDEBAR_WIDTH, + "Got correct initial split sidebar width." + ); + + info("Simulate updates to the dimensions of the various splitboxes."); + inspector.splitBox.setState({ + width: EXPECTED_NEW_WIDTH, + height: EXPECTED_NEW_HEIGHT, + }); + inspector.sidebarSplitBoxRef.current.setState({ + width: EXPECTED_NEW_SIDEBAR_WIDTH, + }); + + await closeToolbox(); + + info( + "Check the stored sizes of the inspector in the preferences when the inspector " + + "is closed" + ); + const storedWidth = Services.prefs.getIntPref( + "devtools.toolsidebar-width.inspector" + ); + const storedHeight = Services.prefs.getIntPref( + "devtools.toolsidebar-height.inspector" + ); + const storedSplitSidebarWidth = Services.prefs.getIntPref( + "devtools.toolsidebar-width.inspector.splitsidebar" + ); + is(storedWidth, EXPECTED_NEW_WIDTH, "Got correct stored width."); + is(storedHeight, EXPECTED_NEW_HEIGHT, "Got correct stored height"); + is( + storedSplitSidebarWidth, + EXPECTED_NEW_SIDEBAR_WIDTH, + "Got correct stored split sidebar width." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_picker-reset-reference.js b/devtools/client/inspector/test/browser_inspector_picker-reset-reference.js new file mode 100644 index 0000000000..2a9bc39ca6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_picker-reset-reference.js @@ -0,0 +1,69 @@ +/* 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/. */ + +"use strict"; + +// Test that the node picker reset its reference for the last hovered node when the user +// stops picking (See Bug 1736183). + +const TEST_URL = + "data:text/html;charset=utf8,<h1 id=target>Pick target</h1><h2>ignore me</h2>"; + +add_task(async () => { + const { inspector, toolbox, highlighterTestFront } = + await openInspectorForURL(TEST_URL); + + const { waitForHighlighterTypeHidden } = getHighlighterTestHelpers(inspector); + + info( + "Start the picker and hover an element to populate the picker hovered node reference" + ); + await startPicker(toolbox); + await hoverElement(inspector, "#target"); + ok( + await highlighterTestFront.assertHighlightedNode("#target"), + "The highlighter is shown on the expected node" + ); + + info("Hit Escape to cancel picking"); + let onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + await stopPickerWithEscapeKey(toolbox); + await onHighlighterHidden; + + info("And start it again, and hover the same node again"); + await startPicker(toolbox); + await hoverElement(inspector, "#target"); + ok( + await highlighterTestFront.assertHighlightedNode("#target"), + "The highlighter is shown on the expected node again" + ); + + info("Pick the element to stop the picker"); + onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + // nodePicker isPicking property is set to false _after_ picker-node-picked event, so + // we need to wait for picker-stopped here. + const onPickerStopped = toolbox.nodePicker.once("picker-stopped"); + await pickElement(inspector, "#target", 5, 5); + await onHighlighterHidden; + await onPickerStopped; + + info("And start it and hover the same node, again"); + await startPicker(toolbox); + await hoverElement(inspector, "#target"); + ok( + await highlighterTestFront.assertHighlightedNode("#target"), + "The highlighter is shown on the expected node again" + ); + + info("Stop the picker to avoid pending Promise"); + onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.BOXMODEL + ); + await stopPickerWithEscapeKey(toolbox); + await onHighlighterHidden; +}); diff --git a/devtools/client/inspector/test/browser_inspector_picker-shift-key.js b/devtools/client/inspector/test/browser_inspector_picker-shift-key.js new file mode 100644 index 0000000000..2eb1c04709 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_picker-shift-key.js @@ -0,0 +1,94 @@ +/* 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/. */ + +"use strict"; + +// Test that the node picker can pick elements with pointer-events: none when holding Shift. + +const TEST_URI = `data:text/html;charset=utf-8, + <!DOCTYPE html> + <main style="display:flex"> + <div id="pointer">Regular element</div> + <div id="nopointer" style="pointer-events: none">Element with pointer-events: none</div> + <div id="transluscent" style="pointer-events: none;opacity: 0.1">Element with opacity of 0.1</div> + <div id="invisible" style="pointer-events: none;opacity: 0">Element with opacity of 0</div> + </main>`; +const IS_OSX = Services.appinfo.OS === "Darwin"; + +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL(TEST_URI); + + const body = await getNodeFront("body", inspector); + is( + inspector.selection.nodeFront, + body, + "By default the body node is selected" + ); + + info("Start the element picker"); + await startPicker(toolbox); + + info( + "Shift-clicking element with pointer-events: none does select the element" + ); + await clickElement({ + inspector, + selector: "#nopointer", + shiftKey: true, + }); + await checkElementSelected("#nopointer", inspector); + + info("Shift-clicking element with default pointer-events value also works"); + await clickElement({ + inspector, + selector: "#pointer", + shiftKey: true, + }); + await checkElementSelected("#pointer", inspector); + + info( + "Clicking element with pointer-events: none without holding Shift won't select the element but its parent" + ); + await clickElement({ + inspector, + selector: "#nopointer", + shiftKey: false, + }); + await checkElementSelected("main", inspector); + + info("Shift-clicking transluscent visible element works"); + await clickElement({ + inspector, + selector: "#transluscent", + shiftKey: true, + }); + await checkElementSelected("#transluscent", inspector); + + info("Shift-clicking invisible element select its parent"); + await clickElement({ + inspector, + selector: "#invisible", + shiftKey: true, + }); + await checkElementSelected("main", inspector); +}); + +async function clickElement({ selector, inspector, shiftKey }) { + const onSelectionChanged = inspector.once("inspector-updated"); + await safeSynthesizeMouseEventAtCenterInContentPage(selector, { + shiftKey, + // Hold meta/ctrl so we don't have to start the picker again + [IS_OSX ? "metaKey" : "ctrlKey"]: true, + }); + await onSelectionChanged; +} + +async function checkElementSelected(selector, inspector) { + const el = await getNodeFront(selector, inspector); + is( + inspector.selection.nodeFront, + el, + `The element ${selector} is now selected` + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_picker-stop-on-eyedropper.js b/devtools/client/inspector/test/browser_inspector_picker-stop-on-eyedropper.js new file mode 100644 index 0000000000..bed9e2e82c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_picker-stop-on-eyedropper.js @@ -0,0 +1,46 @@ +/* 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/. */ +"use strict"; + +// Test that the highlighter's picker is stopped when the eyedropper tool is +// selected + +const TEST_URI = + "data:text/html;charset=UTF-8," + + "testing the highlighter goes away on eyedropper selection"; + +add_task(async function () { + const { toolbox } = await openInspectorForURL(TEST_URI); + const pickerStopped = toolbox.nodePicker.once("picker-stopped"); + const eyeDropperButtonClasses = + toolbox.getPanel("inspector").eyeDropperButton.classList; + + const eyeDropperStopped = waitFor( + () => !eyeDropperButtonClasses.contains("checked") + ); + + info("Starting the inspector picker"); + await startPicker(toolbox); + + info("Starting the eyedropper tool"); + await startEyeDropper(toolbox); + + ok(eyeDropperButtonClasses.contains("checked"), "eyedropper is started"); + + info("Waiting for the picker-stopped event to be fired"); + await pickerStopped; + + ok( + true, + "picker-stopped event fired after eyedropper is selected; picker is closed" + ); + + info("Starting the inspector picker again"); + await startPicker(toolbox); + + info("Checking if eyedropper is stopped"); + await eyeDropperStopped; + + ok(true, "eyedropper stopped after node picker; eyedropper is closed"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js b/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js new file mode 100644 index 0000000000..e8a5796f49 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js @@ -0,0 +1,27 @@ +/* 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/. */ +"use strict"; + +// Test that the highlighter's picker is stopped when a different tool is +// selected + +const TEST_URI = + "data:text/html;charset=UTF-8," + + "testing the highlighter goes away on tool selection"; + +add_task(async function () { + const { toolbox } = await openInspectorForURL(TEST_URI); + const pickerStopped = toolbox.nodePicker.once("picker-stopped"); + + info("Starting the inspector picker"); + await startPicker(toolbox); + + info("Selecting another tool than the inspector in the toolbox"); + await toolbox.selectNextTool(); + + info("Waiting for the picker-stopped event to be fired"); + await pickerStopped; + + ok(true, "picker-stopped event fired after switch tools; picker is closed"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_picker-useragent-widget.js b/devtools/client/inspector/test/browser_inspector_picker-useragent-widget.js new file mode 100644 index 0000000000..11b45e8dca --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_picker-useragent-widget.js @@ -0,0 +1,74 @@ +/* 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/. */ + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, + <!DOCTYPE html> + <video controls></video>`; + +// Test that using the node picker on user agent widgets only selects shadow dom +// elements if `devtools.inspector.showAllAnonymousContent` is true. +// If not, we should only surface the host, in this case, the <video>. +// +// For this test we use a <video controls> tag, which is using shadow dom. +add_task(async function () { + // Run the test for both values for devtools.inspector.showAllAnonymousContent + await runUserAgentWidgetPickerTest({ enableAnonymousContent: false }); + await runUserAgentWidgetPickerTest({ enableAnonymousContent: true }); +}); + +async function runUserAgentWidgetPickerTest({ enableAnonymousContent }) { + await pushPref( + "devtools.inspector.showAllAnonymousContent", + enableAnonymousContent + ); + const { inspector, toolbox } = await openInspectorForURL(TEST_URI); + + info("Use the node picker inside the <video> element"); + await startPicker(toolbox); + const onPickerStopped = toolbox.nodePicker.once("picker-stopped"); + await pickElement(inspector, "video", 5, 5); + await onPickerStopped; + + const selectedNode = inspector.selection.nodeFront; + if (enableAnonymousContent) { + // We do not assert specifically which node was selected, we just want to + // check the node was under the shadow DOM for the <video type=date> + const shadowHost = getShadowHost(selectedNode); + Assert.notStrictEqual( + selectedNode.tagName.toLowerCase(), + "video", + "The selected node is not the <video>" + ); + ok(shadowHost, "The selected node is in a shadow root"); + is(shadowHost.tagName.toLowerCase(), "video", "The shadowHost is <video>"); + } else { + is( + selectedNode.tagName.toLowerCase(), + "video", + "The selected node is the <video>" + ); + } +} + +/** + * Retrieve the nodeFront for the shadow host containing the provided nodeFront. + * Returns null if the nodeFront is not in a shadow DOM. + * + * @param {NodeFront} nodeFront + * The nodeFront for which we want to retrieve the shadow host. + * @return {NodeFront} The nodeFront corresponding to the shadow host, or null + * if the nodeFront is not in shadow DOM. + */ +function getShadowHost(nodeFront) { + let parent = nodeFront; + while (parent) { + if (parent.isShadowHost) { + return parent; + } + parent = parent.parentOrHost(); + } + return null; +} diff --git a/devtools/client/inspector/test/browser_inspector_portrait_mode.js b/devtools/client/inspector/test/browser_inspector_portrait_mode.js new file mode 100644 index 0000000000..c4734645c3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_portrait_mode.js @@ -0,0 +1,82 @@ +/* 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/. */ + +"use strict"; + +// Test that the inspector splitter is properly initialized in horizontal mode if the +// inspector starts in portrait mode. + +add_task(async function () { + let { inspector, toolbox } = await openInspectorForURL( + "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>", + "window" + ); + + const hostWindow = toolbox.win.parent; + const originalWidth = hostWindow.outerWidth; + const originalHeight = hostWindow.outerHeight; + + let splitter = inspector.panelDoc.querySelector( + ".inspector-sidebar-splitter" + ); + + // If the inspector is not already in landscape mode. + if (!splitter.classList.contains("vert")) { + info("Resize toolbox window to force inspector to landscape mode"); + const onClassnameMutation = waitForClassMutation(splitter); + hostWindow.resizeTo(800, 500); + await onClassnameMutation; + + ok(splitter.classList.contains("vert"), "Splitter is in vertical mode"); + } + + info("Resize toolbox window to force inspector to portrait mode"); + const onClassnameMutation = waitForClassMutation(splitter); + hostWindow.resizeTo(500, 500); + await onClassnameMutation; + + ok(splitter.classList.contains("horz"), "Splitter is in horizontal mode"); + + info("Close the inspector"); + await toolbox.destroy(); + + info("Reopen inspector"); + ({ inspector, toolbox } = await openInspector("window")); + + // Devtools window should still be 500px * 500px, inspector should still be in portrait. + splitter = inspector.panelDoc.querySelector(".inspector-sidebar-splitter"); + ok(splitter.classList.contains("horz"), "Splitter is in horizontal mode"); + + info("Restore original window size"); + toolbox.win.parent.resizeTo(originalWidth, originalHeight); +}); + +/** + * Helper waiting for a class attribute mutation on the provided target. Returns a + * promise. + * + * @param {Node} target + * Node to observe + * @return {Promise} promise that will resolve upon receiving a mutation for the class + * attribute on the target. + */ +function waitForClassMutation(target) { + return new Promise(resolve => { + const observer = new MutationObserver(mutations => { + for (const mutation of mutations) { + if (mutation.attributeName === "class") { + observer.disconnect(); + resolve(); + return; + } + } + }); + observer.observe(target, { attributes: true }); + }); +} + +registerCleanupFunction(function () { + // Restore the host type for other tests. + Services.prefs.clearUserPref("devtools.toolbox.host"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js b/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js new file mode 100644 index 0000000000..df924aab8c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js @@ -0,0 +1,235 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* globals getHighlighterTestFrontWithoutToolbox */ +"use strict"; + +// Test that locking the pseudoclass displays correctly in the ruleview + +const PSEUDO = ":hover"; +const TEST_URL = + "data:text/html;charset=UTF-8," + + "<head>" + + " <style>div {color:red;} div:hover {color:blue;}</style>" + + "</head>" + + "<body>" + + ' <div id="parent-div">' + + ' <div id="div-1">test div</div>' + + ' <div id="div-2">test div2</div>' + + " </div>" + + "</body>"; + +add_task(async function () { + info("Creating the test tab and opening the rule-view"); + let { tab, toolbox, inspector, highlighterTestFront } = + await openInspectorForURL(TEST_URL); + + info("Selecting the ruleview sidebar"); + inspector.sidebar.select("ruleview"); + + const view = inspector.getPanel("ruleview").view; + + info("Selecting the test node"); + await selectNode("#div-1", inspector); + + await togglePseudoClass(inspector); + await assertPseudoAddedToNode( + inspector, + highlighterTestFront, + view, + "#div-1" + ); + + await togglePseudoClass(inspector); + await assertPseudoRemovedFromNode(highlighterTestFront, "#div-1"); + await assertPseudoRemovedFromView( + inspector, + highlighterTestFront, + view, + "#div-1" + ); + + await togglePseudoClass(inspector); + await testNavigate(inspector); + + info("Toggle pseudo on the parent and ensure everything is toggled off"); + await selectNode("#parent-div", inspector); + await togglePseudoClass(inspector); + await assertPseudoRemovedFromNode(highlighterTestFront, "#div-1"); + await assertPseudoRemovedFromView( + inspector, + highlighterTestFront, + view, + "#div-1" + ); + + await togglePseudoClass(inspector); + info("Assert pseudo is dismissed when toggling it on a sibling node"); + await selectNode("#div-2", inspector); + await togglePseudoClass(inspector); + await assertPseudoAddedToNode( + inspector, + highlighterTestFront, + view, + "#div-2" + ); + const hasLock = await hasPseudoClassLock("#div-1", PSEUDO); + ok( + !hasLock, + "pseudo-class lock has been removed for the previous locked node" + ); + + info("Destroying the toolbox"); + await toolbox.destroy(); + + // As the toolbox get destroyed, we need to fetch a new test-actor + highlighterTestFront = await getHighlighterTestFrontWithoutToolbox(tab); + + await assertPseudoRemovedFromNode(highlighterTestFront, "#div-1"); + await assertPseudoRemovedFromNode(highlighterTestFront, "#div-2"); +}); + +async function togglePseudoClass(inspector) { + info("Toggle the pseudoclass, wait for it to be applied"); + + // Give the inspector panels a chance to update when the pseudoclass changes + const onPseudo = inspector.selection.once("pseudoclass"); + const onRefresh = inspector.once("rule-view-refreshed"); + + // Walker uses SDK-events so calling walker.once does not return a promise. + const onMutations = once(inspector.walker, "mutations"); + + await inspector.togglePseudoClass(PSEUDO); + + await onPseudo; + await onRefresh; + await onMutations; +} + +async function testNavigate(inspector) { + await selectNode("#parent-div", inspector); + + info("Make sure the pseudoclass is still on after navigating to a parent"); + + ok( + await hasPseudoClassLock("#div-1", PSEUDO), + "pseudo-class lock is still applied after inspecting ancestor" + ); + + await selectNode("#div-2", inspector); + + info( + "Make sure the pseudoclass is still set after navigating to a " + + "non-hierarchy node" + ); + ok( + await hasPseudoClassLock("#div-1", PSEUDO), + "pseudo-class lock is still on after inspecting sibling node" + ); + + await selectNode("#div-1", inspector); +} + +async function assertPseudoAddedToNode( + inspector, + highlighterTestFront, + ruleview, + selector +) { + info( + "Make sure the pseudoclass lock is applied to " + + selector + + " and its ancestors" + ); + + let hasLock = await hasPseudoClassLock(selector, PSEUDO); + ok(hasLock, "pseudo-class lock has been applied"); + hasLock = await hasPseudoClassLock("#parent-div", PSEUDO); + ok(hasLock, "pseudo-class lock has been applied"); + hasLock = await hasPseudoClassLock("body", PSEUDO); + ok(hasLock, "pseudo-class lock has been applied"); + + info("Check that the ruleview contains the pseudo-class rule"); + const rules = ruleview.element.querySelectorAll(".ruleview-rule"); + is( + rules.length, + 3, + "rule view is showing 3 rules for pseudo-class locked div" + ); + is( + rules[1]._ruleEditor.rule.selectorText, + "div:hover", + "rule view is showing " + PSEUDO + " rule" + ); + + info("Show the highlighter on " + selector); + const nodeFront = await getNodeFront(selector, inspector); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront + ); + + info("Check that the infobar selector contains the pseudo-class"); + const value = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-pseudo-classes" + ); + is(value, PSEUDO, "pseudo-class in infobar selector"); + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); +} + +async function assertPseudoRemovedFromNode(highlighterTestFront, selector) { + info( + "Make sure the pseudoclass lock is removed from #div-1 and its " + + "ancestors" + ); + + let hasLock = await hasPseudoClassLock(selector, PSEUDO); + ok(!hasLock, "pseudo-class lock has been removed"); + hasLock = await hasPseudoClassLock("#parent-div", PSEUDO); + ok(!hasLock, "pseudo-class lock has been removed"); + hasLock = await hasPseudoClassLock("body", PSEUDO); + ok(!hasLock, "pseudo-class lock has been removed"); +} + +async function assertPseudoRemovedFromView( + inspector, + highlighterTestFront, + ruleview, + selector +) { + info("Check that the ruleview no longer contains the pseudo-class rule"); + const rules = ruleview.element.querySelectorAll(".ruleview-rule"); + is(rules.length, 2, "rule view is showing 2 rules after removing lock"); + + const nodeFront = await getNodeFront(selector, inspector); + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront + ); + + const value = await highlighterTestFront.getHighlighterNodeTextContent( + "box-model-infobar-pseudo-classes" + ); + is(value, "", "pseudo-class removed from infobar selector"); + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); +} + +/** + * Check that an element currently has a pseudo-class lock. + * @param {String} selector The node selector to get the pseudo-class from + * @param {String} pseudo The pseudoclass to check for + * @return {Promise<Boolean>} + */ +function hasPseudoClassLock(selector, pseudoClass) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, pseudoClass], + (_selector, _pseudoClass) => { + const element = content.document.querySelector(_selector); + return InspectorUtils.hasPseudoClassLock(element, _pseudoClass); + } + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js b/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js new file mode 100644 index 0000000000..36ecec1638 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the inspector has the correct pseudo-class locking menu items and +// that these items actually work + +const { + PSEUDO_CLASSES, +} = require("resource://devtools/shared/css/constants.js"); +const TEST_URI = + "data:text/html;charset=UTF-8," + + "pseudo-class lock node menu tests" + + "<div>test div</div>"; +// Strip the colon prefix from pseudo-classes (:before => before) +const PSEUDOS = PSEUDO_CLASSES.map(pseudo => pseudo.substr(1)); + +add_task(async function () { + const { inspector, highlighterTestFront } = await openInspectorForURL( + TEST_URI + ); + await selectNode("div", inspector); + + const allMenuItems = openContextMenuAndGetAllItems(inspector); + + await testMenuItems(highlighterTestFront, allMenuItems, inspector); +}); + +async function testMenuItems(highlighterTestFront, allMenuItems, inspector) { + for (const pseudo of PSEUDOS) { + const menuItem = allMenuItems.find( + item => item.id === "node-menu-pseudo-" + pseudo + ); + ok(menuItem, ":" + pseudo + " menuitem exists"); + is(menuItem.disabled, false, ":" + pseudo + " menuitem is enabled"); + + // Give the inspector panels a chance to update when the pseudoclass changes + const onPseudo = inspector.selection.once("pseudoclass"); + const onRefresh = inspector.once("rule-view-refreshed"); + + // Walker uses SDK-events so calling walker.once does not return a promise. + const onMutations = once(inspector.walker, "mutations"); + + menuItem.click(); + + await onPseudo; + await onRefresh; + await onMutations; + + const hasLock = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [`:${pseudo}`], + pseudoClass => { + const element = content.document.querySelector("div"); + return InspectorUtils.hasPseudoClassLock(element, pseudoClass); + } + ); + ok(hasLock, "pseudo-class lock has been applied"); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_reload-01.js b/devtools/client/inspector/test/browser_inspector_reload-01.js new file mode 100644 index 0000000000..7adf53ceb3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload-01.js @@ -0,0 +1,30 @@ +/* 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/. */ +"use strict"; + +// A test to ensure reloading a page doesn't break the inspector. + +// Reload should reselect the currently selected markup view element. +// This should work even when an element whose selector needs escaping +// is selected (bug 1002280). +const TEST_URI = "data:text/html,<p id='1'>p</p>"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + await selectNode("p", inspector); + + const markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + await navigateTo(TEST_URI); + + info("Waiting for markupview to load after reload."); + await markupLoaded; + + const nodeFront = await getNodeFront("p", inspector); + is(inspector.selection.nodeFront, nodeFront, "<p> selected after reload."); + + info("Selecting a node to see that inspector still works."); + await selectNode("body", inspector); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload-02.js b/devtools/client/inspector/test/browser_inspector_reload-02.js new file mode 100644 index 0000000000..da27805f21 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload-02.js @@ -0,0 +1,47 @@ +/* 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/. */ +"use strict"; + +// A test to ensure reloading a page doesn't break the inspector. + +// Reload should reselect the currently selected markup view element. +// This should work even when an element whose selector is inaccessible +// is selected (bug 1038651). +const TEST_URI = + 'data:text/xml,<?xml version="1.0" standalone="no"?>' + + '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"' + + ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + + '<svg width="4cm" height="4cm" viewBox="0 0 400 400"' + + ' xmlns="http://www.w3.org/2000/svg" version="1.1">' + + " <title>Example triangle01- simple example of a path</title>" + + " <desc>A path that draws a triangle</desc>" + + ' <rect x="1" y="1" width="398" height="398"' + + ' fill="none" stroke="blue" />' + + ' <path d="M 100 100 L 300 100 L 200 300 z"' + + ' fill="red" stroke="blue" stroke-width="3" />' + + "</svg>"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + const markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + await navigateTo(TEST_URI); + + info("Waiting for markupview to load after reload."); + await markupLoaded; + + const svgFront = await getNodeFront("svg", inspector); + is(inspector.selection.nodeFront, svgFront, "<svg> selected after reload."); + + info("Selecting a node to see that inspector still works."); + await selectNode("rect", inspector); + + info("Reloading page."); + await navigateTo(TEST_URI); + + const rectFront = await getNodeFront("rect", inspector); + is(inspector.selection.nodeFront, rectFront, "<rect> selected after reload."); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload_iframe.js b/devtools/client/inspector/test/browser_inspector_reload_iframe.js new file mode 100644 index 0000000000..8c7a6a5407 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload_iframe.js @@ -0,0 +1,48 @@ +/* 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/. */ +"use strict"; + +// Check that the markup view selection is preserved even if the selection is +// in an iframe. + +// We're loading an image that would take a few second to load so the iframe won't have +// its readyState to "complete" (it should be "interactive"). +// That was causing some issue, see Bug 1733539. +const IMG_URL = URL_ROOT_COM_SSL + "sjs_slow-loading-image.sjs"; +const FRAME_URI = + "data:text/html;charset=utf-8," + + encodeURI(` + <div id="in-frame">div in the iframe</div> + <img src="${IMG_URL}"></img> + `); +const HTML = ` + <iframe src="${FRAME_URI}"></iframe> +`; + +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + await selectNodeInFrames(["iframe", "#in-frame"], inspector); + + const markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + await navigateTo(TEST_URI); + + info("Waiting for markupview to load after reload."); + await markupLoaded; + + const reloadedNodeFront = await getNodeFrontInFrames( + ["iframe", "#in-frame"], + inspector + ); + + is( + inspector.selection.nodeFront, + reloadedNodeFront, + "#in-frame selected after reload." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload_invalid_iframe.js b/devtools/client/inspector/test/browser_inspector_reload_invalid_iframe.js new file mode 100644 index 0000000000..2a6bd69e48 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload_invalid_iframe.js @@ -0,0 +1,63 @@ +/* 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/. */ + +"use strict"; + +// When reselecting the selected node, if an element expected to be an iframe is +// NOT actually an iframe, we should still fallback on the root body element. + +const TEST_URI = "data:text/html;charset=utf-8,<div id='fake-iframe'>"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + info("Replace fake-iframe div with a real iframe"); + await ContentTask.spawn(gBrowser.selectedBrowser, null, async function () { + await new Promise(resolve => { + // Remove the fake-iframe div + content.document.querySelector("#fake-iframe").remove(); + + // Create an iframe element with the same id "fake-iframe". + const iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + iframe.setAttribute("id", "fake-iframe"); + + iframe.contentWindow.addEventListener("load", () => { + // Create a div element and append it to the iframe + const div = content.document.createElement("div"); + div.id = "in-frame"; + div.textContent = "div in frame"; + + const frameContent = + iframe.contentWindow.document.querySelector("body"); + frameContent.appendChild(div); + resolve(); + }); + }); + }); + + ok( + await hasMatchingElementInContentPage("iframe"), + "The iframe has been added to the page" + ); + + info("Select node inside iframe."); + await selectNodeInFrames(["iframe", "#in-frame"], inspector); + + const markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + await navigateTo(TEST_URI); + + info("Waiting for markupview to load after reload."); + await markupLoaded; + + const rootNodeFront = await getNodeFront("body", inspector); + + is( + inspector.selection.nodeFront, + rootNodeFront, + "body node selected after reload." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload_missing-iframe-node.js b/devtools/client/inspector/test/browser_inspector_reload_missing-iframe-node.js new file mode 100644 index 0000000000..82e154d5ef --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload_missing-iframe-node.js @@ -0,0 +1,58 @@ +/* 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/. */ + +"use strict"; + +// Check that the markup view selection falls back to the document body if an iframe node +// becomes missing after a reload. This can happen if the iframe and its contents are +// added dynamically to the page before reloading. + +const TEST_URI = "data:text/html;charset=utf-8,"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + info("Create new iframe and add it to the page."); + await ContentTask.spawn(gBrowser.selectedBrowser, null, async function () { + await new Promise(resolve => { + const iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + + iframe.contentWindow.addEventListener("load", () => { + // Create a div element and append it to the iframe + const div = content.document.createElement("div"); + div.id = "in-frame"; + div.textContent = "div in frame"; + + const frameContent = + iframe.contentWindow.document.querySelector("body"); + frameContent.appendChild(div); + resolve(); + }); + }); + }); + ok( + await hasMatchingElementInContentPage("iframe"), + "The iframe has been added to the page" + ); + + info("Select node inside iframe."); + await selectNodeInFrames(["iframe", "#in-frame"], inspector); + + const markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + await navigateTo(TEST_URI); + + info("Waiting for markupview to load after reload."); + await markupLoaded; + + const rootNodeFront = await getNodeFront("body", inspector); + + is( + inspector.selection.nodeFront, + rootNodeFront, + "body node selected after reload." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload_nested_iframe.js b/devtools/client/inspector/test/browser_inspector_reload_nested_iframe.js new file mode 100644 index 0000000000..46486996e3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload_nested_iframe.js @@ -0,0 +1,50 @@ +/* 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/. */ +"use strict"; + +// Check that the markup view selection is preserved even if the selection is +// in a nested iframe. + +const NESTED_IFRAME_URL = `https://example.com/document-builder.sjs?html=${encodeURIComponent( + "<h3>second level iframe</h3>" +)}&delay=500`; + +const TEST_URI = `data:text/html;charset=utf-8, + <h1>Top-level</h1> + <iframe id=first-level + src='data:text/html;charset=utf-8,${encodeURIComponent( + `<h2>first level iframe</h2><iframe id=second-level src="${NESTED_IFRAME_URL}"></iframe>` + )}' + ></iframe>`; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + await selectNodeInFrames( + ["iframe#first-level", "iframe#second-level", "h3"], + inspector + ); + + const markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + await navigateTo(TEST_URI); + + info("Waiting for markupview to load after reload."); + await markupLoaded; + + // This was broken at some point, see Bug 1733539. + ok(true, "The markup did reload fine"); + + const reloadedNodeFront = await getNodeFrontInFrames( + ["iframe#first-level", "iframe#second-level", "h3"], + inspector + ); + + is( + inspector.selection.nodeFront, + reloadedNodeFront, + `h3 selected after reload` + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload_shadow_dom.js b/devtools/client/inspector/test/browser_inspector_reload_shadow_dom.js new file mode 100644 index 0000000000..1bcdeb9ff6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload_shadow_dom.js @@ -0,0 +1,61 @@ +/* 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/. */ +"use strict"; + +// Check that the markup view selection is preserved even if the selection is in shadow-dom. + +const HTML = ` + <html> + <head> + <meta charset="utf8"> + <title>Test</title> + </head> + <body> + <h1>Shadow DOM test</h1> + <test-component> + <div slot="slot1" id="el1">content</div> + </test-component> + <script> + 'use strict'; + + customElements.define('test-component', class extends HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({mode: 'open'}); + shadowRoot.innerHTML = '<slot class="slot-class" name="slot1"></slot>'; + } + }); + </script> + </body> + </html> +`; + +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + info("Select node in shadow DOM"); + const nodeFront = await getNodeFrontInShadowDom( + "slot", + "test-component", + inspector + ); + await selectNode(nodeFront, inspector); + + info("Reloading page."); + await navigateTo(TEST_URI); + + const reloadedNodeFront = await getNodeFrontInShadowDom( + "slot", + "test-component", + inspector + ); + + is( + inspector.selection.nodeFront, + reloadedNodeFront, + "<slot> is selected after reload." + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload_xul.js b/devtools/client/inspector/test/browser_inspector_reload_xul.js new file mode 100644 index 0000000000..dbbe906a09 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload_xul.js @@ -0,0 +1,48 @@ +/* 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/. */ +"use strict"; + +// Tests for inspecting a node on a XUL document, spanning a tab reload. + +const TEST_URI = URL_ROOT_SSL + "doc_inspector_reload_xul.xhtml"; + +add_task(async function () { + const { tab, inspector, toolbox } = await openInspectorForURL(TEST_URI); + await testToolboxInitialization(tab, inspector, toolbox); +}); + +async function testToolboxInitialization(tab, inspector, toolbox) { + ok(true, "Inspector started, and notification received."); + ok(inspector, "Inspector instance is accessible."); + + await selectNode("#p", inspector); + await testMarkupView("#p", inspector); + + info("Reloading the page."); + await navigateTo(TEST_URI); + + await selectNode("#q", inspector); + await testMarkupView("#q", inspector); + + info("Destroying toolbox."); + await toolbox.destroy(); + + ok(true, "'destroyed' notification received."); + const toolboxForTab = gDevTools.getToolboxForTab(tab); + ok(!toolboxForTab, "Toolbox destroyed."); +} + +async function testMarkupView(selector, inspector) { + const nodeFront = await getNodeFront(selector, inspector); + try { + is( + inspector.selection.nodeFront, + nodeFront, + "Right node is selected in the markup view" + ); + } catch (ex) { + ok(false, "Got exception while resolving selected node of markup view."); + console.error(ex); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js b/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js new file mode 100644 index 0000000000..c11957424d --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing that the inspector doesn't go blank when navigating to a page that +// deletes an iframe while loading. + +const TEST_URL = URL_ROOT + "doc_inspector_remove-iframe-during-load.html"; + +add_task(async function () { + const { inspector, tab } = await openInspectorForURL("about:blank"); + await selectNode("body", inspector); + + // Before we start navigating, attach a listener on the reloaded event. + const onInspectorReloaded = inspector.once("reloaded"); + + // Note: here we don't want to use the `navigateTo` helper from shared-head.js + // because we want to modify the page as early as possible after the + // navigation, ideally before the inspector has fully initialized. + // See next comments. + const browser = tab.linkedBrowser; + const onBrowserLoaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.startLoadingURIString(browser, TEST_URL); + await onBrowserLoaded; + + // We do not want to wait for the inspector to be fully ready before testing + // so we load TEST_URL and just wait for the content window to be done loading + await SpecialPowers.spawn(browser, [], async function () { + await content.wrappedJSObject.readyPromise; + }); + + // The content doc contains a script that creates iframes and deletes them + // immediately after. It does this before the load event, after + // DOMContentLoaded and after load. This is what used to make the inspector go + // blank when navigating to that page. + // At this stage, there should be no iframes in the page anymore. + ok( + !(await contentPageHasNode(browser, "iframe")), + "Iframes added by the content page should have been removed" + ); + + // Create/remove an extra one now, after the load event. + info("Creating and removing an iframe."); + await SpecialPowers.spawn(browser, [], async function () { + const iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + iframe.remove(); + }); + + ok( + !(await contentPageHasNode(browser, "iframe")), + "The after-load iframe should have been removed." + ); + + // Assert that the markup-view is displayed and works + ok(!(await contentPageHasNode(browser, "iframe")), "Iframe has been removed"); + + const expectedText = await SpecialPowers.spawn( + browser, + [], + async function () { + return content.document.querySelector("#yay").textContent; + } + ); + is(expectedText, "load", "Load event fired."); + + info("Wait for the inspector to be properly reloaded"); + await onInspectorReloaded; + + // Smoke test to check that the inspector can still select nodes and hasn't + // gone blank. + await selectNode("#yay", inspector); +}); + +function contentPageHasNode(browser, selector) { + return SpecialPowers.spawn( + browser, + [selector], + async function (selectorChild) { + return !!content.document.querySelector(selectorChild); + } + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_search-01.js b/devtools/client/inspector/test/browser_inspector_search-01.js new file mode 100644 index 0000000000..3d56252f25 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-01.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +requestLongerTimeout(2); + +// Test that searching for nodes in the search field actually selects those +// nodes. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +// The various states of the inspector: [key, id, isValid] +// [ +// what key to press, +// what id should be selected after the keypress, +// is the searched text valid selector +// ] +const KEY_STATES = [ + ["#", "b1", true], // # + ["d", "b1", true], // #d + ["1", "b1", true], // #d1 + ["VK_RETURN", "d1", true], // #d1 + ["VK_BACK_SPACE", "d1", true], // #d + ["2", "d1", true], // #d2 + ["VK_RETURN", "d2", true], // #d2 + ["2", "d2", true], // #d22 + ["VK_RETURN", "d2", false], // #d22 + ["VK_BACK_SPACE", "d2", false], // #d2 + ["VK_RETURN", "d2", true], // #d2 + ["VK_BACK_SPACE", "d2", true], // #d + ["1", "d2", true], // #d1 + ["VK_RETURN", "d1", true], // #d1 + ["VK_BACK_SPACE", "d1", true], // #d + ["VK_BACK_SPACE", "d1", true], // # + ["VK_BACK_SPACE", "d1", true], // + ["d", "d1", true], // d + ["i", "d1", true], // di + ["v", "d1", true], // div + [".", "d1", true], // div. + ["c", "d1", true], // div.c + ["VK_UP", "d1", true], // div.c1 + ["VK_TAB", "d1", true], // div.c1 + ["VK_RETURN", "d2", true], // div.c1 + ["VK_BACK_SPACE", "d2", true], // div.c + ["VK_BACK_SPACE", "d2", true], // div. + ["VK_BACK_SPACE", "d2", true], // div + ["VK_BACK_SPACE", "d2", true], // di + ["VK_BACK_SPACE", "d2", true], // d + ["VK_BACK_SPACE", "d2", true], // + [".", "d2", true], // . + ["c", "d2", true], // .c + ["1", "d2", true], // .c1 + ["VK_RETURN", "d2", true], // .c1 + ["VK_RETURN", "s2", true], // .c1 + ["VK_RETURN", "p1", true], // .c1 + ["P", "p1", true], // .c1P + ["VK_RETURN", "p1", false], // .c1P +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { searchBox } = inspector; + + await selectNode("#b1", inspector); + await focusSearchBoxUsingShortcut(inspector.panelWin); + + let index = 0; + for (const [key, id, isValid] of KEY_STATES) { + const promises = []; + info(index + ": Pressing key " + key + " to get id " + id + "."); + + info("Waiting for current key press processing to complete"); + promises.push(inspector.searchSuggestions.once("processing-done")); + + if (key === "VK_RETURN") { + info("Waiting for " + (isValid ? "NO " : "") + "results"); + promises.push(inspector.search.once("search-result")); + } + + info("Waiting for search query to complete"); + promises.push(inspector.searchSuggestions.once("processing-done")); + + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + + await Promise.all(promises); + info( + "The keypress press process, any possible search results and the search query are complete." + ); + + info( + inspector.selection.nodeFront.id + + " is selected with text " + + searchBox.value + ); + const nodeFront = await getNodeFront("#" + id, inspector); + is( + inspector.selection.nodeFront, + nodeFront, + "Correct node is selected for state " + index + ); + + is( + !searchBox.parentNode.classList.contains("devtools-searchbox-no-match"), + isValid, + "Correct searchbox result state for state " + index + ); + + index++; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-02.js b/devtools/client/inspector/test/browser_inspector_search-02.js new file mode 100644 index 0000000000..767c0428a5 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-02.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing that searching for combining selectors using the inspector search +// field produces correct suggestions. + +const TEST_URL = URL_ROOT + "doc_inspector_search-suggestions.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +const TEST_DATA = [ + { + key: "d", + suggestions: [{ label: "div" }, { label: "#d1" }, { label: "#d2" }], + }, + { + key: "i", + suggestions: [{ label: "div" }], + }, + { + key: "v", + suggestions: [], + }, + { + key: " ", + suggestions: [{ label: "div div" }, { label: "div span" }], + }, + { + key: ">", + suggestions: [{ label: "div >div" }, { label: "div >span" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div div" }, { label: "div span" }], + }, + { + key: "+", + suggestions: [{ label: "div +span" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div div" }, { label: "div span" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div" }, { label: "#d1" }, { label: "#d2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "p", + suggestions: [ + { label: "p" }, + { label: "#p1" }, + { label: "#p2" }, + { label: "#p3" }, + ], + }, + { + key: " ", + suggestions: [{ label: "p strong" }], + }, + { + key: "+", + suggestions: [{ label: "p +button" }, { label: "p +p" }], + }, + { + key: "b", + suggestions: [{ label: "p +button" }], + }, + { + key: "u", + suggestions: [{ label: "p +button" }], + }, + { + key: "t", + suggestions: [{ label: "p +button" }], + }, + { + key: "t", + suggestions: [{ label: "p +button" }], + }, + { + key: "o", + suggestions: [{ label: "p +button" }], + }, + { + key: "n", + suggestions: [], + }, + { + key: "+", + suggestions: [{ label: "p +button+p" }], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const searchBox = inspector.searchBox; + const popup = inspector.searchSuggestions.searchPopup; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + const command = once(searchBox, "input"); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete"); + await onSearchProcessingDone; + + info( + "Query completed. Performing checks for input '" + + searchBox.value + + "' - key pressed: " + + key + ); + const actualSuggestions = popup.getItems(); + + is( + popup.isOpen ? actualSuggestions.length : 0, + suggestions.length, + "There are expected number of suggestions." + ); + + for (let i = 0; i < suggestions.length; i++) { + is( + actualSuggestions[i].label, + suggestions[i].label, + "The suggestion at " + i + "th index is correct." + ); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions.map(s => "'" + s.label + "'").join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-03.js b/devtools/client/inspector/test/browser_inspector_search-03.js new file mode 100644 index 0000000000..7ecb390b47 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-03.js @@ -0,0 +1,228 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing that searching for elements using the inspector search field +// produces correct suggestions. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +var TEST_DATA = [ + { + key: "d", + suggestions: [{ label: "div" }, { label: "#d1" }, { label: "#d2" }], + }, + { + key: "i", + suggestions: [{ label: "div" }], + }, + { + key: "v", + suggestions: [], + }, + { + key: ".", + suggestions: [{ label: "div.c1" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "#", + suggestions: [{ label: "div#d1" }, { label: "div#d2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div" }, { label: "#d1" }, { label: "#d2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: ".", + suggestions: [{ label: ".c1" }, { label: ".c2" }], + }, + { + key: "c", + suggestions: [{ label: ".c1" }, { label: ".c2" }], + }, + { + key: "2", + suggestions: [], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: ".c1" }, { label: ".c2" }], + }, + { + key: "1", + suggestions: [], + }, + { + key: "#", + suggestions: [{ label: "#d2" }, { label: "#p1" }, { label: "#s2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: ".c1" }, { label: ".c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: ".c1" }, { label: ".c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "#", + suggestions: [ + { label: "#b1" }, + { label: "#d1" }, + { label: "#d2" }, + { label: "#p1" }, + { label: "#p2" }, + { label: "#p3" }, + { label: "#root" }, + { label: "#s1" }, + { label: "#s2" }, + ], + }, + { + key: "p", + suggestions: [{ label: "#p1" }, { label: "#p2" }, { label: "#p3" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + { label: "#b1" }, + { label: "#d1" }, + { label: "#d2" }, + { label: "#p1" }, + { label: "#p2" }, + { label: "#p3" }, + { label: "#root" }, + { label: "#s1" }, + { label: "#s2" }, + ], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "p", + suggestions: [ + { label: "p" }, + { label: "#p1" }, + { label: "#p2" }, + { label: "#p3" }, + ], + }, + { + key: "[", + suggestions: [], + }, + { + key: "i", + suggestions: [], + }, + { + key: "d", + suggestions: [], + }, + { + key: "*", + suggestions: [], + }, + { + key: "=", + suggestions: [], + }, + { + key: "p", + suggestions: [], + }, + { + key: "]", + suggestions: [], + }, + { + key: ".", + suggestions: [{ label: "p[id*=p].c1" }, { label: "p[id*=p].c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "#", + suggestions: [ + { label: "p[id*=p]#p1" }, + { label: "p[id*=p]#p2" }, + { label: "p[id*=p]#p3" }, + ], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const searchBox = inspector.searchBox; + const popup = inspector.searchSuggestions.searchPopup; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + const command = once(searchBox, "input"); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete"); + await onSearchProcessingDone; + + info( + "Query completed. Performing checks for input '" + searchBox.value + "'" + ); + const actualSuggestions = popup.getItems(); + + is( + popup.isOpen ? actualSuggestions.length : 0, + suggestions.length, + "There are expected number of suggestions." + ); + + for (let i = 0; i < suggestions.length; i++) { + is( + actualSuggestions[i].label, + suggestions[i].label, + "The suggestion at " + i + "th index is correct." + ); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions.map(s => "'" + s.label + "'").join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-04.js b/devtools/client/inspector/test/browser_inspector_search-04.js new file mode 100644 index 0000000000..ff4c789b24 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-04.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing that searching for elements inside iframes does work. + +const IFRAME_SRC = "doc_inspector_search.html"; +const TEST_URL = + "data:text/html;charset=utf-8," + + '<div class="c1 c2">' + + '<iframe src="' + + URL_ROOT + + IFRAME_SRC + + '"></iframe>' + + '<iframe src="' + + URL_ROOT + + IFRAME_SRC + + '"></iframe>'; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +var TEST_DATA = [ + { + key: "d", + suggestions: [{ label: "div" }, { label: "#d1" }, { label: "#d2" }], + }, + { + key: "i", + suggestions: [{ label: "div" }], + }, + { + key: "v", + suggestions: [], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "div" }, { label: "#d1" }, { label: "#d2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: ".", + suggestions: [{ label: ".c1" }, { label: ".c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "#", + suggestions: [ + { label: "#b1" }, + { label: "#d1" }, + { label: "#d2" }, + { label: "#p1" }, + { label: "#p2" }, + { label: "#p3" }, + { label: "#root" }, + { label: "#s1" }, + { label: "#s2" }, + ], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const searchBox = inspector.searchBox; + const popup = inspector.searchSuggestions.searchPopup; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + const command = once(searchBox, "input"); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete"); + await onSearchProcessingDone; + + info( + "Query completed. Performing checks for input '" + searchBox.value + "'" + ); + const actualSuggestions = popup.getItems(); + + is( + popup.isOpen ? actualSuggestions.length : 0, + suggestions.length, + "There are expected number of suggestions." + ); + + for (let i = 0; i < suggestions.length; i++) { + is( + actualSuggestions[i].label, + suggestions[i].label, + "The suggestion at " + i + "th index is correct." + ); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions.map(s => "'" + s.label + "'").join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-05.js b/devtools/client/inspector/test/browser_inspector_search-05.js new file mode 100644 index 0000000000..7be5c66d56 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-05.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing that when search results contain suggestions for nodes in other +// frames, selecting these suggestions actually selects the right nodes. + +requestLongerTimeout(2); + +add_task(async function () { + const { inspector } = await openInspectorForURL( + `${URL_ROOT_ORG_SSL}doc_inspector_search-iframes.html` + ); + + info("Focus the search box"); + await focusSearchBoxUsingShortcut(inspector.panelWin); + + info("Enter # to search for all ids"); + let processingDone = once(inspector.searchSuggestions, "processing-done"); + EventUtils.synthesizeKey("#", {}, inspector.panelWin); + + info("Wait for search query to complete"); + await processingDone; + + info("Press tab to fill the search input with the first suggestion"); + processingDone = once(inspector.searchSuggestions, "processing-done"); + EventUtils.synthesizeKey("VK_TAB", {}, inspector.panelWin); + await processingDone; + + info("Press enter and expect a new selection"); + let onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + await onSelect; + + await checkCorrectButton(inspector, ["#iframe-1"]); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + await onSelect; + + await checkCorrectButton(inspector, ["#iframe-2"]); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + await onSelect; + + await checkCorrectButton(inspector, ["#iframe-3"]); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + await onSelect; + + await checkCorrectButton(inspector, ["#iframe-3", "#iframe-4"]); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + await onSelect; + + await checkCorrectButton(inspector, ["#iframe-1"]); +}); + +const checkCorrectButton = async function (inspector, frameSelector) { + const nodeFrontInfo = await getSelectedNodeFrontInfo(inspector); + is(nodeFrontInfo.nodeFront.id, "b1", "The selected node is #b1"); + is( + nodeFrontInfo.nodeFront.tagName.toLowerCase(), + "button", + "The selected node is <button>" + ); + + const iframe = await getNodeFrontInFrames(frameSelector, inspector); + const expectedDocument = (await iframe.walkerFront.children(iframe)).nodes[0]; + + is( + nodeFrontInfo.document, + expectedDocument, + "The selected node is in " + frameSelector + ); +}; +/** + * Gets the currently selected nodefront. It also finds the + * document node which contains the node. + * @param {Object} inspector + * @returns {Object} + * nodeFront - The currently selected nodeFront + * document - The document which contains the node. + * + */ +async function getSelectedNodeFrontInfo(inspector) { + const { selection, commands } = inspector; + + const nodeFront = selection.nodeFront; + const inspectors = await commands.inspectorCommand.getAllInspectorFronts(); + + for (let i = 0; i < inspectors.length; i++) { + const inspectorFront = inspectors[i]; + if (inspectorFront.walker == nodeFront.walkerFront) { + const document = await inspectorFront.walker.document(nodeFront); + return { nodeFront, document }; + } + } + return null; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-06.js b/devtools/client/inspector/test/browser_inspector_search-06.js new file mode 100644 index 0000000000..96dab6dd2b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-06.js @@ -0,0 +1,103 @@ +/* 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/. */ + +"use strict"; + +// Check that searching again for nodes after they are removed or added from the +// DOM works correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Searching for test node #d1"); + await focusSearchBoxUsingShortcut(inspector.panelWin); + await synthesizeKeys(["#", "d", "1", "VK_RETURN"], inspector); + + await inspector.search.once("search-result"); + assertHasResult(inspector, true); + + info("Removing node #d1"); + // Expect an inspector-updated event here, because removing #d1 causes the + // breadcrumbs to update (since #d1 is displayed in it). + const onUpdated = inspector.once("inspector-updated"); + await mutatePage(inspector, () => + content.document.getElementById("d1").remove() + ); + await onUpdated; + + info("Pressing return button to search again for node #d1."); + await synthesizeKeys("VK_RETURN", inspector); + + await inspector.search.once("search-result"); + assertHasResult(inspector, false); + + info("Emptying the field and searching for a node that doesn't exist: #d3"); + const keys = [ + "VK_BACK_SPACE", + "VK_BACK_SPACE", + "VK_BACK_SPACE", + "#", + "d", + "3", + "VK_RETURN", + ]; + await synthesizeKeys(keys, inspector); + + await inspector.search.once("search-result"); + assertHasResult(inspector, false); + + info("Create the #d3 node in the page"); + // No need to expect an inspector-updated event here, Creating #d3 isn't going + // to update the breadcrumbs in any ways. + await mutatePage(inspector, () => + content.document + .getElementById("d2") + .insertAdjacentHTML("afterend", "<div id=d3></div>") + ); + + info("Pressing return button to search again for node #d3."); + await synthesizeKeys("VK_RETURN", inspector); + + await inspector.search.once("search-result"); + assertHasResult(inspector, true); + + // Catch-all event for remaining server requests when searching for the new + // node. + await inspector.once("inspector-updated"); +}); + +async function synthesizeKeys(keys, inspector) { + if (typeof keys === "string") { + keys = [keys]; + } + + for (const key of keys) { + info("Synthesizing key " + key + " in the search box"); + const eventHandled = once(inspector.searchBox, "keypress", true); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await eventHandled; + info("Waiting for the search query to complete"); + await onSearchProcessingDone; + } +} + +function assertHasResult(inspector, expectResult) { + is( + inspector.searchBox.parentNode.classList.contains( + "devtools-searchbox-no-match" + ), + !expectResult, + "There are" + (expectResult ? "" : " no") + " search results" + ); +} + +async function mutatePage(inspector, mutationFn) { + const onMutation = inspector.once("markupmutation"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], mutationFn); + await onMutation; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-07.js b/devtools/client/inspector/test/browser_inspector_search-07.js new file mode 100644 index 0000000000..e112fca944 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-07.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that searching for classes on SVG elements does work (see bug 1219920). + +const TEST_URL = URL_ROOT + "doc_inspector_search-svg.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +const TEST_DATA = [ + { + key: "c", + suggestions: ["circle", "clipPath", ".class1", ".class2"], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: ".", + suggestions: [".class1", ".class2"], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { searchBox } = inspector; + const popup = inspector.searchSuggestions.searchPopup; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + suggestions); + + const command = once(searchBox, "input"); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete and getting the suggestions"); + await onSearchProcessingDone; + const actualSuggestions = popup.getItems(); + + is( + popup.isOpen ? actualSuggestions.length : 0, + suggestions.length, + "There are expected number of suggestions." + ); + + for (let i = 0; i < suggestions.length; i++) { + is( + actualSuggestions[i].label, + suggestions[i], + "The suggestion at " + i + "th index is correct." + ); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-08.js b/devtools/client/inspector/test/browser_inspector_search-08.js new file mode 100644 index 0000000000..64c62d8b1a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-08.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that searching for namespaced elements does work. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath> + <svg:rect x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +const TEST_DATA = [ + { + key: "c", + suggestions: ["circle", "clipPath"], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "s", + suggestions: ["svg"], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + const { searchBox } = inspector; + const popup = inspector.searchSuggestions.searchPopup; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + suggestions.join(", ")); + + const command = once(searchBox, "input"); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete and getting the suggestions"); + await onSearchProcessingDone; + const actualSuggestions = popup.getItems(); + + is( + popup.isOpen ? actualSuggestions.length : 0, + suggestions.length, + "There are expected number of suggestions." + ); + + for (let i = 0; i < suggestions.length; i++) { + is( + actualSuggestions[i].label, + suggestions[i], + "The suggestion at " + i + "th index is correct." + ); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-09.js b/devtools/client/inspector/test/browser_inspector_search-09.js new file mode 100644 index 0000000000..f61d1c9213 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-09.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +requestLongerTimeout(2); + +// Test that searching for XPaths via the search field actually selects the +// matching nodes. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +// The various states of the inspector: [key, id, isTextNode, isValid] +// [ +// what key to press, +// what id should be selected after the keypress, +// is the selected node a text node, +// is the searched text valid selector +// ] +const KEY_STATES = [ + ["/", "b1", false, true], // / + ["*", "b1", false, true], // /* + ["VK_RETURN", "root", false, true], // /* + ["VK_BACK_SPACE", "root", false, true], // / + ["/", "root", false, true], // // + ["d", "root", false, true], // //d + ["i", "root", false, true], // //di + ["v", "root", false, true], // //div + ["VK_RETURN", "d1", false, true], // //div + ["VK_RETURN", "d2", false, true], // //div + ["VK_RETURN", "d1", false, true], // //div + ["VK_BACK_SPACE", "d1", false, true], // //di + ["VK_BACK_SPACE", "d1", false, true], // //d + ["VK_BACK_SPACE", "d1", false, true], // // + ["s", "d1", false, true], // //s + ["p", "d1", false, true], // //sp + ["a", "d1", false, true], // //spa + ["n", "d1", false, true], // //span + ["/", "d1", false, true], // //span/ + ["t", "d1", false, true], // //span/t + ["e", "d1", false, true], // //span/te + ["x", "d1", false, true], // //span/tex + ["t", "d1", false, true], // //span/text + ["(", "d1", false, true], // //span/text( + [")", "d1", false, true], // //span/text() + ["VK_RETURN", "s1", false, true], // //span/text() + ["VK_RETURN", "s2", true, true], // //span/text() +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { searchBox } = inspector; + + await selectNode("#b1", inspector); + await focusSearchBoxUsingShortcut(inspector.panelWin); + + let index = 0; + for (const [key, id, isTextNode, isValid] of KEY_STATES) { + info(index + ": Pressing key " + key + " to get id " + id + "."); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + const onSearchResult = inspector.search.once("search-result"); + EventUtils.synthesizeKey( + key, + { shiftKey: key === "*" }, + inspector.panelWin + ); + + if (key === "VK_RETURN") { + info("Waiting for " + (isValid ? "NO " : "") + "results"); + await onSearchResult; + } + + info("Waiting for search query to complete"); + await onSearchProcessingDone; + + if (isTextNode) { + info( + "Text node of " + + inspector.selection.nodeFront.parentNode.id + + " is selected with text " + + searchBox.value + ); + + is( + inspector.selection.nodeFront.nodeType, + Node.TEXT_NODE, + "Correct node is selected for state " + index + ); + } else { + info( + inspector.selection.nodeFront.id + + " is selected with text " + + searchBox.value + ); + + const nodeFront = await getNodeFront("#" + id, inspector); + is( + inspector.selection.nodeFront, + nodeFront, + "Correct node is selected for state " + index + ); + } + + is( + !searchBox.parentNode.classList.contains("devtools-searchbox-no-match"), + isValid, + "Correct searchbox result state for state " + index + ); + + index++; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-10.js b/devtools/client/inspector/test/browser_inspector_search-10.js new file mode 100644 index 0000000000..2912c9588b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-10.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Bug 1830111 - Test that searching elements while having hidden <iframe> works + +const HTML = ` + <!DOCTYPE html> + <html> + <body> + <!-- The nested iframe, will be a children node of the top iframe + but won't be displayed, not considered as valid children by the inspector --> + <iframe><iframe></iframe></iframe> + <div>after iframe</<div> + <script> + document.querySelector("iframe").appendChild(document.createElement("iframe")); + </script> + </body> + </html> +`; + +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + synthesizeKeys("div", inspector.panelWin); + info("Waiting for search query to complete"); + await onSearchProcessingDone; + + const popup = inspector.searchSuggestions.searchPopup; + const actualSuggestions = popup.getItems().map(item => item.label); + Assert.deepEqual( + actualSuggestions, + ["div"], + "autocomplete popup displays the right suggestions" + ); + + const onSearchResult = inspector.search.once("search-result"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + + info("Waiting for results"); + await onSearchResult; + + const nodeFront = await getNodeFront("div", inspector); + is(inspector.selection.nodeFront, nodeFront, "The <div> element is selected"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-clear.js b/devtools/client/inspector/test/browser_inspector_search-clear.js new file mode 100644 index 0000000000..8056296907 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-clear.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Bug 1295081 Test searchbox clear button's display behavior is correct + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath> + <svg:rect x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +// Type "d" in inspector-searchbox, Enter [Back space] key and check if the +// clear button is shown correctly +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URI); + const { searchBox, searchClearButton } = inspector; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + info("Type d and the clear button will be shown"); + + const command = once(searchBox, "input"); + let onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey("c", {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete and getting the suggestions"); + await onSearchProcessingDone; + + ok( + !searchClearButton.hidden, + "The clear button is shown when some word is in searchBox" + ); + + onSearchProcessingDone = inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete and getting the suggestions"); + await onSearchProcessingDone; + + ok( + searchClearButton.hidden, + "The clear button is hidden when no word is in searchBox" + ); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js new file mode 100644 index 0000000000..dad2ffa0b8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test inspector's markup view search filter context menu works properly. + +const TEST_INPUT = "h1"; +const TEST_URI = "<h1>test filter context menu</h1>"; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { toolbox, inspector } = await openInspector(); + const { searchBox } = inspector; + await selectNode("h1", inspector); + + emptyClipboard(); + + info("Opening context menu"); + const onFocus = once(searchBox, "focus"); + searchBox.focus(); + await onFocus; + + let onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchBox); + await onContextMenuOpen; + + let searchContextMenu = toolbox.getTextBoxContextMenu(); + ok( + searchContextMenu, + "The search filter context menu is loaded in the computed view" + ); + + let cmdUndo = searchContextMenu.querySelector("#editmenu-undo"); + let cmdDelete = searchContextMenu.querySelector("#editmenu-delete"); + let cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll"); + let cmdCut = searchContextMenu.querySelector("#editmenu-cut"); + let cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + let cmdPaste = searchContextMenu.querySelector("#editmenu-paste"); + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled"); + is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled"); + + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + info("Closing context menu"); + let onContextMenuClose = toolbox.once("menu-close"); + searchContextMenu.hidePopup(); + await onContextMenuClose; + + info("Copy text in search field using the context menu"); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + searchBox.setUserInput(TEST_INPUT); + searchBox.select(); + searchBox.focus(); + + // We have to wait for search query to avoid test failure. + info("Waiting for search query to complete and getting the suggestions"); + await onSearchProcessingDone; + + onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchBox); + await onContextMenuOpen; + + searchContextMenu = toolbox.getTextBoxContextMenu(); + + // Simulating a click on cmdCopy will also close the context menu. + onContextMenuClose = toolbox.once("menu-close"); + + cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + await waitForClipboardPromise( + () => searchContextMenu.activateItem(cmdCopy), + TEST_INPUT + ); + + info("Wait for context menu to close"); + await onContextMenuClose; + + info("Reopen context menu and check command properties"); + + onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchBox); + await onContextMenuOpen; + + searchContextMenu = toolbox.getTextBoxContextMenu(); + cmdUndo = searchContextMenu.querySelector("#editmenu-undo"); + cmdDelete = searchContextMenu.querySelector("#editmenu-delete"); + cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll"); + cmdCut = searchContextMenu.querySelector("#editmenu-cut"); + cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + cmdPaste = searchContextMenu.querySelector("#editmenu-paste"); + + is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled"); + is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled"); + is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled"); + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled"); + + const onContextMenuHidden = toolbox.once("menu-close"); + searchContextMenu.hidePopup(); + await onContextMenuHidden; +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-label.js b/devtools/client/inspector/test/browser_inspector_search-label.js new file mode 100644 index 0000000000..4a432c0c01 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-label.js @@ -0,0 +1,33 @@ +/* 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/. */ + +"use strict"; + +// Check that search label updated correctcly based on the search result. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { panelWin, searchResultsLabel } = inspector; + + info("Searching for test node #d1"); + // Expect the label shows 1 result + await focusSearchBoxUsingShortcut(panelWin); + synthesizeKeys("#d1", panelWin); + EventUtils.synthesizeKey("VK_RETURN", {}, panelWin); + + await inspector.search.once("search-result"); + is(searchResultsLabel.textContent, "1 of 1"); + + info("Click the clear button"); + // Expect the label is cleared after clicking the clear button. + + inspector.searchClearButton.click(); + is(searchResultsLabel.textContent, ""); + + // Catch-all event for remaining server requests when searching for the new + // node. + await inspector.once("inspector-updated"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-navigation.js b/devtools/client/inspector/test/browser_inspector_search-navigation.js new file mode 100644 index 0000000000..b53ac320a6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-navigation.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Check that searchbox value is correct when suggestions popup is navigated +// with keyboard. + +// Test data as pairs of [key to press, expected content of searchbox]. +const KEY_STATES = [ + ["d", "d"], + ["i", "di"], + ["v", "div"], + [".", "div."], + ["VK_UP", "div.c1"], + ["VK_DOWN", "div.l1"], + ["VK_BACK_SPACE", "div.l"], + ["VK_TAB", "div.l1"], + [" ", "div.l1 "], + ["VK_UP", "div.l1 div"], + ["VK_UP", "div.l1 span"], + ["VK_UP", "div.l1 div"], + [".", "div.l1 div."], + ["VK_TAB", "div.l1 div.c1"], + ["VK_BACK_SPACE", "div.l1 div.c"], + ["VK_BACK_SPACE", "div.l1 div."], + ["VK_BACK_SPACE", "div.l1 div"], + ["VK_BACK_SPACE", "div.l1 di"], + ["VK_BACK_SPACE", "div.l1 d"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_UP", "div.l1 div"], + ["VK_BACK_SPACE", "div.l1 di"], + ["VK_BACK_SPACE", "div.l1 d"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_UP", "div.l1 div"], + ["VK_UP", "div.l1 span"], + ["VK_UP", "div.l1 div"], + ["VK_TAB", "div.l1 div"], + ["VK_BACK_SPACE", "div.l1 di"], + ["VK_BACK_SPACE", "div.l1 d"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_DOWN", "div.l1 div"], + ["VK_DOWN", "div.l1 span"], + ["VK_BACK_SPACE", "div.l1 spa"], + ["VK_BACK_SPACE", "div.l1 sp"], + ["VK_BACK_SPACE", "div.l1 s"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_BACK_SPACE", "div.l1"], + ["VK_BACK_SPACE", "div.l"], + ["VK_BACK_SPACE", "div."], + ["VK_BACK_SPACE", "div"], + ["VK_BACK_SPACE", "di"], + ["VK_BACK_SPACE", "d"], + ["VK_BACK_SPACE", ""], +]; + +const TEST_URL = URL_ROOT + "doc_inspector_search-suggestions.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const [key, query] of KEY_STATES) { + info("Pressing key " + key + " to get searchbox value as " + query); + + const done = inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + + info("Waiting for search query to complete"); + await done; + + is(inspector.searchBox.value, query, "The searchbox value is correct"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-reserved.js b/devtools/client/inspector/test/browser_inspector_search-reserved.js new file mode 100644 index 0000000000..a4acc469bc --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-reserved.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing searching for ids and classes that contain reserved characters. +const TEST_URL = URL_ROOT + "doc_inspector_search-reserved.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +const TEST_DATA = [ + { + key: "#", + suggestions: [{ label: "#d1\\.d2" }], + }, + { + key: "d", + suggestions: [{ label: "#d1\\.d2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "#d1\\.d2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: ".", + suggestions: [{ label: ".c1\\.c2" }], + }, + { + key: "c", + suggestions: [{ label: ".c1\\.c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: ".c1\\.c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "d", + suggestions: [{ label: "div" }, { label: "#d1\\.d2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "c", + suggestions: [{ label: ".c1\\.c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [], + }, + { + key: "b", + suggestions: [{ label: "body" }], + }, + { + key: "o", + suggestions: [{ label: "body" }], + }, + { + key: "d", + suggestions: [{ label: "body" }], + }, + { + key: "y", + suggestions: [], + }, + { + key: " ", + suggestions: [{ label: "body div" }], + }, + { + key: ".", + suggestions: [{ label: "body .c1\\.c2" }], + }, + { + key: "VK_BACK_SPACE", + suggestions: [{ label: "body div" }], + }, + { + key: "#", + suggestions: [{ label: "body #d1\\.d2" }], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const searchBox = inspector.searchBox; + const popup = inspector.searchSuggestions.searchPopup; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + const command = once(searchBox, "input"); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await command; + + info("Waiting for search query to complete"); + await onSearchProcessingDone; + + info( + "Query completed. Performing checks for input '" + searchBox.value + "'" + ); + const actualSuggestions = popup.getItems(); + + is( + popup.isOpen ? actualSuggestions.length : 0, + suggestions.length, + "There are expected number of suggestions." + ); + + for (let i = 0; i < suggestions.length; i++) { + is( + suggestions[i].label, + actualSuggestions[i].label, + "The suggestion at " + i + "th index is correct." + ); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions.map(s => "'" + s.label + "'").join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-selection.js b/devtools/client/inspector/test/browser_inspector_search-selection.js new file mode 100644 index 0000000000..bdc65945af --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-selection.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing navigation between nodes in search results + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + + info("Focus the search box"); + await focusSearchBoxUsingShortcut(inspector.panelWin); + + info("Enter body > p to search"); + const searchText = "body > p"; + // EventUtils.sendString will trigger multiple updates, so wait until the final one. + const processingDone = new Promise(resolve => { + const off = inspector.searchSuggestions.on("processing-done", data => { + if (data.query == searchText) { + resolve(); + off(); + } + }); + }); + EventUtils.sendString(searchText, inspector.panelWin); + + info("Wait for search query to complete"); + await processingDone; + + let msg = "Press enter and expect a new selection"; + await sendKeyAndCheck(inspector, msg, "VK_RETURN", {}, "#p1"); + + msg = "Press enter to cycle through multiple nodes"; + await sendKeyAndCheck(inspector, msg, "VK_RETURN", {}, "#p2"); + + msg = "Press shift-enter to select the previous node"; + await sendKeyAndCheck(inspector, msg, "VK_RETURN", { shiftKey: true }, "#p1"); + + if (AppConstants.platform === "macosx") { + msg = "Press meta-g to cycle through multiple nodes"; + await sendKeyAndCheck(inspector, msg, "VK_G", { metaKey: true }, "#p2"); + + msg = "Press shift+meta-g to select the previous node"; + await sendKeyAndCheck( + inspector, + msg, + "VK_G", + { metaKey: true, shiftKey: true }, + "#p1" + ); + } else { + msg = "Press ctrl-g to cycle through multiple nodes"; + await sendKeyAndCheck(inspector, msg, "VK_G", { ctrlKey: true }, "#p2"); + + msg = "Press shift+ctrl-g to select the previous node"; + await sendKeyAndCheck( + inspector, + msg, + "VK_G", + { ctrlKey: true, shiftKey: true }, + "#p1" + ); + } +}); + +const sendKeyAndCheck = async function ( + inspector, + description, + key, + modifiers, + expectedId +) { + info(description); + const onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey(key, modifiers, inspector.panelWin); + await onSelect; + + const selectedNode = inspector.selection.nodeFront; + info(selectedNode.id + " is selected with text " + inspector.searchBox.value); + const targetNode = await getNodeFront(expectedId, inspector); + is(selectedNode, targetNode, "Correct node " + expectedId + " is selected"); +}; diff --git a/devtools/client/inspector/test/browser_inspector_search-sidebar.js b/devtools/client/inspector/test/browser_inspector_search-sidebar.js new file mode 100644 index 0000000000..06370a6788 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-sidebar.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that depending where the user last clicked in the inspector, the right search +// field is focused when ctrl+F is pressed. + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8,Search!" + ); + + info("Check that by default, the inspector search field gets focused"); + pressCtrlF(); + isInInspectorSearchBox(inspector); + + info("Click somewhere in the rule-view"); + moveFocusInRuleView(inspector); + + info("Check that the rule-view search field gets focused"); + pressCtrlF(); + isInRuleViewSearchBox(inspector); + + info("Click in the inspector again"); + await clickContainer("head", inspector); + + info( + "Check that now we're back in the inspector, its search field gets focused" + ); + pressCtrlF(); + isInInspectorSearchBox(inspector); + + info("Switch to the computed view, and click somewhere inside it"); + selectComputedView(inspector); + clickInComputedView(inspector); + + info("Check that the computed-view search field gets focused"); + pressCtrlF(); + isInComputedViewSearchBox(inspector); + + info("Click in the inspector yet again"); + await clickContainer("body", inspector); + + info( + "We're back in the inspector again, check the inspector search field focuses" + ); + pressCtrlF(); + isInInspectorSearchBox(inspector); +}); + +function pressCtrlF() { + EventUtils.synthesizeKey("f", { accelKey: true }); +} + +function moveFocusInRuleView(inspector) { + // Only focus an element in the view so this has no unintended effects. + // Put the focus on the `element` closing bracket, which should always be visible + // and is focusable as it's a button. + inspector.panelDoc + .querySelector("#sidebar-panel-ruleview .ruleview-ruleclose") + .focus(); +} + +function clickInComputedView(inspector) { + const el = inspector.panelDoc.querySelector("#sidebar-panel-computedview"); + EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView); +} + +function isInInspectorSearchBox(inspector) { + // Focus ends up in an anonymous child of the XUL textbox. + ok( + inspector.panelDoc.activeElement.closest("#inspector-searchbox"), + "The inspector search field is focused when ctrl+F is pressed" + ); +} + +function isInRuleViewSearchBox(inspector) { + is( + inspector.panelDoc.activeElement, + inspector.getPanel("ruleview").view.searchField, + "The rule-view search field is focused when ctrl+F is pressed" + ); +} + +function isInComputedViewSearchBox(inspector) { + is( + inspector.panelDoc.activeElement, + inspector.getPanel("computedview").computedView.searchField, + "The computed-view search field is focused when ctrl+F is pressed" + ); +} diff --git a/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js b/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js new file mode 100644 index 0000000000..5360d63298 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the selector-search input proposes ids and classes even when . and +// # is missing, but that this only occurs when the query is one word (no +// selector combination) + +// The various states of the inspector: [key, suggestions array] +// [ +// what key to press, +// suggestions array with count [ +// [suggestion1, count1], [suggestion2] ... +// ] count can be left to represent 1 +// ] +const KEY_STATES = [ + [ + "s", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + [ + "p", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + [ + "a", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + [ + "n", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + [" ", [["span div", 1]]], + // mixed tag/class/id suggestions only work for the first word + ["d", [["span div", 1]]], + ["VK_BACK_SPACE", [["span div", 1]]], + [ + "VK_BACK_SPACE", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + [ + "VK_BACK_SPACE", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + [ + "VK_BACK_SPACE", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + [ + "VK_BACK_SPACE", + [ + ["span", 1], + [".span", 1], + ["#span", 1], + ], + ], + ["VK_BACK_SPACE", []], + // Test that mixed tags, classes and ids are grouped by types, sorted by + // count and alphabetical order + [ + "b", + [ + ["button", 3], + ["body", 1], + [".bc", 3], + [".ba", 1], + [".bb", 1], + ["#ba", 1], + ["#bb", 1], + ["#bc", 1], + ], + ], +]; + +const TEST_URL = `<span class="span" id="span"> + <div class="div" id="div"></div> + </span> + <button class="ba bc" id="bc"></button> + <button class="bb bc" id="bb"></button> + <button class="bc" id="ba"></button>`; + +add_task(async function () { + const { inspector } = await openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURI(TEST_URL) + ); + + const searchBox = inspector.panelWin.document.getElementById( + "inspector-searchbox" + ); + const popup = inspector.searchSuggestions.searchPopup; + + await focusSearchBoxUsingShortcut(inspector.panelWin); + + for (const [key, expectedSuggestions] of KEY_STATES) { + info( + "pressing key " + + key + + " to get suggestions " + + JSON.stringify(expectedSuggestions) + ); + + const onCommand = once(searchBox, "input", true); + const onSearchProcessingDone = + inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + await onCommand; + + info("Waiting for the suggestions to be retrieved"); + await onSearchProcessingDone; + + const actualSuggestions = popup.getItems(); + is( + popup.isOpen ? actualSuggestions.length : 0, + expectedSuggestions.length, + "There are expected number of suggestions" + ); + + for (let i = 0; i < expectedSuggestions.length; i++) { + is( + expectedSuggestions[i][0], + actualSuggestions[i].label, + "The suggestion at " + i + "th index is correct." + ); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search_keyboard_shortcut_conflict.js b/devtools/client/inspector/test/browser_inspector_search_keyboard_shortcut_conflict.js new file mode 100644 index 0000000000..696010fea4 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search_keyboard_shortcut_conflict.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the inspector search shortcut works from the inner iframes of the +// inspector panel (eg markup-view iframe) and that shortcuts triggered by other +// panels are not consumed by the inspector. +// See Bug 1589617. +add_task(async function () { + const { inspector, toolbox } = await openInspectorForURL( + "data:text/html;charset=utf-8,<span>Test search shortcut conflicts</span>" + ); + const { searchBox } = inspector; + const doc = inspector.panelDoc; + + info("Check that the shortcut works when opening the inspector"); + await focusSearchBoxUsingShortcut(inspector.panelWin); + ok(containsFocus(doc, searchBox), "Focus is in a searchbox"); + + info("Focus the markup view"); + inspector.markup._frame.focus(); + ok(!containsFocus(doc, searchBox), "Focus is no longer in the searchbox"); + + info("Check that the shortcut works from the markup view"); + const focused = once(searchBox, "focus"); + synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key")); + await focused; + ok(containsFocus(doc, searchBox), "Focus is in the searchbox again"); + + // We focus the markup view again to check if using the shortcut from the + // webconsole will focus the inspector searchbox unintentionally. + inspector.markup._frame.focus(); + ok(!containsFocus(doc, searchBox), "Focus is no longer in the searchbox"); + + info("Switch to webconsole"); + await toolbox.selectTool("webconsole"); + const hud = toolbox.getCurrentPanel().hud; + const consoleSearchBox = hud.ui.outputNode.querySelector( + ".devtools-searchbox input" + ); + + info("Check that the console search shortcut works"); + const consoleSearchFocused = once(consoleSearchBox, "focus"); + + // Note: we expect the console and inspector to share the same shortcut. + // If they diverge, the test will need to be updated. + synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key")); + await consoleSearchFocused; + const consoleDoc = hud.ui.outputNode.ownerDocument; + ok( + containsFocus(consoleDoc, consoleSearchBox), + "Focus is in the console searchbox" + ); + + info("Switch back to the inspector"); + await toolbox.selectTool("inspector"); + ok(!containsFocus(doc, searchBox), "Focus is not in the inspector searchbox"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js new file mode 100644 index 0000000000..b3f4fcdb3d --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test ability to tab to and away from inspector search using keyboard. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * focused {Boolean} flag, indicating if search box contains focus + * keys: {Array} list of keys that include key code and optional + * event data (shiftKey, etc) + * } + * + */ +const TEST_DATA = [ + { + desc: "Move focus to a next focusable element", + focused: false, + keys: [ + { + key: "VK_TAB", + options: {}, + }, + ], + }, + { + desc: "Move focus back to searchbox", + focused: true, + keys: [ + { + key: "VK_TAB", + options: { shiftKey: true }, + }, + ], + }, + { + desc: + "Open popup and then tab away (2 times) to the a next focusable " + + "element", + focused: false, + keys: [ + { + key: "d", + options: {}, + }, + { + key: "VK_TAB", + options: {}, + }, + { + key: "VK_TAB", + options: {}, + }, + ], + }, + { + desc: "Move focus back to searchbox", + focused: true, + keys: [ + { + key: "VK_TAB", + options: { shiftKey: true }, + }, + ], + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(TEST_URL); + const { searchBox } = inspector; + const doc = inspector.panelDoc; + + await selectNode("#b1", inspector); + await focusSearchBoxUsingShortcut(inspector.panelWin); + + // Ensure a searchbox is focused. + ok(containsFocus(doc, searchBox), "Focus is in a searchbox"); + + for (const { desc, focused, keys } of TEST_DATA) { + info(desc); + for (const { key, options } of keys) { + const done = !focused + ? inspector.searchSuggestions.once("processing-done") + : Promise.resolve(); + EventUtils.synthesizeKey(key, options); + await done; + } + is(containsFocus(doc, searchBox), focused, "Focus is set correctly"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_select-last-selected.js b/devtools/client/inspector/test/browser_inspector_select-last-selected.js new file mode 100644 index 0000000000..fe56ff5ae7 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_select-last-selected.js @@ -0,0 +1,75 @@ +/* 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/. */ +"use strict"; + +requestLongerTimeout(2); + +// Checks that the expected default node is selected after a page navigation or +// a reload. +var PAGE_1 = URL_ROOT_SSL + "doc_inspector_select-last-selected-01.html"; +var PAGE_2 = URL_ROOT_SSL + "doc_inspector_select-last-selected-02.html"; + +// An array of test cases with following properties: +// - url: URL to navigate to. If URL == content.location, reload instead. +// - nodeToSelect: a selector for a node to select before navigation. If null, +// whatever is selected stays selected. +// - selectedNode: a selector for a node that is selected after navigation. +var TEST_DATA = [ + { + url: PAGE_1, + nodeToSelect: "#id1", + selectedNode: "#id1", + }, + { + url: PAGE_1, + nodeToSelect: "#id2", + selectedNode: "#id2", + }, + { + url: PAGE_1, + nodeToSelect: "#id3", + selectedNode: "#id3", + }, + { + url: PAGE_1, + nodeToSelect: "#id4", + selectedNode: "#id4", + }, + { + url: PAGE_2, + nodeToSelect: null, + selectedNode: "body", + }, + { + url: PAGE_1, + nodeToSelect: "#id5", + selectedNode: "body", + }, + { + url: PAGE_2, + nodeToSelect: null, + selectedNode: "body", + }, +]; + +add_task(async function () { + const { inspector } = await openInspectorForURL(PAGE_1); + + for (const { url, nodeToSelect, selectedNode } of TEST_DATA) { + if (nodeToSelect) { + info("Selecting node " + nodeToSelect + " before navigation."); + await selectNode(nodeToSelect, inspector); + } + + await navigateTo(url); + + const nodeFront = await getNodeFront(selectedNode, inspector); + ok(nodeFront, "Got expected node front"); + is( + inspector.selection.nodeFront, + nodeFront, + selectedNode + " is selected after navigation." + ); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_sidebarstate.js b/devtools/client/inspector/test/browser_inspector_sidebarstate.js new file mode 100644 index 0000000000..396e78c91f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_sidebarstate.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_URI = + "data:text/html;charset=UTF-8," + + "<h1>browser_inspector_sidebarstate.js</h1>"; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +const TELEMETRY_DATA = [ + { + timestamp: null, + category: "devtools.main", + method: "tool_timer", + object: "layoutview", + value: null, + extra: { + time_open: "", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "tool_timer", + object: "fontinspector", + value: null, + extra: { + time_open: "", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "tool_timer", + object: "compatibilityview", + value: null, + extra: { + time_open: "", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "tool_timer", + object: "computedview", + value: null, + extra: { + time_open: "", + }, + }, +]; + +add_task(async function () { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + let { inspector, toolbox } = await openInspectorForURL(TEST_URI); + + info("Selecting font inspector."); + inspector.sidebar.select("fontinspector"); + + is( + inspector.sidebar.getCurrentTabID(), + "fontinspector", + "Font Inspector is selected" + ); + + info("Selecting compatibility view."); + const onCompatibilityViewInitialized = inspector.once( + "compatibilityview-initialized" + ); + inspector.sidebar.select("compatibilityview"); + await onCompatibilityViewInitialized; + + is( + inspector.sidebar.getCurrentTabID(), + "compatibilityview", + "Compatibility View is selected" + ); + + info("Selecting computed view."); + inspector.sidebar.select("computedview"); + + is( + inspector.sidebar.getCurrentTabID(), + "computedview", + "Computed View is selected" + ); + + info("Closing inspector."); + await toolbox.destroy(); + + info("Re-opening inspector."); + inspector = (await openInspector()).inspector; + + if (!inspector.sidebar.getCurrentTabID()) { + info("Default sidebar still to be selected, adding select listener."); + await inspector.sidebar.once("select"); + } + + is( + inspector.sidebar.getCurrentTabID(), + "computedview", + "Computed view is selected by default." + ); + + checkTelemetryResults(); +}); + +function checkTelemetryResults() { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter( + event => event[1] === "devtools.main" && event[2] === "tool_timer" + ); + + for (const i in TELEMETRY_DATA) { + const [timestamp, category, method, object, value, extra] = events[i]; + const expected = TELEMETRY_DATA[i]; + + // ignore timestamp + ok(timestamp > 0, "timestamp is greater than 0"); + ok(extra.time_open > 0, "time_open is greater than 0"); + is(category, expected.category, "category is correct"); + is(method, expected.method, "method is correct"); + is(object, expected.object, "object is correct"); + is(value, expected.value, "value is correct"); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_startup.js b/devtools/client/inspector/test/browser_inspector_startup.js new file mode 100644 index 0000000000..7953b5c473 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_startup.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the inspector loads early without waiting for load events. + +const server = createTestHTTPServer(); + +// Register a slow image handler so we can simulate a long time between +// a reload and the load event firing. +server.registerContentType("gif", "image/gif"); +function onPageResourceRequest() { + return new Promise(done => { + server.registerPathHandler("/slow.gif", function (metadata, response) { + info("Image has been requested"); + response.processAsync(); + done(response); + }); + }); +} + +// Test page load events. +const TEST_URL = + "data:text/html," + + "<!DOCTYPE html>" + + "<head><meta charset='utf-8' /></head>" + + "<body>" + + "<p>Page loading slowly</p>" + + "<img src='http://localhost:" + + server.identity.primaryPort + + "/slow.gif' />" + + "</body>" + + "</html>"; + +add_task(async function () { + const { inspector, tab } = await openInspectorForURL("about:blank"); + + const domContentLoaded = waitForLinkedBrowserEvent(tab, "DOMContentLoaded"); + const pageLoaded = waitForLinkedBrowserEvent(tab, "load"); + + const markupLoaded = inspector.once("markuploaded"); + const onRequest = onPageResourceRequest(); + + info("Navigate to the slow loading page"); + const target = inspector.toolbox.target; + await target.navigateTo({ url: TEST_URL }); + + info("Wait for request made to the image"); + const response = await onRequest; + + // The request made to the image shouldn't block the DOMContentLoaded event + info("Wait for DOMContentLoaded"); + await domContentLoaded; + + // Nor does it prevent the inspector from loading + info("Wait for markup-loaded"); + await markupLoaded; + + ok(inspector.markup, "There is a markup view"); + is(inspector.markup._elt.children.length, 1, "The markup view is rendering"); + is( + await contentReadyState(tab), + "interactive", + "Page is still loading but the inspector is ready" + ); + + // Ends page load by unblocking the image request + response.finish(); + + // We should then receive the page load event + info("Wait for load"); + await pageLoaded; +}); + +function waitForLinkedBrowserEvent(tab, event) { + return BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, event, true); +} + +function contentReadyState(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], function () { + return content.document.readyState; + }); +} diff --git a/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js b/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js new file mode 100644 index 0000000000..171f4f8e7f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Testing that clicking the pick button switches the toolbox to the inspector +// panel. + +const TEST_URI = + "data:text/html;charset=UTF-8,<!DOCTYPE html><script>console.log(`hello`)</script><p>Switch to inspector on pick</p>"; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +const DATA = [ + { + timestamp: 3562, + category: "devtools.main", + method: "enter", + object: "webconsole", + extra: { + host: "bottom", + start_state: "initial_panel", + panel_name: "webconsole", + cold: "true", + message_count: "1", + width: "1300", + }, + }, + { + timestamp: 3671, + category: "devtools.main", + method: "exit", + object: "webconsole", + extra: { + host: "bottom", + width: "1300", + panel_name: "webconsole", + next_panel: "inspector", + reason: "inspect_dom", + }, + }, + { + timestamp: 3671, + category: "devtools.main", + method: "enter", + object: "inspector", + extra: { + host: "bottom", + start_state: "inspect_dom", + panel_name: "inspector", + cold: "true", + width: "1300", + }, + }, +]; + +add_task(async function () { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const tab = await addTab(TEST_URI); + const toolbox = await openToolbox(tab); + + await startPickerAndAssertSwitchToInspector(toolbox); + + info("Stopping element picker."); + await toolbox.nodePicker.stop({ canceled: true }); + + checkResults(); +}); + +async function openToolbox(tab) { + info("Opening webconsole."); + return gDevTools.showToolboxForTab(tab, { toolId: "webconsole" }); +} + +async function startPickerAndAssertSwitchToInspector(toolbox) { + info("Clicking element picker button."); + const pickButton = toolbox.doc.querySelector("#command-button-pick"); + pickButton.click(); + + info("Waiting for inspector to be selected."); + await toolbox.once("inspector-selected"); + is(toolbox.currentToolId, "inspector", "Switched to the inspector"); +} + +function checkResults() { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter( + event => + (event[1] === "devtools.main" && event[2] === "enter") || + event[2] === "exit" + ); + + for (const i in DATA) { + const [timestamp, category, method, object, value, extra] = events[i]; + const expected = DATA[i]; + + // ignore timestamp + Assert.greater(timestamp, 0, "timestamp is greater than 0"); + is(category, expected.category, "category is correct"); + is(method, expected.method, "method is correct"); + is(object, expected.object, "object is correct"); + is(value, null, "value is correct"); + ok(extra.width > 0, "width is greater than 0"); + + checkExtra("host", extra, expected); + checkExtra("start_state", extra, expected); + checkExtra("reason", extra, expected); + checkExtra("panel_name", extra, expected); + checkExtra("next_panel", extra, expected); + checkExtra("message_count", extra, expected); + checkExtra("cold", extra, expected); + } +} + +function checkExtra(propName, extra, expected) { + if (extra[propName]) { + is(extra[propName], expected.extra[propName], `${propName} is correct`); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_textbox-menu.js b/devtools/client/inspector/test/browser_inspector_textbox-menu.js new file mode 100644 index 0000000000..a95fcc02b8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_textbox-menu.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that when right-clicking on various text boxes throughout the inspector does use +// the toolbox's context menu (copy/cut/paste/selectAll/Undo). + +add_task(async function () { + await addTab(`data:text/html;charset=utf-8, + <style>h1 { color: red; }</style> + <h1 id="title">textbox context menu test</h1>`); + const { toolbox, inspector } = await openInspector(); + await selectNode("h1", inspector); + + info("Testing the markup-view tagname"); + const container = await focusNode("h1", inspector); + const tag = container.editor.tag; + tag.focus(); + EventUtils.sendKey("return", inspector.panelWin); + await checkTextBox(inspector.markup.doc.activeElement, toolbox); + + info("Testing the markup-view attribute"); + EventUtils.sendKey("tab", inspector.panelWin); + await checkTextBox(inspector.markup.doc.activeElement, toolbox); + + info("Testing the markup-view new attribute"); + // It takes 2 tabs to focus the newAttr field, the first one just moves the cursor to + // the end of the field. + EventUtils.sendKey("tab", inspector.panelWin); + EventUtils.sendKey("tab", inspector.panelWin); + await checkTextBox(inspector.markup.doc.activeElement, toolbox); + + info("Testing the markup-view textcontent"); + EventUtils.sendKey("tab", inspector.panelWin); + await checkTextBox(inspector.markup.doc.activeElement, toolbox); + // Blur this last markup-view field, since we're moving on to the rule-view next. + EventUtils.sendKey("escape", inspector.panelWin); + + info("Testing the rule-view selector"); + const ruleView = inspector.getPanel("ruleview").view; + const cssRuleEditor = getRuleViewRuleEditor(ruleView, 1); + EventUtils.synthesizeMouseAtCenter( + cssRuleEditor.selectorText, + {}, + inspector.panelWin + ); + await checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Testing the rule-view property name"); + EventUtils.sendKey("tab", inspector.panelWin); + await checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Testing the rule-view property value"); + EventUtils.sendKey("tab", inspector.panelWin); + await checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Testing the rule-view new property"); + // Tabbing out of the value field triggers a ruleview-changed event that we need to wait + // for. + const onRuleViewChanged = once(ruleView, "ruleview-changed"); + EventUtils.sendKey("tab", inspector.panelWin); + await onRuleViewChanged; + await checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Switching to the layout-view"); + const onBoxModelUpdated = inspector.once("boxmodel-view-updated"); + selectLayoutView(inspector); + await onBoxModelUpdated; + + info("Testing the box-model region"); + const margin = inspector.panelDoc.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + EventUtils.synthesizeMouseAtCenter(margin, {}, inspector.panelWin); + await checkTextBox(inspector.panelDoc.activeElement, toolbox); + + // Move the mouse out of the box-model region to avoid triggering the box model + // highlighter. + EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.panelWin); +}); + +async function checkTextBox(textBox, toolbox) { + let textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(!textboxContextMenu, "The menu is closed"); + + info( + "Simulating context click on the textbox and expecting the menu to open" + ); + const onContextMenu = toolbox.once("menu-open"); + synthesizeContextMenuEvent(textBox); + await onContextMenu; + + textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(textboxContextMenu, "The menu is now visible"); + + info("Closing the menu"); + const onContextMenuHidden = toolbox.once("menu-close"); + textboxContextMenu.hidePopup(); + await onContextMenuHidden; + + textboxContextMenu = toolbox.getTextBoxContextMenu(); + ok(!textboxContextMenu, "The menu is closed again"); +} diff --git a/devtools/client/inspector/test/browser_inspector_textbox-menu_reopen_toolbox.js b/devtools/client/inspector/test/browser_inspector_textbox-menu_reopen_toolbox.js new file mode 100644 index 0000000000..ea9025a5aa --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_textbox-menu_reopen_toolbox.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that textbox context menu elements are still displayed correctly after reopening +// the toolbox. Fixes https://bugzilla.mozilla.org/show_bug.cgi?id=1510182. + +add_task(async function () { + await addTab(`data:text/html;charset=utf-8,<div>test</div>`); + + info("Testing the textbox context menu a first time"); + const { toolbox, inspector } = await openInspector(); + await checkContextMenuOnSearchbox(inspector, toolbox); + + // Destroy the toolbox and try the context menu again with a new toolbox. + await toolbox.destroy(); + + info("Testing the textbox context menu after reopening the toolbox"); + const { toolbox: newToolbox, inspector: newInspector } = + await openInspector(); + await checkContextMenuOnSearchbox(newInspector, newToolbox); +}); + +async function checkContextMenuOnSearchbox(inspector, toolbox) { + // The same context menu is used for any text input. + // Here we use the inspector searchbox for this test because it is always available. + const searchbox = inspector.panelDoc.getElementById("inspector-searchbox"); + + info( + "Simulating context click on the textbox and expecting the menu to open" + ); + const onContextMenu = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchbox); + await onContextMenu; + + const textboxContextMenu = toolbox.getTextBoxContextMenu(); + info("Wait until menu items are rendered"); + const pasteElement = textboxContextMenu.querySelector("#editmenu-paste"); + await waitUntil(() => !!pasteElement.getAttribute("label")); + + is( + pasteElement.getAttribute("label"), + "Paste", + "Paste is visible and localized" + ); + + info("Closing the menu"); + const onContextMenuHidden = toolbox.once("menu-close"); + textboxContextMenu.hidePopup(); + await onContextMenuHidden; +} diff --git a/devtools/client/inspector/test/browser_inspector_use-in-console-conflict.js b/devtools/client/inspector/test/browser_inspector_use-in-console-conflict.js new file mode 100644 index 0000000000..8c19a56494 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_use-in-console-conflict.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests "Use in Console" menu item with conflicting binding in the web content. + +const TEST_URL = `data:text/html;charset=utf-8,<!DOCTYPE html> +<p id="console-var">Paragraph for testing console variables</p> +<script> + /* Verify that the conflicting binding on user code doesn't break the + * functionality. */ + var $0 = "user-defined variable"; +</script>`; + +add_task(async function () { + // Disable eager evaluation to avoid intermittent failures due to pending + // requests to evaluateJSAsync. + await pushPref("devtools.webconsole.input.eagerEvaluation", false); + + const { inspector, toolbox } = await openInspectorForURL(TEST_URL); + + info("Testing 'Use in Console' menu item."); + + await selectNode("#console-var", inspector); + const container = await getContainerForSelector("#console-var", inspector); + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: container.tagLine, + }); + const menuItem = allMenuItems.find(i => i.id === "node-menu-useinconsole"); + const onConsoleVarReady = inspector.once("console-var-ready"); + + menuItem.click(); + + await onConsoleVarReady; + + const hud = toolbox.getPanel("webconsole").hud; + + const getConsoleResults = () => hud.ui.outputNode.querySelectorAll(".result"); + + is(hud.getInputValue(), "temp0", "first console variable is named temp0"); + hud.ui.wrapper.dispatchEvaluateExpression(); + + await waitUntil(() => getConsoleResults().length === 1); + const result = getConsoleResults()[0]; + ok( + result.textContent.includes('<p id="console-var">'), + "variable temp0 references correct node" + ); + + hud.ui.wrapper.dispatchClearHistory(); +}); diff --git a/devtools/client/inspector/test/doc_inspector_add_node.html b/devtools/client/inspector/test/doc_inspector_add_node.html new file mode 100644 index 0000000000..e1205a79ba --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_add_node.html @@ -0,0 +1,22 @@ +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Add elements tests</title>
+ <style>
+ body::before {
+ content: "pseudo-element";
+ }
+ </style>
+</head>
+<body>
+ <div id="foo"></div>
+ <svg>
+ <rect x="0" y="0" width="100" height="50"></rect>
+ </svg>
+ <div id="bar">
+ <div id="baz"></div>
+ </div>
+ <iframe src="data:text/html;charset=utf-8,Test iframe content"></iframe>
+</body>
+</html> diff --git a/devtools/client/inspector/test/doc_inspector_breadcrumbs.html b/devtools/client/inspector/test/doc_inspector_breadcrumbs.html new file mode 100644 index 0000000000..fee0636112 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_breadcrumbs.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<html> + <head> + <style> + div { + min-height: 10px; min-width: 10px; + border: 1px solid red; + margin: 10px; + } + #pseudo-container::before { + content: 'before'; + } + #pseudo-container::after { + content: 'after'; + } + </style> + </head> + <body> + <article id="i1"> + <div id="i11"> + <div id="i111"> + <div id="i1111"> + </div> + </div> + </div> + </article> + <article id="i2"> + <div id="i21"> + <div id="i211"> + <div id="i2111"> + </div> + </div> + </div> + <div id="i22"> + <div id="i221"> + </div> + <div id="i222"> + <div id="i2221"> + <div id="i22211"> + </div> + </div> + </div> + </div> + </article> + <article id="i3"> + <link id="i31" /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + </article> + <div id='pseudo-container'></div> + <!-- This is a comment node --> + <svg id="vector" viewBox="0 0 10 10"> + <clipPath id="clip"> + <rect id="rectangle" x="0" y="0" width="10" height="5"></rect> + </clipPath> + <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle> + </svg> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html b/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html new file mode 100644 index 0000000000..862f324079 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html @@ -0,0 +1,22 @@ +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=windows-1252"> + </head> + <body> + <div id="aVeryLongIdToExceedTheBreadcrumbTruncationLimit"> + <div id="anotherVeryLongIdToExceedTheBreadcrumbTruncationLimit"> + <div id="aThirdVeryLongIdToExceedTheTruncationLimit"> + <div id="aFourthOneToExceedTheTruncationLimit"> + <div id="aFifthOneToExceedTheTruncationLimit"> + <div id="aSixthOneToExceedTheTruncationLimit"> + <div id="aSeventhOneToExceedTheTruncationLimit"> + A text node at the end + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_csp.html b/devtools/client/inspector/test/doc_inspector_csp.html new file mode 100644 index 0000000000..fcd320dae1 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_csp.html @@ -0,0 +1,9 @@ +<!DOCTYPE html>
+<html>
+ <head>
+ <title>Inspector CSP Test</title>
+ <link rel="stylesheet" href="style_inspector_csp.css" type="text/css"/>
+ <meta charset="utf-8">
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_csp.html^headers^ b/devtools/client/inspector/test/doc_inspector_csp.html^headers^ new file mode 100644 index 0000000000..3345a82b84 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_csp.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html; charset=UTF-8
+content-security-policy: default-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self';
diff --git a/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html b/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html new file mode 100644 index 0000000000..70edbd9366 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> + +<h1>mop</h1> +<iframe src="data:text/html;charset=utf-8,<!DOCTYPE HTML>%0D%0A<h1>kill me<span>.</span><%2Fh1>"></iframe> diff --git a/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html b/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html new file mode 100644 index 0000000000..0749b064a4 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html @@ -0,0 +1,20 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>node delete - reset selection - test</title> +</head> +<body> + <ul id="deleteChildren"> + <li id="deleteManually">Delete me via the inspector</li> + <li id="selectedAfterDelete">This node is selected after manual delete</li> + <li id="deleteAutomatically">Delete me via javascript</li> + </ul> + <iframe id="deleteIframe" src="data:text/html,%3C!DOCTYPE%20html%3E%3Chtml%20lang%3D%22en%22%3E%3Cbody%3E%3Cp%20id%3D%22deleteInIframe%22%3EDelete my container iframe%3C%2Fp%3E%3C%2Fbody%3E%3C%2Fhtml%3E"></iframe> + <div id="deleteToMakeSingleTextNode"> + 1 + <b id="deleteWithNonElement">Delete me and select the non-element node</b> + 2 + </div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_embed.html b/devtools/client/inspector/test/doc_inspector_embed.html new file mode 100644 index 0000000000..8262fc2934 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_embed.html @@ -0,0 +1,6 @@ +<!doctype html><html><head><meta charset="UTF-8"></head><body>
+<object>
+ <embed src="doc_inspector_menu.html" type="text/html"
+ width="422" height="258"></embed>
+</object>
+</body></html> diff --git a/devtools/client/inspector/test/doc_inspector_eyedropper_disabled.xhtml b/devtools/client/inspector/test/doc_inspector_eyedropper_disabled.xhtml new file mode 100644 index 0000000000..9954b1bac8 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_eyedropper_disabled.xhtml @@ -0,0 +1,3 @@ + <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <box id="box" style="background-color: red;">Hello</box> + </window> diff --git a/devtools/client/inspector/test/doc_inspector_fission_frame_navigation.html b/devtools/client/inspector/test/doc_inspector_fission_frame_navigation.html new file mode 100644 index 0000000000..e454fb0c83 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_fission_frame_navigation.html @@ -0,0 +1,15 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Test remote frame navigation</title> +</head> +<body> + <p>Test remote frame navigation</p> + <div id="root"> + <iframe src='https://example.org/document-builder.sjs?html=<div id=org>org'></iframe> + </div> +</body> +</html>
\ No newline at end of file diff --git a/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html b/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html new file mode 100644 index 0000000000..b2ba0b066f --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + div { + opacity: 0; + height: 0; + background: red; + border-top: 1px solid #888; + transition-property: height, opacity; + transition-duration: 3000ms; + transition-timing-function: ease-in-out, ease-in-out, linear; + } + + div[visible] { + opacity: 1; + height: 200px; + } + </style> +</head> +<body> + <div></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-comments.html b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html new file mode 100644 index 0000000000..3dedc9f369 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html @@ -0,0 +1,19 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Highlighter Test</title> +</head> +<body> + <p></p> + <div id="id1">Visible div 1</div> + <!-- Invisible comment node --> + <div id="id2">Visible div 2</div> + <script type="text/javascript"> + /* Invisible script node */ + </script> + <div id="id3">Visible div 3</div> + <div id="id4" style="display:none;">Invisible div node</div> + Visible text node +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html new file mode 100644 index 0000000000..f05f15deb2 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html @@ -0,0 +1,90 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>geometry highlighter test page</title> + <style type="text/css"> + html, body { + margin: 0; + padding: 0; + } + + .relative-sized-parent { + position: relative; + border: 2px solid black; + border-radius: 25px; + } + .size { + width: 300px; + height: 300px; + } + + .positioned-child { + position: absolute; + background: #f06; + } + .pos-top-left { + top: 30px; + left: 25%; + } + .pos-bottom-right { + bottom: 10em; + right: -10px; + } + + .inline-positioned { + background: yellow; + } + + #absolute-container { + position: absolute; + top: 50px; + left: 400px; + width: 500px; + height: 400px; + border: 1px solid black; + } + + .absolute-all-4 { + position: absolute; + top: 10px; + left: 10px; + bottom: 200px; + right: 300px; + border: 1px solid red; + } + + .relative { + position: relative; + top: 10%; + left: 50%; + height: 10px; + border: 1px solid blue; + } + + .fixed { + position: fixed; + top: 400px; + left: 0; + width: 50px; + height: 50px; + border-radius: 50%; + background: green; + } + </style> +</head> +<body> + <div id="node1" class="relative-sized-parent size"> + <div id="node2" class="positioned-child pos-top-left pos-bottom-right"> + <div id="node3" class="inline-positioned positioned-child pos-top-left" style="width:50px;height:50px;"></div> + </div> + </div> + + <div id="absolute-container"> + <div class="absolute-all-4"></div> + <div class="relative"></div> + </div> + + <div class="fixed"></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html new file mode 100644 index 0000000000..59c3b88571 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html @@ -0,0 +1,120 @@ +<!doctype html><html><head><meta charset="UTF-8"></head><body class="header"> + +<style> +.fixed { position: fixed; top: 40px; right: 20px; margin-top: 20px; background: #ccf; } +.fixed-bottom-right { position: fixed; bottom: 4em; right: 25%; margin: 20px; background: #ccf; } + +#absolute-container { position: relative; height: 150px; margin: 20px; } +.absolute { position: absolute; top: 20px; left: 400px; background: #fcc; } +.absolute-bottom-right { position: absolute; bottom: 20px; right: 50px; background: #fcc; } +.absolute-all-4 { position: absolute; top: 100px; bottom: 10px; left: 20px; right: 700px; background: #fcc; } +.absolute-negative { position: absolute; bottom: -25px; background: #fcc; } +.absolute-width-margin { position: absolute; top: 20px; right: 20px; width: 450px; margin: .3em; padding: 10px; border: 2px solid red; box-sizing: border-box; background: #fcc; } + +.relative { position: relative; top: 10px; left: 10px; background: #cfc;} +.relative-inline { position: relative; top: 10px; left: 10px; display: inline; background: #cfc;} + +.static { position: static; top: 10px; left: 10px; background: #fcf; } +.static-size { position: static; top: 10px; left: 10px; width: 300px; height: 100px; background: #fcf; } + +#sticky-container { + margin: 50px; + height: 400px; + width: 400px; + padding: 40px; + overflow: scroll; +} +#sticky-container dl { + margin: 0; + padding: 24px 0 0 0; +} + +#sticky-container dt { + background: #ffc; + border-bottom: 1px solid #989EA4; + border-top: 1px solid #717D85; + color: #FFF; + font: bold 18px/21px Helvetica, Arial, sans-serif; + margin: 0; + padding: 2px 0 0 12px; + position: sticky; + width: 99%; + top: 0px; +} + +#sticky-container dd { + font: bold 20px/45px Helvetica, Arial, sans-serif; + margin: 0; + padding: 0 0 0 12px; + white-space: nowrap; +} + +#sticky-container dd + dd { + border-top: 1px solid #CCC +} +</style> + +<h1>Positioning playground</h1> +<p>A demo of various positioning schemes: <a href="http://dev.w3.org/csswg/css-position/#pos-sch">http://dev.w3.org/csswg/css-position/#pos-sch</a>.</p> +<p>absolute, static, fixed, relative, sticky</p> + +<h2>Absolute positioning</h2> +<div class="absolute"> + Absolute child with no relative parent +</div> +<div id="absolute-container"> + <div class="absolute"> + Absolute child with a relative parent + </div> + <div class="absolute-bottom-right"> + Absolute child with a relative parent, positioned from the bottom right + </div> + <div class="absolute-all-4"> + Absolute child with a relative parent, with all 4 positions + </div> + <div class="absolute-negative"> + Absolute child with a relative parent, with negative positions + </div> + <div class="absolute-width-margin"> + Absolute child with a relative parent, size, margin + </div> +</div> + +<h2>Relative positioning</h2> +<div id="relative-container"> + <div class="relative"> + Relative child + </div> + <div style="width: 100px;"> + <div class="relative-inline"> + Relative inline child, across multiple lines + </div> + </div> + <div style="position:relative;"> + <div class="relative"> + Relative child, in a positioned parent + </div> + </div> +</div> + +<h2>Fixed positioning</h2> +<div id="fixed-container"> + <div class="fixed"> + Fixed child + </div> + <div class="fixed-bottom-right"> + Fixed child, bottom right + </div> +</div> + +<h2>Static positioning</h2> +<div id="static-container"> + <div class="static"> + Static child with no width/height + </div> + <div class="static-size"> + Static child with width/height + </div> +</div> + +</body></html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter.html b/devtools/client/inspector/test/doc_inspector_highlighter.html new file mode 100644 index 0000000000..376a9c714d --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + div { + position:absolute; + } + + #simple-div { + padding: 5px; + border: 7px solid red; + margin: 9px; + top: 30px; + left: 150px; + } + + #rotated-div { + padding: 5px; + border: 7px solid red; + margin: 9px; + transform: rotate(45deg); + top: 30px; + left: 80px; + } + + #widthHeightZero-div { + top: 30px; + left: 10px; + width: 0; + height: 0; + } + </style> + </head> + <body> + <div id="simple-div">Gort! Klaatu barada nikto!</div> + <div id="rotated-div"></div> + <div id="widthHeightZero-div">Width & height = 0</div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes-percent.html b/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes-percent.html new file mode 100644 index 0000000000..66050e9612 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes-percent.html @@ -0,0 +1,18 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<meta charset="utf-8"> +<style> + html, body { + height: 100%; + margin: 0; + } + + #inset { + /* clip-path gets added with JavaScript */ + width: 100%; + height: 100%; + background: #f06; + } +</style> +<div id="inset"></div> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes.html b/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes.html new file mode 100644 index 0000000000..0c38b8e636 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes.html @@ -0,0 +1,93 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<meta charset="utf-8"> +<style> + html, body { + height: 100%; + margin: 0; + } + .wrapper { + width: 800px; + height: 800px; + background: #f06; + } + #polygon { + clip-path: polygon(0 0%, + 100px 50%, + 200px 0, + 300px 50%, + 400px 0, + 500px 50%, + 600px 0, + 700px 50%, + 800px 0, + 90% 100%, + 50% 60%, + 10% 100%); + } + #circle { + clip-path: circle(25% at 30% 40%); + } + #circle-without-position { + clip-path: circle(25%); + } + #ellipse { + clip-path: ellipse(40% 30% at 25% 30%) content-box; + padding: 20px; + } + #ellipse-padding-box { + clip-path: ellipse(40% 30% at 25% 30%) padding-box; + padding: 20px; + } + #inset { + clip-path: inset(200px 100px 30% 15%); + } + .svg { + width: 800px; + height: 800px; + } + #rect { + clip-path: polygon(0 0, + 100px 50%, + 200px 0, + 300px 50%, + 400px 0, + 500px 50%, + 600px 0, + 700px 50%, + 800px 0, + 90% 100%, + 50% 60%, + 10% 100%); + stroke: red; + stroke-width: 20px; + fill: blue; + } + #polygon-transform { + width: 600px; + height: 600px; + clip-path: polygon(0 0, + 100px 50%, + 200px 0, + 300px 50%, + 400px 0, + 500px 50%, + 600px 0, + 700px 50%, + 800px 0, + 90% 100%, + 50% 60%, + 10% 100%); + } +</style> +<div class="wrapper" id="polygon"></div> +<div class="wrapper" id="circle"></div> +<div class="wrapper" id="circle-without-position"></div> +<div class="wrapper" id="ellipse"></div> +<div class="wrapper" id="ellipse-padding-box"></div> +<div class="wrapper" id="inset"></div> +<div class="wrapper" id="polygon-transform"></div> +<svg class="svg"> + <rect id="rect" x="10" y="10" width="700" height="700"></rect> +</svg> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes_iframe.html b/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes_iframe.html new file mode 100644 index 0000000000..ad32ef09b5 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_cssshapes_iframe.html @@ -0,0 +1,11 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<meta charset="utf-8"> +<style> + html, body { + height: 100%; + margin: 10px; + } +</style> +<iframe id="frame" src="doc_inspector_highlighter_cssshapes.html"></iframe> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html b/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html new file mode 100644 index 0000000000..cfa2761d73 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>css transform highlighter test</title> + <style type="text/css"> + #test-node { + position: absolute; + top: 0; + left: 0; + + width: 300px; + height: 300px; + + transform: rotate(90deg) skew(13deg) scale(.8) translateX(50px); + transform-origin: 50%; + + background: linear-gradient(green, yellow); + } + </style> +</head> +<body> + <div id="test-node"></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_custom_element.xhtml b/devtools/client/inspector/test/doc_inspector_highlighter_custom_element.xhtml new file mode 100644 index 0000000000..83d6bc1735 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_custom_element.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test that the picker works correctly with custom element anonymous nodes</title> +</head> +<body> +<template id="template"><div class="custom-element-anon">Anonymous</div></template> +<custom-element id="custom-element" style="background:red; display: block;"/> +<script> + "use strict"; + customElements.define("custom-element", class extends HTMLElement { + constructor() { + super(); + const template = document.getElementById("template"); + this.attachShadow({mode: "open"}) + .appendChild(template.content.cloneNode(true)); + } + }); +</script> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_dom.html b/devtools/client/inspector/test/doc_inspector_highlighter_dom.html new file mode 100644 index 0000000000..dc04828e41 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_dom.html @@ -0,0 +1,20 @@ +<!DOCTYPE html>
+<html>
+<body>
+
+<p>Hello World!</p>
+
+<div id="complex-div">
+ <div id="simple-div1">
+ <p id="useless-para">The DOM is very useful! <em>#useless-para</em></p>
+ <p id="useful-para">This example is <b id="bold">really</b> useful. <em>#useful-para</em></p>
+ </div>
+
+ <div id="simple-div2">
+ <p id="another">This is another node. You won't reach this in my test.</p>
+ <p id="ahoy">Ahoy! How you doin' Capn'? <em>#ahoy</em></p>
+ </div>
+</div>
+
+</body>
+</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_inline.html b/devtools/client/inspector/test/doc_inspector_highlighter_inline.html new file mode 100644 index 0000000000..e1aa5bb1f7 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_inline.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + html { + height: 100%; + background: #eee; + } + body { + margin: 0 auto; + padding: 1em; + box-sizing: border-box; + width: 500px; + height: 100%; + background: white; + font-family: Arial; + font-size: 15px; + line-height: 40px; + } + p span { + padding: 5px 0; + margin: 0 5px; + border: 5px solid #eee; + } + </style> + </head> + <body> + <h1>Lorem Ipsum</h1> + <h2>Lorem ipsum <em>dolor sit amet</em></h2> + <p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed varius, nisl eget semper maximus, dui tellus tempor leo, at pharetra eros tortor sed odio. Nullam sagittis ex nec mi sagittis pulvinar. Pellentesque dapibus feugiat fermentum. Curabitur lacinia quis enim et tristique. Aliquam in semper massa. In ac vulputate nunc, at rutrum neque. Fusce condimentum, tellus quis placerat imperdiet, dolor tortor mattis erat, nec luctus magna diam pharetra mauris.</span></p> + <div dir="rtl"> + <span><span></span>some ltr text in an rtl container</span> + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_rect.html b/devtools/client/inspector/test/doc_inspector_highlighter_rect.html new file mode 100644 index 0000000000..4d23d52fd2 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_rect.html @@ -0,0 +1,22 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>rect highlighter parent test page</title> + <style type="text/css"> + body { + margin: 50px; + border: 10px solid red; + } + + iframe { + border: 10px solid yellow; + padding: 0; + margin: 50px; + } + </style> +</head> +<body> + <iframe src="doc_inspector_highlighter_rect_iframe.html"></iframe> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html b/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html new file mode 100644 index 0000000000..d59050f69b --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html @@ -0,0 +1,15 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>rect highlighter child test page</title> + <style type="text/css"> + body { + margin: 0; + } + </style> +</head> +<body> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_scroll.html b/devtools/client/inspector/test/doc_inspector_highlighter_scroll.html new file mode 100644 index 0000000000..60252f1b7a --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_scroll.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + p { + height: 200vh; + } + </style> + </head> + <body> + <p>Bug 1382341 - test page reload and scroll position</p> + <a>An element anchor, used to scroll the page</a> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar.html b/devtools/client/inspector/test/doc_inspector_infobar.html new file mode 100644 index 0000000000..137b3487f7 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + width: 100%; + height: 100%; + } + + div { + position: absolute; + height: 100px; + width: 500px; + } + + #bottom { + bottom: 0px; + } + + #vertical { + height: 100%; + } + + #farbottom { + top: 2000px; + background: red; + } + + #abovetop { + top: -123px; + }"; + </style> +</head> +<body> + <div id="abovetop"></div> + <div id="vertical"></div> + <div id="top" class="class1 class2"></div> + <div id="bottom"></div> + <div id="farbottom"></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_01.html b/devtools/client/inspector/test/doc_inspector_infobar_01.html new file mode 100644 index 0000000000..a0c42ee38d --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_01.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + width: 100%; + height: 100%; + } + div { + position: absolute; + height: 100px; + width: 500px; + } + + #bottom { + bottom: 0px; + background: blue; + } + + #vertical { + height: 100%; + background: green; + } + + svg { + width: 10px; + height: 10px; + } + </style> + </head> + <body> + <div id="vertical">Vertical</div> + <div id="top" class="class1 class2">Top</div> + <div id="bottom">Bottom</div> + <svg viewBox="0 0 10 10"> + <clipPath id="clip"> + <rect x="0" y="0" width="10" height="5"></rect> + </clipPath> + <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle> + </svg> + </body> + </html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_02.html b/devtools/client/inspector/test/doc_inspector_infobar_02.html new file mode 100644 index 0000000000..ed1843f8db --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_02.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + width: 100%; + height: 100%; + } + + div { + position: absolute; + height: 100px; + width: 500px; + } + + #below-bottom { + bottom: -200px; + background: red; + } + + #above-top { + top: -200px; + background: black; + color: white; + }"; + </style> +</head> +<body> + <div id="above-top">Above top</div> + <div id="below-bottom">Far bottom</div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_03.html b/devtools/client/inspector/test/doc_inspector_infobar_03.html new file mode 100644 index 0000000000..a9aa05fa03 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_03.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + height: 300vh; + } + </style> + </head> + <body> + </body> + </html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_04.html b/devtools/client/inspector/test/doc_inspector_infobar_04.html new file mode 100644 index 0000000000..e6d3325965 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_04.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + + <style> + .flex { + display: flex; + } + + .grid { + display: grid; + } + </style> + </head> + <body> + <div id="flex-container" class="flex"> + <div id="flex-item"></div> + <div id="flex-container-item" class="flex"></div> + </div> + + <div id="grid-container" class="grid"> + <div id="grid-item"></div> + <div id="grid-container-item" class="grid"></div> + </div> + + <div id="flex-container-with-grid" class="flex"> + <div id="flex-item-grid-container" class="grid"></div> + </div> + + <div id="flex-text-container" class="flex"> + flex item (node text) + </div> + + <div id="grid-text-container" class="grid"> + grid item (node text). The text content for this text node needs to be long enough + so that the inspector does not inline it. Indeed we want to be able to select and + highlight it independently in the inspector so we can test the grid item infobar. + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_textnode.html b/devtools/client/inspector/test/doc_inspector_infobar_textnode.html new file mode 100644 index 0000000000..2370708f43 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_textnode.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> +</head> +<body> + <div id="textnode-container"> + text + <span>content</span> + <span>content</span> + text + </div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_long-divs.html b/devtools/client/inspector/test/doc_inspector_long-divs.html new file mode 100644 index 0000000000..52d6343aaa --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_long-divs.html @@ -0,0 +1,104 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Long Div Listing</title> + <style> + div { + background-color: #0002; + padding-left: 1em; + } + </style> +</head> +<body> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div id="focus-here">focus here</div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div> + <div> + <div> + <div> + <div id="zoom-here">zoom-here</div> + </div> + </div> + </div> + </div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_menu.html b/devtools/client/inspector/test/doc_inspector_menu.html new file mode 100644 index 0000000000..03310600c0 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_menu.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> + <head> + <title>Inspector Tree Menu Test</title> + <meta charset="utf-8"> + </head> + <body> + <div> + <div id="paste-area"> + <h1>Inspector Tree Menu Test</h1> + <p class="inner">Unset</p> + <p class="adjacent"> + <span class="ref">3</span> + </p> + </div> + <p data-id="copy">Paragraph for testing copy</p> + <p id="sensitivity">Paragraph for sensitivity</p> + <p class="duplicate">This will be duplicated</p> + <p id="delete">This has to be deleted</p> + <img id="copyimage" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==" /> + <div id="hiddenElement" style="display: none;"> + <p id="nestedHiddenElement">Visible element nested inside a non-visible element</p> + </div> + <p id="console-var">Paragraph for testing console variables</p> + <p id="console-var-multi">Paragraph for testing multiple console variables</p> + + + <p id="attributes" data-copy="the" data-long-copy="#01234567890123456789012345678901234567890123456789123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" data-edit="original" data-remove="thing">Attributes are going to be changed here</p> + + <div id="host"></div> + <script> + 'use strict'; + document.getElementById("host").attachShadow({ mode: "open" }); + </script> + <iframe srcdoc="<p>Paragraph in iFrame</p>"></iframe> + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_outerhtml.html b/devtools/client/inspector/test/doc_inspector_outerhtml.html new file mode 100644 index 0000000000..cc400674d2 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_outerhtml.html @@ -0,0 +1,11 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Copy OuterHTML Test</title> +</head> +<body> + <!-- Comment --> + <div><p>Test copy OuterHTML</p></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_pane-toggle-layout-invariant.html b/devtools/client/inspector/test/doc_inspector_pane-toggle-layout-invariant.html new file mode 100644 index 0000000000..3e256b4a60 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_pane-toggle-layout-invariant.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="UTF-8"> +<style> +.header { + height: 81px; + border-bottom: 1px solid #ddd; +} + +.container-fluid { + display: table; + height: 100%; +} + +.inner-nav, .extra-nav { + display: table-cell; + height: 100%; +} +</style> +<header class="header"> + <div class="container-fluid"> + <div class="inner-nav"> + <a id="invariant" href="#">Office Lunch</a> + </div> + <div class="extra-nav"> + </div> + </div> +</header> diff --git a/devtools/client/inspector/test/doc_inspector_reload_xul.xhtml b/devtools/client/inspector/test/doc_inspector_reload_xul.xhtml new file mode 100644 index 0000000000..d7a17c5a9c --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_reload_xul.xhtml @@ -0,0 +1,9 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/content/xul.css" type="text/css"?> +<!DOCTYPE window> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <div id="p">a node inspected before reload</div> + <div id="q">a node inspected after reload</div> + </body> +</window> diff --git a/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html b/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html new file mode 100644 index 0000000000..233ebe1183 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>iframe creation/deletion test</title> +</head> +<body> + <div id="yay"></div> + <script type="text/javascript"> + "use strict"; + + var yay = document.querySelector("#yay"); + yay.textContent = "nothing"; + + // Create a promise that the test can wait for. + let resolveReadyPromise; + window.readyPromise = new Promise(r => (resolveReadyPromise = r)); + + // Create/remove an iframe before load. + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + iframe.remove(); + yay.textContent = "before events"; + + // Create/remove an iframe on DOMContentLoaded. + document.addEventListener("DOMContentLoaded", function() { + const newIframe = document.createElement("iframe"); + document.body.appendChild(newIframe); + newIframe.remove(); + yay.textContent = "DOMContentLoaded"; + }); + + // Create/remove an iframe on window load. + window.addEventListener("load", function() { + const newIframe = document.createElement("iframe"); + document.body.appendChild(newIframe); + newIframe.remove(); + yay.textContent = "load"; + + resolveReadyPromise(); + }); + </script> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_search-iframes.html b/devtools/client/inspector/test/doc_inspector_search-iframes.html new file mode 100644 index 0000000000..fa122d5a55 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search-iframes.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en" id="root"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Test in Iframes</title> +</head> +<body> + <iframe id="iframe-1" src="/browser/devtools/client/inspector/test/doc_inspector_search.html"></iframe> + <iframe id="iframe-2" src="/browser/devtools/client/inspector/test/doc_inspector_search.html"></iframe> + <iframe id="iframe-3" src="data:text/html;charset=utf-8,%0A%20%20%3Cbutton%20id=%22b1%22%3ENested%20button%3C/button%3E%0A%20%20%3Ciframe%20id=%22iframe-4%22%20src=%22https://example.com/browser/devtools/client/inspector/test/doc_inspector_search.html%22%3E%3C/iframe%3E%0A"> + </iframe> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_search-reserved.html b/devtools/client/inspector/test/doc_inspector_search-reserved.html new file mode 100644 index 0000000000..15cf8c3af6 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search-reserved.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Reserved Character Test</title> +</head> +<body> + <div id="d1.d2">Hi, I'm an id that contains a CSS reserved character</div> + <div class="c1.c2">Hi, a class that contains a CSS reserved character</div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_search-suggestions.html b/devtools/client/inspector/test/doc_inspector_search-suggestions.html new file mode 100644 index 0000000000..a84a2e3d40 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search-suggestions.html @@ -0,0 +1,27 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Test</title> +</head> +<body> + <div id="d1"> + <div class="l1"> + <div id="d2" class="c1">Hello, I'm nested div</div> + </div> + </div> + <span id="s1">Hello, I'm a span + <div class="l1"> + <span>Hi I am a nested span</span> + <span class="s4">Hi I am a nested classed span</span> + </div> + </span> + <span class="c1" id="s2">And me</span> + + <p class="c1" id="p1">.someclass</p> + <p id="p2">#someid</p> + <button id="b1" disabled>button[disabled]</button> + <p id="p3" class="c2"><strong>p>strong</strong></p> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_search-svg.html b/devtools/client/inspector/test/doc_inspector_search-svg.html new file mode 100644 index 0000000000..a78a20ff7a --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search-svg.html @@ -0,0 +1,16 @@ +<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector SVG Search Box Test</title>
+</head>
+<body>
+ <div class="class1"></div>
+ <svg>
+ <clipPath>
+ <rect x="0" y="0" width="10" height="5"></rect>
+ </clipPath>
+ <circle cx="0" cy="0" r="50" class="class2" />
+ </svg>
+</body>
+</html> diff --git a/devtools/client/inspector/test/doc_inspector_search.html b/devtools/client/inspector/test/doc_inspector_search.html new file mode 100644 index 0000000000..54719bec44 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search.html @@ -0,0 +1,26 @@ +<!doctype html> +<html lang="en" id="root"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Test</title> +</head> +<body> + + <!-- This is a list of 0 h1 elements --> + + <!-- This is a list of 2 div elements --> + <div id="d1">Hello, I'm a div</div> + <div id="d2" class="c1">Hello, I'm another div</div> + + <!-- This is a list of 2 span elements --> + <span id="s1">Hello, I'm a span</span> + <span class="c1" id="s2">And I am also a span but I contain more text than the other one.</span> + + <!-- This is a collection of various things that match only once --> + <p class="c1" id="p1">.someclass</p> + <p id="p2">#someid</p> + <button id="b1" disabled>button[disabled]</button> + <p id="p3" class="c2"><strong>p>strong</strong></p> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html b/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html new file mode 100644 index 0000000000..a78e281ca3 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>select last selected test</title> + </head> + <body> + <div id="id1"></div> + <div id="id2"></div> + <div id="id3"> + <ul class="aList"> + <li class="item"></li> + <li class="item"></li> + <li class="item"></li> + <li class="item"> + <span id="id4"></span> + </li> + </ul> + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html b/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html new file mode 100644 index 0000000000..4c7c4e7d1e --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>select last selected test</title> + </head> + <body> + <div id="id5"></div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_svg.svg b/devtools/client/inspector/test/doc_inspector_svg.svg new file mode 100644 index 0000000000..75154dcf3d --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_svg.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <circle r="5"/> +</svg> diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js new file mode 100644 index 0000000000..cb1b00030c --- /dev/null +++ b/devtools/client/inspector/test/head.js @@ -0,0 +1,1499 @@ +/* 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/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"use strict"; + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); + +// Services.prefs.setBoolPref("devtools.debugger.log", true); +// SimpleTest.registerCleanupFunction(() => { +// Services.prefs.clearUserPref("devtools.debugger.log"); +// }); + +// Import helpers for the inspector that are also shared with others +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", + this +); + +const INSPECTOR_L10N = new LocalizationHelper( + "devtools/client/locales/inspector.properties" +); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.inspector.activeSidebar"); + Services.prefs.clearUserPref("devtools.inspector.selectedSidebar"); +}); + +registerCleanupFunction(function () { + // Move the mouse outside inspector. If the test happened fake a mouse event + // somewhere over inspector the pointer is considered to be there when the + // next test begins. This might cause unexpected events to be emitted when + // another test moves the mouse. + // Move the mouse at the top-right corner of the browser, to prevent + // the mouse from triggering the tab tooltip to be shown while the tab is + // being closed because the test is exiting (See Bug 1378524 for rationale). + EventUtils.synthesizeMouseAtPoint( + window.innerWidth, + 1, + { type: "mousemove" }, + window + ); +}); + +/** + * Start the element picker and focus the content window. + * @param {Toolbox} toolbox + * @param {Boolean} skipFocus - Allow tests to bypass the focus event. + */ +var startPicker = async function (toolbox, skipFocus) { + info("Start the element picker"); + toolbox.win.focus(); + await toolbox.nodePicker.start(); + if (!skipFocus) { + // By default make sure the content window is focused since the picker may not focus + // the content window by default. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.focus(); + }); + } +}; + +/** + * Stop the element picker using the Escape keyboard shortcut + * @param {Toolbox} toolbox + */ +var stopPickerWithEscapeKey = async function (toolbox) { + const onPickerStopped = toolbox.nodePicker.once("picker-node-canceled"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, toolbox.win); + await onPickerStopped; +}; + +/** + * Start the eye dropper tool. + * @param {Toolbox} toolbox + */ +var startEyeDropper = async function (toolbox) { + info("Start the eye dropper tool"); + toolbox.win.focus(); + await toolbox.getPanel("inspector").showEyeDropper(); +}; + +/** + * Pick an element from the content page using the element picker. + * + * @param {Inspector} inspector + * Inspector instance + * @param {String} selector + * CSS selector to identify the click target + * @param {Number} x + * X-offset from the top-left corner of the element matching the provided selector + * @param {Number} y + * Y-offset from the top-left corner of the element matching the provided selector + * @return {Promise} promise that resolves when the selection is updated with the picked + * node. + */ +function pickElement(inspector, selector, x, y) { + info("Waiting for element " + selector + " to be picked"); + // Use an empty options argument in order trigger the default synthesizeMouse behavior + // which will trigger mousedown, then mouseup. + const onNewNodeFront = inspector.selection.once("new-node-front"); + BrowserTestUtils.synthesizeMouse( + selector, + x, + y, + {}, + gBrowser.selectedTab.linkedBrowser + ); + return onNewNodeFront; +} + +/** + * Hover an element from the content page using the element picker. + * + * @param {Inspector} inspector + * Inspector instance + * @param {String|Array} selector + * CSS selector to identify the hover target. + * Example: ".target" + * If the element is at the bottom of a nested iframe stack, the selector should + * be an array with each item identifying the iframe within its host document. + * The last item of the array should be the element selector within the deepest + * nested iframe. + Example: ["iframe#top", "iframe#nested", ".target"] + * @param {Number} x + * X-offset from the top-left corner of the element matching the provided selector + * @param {Number} y + * Y-offset from the top-left corner of the element matching the provided selector + * @return {Promise} promise that resolves when both the "picker-node-hovered" and + * "highlighter-shown" events are emitted. + */ +async function hoverElement(inspector, selector, x, y) { + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + info(`Waiting for element "${selector}" to be hovered`); + const onHovered = inspector.toolbox.nodePicker.once("picker-node-hovered"); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + + // Default to the top-level target browsing context + let browsingContext = gBrowser.selectedTab.linkedBrowser; + + if (Array.isArray(selector)) { + // Get the browsing context for the deepest nested frame; exclude the last array item. + // Cloning the array so it can be safely mutated. + browsingContext = await getBrowsingContextForNestedFrame( + selector.slice(0, selector.length - 1) + ); + // Assume the last item in the selector array is the actual element selector. + // DO NOT mutate the selector array with .pop(), it might still be used by a test. + selector = selector[selector.length - 1]; + } + + if (isNaN(x) || isNaN(y)) { + BrowserTestUtils.synthesizeMouseAtCenter( + selector, + { type: "mousemove" }, + browsingContext + ); + } else { + BrowserTestUtils.synthesizeMouse( + selector, + x, + y, + { type: "mousemove" }, + browsingContext + ); + } + + info("Wait for picker-node-hovered"); + await onHovered; + + info("Wait for highlighter shown"); + await onHighlighterShown; + + return Promise.all([onHighlighterShown, onHovered]); +} + +/** + * Get the browsing context for the deepest nested iframe + * as identified by an array of selectors. + * + * @param {Array} selectorArray + * Each item in the array is a selector that identifies the iframe + * within its host document. + * Example: ["iframe#top", "iframe#nested"] + * @return {BrowsingContext} + * BrowsingContext for the deepest nested iframe. + */ +async function getBrowsingContextForNestedFrame(selectorArray = []) { + // Default to the top-level target browsing context + let browsingContext = gBrowser.selectedTab.linkedBrowser; + + // Return the top-level target browsing context if the selector is not an array. + if (!Array.isArray(selectorArray)) { + return browsingContext; + } + + // Recursively get the browsing context for each nested iframe. + while (selectorArray.length) { + browsingContext = await SpecialPowers.spawn( + browsingContext, + [selectorArray.shift()], + function (selector) { + const iframe = content.document.querySelector(selector); + return iframe.browsingContext; + } + ); + } + + return browsingContext; +} + +/** + * Highlight a node and set the inspector's current selection to the node or + * the first match of the given css selector. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return a promise that resolves when the inspector is updated with the new + * node + */ +async function selectAndHighlightNode(selector, inspector) { + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + info("Highlighting and selecting the node " + selector); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + + await selectNode(selector, inspector, "test-highlight"); + await onHighlighterShown; +} + +/** + * Select node for a given selector, make it focusable and set focus in its + * container element. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The current inspector-panel instance. + * @return {MarkupContainer} + */ +async function focusNode(selector, inspector) { + getContainerForNodeFront(inspector.walker.rootNode, inspector).elt.focus(); + const nodeFront = await getNodeFront(selector, inspector); + const container = getContainerForNodeFront(nodeFront, inspector); + await selectNode(nodeFront, inspector); + EventUtils.sendKey("return", inspector.panelWin); + return container; +} + +/** + * Set the inspector's current selection to null so that no node is selected + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return a promise that resolves when the inspector is updated + */ +function clearCurrentNodeSelection(inspector) { + info("Clearing the current selection"); + const updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(null); + return updated; +} + +/** + * Right click on a node in the test page and click on the inspect menu item. + * @param {String} selector The selector for the node to click on in the page. + * @return {Promise} Resolves to the inspector when it has opened and is updated + */ +var clickOnInspectMenuItem = async function (selector) { + info("Showing the contextual menu on node " + selector); + const contentAreaContextMenu = document.querySelector( + "#contentAreaContextMenu" + ); + const contextOpened = once(contentAreaContextMenu, "popupshown"); + + await safeSynthesizeMouseEventAtCenterInContentPage(selector, { + type: "contextmenu", + button: 2, + }); + + await contextOpened; + + info("Triggering the inspect action"); + await gContextMenu.inspectNode(); + + info("Hiding the menu"); + const contextClosed = once(contentAreaContextMenu, "popuphidden"); + contentAreaContextMenu.hidePopup(); + await contextClosed; + + return getActiveInspector(); +}; + +/** + * Get the NodeFront for the document node inside a given iframe. + * + * @param {String|NodeFront} frameSelector + * A selector that matches the iframe the document node is in + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return {Promise} Resolves the node front when the inspector is updated with the new + * node. + */ +var getFrameDocument = async function (frameSelector, inspector) { + const iframe = await getNodeFront(frameSelector, inspector); + const { nodes } = await inspector.walker.children(iframe); + + // Find the document node in the children of the iframe element. + return nodes.filter(node => node.displayName === "#document")[0]; +}; + +/** + * Get the NodeFront for the shadowRoot of a shadow host. + * + * @param {String|NodeFront} hostSelector + * Selector or front of the element to which the shadow root is attached. + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return {Promise} Resolves the node front when the inspector is updated with the new + * node. + */ +var getShadowRoot = async function (hostSelector, inspector) { + const hostFront = await getNodeFront(hostSelector, inspector); + const { nodes } = await inspector.walker.children(hostFront); + + // Find the shadow root in the children of the host element. + return nodes.filter(node => node.isShadowRoot)[0]; +}; + +/** + * Get the NodeFront for a node that matches a given css selector inside a shadow root. + * + * @param {String} selector + * CSS selector of the node inside the shadow root. + * @param {String|NodeFront} hostSelector + * Selector or front of the element to which the shadow root is attached. + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return {Promise} Resolves the node front when the inspector is updated with the new + * node. + */ +var getNodeFrontInShadowDom = async function ( + selector, + hostSelector, + inspector +) { + const shadowRoot = await getShadowRoot(hostSelector, inspector); + if (!shadowRoot) { + throw new Error( + "Could not find a shadow root under selector: " + hostSelector + ); + } + + return inspector.walker.querySelector(shadowRoot, selector); +}; + +var focusSearchBoxUsingShortcut = async function (panelWin, callback) { + info("Focusing search box"); + const searchBox = panelWin.document.getElementById("inspector-searchbox"); + const focused = once(searchBox, "focus"); + + panelWin.focus(); + + synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key")); + + await focused; + + if (callback) { + callback(); + } +}; + +/** + * Get the MarkupContainer object instance that corresponds to the given + * NodeFront + * @param {NodeFront} nodeFront + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {MarkupContainer} + */ +function getContainerForNodeFront(nodeFront, { markup }) { + return markup.getContainer(nodeFront); +} + +/** + * Get the MarkupContainer object instance that corresponds to the given + * selector + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @param {Boolean} Set to true in the event that the node shouldn't be found. + * @return {MarkupContainer} + */ +var getContainerForSelector = async function ( + selector, + inspector, + expectFailure = false +) { + info("Getting the markup-container for node " + selector); + const nodeFront = await getNodeFront(selector, inspector); + const container = getContainerForNodeFront(nodeFront, inspector); + + if (expectFailure) { + ok(!container, "Shouldn't find markup-container for selector: " + selector); + } else { + ok(container, "Found markup-container for selector: " + selector); + } + + return container; +}; + +/** + * Simulate a mouse-over on the markup-container (a line in the markup-view) + * that corresponds to the selector passed. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the container is hovered and the higlighter + * is shown on the corresponding node + */ +var hoverContainer = async function (selector, inspector) { + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + info("Hovering over the markup-container for node " + selector); + + const nodeFront = await getNodeFront(selector, inspector); + const container = getContainerForNodeFront(nodeFront, inspector); + + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + EventUtils.synthesizeMouseAtCenter( + container.tagLine, + { type: "mousemove" }, + inspector.markup.doc.defaultView + ); + await onHighlighterShown; +}; + +/** + * Simulate a click on the markup-container (a line in the markup-view) + * that corresponds to the selector passed. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the node has been selected. + */ +var clickContainer = async function (selector, inspector) { + info("Clicking on the markup-container for node " + selector); + + const nodeFront = await getNodeFront(selector, inspector); + const container = getContainerForNodeFront(nodeFront, inspector); + + const updated = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter( + container.tagLine, + { type: "mousedown" }, + inspector.markup.doc.defaultView + ); + EventUtils.synthesizeMouseAtCenter( + container.tagLine, + { type: "mouseup" }, + inspector.markup.doc.defaultView + ); + return updated; +}; + +/** + * Simulate the mouse leaving the markup-view area + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise when done + */ +function mouseLeaveMarkupView(inspector) { + info("Leaving the markup-view area"); + + // Find another element to mouseover over in order to leave the markup-view + const btn = inspector.toolbox.doc.querySelector("#toolbox-controls"); + + EventUtils.synthesizeMouseAtCenter( + btn, + { type: "mousemove" }, + inspector.toolbox.win + ); + + return new Promise(resolve => { + executeSoon(resolve); + }); +} + +/** + * Dispatch the copy event on the given element + */ +function fireCopyEvent(element) { + const evt = element.ownerDocument.createEvent("Event"); + evt.initEvent("copy", true, true); + element.dispatchEvent(evt); +} + +/** + * Undo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no undo action is possible + */ +function undoChange(inspector) { + const canUndo = inspector.markup.undo.canUndo(); + ok(canUndo, "The last change in the markup-view can be undone"); + if (!canUndo) { + return Promise.reject(); + } + + const mutated = inspector.once("markupmutation"); + inspector.markup.undo.undo(); + return mutated; +} + +/** + * Redo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no redo action is possible + */ +function redoChange(inspector) { + const canRedo = inspector.markup.undo.canRedo(); + ok(canRedo, "The last change in the markup-view can be redone"); + if (!canRedo) { + return Promise.reject(); + } + + const mutated = inspector.once("markupmutation"); + inspector.markup.undo.redo(); + return mutated; +} + +/** + * A helper that fetches a front for a node that matches the given selector or + * doctype node if the selector is falsy. + */ +async function getNodeFrontForSelector(selector, inspector) { + if (selector) { + info("Retrieving front for selector " + selector); + return getNodeFront(selector, inspector); + } + + info("Retrieving front for doctype node"); + const { nodes } = await inspector.walker.children(inspector.walker.rootNode); + return nodes[0]; +} + +/** + * A simple polling helper that executes a given function until it returns true. + * @param {Function} check A generator function that is expected to return true at some + * stage. + * @param {String} desc A text description to be displayed when the polling starts. + * @param {Number} attemptes Optional number of times we poll. Defaults to 10. + * @param {Number} timeBetweenAttempts Optional time to wait between each attempt. + * Defaults to 200ms. + */ +async function poll(check, desc, attempts = 10, timeBetweenAttempts = 200) { + info(desc); + + for (let i = 0; i < attempts; i++) { + if (await check()) { + return; + } + await new Promise(resolve => setTimeout(resolve, timeBetweenAttempts)); + } + + throw new Error(`Timeout while: ${desc}`); +} + +/** + * Encapsulate some common operations for highlighter's tests, to have + * the tests cleaner, without exposing directly `inspector`, `highlighter`, and + * `highlighterTestFront` if not needed. + * + * @param {String} + * The highlighter's type + * @return + * A generator function that takes an object with `inspector` and `highlighterTestFront` + * properties. (see `openInspector`) + */ +const getHighlighterHelperFor = type => + async function ({ inspector, highlighterTestFront }) { + const front = inspector.inspectorFront; + const highlighter = await front.getHighlighterByType(type); + + let prefix = ""; + + // Internals for mouse events + let prevX, prevY; + + // Highlighted node + let highlightedNode = null; + + return { + set prefix(value) { + prefix = value; + }, + + get highlightedNode() { + if (!highlightedNode) { + return null; + } + + return { + async getComputedStyle(options = {}) { + const pageStyle = highlightedNode.inspectorFront.pageStyle; + return pageStyle.getComputed(highlightedNode, options); + }, + }; + }, + + get actorID() { + if (!highlighter) { + return null; + } + + return highlighter.actorID; + }, + + async show(selector = ":root", options, frameSelector = null) { + if (frameSelector) { + highlightedNode = await getNodeFrontInFrames( + [frameSelector, selector], + inspector + ); + } else { + highlightedNode = await getNodeFront(selector, inspector); + } + return highlighter.show(highlightedNode, options); + }, + + async hide() { + await highlighter.hide(); + }, + + async isElementHidden(id) { + return ( + (await highlighterTestFront.getHighlighterNodeAttribute( + prefix + id, + "hidden", + highlighter + )) === "true" + ); + }, + + async getElementTextContent(id) { + return highlighterTestFront.getHighlighterNodeTextContent( + prefix + id, + highlighter + ); + }, + + async getElementAttribute(id, name) { + return highlighterTestFront.getHighlighterNodeAttribute( + prefix + id, + name, + highlighter + ); + }, + + async waitForElementAttributeSet(id, name) { + await poll(async function () { + const value = await highlighterTestFront.getHighlighterNodeAttribute( + prefix + id, + name, + highlighter + ); + return !!value; + }, `Waiting for element ${id} to have attribute ${name} set`); + }, + + async waitForElementAttributeRemoved(id, name) { + await poll(async function () { + const value = await highlighterTestFront.getHighlighterNodeAttribute( + prefix + id, + name, + highlighter + ); + return !value; + }, `Waiting for element ${id} to have attribute ${name} removed`); + }, + + async synthesizeMouse({ + selector = ":root", + center, + x, + y, + options, + } = {}) { + if (center === true) { + await safeSynthesizeMouseEventAtCenterInContentPage( + selector, + options + ); + } else { + await safeSynthesizeMouseEventInContentPage(selector, x, y, options); + } + }, + + // This object will synthesize any "mouse" prefixed event to the + // `highlighterTestFront`, using the name of method called as suffix for the + // event's name. + // If no x, y coords are given, the previous ones are used. + // + // For example: + // mouse.down(10, 20); // synthesize "mousedown" at 10,20 + // mouse.move(20, 30); // synthesize "mousemove" at 20,30 + // mouse.up(); // synthesize "mouseup" at 20,30 + mouse: new Proxy( + {}, + { + get: (target, name) => + async function (x = prevX, y = prevY, selector = ":root") { + prevX = x; + prevY = y; + await safeSynthesizeMouseEventInContentPage(selector, x, y, { + type: "mouse" + name, + }); + }, + } + ), + + async finalize() { + highlightedNode = null; + await highlighter.finalize(); + }, + }; + }; + +/** + * Inspector-scoped wrapper for highlighter helpers to be used in tests. + * + * @param {Inspector} inspector + * Inspector client object instance. + * @return {Object} Object with helper methods + */ +function getHighlighterTestHelpers(inspector) { + /** + * Return a promise which resolves when a highlighter triggers the given event. + * + * @param {String} type + * Highlighter type. + * @param {String} eventName + * Name of the event to listen to. + * @return {Promise} + * Promise which resolves when the highlighter event occurs. + * Resolves with the data payload attached to the event. + */ + function _waitForHighlighterTypeEvent(type, eventName) { + return new Promise(resolve => { + function _handler(data) { + if (type === data.type) { + inspector.highlighters.off(eventName, _handler); + resolve(data); + } + } + + inspector.highlighters.on(eventName, _handler); + }); + } + + return { + getActiveHighlighter(type) { + return inspector.highlighters.getActiveHighlighter(type); + }, + getNodeForActiveHighlighter(type) { + return inspector.highlighters.getNodeForActiveHighlighter(type); + }, + waitForHighlighterTypeShown(type) { + return _waitForHighlighterTypeEvent(type, "highlighter-shown"); + }, + waitForHighlighterTypeHidden(type) { + return _waitForHighlighterTypeEvent(type, "highlighter-hidden"); + }, + waitForHighlighterTypeRestored(type) { + return _waitForHighlighterTypeEvent(type, "highlighter-restored"); + }, + waitForHighlighterTypeDiscarded(type) { + return _waitForHighlighterTypeEvent(type, "highlighter-discarded"); + }, + }; +} + +/** + * Wait for the toolbox to emit the styleeditor-selected event and when done + * wait for the stylesheet identified by href to be loaded in the stylesheet + * editor + * + * @param {Toolbox} toolbox + * @param {String} href + * Optional, if not provided, wait for the first editor to be ready + * @return a promise that resolves to the editor when the stylesheet editor is + * ready + */ +function waitForStyleEditor(toolbox, href) { + info("Waiting for the toolbox to switch to the styleeditor"); + + return new Promise(resolve => { + toolbox.once("styleeditor-selected").then(() => { + const panel = toolbox.getCurrentPanel(); + ok(panel && panel.UI, "Styleeditor panel switched to front"); + + // A helper that resolves the promise once it receives an editor that + // matches the expected href. Returns false if the editor was not correct. + const gotEditor = editor => { + if (!editor) { + info("Editor went away after selected?"); + return false; + } + + const currentHref = editor.styleSheet.href; + if (!href || (href && currentHref.endsWith(href))) { + info("Stylesheet editor selected"); + panel.UI.off("editor-selected", gotEditor); + + editor.getSourceEditor().then(sourceEditor => { + info("Stylesheet editor fully loaded"); + resolve(sourceEditor); + }); + + return true; + } + + info("The editor was incorrect. Waiting for editor-selected event."); + return false; + }; + + // The expected editor may already be selected. Check the if the currently + // selected editor is the expected one and if not wait for an + // editor-selected event. + if (!gotEditor(panel.UI.selectedEditor)) { + // The expected editor is not selected (yet). Wait for it. + panel.UI.on("editor-selected", gotEditor); + } + }); + }); +} + +/** + * Checks if document's active element is within the given element. + * @param {HTMLDocument} doc document with active element in question + * @param {DOMNode} container element tested on focus containment + * @return {Boolean} + */ +function containsFocus(doc, container) { + let elm = doc.activeElement; + while (elm) { + if (elm === container) { + return true; + } + elm = elm.parentNode; + } + return false; +} + +/** + * Listen for a new tab to open and return a promise that resolves when one + * does and completes the load event. + * + * @return a promise that resolves to the tab object + */ +var waitForTab = async function () { + info("Waiting for a tab to open"); + await once(gBrowser.tabContainer, "TabOpen"); + const tab = gBrowser.selectedTab; + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + info("The tab load completed"); + return tab; +}; + +/** + * Simulate the key input for the given input in the window. + * + * @param {String} input + * The string value to input + * @param {Window} win + * The window containing the panel + */ +function synthesizeKeys(input, win) { + for (const key of input.split("")) { + EventUtils.synthesizeKey(key, {}, win); + } +} + +/** + * Make sure window is properly focused before sending a key event. + * + * @param {Window} win + * The window containing the panel + * @param {String} key + * The string value to input + */ +function focusAndSendKey(win, key) { + win.document.documentElement.focus(); + EventUtils.sendKey(key, win); +} + +/** + * Given a Tooltip instance, fake a mouse event on the `target` DOM Element + * and assert that the `tooltip` is correctly displayed. + * + * @param {Tooltip} tooltip + * The tooltip instance + * @param {DOMElement} target + * The DOM Element on which a tooltip should appear + * + * @return a promise that resolves with the tooltip object + */ +async function assertTooltipShownOnHover(tooltip, target) { + const mouseEvent = new target.ownerDocument.defaultView.MouseEvent( + "mousemove", + { + bubbles: true, + } + ); + target.dispatchEvent(mouseEvent); + + if (!tooltip.isVisible()) { + info("Waiting for tooltip to be shown"); + await tooltip.once("shown"); + } + + ok(tooltip.isVisible(), `The tooltip is visible`); + + return tooltip; +} + +/** + * Given an inspector `view` object, fake a mouse event on the `target` DOM + * Element and assert that the preview tooltip is correctly displayed. + * + * @param {CssRuleView|ComputedView|...} view + * The instance of an inspector panel + * @param {DOMElement} target + * The DOM Element on which a tooltip should appear + * + * @return a promise that resolves with the tooltip object + */ +async function assertShowPreviewTooltip(view, target) { + const name = "previewTooltip"; + + // Get the tooltip. If it does not exist one will be created. + const tooltip = view.tooltips.getTooltip(name); + ok(tooltip, `Tooltip '${name}' has been instantiated`); + + const shown = tooltip.once("shown"); + const mouseEvent = new target.ownerDocument.defaultView.MouseEvent( + "mousemove", + { + bubbles: true, + } + ); + target.dispatchEvent(mouseEvent); + + info("Waiting for tooltip to be shown"); + await shown; + + ok(tooltip.isVisible(), `The tooltip '${name}' is visible`); + + return tooltip; +} + +/** + * Given a `tooltip` instance, fake a mouse event on `target` DOM element + * and check that the tooltip correctly disappear. + * + * @param {Tooltip} tooltip + * The tooltip instance + * @param {DOMElement} target + * The DOM Element on which a tooltip should appear + */ +async function assertTooltipHiddenOnMouseOut(tooltip, target) { + // The tooltip actually relies on mousemove events to check if it sould be hidden. + const mouseEvent = new target.ownerDocument.defaultView.MouseEvent( + "mousemove", + { + bubbles: true, + relatedTarget: target, + } + ); + target.parentNode.dispatchEvent(mouseEvent); + + await tooltip.once("hidden"); + + ok(!tooltip.isVisible(), "The tooltip is hidden on mouseout"); +} + +/** + * Get the text displayed for a given DOM Element's textContent within the + * markup view. + * + * @param {String} selector + * @param {InspectorPanel} inspector + * @return {String} The text displayed in the markup view + */ +async function getDisplayedNodeTextContent(selector, inspector) { + // We have to ensure that the textContent is displayed, for that the DOM + // Element has to be selected in the markup view and to be expanded. + await selectNode(selector, inspector); + + const container = await getContainerForSelector(selector, inspector); + await inspector.markup.expandNode(container.node); + await waitForMultipleChildrenUpdates(inspector); + if (container) { + const textContainer = container.elt.querySelector("pre"); + return textContainer.textContent; + } + return null; +} + +/** + * Toggle the shapes highlighter by simulating a click on the toggle + * in the rules view with the given selector and property + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selector + * The selector in the rule-view to look for the property in + * @param {String} property + * The name of the property + * @param {Boolean} show + * If true, the shapes highlighter is being shown. If false, it is being hidden + * @param {Options} options + * Config option for the shapes highlighter. Contains: + * - {Boolean} transformMode: whether to show the highlighter in transforms mode + */ +async function toggleShapesHighlighter( + view, + selector, + property, + show, + options = {} +) { + info( + `Toggle shapes highlighter ${ + show ? "on" : "off" + } for ${property} on ${selector}` + ); + const highlighters = view.highlighters; + const container = getRuleViewProperty(view, selector, property).valueSpan; + const shapesToggle = container.querySelector(".ruleview-shapeswatch"); + + const metaKey = options.transformMode; + const ctrlKey = options.transformMode; + + if (show) { + const onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + EventUtils.sendMouseEvent( + { type: "click", metaKey, ctrlKey }, + shapesToggle, + view.styleWindow + ); + await onHighlighterShown; + } else { + const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden"); + EventUtils.sendMouseEvent( + { type: "click", metaKey, ctrlKey }, + shapesToggle, + view.styleWindow + ); + await onHighlighterHidden; + } +} + +/** + * Toggle the provided markup container by clicking on the expand arrow and waiting for + * children to update. Similar to expandContainer helper, but this method + * uses a click rather than programatically calling expandNode(). + * + * @param {InspectorPanel} inspector + * The current inspector instance. + * @param {MarkupContainer} container + * The markup container to click on. + * @param {Object} modifiers + * options.altKey {Boolean} Use the altKey modifier, to recursively apply + * the action to all the children of the container. + */ +async function toggleContainerByClick( + inspector, + container, + { altKey = false } = {} +) { + EventUtils.synthesizeMouseAtCenter( + container.expander, + { + altKey, + }, + inspector.markup.doc.defaultView + ); + + // Wait for any pending children updates + await waitForMultipleChildrenUpdates(inspector); +} + +/** + * Simulate a color change in a given color picker tooltip. + * + * @param {Spectrum} colorPicker + * The color picker widget. + * @param {Array} newRgba + * Array of the new rgba values to be set in the color widget. + */ +async function simulateColorPickerChange(colorPicker, newRgba) { + info("Getting the spectrum colorpicker object"); + const spectrum = await colorPicker.spectrum; + info("Setting the new color"); + spectrum.rgb = newRgba; + info("Applying the change"); + spectrum.updateUI(); + spectrum.onChange(); +} + +/** + * Assert method to compare the current content of the markupview to a text based tree. + * + * @param {String} tree + * Multiline string representing the markup view tree, for instance: + * `root + * child1 + * subchild1 + * subchild2 + * child2 + * subchild3!slotted` + * child3!ignore-children + * Each sub level should be indented by 2 spaces. + * Each line contains text expected to match with the text of the corresponding + * node in the markup view. Some suffixes are supported: + * - !slotted -> indicates that the line corresponds to the slotted version + * - !ignore-children -> the node might have children but do not assert them + * @param {String} selector + * A CSS selector that will uniquely match the "root" element from the tree + * @param {Inspector} inspector + * The inspector instance. + */ +async function assertMarkupViewAsTree(tree, selector, inspector) { + const { markup } = inspector; + + info(`Find and expand the shadow DOM host matching selector ${selector}.`); + const rootFront = await getNodeFront(selector, inspector); + const rootContainer = markup.getContainer(rootFront); + + const parsedTree = _parseMarkupViewTree(tree); + const treeRoot = parsedTree.children[0]; + await _checkMarkupViewNode(treeRoot, rootContainer, inspector); +} + +async function _checkMarkupViewNode(treeNode, container, inspector) { + const { node, children, path } = treeNode; + info("Checking [" + path + "]"); + info("Checking node: " + node); + + const ignoreChildren = node.includes("!ignore-children"); + const slotted = node.includes("!slotted"); + + // Remove optional suffixes. + const nodeText = node.replace("!slotted", "").replace("!ignore-children", ""); + + assertContainerHasText(container, nodeText); + + if (slotted) { + assertContainerSlotted(container); + } + + if (ignoreChildren) { + return; + } + + if (!children.length) { + ok(!container.canExpand, "Container for [" + path + "] has no children"); + return; + } + + // Expand the container if not already done. + if (!container.expanded) { + await expandContainer(inspector, container); + } + + const containers = container.getChildContainers(); + is( + containers.length, + children.length, + "Node [" + path + "] has the expected number of children" + ); + for (let i = 0; i < children.length; i++) { + await _checkMarkupViewNode(children[i], containers[i], inspector); + } +} + +/** + * Helper designed to parse a tree represented as: + * root + * child1 + * subchild1 + * subchild2 + * child2 + * subchild3!slotted + * + * Lines represent a simplified view of the markup, where the trimmed line is supposed to + * be included in the text content of the actual markupview container. + * This method returns an object that can be passed to _checkMarkupViewNode() to verify + * the current markup view displays the expected structure. + */ +function _parseMarkupViewTree(inputString) { + const tree = { + level: 0, + children: [], + }; + let lines = inputString.split("\n"); + lines = lines.filter(l => l.trim()); + + let currentNode = tree; + for (const line of lines) { + const nodeString = line.trim(); + const level = line.split(" ").length; + + let parent; + if (level > currentNode.level) { + parent = currentNode; + } else { + parent = currentNode.parent; + for (let i = 0; i < currentNode.level - level; i++) { + parent = parent.parent; + } + } + + const node = { + node: nodeString, + children: [], + parent, + level, + path: parent.path + " " + nodeString, + }; + + parent.children.push(node); + currentNode = node; + } + + return tree; +} + +/** + * Assert whether the provided container is slotted. + */ +function assertContainerSlotted(container) { + ok(container.isSlotted(), "Container is a slotted container"); + ok( + container.elt.querySelector(".reveal-link"), + "Slotted container has a reveal link element" + ); +} + +/** + * Check if the provided text can be matched anywhere in the text content for the provided + * container. + */ +function assertContainerHasText(container, expectedText) { + const textContent = container.elt.textContent; + ok( + textContent.includes(expectedText), + "Container has expected text: " + expectedText + ); +} + +function waitForMutation(inspector, type) { + return waitForNMutations(inspector, type, 1); +} + +function waitForNMutations(inspector, type, count) { + info(`Expecting ${count} markupmutation of type ${type}`); + let receivedMutations = 0; + return new Promise(resolve => { + inspector.on("markupmutation", function onMutation(mutations) { + const validMutations = mutations.filter(m => m.type === type).length; + receivedMutations = receivedMutations + validMutations; + if (receivedMutations == count) { + inspector.off("markupmutation", onMutation); + resolve(); + } + }); + }); +} + +/** + * Move the mouse on the content page at the x,y position and check the color displayed + * in the eyedropper label. + * + * @param {HighlighterTestFront} highlighterTestFront + * @param {Number} x + * @param {Number} y + * @param {String} expectedColor: Hexa string of the expected color + * @param {String} assertionDescription + */ +async function checkEyeDropperColorAt( + highlighterTestFront, + x, + y, + expectedColor, + assertionDescription +) { + info(`Move mouse to ${x},${y}`); + await safeSynthesizeMouseEventInContentPage(":root", x, y, { + type: "mousemove", + }); + + const colorValue = await highlighterTestFront.getEyeDropperColorValue(); + is(colorValue, expectedColor, assertionDescription); +} + +/** + * Delete the provided node front using the context menu in the markup view. + * Will resolve after the inspector UI was fully updated. + * + * @param {NodeFront} node + * The node front to delete. + * @param {Inspector} inspector + * The current inspector panel instance. + */ +async function deleteNodeWithContextMenu(node, inspector) { + const container = inspector.markup.getContainer(node); + + const allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: container.tagLine, + }); + const menuItem = allMenuItems.find(item => item.id === "node-menu-delete"); + const onInspectorUpdated = inspector.once("inspector-updated"); + + info("Clicking 'Delete Node' in the context menu."); + is(menuItem.disabled, false, "delete menu item is enabled"); + menuItem.click(); + + // close the open context menu + EventUtils.synthesizeKey("KEY_Escape"); + + info("Waiting for inspector to update."); + await onInspectorUpdated; + + // Since the mutations are sent asynchronously from the server, the + // inspector-updated event triggered by the deletion might happen before + // the mutation is received and the element is removed from the + // breadcrumbs. See bug 1284125. + if (inspector.breadcrumbs.indexOf(node) > -1) { + info("Crumbs haven't seen deletion. Waiting for breadcrumbs-updated."); + await inspector.once("breadcrumbs-updated"); + } +} + +/** + * Forces the content page to reflow and waits for the next repaint. + */ +function reflowContentPage() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + return new Promise(resolve => { + content.document.documentElement.offsetWidth; + content.requestAnimationFrame(resolve); + }); + }); +} + +/** + * Get all box-model regions' adjusted boxquads for the given element + * @param {String|Array} selector The node selector to target a given element + * @return {Promise<Object>} A promise that resolves with an object with each property of + * a box-model region, each of them being an object with the p1/p2/p3/p4 properties. + */ +async function getAllAdjustedQuadsForContentPageElement( + selector, + useTopWindowAsBoundary = true +) { + const selectors = Array.isArray(selector) ? selector : [selector]; + + const browsingContext = + selectors.length == 1 + ? gBrowser.selectedBrowser.browsingContext + : await getBrowsingContextInFrames( + gBrowser.selectedBrowser.browsingContext, + selectors.slice(0, -1) + ); + + const inBrowsingContextSelector = selectors.at(-1); + return SpecialPowers.spawn( + browsingContext, + [inBrowsingContextSelector, useTopWindowAsBoundary], + (_selector, _useTopWindowAsBoundary) => { + const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { + getAdjustedQuads, + } = require("resource://devtools/shared/layout/utils.js"); + + const node = content.document.querySelector(_selector); + + const boundaryWindow = _useTopWindowAsBoundary ? content.top : content; + const regions = {}; + for (const boxType of ["content", "padding", "border", "margin"]) { + regions[boxType] = getAdjustedQuads(boundaryWindow, node, boxType); + } + + return regions; + } + ); +} + +/** + * Assert that the box-model highlighter's current position corresponds to the + * given node boxquads. + * + * @param {HighlighterTestFront} highlighterTestFront + * @param {String} selector The node selector to get the boxQuads from + */ +async function isNodeCorrectlyHighlighted(highlighterTestFront, selector) { + const boxModel = await highlighterTestFront.getBoxModelStatus(); + + const useTopWindowAsBoundary = !!highlighterTestFront.parentFront.isTopLevel; + const regions = await getAllAdjustedQuadsForContentPageElement( + selector, + useTopWindowAsBoundary + ); + + for (const boxType of ["content", "padding", "border", "margin"]) { + const [quad] = regions[boxType]; + for (const point in boxModel[boxType].points) { + is( + boxModel[boxType].points[point].x, + quad[point].x, + `${selector} ${boxType} point ${point} x coordinate is correct` + ); + is( + boxModel[boxType].points[point].y, + quad[point].y, + `${selector} ${boxType} point ${point} y coordinate is correct` + ); + } + } +} + +/** + * Get the position and size of the measuring tool. + * + * @param {Object} Object returned by getHighlighterHelperFor() + * @return {Promise<Object>} A promise that resolves with an object containing + * the x, y, width, and height properties of the measuring tool which has + * been drawn on-screen + */ +async function getAreaRect({ getElementAttribute }) { + // The 'box-path' element holds the width and height of the + // measuring area as well as the position relative to its + // parent <g> element. + const d = await getElementAttribute("box-path", "d"); + // The tool element itself is a <g> element grouping all paths. + // Though <g> elements do not have coordinates by themselves, + // therefore it is positioned using the 'transform' CSS property. + // So, in order to get the position of the measuring area, the + // coordinates need to be read from the translate() function. + const transform = await getElementAttribute("tool", "transform"); + const reDir = /(\d+) (\d+)/g; + const reTransform = /(\d+),(\d+)/; + const coords = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + let match; + while ((match = reDir.exec(d))) { + let [, x, y] = match; + x = Number(x); + y = Number(y); + if (x < coords.x) { + coords.x = x; + } + if (y < coords.y) { + coords.y = y; + } + if (x > coords.width) { + coords.width = x; + } + if (y > coords.height) { + coords.height = y; + } + } + + match = reTransform.exec(transform); + coords.x += Number(match[1]); + coords.y += Number(match[2]); + + return coords; +} diff --git a/devtools/client/inspector/test/img_browser_inspector_highlighter-eyedropper-image.png b/devtools/client/inspector/test/img_browser_inspector_highlighter-eyedropper-image.png Binary files differnew file mode 100644 index 0000000000..a0aef4f93b --- /dev/null +++ b/devtools/client/inspector/test/img_browser_inspector_highlighter-eyedropper-image.png diff --git a/devtools/client/inspector/test/shared-head.js b/devtools/client/inspector/test/shared-head.js new file mode 100644 index 0000000000..9e82d7e087 --- /dev/null +++ b/devtools/client/inspector/test/shared-head.js @@ -0,0 +1,1158 @@ +/* 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/. */ + +"use strict"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* globals getHighlighterTestFront, openToolboxForTab, gBrowser */ +/* import-globals-from ../../shared/test/shared-head.js */ + +var { + getInplaceEditorForSpan: inplaceEditor, +} = require("resource://devtools/client/shared/inplace-editor.js"); + +// This file contains functions related to the inspector that are also of interest to +// other test directores as well. + +/** + * Open the toolbox, with the inspector tool visible. + * @param {String} hostType Optional hostType, as defined in Toolbox.HostType + * @return {Promise} A promise that resolves when the inspector is ready.The promise + * resolves with an object containing the following properties: + * - toolbox + * - inspector + * - highlighterTestFront + */ +var openInspector = async function (hostType) { + info("Opening the inspector"); + + const toolbox = await openToolboxForTab( + gBrowser.selectedTab, + "inspector", + hostType + ); + const inspector = toolbox.getPanel("inspector"); + + const highlighterTestFront = await getHighlighterTestFront(toolbox); + + return { toolbox, inspector, highlighterTestFront }; +}; + +/** + * Open the toolbox, with the inspector tool visible, and the one of the sidebar + * tabs selected. + * + * @param {String} id + * The ID of the sidebar tab to be opened + * @return {Promise<Object>} A promise that resolves when the inspector is ready and the tab is + * visible and ready. The promise resolves with an object containing the + * following properties: + * - toolbox + * - inspector + * - highlighterTestFront + */ +var openInspectorSidebarTab = async function (id) { + const { toolbox, inspector, highlighterTestFront } = await openInspector(); + + info("Selecting the " + id + " sidebar"); + + const onSidebarSelect = inspector.sidebar.once("select"); + if (id === "layoutview") { + // The layout view should wait until the box-model and grid-panel are ready. + const onBoxModelViewReady = inspector.once("boxmodel-view-updated"); + const onGridPanelReady = inspector.once("grid-panel-updated"); + inspector.sidebar.select(id); + await onBoxModelViewReady; + await onGridPanelReady; + } else { + inspector.sidebar.select(id); + } + await onSidebarSelect; + + return { + toolbox, + inspector, + highlighterTestFront, + }; +}; + +/** + * Open the toolbox, with the inspector tool visible, and the rule-view + * sidebar tab selected. + * + * @return a promise that resolves when the inspector is ready and the rule view + * is visible and ready + */ +async function openRuleView() { + const { inspector, toolbox, highlighterTestFront } = await openInspector(); + + const ruleViewPanel = inspector.getPanel("ruleview"); + await ruleViewPanel.readyPromise; + const view = ruleViewPanel.view; + + // Replace the view to use a custom debounce function that can be triggered manually + // through an additional ".flush()" property. + view.debounce = manualDebounce(); + + return { + toolbox, + inspector, + highlighterTestFront, + view, + }; +} + +/** + * Open the toolbox, with the inspector tool visible, and the computed-view + * sidebar tab selected. + * + * @return a promise that resolves when the inspector is ready and the computed + * view is visible and ready + */ +function openComputedView() { + return openInspectorSidebarTab("computedview").then(data => { + const view = data.inspector.getPanel("computedview").computedView; + + return { + toolbox: data.toolbox, + inspector: data.inspector, + highlighterTestFront: data.highlighterTestFront, + view, + }; + }); +} + +/** + * Open the toolbox, with the inspector tool visible, and the changes view + * sidebar tab selected. + * + * @return a promise that resolves when the inspector is ready and the changes + * view is visible and ready + */ +function openChangesView() { + return openInspectorSidebarTab("changesview").then(data => { + return { + toolbox: data.toolbox, + inspector: data.inspector, + highlighterTestFront: data.highlighterTestFront, + view: data.inspector.getPanel("changesview"), + }; + }); +} + +/** + * Open the toolbox, with the inspector tool visible, and the layout view + * sidebar tab selected to display the box model view with properties. + * + * @return {Promise} a promise that resolves when the inspector is ready and the layout + * view is visible and ready. + */ +function openLayoutView() { + return openInspectorSidebarTab("layoutview").then(data => { + return { + toolbox: data.toolbox, + inspector: data.inspector, + boxmodel: data.inspector.getPanel("boxmodel"), + gridInspector: data.inspector.getPanel("layoutview").gridInspector, + flexboxInspector: data.inspector.getPanel("layoutview").flexboxInspector, + layoutView: data.inspector.getPanel("layoutview"), + highlighterTestFront: data.highlighterTestFront, + }; + }); +} + +/** + * Select the rule view sidebar tab on an already opened inspector panel. + * + * @param {InspectorPanel} inspector + * The opened inspector panel + * @return {CssRuleView} the rule view + */ +function selectRuleView(inspector) { + return inspector.getPanel("ruleview").view; +} + +/** + * Select the computed view sidebar tab on an already opened inspector panel. + * + * @param {InspectorPanel} inspector + * The opened inspector panel + * @return {CssComputedView} the computed view + */ +function selectComputedView(inspector) { + inspector.sidebar.select("computedview"); + return inspector.getPanel("computedview").computedView; +} + +/** + * Select the changes view sidebar tab on an already opened inspector panel. + * + * @param {InspectorPanel} inspector + * The opened inspector panel + * @return {ChangesView} the changes view + */ +function selectChangesView(inspector) { + inspector.sidebar.select("changesview"); + return inspector.getPanel("changesview"); +} + +/** + * Select the layout view sidebar tab on an already opened inspector panel. + * + * @param {InspectorPanel} inspector + * @return {BoxModel} the box model + */ +function selectLayoutView(inspector) { + inspector.sidebar.select("layoutview"); + return inspector.getPanel("boxmodel"); +} + +/** + * Get the NodeFront for a node that matches a given css selector, via the + * protocol. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves to the NodeFront instance + */ +function getNodeFront(selector, { walker }) { + if (selector._form) { + return selector; + } + return walker.querySelector(walker.rootNode, selector); +} + +/** + * Set the inspector's current selection to the first match of the given css + * selector + * + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox. + * @param {String} reason + * Defaults to "test" which instructs the inspector not to highlight the + * node upon selection. + * @param {Boolean} isSlotted + * Is the selection representing the slotted version the node. + * @return {Promise} Resolves when the inspector is updated with the new node + */ +var selectNode = async function ( + selector, + inspector, + reason = "test", + isSlotted +) { + info("Selecting the node for '" + selector + "'"); + const nodeFront = await getNodeFront(selector, inspector); + const updated = inspector.once("inspector-updated"); + + const { + ELEMENT_NODE, + } = require("resource://devtools/shared/dom-node-constants.js"); + const onSelectionCssSelectorsUpdated = + nodeFront?.nodeType == ELEMENT_NODE + ? inspector.once("selection-css-selectors-updated") + : null; + + inspector.selection.setNodeFront(nodeFront, { reason, isSlotted }); + await updated; + await onSelectionCssSelectorsUpdated; +}; + +/** + * Using the markupview's _waitForChildren function, wait for all queued + * children updates to be handled. + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when all queued children updates have been + * handled + */ +function waitForChildrenUpdated({ markup }) { + info("Waiting for queued children updates to be handled"); + return new Promise(resolve => { + markup._waitForChildren().then(() => { + executeSoon(resolve); + }); + }); +} + +// The expand all operation of the markup-view calls itself recursively and +// there's not one event we can wait for to know when it's done, so use this +// helper function to wait until all recursive children updates are done. +async function waitForMultipleChildrenUpdates(inspector) { + // As long as child updates are queued up while we wait for an update already + // wait again + if ( + inspector.markup._queuedChildUpdates && + inspector.markup._queuedChildUpdates.size + ) { + await waitForChildrenUpdated(inspector); + return waitForMultipleChildrenUpdates(inspector); + } + return null; +} + +/** + * Expand the provided markup container programmatically and wait for all + * children to update. + */ +async function expandContainer(inspector, container) { + await inspector.markup.expandNode(container.node); + await waitForMultipleChildrenUpdates(inspector); +} + +/** + * Get the NodeFront for a node that matches a given css selector inside a + * given iframe. + * + * @param {Array} selectors + * Arrays of CSS selectors from the root document to the node. + * The last CSS selector of the array is for the node in its frame doc. + * The before-last CSS selector is for the frame in its parent frame, etc... + * Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"] + * @param {InspectorPanel} inspector + * See `selectNode` + * @return {NodeFront} Resolves the corresponding node front. + */ +async function getNodeFrontInFrames(selectors, inspector) { + let walker = inspector.walker; + let rootNode = walker.rootNode; + + // clone the array since `selectors` could be used from callsite after. + selectors = [...selectors]; + // Extract the last selector from the provided array of selectors. + const nodeSelector = selectors.pop(); + + // Remaining selectors should all be frame selectors. Renaming for clarity. + const frameSelectors = selectors; + + info("Loop through all frame selectors"); + for (const frameSelector of frameSelectors) { + const url = walker.targetFront.url; + info(`Find the frame element for selector ${frameSelector} in ${url}`); + + const frameNodeFront = await walker.querySelector(rootNode, frameSelector); + + // If needed, connect to the corresponding frame target. + // Otherwise, reuse the current targetFront. + let frameTarget = frameNodeFront.targetFront; + if (frameNodeFront.useChildTargetToFetchChildren) { + info("Connect to frame and retrieve the targetFront"); + frameTarget = await frameNodeFront.connectToFrame(); + } + + walker = (await frameTarget.getFront("inspector")).walker; + + if (frameNodeFront.useChildTargetToFetchChildren) { + // For frames or browser elements, use the walker's rootNode. + rootNode = walker.rootNode; + } else { + // For same-process frames, select the document front as the root node. + // It is a different node from the walker's rootNode. + info("Retrieve the children of the frame to find the document node"); + const { nodes } = await walker.children(frameNodeFront); + rootNode = nodes.find(n => n.nodeType === Node.DOCUMENT_NODE); + } + } + + return walker.querySelector(rootNode, nodeSelector); +} + +/** + * Helper to select a node in the markup-view, in a nested tree of + * frames/browser elements. The iframes can either be remote or same-process. + * + * Note: "frame" will refer to either "frame" or "browser" in the documentation + * and method. + * + * @param {Array} selectors + * Arrays of CSS selectors from the root document to the node. + * The last CSS selector of the array is for the node in its frame doc. + * The before-last CSS selector is for the frame in its parent frame, etc... + * Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"] + * @param {InspectorPanel} inspector + * See `selectNode` + * @param {String} reason + * See `selectNode` + * @param {Boolean} isSlotted + * See `selectNode` + * @return {NodeFront} The selected node front. + */ +async function selectNodeInFrames( + selectors, + inspector, + reason = "test", + isSlotted +) { + const nodeFront = await getNodeFrontInFrames(selectors, inspector); + await selectNode(nodeFront, inspector, reason, isSlotted); + return nodeFront; +} + +/** + * Create a throttling function that can be manually "flushed". This is to replace the + * use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which + * has a setTimeout that can cause intermittents. + * @return {Function} This function has the same function signature as debounce, but + * the property `.flush()` has been added for flushing out any + * debounced calls. + */ +function manualDebounce() { + let calls = []; + + function debounce(func, wait, scope) { + return function () { + const existingCall = calls.find(call => call.func === func); + if (existingCall) { + existingCall.args = arguments; + } else { + calls.push({ func, wait, scope, args: arguments }); + } + }; + } + + debounce.flush = function () { + calls.forEach(({ func, scope, args }) => func.apply(scope, args)); + calls = []; + }; + + return debounce; +} + +/** + * Get the requested rule style property from the current browser. + * + * @param {Number} styleSheetIndex + * @param {Number} ruleIndex + * @param {String} name + * @return {String} The value, if found, null otherwise + */ + +async function getRulePropertyValue(styleSheetIndex, ruleIndex, name) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [styleSheetIndex, ruleIndex, name], + (styleSheetIndexChild, ruleIndexChild, nameChild) => { + let value = null; + + info( + "Getting the value for property name " + + nameChild + + " in sheet " + + styleSheetIndexChild + + " and rule " + + ruleIndexChild + ); + + const sheet = content.document.styleSheets[styleSheetIndexChild]; + if (sheet) { + const rule = sheet.cssRules[ruleIndexChild]; + if (rule) { + value = rule.style.getPropertyValue(nameChild); + } + } + + return value; + } + ); +} + +/** + * Get the requested computed style property from the current browser. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} propName + * name of the property. + */ +async function getComputedStyleProperty(selector, pseudo, propName) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, pseudo, propName], + (selectorChild, pseudoChild, propNameChild) => { + const element = content.document.querySelector(selectorChild); + return content.document.defaultView + .getComputedStyle(element, pseudoChild) + .getPropertyValue(propNameChild); + } + ); +} + +/** + * Wait until the requested computed style property has the + * expected value in the the current browser. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} propName + * name of the property. + * @param {String} expected + * expected value of property + */ +async function waitForComputedStyleProperty( + selector, + pseudo, + propName, + expected +) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, pseudo, propName, expected], + (selectorChild, pseudoChild, propNameChild, expectedChild) => { + const element = content.document.querySelector(selectorChild); + return ContentTaskUtils.waitForCondition(() => { + const value = content.document.defaultView + .getComputedStyle(element, pseudoChild) + .getPropertyValue(propNameChild); + return value === expectedChild; + }); + } + ); +} + +/** + * Given an inplace editable element, click to switch it to edit mode, wait for + * focus + * + * @return a promise that resolves to the inplace-editor element when ready + */ +var focusEditableField = async function ( + ruleView, + editable, + xOffset = 1, + yOffset = 1, + options = {} +) { + editable.scrollIntoView(); + const onFocus = once(editable.parentNode, "focus", true); + info("Clicking on editable field to turn to edit mode"); + if (options.type === undefined) { + // "mousedown" and "mouseup" flushes any pending layout. Therefore, + // if the caller wants to click an element, e.g., closebrace to add new + // property, we need to guarantee that the element is clicked here even + // if it's moved by flushing the layout because whether the UI is useful + // or not when there is pending reflow is not scope of the tests. + options.type = "mousedown"; + EventUtils.synthesizeMouse( + editable, + xOffset, + yOffset, + options, + editable.ownerGlobal + ); + options.type = "mouseup"; + EventUtils.synthesizeMouse( + editable, + xOffset, + yOffset, + options, + editable.ownerGlobal + ); + } else { + EventUtils.synthesizeMouse( + editable, + xOffset, + yOffset, + options, + editable.ownerGlobal + ); + } + await onFocus; + + info("Editable field gained focus, returning the input field now"); + const onEdit = inplaceEditor(editable.ownerDocument.activeElement); + + return onEdit; +}; + +/** + * Get the DOMNode for a css rule in the rule-view that corresponds to the given + * selector. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view for which the rule + * object is wanted + * @param {Number} index + * If there are more than 1 rule with the same selector, you may pass a + * index to determine which of the rules you want. + * @return {DOMNode} + */ +function getRuleViewRule(view, selectorText, index = 0) { + let rule; + let pos = 0; + for (const r of view.styleDocument.querySelectorAll(".ruleview-rule")) { + const selector = r.querySelector( + ".ruleview-selectors-container, .ruleview-selector.matched" + ); + if (selector && selector.textContent === selectorText) { + if (index == pos) { + rule = r; + break; + } + pos++; + } + } + + return rule; +} + +/** + * Get references to the name and value span nodes corresponding to a given + * selector and property name in the rule-view. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @param {Object=} options + * @param {Boolean=} options.wait + * When true, returns a promise which waits until a valid rule view + * property can be retrieved for the provided selectorText & propertyName. + * Defaults to false. + * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode} + */ +function getRuleViewProperty(view, selectorText, propertyName, options = {}) { + if (options.wait) { + return waitFor(() => + _syncGetRuleViewProperty(view, selectorText, propertyName) + ); + } + return _syncGetRuleViewProperty(view, selectorText, propertyName); +} + +function _syncGetRuleViewProperty(view, selectorText, propertyName) { + const rule = getRuleViewRule(view, selectorText); + if (!rule) { + return null; + } + + // Look for the propertyName in that rule element + for (const p of rule.querySelectorAll(".ruleview-property")) { + const nameSpan = p.querySelector(".ruleview-propertyname"); + const valueSpan = p.querySelector(".ruleview-propertyvalue"); + + if (nameSpan.textContent === propertyName) { + return { nameSpan, valueSpan }; + } + } + return null; +} + +/** + * Get the text value of the property corresponding to a given selector and name + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @return {String} The property value + */ +function getRuleViewPropertyValue(view, selectorText, propertyName) { + return getRuleViewProperty(view, selectorText, propertyName).valueSpan + .textContent; +} + +/** + * Get a reference to the selector DOM element corresponding to a given selector + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for + * @return {DOMNode} The selector DOM element + */ +function getRuleViewSelector(view, selectorText) { + const rule = getRuleViewRule(view, selectorText); + return rule.querySelector( + ".ruleview-selectors-container, .ruleview-selector.matched" + ); +} + +/** + * Get a rule-link from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {DOMNode} The link if any at this index + */ +function getRuleViewLinkByIndex(view, index) { + const links = view.styleDocument.querySelectorAll(".ruleview-rule-source"); + return links[index]; +} + +/** + * Get rule-link text from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {String} The string at this index + */ +function getRuleViewLinkTextByIndex(view, index) { + const link = getRuleViewLinkByIndex(view, index); + return link.querySelector(".ruleview-rule-source-label").textContent; +} + +/** + * Click on a rule-view's close brace to focus a new property name editor + * + * @param {RuleEditor} ruleEditor + * An instance of RuleEditor that will receive the new property + * @return a promise that resolves to the newly created editor when ready and + * focused + */ +var focusNewRuleViewProperty = async function (ruleEditor) { + info("Clicking on a close ruleEditor brace to start editing a new property"); + + // Use bottom alignment to avoid scrolling out of the parent element area. + ruleEditor.closeBrace.scrollIntoView(false); + const editor = await focusEditableField( + ruleEditor.ruleView, + ruleEditor.closeBrace + ); + + is( + inplaceEditor(ruleEditor.newPropSpan), + editor, + "Focused editor is the new property editor." + ); + + return editor; +}; + +/** + * Create a new property name in the rule-view, focusing a new property editor + * by clicking on the close brace, and then entering the given text. + * Keep in mind that the rule-view knows how to handle strings with multiple + * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3". + * + * @param {RuleEditor} ruleEditor + * The instance of RuleEditor that will receive the new property(ies) + * @param {String} inputValue + * The text to be entered in the new property name field + * @return a promise that resolves when the new property name has been entered + * and once the value field is focused + */ +var createNewRuleViewProperty = async function (ruleEditor, inputValue) { + info("Creating a new property editor"); + const editor = await focusNewRuleViewProperty(ruleEditor); + + info("Entering the value " + inputValue); + editor.input.value = inputValue; + + info("Submitting the new value and waiting for value field focus"); + const onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey( + "VK_RETURN", + {}, + ruleEditor.element.ownerDocument.defaultView + ); + await onFocus; +}; + +/** + * Set the search value for the rule-view filter styles search box. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} searchValue + * The filter search value + * @return a promise that resolves when the rule-view is filtered for the + * search term + */ +var setSearchFilter = async function (view, searchValue) { + info('Setting filter text to "' + searchValue + '"'); + + const searchField = view.searchField; + searchField.focus(); + + for (const key of searchValue.split("")) { + EventUtils.synthesizeKey(key, {}, view.styleWindow); + } + + await view.inspector.once("ruleview-filtered"); +}; + +/** + * Flatten all context menu items into a single array to make searching through + * it easier. + */ +function buildContextMenuItems(menu) { + const allItems = [].concat.apply( + [], + menu.items.map(function addItem(item) { + if (item.submenu) { + return addItem(item.submenu.items); + } + return item; + }) + ); + + return allItems; +} + +/** + * Open the style editor context menu and return all of it's items in a flat array + * @param {CssRuleView} view + * The instance of the rule-view panel + * @return An array of MenuItems + */ +function openStyleContextMenuAndGetAllItems(view, target) { + const menu = view.contextMenu._openMenu({ target }); + return buildContextMenuItems(menu); +} + +/** + * Open the inspector menu and return all of it's items in a flat array + * @param {InspectorPanel} inspector + * @param {Object} options to pass into openMenu + * @return An array of MenuItems + */ +function openContextMenuAndGetAllItems(inspector, options) { + const menu = inspector.markup.contextMenu._openMenu(options); + return buildContextMenuItems(menu); +} + +/** + * Wait until the elements the given selectors indicate come to have the visited state. + * + * @param {Tab} tab + * The tab where the elements on. + * @param {Array} selectors + * The selectors for the elements. + */ +async function waitUntilVisitedState(tab, selectors) { + await asyncWaitUntil(async () => { + const hasVisitedState = await ContentTask.spawn( + tab.linkedBrowser, + selectors, + args => { + // ElementState::VISITED + const ELEMENT_STATE_VISITED = 1 << 18; + + for (const selector of args) { + const target = + content.wrappedJSObject.document.querySelector(selector); + if ( + !( + target && + InspectorUtils.getContentState(target) & ELEMENT_STATE_VISITED + ) + ) { + return false; + } + } + return true; + } + ); + return hasVisitedState; + }); +} + +/** + * Return wether or not the passed selector matches an element in the content page. + * + * @param {string} selector + * @returns Promise<Boolean> + */ +function hasMatchingElementInContentPage(selector) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector], + function (innerSelector) { + return content.document.querySelector(innerSelector) !== null; + } + ); +} + +/** + * Return the number of elements matching the passed selector. + * + * @param {string} selector + * @returns Promise<Number> the number of matching elements + */ +function getNumberOfMatchingElementsInContentPage(selector) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector], + function (innerSelector) { + return content.document.querySelectorAll(innerSelector).length; + } + ); +} + +/** + * Get the property of an element in the content page + * + * @param {string} selector: The selector to get the element we want the property of + * @param {string} propertyName: The name of the property we want the value of + * @returns {Promise} A promise that returns with the value of the property for the element + */ +function getContentPageElementProperty(selector, propertyName) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, propertyName], + function (innerSelector, innerPropertyName) { + return content.document.querySelector(innerSelector)[innerPropertyName]; + } + ); +} + +/** + * Set the property of an element in the content page + * + * @param {string} selector: The selector to get the element we want to set the property on + * @param {string} propertyName: The name of the property we want to set + * @param {string} propertyValue: The value that is going to be assigned to the property + * @returns {Promise} + */ +function setContentPageElementProperty(selector, propertyName, propertyValue) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, propertyName, propertyValue], + function (innerSelector, innerPropertyName, innerPropertyValue) { + content.document.querySelector(innerSelector)[innerPropertyName] = + innerPropertyValue; + } + ); +} + +/** + * Get all the attributes for a DOM Node living in the content page. + * + * @param {String} selector The node selector + * @returns {Array<Object>} An array of {name, value} objects. + */ +async function getContentPageElementAttributes(selector) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector], + _selector => { + const node = content.document.querySelector(_selector); + return Array.from(node.attributes).map(({ name, value }) => ({ + name, + value, + })); + } + ); +} + +/** + * Get an attribute on a DOM Node living in the content page. + * + * @param {String} selector The node selector + * @param {String} attribute The attribute name + * @return {String} value The attribute value + */ +async function getContentPageElementAttribute(selector, attribute) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, attribute], + (_selector, _attribute) => { + return content.document.querySelector(_selector).getAttribute(_attribute); + } + ); +} + +/** + * Set an attribute on a DOM Node living in the content page. + * + * @param {String} selector The node selector + * @param {String} attribute The attribute name + * @param {String} value The attribute value + */ +async function setContentPageElementAttribute(selector, attribute, value) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, attribute, value], + (_selector, _attribute, _value) => { + content.document + .querySelector(_selector) + .setAttribute(_attribute, _value); + } + ); +} + +/** + * Remove an attribute from a DOM Node living in the content page. + * + * @param {String} selector The node selector + * @param {String} attribute The attribute name + */ +async function removeContentPageElementAttribute(selector, attribute) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, attribute], + (_selector, _attribute) => { + content.document.querySelector(_selector).removeAttribute(_attribute); + } + ); +} + +/** + * Get the rule editor from the rule-view given its index + * + * @param {CssRuleView} ruleView + * The instance of the rule-view panel + * @param {Number} childrenIndex + * The children index of the element to get + * @param {Number} nodeIndex + * The child node index of the element to get + * @return {DOMNode} The rule editor if any at this index + */ +function getRuleViewRuleEditor(ruleView, childrenIndex, nodeIndex) { + const child = ruleView.element.children[childrenIndex]; + if (!child) { + return null; + } + + return nodeIndex !== undefined + ? child.childNodes[nodeIndex]?._ruleEditor + : child._ruleEditor; +} + +/** + * Get the TextProperty instance corresponding to a CSS declaration + * from a CSS rule in the Rules view. + * + * @param {RuleView} ruleView + * Instance of RuleView. + * @param {Number} ruleIndex + * The index of the CSS rule where to find the declaration. + * @param {Object} declaration + * An object representing the target declaration e.g. { color: red }. + * The first TextProperty instance which matches will be returned. + * @return {TextProperty} + */ +function getTextProperty(ruleView, ruleIndex, declaration) { + const ruleEditor = getRuleViewRuleEditor(ruleView, ruleIndex); + const [[name, value]] = Object.entries(declaration); + const textProp = ruleEditor.rule.textProps.find(prop => { + return prop.name === name && prop.value === value; + }); + + if (!textProp) { + throw Error( + `Declaration ${name}:${value} not found on rule at index ${ruleIndex}` + ); + } + + return textProp; +} + +/** + * Simulate changing the value of a property in a rule in the rule-view. + * + * @param {CssRuleView} ruleView + * The instance of the rule-view panel + * @param {TextProperty} textProp + * The instance of the TextProperty to be changed + * @param {String} value + * The new value to be used. If null is passed, then the value will be + * deleted + * @param {Object} options + * @param {Boolean} options.blurNewProperty + * After the value has been changed, a new property would have been + * focused. This parameter is true by default, and that causes the new + * property to be blurred. Set to false if you don't want this. + * @param {number} options.flushCount + * The ruleview uses a manual flush for tests only, and some properties are + * only updated after several flush. Allow tests to trigger several flushes + * if necessary. Defaults to 1. + */ +async function setProperty( + ruleView, + textProp, + value, + { blurNewProperty = true, flushCount = 1 } = {} +) { + info("Set property to: " + value); + await focusEditableField(ruleView, textProp.editor.valueSpan); + + // Because of the manual flush approach used for tests, we might have an + // unknown number of debounced "preview" requests . Each preview should + // synchronously emit "start-preview-property-value". + // Listen to both this event and "ruleview-changed" which is emitted at the + // end of a preview and make sure each preview completes successfully. + let previewStartedCounter = 0; + const onStartPreview = () => previewStartedCounter++; + ruleView.on("start-preview-property-value", onStartPreview); + + let previewCounter = 0; + const onPreviewApplied = () => previewCounter++; + ruleView.on("ruleview-changed", onPreviewApplied); + + if (value === null) { + const onPopupOpened = once(ruleView.popup, "popup-opened"); + EventUtils.synthesizeKey("VK_DELETE", {}, ruleView.styleWindow); + await onPopupOpened; + } else { + await wait(500); + EventUtils.sendString(value, ruleView.styleWindow); + } + + info(`Flush debounced ruleview methods (remaining: ${flushCount})`); + ruleView.debounce.flush(); + await waitFor(() => previewCounter >= previewStartedCounter); + + flushCount--; + + while (flushCount > 0) { + // Wait for some time before triggering a new flush to let new debounced + // functions queue in-between. + await wait(100); + + info(`Flush debounced ruleview methods (remaining: ${flushCount})`); + ruleView.debounce.flush(); + await waitFor(() => previewCounter >= previewStartedCounter); + + flushCount--; + } + + ruleView.off("start-preview-property-value", onStartPreview); + ruleView.off("ruleview-changed", onPreviewApplied); + + const onValueDone = ruleView.once("ruleview-changed"); + // In case the popup was opened, wait until it closes + let onPopupClosed; + if (ruleView.popup?.isOpen) { + // it might happen that the popup is still in the process of being opened, + // so wait until it's properly opened + await ruleView.popup._pendingShowPromise; + onPopupClosed = once(ruleView.popup, "popup-closed"); + } + + EventUtils.synthesizeKey( + blurNewProperty ? "VK_RETURN" : "VK_TAB", + {}, + ruleView.styleWindow + ); + + info("Waiting for another ruleview-changed after setting property"); + await onValueDone; + + const focusNextOnEnter = Services.prefs.getBoolPref( + "devtools.inspector.rule-view.focusNextOnEnter" + ); + if (blurNewProperty && !focusNextOnEnter) { + info("Force blur on the active element"); + ruleView.styleDocument.activeElement.blur(); + } + await onPopupClosed; +} diff --git a/devtools/client/inspector/test/sjs_slow-loading-image.sjs b/devtools/client/inspector/test/sjs_slow-loading-image.sjs new file mode 100644 index 0000000000..c4b12d7c60 --- /dev/null +++ b/devtools/client/inspector/test/sjs_slow-loading-image.sjs @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.processAsync(); + + const params = new Map( + request.queryString + .replace("?", "") + .split("&") + .map(s => s.split("=")) + ); + const delay = params.has("delay") ? params.get("delay") : 2000; + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + // to avoid garbage collection + timer = null; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/svg+xml", false); + // This is the mozilla logo + response.write(`<svg viewBox="0 0 280 80" width="280" height="80" xmlns="http://www.w3.org/2000/svg"> + <path d="M0 0h279.77v80H0z"/> + <path d="M267.208 57.423c-.545.154-1.007.238-1.468.238-1.637 0-2.406-.7-2.406-2.714V39.74c0-7.987-6.365-11.876-13.891-11.876-5.75 0-8.84.7-14.982 3.175l-1.37 8.058 7.987.853 1.133-3.945c1.637-.853 3.26-1.007 5.357-1.007 5.666 0 5.75 4.267 5.75 7.834v1.16c-1.79-.237-3.805-.307-5.75-.307-7.987 0-16.296 2.014-16.296 10.631 0 7.288 5.735 10.016 10.785 10.016 5.665 0 9.232-3.413 11.247-6.98.461 4.266 3.021 6.98 7.68 6.98 2.168 0 4.42-.615 6.28-1.637l-.056-5.273zm-21.486-.224c-3.022 0-4.113-1.79-4.113-4.043 0-3.805 3.106-4.812 6.673-4.812 1.623 0 3.413.238 5.05.462-.238 5.833-4.043 8.393-7.61 8.393zm-13.443-47.03l-15.136 53.395h-9.861l15.135-53.394h9.862zm-20.325 0l-15.136 53.395h-9.848l15.136-53.394h9.848zm-42.022 18.396h10.478v12.561h-10.478V28.565zm0 22.423h10.478v12.576h-10.478V50.988zm-15.247-.462l7.917.77-2.168 12.268h-30.579l-1.007-5.274 19.248-22.116h-10.939l-1.552 5.428-7.219-.784 1.245-12.267h30.733l.784 5.273-19.417 22.13h11.331l1.623-5.428zm-50.149-22.66c-12.576 0-18.786 8.462-18.786 18.702 0 11.177 7.455 17.765 18.24 17.765 11.177 0 19.249-7.064 19.249-18.24 0-9.779-6.141-18.228-18.703-18.228zm-.238 28.787c-5.427 0-8.225-4.658-8.225-10.715 0-6.602 3.175-10.393 8.31-10.393 4.727 0 8.532 3.175 8.532 10.24 0 6.672-3.413 10.868-8.617 10.868zm-27.557-.699h4.658v7.61H66.725v-19.71c0-6.057-2.014-8.38-5.973-8.38-4.812 0-6.756 3.414-6.756 8.31v12.17h4.658v7.61h-14.66v-19.71c0-6.057-2.015-8.38-5.973-8.38-4.812 0-6.757 3.414-6.757 8.31v12.17h6.673v7.61H16.604v-7.61h4.659V36.16h-4.659v-7.61h14.66v5.274c2.099-3.72 5.75-5.973 10.632-5.973 5.05 0 9.694 2.406 11.414 7.526 1.945-4.658 5.903-7.526 11.415-7.526 6.28 0 12.03 3.805 12.03 12.1v16.003z" fill="#fff"/> + </svg>`); + response.finish(); + }, + delay, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/devtools/client/inspector/test/style_inspector_csp.css b/devtools/client/inspector/test/style_inspector_csp.css new file mode 100644 index 0000000000..74031be4d2 --- /dev/null +++ b/devtools/client/inspector/test/style_inspector_csp.css @@ -0,0 +1,3 @@ +html { + background: red; +} diff --git a/devtools/client/inspector/test/style_inspector_eyedropper_ruleview.css b/devtools/client/inspector/test/style_inspector_eyedropper_ruleview.css new file mode 100644 index 0000000000..b719c60846 --- /dev/null +++ b/devtools/client/inspector/test/style_inspector_eyedropper_ruleview.css @@ -0,0 +1,3 @@ +body { + color: red; +}
\ No newline at end of file |