From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../tests/SimpleTest/AccessibilityUtils.js | 1073 ++++++ testing/mochitest/tests/SimpleTest/ChromeTask.js | 174 + testing/mochitest/tests/SimpleTest/EventUtils.js | 3919 ++++++++++++++++++++ .../tests/SimpleTest/ExtensionTestUtils.js | 180 + .../mochitest/tests/SimpleTest/LogController.js | 96 + testing/mochitest/tests/SimpleTest/MemoryStats.js | 131 + testing/mochitest/tests/SimpleTest/MockObjects.js | 95 + .../mochitest/tests/SimpleTest/MozillaLogger.js | 102 + .../mochitest/tests/SimpleTest/NativeKeyCodes.js | 722 ++++ testing/mochitest/tests/SimpleTest/SimpleTest.js | 2267 +++++++++++ testing/mochitest/tests/SimpleTest/TestRunner.js | 1168 ++++++ .../mochitest/tests/SimpleTest/WindowSnapshot.js | 122 + .../mochitest/tests/SimpleTest/WorkerHandler.js | 46 + .../mochitest/tests/SimpleTest/WorkerSimpleTest.js | 44 + .../tests/SimpleTest/iframe-between-tests.html | 20 + testing/mochitest/tests/SimpleTest/moz.build | 27 + .../mochitest/tests/SimpleTest/paint_listener.js | 109 + testing/mochitest/tests/SimpleTest/setup.js | 386 ++ testing/mochitest/tests/SimpleTest/test.css | 39 + 19 files changed, 10720 insertions(+) create mode 100644 testing/mochitest/tests/SimpleTest/AccessibilityUtils.js create mode 100644 testing/mochitest/tests/SimpleTest/ChromeTask.js create mode 100644 testing/mochitest/tests/SimpleTest/EventUtils.js create mode 100644 testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js create mode 100644 testing/mochitest/tests/SimpleTest/LogController.js create mode 100644 testing/mochitest/tests/SimpleTest/MemoryStats.js create mode 100644 testing/mochitest/tests/SimpleTest/MockObjects.js create mode 100644 testing/mochitest/tests/SimpleTest/MozillaLogger.js create mode 100644 testing/mochitest/tests/SimpleTest/NativeKeyCodes.js create mode 100644 testing/mochitest/tests/SimpleTest/SimpleTest.js create mode 100644 testing/mochitest/tests/SimpleTest/TestRunner.js create mode 100644 testing/mochitest/tests/SimpleTest/WindowSnapshot.js create mode 100644 testing/mochitest/tests/SimpleTest/WorkerHandler.js create mode 100644 testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js create mode 100644 testing/mochitest/tests/SimpleTest/iframe-between-tests.html create mode 100644 testing/mochitest/tests/SimpleTest/moz.build create mode 100644 testing/mochitest/tests/SimpleTest/paint_listener.js create mode 100644 testing/mochitest/tests/SimpleTest/setup.js create mode 100644 testing/mochitest/tests/SimpleTest/test.css (limited to 'testing/mochitest/tests/SimpleTest') diff --git a/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js new file mode 100644 index 0000000000..0e95565019 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js @@ -0,0 +1,1073 @@ +/* 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"; + +/** + * Accessible states used to check node's state from the accessiblity API + * perspective. + * + * Note: if gecko is built with --disable-accessibility, the interfaces + * are not defined. This is why we use getters instead to be able to use + * these statically. + */ + +this.AccessibilityUtils = (function () { + const FORCE_DISABLE_ACCESSIBILITY_PREF = "accessibility.force_disabled"; + + // Accessible states. + const { STATE_FOCUSABLE, STATE_INVISIBLE, STATE_LINKED, STATE_UNAVAILABLE } = + Ci.nsIAccessibleStates; + + // Accessible action for showing long description. + const CLICK_ACTION = "click"; + + // Roles that are considered focusable with the keyboard. + const KEYBOARD_FOCUSABLE_ROLES = new Set([ + Ci.nsIAccessibleRole.ROLE_BUTTONMENU, + Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, + Ci.nsIAccessibleRole.ROLE_COMBOBOX, + Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX, + Ci.nsIAccessibleRole.ROLE_ENTRY, + Ci.nsIAccessibleRole.ROLE_LINK, + Ci.nsIAccessibleRole.ROLE_LISTBOX, + Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, + Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, + Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, + Ci.nsIAccessibleRole.ROLE_SLIDER, + Ci.nsIAccessibleRole.ROLE_SPINBUTTON, + Ci.nsIAccessibleRole.ROLE_SUMMARY, + Ci.nsIAccessibleRole.ROLE_SWITCH, + Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, + ]); + + // Roles that are user interactive. + const INTERACTIVE_ROLES = new Set([ + ...KEYBOARD_FOCUSABLE_ROLES, + Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM, + Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION, + Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION, + Ci.nsIAccessibleRole.ROLE_MENUITEM, + Ci.nsIAccessibleRole.ROLE_OPTION, + Ci.nsIAccessibleRole.ROLE_OUTLINE, + Ci.nsIAccessibleRole.ROLE_OUTLINEITEM, + Ci.nsIAccessibleRole.ROLE_PAGETAB, + Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM, + Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM, + Ci.nsIAccessibleRole.ROLE_RICH_OPTION, + ]); + + // Roles that are considered interactive when they are focusable. + const INTERACTIVE_IF_FOCUSABLE_ROLES = new Set([ + // If article is focusable, we can assume it is inside a feed. + Ci.nsIAccessibleRole.ROLE_ARTICLE, + // Column header can be focusable. + Ci.nsIAccessibleRole.ROLE_COLUMNHEADER, + Ci.nsIAccessibleRole.ROLE_GRID_CELL, + Ci.nsIAccessibleRole.ROLE_MENUBAR, + Ci.nsIAccessibleRole.ROLE_MENUPOPUP, + Ci.nsIAccessibleRole.ROLE_PAGETABLIST, + // Row header can be focusable. + Ci.nsIAccessibleRole.ROLE_ROWHEADER, + Ci.nsIAccessibleRole.ROLE_SCROLLBAR, + Ci.nsIAccessibleRole.ROLE_SEPARATOR, + Ci.nsIAccessibleRole.ROLE_TOOLBAR, + ]); + + // Roles that are considered form controls. + const FORM_ROLES = new Set([ + Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, + Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION, + Ci.nsIAccessibleRole.ROLE_COMBOBOX, + Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX, + Ci.nsIAccessibleRole.ROLE_ENTRY, + Ci.nsIAccessibleRole.ROLE_LISTBOX, + Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, + Ci.nsIAccessibleRole.ROLE_PROGRESSBAR, + Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, + Ci.nsIAccessibleRole.ROLE_SLIDER, + Ci.nsIAccessibleRole.ROLE_SPINBUTTON, + Ci.nsIAccessibleRole.ROLE_SWITCH, + ]); + + const DEFAULT_ENV = Object.freeze({ + // Checks that accessible object has at least one accessible action. + actionCountRule: true, + // Checks that accessible object (and its corresponding node) is focusable + // (has focusable state and its node's tabindex is not set to -1). + focusableRule: true, + // Checks that clickable accessible object (and its corresponding node) has + // appropriate interactive semantics. + ifClickableThenInteractiveRule: true, + // Checks that accessible object has a role that is considered to be + // interactive. + interactiveRule: true, + // Checks that accessible object has a non-empty label. + labelRule: true, + // Checks that a node is enabled and is expected to be enabled via + // the accessibility API. + mustBeEnabled: true, + // Checks that a node has a corresponding accessible object. + mustHaveAccessibleRule: true, + // Checks that accessible object (and its corresponding node) have a non- + // negative tabindex. Platform accessibility API still sets focusable state + // on tabindex=-1 nodes. + nonNegativeTabIndexRule: true, + }); + + let gA11YChecks = false; + + let gEnv = { + ...DEFAULT_ENV, + }; + + /** + * Get role attribute for an accessible object if specified for its + * corresponding {@code DOMNode}. + * + * @param {nsIAccessible} accessible + * Accessible for which to determine its role attribute value. + * + * @returns {String} + * Role attribute value if specified. + */ + function getAriaRoles(accessible) { + try { + return accessible.attributes.getStringProperty("xml-roles"); + } catch (e) { + // No xml-roles. nsPersistentProperties throws if the attribute for a key + // is not found. + } + + return ""; + } + + /** + * Get related accessible objects that are targets of labelled by relation e.g. + * labels. + * @param {nsIAccessible} accessible + * Accessible objects to get labels for. + * + * @returns {Array} + * A list of accessible objects that are labels for a given accessible. + */ + function getLabels(accessible) { + const relation = accessible.getRelationByType( + Ci.nsIAccessibleRelation.RELATION_LABELLED_BY + ); + return [...relation.getTargets().enumerate(Ci.nsIAccessible)]; + } + + /** + * Test if an accessible has a {@code hidden} attribute. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @return {boolean} + * True if the accessible object has a {@code hidden} attribute, false + * otherwise. + */ + function hasHiddenAttribute(accessible) { + let hidden = false; + try { + hidden = accessible.attributes.getStringProperty("hidden"); + } catch (e) {} + // If the property is missing, error will be thrown + return hidden && hidden === "true"; + } + + /** + * Test if an accessible is hidden from the user. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @return {boolean} + * True if accessible is hidden from user, false otherwise. + */ + function isHidden(accessible) { + if (!accessible) { + return true; + } + + while (accessible) { + if (hasHiddenAttribute(accessible)) { + return true; + } + + accessible = accessible.parent; + } + + return false; + } + + /** + * Check if an accessible has a given state. + * + * @param {nsIAccessible} accessible + * Accessible object to test. + * @param {number} stateToMatch + * State to match. + * + * @return {boolean} + * True if |accessible| has |stateToMatch|, false otherwise. + */ + function matchState(accessible, stateToMatch) { + const state = {}; + accessible.getState(state, {}); + + return !!(state.value & stateToMatch); + } + + /** + * Determine if an accessible is a keyboard focusable browser toolbar button. + * Browser toolbar buttons aren't keyboard focusable in the usual way. + * Instead, focus is managed by JS code which sets tabindex on a single + * button at a time. Thus, we need to special case the focusable check for + * these buttons. + */ + function isKeyboardFocusableBrowserToolbarButton(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const toolbar = + node.closest("toolbar") || + node.flattenedTreeParentNode.closest("toolbar"); + if (!toolbar || toolbar.getAttribute("keyNav") != "true") { + return false; + } + // The Go button in the Url Bar is an example of a purposefully + // non-focusable image toolbar button that provides an mouse/touch-only + // control for the search query submission, while a keyboard user could + // press `Enter` to do it. Similarly, two scroll buttons that appear when + // toolbar is overflowing, and keyboard-only users would actually scroll + // tabs in the toolbar while trying to navigate to these controls. When + // toolbarbuttons are redundant for keyboard users, we do not want to + // create an extra tab stop for such controls, thus we are expecting the + // button markup to include `keyNav="false"` attribute to flag it. + if (node.getAttribute("keyNav") == "false") { + const ariaRoles = getAriaRoles(accessible); + return ( + ariaRoles.includes("button") || + accessible.role == Ci.nsIAccessibleRole.ROLE_PUSHBUTTON + ); + } + return node.ownerGlobal.ToolbarKeyboardNavigator._isButton(node); + } + + /** + * Determine if an accessible is a keyboard focusable control within a Firefox + * View list. The main landmark of the Firefox View has role="application" for + * users to expect a custom keyboard navigation pattern. Controls within this + * area aren't keyboard focusable in the usual way. Instead, focus is managed + * by JS code which sets tabindex on a single control within each list at a + * time. Thus, we need to special case the focusable check for these controls. + */ + function isKeyboardFocusableFxviewControlInApplication(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + // Firefox View application rows currently include only buttons and links: + if ( + !node.className.includes("fxview-tab-row-") || + (accessible.role != Ci.nsIAccessibleRole.ROLE_PUSHBUTTON && + accessible.role != Ci.nsIAccessibleRole.ROLE_LINK) + ) { + return false; // Not a button or a link in a Firefox View app. + } + // ToDo: We may eventually need to support intervening generics between + // a list and its listitem here and/or aria-owns lists. + const listitemAcc = accessible.parent; + const listAcc = listitemAcc.parent; + if ( + (!listAcc || listAcc.role != Ci.nsIAccessibleRole.ROLE_LIST) && + (!listitemAcc || listitemAcc.role != Ci.nsIAccessibleRole.ROLE_LISTITEM) + ) { + return false; // This button/link isn't inside a listitem within a list. + } + // All listitems should be not focusable while both a button and a link + // within each list item might have tabindex="-1". + if ( + node.tabIndex && + matchState(accessible, STATE_FOCUSABLE) && + !matchState(listitemAcc, STATE_FOCUSABLE) + ) { + // ToDo: We may eventually need to support lists which use aria-owns here. + // Check that there is only one keyboard reachable control within the list. + const childCount = listAcc.childCount; + let foundFocusable = false; + for (let c = 0; c < childCount; c++) { + const listitem = listAcc.getChildAt(c); + const listitemChildCount = listitem.childCount; + for (let i = 0; i < listitemChildCount; i++) { + const listitemControl = listitem.getChildAt(i); + // Use tabIndex rather than a11y focusable state because all controls + // within the listitem might have tabindex="-1". + if (listitemControl.DOMNode.tabIndex == 0) { + if (foundFocusable) { + // Only one control within a list should be focusable. + // ToDo: Fine-tune the a11y-check error message generated in this case. + // Strictly speaking, it's not ideal that we're performing an action + // from an is function, which normally only queries something without + // any externally observable behaviour. That said, fixing that would + // involve different return values for different cases (not a list, + // too many focusable listitem controls, etc) so we could move the + // a11yFail call to the caller. + a11yFail( + "Only one control should be focusable in a list", + accessible + ); + return false; + } + foundFocusable = true; + } + } + } + return foundFocusable; + } + return false; + } + + /** + * Determine if an accessible is a keyboard focusable option within a listbox. + * We use it in the Url bar results - these controls are't keyboard focusable + * in the usual way. Instead, focus is managed by JS code which sets tabindex + * on a single option at a time. Thus, we need to special case the focusable + * check for these option items. + */ + function isKeyboardFocusableOption(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const urlbarListbox = node.closest(".urlbarView-results"); + if (!urlbarListbox || urlbarListbox.getAttribute("role") != "listbox") { + return false; + } + return node.getAttribute("role") == "option"; + } + + /** + * Determine if an accessible is a keyboard focusable PanelMultiView control. + * These controls aren't keyboard focusable in the usual way. Instead, focus + * is managed by JS code which sets tabindex dynamically. Thus, we need to + * special case the focusable check for these controls. + */ + function isKeyboardFocusablePanelMultiViewControl(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const panelview = node.closest("panelview"); + if (!panelview || panelview.hasAttribute("disablekeynav")) { + return false; + } + return ( + node.ownerGlobal.PanelView.forNode(panelview)._tabNavigableWalker.filter( + node + ) == NodeFilter.FILTER_ACCEPT + ); + } + + /** + * Determine if an accessible is a keyboard focusable tab within a tablist. + * Per the ARIA design pattern, these controls aren't keyboard focusable in + * the usual way. Instead, focus is managed by JS code which sets tabindex on + * a single tab at a time. Thus, we need to special case the focusable check + * for these tab controls. + */ + function isKeyboardFocusableTabInTablist(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + if (accessible.role != Ci.nsIAccessibleRole.ROLE_PAGETAB) { + return false; // Not a tab. + } + // ToDo: We may eventually need to support intervening generics between + // a tab and its tablist here. + const tablist = accessible.parent; + if (!tablist || tablist.role != Ci.nsIAccessibleRole.ROLE_PAGETABLIST) { + return false; // The tab isn't inside a tablist. + } + // ToDo: We may eventually need to support tablists which use + // aria-activedescendant here. + // Check that there is only one keyboard reachable tab. + const childCount = tablist.childCount; + let foundFocusable = false; + for (let c = 0; c < childCount; c++) { + const tab = tablist.getChildAt(c); + // Use tabIndex rather than a11y focusable state because all tabs might + // have tabindex="-1". + if (tab.DOMNode.tabIndex == 0) { + if (foundFocusable) { + // Only one tab within a tablist should be focusable. + // ToDo: Fine-tune the a11y-check error message generated in this case. + // Strictly speaking, it's not ideal that we're performing an action + // from an is function, which normally only queries something without + // any externally observable behaviour. That said, fixing that would + // involve different return values for different cases (not a tab, + // too many focusable tabs, etc) so we could move the a11yFail call + // to the caller. + a11yFail("Only one tab should be focusable in a tablist", accessible); + return false; + } + foundFocusable = true; + } + } + return foundFocusable; + } + + /** + * Determine if an accessible is a keyboard focusable button in the url bar. + * Url bar buttons aren't keyboard focusable in the usual way. Instead, + * focus is managed by JS code which sets tabindex on a single button at a + * time. Thus, we need to special case the focusable check for these buttons. + * This also applies to the search bar buttons that reuse the same pattern. + */ + function isKeyboardFocusableUrlbarButton(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const isUrlBar = + node + .closest(".urlbarView > .search-one-offs") + ?.getAttribute("disabletab") == "true"; + const isSearchBar = + node + .closest("#PopupSearchAutoComplete > .search-one-offs") + ?.getAttribute("is_searchbar") == "true"; + return ( + (isUrlBar || isSearchBar) && + node.getAttribute("tabindex") == "-1" && + node.tagName == "button" && + node.classList.contains("searchbar-engine-one-off-item") + ); + } + + /** + * Determine if an accessible is a keyboard focusable XUL tab. + * Only one tab is focusable at a time, but after focusing it, you can use + * the keyboard to focus other tabs. + */ + function isKeyboardFocusableXULTab(accessible) { + const node = accessible.DOMNode; + return node && XULElement.isInstance(node) && node.tagName == "tab"; + } + + /** + * XUL treecol elements currently aren't focusable, making them inaccessible. + * For now, we don't flag these as a failure to avoid breaking multiple tests. + * ToDo: We should remove this exception after this is fixed in bug 1848397. + */ + function isInaccessibleXulTreecol(node) { + if (!node || !node.ownerGlobal) { + return false; + } + const listheader = node.flattenedTreeParentNode; + if (listheader.tagName !== "listheader" || node.tagName !== "treecol") { + return false; + } + return true; + } + + /** + * Determine if an accessible is a combobox container of the url bar. We + * intentionally leave this element unlabeled, because its child is a search + * input that is the target and main control of this component. In general, we + * want to avoid duplication in the label announcement when a user focuses the + * input. Both NVDA and VO ignore the label on at least one of these controls + * if both have a label. But the bigger concern here is that it's very + * difficult to keep the accessible name synchronized between the combobox and + * the input. Thus, we need to special case the label check for this control. + */ + function isUnlabeledUrlBarCombobox(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const ariaRoles = getAriaRoles(accessible); + // There are only two cases of this pattern: and + const isMozInputBox = + node.tagName == "moz-input-box" && + node.classList.contains("urlbar-input-box"); + const isSearchbar = node.tagName == "searchbar" && node.id == "searchbar"; + return (isMozInputBox || isSearchbar) && ariaRoles.includes("combobox"); + } + + /** + * Determine if an accessible is an option within the url bar. We know each + * url bar option is accessible, but it disappears as soon as it is clicked + * during tests and the a11y-checks do not have time to test the label, + * because the Fluent localization is not yet completed by then. Thus, we + * need to special case the label check for these controls. + */ + function isUnlabeledUrlBarOption(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const ariaRoles = getAriaRoles(accessible); + return ( + node.tagName == "span" && + ariaRoles.includes("option") && + node.classList.contains("urlbarView-row-inner") && + node.hasAttribute("data-l10n-id") + ); + } + + /** + * Determine if an accessible is a menuitem within the XUL menu. We know each + * menuitem is accessible, but it disappears as soon as it is clicked during + * tests and the a11y-checks do not have time to test the label, because the + * Fluent localization is not yet completed by then. Thus, we need to special + * case the label check for these controls. + */ + function isUnlabeledMenuitem(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + let hasLabel = false; + for (const child of node.childNodes) { + if (child.tagName == "label") { + hasLabel = true; + } + } + return ( + accessible.role == Ci.nsIAccessibleRole.ROLE_MENUITEM && + accessible.parent.role == Ci.nsIAccessibleRole.ROLE_MENUPOPUP && + hasLabel && + node.hasAttribute("data-l10n-id") + ); + } + + /** + * Determine if a node is a XUL element for which tabIndex should be ignored. + * Some XUL elements report -1 for the .tabIndex property, even though they + * are in fact keyboard focusable. + */ + function shouldIgnoreTabIndex(node) { + if (!XULElement.isInstance(node)) { + return false; + } + return node.tagName == "label" && node.getAttribute("is") == "text-link"; + } + + /** + * Determine if accessible is focusable with the keyboard. + * + * @param {nsIAccessible} accessible + * Accessible for which to determine if it is keyboard focusable. + * + * @returns {Boolean} + * True if focusable with the keyboard. + */ + function isKeyboardFocusable(accessible) { + if ( + isKeyboardFocusableBrowserToolbarButton(accessible) || + isKeyboardFocusableOption(accessible) || + isKeyboardFocusablePanelMultiViewControl(accessible) || + isKeyboardFocusableUrlbarButton(accessible) || + isKeyboardFocusableXULTab(accessible) || + isKeyboardFocusableTabInTablist(accessible) || + isKeyboardFocusableFxviewControlInApplication(accessible) + ) { + return true; + } + // State will be focusable even if the tabindex is negative. + const node = accessible.DOMNode; + const role = accessible.role; + return ( + matchState(accessible, STATE_FOCUSABLE) && + // Platform accessibility will still report STATE_FOCUSABLE even with the + // tabindex="-1" so we need to check that it is >= 0 to be considered + // keyboard focusable. + (!gEnv.nonNegativeTabIndexRule || + node.tabIndex > -1 || + node.closest('[aria-activedescendant][tabindex="0"]') || + // If an ARIA toolbar uses a roving tabindex, some controls on the + // toolbar might not currently be focusable even though they can be + // reached with arrow keys and become focusable at that point. + ((role == Ci.nsIAccessibleRole.ROLE_PUSHBUTTON || + role == Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON) && + node.closest('[role="toolbar"]')) || + shouldIgnoreTabIndex(node)) + ); + } + + function buildMessage(message, DOMNode) { + if (DOMNode) { + const { id, tagName, className } = DOMNode; + message += `: id: ${id}, tagName: ${tagName}, className: ${className}`; + } + + return message; + } + + /** + * Fail a test with a given message because of an issue with a given + * accessible object. This is used for cases where there's an actual + * accessibility failure that prevents UI from being accessible to keyboard/AT + * users. + * + * @param {String} message + * @param {nsIAccessible} accessible + * Accessible to log along with the failure message. + */ + function a11yFail(message, { DOMNode }) { + SpecialPowers.SimpleTest.ok(false, buildMessage(message, DOMNode)); + } + + /** + * Log a todo statement with a given message because of an issue with a given + * accessible object. This is used for cases where accessibility best + * practices are not followed or for something that is not as severe to be + * considered a failure. + * @param {String} message + * @param {nsIAccessible} accessible + * Accessible to log along with the todo message. + */ + function a11yWarn(message, { DOMNode }) { + SpecialPowers.SimpleTest.todo(false, buildMessage(message, DOMNode)); + } + + /** + * Test if the node's unavailable via the accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object. + */ + function assertEnabled(accessible) { + if (gEnv.mustBeEnabled && matchState(accessible, STATE_UNAVAILABLE)) { + a11yFail( + "Node expected to be enabled but is disabled via the accessibility API", + accessible + ); + } + } + + /** + * Test if it is possible to focus on a node with the keyboard. This method + * also checks for additional keyboard focus issues that might arise. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertFocusable(accessible) { + if ( + gEnv.mustBeEnabled && + gEnv.focusableRule && + !isKeyboardFocusable(accessible) + ) { + const ariaRoles = getAriaRoles(accessible); + // Do not force ARIA combobox or listbox to be focusable. + if (!ariaRoles.includes("combobox") && !ariaRoles.includes("listbox")) { + a11yFail("Node is not focusable via the accessibility API", accessible); + } + + return; + } + + if (!INTERACTIVE_IF_FOCUSABLE_ROLES.has(accessible.role)) { + // ROLE_TABLE is used for grids too which are considered interactive. + if ( + accessible.role === Ci.nsIAccessibleRole.ROLE_TABLE && + !getAriaRoles(accessible).includes("grid") + ) { + a11yWarn( + "Focusable nodes should have interactive semantics", + accessible + ); + + return; + } + } + + if (accessible.DOMNode.tabIndex > 0) { + a11yWarn("Avoid using tabindex attribute greater than zero", accessible); + } + } + + /** + * Test if it is possible to interact with a node via the accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertInteractive(accessible) { + if ( + gEnv.mustBeEnabled && + gEnv.actionCountRule && + accessible.actionCount === 0 + ) { + a11yFail("Node does not support any accessible actions", accessible); + + return; + } + + if ( + gEnv.mustBeEnabled && + gEnv.interactiveRule && + !INTERACTIVE_ROLES.has(accessible.role) + ) { + if ( + // Labels that have a label for relation with their target are clickable. + (accessible.role !== Ci.nsIAccessibleRole.ROLE_LABEL || + accessible.getRelationByType( + Ci.nsIAccessibleRelation.RELATION_LABEL_FOR + ).targetsCount === 0) && + // Images that are inside an anchor (have linked state). + (accessible.role !== Ci.nsIAccessibleRole.ROLE_GRAPHIC || + !matchState(accessible, STATE_LINKED)) + ) { + // Look for click action in the list of actions. + for (let i = 0; i < accessible.actionCount; i++) { + if ( + gEnv.ifClickableThenInteractiveRule && + accessible.getActionName(i) === CLICK_ACTION + ) { + a11yFail( + "Clickable nodes must have interactive semantics", + accessible + ); + } + } + } + + a11yFail( + "Node does not have a correct interactive role and may not be " + + "manipulated via the accessibility API", + accessible + ); + } + } + + /** + * Test if the node is labelled appropriately for accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertLabelled(accessible, allowRecurse = true) { + const { DOMNode } = accessible; + let name = accessible.name; + if (!name) { + if ( + isUnlabeledUrlBarCombobox(accessible) || + isUnlabeledUrlBarOption(accessible) || + isUnlabeledMenuitem(accessible) + ) { + return; + } + // If text has just been inserted into the tree, the a11y engine might not + // have picked it up yet. + forceRefreshDriverTick(DOMNode); + try { + name = accessible.name; + } catch (e) { + // The Accessible died because the DOM node was removed or hidden. + if (gEnv.labelRule) { + a11yWarn("Unlabeled element removed before l10n finished", { + DOMNode, + }); + } + return; + } + const doc = DOMNode.ownerDocument; + if ( + !name && + allowRecurse && + gEnv.labelRule && + doc.hasPendingL10nMutations + ) { + // There are pending async l10n mutations which might result in a valid + // accessible name. Try this check again once l10n is finished. + doc.addEventListener( + "L10nMutationsFinished", + () => { + try { + accessible.name; + } catch (e) { + // The Accessible died because the DOM node was removed or hidden. + a11yWarn("Unlabeled element removed before l10n finished", { + DOMNode, + }); + return; + } + assertLabelled(accessible, false); + }, + { once: true } + ); + return; + } + } + if (name) { + name = name.trim(); + } + if (gEnv.labelRule && !name) { + a11yFail("Interactive elements must be labeled", accessible); + + return; + } + + if (FORM_ROLES.has(accessible.role)) { + const labels = getLabels(accessible); + const hasNameFromVisibleLabel = labels.some( + label => !matchState(label, STATE_INVISIBLE) + ); + + if (!hasNameFromVisibleLabel) { + a11yWarn("Form elements should have a visible text label", accessible); + } + } else if ( + accessible.role === Ci.nsIAccessibleRole.ROLE_LINK && + DOMNode.nodeName === "AREA" && + DOMNode.hasAttribute("href") + ) { + const alt = DOMNode.getAttribute("alt"); + if (alt && alt.trim() !== name) { + a11yFail( + "Use alt attribute to label area elements that have the href attribute", + accessible + ); + } + } + } + + /** + * Test if the node's visible via accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertVisible(accessible) { + if (isHidden(accessible)) { + a11yFail( + "Node is not currently visible via the accessibility API and may not " + + "be manipulated by it", + accessible + ); + } + } + + /** + * Walk node ancestry and force refresh driver tick in every document. + * @param {DOMNode} node + * Node for traversing the ancestry. + */ + function forceRefreshDriverTick(node) { + const wins = []; + let bc = BrowsingContext.getFromWindow(node.ownerDocument.defaultView); // eslint-disable-line + while (bc) { + wins.push(bc.associatedWindow); + bc = bc.embedderWindowGlobal?.browsingContext; + } + + let win = wins.pop(); + while (win) { + // Stop the refresh driver from doing its regular ticks and force two + // refresh driver ticks: first to let layout update and notify a11y, and + // the second to let a11y process updates. + win.windowUtils.advanceTimeAndRefresh(100); + win.windowUtils.advanceTimeAndRefresh(100); + // Go back to normal refresh driver ticks. + win.windowUtils.restoreNormalRefresh(); + win = wins.pop(); + } + } + + /** + * Get an accessible object for a node. + * Note: this method will not resolve if accessible object does not become + * available for a given node. + * + * @param {DOMNode} node + * Node to get the accessible object for. + * + * @return {nsIAccessible} + * Accessibility object for a given node. + */ + function getAccessible(node) { + const accessibilityService = Cc[ + "@mozilla.org/accessibilityService;1" + ].getService(Ci.nsIAccessibilityService); + if (!accessibilityService) { + // This is likely a build with --disable-accessibility + return null; + } + + let acc = accessibilityService.getAccessibleFor(node); + if (acc) { + return acc; + } + + // Force refresh tick throughout document hierarchy + forceRefreshDriverTick(node); + return accessibilityService.getAccessibleFor(node); + } + + /** + * Find the nearest interactive accessible ancestor for a node. + */ + function findInteractiveAccessible(node) { + let acc; + // Walk DOM ancestors until we find one with an accessible. + for (; node && !acc; node = node.flattenedTreeParentNode) { + acc = getAccessible(node); + } + if (!acc) { + // No accessible ancestor. + return acc; + } + // Walk a11y ancestors until we find one which is interactive. + for (; acc; acc = acc.parent) { + if (INTERACTIVE_ROLES.has(acc.role)) { + return acc; + } + } + // No interactive ancestor. + return null; + } + + function runIfA11YChecks(task) { + return (...args) => (gA11YChecks ? task(...args) : null); + } + + /** + * AccessibilityUtils provides utility methods for retrieving accessible objects + * and performing accessibility related checks. + * Current methods: + * assertCanBeClicked + * setEnv + * resetEnv + * + */ + const AccessibilityUtils = { + assertCanBeClicked(node) { + // Click events might fire on an inaccessible or non-interactive + // descendant, even if the test author targeted them at an interactive + // element. For example, if there's a button with an image inside it, + // node might be the image. + const acc = findInteractiveAccessible(node); + if (!acc) { + if (isInaccessibleXulTreecol(node)) { + return; + } + if (gEnv.mustHaveAccessibleRule) { + a11yFail("Node is not accessible via accessibility API", { + DOMNode: node, + }); + } + + return; + } + + assertInteractive(acc); + assertFocusable(acc); + assertVisible(acc); + assertEnabled(acc); + assertLabelled(acc); + }, + + setEnv(env = DEFAULT_ENV) { + gEnv = { + ...DEFAULT_ENV, + ...env, + }; + }, + + resetEnv() { + gEnv = { ...DEFAULT_ENV }; + }, + + reset(a11yChecks = false, testPath = "") { + gA11YChecks = a11yChecks; + + const { Services } = SpecialPowers; + // Disable accessibility service if it is running and if a11y checks are + // disabled. However, don't do this for accessibility engine tests. + if ( + !gA11YChecks && + Services.appinfo.accessibilityEnabled && + !testPath.startsWith("chrome://mochitests/content/browser/accessible/") + ) { + Services.prefs.setIntPref(FORCE_DISABLE_ACCESSIBILITY_PREF, 1); + Services.prefs.clearUserPref(FORCE_DISABLE_ACCESSIBILITY_PREF); + } + + // Reset accessibility environment flags that might've been set within the + // test. + this.resetEnv(); + }, + + init() { + this._shouldHandleClicks = true; + // A top level xul window's DocShell doesn't have a chromeEventHandler + // attribute. In that case, the chrome event handler is just the global + // window object. + this._handler ??= + window.docShell.chromeEventHandler ?? window.docShell.domWindow; + this._handler.addEventListener("click", this, true, true); + }, + + uninit() { + this._handler?.removeEventListener("click", this, true); + this._handler = null; + }, + + /** + * Suppress (or disable suppression of) handling of captured click events. + * This should only be called by EventUtils, etc. when a click event will + * be generated but we know it is not actually a click intended to activate + * a control; e.g. drag/drop. Tests that wish to disable specific checks + * should use setEnv instead. + */ + suppressClickHandling(shouldSuppress) { + this._shouldHandleClicks = !shouldSuppress; + }, + + handleEvent({ composedTarget }) { + if (!this._shouldHandleClicks) { + return; + } + if (composedTarget.tagName.toLowerCase() == "slot") { + // The click occurred on a text node inside a slot. Since events don't + // target text nodes, the event was retargeted to the slot. However, a + // slot isn't itself rendered. To deal with this, use the slot's parent + // instead. + composedTarget = composedTarget.flattenedTreeParentNode; + } + const bounds = + composedTarget.ownerGlobal?.windowUtils?.getBoundsWithoutFlushing( + composedTarget + ); + if (bounds && (bounds.width == 0 || bounds.height == 0)) { + // Some tests click hidden nodes. These clearly aren't testing the UI + // for the node itself (and presumably there is a test somewhere else + // that does). Therefore, we can't (and shouldn't) do a11y checks. + return; + } + this.assertCanBeClicked(composedTarget); + }, + }; + + AccessibilityUtils.assertCanBeClicked = runIfA11YChecks( + AccessibilityUtils.assertCanBeClicked.bind(AccessibilityUtils) + ); + + AccessibilityUtils.setEnv = runIfA11YChecks( + AccessibilityUtils.setEnv.bind(AccessibilityUtils) + ); + + AccessibilityUtils.resetEnv = runIfA11YChecks( + AccessibilityUtils.resetEnv.bind(AccessibilityUtils) + ); + + return AccessibilityUtils; +})(); diff --git a/testing/mochitest/tests/SimpleTest/ChromeTask.js b/testing/mochitest/tests/SimpleTest/ChromeTask.js new file mode 100644 index 0000000000..289f6f2eb2 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ChromeTask.js @@ -0,0 +1,174 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* 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"; + +function ChromeTask_ChromeScript() { + /* eslint-env mozilla/chrome-script */ + + "use strict"; + + const { Assert: AssertCls } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + addMessageListener("chrome-task:spawn", async function (aData) { + let id = aData.id; + let source = aData.runnable || "()=>{}"; + + function getStack(aStack) { + let frames = []; + for (let frame = aStack; frame; frame = frame.caller) { + frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber); + } + return frames.join("\n"); + } + + /* eslint-disable no-unused-vars */ + var Assert = new AssertCls((err, message, stack) => { + sendAsyncMessage("chrome-task:test-result", { + id, + condition: !err, + name: err ? err.message : message, + stack: getStack(err ? err.stack : stack), + }); + }); + + var ok = Assert.ok.bind(Assert); + var is = Assert.equal.bind(Assert); + var isnot = Assert.notEqual.bind(Assert); + + function todo(expr, name) { + sendAsyncMessage("chrome-task:test-todo", { id, expr, name }); + } + + function todo_is(a, b, name) { + sendAsyncMessage("chrome-task:test-todo_is", { id, a, b, name }); + } + + function info(name) { + sendAsyncMessage("chrome-task:test-info", { id, name }); + } + /* eslint-enable no-unused-vars */ + + try { + let runnablestr = ` + (() => { + return (${source}); + })();`; + + // eslint-disable-next-line no-eval + let runnable = eval(runnablestr); + let result = await runnable.call(this, aData.arg); + sendAsyncMessage("chrome-task:complete", { + id, + result, + }); + } catch (ex) { + sendAsyncMessage("chrome-task:complete", { + id, + error: ex.toString(), + }); + } + }); +} + +/** + * This object provides the public module functions. + */ +var ChromeTask = { + /** + * the ChromeScript if it has already been loaded. + */ + _chromeScript: null, + + /** + * Mapping from message id to associated promise. + */ + _promises: new Map(), + + /** + * Incrementing integer to generate unique message id. + */ + _messageID: 1, + + /** + * Creates and starts a new task in the chrome process. + * + * @param arg A single serializable argument that will be passed to the + * task when executed on the content process. + * @param task + * - A generator or function which will be serialized and sent to + * the remote browser to be executed. Unlike Task.spawn, this + * argument may not be an iterator as it will be serialized and + * sent to the remote browser. + * @return A promise object where you can register completion callbacks to be + * called when the task terminates. + * @resolves With the final returned value of the task if it executes + * successfully. + * @rejects An error message if execution fails. + */ + spawn: function ChromeTask_spawn(arg, task) { + // Load the frame script if needed. + let handle = ChromeTask._chromeScript; + if (!handle) { + handle = SpecialPowers.loadChromeScript(ChromeTask_ChromeScript); + handle.addMessageListener("chrome-task:complete", ChromeTask.onComplete); + handle.addMessageListener("chrome-task:test-result", ChromeTask.onResult); + handle.addMessageListener("chrome-task:test-info", ChromeTask.onInfo); + handle.addMessageListener("chrome-task:test-todo", ChromeTask.onTodo); + handle.addMessageListener( + "chrome-task:test-todo_is", + ChromeTask.onTodoIs + ); + ChromeTask._chromeScript = handle; + } + + let deferred = {}; + deferred.promise = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + let id = ChromeTask._messageID++; + ChromeTask._promises.set(id, deferred); + + handle.sendAsyncMessage("chrome-task:spawn", { + id, + runnable: task.toString(), + arg, + }); + + return deferred.promise; + }, + + onComplete(aData) { + let deferred = ChromeTask._promises.get(aData.id); + ChromeTask._promises.delete(aData.id); + + if (aData.error) { + deferred.reject(aData.error); + } else { + deferred.resolve(aData.result); + } + }, + + onResult(aData) { + SimpleTest.record(aData.condition, aData.name); + }, + + onInfo(aData) { + SimpleTest.info(aData.name); + }, + + onTodo(aData) { + SimpleTest.todo(aData.expr, aData.name); + }, + + onTodoIs(aData) { + SimpleTest.todo_is(aData.a, aData.b, aData.name); + }, +}; diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js new file mode 100644 index 0000000000..739c7052ea --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -0,0 +1,3919 @@ +/** + * EventUtils provides some utility methods for creating and sending DOM events. + * + * When adding methods to this file, please add a performance test for it. + */ + +// Certain functions assume this is loaded into browser window scope. +// This is modifiable because certain chrome tests create their own gBrowser. +/* global gBrowser:true */ + +// This file is used both in privileged and unprivileged contexts, so we have to +// be careful about our access to Components.interfaces. We also want to avoid +// naming collisions with anything that might be defined in the scope that imports +// this script. +// +// Even if the real |Components| doesn't exist, we might shim in a simple JS +// placebo for compat. An easy way to differentiate this from the real thing +// is whether the property is read-only or not. The real |Components| property +// is read-only. +/* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */ +window.__defineGetter__("_EU_Ci", function () { + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? Ci : SpecialPowers.Ci; +}); + +window.__defineGetter__("_EU_Cc", function () { + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? Cc : SpecialPowers.Cc; +}); + +window.__defineGetter__("_EU_Cu", function () { + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? Cu : SpecialPowers.Cu; +}); + +window.__defineGetter__("_EU_ChromeUtils", function () { + var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils"); + return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils; +}); + +window.__defineGetter__("_EU_OS", function () { + delete this._EU_OS; + try { + this._EU_OS = _EU_ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ).platform; + } catch (ex) { + this._EU_OS = null; + } + return this._EU_OS; +}); + +function _EU_isMac(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "macosx"; + } + if (aWindow) { + try { + return aWindow.navigator.platform.indexOf("Mac") > -1; + } catch (ex) {} + } + return navigator.platform.indexOf("Mac") > -1; +} + +function _EU_isWin(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "win"; + } + if (aWindow) { + try { + return aWindow.navigator.platform.indexOf("Win") > -1; + } catch (ex) {} + } + return navigator.platform.indexOf("Win") > -1; +} + +function _EU_isLinux(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "linux"; + } + if (aWindow) { + try { + return aWindow.navigator.platform.startsWith("Linux"); + } catch (ex) {} + } + return navigator.platform.startsWith("Linux"); +} + +function _EU_isAndroid(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "android"; + } + if (aWindow) { + try { + return aWindow.navigator.userAgent.includes("Android"); + } catch (ex) {} + } + return navigator.userAgent.includes("Android"); +} + +function _EU_maybeWrap(o) { + // We're used in some contexts where there is no SpecialPowers and also in + // some where it exists but has no wrap() method. And this is somewhat + // independent of whether window.Components is a thing... + var haveWrap = false; + try { + haveWrap = SpecialPowers.wrap != undefined; + } catch (e) { + // Just leave it false. + } + if (!haveWrap) { + // Not much we can do here. + return o; + } + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? o : SpecialPowers.wrap(o); +} + +function _EU_maybeUnwrap(o) { + var haveWrap = false; + try { + haveWrap = SpecialPowers.unwrap != undefined; + } catch (e) { + // Just leave it false. + } + if (!haveWrap) { + // Not much we can do here. + return o; + } + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o); +} + +function _EU_getPlatform() { + if (_EU_isWin()) { + return "windows"; + } + if (_EU_isMac()) { + return "mac"; + } + if (_EU_isAndroid()) { + return "android"; + } + if (_EU_isLinux()) { + return "linux"; + } + return "unknown"; +} + +/** + * promiseElementReadyForUserInput() dispatches mousemove events to aElement + * and waits one of them for a while. Then, returns "resolved" state when it's + * successfully received. Otherwise, if it couldn't receive mousemove event on + * it, this throws an exception. So, aElement must be an element which is + * assumed non-collapsed visible element in the window. + * + * This is useful if you need to synthesize mouse events via the main process + * but your test cannot check whether the element is now in APZ to deliver + * a user input event. + */ +async function promiseElementReadyForUserInput( + aElement, + aWindow = window, + aLogFunc = null +) { + if (typeof aElement == "string") { + aElement = aWindow.document.getElementById(aElement); + } + + function waitForMouseMoveForHittest() { + return new Promise(resolve => { + let timeout; + const onHit = () => { + if (aLogFunc) { + aLogFunc("mousemove received"); + } + aWindow.clearInterval(timeout); + resolve(true); + }; + aElement.addEventListener("mousemove", onHit, { + capture: true, + once: true, + }); + timeout = aWindow.setInterval(() => { + if (aLogFunc) { + aLogFunc("mousemove not received in this 300ms"); + } + aElement.removeEventListener("mousemove", onHit, { + capture: true, + }); + resolve(false); + }, 300); + synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow); + }); + } + for (let i = 0; i < 20; i++) { + if (await waitForMouseMoveForHittest()) { + return Promise.resolve(); + } + } + throw new Error("The element or the window did not become interactive"); +} + +function getElement(id) { + return typeof id == "string" ? document.getElementById(id) : id; +} + +this.$ = this.getElement; + +function computeButton(aEvent) { + if (typeof aEvent.button != "undefined") { + return aEvent.button; + } + return aEvent.type == "contextmenu" ? 2 : 0; +} + +function computeButtons(aEvent, utils) { + if (typeof aEvent.buttons != "undefined") { + return aEvent.buttons; + } + + if (typeof aEvent.button != "undefined") { + return utils.MOUSE_BUTTONS_NOT_SPECIFIED; + } + + if (typeof aEvent.type != "undefined" && aEvent.type != "mousedown") { + return utils.MOUSE_BUTTONS_NO_BUTTON; + } + + return utils.MOUSE_BUTTONS_NOT_SPECIFIED; +} + +/** + * Send a mouse event to the node aTarget (aTarget can be an id, or an + * actual node) . The "event" passed in to aEvent is just a JavaScript + * object with the properties set that the real mouse event object should + * have. This includes the type of the mouse event. Pretty much all those + * properties are optional. + * E.g. to send an click event to the node with id 'node' you might do this: + * + * ``sendMouseEvent({type:'click'}, 'node');`` + */ +function sendMouseEvent(aEvent, aTarget, aWindow) { + if ( + ![ + "click", + "contextmenu", + "dblclick", + "mousedown", + "mouseup", + "mouseover", + "mouseout", + ].includes(aEvent.type) + ) { + throw new Error( + "sendMouseEvent doesn't know about event type '" + aEvent.type + "'" + ); + } + + if (!aWindow) { + aWindow = window; + } + + if (typeof aTarget == "string") { + aTarget = aWindow.document.getElementById(aTarget); + } + + var event = aWindow.document.createEvent("MouseEvent"); + + var typeArg = aEvent.type; + var canBubbleArg = true; + var cancelableArg = true; + var viewArg = aWindow; + var detailArg = + aEvent.detail || + // eslint-disable-next-line no-nested-ternary + (aEvent.type == "click" || + aEvent.type == "mousedown" || + aEvent.type == "mouseup" + ? 1 + : aEvent.type == "dblclick" + ? 2 + : 0); + var screenXArg = aEvent.screenX || 0; + var screenYArg = aEvent.screenY || 0; + var clientXArg = aEvent.clientX || 0; + var clientYArg = aEvent.clientY || 0; + var ctrlKeyArg = aEvent.ctrlKey || false; + var altKeyArg = aEvent.altKey || false; + var shiftKeyArg = aEvent.shiftKey || false; + var metaKeyArg = aEvent.metaKey || false; + var buttonArg = computeButton(aEvent); + var relatedTargetArg = aEvent.relatedTarget || null; + + event.initMouseEvent( + typeArg, + canBubbleArg, + cancelableArg, + viewArg, + detailArg, + screenXArg, + screenYArg, + clientXArg, + clientYArg, + ctrlKeyArg, + altKeyArg, + shiftKeyArg, + metaKeyArg, + buttonArg, + relatedTargetArg + ); + + // If documentURIObject exists or `window` is a stub object, we're in + // a chrome scope, so don't bother trying to go through SpecialPowers. + if (!window.document || window.document.documentURIObject) { + return aTarget.dispatchEvent(event); + } + return SpecialPowers.dispatchEvent(aWindow, aTarget, event); +} + +function isHidden(aElement) { + var box = aElement.getBoundingClientRect(); + return box.width == 0 && box.height == 0; +} + +/** + * Send a drag event to the node aTarget (aTarget can be an id, or an + * actual node) . The "event" passed in to aEvent is just a JavaScript + * object with the properties set that the real drag event object should + * have. This includes the type of the drag event. + */ +function sendDragEvent(aEvent, aTarget, aWindow = window) { + if ( + ![ + "drag", + "dragstart", + "dragend", + "dragover", + "dragenter", + "dragleave", + "drop", + ].includes(aEvent.type) + ) { + throw new Error( + "sendDragEvent doesn't know about event type '" + aEvent.type + "'" + ); + } + + if (typeof aTarget == "string") { + aTarget = aWindow.document.getElementById(aTarget); + } + + /* + * Drag event cannot be performed if the element is hidden, except 'dragend' + * event where the element can becomes hidden after start dragging. + */ + if (aEvent.type != "dragend" && isHidden(aTarget)) { + var targetName = aTarget.nodeName; + if ("id" in aTarget && aTarget.id) { + targetName += "#" + aTarget.id; + } + throw new Error(`${aEvent.type} event target ${targetName} is hidden`); + } + + var event = aWindow.document.createEvent("DragEvent"); + + var typeArg = aEvent.type; + var canBubbleArg = true; + var cancelableArg = true; + var viewArg = aWindow; + var detailArg = aEvent.detail || 0; + var screenXArg = aEvent.screenX || 0; + var screenYArg = aEvent.screenY || 0; + var clientXArg = aEvent.clientX || 0; + var clientYArg = aEvent.clientY || 0; + var ctrlKeyArg = aEvent.ctrlKey || false; + var altKeyArg = aEvent.altKey || false; + var shiftKeyArg = aEvent.shiftKey || false; + var metaKeyArg = aEvent.metaKey || false; + var buttonArg = computeButton(aEvent); + var relatedTargetArg = aEvent.relatedTarget || null; + var dataTransfer = aEvent.dataTransfer || null; + + event.initDragEvent( + typeArg, + canBubbleArg, + cancelableArg, + viewArg, + detailArg, + screenXArg, + screenYArg, + clientXArg, + clientYArg, + ctrlKeyArg, + altKeyArg, + shiftKeyArg, + metaKeyArg, + buttonArg, + relatedTargetArg, + dataTransfer + ); + + if (aEvent._domDispatchOnly) { + return aTarget.dispatchEvent(event); + } + + var utils = _getDOMWindowUtils(aWindow); + return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event); +} + +/** + * Send the char aChar to the focused element. This method handles casing of + * chars (sends the right charcode, and sends a shift key for uppercase chars). + * No other modifiers are handled at this point. + * + * For now this method only works for ASCII characters and emulates the shift + * key state on US keyboard layout. + */ +function sendChar(aChar, aWindow) { + var hasShift; + // Emulate US keyboard layout for the shiftKey state. + switch (aChar) { + case "!": + case "@": + case "#": + case "$": + case "%": + case "^": + case "&": + case "*": + case "(": + case ")": + case "_": + case "+": + case "{": + case "}": + case ":": + case '"': + case "|": + case "<": + case ">": + case "?": + hasShift = true; + break; + default: + hasShift = + aChar.toLowerCase() != aChar.toUpperCase() && + aChar == aChar.toUpperCase(); + break; + } + synthesizeKey(aChar, { shiftKey: hasShift }, aWindow); +} + +/** + * Send the string aStr to the focused element. + * + * For now this method only works for ASCII characters and emulates the shift + * key state on US keyboard layout. + */ +function sendString(aStr, aWindow) { + for (let i = 0; i < aStr.length; ++i) { + // Do not split a surrogate pair to call synthesizeKey. Dispatching two + // sets of keydown and keyup caused by two calls of synthesizeKey is not + // good behavior. It could happen due to a bug, but a surrogate pair should + // be introduced with one key press operation. Therefore, calling it with + // a surrogate pair is the right thing. + // Note that TextEventDispatcher will consider whether a surrogate pair + // should cause one or two keypress events automatically. Therefore, we + // don't need to check the related prefs here. + if ( + (aStr.charCodeAt(i) & 0xfc00) == 0xd800 && + i + 1 < aStr.length && + (aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00 + ) { + sendChar(aStr.substring(i, i + 2), aWindow); + i++; + } else { + sendChar(aStr.charAt(i), aWindow); + } + } +} + +/** + * Send the non-character key aKey to the focused node. + * The name of the key should be the part that comes after ``DOM_VK_`` in the + * KeyEvent constant name for this key. + * No modifiers are handled at this point. + */ +function sendKey(aKey, aWindow) { + var keyName = "VK_" + aKey.toUpperCase(); + synthesizeKey(keyName, { shiftKey: false }, aWindow); +} + +/** + * Parse the key modifier flags from aEvent. Used to share code between + * synthesizeMouse and synthesizeKey. + */ +function _parseModifiers(aEvent, aWindow = window) { + var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils; + var mval = 0; + if (aEvent.shiftKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SHIFT; + } + if (aEvent.ctrlKey) { + mval |= nsIDOMWindowUtils.MODIFIER_CONTROL; + } + if (aEvent.altKey) { + mval |= nsIDOMWindowUtils.MODIFIER_ALT; + } + if (aEvent.metaKey) { + mval |= nsIDOMWindowUtils.MODIFIER_META; + } + if (aEvent.accelKey) { + mval |= _EU_isMac(aWindow) + ? nsIDOMWindowUtils.MODIFIER_META + : nsIDOMWindowUtils.MODIFIER_CONTROL; + } + if (aEvent.altGrKey) { + mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH; + } + if (aEvent.capsLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK; + } + if (aEvent.fnKey) { + mval |= nsIDOMWindowUtils.MODIFIER_FN; + } + if (aEvent.fnLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK; + } + if (aEvent.numLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK; + } + if (aEvent.scrollLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK; + } + if (aEvent.symbolKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL; + } + if (aEvent.symbolLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK; + } + + return mval; +} + +/** + * Synthesize a mouse event on a target. The actual client point is determined + * by taking the aTarget's client box and offseting it by aOffsetX and + * aOffsetY. This allows mouse clicks to be simulated by calling this method. + * + * aEvent is an object which may contain the properties: + * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`, + * `button`, `type`. + * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`. + * + * If the type is specified, an mouse event of that type is fired. Otherwise, + * a mousedown followed by a mouseup is performed. + * + * aWindow is optional, and defaults to the current window object. + * + * Returns whether the event had preventDefault() called on it. + */ +function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + return synthesizeMouseAtPoint( + rect.left + aOffsetX, + rect.top + aOffsetY, + aEvent, + aWindow + ); +} +function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + return synthesizeTouchAtPoint( + rect.left + aOffsetX, + rect.top + aOffsetY, + aEvent, + aWindow + ); +} + +/** + * Return the drag service. Note that if we're in the headless mode, this + * may return null because the service may be never instantiated (e.g., on + * Linux). + */ +function getDragService() { + try { + return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + } catch (e) { + // If we're in the headless mode, the drag service may be never + // instantiated. In this case, an exception is thrown. Let's ignore + // any exceptions since without the drag service, nobody can create a + // drag session. + return null; + } +} + +/** + * End drag session if there is. + * + * TODO: This should synthesize "drop" if necessary. + * + * @param left X offset in the viewport + * @param top Y offset in the viewport + * @param aEvent The event data, the modifiers are applied to the + * "dragend" event. + * @param aWindow The window. + * @return true if handled. In this case, the caller should not + * synthesize DOM events basically. + */ +function _maybeEndDragSession(left, top, aEvent, aWindow) { + const dragService = getDragService(); + const dragSession = dragService?.getCurrentSession(); + if (!dragSession) { + return false; + } + // FIXME: If dragSession.dragAction is not + // nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we + // need to synthesize a "drop" event or call setDragEndPointForTests here to + // set proper left/top to `dragend` event. + try { + dragService.endDragSession(false, _parseModifiers(aEvent, aWindow)); + } catch (e) {} + return true; +} + +function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) { + const dragSession = getDragService()?.getCurrentSession(); + if (!dragSession) { + return false; + } + const target = aWindow.document.elementFromPoint(left, top); + if (target) { + sendDragEvent( + createDragEventObject( + "dragover", + target, + aWindow, + dragSession.dataTransfer, + { + accelKey: aEvent.accelKey, + altKey: aEvent.altKey, + altGrKey: aEvent.altGrKey, + ctrlKey: aEvent.ctrlKey, + metaKey: aEvent.metaKey, + shiftKey: aEvent.shiftKey, + capsLockKey: aEvent.capsLockKey, + fnKey: aEvent.fnKey, + fnLockKey: aEvent.fnLockKey, + numLockKey: aEvent.numLockKey, + scrollLockKey: aEvent.scrollLockKey, + symbolKey: aEvent.symbolKey, + symbolLockKey: aEvent.symbolLockKey, + } + ), + target, + aWindow + ); + } + return true; +} + +/* + * Synthesize a mouse event at a particular point in aWindow. + * + * aEvent is an object which may contain the properties: + * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`, + * `button`, `type`. + * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`. + * + * If the type is specified, an mouse event of that type is fired. Otherwise, + * a mousedown followed by a mouseup is performed. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) { + if (aEvent.allowToHandleDragDrop) { + if (aEvent.type == "mouseup" || !aEvent.type) { + if (_maybeEndDragSession(left, top, aEvent, aWindow)) { + return false; + } + } else if (aEvent.type == "mousemove") { + if (_maybeSynthesizeDragOver(left, top, aEvent, aWindow)) { + return false; + } + } + } + + var utils = _getDOMWindowUtils(aWindow); + var defaultPrevented = false; + + if (utils) { + var button = computeButton(aEvent); + var clickCount = aEvent.clickCount || 1; + var modifiers = _parseModifiers(aEvent, aWindow); + var pressure = "pressure" in aEvent ? aEvent.pressure : 0; + + // aWindow might be cross-origin from us. + var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent; + + // Default source to mouse. + var inputSource = + "inputSource" in aEvent + ? aEvent.inputSource + : MouseEvent.MOZ_SOURCE_MOUSE; + // Compute a pointerId if needed. + var id; + if ("id" in aEvent) { + id = aEvent.id; + } else { + var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN; + id = isFromPen + ? utils.DEFAULT_PEN_POINTER_ID + : utils.DEFAULT_MOUSE_POINTER_ID; + } + + var isDOMEventSynthesized = + "isSynthesized" in aEvent ? aEvent.isSynthesized : true; + var isWidgetEventSynthesized = + "isWidgetEventSynthesized" in aEvent + ? aEvent.isWidgetEventSynthesized + : false; + if ("type" in aEvent && aEvent.type) { + defaultPrevented = utils.sendMouseEvent( + aEvent.type, + left, + top, + button, + clickCount, + modifiers, + false, + pressure, + inputSource, + isDOMEventSynthesized, + isWidgetEventSynthesized, + computeButtons(aEvent, utils), + id + ); + } else { + utils.sendMouseEvent( + "mousedown", + left, + top, + button, + clickCount, + modifiers, + false, + pressure, + inputSource, + isDOMEventSynthesized, + isWidgetEventSynthesized, + computeButtons(Object.assign({ type: "mousedown" }, aEvent), utils), + id + ); + utils.sendMouseEvent( + "mouseup", + left, + top, + button, + clickCount, + modifiers, + false, + pressure, + inputSource, + isDOMEventSynthesized, + isWidgetEventSynthesized, + computeButtons(Object.assign({ type: "mouseup" }, aEvent), utils), + id + ); + } + } + + return defaultPrevented; +} + +function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window) { + var utils = _getDOMWindowUtils(aWindow); + let defaultPrevented = false; + + if (utils) { + var id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID; + var rx = aEvent.rx || 1; + var ry = aEvent.ry || 1; + var angle = aEvent.angle || 0; + var force = aEvent.force || (aEvent.type === "touchend" ? 0 : 1); + var tiltX = aEvent.tiltX || 0; + var tiltY = aEvent.tiltY || 0; + var twist = aEvent.twist || 0; + var modifiers = _parseModifiers(aEvent, aWindow); + + if ("type" in aEvent && aEvent.type) { + defaultPrevented = utils.sendTouchEvent( + aEvent.type, + [id], + [left], + [top], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + modifiers + ); + } else { + utils.sendTouchEvent( + "touchstart", + [id], + [left], + [top], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + modifiers + ); + utils.sendTouchEvent( + "touchend", + [id], + [left], + [top], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + modifiers + ); + } + } + return defaultPrevented; +} + +// Call synthesizeMouse with coordinates at the center of aTarget. +function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + return synthesizeMouse( + aTarget, + rect.width / 2, + rect.height / 2, + aEvent, + aWindow + ); +} +function synthesizeTouchAtCenter(aTarget, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + synthesizeTouchAtPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2, + aEvent, + aWindow + ); +} + +/** + * Synthesize a wheel event without flush layout at a particular point in + * aWindow. + * + * aEvent is an object which may contain the properties: + * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ, + * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, + * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX, + * expectedOverflowDeltaY + * + * deltaMode must be defined, others are ok even if undefined. + * + * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The + * value is just checked as 0 or positive or negative. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeWheelAtPoint(aLeft, aTop, aEvent, aWindow = window) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + var modifiers = _parseModifiers(aEvent, aWindow); + var options = 0; + if (aEvent.isNoLineOrPageDelta) { + options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE; + } + if (aEvent.isMomentum) { + options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM; + } + if (aEvent.isCustomizedByPrefs) { + options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS; + } + if (typeof aEvent.expectedOverflowDeltaX !== "undefined") { + if (aEvent.expectedOverflowDeltaX === 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO; + } else if (aEvent.expectedOverflowDeltaX > 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE; + } else { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE; + } + } + if (typeof aEvent.expectedOverflowDeltaY !== "undefined") { + if (aEvent.expectedOverflowDeltaY === 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO; + } else if (aEvent.expectedOverflowDeltaY > 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE; + } else { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE; + } + } + + // Avoid the JS warnings "reference to undefined property" + if (!aEvent.deltaX) { + aEvent.deltaX = 0; + } + if (!aEvent.deltaY) { + aEvent.deltaY = 0; + } + if (!aEvent.deltaZ) { + aEvent.deltaZ = 0; + } + + var lineOrPageDeltaX = + // eslint-disable-next-line no-nested-ternary + aEvent.lineOrPageDeltaX != null + ? aEvent.lineOrPageDeltaX + : aEvent.deltaX > 0 + ? Math.floor(aEvent.deltaX) + : Math.ceil(aEvent.deltaX); + var lineOrPageDeltaY = + // eslint-disable-next-line no-nested-ternary + aEvent.lineOrPageDeltaY != null + ? aEvent.lineOrPageDeltaY + : aEvent.deltaY > 0 + ? Math.floor(aEvent.deltaY) + : Math.ceil(aEvent.deltaY); + utils.sendWheelEvent( + aLeft, + aTop, + aEvent.deltaX, + aEvent.deltaY, + aEvent.deltaZ, + aEvent.deltaMode, + modifiers, + lineOrPageDeltaX, + lineOrPageDeltaY, + options + ); +} + +/** + * Synthesize a wheel event on a target. The actual client point is determined + * by taking the aTarget's client box and offseting it by aOffsetX and + * aOffsetY. + * + * aEvent is an object which may contain the properties: + * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ, + * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, + * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX, + * expectedOverflowDeltaY + * + * deltaMode must be defined, others are ok even if undefined. + * + * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The + * value is just checked as 0 or positive or negative. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + synthesizeWheelAtPoint( + rect.left + aOffsetX, + rect.top + aOffsetY, + aEvent, + aWindow + ); +} + +const _FlushModes = { + FLUSH: 0, + NOFLUSH: 1, +}; + +function _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aFlushMode = _FlushModes.FLUSH, + aWindow = window +) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + if (utils.isMozAfterPaintPending) { + // If a paint is pending, then APZ may be waiting for a scroll acknowledgement + // from the content thread. If we send a wheel event now, it could be ignored + // by APZ (or its scroll offset could be overridden). To avoid problems we + // just wait for the paint to complete. + aWindow.waitForAllPaintsFlushed(function () { + _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aFlushMode, + aWindow + ); + }); + return; + } + + var onwheel = function () { + SpecialPowers.removeSystemEventListener(window, "wheel", onwheel); + + // Wait one frame since the wheel event has not caused a refresh observer + // to be added yet. + setTimeout(function () { + utils.advanceTimeAndRefresh(1000); + + if (!aCallback) { + utils.advanceTimeAndRefresh(0); + return; + } + + var waitForPaints = function () { + SpecialPowers.Services.obs.removeObserver( + waitForPaints, + "apz-repaints-flushed" + ); + aWindow.waitForAllPaintsFlushed(function () { + utils.restoreNormalRefresh(); + aCallback(); + }); + }; + + SpecialPowers.Services.obs.addObserver( + waitForPaints, + "apz-repaints-flushed" + ); + if (!utils.flushApzRepaints(aWindow)) { + waitForPaints(); + } + }, 0); + }; + + // Listen for the system wheel event, because it happens after all of + // the other wheel events, including legacy events. + SpecialPowers.addSystemEventListener(aWindow, "wheel", onwheel); + if (aFlushMode === _FlushModes.FLUSH) { + synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); + } else { + synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow); + } +} + +/** + * This is a wrapper around synthesizeWheel that waits for the wheel event + * to be dispatched and for the subsequent layout/paints to be flushed. + * + * This requires including paint_listener.js. Tests must call + * DOMWindowUtils.restoreNormalRefresh() before finishing, if they use this + * function. + * + * If no callback is provided, the caller is assumed to have its own method of + * determining scroll completion and the refresh driver is not automatically + * restored. + */ +function sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aWindow = window +) { + _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + _FlushModes.FLUSH, + aWindow + ); +} + +/** + * Similar to sendWheelAndPaint but without flushing layout for obtaining + * ``aTarget`` position in ``aWindow`` before sending the wheel event. + * ``aOffsetX`` and ``aOffsetY`` should be offsets against aWindow. + */ +function sendWheelAndPaintNoFlush( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aWindow = window +) { + _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + _FlushModes.NOFLUSH, + aWindow + ); +} + +function synthesizeNativeTapAtCenter( + aTarget, + aLongTap = false, + aCallback = null, + aWindow = window +) { + let rect = aTarget.getBoundingClientRect(); + return synthesizeNativeTap( + aTarget, + rect.width / 2, + rect.height / 2, + aLongTap, + aCallback, + aWindow + ); +} + +function synthesizeNativeTap( + aTarget, + aOffsetX, + aOffsetY, + aLongTap = false, + aCallback = null, + aWindow = window +) { + let utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + let scale = aWindow.devicePixelRatio; + let rect = aTarget.getBoundingClientRect(); + let x = (aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale; + let y = (aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale; + + let observer = { + observe: (subject, topic, data) => { + if (aCallback && topic == "mouseevent") { + aCallback(data); + } + }, + }; + utils.sendNativeTouchTap(x, y, aLongTap, observer); +} + +/** + * Similar to synthesizeMouse but generates a native widget level event + * (so will actually move the "real" mouse cursor etc. Be careful because + * this can impact later code as well! (e.g. with hover states etc.) + * + * @description There are 3 mutually exclusive ways of indicating the location of the + * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``, + * or pass ``screenX`` and ``screenY``. Do not attempt to mix these. + * + * @param {object} aParams + * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove" + * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element + * @param {Boolean} [aParams.atCenter] + * Instead of offsetX/Y, synthesize the event at center of `target`. + * @param {Number} [aParams.offsetX] + * X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") + * @param {Number} [aParams.offsetY] + * Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") + * @param {Number} [aParams.screenX] + * X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), + * Neither offsetX/Y nor atCenter must be set if this is set. + * @param {Number} [aParams.screenY] + * Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), + * Neither offsetX/Y nor atCenter must be set if this is set. + * @param {String} [aParams.scale="screenPixelsPerCSSPixel"] + * If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used. + * If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel. + * @param {Number} [aParams.button=0] + * Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button + * @param {Object} [aParams.modifiers={}] + * Active modifiers, see `_parseNativeModifiers` + * @param {Window} [aParams.win=window] + * The window to use its utils. Defaults to the window in which EventUtils.js is running. + * @param {Element} [aParams.elementOnWidget=target] + * Defaults to target. If element under the point is in another widget from target's widget, + * e.g., when it's in a XUL , specify this. + */ +function synthesizeNativeMouseEvent(aParams, aCallback = null) { + const { + type, + target, + offsetX, + offsetY, + atCenter, + screenX, + screenY, + scale = "screenPixelsPerCSSPixel", + button = 0, + modifiers = {}, + win = window, + elementOnWidget = target, + } = aParams; + if (atCenter) { + if (offsetX != undefined || offsetY != undefined) { + throw Error( + `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` + ); + } + if (screenX != undefined || screenY != undefined) { + throw Error( + `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` + ); + } + if (!target) { + throw Error("atCenter is specified, but target is not specified"); + } + } else if (offsetX != undefined && offsetY != undefined) { + if (screenX != undefined || screenY != undefined) { + throw Error( + `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` + ); + } + if (!target) { + throw Error( + "offsetX and offsetY are specified, but target is not specified" + ); + } + } else if (screenX != undefined && screenY != undefined) { + if (offsetX != undefined || offsetY != undefined) { + throw Error( + `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` + ); + } + } + const utils = _getDOMWindowUtils(win); + if (!utils) { + return; + } + + const rect = target?.getBoundingClientRect(); + let resolution = 1.0; + try { + resolution = _getDOMWindowUtils(win.top).getResolution(); + } catch (e) { + // XXX How to get mobile viewport scale on Fission+xorigin since + // window.top access isn't allowed due to cross-origin? + } + const scaleValue = (() => { + if (scale === "inScreenPixels") { + return 1.0; + } + if (scale === "screenPixelsPerCSSPixel") { + return win.devicePixelRatio; + } + throw Error(`invalid scale value (${scale}) is specified`); + })(); + // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546), + // so use window.top's mozInnerScreen. But this won't work fission+xorigin + // with mobile viewport until mozInnerScreen returns valid value with + // scale. + const x = (() => { + if (screenX != undefined) { + return screenX * scaleValue; + } + let winInnerOffsetX = win.mozInnerScreenX; + try { + winInnerOffsetX = + win.top.mozInnerScreenX + + (win.mozInnerScreenX - win.top.mozInnerScreenX) * resolution; + } catch (e) { + // XXX fission+xorigin test throws permission denied since win.top is + // cross-origin. + } + return ( + (((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution + + winInnerOffsetX) * + scaleValue + ); + })(); + const y = (() => { + if (screenY != undefined) { + return screenY * scaleValue; + } + let winInnerOffsetY = win.mozInnerScreenY; + try { + winInnerOffsetY = + win.top.mozInnerScreenY + + (win.mozInnerScreenY - win.top.mozInnerScreenY) * resolution; + } catch (e) { + // XXX fission+xorigin test throws permission denied since win.top is + // cross-origin. + } + return ( + (((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution + + winInnerOffsetY) * + scaleValue + ); + })(); + const modifierFlags = _parseNativeModifiers(modifiers); + + const observer = { + observe: (subject, topic, data) => { + if (aCallback && topic == "mouseevent") { + aCallback(data); + } + }, + }; + if (type === "click") { + utils.sendNativeMouseEvent( + x, + y, + utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN, + button, + modifierFlags, + elementOnWidget, + function () { + utils.sendNativeMouseEvent( + x, + y, + utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP, + button, + modifierFlags, + elementOnWidget, + observer + ); + } + ); + return; + } + utils.sendNativeMouseEvent( + x, + y, + (() => { + switch (type) { + case "mousedown": + return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN; + case "mouseup": + return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP; + case "mousemove": + return utils.NATIVE_MOUSE_MESSAGE_MOVE; + default: + throw Error(`Invalid type is specified: ${type}`); + } + })(), + button, + modifierFlags, + elementOnWidget, + observer + ); +} + +function promiseNativeMouseEvent(aParams) { + return new Promise(resolve => synthesizeNativeMouseEvent(aParams, resolve)); +} + +function synthesizeNativeMouseEventAndWaitForEvent(aParams, aCallback) { + const listener = aParams.eventTargetToListen || aParams.target; + const eventType = aParams.eventTypeToWait || aParams.type; + listener.addEventListener(eventType, aCallback, { + capture: true, + once: true, + }); + synthesizeNativeMouseEvent(aParams); +} + +function promiseNativeMouseEventAndWaitForEvent(aParams) { + return new Promise(resolve => + synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve) + ); +} + +/** + * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse + * event to be dispatched to the target content. + * + * This API is supposed to be used in those test cases that synthesize some + * input events to chrome process and have some checks in content. + */ +function synthesizeAndWaitNativeMouseMove( + aTarget, + aOffsetX, + aOffsetY, + aCallback, + aWindow = window +) { + let browser = gBrowser.selectedTab.linkedBrowser; + let mm = browser.messageManager; + let { ContentTask } = _EU_ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" + ); + + let eventRegisteredPromise = new Promise(resolve => { + mm.addMessageListener( + "Test:MouseMoveRegistered", + function processed(message) { + mm.removeMessageListener("Test:MouseMoveRegistered", processed); + resolve(); + } + ); + }); + let eventReceivedPromise = ContentTask.spawn( + browser, + [aOffsetX, aOffsetY], + ([clientX, clientY]) => { + return new Promise(resolve => { + addEventListener("mousemove", function onMouseMoveEvent(e) { + if (e.clientX == clientX && e.clientY == clientY) { + removeEventListener("mousemove", onMouseMoveEvent); + resolve(); + } + }); + sendAsyncMessage("Test:MouseMoveRegistered"); + }); + } + ); + eventRegisteredPromise.then(() => { + synthesizeNativeMouseEvent({ + type: "mousemove", + target: aTarget, + offsetX: aOffsetX, + offsetY: aOffsetY, + win: aWindow, + }); + }); + return eventReceivedPromise; +} + +/** + * Synthesize a key event. It is targeted at whatever would be targeted by an + * actual keypress by the user, typically the focused element. + * + * @param {String} aKey + * Should be either: + * + * - key value (recommended). If you specify a non-printable key name, + * prepend the ``KEY_`` prefix. Otherwise, specifying a printable key, the + * key value should be specified. + * + * - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``). This is available + * only for compatibility with legacy API. Don't use this with new tests. + * + * @param {Object} [aEvent] + * Optional event object with more specifics about the key event to + * synthesize. + * @param {String} [aEvent.code] + * If you don't specify this explicitly, it'll be guessed from aKey + * of US keyboard layout. Note that this value may be different + * between browsers. For example, "Insert" is never set only on + * macOS since actual key operation won't cause this code value. + * In such case, the value becomes empty string. + * If you need to emulate non-US keyboard layout or virtual keyboard + * which doesn't emulate hardware key input, you should set this value + * to empty string explicitly. + * @param {Number} [aEvent.repeat] + * If you emulate auto-repeat, you should set the count of repeat. + * This method will automatically synthesize keydown (and keypress). + * @param {*} aEvent.location + * If you want to specify this, you can specify this explicitly. + * However, if you don't specify this value, it will be computed + * from code value. + * @param {String} aEvent.type + * Basically, you shouldn't specify this. Then, this function will + * synthesize keydown (, keypress) and keyup. + * If keydown is specified, this only fires keydown (and keypress if + * it should be fired). + * If keyup is specified, this only fires keyup. + * @param {Number} aEvent.keyCode + * Must be 0 - 255 (0xFF). If this is specified explicitly, + * .keyCode value is initialized with this value. + * @param {Window} aWindow + * Is optional and defaults to the current window object. + * @param {Function} aCallback + * Is optional and can be used to receive notifications from TIP. + * + * @description + * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``, + * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``scrollLockKey``, + * ``shiftKey``, ``symbolKey``, ``symbolLockKey`` + * Basically, you shouldn't use these attributes. nsITextInputProcessor + * manages modifier key state when you synthesize modifier key events. + * However, if some of these attributes are true, this function activates + * the modifiers only during dispatching the key events. + * Note that if some of these values are false, they are ignored (i.e., + * not inactivated with this function). + * + */ +function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) { + const event = aEvent === undefined || aEvent === null ? {} : aEvent; + let dispatchKeydown = + !("type" in event) || event.type === "keydown" || !event.type; + const dispatchKeyup = + !("type" in event) || event.type === "keyup" || !event.type; + + if (dispatchKeydown && aKey == "KEY_Escape") { + let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event))); + eventForKeydown.type = "keydown"; + if ( + _maybeEndDragSession( + // TODO: We should set the last dragover point instead + 0, + 0, + eventForKeydown, + aWindow + ) + ) { + if (!dispatchKeyup) { + return; + } + // We don't need to dispatch only keydown event because it's consumed by + // the drag session. + dispatchKeydown = false; + } + } + + var TIP = _getTIP(aWindow, aCallback); + if (!TIP) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + var modifiers = _emulateToActivateModifiers(TIP, event, aWindow); + var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow); + var keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + + try { + if (dispatchKeydown) { + TIP.keydown(keyEvent, keyEventDict.flags); + if ("repeat" in event && event.repeat > 1) { + keyEventDict.dictionary.repeat = true; + var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary); + for (var i = 1; i < event.repeat; i++) { + TIP.keydown(repeatedKeyEvent, keyEventDict.flags); + } + } + } + if (dispatchKeyup) { + TIP.keyup(keyEvent, keyEventDict.flags); + } + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} + +/** + * This is a wrapper around synthesizeKey that waits for the key event to be + * dispatched to the target content. It returns a promise which is resolved + * when the content receives the key event. + * + * This API is supposed to be used in those test cases that synthesize some + * input events to chrome process and have some checks in content. + */ +function synthesizeAndWaitKey( + aKey, + aEvent, + aWindow = window, + checkBeforeSynthesize, + checkAfterSynthesize +) { + let browser = gBrowser.selectedTab.linkedBrowser; + let mm = browser.messageManager; + let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow) + .dictionary.keyCode; + let { ContentTask } = _EU_ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" + ); + + let keyRegisteredPromise = new Promise(resolve => { + mm.addMessageListener("Test:KeyRegistered", function processed(message) { + mm.removeMessageListener("Test:KeyRegistered", processed); + resolve(); + }); + }); + // eslint-disable-next-line no-shadow + let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => { + return new Promise(resolve => { + addEventListener("keyup", function onKeyEvent(e) { + if (e.keyCode == keyCode) { + removeEventListener("keyup", onKeyEvent); + resolve(); + } + }); + sendAsyncMessage("Test:KeyRegistered"); + }); + }); + keyRegisteredPromise.then(() => { + if (checkBeforeSynthesize) { + checkBeforeSynthesize(); + } + synthesizeKey(aKey, aEvent, aWindow); + if (checkAfterSynthesize) { + checkAfterSynthesize(); + } + }); + return keyReceivedPromise; +} + +function _parseNativeModifiers(aModifiers, aWindow = window) { + let modifiers = 0; + if (aModifiers.capsLockKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK; + } + if (aModifiers.numLockKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK; + } + if (aModifiers.shiftKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT; + } + if (aModifiers.shiftRightKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT; + } + if (aModifiers.ctrlKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; + } + if (aModifiers.ctrlRightKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; + } + if (aModifiers.altKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT; + } + if (aModifiers.altRightKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT; + } + if (aModifiers.metaKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT; + } + if (aModifiers.metaRightKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT; + } + if (aModifiers.helpKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP; + } + if (aModifiers.fnKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION; + } + if (aModifiers.numericKeyPadKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD; + } + + if (aModifiers.accelKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; + } + if (aModifiers.accelRightKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; + } + if (aModifiers.altGrKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH; + } + return modifiers; +} + +// Mac: Any unused number is okay for adding new keyboard layout. +// When you add new keyboard layout here, you need to modify +// TISInputSourceWrapper::InitByLayoutID(). +// Win: These constants can be found by inspecting registry keys under +// HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts + +const KEYBOARD_LAYOUT_ARABIC = { + name: "Arabic", + Mac: 6, + Win: 0x00000401, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_ARABIC", KEYBOARD_LAYOUT_ARABIC); +const KEYBOARD_LAYOUT_ARABIC_PC = { + name: "Arabic - PC", + Mac: 7, + Win: null, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_ARABIC_PC", KEYBOARD_LAYOUT_ARABIC_PC); +const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = { + name: "Brazilian ABNT", + Mac: null, + Win: 0x00000416, + hasAltGrOnWin: true, +}; +_defineConstant( + "KEYBOARD_LAYOUT_BRAZILIAN_ABNT", + KEYBOARD_LAYOUT_BRAZILIAN_ABNT +); +const KEYBOARD_LAYOUT_DVORAK_QWERTY = { + name: "Dvorak-QWERTY", + Mac: 4, + Win: null, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_DVORAK_QWERTY", KEYBOARD_LAYOUT_DVORAK_QWERTY); +const KEYBOARD_LAYOUT_EN_US = { + name: "US", + Mac: 0, + Win: 0x00000409, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_EN_US", KEYBOARD_LAYOUT_EN_US); +const KEYBOARD_LAYOUT_FRENCH = { + name: "French", + Mac: 8, // Some keys mapped different from PC, e.g., Digit6, Digit8, Equal, Slash and Backslash + Win: 0x0000040c, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_FRENCH", KEYBOARD_LAYOUT_FRENCH); +const KEYBOARD_LAYOUT_FRENCH_PC = { + name: "French-PC", + Mac: 13, // Compatible with Windows + Win: 0x0000040c, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_FRENCH_PC", KEYBOARD_LAYOUT_FRENCH_PC); +const KEYBOARD_LAYOUT_GREEK = { + name: "Greek", + Mac: 1, + Win: 0x00000408, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_GREEK", KEYBOARD_LAYOUT_GREEK); +const KEYBOARD_LAYOUT_GERMAN = { + name: "German", + Mac: 2, + Win: 0x00000407, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_GERMAN", KEYBOARD_LAYOUT_GERMAN); +const KEYBOARD_LAYOUT_HEBREW = { + name: "Hebrew", + Mac: 9, + Win: 0x0000040d, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_HEBREW", KEYBOARD_LAYOUT_HEBREW); +const KEYBOARD_LAYOUT_JAPANESE = { + name: "Japanese", + Mac: null, + Win: 0x00000411, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_JAPANESE", KEYBOARD_LAYOUT_JAPANESE); +const KEYBOARD_LAYOUT_KHMER = { + name: "Khmer", + Mac: null, + Win: 0x00000453, + hasAltGrOnWin: true, +}; // available on Win7 or later. +_defineConstant("KEYBOARD_LAYOUT_KHMER", KEYBOARD_LAYOUT_KHMER); +const KEYBOARD_LAYOUT_LITHUANIAN = { + name: "Lithuanian", + Mac: 10, + Win: 0x00010427, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_LITHUANIAN", KEYBOARD_LAYOUT_LITHUANIAN); +const KEYBOARD_LAYOUT_NORWEGIAN = { + name: "Norwegian", + Mac: 11, + Win: 0x00000414, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_NORWEGIAN", KEYBOARD_LAYOUT_NORWEGIAN); +const KEYBOARD_LAYOUT_RUSSIAN = { + name: "Russian", + Mac: null, + Win: 0x00000419, + hasAltGrOnWin: true, // No AltGr, but Ctrl + Alt + Digit8 introduces a char +}; +_defineConstant("KEYBOARD_LAYOUT_RUSSIAN", KEYBOARD_LAYOUT_RUSSIAN); +const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = { + name: "Russian - Mnemonic", + Mac: null, + Win: 0x00020419, + hasAltGrOnWin: true, +}; // available on Win8 or later. +_defineConstant( + "KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC", + KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC +); +const KEYBOARD_LAYOUT_SPANISH = { + name: "Spanish", + Mac: 12, + Win: 0x0000040a, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_SPANISH", KEYBOARD_LAYOUT_SPANISH); +const KEYBOARD_LAYOUT_SWEDISH = { + name: "Swedish", + Mac: 3, + Win: 0x0000041d, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_SWEDISH", KEYBOARD_LAYOUT_SWEDISH); +const KEYBOARD_LAYOUT_THAI = { + name: "Thai", + Mac: 5, + Win: 0x0002041e, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_THAI", KEYBOARD_LAYOUT_THAI); + +/** + * synthesizeNativeKey() dispatches native key event on active window. + * This is implemented only on Windows and Mac. Note that this function + * dispatches the key event asynchronously and returns immediately. If a + * callback function is provided, the callback will be called upon + * completion of the key dispatch. + * + * @param aKeyboardLayout One of KEYBOARD_LAYOUT_* defined above. + * @param aNativeKeyCode A native keycode value defined in + * NativeKeyCodes.js. + * @param aModifiers Modifier keys. If no modifire key is pressed, + * this must be {}. Otherwise, one or more items + * referred in _parseNativeModifiers() must be + * true. + * @param aChars Specify characters which should be generated + * by the key event. + * @param aUnmodifiedChars Specify characters of unmodified (except Shift) + * aChar value. + * @param aCallback If provided, this callback will be invoked + * once the native keys have been processed + * by Gecko. Will never be called if this + * function returns false. + * @return True if this function succeed dispatching + * native key event. Otherwise, false. + */ + +function synthesizeNativeKey( + aKeyboardLayout, + aNativeKeyCode, + aModifiers, + aChars, + aUnmodifiedChars, + aCallback, + aWindow = window +) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return false; + } + var nativeKeyboardLayout = null; + if (_EU_isMac(aWindow)) { + nativeKeyboardLayout = aKeyboardLayout.Mac; + } else if (_EU_isWin(aWindow)) { + nativeKeyboardLayout = aKeyboardLayout.Win; + } + if (nativeKeyboardLayout === null) { + return false; + } + + var observer = { + observe(aSubject, aTopic, aData) { + if (aCallback && aTopic == "keyevent") { + aCallback(aData); + } + }, + }; + utils.sendNativeKeyEvent( + nativeKeyboardLayout, + aNativeKeyCode, + _parseNativeModifiers(aModifiers, aWindow), + aChars, + aUnmodifiedChars, + observer + ); + return true; +} + +var _gSeenEvent = false; + +/** + * Indicate that an event with an original target of aExpectedTarget and + * a type of aExpectedEvent is expected to be fired, or not expected to + * be fired. + */ +function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) { + if (!aExpectedTarget || !aExpectedEvent) { + return null; + } + + _gSeenEvent = false; + + var type = + aExpectedEvent.charAt(0) == "!" + ? aExpectedEvent.substring(1) + : aExpectedEvent; + var eventHandler = function (event) { + var epassed = + !_gSeenEvent && + event.originalTarget == aExpectedTarget && + event.type == type; + is( + epassed, + true, + aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "") + ); + _gSeenEvent = true; + }; + + aExpectedTarget.addEventListener(type, eventHandler); + return eventHandler; +} + +/** + * Check if the event was fired or not. The event handler aEventHandler + * will be removed. + */ +function _checkExpectedEvent( + aExpectedTarget, + aExpectedEvent, + aEventHandler, + aTestName +) { + if (aEventHandler) { + var expectEvent = aExpectedEvent.charAt(0) != "!"; + var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1); + aExpectedTarget.removeEventListener(type, aEventHandler); + var desc = type + " event"; + if (!expectEvent) { + desc += " not"; + } + is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired"); + } + + _gSeenEvent = false; +} + +/** + * Similar to synthesizeMouse except that a test is performed to see if an + * event is fired at the right target as a result. + * + * aExpectedTarget - the expected originalTarget of the event. + * aExpectedEvent - the expected type of the event, such as 'select'. + * aTestName - the test name when outputing results + * + * To test that an event is not fired, use an expected type preceded by an + * exclamation mark, such as '!select'. This might be used to test that a + * click on a disabled element doesn't fire certain events for instance. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeMouseExpectEvent( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aExpectedTarget, + aExpectedEvent, + aTestName, + aWindow +) { + var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); + synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); + _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); +} + +/** + * Similar to synthesizeKey except that a test is performed to see if an + * event is fired at the right target as a result. + * + * aExpectedTarget - the expected originalTarget of the event. + * aExpectedEvent - the expected type of the event, such as 'select'. + * aTestName - the test name when outputing results + * + * To test that an event is not fired, use an expected type preceded by an + * exclamation mark, such as '!select'. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeKeyExpectEvent( + key, + aEvent, + aExpectedTarget, + aExpectedEvent, + aTestName, + aWindow +) { + var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); + synthesizeKey(key, aEvent, aWindow); + _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); +} + +function disableNonTestMouseEvents(aDisable) { + var domutils = _getDOMWindowUtils(); + domutils.disableNonTestMouseEvents(aDisable); +} + +function _getDOMWindowUtils(aWindow = window) { + // Leave this here as something, somewhere, passes a falsy argument + // to this, causing the |window| default argument not to get picked up. + if (!aWindow) { + aWindow = window; + } + + // If documentURIObject exists or `window` is a stub object, we're in + // a chrome scope, so don't bother trying to go through SpecialPowers. + if (!window.document || window.document.documentURIObject) { + return aWindow.windowUtils; + } + + // we need parent.SpecialPowers for: + // layout/base/tests/test_reftests_with_caret.html + // chrome: toolkit/content/tests/chrome/test_findbar.xul + // chrome: toolkit/content/tests/chrome/test_popup_anchor.xul + if ("SpecialPowers" in window && window.SpecialPowers != undefined) { + return SpecialPowers.getDOMWindowUtils(aWindow); + } + if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) { + return parent.SpecialPowers.getDOMWindowUtils(aWindow); + } + + // TODO: this is assuming we are in chrome space + return aWindow.windowUtils; +} + +function _defineConstant(name, value) { + Object.defineProperty(this, name, { + value, + enumerable: true, + writable: false, + }); +} + +const COMPOSITION_ATTR_RAW_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE; +_defineConstant("COMPOSITION_ATTR_RAW_CLAUSE", COMPOSITION_ATTR_RAW_CLAUSE); +const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE; +_defineConstant( + "COMPOSITION_ATTR_SELECTED_RAW_CLAUSE", + COMPOSITION_ATTR_SELECTED_RAW_CLAUSE +); +const COMPOSITION_ATTR_CONVERTED_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE; +_defineConstant( + "COMPOSITION_ATTR_CONVERTED_CLAUSE", + COMPOSITION_ATTR_CONVERTED_CLAUSE +); +const COMPOSITION_ATTR_SELECTED_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE; +_defineConstant( + "COMPOSITION_ATTR_SELECTED_CLAUSE", + COMPOSITION_ATTR_SELECTED_CLAUSE +); + +var TIPMap = new WeakMap(); + +function _getTIP(aWindow, aCallback) { + if (!aWindow) { + aWindow = window; + } + var tip; + if (TIPMap.has(aWindow)) { + tip = TIPMap.get(aWindow); + } else { + tip = _EU_Cc["@mozilla.org/text-input-processor;1"].createInstance( + _EU_Ci.nsITextInputProcessor + ); + TIPMap.set(aWindow, tip); + } + if (!tip.beginInputTransactionForTests(aWindow, aCallback)) { + tip = null; + TIPMap.delete(aWindow); + } + return tip; +} + +function _getKeyboardEvent(aWindow = window) { + if (typeof KeyboardEvent != "undefined") { + try { + // See if the object can be instantiated; sometimes this yields + // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'. + new KeyboardEvent("", {}); + return KeyboardEvent; + } catch (ex) {} + } + if (typeof content != "undefined" && "KeyboardEvent" in content) { + return content.KeyboardEvent; + } + return aWindow.KeyboardEvent; +} + +// eslint-disable-next-line complexity +function _guessKeyNameFromKeyCode(aKeyCode, aWindow = window) { + var KeyboardEvent = _getKeyboardEvent(aWindow); + switch (aKeyCode) { + case KeyboardEvent.DOM_VK_CANCEL: + return "Cancel"; + case KeyboardEvent.DOM_VK_HELP: + return "Help"; + case KeyboardEvent.DOM_VK_BACK_SPACE: + return "Backspace"; + case KeyboardEvent.DOM_VK_TAB: + return "Tab"; + case KeyboardEvent.DOM_VK_CLEAR: + return "Clear"; + case KeyboardEvent.DOM_VK_RETURN: + return "Enter"; + case KeyboardEvent.DOM_VK_SHIFT: + return "Shift"; + case KeyboardEvent.DOM_VK_CONTROL: + return "Control"; + case KeyboardEvent.DOM_VK_ALT: + return "Alt"; + case KeyboardEvent.DOM_VK_PAUSE: + return "Pause"; + case KeyboardEvent.DOM_VK_EISU: + return "Eisu"; + case KeyboardEvent.DOM_VK_ESCAPE: + return "Escape"; + case KeyboardEvent.DOM_VK_CONVERT: + return "Convert"; + case KeyboardEvent.DOM_VK_NONCONVERT: + return "NonConvert"; + case KeyboardEvent.DOM_VK_ACCEPT: + return "Accept"; + case KeyboardEvent.DOM_VK_MODECHANGE: + return "ModeChange"; + case KeyboardEvent.DOM_VK_PAGE_UP: + return "PageUp"; + case KeyboardEvent.DOM_VK_PAGE_DOWN: + return "PageDown"; + case KeyboardEvent.DOM_VK_END: + return "End"; + case KeyboardEvent.DOM_VK_HOME: + return "Home"; + case KeyboardEvent.DOM_VK_LEFT: + return "ArrowLeft"; + case KeyboardEvent.DOM_VK_UP: + return "ArrowUp"; + case KeyboardEvent.DOM_VK_RIGHT: + return "ArrowRight"; + case KeyboardEvent.DOM_VK_DOWN: + return "ArrowDown"; + case KeyboardEvent.DOM_VK_SELECT: + return "Select"; + case KeyboardEvent.DOM_VK_PRINT: + return "Print"; + case KeyboardEvent.DOM_VK_EXECUTE: + return "Execute"; + case KeyboardEvent.DOM_VK_PRINTSCREEN: + return "PrintScreen"; + case KeyboardEvent.DOM_VK_INSERT: + return "Insert"; + case KeyboardEvent.DOM_VK_DELETE: + return "Delete"; + case KeyboardEvent.DOM_VK_WIN: + return "OS"; + case KeyboardEvent.DOM_VK_CONTEXT_MENU: + return "ContextMenu"; + case KeyboardEvent.DOM_VK_SLEEP: + return "Standby"; + case KeyboardEvent.DOM_VK_F1: + return "F1"; + case KeyboardEvent.DOM_VK_F2: + return "F2"; + case KeyboardEvent.DOM_VK_F3: + return "F3"; + case KeyboardEvent.DOM_VK_F4: + return "F4"; + case KeyboardEvent.DOM_VK_F5: + return "F5"; + case KeyboardEvent.DOM_VK_F6: + return "F6"; + case KeyboardEvent.DOM_VK_F7: + return "F7"; + case KeyboardEvent.DOM_VK_F8: + return "F8"; + case KeyboardEvent.DOM_VK_F9: + return "F9"; + case KeyboardEvent.DOM_VK_F10: + return "F10"; + case KeyboardEvent.DOM_VK_F11: + return "F11"; + case KeyboardEvent.DOM_VK_F12: + return "F12"; + case KeyboardEvent.DOM_VK_F13: + return "F13"; + case KeyboardEvent.DOM_VK_F14: + return "F14"; + case KeyboardEvent.DOM_VK_F15: + return "F15"; + case KeyboardEvent.DOM_VK_F16: + return "F16"; + case KeyboardEvent.DOM_VK_F17: + return "F17"; + case KeyboardEvent.DOM_VK_F18: + return "F18"; + case KeyboardEvent.DOM_VK_F19: + return "F19"; + case KeyboardEvent.DOM_VK_F20: + return "F20"; + case KeyboardEvent.DOM_VK_F21: + return "F21"; + case KeyboardEvent.DOM_VK_F22: + return "F22"; + case KeyboardEvent.DOM_VK_F23: + return "F23"; + case KeyboardEvent.DOM_VK_F24: + return "F24"; + case KeyboardEvent.DOM_VK_NUM_LOCK: + return "NumLock"; + case KeyboardEvent.DOM_VK_SCROLL_LOCK: + return "ScrollLock"; + case KeyboardEvent.DOM_VK_VOLUME_MUTE: + return "AudioVolumeMute"; + case KeyboardEvent.DOM_VK_VOLUME_DOWN: + return "AudioVolumeDown"; + case KeyboardEvent.DOM_VK_VOLUME_UP: + return "AudioVolumeUp"; + case KeyboardEvent.DOM_VK_META: + return "Meta"; + case KeyboardEvent.DOM_VK_ALTGR: + return "AltGraph"; + case KeyboardEvent.DOM_VK_PROCESSKEY: + return "Process"; + case KeyboardEvent.DOM_VK_ATTN: + return "Attn"; + case KeyboardEvent.DOM_VK_CRSEL: + return "CrSel"; + case KeyboardEvent.DOM_VK_EXSEL: + return "ExSel"; + case KeyboardEvent.DOM_VK_EREOF: + return "EraseEof"; + case KeyboardEvent.DOM_VK_PLAY: + return "Play"; + default: + return "Unidentified"; + } +} + +function _createKeyboardEventDictionary( + aKey, + aKeyEvent, + aTIP = null, + aWindow = window +) { + var result = { dictionary: null, flags: 0 }; + var keyCodeIsDefined = "keyCode" in aKeyEvent; + var keyCode = + keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255 + ? aKeyEvent.keyCode + : 0; + var keyName = "Unidentified"; + var code = aKeyEvent.code; + if (!aTIP) { + aTIP = _getTIP(aWindow); + } + if (aKey.indexOf("KEY_") == 0) { + keyName = aKey.substr("KEY_".length); + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + if (code === undefined) { + code = aTIP.computeCodeValueOfNonPrintableKey( + keyName, + aKeyEvent.location + ); + } + } else if (aKey.indexOf("VK_") == 0) { + keyCode = _getKeyboardEvent(aWindow)["DOM_" + aKey]; + if (!keyCode) { + throw new Error("Unknown key: " + aKey); + } + keyName = _guessKeyNameFromKeyCode(keyCode, aWindow); + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + if (code === undefined) { + code = aTIP.computeCodeValueOfNonPrintableKey( + keyName, + aKeyEvent.location + ); + } + } else if (aKey != "") { + keyName = aKey; + if (!keyCodeIsDefined) { + keyCode = aTIP.guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout( + aKey, + aKeyEvent.location + ); + } + if (!keyCode) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO; + } + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY; + if (code === undefined) { + code = aTIP.guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout( + keyName, + aKeyEvent.location + ); + } + } + var locationIsDefined = "location" in aKeyEvent; + if (locationIsDefined && aKeyEvent.location === 0) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD; + } + if (aKeyEvent.doNotMarkKeydownAsProcessed) { + result.flags |= + _EU_Ci.nsITextInputProcessor.KEY_DONT_MARK_KEYDOWN_AS_PROCESSED; + } + if (aKeyEvent.markKeyupAsProcessed) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_MARK_KEYUP_AS_PROCESSED; + } + result.dictionary = { + key: keyName, + code, + location: locationIsDefined ? aKeyEvent.location : 0, + repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false, + keyCode, + }; + return result; +} + +function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) { + if (!aKeyEvent) { + return null; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + + var modifiers = { + normal: [ + { key: "Alt", attr: "altKey" }, + { key: "AltGraph", attr: "altGraphKey" }, + { key: "Control", attr: "ctrlKey" }, + { key: "Fn", attr: "fnKey" }, + { key: "Meta", attr: "metaKey" }, + { key: "Shift", attr: "shiftKey" }, + { key: "Symbol", attr: "symbolKey" }, + { key: _EU_isMac(aWindow) ? "Meta" : "Control", attr: "accelKey" }, + ], + lockable: [ + { key: "CapsLock", attr: "capsLockKey" }, + { key: "FnLock", attr: "fnLockKey" }, + { key: "NumLock", attr: "numLockKey" }, + { key: "ScrollLock", attr: "scrollLockKey" }, + { key: "SymbolLock", attr: "symbolLockKey" }, + ], + }; + + for (let i = 0; i < modifiers.normal.length; i++) { + if (!aKeyEvent[modifiers.normal[i].attr]) { + continue; + } + if (aTIP.getModifierState(modifiers.normal[i].key)) { + continue; // already activated. + } + let event = new KeyboardEvent("", { key: modifiers.normal[i].key }); + aTIP.keydown( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + modifiers.normal[i].activated = true; + } + for (let i = 0; i < modifiers.lockable.length; i++) { + if (!aKeyEvent[modifiers.lockable[i].attr]) { + continue; + } + if (aTIP.getModifierState(modifiers.lockable[i].key)) { + continue; // already activated. + } + let event = new KeyboardEvent("", { key: modifiers.lockable[i].key }); + aTIP.keydown( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + aTIP.keyup( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + modifiers.lockable[i].activated = true; + } + return modifiers; +} + +function _emulateToInactivateModifiers(aTIP, aModifiers, aWindow = window) { + if (!aModifiers) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + for (let i = 0; i < aModifiers.normal.length; i++) { + if (!aModifiers.normal[i].activated) { + continue; + } + let event = new KeyboardEvent("", { key: aModifiers.normal[i].key }); + aTIP.keyup( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + } + for (let i = 0; i < aModifiers.lockable.length; i++) { + if (!aModifiers.lockable[i].activated) { + continue; + } + if (!aTIP.getModifierState(aModifiers.lockable[i].key)) { + continue; // who already inactivated this? + } + let event = new KeyboardEvent("", { key: aModifiers.lockable[i].key }); + aTIP.keydown( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + aTIP.keyup( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + } +} + +/** + * Synthesize a composition event and keydown event and keyup events unless + * you prevent to dispatch them explicitly (see aEvent.key's explanation). + * + * Note that you shouldn't call this with "compositionstart" unless you need to + * test compositionstart event which is NOT followed by compositionupdate + * event immediately. Typically, native IME starts composition with + * a pair of keydown and keyup event and dispatch compositionstart and + * compositionupdate (and non-standard text event) between them. So, in most + * cases, you should call synthesizeCompositionChange() directly. + * If you call this with compositionstart, keyup event will be fired + * immediately after compositionstart. In other words, you should use + * "compositionstart" only when you need to emulate IME which just starts + * composition with compositionstart event but does not send composing text to + * us until committing the composition. This is behavior of some Chinese IMEs. + * + * @param aEvent The composition event information. This must + * have |type| member. The value must be + * "compositionstart", "compositionend", + * "compositioncommitasis" or "compositioncommit". + * + * And also this may have |data| and |locale| which + * would be used for the value of each property of + * the composition event. Note that the |data| is + * ignored if the event type is "compositionstart" + * or "compositioncommitasis". + * + * If |key| is undefined, "keydown" and "keyup" + * events which are marked as "processed by IME" + * are dispatched. If |key| is not null, "keydown" + * and/or "keyup" events are dispatched (if the + * |key.type| is specified as "keydown", only + * "keydown" event is dispatched). Otherwise, + * i.e., if |key| is null, neither "keydown" nor + * "keyup" event is dispatched. + * + * If |key.doNotMarkKeydownAsProcessed| is not true, + * key value and keyCode value of "keydown" event + * will be set to "Process" and DOM_VK_PROCESSKEY. + * If |key.markKeyupAsProcessed| is true, + * key value and keyCode value of "keyup" event + * will be set to "Process" and DOM_VK_PROCESSKEY. + * @param aWindow Optional (If null, current |window| will be used) + * @param aCallback Optional (If non-null, use the callback for + * receiving notifications to IME) + */ +function synthesizeComposition(aEvent, aWindow = window, aCallback) { + var TIP = _getTIP(aWindow, aCallback); + if (!TIP) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); + var keyEventDict = { dictionary: null, flags: 0 }; + var keyEvent = null; + if (aEvent.key && typeof aEvent.key.key === "string") { + keyEventDict = _createKeyboardEventDictionary( + aEvent.key.key, + aEvent.key, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent( + // eslint-disable-next-line no-nested-ternary + aEvent.key.type === "keydown" + ? "keydown" + : aEvent.key.type === "keyup" + ? "keyup" + : "", + keyEventDict.dictionary + ); + } else if (aEvent.key === undefined) { + keyEventDict = _createKeyboardEventDictionary( + "KEY_Process", + {}, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + } + try { + switch (aEvent.type) { + case "compositionstart": + TIP.startComposition(keyEvent, keyEventDict.flags); + break; + case "compositioncommitasis": + TIP.commitComposition(keyEvent, keyEventDict.flags); + break; + case "compositioncommit": + TIP.commitCompositionWith(aEvent.data, keyEvent, keyEventDict.flags); + break; + } + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} +/** + * Synthesize eCompositionChange event which causes a DOM text event, may + * cause compositionupdate event, and causes keydown event and keyup event + * unless you prevent to dispatch them explicitly (see aEvent.key's + * explanation). + * + * Note that if you call this when there is no composition, compositionstart + * event will be fired automatically. This is better than you use + * synthesizeComposition("compositionstart") in most cases. See the + * explanation of synthesizeComposition(). + * + * @param aEvent The compositionchange event's information, this has + * |composition| and |caret| members. |composition| has + * |string| and |clauses| members. |clauses| must be array + * object. Each object has |length| and |attr|. And |caret| + * has |start| and |length|. See the following tree image. + * + * aEvent + * +-- composition + * | +-- string + * | +-- clauses[] + * | +-- length + * | +-- attr + * +-- caret + * | +-- start + * | +-- length + * +-- key + * + * Set the composition string to |composition.string|. Set its + * clauses information to the |clauses| array. + * + * When it's composing, set the each clauses' length to the + * |composition.clauses[n].length|. The sum of the all length + * values must be same as the length of |composition.string|. + * Set nsICompositionStringSynthesizer.ATTR_* to the + * |composition.clauses[n].attr|. + * + * When it's not composing, set 0 to the + * |composition.clauses[0].length| and + * |composition.clauses[0].attr|. + * + * Set caret position to the |caret.start|. It's offset from + * the start of the composition string. Set caret length to + * |caret.length|. If it's larger than 0, it should be wide + * caret. However, current nsEditor doesn't support wide + * caret, therefore, you should always set 0 now. + * + * If |key| is undefined, "keydown" and "keyup" events which + * are marked as "processed by IME" are dispatched. If |key| + * is not null, "keydown" and/or "keyup" events are dispatched + * (if the |key.type| is specified as "keydown", only "keydown" + * event is dispatched). Otherwise, i.e., if |key| is null, + * neither "keydown" nor "keyup" event is dispatched. + * If |key.doNotMarkKeydownAsProcessed| is not true, key value + * and keyCode value of "keydown" event will be set to + * "Process" and DOM_VK_PROCESSKEY. + * If |key.markKeyupAsProcessed| is true key value and keyCode + * value of "keyup" event will be set to "Process" and + * DOM_VK_PROCESSKEY. + * + * @param aWindow Optional (If null, current |window| will be used) + * @param aCallback Optional (If non-null, use the callback for receiving + * notifications to IME) + */ +function synthesizeCompositionChange(aEvent, aWindow = window, aCallback) { + var TIP = _getTIP(aWindow, aCallback); + if (!TIP) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + + if ( + !aEvent.composition || + !aEvent.composition.clauses || + !aEvent.composition.clauses[0] + ) { + return; + } + + TIP.setPendingCompositionString(aEvent.composition.string); + if (aEvent.composition.clauses[0].length) { + for (var i = 0; i < aEvent.composition.clauses.length; i++) { + switch (aEvent.composition.clauses[i].attr) { + case TIP.ATTR_RAW_CLAUSE: + case TIP.ATTR_SELECTED_RAW_CLAUSE: + case TIP.ATTR_CONVERTED_CLAUSE: + case TIP.ATTR_SELECTED_CLAUSE: + TIP.appendClauseToPendingComposition( + aEvent.composition.clauses[i].length, + aEvent.composition.clauses[i].attr + ); + break; + case 0: + // Ignore dummy clause for the argument. + break; + default: + throw new Error("invalid clause attribute specified"); + } + } + } + + if (aEvent.caret) { + TIP.setCaretInPendingComposition(aEvent.caret.start); + } + + var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); + try { + var keyEventDict = { dictionary: null, flags: 0 }; + var keyEvent = null; + if (aEvent.key && typeof aEvent.key.key === "string") { + keyEventDict = _createKeyboardEventDictionary( + aEvent.key.key, + aEvent.key, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent( + // eslint-disable-next-line no-nested-ternary + aEvent.key.type === "keydown" + ? "keydown" + : aEvent.key.type === "keyup" + ? "keyup" + : "", + keyEventDict.dictionary + ); + } else if (aEvent.key === undefined) { + keyEventDict = _createKeyboardEventDictionary( + "KEY_Process", + {}, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + } + TIP.flushPendingComposition(keyEvent, keyEventDict.flags); + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} + +// Must be synchronized with nsIDOMWindowUtils. +const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; +const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001; + +const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0x0000; +const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0x0002; +const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0x0004; +const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0x0008; +const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0x0010; +const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020; +const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0x0040; +const QUERY_CONTENT_FLAG_SELECTION_FIND = 0x0080; +const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0x0100; +const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0x0200; + +const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0x0400; + +const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; +const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001; +const SELECTION_SET_FLAG_REVERSE = 0x0002; + +/** + * Synthesize a query text content event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of getting text. If the length is too long, + * the extra length is ignored. + * @param aIsRelative Optional (If true, aOffset is relative to start of + * composition if there is, or start of selection.) + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return null; + } + var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; + if (aIsRelative === true) { + flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT; + } + return utils.sendQueryContentEvent( + utils.QUERY_TEXT_CONTENT, + aOffset, + aLength, + 0, + 0, + flags + ); +} + +/** + * Synthesize a query selected text event. + * + * @param aSelectionType Optional, one of QUERY_CONTENT_FLAG_SELECTION_*. + * If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will + * be used. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQuerySelectedText(aSelectionType, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; + if (aSelectionType) { + flags |= aSelectionType; + } + + return utils.sendQueryContentEvent( + utils.QUERY_SELECTED_TEXT, + 0, + 0, + 0, + 0, + flags + ); +} + +/** + * Synthesize a query caret rect event. + * + * @param aOffset The caret offset. 0 means left side of the first character + * in the selection root. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryCaretRect(aOffset, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return null; + } + return utils.sendQueryContentEvent( + utils.QUERY_CARET_RECT, + aOffset, + 0, + 0, + 0, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * Synthesize a selection set event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of the text. If the length is too long, + * the extra length is ignored. + * @param aReverse If true, the selection is from |aOffset + aLength| to + * |aOffset|. Otherwise, from |aOffset| to |aOffset + aLength|. + * @param aWindow Optional (If null, current |window| will be used) + * @return True, if succeeded. Otherwise false. + */ +async function synthesizeSelectionSet( + aOffset, + aLength, + aReverse, + aWindow = window +) { + const utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return false; + } + // eSetSelection event will be compared with selection cache in + // IMEContentObserver, but it may have not been updated yet. Therefore, we + // need to flush pending things of IMEContentObserver. + await new Promise(resolve => + aWindow.requestAnimationFrame(() => aWindow.requestAnimationFrame(resolve)) + ); + const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0; + return utils.sendSelectionSetEvent(aOffset, aLength, flags); +} + +/** + * Synthesize a query text rect event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of the text. If the length is too long, + * the extra length is ignored. + * @param aIsRelative Optional (If true, aOffset is relative to start of + * composition if there is, or start of selection.) + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryTextRect(aOffset, aLength, aIsRelative, aWindow) { + if (aIsRelative !== undefined && typeof aIsRelative !== "boolean") { + throw new Error( + "Maybe, you set Window object to the 3rd argument, but it should be a boolean value" + ); + } + var utils = _getDOMWindowUtils(aWindow); + let flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; + if (aIsRelative === true) { + flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT; + } + return utils.sendQueryContentEvent( + utils.QUERY_TEXT_RECT, + aOffset, + aLength, + 0, + 0, + flags + ); +} + +/** + * Synthesize a query text rect array event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of the text. If the length is too long, + * the extra length is ignored. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryTextRectArray(aOffset, aLength, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + return utils.sendQueryContentEvent( + utils.QUERY_TEXT_RECT_ARRAY, + aOffset, + aLength, + 0, + 0, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * Synthesize a query editor rect event. + * + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryEditorRect(aWindow) { + var utils = _getDOMWindowUtils(aWindow); + return utils.sendQueryContentEvent( + utils.QUERY_EDITOR_RECT, + 0, + 0, + 0, + 0, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * Synthesize a character at point event. + * + * @param aX, aY The offset in the client area of the DOM window. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeCharAtPoint(aX, aY, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + return utils.sendQueryContentEvent( + utils.QUERY_CHARACTER_AT_POINT, + 0, + 0, + aX, + aY, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * INTERNAL USE ONLY + * Create an event object to pass to sendDragEvent. + * + * @param aType The string represents drag event type. + * @param aDestElement The element to fire the drag event, used to calculate + * screenX/Y and clientX/Y. + * @param aDestWindow Optional; Defaults to the current window object. + * @param aDataTransfer dataTransfer for current drag session. + * @param aDragEvent The object contains properties to override the event + * object + * @return An object to pass to sendDragEvent. + */ +function createDragEventObject( + aType, + aDestElement, + aDestWindow, + aDataTransfer, + aDragEvent +) { + var destRect = aDestElement.getBoundingClientRect(); + var destClientX = destRect.left + destRect.width / 2; + var destClientY = destRect.top + destRect.height / 2; + var destScreenX = aDestWindow.mozInnerScreenX + destClientX; + var destScreenY = aDestWindow.mozInnerScreenY + destClientY; + if ("clientX" in aDragEvent && !("screenX" in aDragEvent)) { + aDragEvent.screenX = aDestWindow.mozInnerScreenX + aDragEvent.clientX; + } + if ("clientY" in aDragEvent && !("screenY" in aDragEvent)) { + aDragEvent.screenY = aDestWindow.mozInnerScreenY + aDragEvent.clientY; + } + + // Wrap only in plain mochitests + let dataTransfer; + if (aDataTransfer) { + dataTransfer = _EU_maybeUnwrap( + _EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType) + ); + + // Copy over the drop effect. This isn't copied over by Clone, as it uses + // more complex logic in the actual implementation (see + // nsContentUtils::SetDataTransferInEvent for actual impl). + dataTransfer.dropEffect = aDataTransfer.dropEffect; + } + + return Object.assign( + { + type: aType, + screenX: destScreenX, + screenY: destScreenY, + clientX: destClientX, + clientY: destClientY, + dataTransfer, + _domDispatchOnly: aDragEvent._domDispatchOnly, + }, + aDragEvent + ); +} + +/** + * Emulate a event sequence of dragstart, dragenter, and dragover. + * + * @param {Element} aSrcElement + * The element to use to start the drag. + * @param {Element} aDestElement + * The element to fire the dragover, dragenter events + * @param {Array} aDragData + * The data to supply for the data transfer. + * This data is in the format: + * + * [ + * [ + * {"type": value, "data": value }, + * ..., + * ], + * ... + * ] + * + * Pass null to avoid modifying dataTransfer. + * @param {String} [aDropEffect="move"] + * The drop effect to set during the dragstart event, or 'move' if omitted. + * @param {Window} [aWindow=window] + * The window in which the drag happens. Defaults to the window in which + * EventUtils.js is loaded. + * @param {Window} [aDestWindow=aWindow] + * Used when aDestElement is in a different window than aSrcElement. + * Default is to match ``aWindow``. + * @param {Object} [aDragEvent={}] + * Defaults to empty object. Overwrites an object passed to sendDragEvent. + * @return {Array} + * A two element array, where the first element is the value returned + * from sendDragEvent for dragover event, and the second element is the + * dataTransfer for the current drag session. + */ +function synthesizeDragOver( + aSrcElement, + aDestElement, + aDragData, + aDropEffect, + aWindow, + aDestWindow, + aDragEvent = {} +) { + if (!aWindow) { + aWindow = window; + } + if (!aDestWindow) { + aDestWindow = aWindow; + } + + // eslint-disable-next-line mozilla/use-services + const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService( + _EU_Ci.nsIObserverService + ); + const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + var sess = ds.getCurrentSession(); + + // This method runs before other callbacks, and acts as a way to inject the + // initial drag data into the DataTransfer. + function fillDrag(event) { + if (aDragData) { + for (var i = 0; i < aDragData.length; i++) { + var item = aDragData[i]; + for (var j = 0; j < item.length; j++) { + _EU_maybeWrap(event.dataTransfer).mozSetDataAt( + item[j].type, + item[j].data, + i + ); + } + } + } + event.dataTransfer.dropEffect = aDropEffect || "move"; + event.preventDefault(); + } + + function trapDrag(subject, topic) { + if (topic == "on-datatransfer-available") { + sess.dataTransfer = _EU_maybeUnwrap( + _EU_maybeWrap(subject).mozCloneForEvent("drop") + ); + sess.dataTransfer.dropEffect = subject.dropEffect; + } + } + + // need to use real mouse action + aWindow.addEventListener("dragstart", fillDrag, true); + obs.addObserver(trapDrag, "on-datatransfer-available"); + synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow); + + var rect = aSrcElement.getBoundingClientRect(); + var x = rect.width / 2; + var y = rect.height / 2; + synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow); + synthesizeMouse(aSrcElement, x + 10, y + 10, { type: "mousemove" }, aWindow); + aWindow.removeEventListener("dragstart", fillDrag, true); + obs.removeObserver(trapDrag, "on-datatransfer-available"); + + var dataTransfer = sess.dataTransfer; + if (!dataTransfer) { + throw new Error("No data transfer object after synthesizing the mouse!"); + } + + // The EventStateManager will fire our dragenter event if it needs to. + var event = createDragEventObject( + "dragover", + aDestElement, + aDestWindow, + dataTransfer, + aDragEvent + ); + var result = sendDragEvent(event, aDestElement, aDestWindow); + + return [result, dataTransfer]; +} + +/** + * Emulate the drop event and mouseup event. + * This should be called after synthesizeDragOver. + * + * @param {*} aResult + * The first element of the array returned from ``synthesizeDragOver``. + * @param {DataTransfer} aDataTransfer + * The second element of the array returned from ``synthesizeDragOver``. + * @param {Element} aDestElement + * The element on which to fire the drop event. + * @param {Window} [aDestWindow=window] + * The window in which the drop happens. Defaults to the window in which + * EventUtils.js is loaded. + * @param {Object} [aDragEvent={}] + * Defaults to empty object. Overwrites an object passed to sendDragEvent. + * @return {String} + * "none" if aResult is true, ``aDataTransfer.dropEffect`` otherwise. + */ +function synthesizeDropAfterDragOver( + aResult, + aDataTransfer, + aDestElement, + aDestWindow, + aDragEvent = {} +) { + if (!aDestWindow) { + aDestWindow = window; + } + + var effect = aDataTransfer.dropEffect; + var event; + + if (aResult) { + effect = "none"; + } else if (effect != "none") { + event = createDragEventObject( + "drop", + aDestElement, + aDestWindow, + aDataTransfer, + aDragEvent + ); + sendDragEvent(event, aDestElement, aDestWindow); + } + // Don't run accessibility checks for this click, since we're not actually + // clicking. It's just generated as part of the drop. + // this.AccessibilityUtils might not be set if this isn't a browser test or + // if a browser test has loaded its own copy of EventUtils for some reason. + // In the latter case, the test probably shouldn't do that. + this.AccessibilityUtils?.suppressClickHandling(true); + synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow); + this.AccessibilityUtils?.suppressClickHandling(false); + + return effect; +} + +/** + * Emulate a drag and drop by emulating a dragstart and firing events dragenter, + * dragover, and drop. + * + * @param {Element} aSrcElement + * The element to use to start the drag. + * @param {Element} aDestElement + * The element to fire the dragover, dragenter events + * @param {Array} aDragData + * The data to supply for the data transfer. + * This data is in the format: + * + * [ + * [ + * {"type": value, "data": value }, + * ..., + * ], + * ... + * ] + * + * Pass null to avoid modifying dataTransfer. + * @param {String} [aDropEffect="move"] + * The drop effect to set during the dragstart event, or 'move' if omitted.. + * @param {Window} [aWindow=window] + * The window in which the drag happens. Defaults to the window in which + * EventUtils.js is loaded. + * @param {Window} [aDestWindow=aWindow] + * Used when aDestElement is in a different window than aSrcElement. + * Default is to match ``aWindow``. + * @param {Object} [aDragEvent={}] + * Defaults to empty object. Overwrites an object passed to sendDragEvent. + * @return {String} + * The drop effect that was desired. + */ +function synthesizeDrop( + aSrcElement, + aDestElement, + aDragData, + aDropEffect, + aWindow, + aDestWindow, + aDragEvent = {} +) { + if (!aWindow) { + aWindow = window; + } + if (!aDestWindow) { + aDestWindow = aWindow; + } + + var ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + + let dropAction; + switch (aDropEffect) { + case null: + case undefined: + case "move": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE; + break; + case "copy": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY; + break; + case "link": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK; + break; + default: + throw new Error(`${aDropEffect} is an invalid drop effect value`); + } + + ds.startDragSessionForTests(dropAction); + + try { + var [result, dataTransfer] = synthesizeDragOver( + aSrcElement, + aDestElement, + aDragData, + aDropEffect, + aWindow, + aDestWindow, + aDragEvent + ); + return synthesizeDropAfterDragOver( + result, + dataTransfer, + aDestElement, + aDestWindow, + aDragEvent + ); + } finally { + ds.endDragSession(true, _parseModifiers(aDragEvent)); + } +} + +function _getFlattenedTreeParentNode(aNode) { + return _EU_maybeUnwrap(_EU_maybeWrap(aNode).flattenedTreeParentNode); +} + +function _getInclusiveFlattenedTreeParentElement(aNode) { + for ( + let inclusiveAncestor = aNode; + inclusiveAncestor; + inclusiveAncestor = _getFlattenedTreeParentNode(inclusiveAncestor) + ) { + if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) { + return inclusiveAncestor; + } + } + return null; +} + +function _nodeIsFlattenedTreeDescendantOf( + aPossibleDescendant, + aPossibleAncestor +) { + do { + if (aPossibleDescendant == aPossibleAncestor) { + return true; + } + aPossibleDescendant = _getFlattenedTreeParentNode(aPossibleDescendant); + } while (aPossibleDescendant); + return false; +} + +function _computeSrcElementFromSrcSelection(aSrcSelection) { + let srcElement = aSrcSelection.focusNode; + while (_EU_maybeWrap(srcElement).isNativeAnonymous) { + srcElement = _getFlattenedTreeParentNode(srcElement); + } + if (srcElement.nodeType !== Node.ELEMENT_NODE) { + srcElement = _getInclusiveFlattenedTreeParentElement(srcElement); + } + return srcElement; +} + +/** + * Emulate a drag and drop by emulating a dragstart by mousedown and mousemove, + * and firing events dragenter, dragover, drop, and dragend. + * This does not modify dataTransfer and tries to emulate the plain drag and + * drop as much as possible, compared to synthesizeDrop. + * Note that if synthesized dragstart is canceled, this throws an exception + * because in such case, Gecko does not start drag session. + * + * @param {Object} aParams + * @param {Event} aParams.dragEvent + * The DnD events will be generated with modifiers specified with this. + * @param {Element} aParams.srcElement + * The element to start dragging. If srcSelection is + * set, this is computed for element at focus node. + * @param {Selection|nil} aParams.srcSelection + * The selection to start to drag, set null if srcElement is set. + * @param {Element|nil} aParams.destElement + * The element to drop on. Pass null to emulate a drop on an invalid target. + * @param {Number} aParams.srcX + * The initial x coordinate inside srcElement or ignored if srcSelection is set. + * @param {Number} aParams.srcY + * The initial y coordinate inside srcElement or ignored if srcSelection is set. + * @param {Number} aParams.stepX + * The x-axis step for mousemove inside srcElement + * @param {Number} aParams.stepY + * The y-axis step for mousemove inside srcElement + * @param {Number} aParams.finalX + * The final x coordinate inside srcElement + * @param {Number} aParams.finalY + * The final x coordinate inside srcElement + * @param {Any} aParams.id + * The pointer event id + * @param {Window} aParams.srcWindow + * The window for dispatching event on srcElement, defaults to the current window object. + * @param {Window} aParams.destWindow + * The window for dispatching event on destElement, defaults to the current window object. + * @param {Boolean} aParams.expectCancelDragStart + * Set to true if the test cancels "dragstart" + * @param {Boolean} aParams.expectSrcElementDisconnected + * Set to true if srcElement will be disconnected and + * "dragend" event won't be fired. + * @param {Function} aParams.logFunc + * Set function which takes one argument if you need to log rect of target. E.g., `console.log`. + */ +// eslint-disable-next-line complexity +async function synthesizePlainDragAndDrop(aParams) { + let { + dragEvent = {}, + srcElement, + srcSelection, + destElement, + srcX = 2, + srcY = 2, + stepX = 9, + stepY = 9, + finalX = srcX + stepX * 2, + finalY = srcY + stepY * 2, + id = _getDOMWindowUtils(window).DEFAULT_MOUSE_POINTER_ID, + srcWindow = window, + destWindow = window, + expectCancelDragStart = false, + expectSrcElementDisconnected = false, + logFunc, + } = aParams; + // Don't modify given dragEvent object because we modify dragEvent below and + // callers may use the object multiple times so that callers must not assume + // that it'll be modified. + if (aParams.dragEvent !== undefined) { + dragEvent = Object.assign({}, aParams.dragEvent); + } + + function rectToString(aRect) { + return `left: ${aRect.left}, top: ${aRect.top}, right: ${aRect.right}, bottom: ${aRect.bottom}`; + } + + if (logFunc) { + logFunc("synthesizePlainDragAndDrop() -- START"); + } + + if (srcSelection) { + srcElement = _computeSrcElementFromSrcSelection(srcSelection); + let srcElementRect = srcElement.getBoundingClientRect(); + if (logFunc) { + logFunc( + `srcElement.getBoundingClientRect(): ${rectToString(srcElementRect)}` + ); + } + // Use last selection client rect because nsIDragSession.sourceNode is + // initialized from focus node which is usually in last rect. + let selectionRectList = srcSelection.getRangeAt(0).getClientRects(); + let lastSelectionRect = selectionRectList[selectionRectList.length - 1]; + if (logFunc) { + logFunc( + `srcSelection.getRangeAt(0).getClientRects()[${ + selectionRectList.length - 1 + }]: ${rectToString(lastSelectionRect)}` + ); + } + // Click at center of last selection rect. + srcX = Math.floor(lastSelectionRect.left + lastSelectionRect.width / 2); + srcY = Math.floor(lastSelectionRect.top + lastSelectionRect.height / 2); + // Then, adjust srcX and srcY for making them offset relative to + // srcElementRect because they will be used when we call synthesizeMouse() + // with srcElement. + srcX = Math.floor(srcX - srcElementRect.left); + srcY = Math.floor(srcY - srcElementRect.top); + // Finally, recalculate finalX and finalY with new srcX and srcY if they + // are not specified by the caller. + if (aParams.finalX === undefined) { + finalX = srcX + stepX * 2; + } + if (aParams.finalY === undefined) { + finalY = srcY + stepY * 2; + } + } else if (logFunc) { + logFunc( + `srcElement.getBoundingClientRect(): ${rectToString( + srcElement.getBoundingClientRect() + )}` + ); + } + + const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + + const editingHost = (() => { + if (!srcElement.matches(":read-write")) { + return null; + } + let lastEditableElement = srcElement; + for ( + let inclusiveAncestor = + _getInclusiveFlattenedTreeParentElement(srcElement); + inclusiveAncestor; + inclusiveAncestor = _getInclusiveFlattenedTreeParentElement( + _getFlattenedTreeParentNode(inclusiveAncestor) + ) + ) { + if (inclusiveAncestor.matches(":read-write")) { + lastEditableElement = inclusiveAncestor; + if (lastEditableElement == srcElement.ownerDocument.body) { + break; + } + } + } + return lastEditableElement; + })(); + try { + _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(true); + + await new Promise(r => setTimeout(r, 0)); + + let mouseDownEvent; + function onMouseDown(aEvent) { + mouseDownEvent = aEvent; + if (logFunc) { + logFunc( + `"${aEvent.type}" event is fired on ${ + aEvent.target + } (composedTarget: ${_EU_maybeUnwrap( + _EU_maybeWrap(aEvent).composedTarget + )}` + ); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + srcElement + ) + ) { + // If srcX and srcY does not point in one of rects in srcElement, + // "mousedown" target is not in srcElement. Such case must not + // be expected by this API users so that we should throw an exception + // for making debugging easier. + throw new Error( + 'event target of "mousedown" is not srcElement nor its descendant' + ); + } + } + try { + srcWindow.addEventListener("mousedown", onMouseDown, { capture: true }); + synthesizeMouse( + srcElement, + srcX, + srcY, + { type: "mousedown", id }, + srcWindow + ); + if (logFunc) { + logFunc(`mousedown at ${srcX}, ${srcY}`); + } + if (!mouseDownEvent) { + throw new Error('"mousedown" event is not fired'); + } + } finally { + srcWindow.removeEventListener("mousedown", onMouseDown, { + capture: true, + }); + } + + let dragStartEvent; + function onDragStart(aEvent) { + dragStartEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + srcElement + ) + ) { + // If srcX and srcY does not point in one of rects in srcElement, + // "dragstart" target is not in srcElement. Such case must not + // be expected by this API users so that we should throw an exception + // for making debugging easier. + throw new Error( + 'event target of "dragstart" is not srcElement nor its descendant' + ); + } + } + let dragEnterEvent; + function onDragEnterGenerated(aEvent) { + dragEnterEvent = aEvent; + } + srcWindow.addEventListener("dragstart", onDragStart, { capture: true }); + srcWindow.addEventListener("dragenter", onDragEnterGenerated, { + capture: true, + }); + try { + // Wait for the next event tick after each event dispatch, so that UI + // elements (e.g. menu) work like the real user input. + await new Promise(r => setTimeout(r, 0)); + + srcX += stepX; + srcY += stepY; + synthesizeMouse( + srcElement, + srcX, + srcY, + { type: "mousemove", id }, + srcWindow + ); + if (logFunc) { + logFunc(`first mousemove at ${srcX}, ${srcY}`); + } + + await new Promise(r => setTimeout(r, 0)); + + srcX += stepX; + srcY += stepY; + synthesizeMouse( + srcElement, + srcX, + srcY, + { type: "mousemove", id }, + srcWindow + ); + if (logFunc) { + logFunc(`second mousemove at ${srcX}, ${srcY}`); + } + + await new Promise(r => setTimeout(r, 0)); + + if (!dragStartEvent) { + throw new Error('"dragstart" event is not fired'); + } + } finally { + srcWindow.removeEventListener("dragstart", onDragStart, { + capture: true, + }); + srcWindow.removeEventListener("dragenter", onDragEnterGenerated, { + capture: true, + }); + } + + let session = ds.getCurrentSession(); + if (!session) { + if (expectCancelDragStart) { + synthesizeMouse( + srcElement, + finalX, + finalY, + { type: "mouseup", id }, + srcWindow + ); + return; + } + throw new Error("drag hasn't been started by the operation"); + } else if (expectCancelDragStart) { + throw new Error("drag has been started by the operation"); + } + + if (destElement) { + if ( + (srcElement != destElement && !dragEnterEvent) || + destElement != dragEnterEvent.target + ) { + if (logFunc) { + logFunc( + `destElement.getBoundingClientRect(): ${rectToString( + destElement.getBoundingClientRect() + )}` + ); + } + + function onDragEnter(aEvent) { + dragEnterEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if (aEvent.target != destElement) { + throw new Error('event target of "dragenter" is not destElement'); + } + } + destWindow.addEventListener("dragenter", onDragEnter, { + capture: true, + }); + try { + let event = createDragEventObject( + "dragenter", + destElement, + destWindow, + null, + dragEvent + ); + sendDragEvent(event, destElement, destWindow); + if (!dragEnterEvent && !destElement.disabled) { + throw new Error('"dragenter" event is not fired'); + } + if (dragEnterEvent && destElement.disabled) { + throw new Error( + '"dragenter" event should not be fired on disable element' + ); + } + } finally { + destWindow.removeEventListener("dragenter", onDragEnter, { + capture: true, + }); + } + } + + let dragOverEvent; + function onDragOver(aEvent) { + dragOverEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if (aEvent.target != destElement) { + throw new Error('event target of "dragover" is not destElement'); + } + } + destWindow.addEventListener("dragover", onDragOver, { capture: true }); + try { + // dragover and drop are only fired to a valid drop target. If the + // destElement parameter is null, this function is being used to + // simulate a drag'n'drop over an invalid drop target. + let event = createDragEventObject( + "dragover", + destElement, + destWindow, + null, + dragEvent + ); + sendDragEvent(event, destElement, destWindow); + if (!dragOverEvent && !destElement.disabled) { + throw new Error('"dragover" event is not fired'); + } + if (dragEnterEvent && destElement.disabled) { + throw new Error( + '"dragover" event should not be fired on disable element' + ); + } + } finally { + destWindow.removeEventListener("dragover", onDragOver, { + capture: true, + }); + } + + await new Promise(r => setTimeout(r, 0)); + + // If there is not accept to drop the data, "drop" event shouldn't be + // fired. + // XXX nsIDragSession.canDrop is different only on Linux. It must be + // a bug of gtk/nsDragService since it manages `mCanDrop` by itself. + // Thus, we should use nsIDragSession.dragAction instead. + if (session.dragAction != _EU_Ci.nsIDragService.DRAGDROP_ACTION_NONE) { + let dropEvent; + function onDrop(aEvent) { + dropEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + destElement + ) + ) { + throw new Error( + 'event target of "drop" is not destElement nor its descendant' + ); + } + } + destWindow.addEventListener("drop", onDrop, { capture: true }); + try { + let event = createDragEventObject( + "drop", + destElement, + destWindow, + null, + dragEvent + ); + sendDragEvent(event, destElement, destWindow); + if (!dropEvent && session.canDrop) { + throw new Error('"drop" event is not fired'); + } + } finally { + destWindow.removeEventListener("drop", onDrop, { capture: true }); + } + return; + } + } + + // Since we don't synthesize drop event, we need to set drag end point + // explicitly for "dragEnd" event which will be fired by + // endDragSession(). + dragEvent.clientX = finalX; + dragEvent.clientY = finalY; + let event = createDragEventObject( + "dragend", + destElement || srcElement, + destElement ? srcWindow : destWindow, + null, + dragEvent + ); + session.setDragEndPointForTests(event.screenX, event.screenY); + } finally { + await new Promise(r => setTimeout(r, 0)); + + if (ds.getCurrentSession()) { + const sourceNode = ds.sourceNode; + let dragEndEvent; + function onDragEnd(aEvent) { + dragEndEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + srcElement + ) && + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) != editingHost + ) { + throw new Error( + 'event target of "dragend" is not srcElement nor its descendant' + ); + } + if (expectSrcElementDisconnected) { + throw new Error( + `"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${ + sourceNode?.isConnected ? "connected" : "null or disconnected" + })` + ); + } + } + srcWindow.addEventListener("dragend", onDragEnd, { capture: true }); + try { + ds.endDragSession(true, _parseModifiers(dragEvent)); + if (!expectSrcElementDisconnected && !dragEndEvent) { + // eslint-disable-next-line no-unsafe-finally + throw new Error( + `"dragend" event is not fired by nsIDragService.endDragSession()${ + ds.sourceNode && !ds.sourceNode.isConnected + ? "(sourceNode was disconnected)" + : "" + }` + ); + } + } finally { + srcWindow.removeEventListener("dragend", onDragEnd, { capture: true }); + } + } + _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(false); + if (logFunc) { + logFunc("synthesizePlainDragAndDrop() -- END"); + } + } +} + +function _checkDataTransferItems(aDataTransfer, aExpectedDragData) { + try { + // We must wrap only in plain mochitests, not chrome + let dataTransfer = _EU_maybeWrap(aDataTransfer); + if (!dataTransfer) { + return null; + } + if ( + aExpectedDragData == null || + dataTransfer.mozItemCount != aExpectedDragData.length + ) { + return dataTransfer; + } + for (let i = 0; i < dataTransfer.mozItemCount; i++) { + let dtTypes = dataTransfer.mozTypesAt(i); + if (dtTypes.length != aExpectedDragData[i].length) { + return dataTransfer; + } + for (let j = 0; j < dtTypes.length; j++) { + if (dtTypes[j] != aExpectedDragData[i][j].type) { + return dataTransfer; + } + let dtData = dataTransfer.mozGetDataAt(dtTypes[j], i); + if (aExpectedDragData[i][j].eqTest) { + if ( + !aExpectedDragData[i][j].eqTest( + dtData, + aExpectedDragData[i][j].data + ) + ) { + return dataTransfer; + } + } else if (aExpectedDragData[i][j].data != dtData) { + return dataTransfer; + } + } + } + } catch (ex) { + return ex; + } + return true; +} + +/** + * This callback type is used with ``synthesizePlainDragAndCancel()``. + * It should compare ``actualData`` and ``expectedData`` and return + * true if the two should be considered equal, false otherwise. + * + * @callback eqTest + * @param {*} actualData + * @param {*} expectedData + * @return {boolean} + */ + +/** + * synthesizePlainDragAndCancel() synthesizes drag start with + * synthesizePlainDragAndDrop(), but always cancel it with preventing default + * of "dragstart". Additionally, this checks whether the dataTransfer of + * "dragstart" event has only expected items. + * + * @param {Object} aParams + * The params which is set to the argument of ``synthesizePlainDragAndDrop()``. + * @param {Array} aExpectedDataTransferItems + * All expected dataTransfer items. + * This data is in the format: + * + * [ + * [ + * {"type": value, "data": value, eqTest: function} + * ..., + * ], + * ... + * ] + * + * This can also be null. + * You can optionally provide ``eqTest`` {@type eqTest} if the + * comparison to the expected data transfer items can't be done + * with x == y; + * @return {boolean} + * true if aExpectedDataTransferItems matches with + * DragEvent.dataTransfer of "dragstart" event. + * Otherwise, the dataTransfer object (may be null) or + * thrown exception, NOT false. Therefore, you shouldn't + * use. + */ +async function synthesizePlainDragAndCancel( + aParams, + aExpectedDataTransferItems +) { + let srcElement = aParams.srcSelection + ? _computeSrcElementFromSrcSelection(aParams.srcSelection) + : aParams.srcElement; + let result; + function onDragStart(aEvent) { + aEvent.preventDefault(); + result = _checkDataTransferItems( + aEvent.dataTransfer, + aExpectedDataTransferItems + ); + } + SpecialPowers.addSystemEventListener( + srcElement.ownerDocument, + "dragstart", + onDragStart, + { capture: true } + ); + try { + aParams.expectCancelDragStart = true; + await synthesizePlainDragAndDrop(aParams); + } finally { + SpecialPowers.removeSystemEventListener( + srcElement.ownerDocument, + "dragstart", + onDragStart, + { capture: true } + ); + } + return result; +} + +class EventCounter { + constructor(aTarget, aType, aOptions = {}) { + this.target = aTarget; + this.type = aType; + this.options = aOptions; + + this.eventCount = 0; + // Bug 1512817: + // SpecialPowers is picky and needs to be passed an explicit reference to + // the function to be called. To avoid having to bind "this", we therefore + // define the method this way, via a property. + this.handleEvent = aEvent => { + this.eventCount++; + }; + + if (aOptions.mozSystemGroup) { + SpecialPowers.addSystemEventListener( + aTarget, + aType, + this.handleEvent, + aOptions.capture + ); + } else { + aTarget.addEventListener(aType, this, aOptions); + } + } + + unregister() { + if (this.options.mozSystemGroup) { + SpecialPowers.removeSystemEventListener( + this.target, + this.type, + this.handleEvent, + this.options.capture + ); + } else { + this.target.removeEventListener(this.type, this, this.options); + } + } + + get count() { + return this.eventCount; + } +} diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js new file mode 100644 index 0000000000..fa86116c92 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js @@ -0,0 +1,180 @@ +const { ExtensionTestCommon } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/ExtensionTestCommon.sys.mjs" +); + +var ExtensionTestUtils = { + // Shortcut to more easily access WebExtensionPolicy.backgroundServiceWorkerEnabled + // from mochitest-plain tests. + getBackgroundServiceWorkerEnabled() { + return ExtensionTestCommon.getBackgroundServiceWorkerEnabled(); + }, + + // A test helper used to check if the pref "extension.backgroundServiceWorker.forceInTestExtension" + // is set to true. + isInBackgroundServiceWorkerTests() { + return ExtensionTestCommon.isInBackgroundServiceWorkerTests(); + }, + + get testAssertions() { + return ExtensionTestCommon.testAssertions; + }, +}; + +ExtensionTestUtils.loadExtension = function (ext) { + // Cleanup functions need to be registered differently depending on + // whether we're in browser chrome or plain mochitests. + var registerCleanup; + /* global registerCleanupFunction */ + if (typeof registerCleanupFunction != "undefined") { + registerCleanup = registerCleanupFunction; + } else { + registerCleanup = SimpleTest.registerCleanupFunction.bind(SimpleTest); + } + + var testResolve; + var testDone = new Promise(resolve => { + testResolve = resolve; + }); + + var messageHandler = new Map(); + var messageAwaiter = new Map(); + + var messageQueue = new Set(); + + registerCleanup(() => { + if (messageQueue.size) { + let names = Array.from(messageQueue, ([msg]) => msg); + SimpleTest.is(JSON.stringify(names), "[]", "message queue is empty"); + } + if (messageAwaiter.size) { + let names = Array.from(messageAwaiter.keys()); + SimpleTest.is( + JSON.stringify(names), + "[]", + "no tasks awaiting on messages" + ); + } + }); + + function checkMessages() { + for (let message of messageQueue) { + let [msg, ...args] = message; + + let listener = messageAwaiter.get(msg); + if (listener) { + messageQueue.delete(message); + messageAwaiter.delete(msg); + + listener.resolve(...args); + return; + } + } + } + + function checkDuplicateListeners(msg) { + if (messageHandler.has(msg) || messageAwaiter.has(msg)) { + throw new Error("only one message handler allowed"); + } + } + + function testHandler(kind, pass, msg, ...args) { + if (kind == "test-eq") { + let [expected, actual] = args; + SimpleTest.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`); + } else if (kind == "test-log") { + SimpleTest.info(msg); + } else if (kind == "test-result") { + SimpleTest.ok(pass, msg); + } + } + + var handler = { + async testResult(kind, pass, msg, ...args) { + if (kind == "test-done") { + SimpleTest.ok(pass, msg); + await testResolve(msg); + } + testHandler(kind, pass, msg, ...args); + }, + + testMessage(msg, ...args) { + var msgHandler = messageHandler.get(msg); + if (msgHandler) { + msgHandler(...args); + } else { + messageQueue.add([msg, ...args]); + checkMessages(); + } + }, + }; + + // Mimic serialization of functions as done in `Extension.generateXPI` and + // `Extension.generateZipFile` because functions are dropped when `ext` object + // is sent to the main process via the message manager. + ext = Object.assign({}, ext); + if (ext.files) { + ext.files = Object.assign({}, ext.files); + for (let filename of Object.keys(ext.files)) { + let file = ext.files[filename]; + if (typeof file === "function" || Array.isArray(file)) { + ext.files[filename] = ExtensionTestCommon.serializeScript(file); + } + } + } + if ("background" in ext) { + ext.background = ExtensionTestCommon.serializeScript(ext.background); + } + + var extension = SpecialPowers.loadExtension(ext, handler); + + registerCleanup(async () => { + if (extension.state == "pending" || extension.state == "running") { + SimpleTest.ok(false, "Extension left running at test shutdown"); + await extension.unload(); + } else if (extension.state == "unloading") { + SimpleTest.ok(false, "Extension not fully unloaded at test shutdown"); + } + }); + + extension.awaitMessage = msg => { + return new Promise(resolve => { + checkDuplicateListeners(msg); + + messageAwaiter.set(msg, { resolve }); + checkMessages(); + }); + }; + + extension.onMessage = (msg, callback) => { + checkDuplicateListeners(msg); + messageHandler.set(msg, callback); + }; + + extension.awaitFinish = msg => { + return testDone.then(actual => { + if (msg) { + SimpleTest.is(actual, msg, "test result correct"); + } + return actual; + }); + }; + + SimpleTest.info(`Extension loaded`); + return extension; +}; + +ExtensionTestUtils.failOnSchemaWarnings = (warningsAsErrors = true) => { + let prefName = "extensions.webextensions.warnings-as-errors"; + let prefPromise = SpecialPowers.setBoolPref(prefName, warningsAsErrors); + if (!warningsAsErrors) { + let registerCleanup; + if (typeof registerCleanupFunction != "undefined") { + registerCleanup = registerCleanupFunction; + } else { + registerCleanup = SimpleTest.registerCleanupFunction.bind(SimpleTest); + } + registerCleanup(() => SpecialPowers.setBoolPref(prefName, true)); + } + // In mochitests, setBoolPref is async. + return prefPromise.then(() => {}); +}; diff --git a/testing/mochitest/tests/SimpleTest/LogController.js b/testing/mochitest/tests/SimpleTest/LogController.js new file mode 100644 index 0000000000..29580022f8 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/LogController.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/. */ + +var LogController = {}; //create the logger object + +LogController.counter = 0; //current log message number +LogController.listeners = []; +LogController.logLevel = { + FATAL: 50, + ERROR: 40, + WARNING: 30, + INFO: 20, + DEBUG: 10, +}; + +/* set minimum logging level */ +LogController.logLevelAtLeast = function (minLevel) { + if (typeof minLevel == "string") { + minLevel = LogController.logLevel[minLevel]; + } + return function (msg) { + var msgLevel = msg.level; + if (typeof msgLevel == "string") { + msgLevel = LogController.logLevel[msgLevel]; + } + return msgLevel >= minLevel; + }; +}; + +/* creates the log message with the given level and info */ +LogController.createLogMessage = function (level, info) { + var msg = {}; + msg.num = LogController.counter; + msg.level = level; + msg.info = info; + msg.timestamp = new Date(); + return msg; +}; + +/* helper method to return a sub-array */ +LogController.extend = function (args, skip) { + var ret = []; + for (var i = skip; i < args.length; i++) { + ret.push(args[i]); + } + return ret; +}; + +/* logs message with given level. Currently used locally by log() and error() */ +LogController.logWithLevel = function (level, message /*, ...*/) { + var msg = LogController.createLogMessage( + level, + LogController.extend(arguments, 1) + ); + LogController.dispatchListeners(msg); + LogController.counter += 1; +}; + +/* log with level INFO */ +LogController.log = function (message /*, ...*/) { + LogController.logWithLevel("INFO", message); +}; + +/* log with level ERROR */ +LogController.error = function (message /*, ...*/) { + LogController.logWithLevel("ERROR", message); +}; + +/* send log message to listeners */ +LogController.dispatchListeners = function (msg) { + for (var k in LogController.listeners) { + var pair = LogController.listeners[k]; + if (pair.ident != k || (pair[0] && !pair[0](msg))) { + continue; + } + pair[1](msg); + } +}; + +/* add a listener to this log given an identifier, a filter (can be null) and the listener object */ +LogController.addListener = function (ident, filter, listener) { + if (typeof filter == "string") { + filter = LogController.logLevelAtLeast(filter); + } else if (filter !== null && typeof filter !== "function") { + throw new Error("Filter must be a string, a function, or null"); + } + var entry = [filter, listener]; + entry.ident = ident; + LogController.listeners[ident] = entry; +}; + +/* remove a listener from this log */ +LogController.removeListener = function (ident) { + delete LogController.listeners[ident]; +}; diff --git a/testing/mochitest/tests/SimpleTest/MemoryStats.js b/testing/mochitest/tests/SimpleTest/MemoryStats.js new file mode 100644 index 0000000000..40548697ea --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MemoryStats.js @@ -0,0 +1,131 @@ +/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */ +/* vim:set ts=4 sw=4 sts=4 et: */ +/* 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/. */ + +var MemoryStats = {}; + +/** + * Statistics that we want to retrieve and display after every test is + * done. The keys of this table are intended to be identical to the + * relevant attributes of nsIMemoryReporterManager. However, since + * nsIMemoryReporterManager doesn't necessarily support all these + * statistics in all build configurations, we also use this table to + * tell us whether statistics are supported or not. + */ +var MEM_STAT_UNKNOWN = 0; +var MEM_STAT_UNSUPPORTED = 1; +var MEM_STAT_SUPPORTED = 2; + +MemoryStats._hasMemoryStatistics = {}; +MemoryStats._hasMemoryStatistics.vsize = MEM_STAT_UNKNOWN; +MemoryStats._hasMemoryStatistics.vsizeMaxContiguous = MEM_STAT_UNKNOWN; +MemoryStats._hasMemoryStatistics.residentFast = MEM_STAT_UNKNOWN; +MemoryStats._hasMemoryStatistics.heapAllocated = MEM_STAT_UNKNOWN; + +MemoryStats._getService = function (className, interfaceName) { + var service; + try { + service = Cc[className].getService(Ci[interfaceName]); + } catch (e) { + service = SpecialPowers.Cc[className].getService( + SpecialPowers.Ci[interfaceName] + ); + } + return service; +}; + +MemoryStats._nsIFile = function (pathname) { + var f; + var contractID = "@mozilla.org/file/local;1"; + try { + f = Cc[contractID].createInstance(Ci.nsIFile); + } catch (e) { + f = SpecialPowers.Cc[contractID].createInstance(SpecialPowers.Ci.nsIFile); + } + f.initWithPath(pathname); + return f; +}; + +MemoryStats.constructPathname = function (directory, basename) { + var d = MemoryStats._nsIFile(directory); + d.append(basename); + return d.path; +}; + +MemoryStats.dump = function ( + testNumber, + testURL, + dumpOutputDirectory, + dumpAboutMemory, + dumpDMD +) { + // Use dump because treeherder uses --quiet, which drops 'info' + // from the structured logger. + var info = function (message) { + dump(message + "\n"); + }; + + var mrm = MemoryStats._getService( + "@mozilla.org/memory-reporter-manager;1", + "nsIMemoryReporterManager" + ); + var statMessage = ""; + for (var stat in MemoryStats._hasMemoryStatistics) { + var supported = MemoryStats._hasMemoryStatistics[stat]; + var firstAccess = false; + if (supported == MEM_STAT_UNKNOWN) { + firstAccess = true; + try { + void mrm[stat]; + supported = MEM_STAT_SUPPORTED; + } catch (e) { + supported = MEM_STAT_UNSUPPORTED; + } + MemoryStats._hasMemoryStatistics[stat] = supported; + } + if (supported == MEM_STAT_SUPPORTED) { + var sizeInMB = Math.round(mrm[stat] / (1024 * 1024)); + statMessage += " | " + stat + " " + sizeInMB + "MB"; + } else if (firstAccess) { + info( + "MEMORY STAT " + stat + " not supported in this build configuration." + ); + } + } + if (statMessage.length) { + info("MEMORY STAT" + statMessage); + } + + if (dumpAboutMemory) { + var basename = "about-memory-" + testNumber + ".json.gz"; + var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory, basename); + info(testURL + " | MEMDUMP-START " + dumpfile); + let md = MemoryStats._getService( + "@mozilla.org/memory-info-dumper;1", + "nsIMemoryInfoDumper" + ); + md.dumpMemoryReportsToNamedFile( + dumpfile, + function () { + info("TEST-INFO | " + testURL + " | MEMDUMP-END"); + }, + null, + /* anonymize = */ false, + /* minimize memory usage = */ false + ); + } + + if (dumpDMD) { + let md = MemoryStats._getService( + "@mozilla.org/memory-info-dumper;1", + "nsIMemoryInfoDumper" + ); + md.dumpMemoryInfoToTempDir( + String(testNumber), + /* anonymize = */ false, + /* minimize memory usage = */ false + ); + } +}; diff --git a/testing/mochitest/tests/SimpleTest/MockObjects.js b/testing/mochitest/tests/SimpleTest/MockObjects.js new file mode 100644 index 0000000000..5782707c04 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MockObjects.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/. */ + +/** + * Allows registering a mock XPCOM component, that temporarily replaces the + * original one when an object implementing a given ContractID is requested + * using createInstance. + * + * @param aContractID + * The ContractID of the component to replace, for example + * "@mozilla.org/filepicker;1". + * + * @param aReplacementCtor + * The constructor function for the JavaScript object that will be + * created every time createInstance is called. This object must + * implement QueryInterface and provide the XPCOM interfaces required by + * the specified ContractID (for example + * Components.interfaces.nsIFilePicker). + */ + +function MockObjectRegisterer(aContractID, aReplacementCtor) { + this._contractID = aContractID; + this._replacementCtor = aReplacementCtor; +} + +MockObjectRegisterer.prototype = { + /** + * Replaces the current factory with one that returns a new mock object. + * + * After register() has been called, it is mandatory to call unregister() to + * restore the original component. Usually, you should use a try-catch block + * to ensure that unregister() is called. + */ + register: function MOR_register() { + if (this._originalCID) { + throw new Error("Invalid object state when calling register()"); + } + + // Define a factory that creates a new object using the given constructor. + var isChrome = location.protocol == "chrome:"; + var providedConstructor = this._replacementCtor; + this._mockFactory = { + createInstance: function MF_createInstance(aIid) { + var inst = new providedConstructor().QueryInterface(aIid); + if (!isChrome) { + inst = SpecialPowers.wrapCallbackObject(inst); + } + return inst; + }, + }; + if (!isChrome) { + this._mockFactory = SpecialPowers.wrapCallbackObject(this._mockFactory); + } + + var retVal = SpecialPowers.swapFactoryRegistration( + null, + this._contractID, + this._mockFactory + ); + if ("error" in retVal) { + throw new Error("ERROR: " + retVal.error); + } else { + this._originalCID = retVal.originalCID; + } + }, + + /** + * Restores the original factory. + */ + unregister: function MOR_unregister() { + if (!this._originalCID) { + throw new Error("Invalid object state when calling unregister()"); + } + + // Free references to the mock factory. + SpecialPowers.swapFactoryRegistration(this._originalCID, this._contractID); + + // Allow registering a mock factory again later. + this._originalCID = null; + this._mockFactory = null; + }, + + // --- Private methods and properties --- + + /** + * The factory of the component being replaced. + */ + _originalCID: null, + + /** + * The nsIFactory that was automatically generated by this object. + */ + _mockFactory: null, +}; diff --git a/testing/mochitest/tests/SimpleTest/MozillaLogger.js b/testing/mochitest/tests/SimpleTest/MozillaLogger.js new file mode 100644 index 0000000000..13ed5bd8f5 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MozillaLogger.js @@ -0,0 +1,102 @@ +/** + * MozillaLogger, a base class logger that just logs to stdout. + */ + +"use strict"; + +function formatLogMessage(msg) { + return msg.info.join(" ") + "\n"; +} + +function importMJS(mjs) { + if (typeof ChromeUtils === "object") { + return ChromeUtils.importESModule(mjs); + } + /* globals SpecialPowers */ + return SpecialPowers.ChromeUtils.importESModule(mjs); +} + +// When running in release builds, we get a fake Components object in +// web contexts, so we need to check that the Components object is sane, +// too, not just that it exists. +let haveComponents = + typeof Components === "object" && + typeof Components.Constructor === "function"; + +let CC = ( + haveComponents ? Components : SpecialPowers.wrap(SpecialPowers.Components) +).Constructor; + +let ConverterOutputStream = CC( + "@mozilla.org/intl/converter-output-stream;1", + "nsIConverterOutputStream", + "init" +); + +class MozillaLogger { + get logCallback() { + return msg => { + this.log(formatLogMessage(msg)); + }; + } + + log(msg) { + dump(msg); + } + + close() {} +} + +/** + * MozillaFileLogger, a log listener that can write to a local file. + * intended to be run from chrome space + */ + +/** Init the file logger with the absolute path to the file. + It will create and append if the file already exists **/ +class MozillaFileLogger extends MozillaLogger { + constructor(aPath) { + super(); + + const { FileUtils } = importMJS("resource://gre/modules/FileUtils.sys.mjs"); + + this._file = FileUtils.File(aPath); + this._foStream = FileUtils.openFileOutputStream( + this._file, + FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_APPEND + ); + + this._converter = ConverterOutputStream(this._foStream, "UTF-8"); + } + + get logCallback() { + return msg => { + if (this._converter) { + var data = formatLogMessage(msg); + this.log(data); + + if (data.includes("SimpleTest FINISH")) { + this.close(); + } + } + }; + } + + log(msg) { + if (this._converter) { + this._converter.writeString(msg); + } + } + + close() { + this._converter.flush(); + this._converter.close(); + + this._foStream = null; + this._converter = null; + this._file = null; + } +} + +this.MozillaLogger = MozillaLogger; +this.MozillaFileLogger = MozillaFileLogger; diff --git a/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js new file mode 100644 index 0000000000..80ff8bf07c --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js @@ -0,0 +1,722 @@ +/** + * This file defines all virtual keycodes for synthesizeNativeKey() of + * EventUtils.js and nsIDOMWindowUtils.sendNativeKeyEvent(). + * These values are defined in each platform's SDK or documents. + */ + +function _defineConstant(name, value) { + Object.defineProperty(this, name, { + value, + enumerable: true, + writable: false, + }); +} + +// Windows +// Windows' native key code values may include scan code value which can be +// retrieved with |((code & 0xFFFF0000 >> 16)|. If the value is 0, it will +// be computed with active keyboard layout automatically. +// FYI: Don't define scan code here for printable keys, numeric keys and +// IME keys because they depend on active keyboard layout. +// XXX: Although, ABNT C1 key depends on keyboard layout in strictly speaking. +// However, computing its scan code from the virtual keycode, +// WIN_VK_ABNT_C1, doesn't work fine (computed as 0x0073, "IntlRo"). +// Therefore, we should specify it here explicitly (it should be 0x0056, +// "IntlBackslash"). Fortunately, the key always generates 0x0056 with +// any keyboard layouts as far as I've tested. So, this must be safe to +// test new regressions. + +const WIN_VK_LBUTTON = 0x00000001; +_defineConstant("WIN_VK_LBUTTON", WIN_VK_LBUTTON); +const WIN_VK_RBUTTON = 0x00000002; +_defineConstant("WIN_VK_RBUTTON", WIN_VK_RBUTTON); +const WIN_VK_CANCEL = 0xe0460003; +_defineConstant("WIN_VK_CANCEL", WIN_VK_CANCEL); +const WIN_VK_MBUTTON = 0x00000004; +_defineConstant("WIN_VK_MBUTTON", WIN_VK_MBUTTON); +const WIN_VK_XBUTTON1 = 0x00000005; +_defineConstant("WIN_VK_XBUTTON1", WIN_VK_XBUTTON1); +const WIN_VK_XBUTTON2 = 0x00000006; +_defineConstant("WIN_VK_XBUTTON2", WIN_VK_XBUTTON2); +const WIN_VK_BACK = 0x000e0008; +_defineConstant("WIN_VK_BACK", WIN_VK_BACK); +const WIN_VK_TAB = 0x000f0009; +_defineConstant("WIN_VK_TAB", WIN_VK_TAB); +const WIN_VK_CLEAR = 0x004c000c; +_defineConstant("WIN_VK_CLEAR", WIN_VK_CLEAR); +const WIN_VK_RETURN = 0x001c000d; +_defineConstant("WIN_VK_RETURN", WIN_VK_RETURN); +const WIN_VK_SHIFT = 0x002a0010; +_defineConstant("WIN_VK_SHIFT", WIN_VK_SHIFT); +const WIN_VK_CONTROL = 0x001d0011; +_defineConstant("WIN_VK_CONTROL", WIN_VK_CONTROL); +const WIN_VK_MENU = 0x00380012; +_defineConstant("WIN_VK_MENU", WIN_VK_MENU); +const WIN_VK_PAUSE = 0x00450013; +_defineConstant("WIN_VK_PAUSE", WIN_VK_PAUSE); +const WIN_VK_CAPITAL = 0x003a0014; +_defineConstant("WIN_VK_CAPITAL", WIN_VK_CAPITAL); +const WIN_VK_KANA = 0x00000015; +_defineConstant("WIN_VK_KANA", WIN_VK_KANA); +const WIN_VK_HANGUEL = 0x00000015; +_defineConstant("WIN_VK_HANGUEL", WIN_VK_HANGUEL); +const WIN_VK_HANGUL = 0x00000015; +_defineConstant("WIN_VK_HANGUL", WIN_VK_HANGUL); +const WIN_VK_JUNJA = 0x00000017; +_defineConstant("WIN_VK_JUNJA", WIN_VK_JUNJA); +const WIN_VK_FINAL = 0x00000018; +_defineConstant("WIN_VK_FINAL", WIN_VK_FINAL); +const WIN_VK_HANJA = 0x00000019; +_defineConstant("WIN_VK_HANJA", WIN_VK_HANJA); +const WIN_VK_KANJI = 0x00000019; +_defineConstant("WIN_VK_KANJI", WIN_VK_KANJI); +const WIN_VK_ESCAPE = 0x0001001b; +_defineConstant("WIN_VK_ESCAPE", WIN_VK_ESCAPE); +const WIN_VK_CONVERT = 0x0000001c; +_defineConstant("WIN_VK_CONVERT", WIN_VK_CONVERT); +const WIN_VK_NONCONVERT = 0x0000001d; +_defineConstant("WIN_VK_NONCONVERT", WIN_VK_NONCONVERT); +const WIN_VK_ACCEPT = 0x0000001e; +_defineConstant("WIN_VK_ACCEPT", WIN_VK_ACCEPT); +const WIN_VK_MODECHANGE = 0x0000001f; +_defineConstant("WIN_VK_MODECHANGE", WIN_VK_MODECHANGE); +const WIN_VK_SPACE = 0x00390020; +_defineConstant("WIN_VK_SPACE", WIN_VK_SPACE); +const WIN_VK_PRIOR = 0xe0490021; +_defineConstant("WIN_VK_PRIOR", WIN_VK_PRIOR); +const WIN_VK_NEXT = 0xe0510022; +_defineConstant("WIN_VK_NEXT", WIN_VK_NEXT); +const WIN_VK_END = 0xe04f0023; +_defineConstant("WIN_VK_END", WIN_VK_END); +const WIN_VK_HOME = 0xe0470024; +_defineConstant("WIN_VK_HOME", WIN_VK_HOME); +const WIN_VK_LEFT = 0xe04b0025; +_defineConstant("WIN_VK_LEFT", WIN_VK_LEFT); +const WIN_VK_UP = 0xe0480026; +_defineConstant("WIN_VK_UP", WIN_VK_UP); +const WIN_VK_RIGHT = 0xe04d0027; +_defineConstant("WIN_VK_RIGHT", WIN_VK_RIGHT); +const WIN_VK_DOWN = 0xe0500028; +_defineConstant("WIN_VK_DOWN", WIN_VK_DOWN); +const WIN_VK_SELECT = 0x00000029; +_defineConstant("WIN_VK_SELECT", WIN_VK_SELECT); +const WIN_VK_PRINT = 0x0000002a; +_defineConstant("WIN_VK_PRINT", WIN_VK_PRINT); +const WIN_VK_EXECUTE = 0x0000002b; +_defineConstant("WIN_VK_EXECUTE", WIN_VK_EXECUTE); +const WIN_VK_SNAPSHOT = 0xe037002c; +_defineConstant("WIN_VK_SNAPSHOT", WIN_VK_SNAPSHOT); +const WIN_VK_INSERT = 0xe052002d; +_defineConstant("WIN_VK_INSERT", WIN_VK_INSERT); +const WIN_VK_DELETE = 0xe053002e; +_defineConstant("WIN_VK_DELETE", WIN_VK_DELETE); +const WIN_VK_HELP = 0x0000002f; +_defineConstant("WIN_VK_HELP", WIN_VK_HELP); +const WIN_VK_0 = 0x00000030; +_defineConstant("WIN_VK_0", WIN_VK_0); +const WIN_VK_1 = 0x00000031; +_defineConstant("WIN_VK_1", WIN_VK_1); +const WIN_VK_2 = 0x00000032; +_defineConstant("WIN_VK_2", WIN_VK_2); +const WIN_VK_3 = 0x00000033; +_defineConstant("WIN_VK_3", WIN_VK_3); +const WIN_VK_4 = 0x00000034; +_defineConstant("WIN_VK_4", WIN_VK_4); +const WIN_VK_5 = 0x00000035; +_defineConstant("WIN_VK_5", WIN_VK_5); +const WIN_VK_6 = 0x00000036; +_defineConstant("WIN_VK_6", WIN_VK_6); +const WIN_VK_7 = 0x00000037; +_defineConstant("WIN_VK_7", WIN_VK_7); +const WIN_VK_8 = 0x00000038; +_defineConstant("WIN_VK_8", WIN_VK_8); +const WIN_VK_9 = 0x00000039; +_defineConstant("WIN_VK_9", WIN_VK_9); +const WIN_VK_A = 0x00000041; +_defineConstant("WIN_VK_A", WIN_VK_A); +const WIN_VK_B = 0x00000042; +_defineConstant("WIN_VK_B", WIN_VK_B); +const WIN_VK_C = 0x00000043; +_defineConstant("WIN_VK_C", WIN_VK_C); +const WIN_VK_D = 0x00000044; +_defineConstant("WIN_VK_D", WIN_VK_D); +const WIN_VK_E = 0x00000045; +_defineConstant("WIN_VK_E", WIN_VK_E); +const WIN_VK_F = 0x00000046; +_defineConstant("WIN_VK_F", WIN_VK_F); +const WIN_VK_G = 0x00000047; +_defineConstant("WIN_VK_G", WIN_VK_G); +const WIN_VK_H = 0x00000048; +_defineConstant("WIN_VK_H", WIN_VK_H); +const WIN_VK_I = 0x00000049; +_defineConstant("WIN_VK_I", WIN_VK_I); +const WIN_VK_J = 0x0000004a; +_defineConstant("WIN_VK_J", WIN_VK_J); +const WIN_VK_K = 0x0000004b; +_defineConstant("WIN_VK_K", WIN_VK_K); +const WIN_VK_L = 0x0000004c; +_defineConstant("WIN_VK_L", WIN_VK_L); +const WIN_VK_M = 0x0000004d; +_defineConstant("WIN_VK_M", WIN_VK_M); +const WIN_VK_N = 0x0000004e; +_defineConstant("WIN_VK_N", WIN_VK_N); +const WIN_VK_O = 0x0000004f; +_defineConstant("WIN_VK_O", WIN_VK_O); +const WIN_VK_P = 0x00000050; +_defineConstant("WIN_VK_P", WIN_VK_P); +const WIN_VK_Q = 0x00000051; +_defineConstant("WIN_VK_Q", WIN_VK_Q); +const WIN_VK_R = 0x00000052; +_defineConstant("WIN_VK_R", WIN_VK_R); +const WIN_VK_S = 0x00000053; +_defineConstant("WIN_VK_S", WIN_VK_S); +const WIN_VK_T = 0x00000054; +_defineConstant("WIN_VK_T", WIN_VK_T); +const WIN_VK_U = 0x00000055; +_defineConstant("WIN_VK_U", WIN_VK_U); +const WIN_VK_V = 0x00000056; +_defineConstant("WIN_VK_V", WIN_VK_V); +const WIN_VK_W = 0x00000057; +_defineConstant("WIN_VK_W", WIN_VK_W); +const WIN_VK_X = 0x00000058; +_defineConstant("WIN_VK_X", WIN_VK_X); +const WIN_VK_Y = 0x00000059; +_defineConstant("WIN_VK_Y", WIN_VK_Y); +const WIN_VK_Z = 0x0000005a; +_defineConstant("WIN_VK_Z", WIN_VK_Z); +const WIN_VK_LWIN = 0xe05b005b; +_defineConstant("WIN_VK_LWIN", WIN_VK_LWIN); +const WIN_VK_RWIN = 0xe05c005c; +_defineConstant("WIN_VK_RWIN", WIN_VK_RWIN); +const WIN_VK_APPS = 0xe05d005d; +_defineConstant("WIN_VK_APPS", WIN_VK_APPS); +const WIN_VK_SLEEP = 0x0000005f; +_defineConstant("WIN_VK_SLEEP", WIN_VK_SLEEP); +const WIN_VK_NUMPAD0 = 0x00520060; +_defineConstant("WIN_VK_NUMPAD0", WIN_VK_NUMPAD0); +const WIN_VK_NUMPAD1 = 0x004f0061; +_defineConstant("WIN_VK_NUMPAD1", WIN_VK_NUMPAD1); +const WIN_VK_NUMPAD2 = 0x00500062; +_defineConstant("WIN_VK_NUMPAD2", WIN_VK_NUMPAD2); +const WIN_VK_NUMPAD3 = 0x00510063; +_defineConstant("WIN_VK_NUMPAD3", WIN_VK_NUMPAD3); +const WIN_VK_NUMPAD4 = 0x004b0064; +_defineConstant("WIN_VK_NUMPAD4", WIN_VK_NUMPAD4); +const WIN_VK_NUMPAD5 = 0x004c0065; +_defineConstant("WIN_VK_NUMPAD5", WIN_VK_NUMPAD5); +const WIN_VK_NUMPAD6 = 0x004d0066; +_defineConstant("WIN_VK_NUMPAD6", WIN_VK_NUMPAD6); +const WIN_VK_NUMPAD7 = 0x00470067; +_defineConstant("WIN_VK_NUMPAD7", WIN_VK_NUMPAD7); +const WIN_VK_NUMPAD8 = 0x00480068; +_defineConstant("WIN_VK_NUMPAD8", WIN_VK_NUMPAD8); +const WIN_VK_NUMPAD9 = 0x00490069; +_defineConstant("WIN_VK_NUMPAD9", WIN_VK_NUMPAD9); +const WIN_VK_MULTIPLY = 0x0037006a; +_defineConstant("WIN_VK_MULTIPLY", WIN_VK_MULTIPLY); +const WIN_VK_ADD = 0x004e006b; +_defineConstant("WIN_VK_ADD", WIN_VK_ADD); +const WIN_VK_SEPARATOR = 0x0000006c; +_defineConstant("WIN_VK_SEPARATOR", WIN_VK_SEPARATOR); +const WIN_VK_OEM_NEC_SEPARATE = 0x0000006c; +_defineConstant("WIN_VK_OEM_NEC_SEPARATE", WIN_VK_OEM_NEC_SEPARATE); +const WIN_VK_SUBTRACT = 0x004a006d; +_defineConstant("WIN_VK_SUBTRACT", WIN_VK_SUBTRACT); +const WIN_VK_DECIMAL = 0x0053006e; +_defineConstant("WIN_VK_DECIMAL", WIN_VK_DECIMAL); +const WIN_VK_DIVIDE = 0xe035006f; +_defineConstant("WIN_VK_DIVIDE", WIN_VK_DIVIDE); +const WIN_VK_F1 = 0x003b0070; +_defineConstant("WIN_VK_F1", WIN_VK_F1); +const WIN_VK_F2 = 0x003c0071; +_defineConstant("WIN_VK_F2", WIN_VK_F2); +const WIN_VK_F3 = 0x003d0072; +_defineConstant("WIN_VK_F3", WIN_VK_F3); +const WIN_VK_F4 = 0x003e0073; +_defineConstant("WIN_VK_F4", WIN_VK_F4); +const WIN_VK_F5 = 0x003f0074; +_defineConstant("WIN_VK_F5", WIN_VK_F5); +const WIN_VK_F6 = 0x00400075; +_defineConstant("WIN_VK_F6", WIN_VK_F6); +const WIN_VK_F7 = 0x00410076; +_defineConstant("WIN_VK_F7", WIN_VK_F7); +const WIN_VK_F8 = 0x00420077; +_defineConstant("WIN_VK_F8", WIN_VK_F8); +const WIN_VK_F9 = 0x00430078; +_defineConstant("WIN_VK_F9", WIN_VK_F9); +const WIN_VK_F10 = 0x00440079; +_defineConstant("WIN_VK_F10", WIN_VK_F10); +const WIN_VK_F11 = 0x0057007a; +_defineConstant("WIN_VK_F11", WIN_VK_F11); +const WIN_VK_F12 = 0x0058007b; +_defineConstant("WIN_VK_F12", WIN_VK_F12); +const WIN_VK_F13 = 0x0064007c; +_defineConstant("WIN_VK_F13", WIN_VK_F13); +const WIN_VK_F14 = 0x0065007d; +_defineConstant("WIN_VK_F14", WIN_VK_F14); +const WIN_VK_F15 = 0x0066007e; +_defineConstant("WIN_VK_F15", WIN_VK_F15); +const WIN_VK_F16 = 0x0067007f; +_defineConstant("WIN_VK_F16", WIN_VK_F16); +const WIN_VK_F17 = 0x00680080; +_defineConstant("WIN_VK_F17", WIN_VK_F17); +const WIN_VK_F18 = 0x00690081; +_defineConstant("WIN_VK_F18", WIN_VK_F18); +const WIN_VK_F19 = 0x006a0082; +_defineConstant("WIN_VK_F19", WIN_VK_F19); +const WIN_VK_F20 = 0x006b0083; +_defineConstant("WIN_VK_F20", WIN_VK_F20); +const WIN_VK_F21 = 0x006c0084; +_defineConstant("WIN_VK_F21", WIN_VK_F21); +const WIN_VK_F22 = 0x006d0085; +_defineConstant("WIN_VK_F22", WIN_VK_F22); +const WIN_VK_F23 = 0x006e0086; +_defineConstant("WIN_VK_F23", WIN_VK_F23); +const WIN_VK_F24 = 0x00760087; +_defineConstant("WIN_VK_F24", WIN_VK_F24); +const WIN_VK_NUMLOCK = 0xe0450090; +_defineConstant("WIN_VK_NUMLOCK", WIN_VK_NUMLOCK); +const WIN_VK_SCROLL = 0x00460091; +_defineConstant("WIN_VK_SCROLL", WIN_VK_SCROLL); +const WIN_VK_OEM_FJ_JISHO = 0x00000092; +_defineConstant("WIN_VK_OEM_FJ_JISHO", WIN_VK_OEM_FJ_JISHO); +const WIN_VK_OEM_NEC_EQUAL = 0x00000092; +_defineConstant("WIN_VK_OEM_NEC_EQUAL", WIN_VK_OEM_NEC_EQUAL); +const WIN_VK_OEM_FJ_MASSHOU = 0x00000093; +_defineConstant("WIN_VK_OEM_FJ_MASSHOU", WIN_VK_OEM_FJ_MASSHOU); +const WIN_VK_OEM_FJ_TOUROKU = 0x00000094; +_defineConstant("WIN_VK_OEM_FJ_TOUROKU", WIN_VK_OEM_FJ_TOUROKU); +const WIN_VK_OEM_FJ_LOYA = 0x00000095; +_defineConstant("WIN_VK_OEM_FJ_LOYA", WIN_VK_OEM_FJ_LOYA); +const WIN_VK_OEM_FJ_ROYA = 0x00000096; +_defineConstant("WIN_VK_OEM_FJ_ROYA", WIN_VK_OEM_FJ_ROYA); +const WIN_VK_LSHIFT = 0x002a00a0; +_defineConstant("WIN_VK_LSHIFT", WIN_VK_LSHIFT); +const WIN_VK_RSHIFT = 0x003600a1; +_defineConstant("WIN_VK_RSHIFT", WIN_VK_RSHIFT); +const WIN_VK_LCONTROL = 0x001d00a2; +_defineConstant("WIN_VK_LCONTROL", WIN_VK_LCONTROL); +const WIN_VK_RCONTROL = 0xe01d00a3; +_defineConstant("WIN_VK_RCONTROL", WIN_VK_RCONTROL); +const WIN_VK_LMENU = 0x003800a4; +_defineConstant("WIN_VK_LMENU", WIN_VK_LMENU); +const WIN_VK_RMENU = 0xe03800a5; +_defineConstant("WIN_VK_RMENU", WIN_VK_RMENU); +const WIN_VK_BROWSER_BACK = 0xe06a00a6; +_defineConstant("WIN_VK_BROWSER_BACK", WIN_VK_BROWSER_BACK); +const WIN_VK_BROWSER_FORWARD = 0xe06900a7; +_defineConstant("WIN_VK_BROWSER_FORWARD", WIN_VK_BROWSER_FORWARD); +const WIN_VK_BROWSER_REFRESH = 0xe06700a8; +_defineConstant("WIN_VK_BROWSER_REFRESH", WIN_VK_BROWSER_REFRESH); +const WIN_VK_BROWSER_STOP = 0xe06800a9; +_defineConstant("WIN_VK_BROWSER_STOP", WIN_VK_BROWSER_STOP); +const WIN_VK_BROWSER_SEARCH = 0x000000aa; +_defineConstant("WIN_VK_BROWSER_SEARCH", WIN_VK_BROWSER_SEARCH); +const WIN_VK_BROWSER_FAVORITES = 0xe06600ab; +_defineConstant("WIN_VK_BROWSER_FAVORITES", WIN_VK_BROWSER_FAVORITES); +const WIN_VK_BROWSER_HOME = 0xe03200ac; +_defineConstant("WIN_VK_BROWSER_HOME", WIN_VK_BROWSER_HOME); +const WIN_VK_VOLUME_MUTE = 0xe02000ad; +_defineConstant("WIN_VK_VOLUME_MUTE", WIN_VK_VOLUME_MUTE); +const WIN_VK_VOLUME_DOWN = 0xe02e00ae; +_defineConstant("WIN_VK_VOLUME_DOWN", WIN_VK_VOLUME_DOWN); +const WIN_VK_VOLUME_UP = 0xe03000af; +_defineConstant("WIN_VK_VOLUME_UP", WIN_VK_VOLUME_UP); +const WIN_VK_MEDIA_NEXT_TRACK = 0xe01900b0; +_defineConstant("WIN_VK_MEDIA_NEXT_TRACK", WIN_VK_MEDIA_NEXT_TRACK); +const WIN_VK_OEM_FJ_000 = 0x000000b0; +_defineConstant("WIN_VK_OEM_FJ_000", WIN_VK_OEM_FJ_000); +const WIN_VK_MEDIA_PREV_TRACK = 0xe01000b1; +_defineConstant("WIN_VK_MEDIA_PREV_TRACK", WIN_VK_MEDIA_PREV_TRACK); +const WIN_VK_OEM_FJ_EUQAL = 0x000000b1; +_defineConstant("WIN_VK_OEM_FJ_EUQAL", WIN_VK_OEM_FJ_EUQAL); +const WIN_VK_MEDIA_STOP = 0xe02400b2; +_defineConstant("WIN_VK_MEDIA_STOP", WIN_VK_MEDIA_STOP); +const WIN_VK_MEDIA_PLAY_PAUSE = 0xe02200b3; +_defineConstant("WIN_VK_MEDIA_PLAY_PAUSE", WIN_VK_MEDIA_PLAY_PAUSE); +const WIN_VK_OEM_FJ_00 = 0x000000b3; +_defineConstant("WIN_VK_OEM_FJ_00", WIN_VK_OEM_FJ_00); +const WIN_VK_LAUNCH_MAIL = 0xe06c00b4; +_defineConstant("WIN_VK_LAUNCH_MAIL", WIN_VK_LAUNCH_MAIL); +const WIN_VK_LAUNCH_MEDIA_SELECT = 0xe06d00b5; +_defineConstant("WIN_VK_LAUNCH_MEDIA_SELECT", WIN_VK_LAUNCH_MEDIA_SELECT); +const WIN_VK_LAUNCH_APP1 = 0xe06b00b6; +_defineConstant("WIN_VK_LAUNCH_APP1", WIN_VK_LAUNCH_APP1); +const WIN_VK_LAUNCH_APP2 = 0xe02100b7; +_defineConstant("WIN_VK_LAUNCH_APP2", WIN_VK_LAUNCH_APP2); +const WIN_VK_OEM_1 = 0x000000ba; +_defineConstant("WIN_VK_OEM_1", WIN_VK_OEM_1); +const WIN_VK_OEM_PLUS = 0x000000bb; +_defineConstant("WIN_VK_OEM_PLUS", WIN_VK_OEM_PLUS); +const WIN_VK_OEM_COMMA = 0x000000bc; +_defineConstant("WIN_VK_OEM_COMMA", WIN_VK_OEM_COMMA); +const WIN_VK_OEM_MINUS = 0x000000bd; +_defineConstant("WIN_VK_OEM_MINUS", WIN_VK_OEM_MINUS); +const WIN_VK_OEM_PERIOD = 0x000000be; +_defineConstant("WIN_VK_OEM_PERIOD", WIN_VK_OEM_PERIOD); +const WIN_VK_OEM_2 = 0x000000bf; +_defineConstant("WIN_VK_OEM_2", WIN_VK_OEM_2); +const WIN_VK_OEM_3 = 0x000000c0; +_defineConstant("WIN_VK_OEM_3", WIN_VK_OEM_3); +const WIN_VK_ABNT_C1 = 0x005600c1; +_defineConstant("WIN_VK_ABNT_C1", WIN_VK_ABNT_C1); +const WIN_VK_ABNT_C2 = 0x000000c2; +_defineConstant("WIN_VK_ABNT_C2", WIN_VK_ABNT_C2); +const WIN_VK_OEM_4 = 0x000000db; +_defineConstant("WIN_VK_OEM_4", WIN_VK_OEM_4); +const WIN_VK_OEM_5 = 0x000000dc; +_defineConstant("WIN_VK_OEM_5", WIN_VK_OEM_5); +const WIN_VK_OEM_6 = 0x000000dd; +_defineConstant("WIN_VK_OEM_6", WIN_VK_OEM_6); +const WIN_VK_OEM_7 = 0x000000de; +_defineConstant("WIN_VK_OEM_7", WIN_VK_OEM_7); +const WIN_VK_OEM_8 = 0x000000df; +_defineConstant("WIN_VK_OEM_8", WIN_VK_OEM_8); +const WIN_VK_OEM_NEC_DP1 = 0x000000e0; +_defineConstant("WIN_VK_OEM_NEC_DP1", WIN_VK_OEM_NEC_DP1); +const WIN_VK_OEM_AX = 0x000000e1; +_defineConstant("WIN_VK_OEM_AX", WIN_VK_OEM_AX); +const WIN_VK_OEM_NEC_DP2 = 0x000000e1; +_defineConstant("WIN_VK_OEM_NEC_DP2", WIN_VK_OEM_NEC_DP2); +const WIN_VK_OEM_102 = 0x000000e2; +_defineConstant("WIN_VK_OEM_102", WIN_VK_OEM_102); +const WIN_VK_OEM_NEC_DP3 = 0x000000e2; +_defineConstant("WIN_VK_OEM_NEC_DP3", WIN_VK_OEM_NEC_DP3); +const WIN_VK_ICO_HELP = 0x000000e3; +_defineConstant("WIN_VK_ICO_HELP", WIN_VK_ICO_HELP); +const WIN_VK_OEM_NEC_DP4 = 0x000000e3; +_defineConstant("WIN_VK_OEM_NEC_DP4", WIN_VK_OEM_NEC_DP4); +const WIN_VK_ICO_00 = 0x000000e4; +_defineConstant("WIN_VK_ICO_00", WIN_VK_ICO_00); +const WIN_VK_PROCESSKEY = 0x000000e5; +_defineConstant("WIN_VK_PROCESSKEY", WIN_VK_PROCESSKEY); +const WIN_VK_ICO_CLEAR = 0x000000e6; +_defineConstant("WIN_VK_ICO_CLEAR", WIN_VK_ICO_CLEAR); +const WIN_VK_PACKET = 0x000000e7; +_defineConstant("WIN_VK_PACKET", WIN_VK_PACKET); +const WIN_VK_ERICSSON_BASE = 0x000000e8; +_defineConstant("WIN_VK_ERICSSON_BASE", WIN_VK_ERICSSON_BASE); +const WIN_VK_OEM_RESET = 0x000000e9; +_defineConstant("WIN_VK_OEM_RESET", WIN_VK_OEM_RESET); +const WIN_VK_OEM_JUMP = 0x000000ea; +_defineConstant("WIN_VK_OEM_JUMP", WIN_VK_OEM_JUMP); +const WIN_VK_OEM_PA1 = 0x000000eb; +_defineConstant("WIN_VK_OEM_PA1", WIN_VK_OEM_PA1); +const WIN_VK_OEM_PA2 = 0x000000ec; +_defineConstant("WIN_VK_OEM_PA2", WIN_VK_OEM_PA2); +const WIN_VK_OEM_PA3 = 0x000000ed; +_defineConstant("WIN_VK_OEM_PA3", WIN_VK_OEM_PA3); +const WIN_VK_OEM_WSCTRL = 0x000000ee; +_defineConstant("WIN_VK_OEM_WSCTRL", WIN_VK_OEM_WSCTRL); +const WIN_VK_OEM_CUSEL = 0x000000ef; +_defineConstant("WIN_VK_OEM_CUSEL", WIN_VK_OEM_CUSEL); +const WIN_VK_OEM_ATTN = 0x000000f0; +_defineConstant("WIN_VK_OEM_ATTN", WIN_VK_OEM_ATTN); +const WIN_VK_OEM_FINISH = 0x000000f1; +_defineConstant("WIN_VK_OEM_FINISH", WIN_VK_OEM_FINISH); +const WIN_VK_OEM_COPY = 0x000000f2; +_defineConstant("WIN_VK_OEM_COPY", WIN_VK_OEM_COPY); +const WIN_VK_OEM_AUTO = 0x000000f3; +_defineConstant("WIN_VK_OEM_AUTO", WIN_VK_OEM_AUTO); +const WIN_VK_OEM_ENLW = 0x000000f4; +_defineConstant("WIN_VK_OEM_ENLW", WIN_VK_OEM_ENLW); +const WIN_VK_OEM_BACKTAB = 0x000000f5; +_defineConstant("WIN_VK_OEM_BACKTAB", WIN_VK_OEM_BACKTAB); +const WIN_VK_ATTN = 0x000000f6; +_defineConstant("WIN_VK_ATTN", WIN_VK_ATTN); +const WIN_VK_CRSEL = 0x000000f7; +_defineConstant("WIN_VK_CRSEL", WIN_VK_CRSEL); +const WIN_VK_EXSEL = 0x000000f8; +_defineConstant("WIN_VK_EXSEL", WIN_VK_EXSEL); +const WIN_VK_EREOF = 0x000000f9; +_defineConstant("WIN_VK_EREOF", WIN_VK_EREOF); +const WIN_VK_PLAY = 0x000000fa; +_defineConstant("WIN_VK_PLAY", WIN_VK_PLAY); +const WIN_VK_ZOOM = 0x000000fb; +_defineConstant("WIN_VK_ZOOM", WIN_VK_ZOOM); +const WIN_VK_NONAME = 0x000000fc; +_defineConstant("WIN_VK_NONAME", WIN_VK_NONAME); +const WIN_VK_PA1 = 0x000000fd; +_defineConstant("WIN_VK_PA1", WIN_VK_PA1); +const WIN_VK_OEM_CLEAR = 0x000000fe; +_defineConstant("WIN_VK_OEM_CLEAR", WIN_VK_OEM_CLEAR); + +const WIN_VK_NUMPAD_RETURN = 0xe01c000d; +_defineConstant("WIN_VK_NUMPAD_RETURN", WIN_VK_NUMPAD_RETURN); +const WIN_VK_NUMPAD_PRIOR = 0x00490021; +_defineConstant("WIN_VK_NUMPAD_PRIOR", WIN_VK_NUMPAD_PRIOR); +const WIN_VK_NUMPAD_NEXT = 0x00510022; +_defineConstant("WIN_VK_NUMPAD_NEXT", WIN_VK_NUMPAD_NEXT); +const WIN_VK_NUMPAD_END = 0x004f0023; +_defineConstant("WIN_VK_NUMPAD_END", WIN_VK_NUMPAD_END); +const WIN_VK_NUMPAD_HOME = 0x00470024; +_defineConstant("WIN_VK_NUMPAD_HOME", WIN_VK_NUMPAD_HOME); +const WIN_VK_NUMPAD_LEFT = 0x004b0025; +_defineConstant("WIN_VK_NUMPAD_LEFT", WIN_VK_NUMPAD_LEFT); +const WIN_VK_NUMPAD_UP = 0x00480026; +_defineConstant("WIN_VK_NUMPAD_UP", WIN_VK_NUMPAD_UP); +const WIN_VK_NUMPAD_RIGHT = 0x004d0027; +_defineConstant("WIN_VK_NUMPAD_RIGHT", WIN_VK_NUMPAD_RIGHT); +const WIN_VK_NUMPAD_DOWN = 0x00500028; +_defineConstant("WIN_VK_NUMPAD_DOWN", WIN_VK_NUMPAD_DOWN); +const WIN_VK_NUMPAD_INSERT = 0x0052002d; +_defineConstant("WIN_VK_NUMPAD_INSERT", WIN_VK_NUMPAD_INSERT); +const WIN_VK_NUMPAD_DELETE = 0x0053002e; +_defineConstant("WIN_VK_NUMPAD_DELETE", WIN_VK_NUMPAD_DELETE); + +// Mac + +const MAC_VK_ANSI_A = 0x00; +_defineConstant("MAC_VK_ANSI_A", MAC_VK_ANSI_A); +const MAC_VK_ANSI_S = 0x01; +_defineConstant("MAC_VK_ANSI_S", MAC_VK_ANSI_S); +const MAC_VK_ANSI_D = 0x02; +_defineConstant("MAC_VK_ANSI_D", MAC_VK_ANSI_D); +const MAC_VK_ANSI_F = 0x03; +_defineConstant("MAC_VK_ANSI_F", MAC_VK_ANSI_F); +const MAC_VK_ANSI_H = 0x04; +_defineConstant("MAC_VK_ANSI_H", MAC_VK_ANSI_H); +const MAC_VK_ANSI_G = 0x05; +_defineConstant("MAC_VK_ANSI_G", MAC_VK_ANSI_G); +const MAC_VK_ANSI_Z = 0x06; +_defineConstant("MAC_VK_ANSI_Z", MAC_VK_ANSI_Z); +const MAC_VK_ANSI_X = 0x07; +_defineConstant("MAC_VK_ANSI_X", MAC_VK_ANSI_X); +const MAC_VK_ANSI_C = 0x08; +_defineConstant("MAC_VK_ANSI_C", MAC_VK_ANSI_C); +const MAC_VK_ANSI_V = 0x09; +_defineConstant("MAC_VK_ANSI_V", MAC_VK_ANSI_V); +const MAC_VK_ISO_Section = 0x0a; +_defineConstant("MAC_VK_ISO_Section", MAC_VK_ISO_Section); +const MAC_VK_ANSI_B = 0x0b; +_defineConstant("MAC_VK_ANSI_B", MAC_VK_ANSI_B); +const MAC_VK_ANSI_Q = 0x0c; +_defineConstant("MAC_VK_ANSI_Q", MAC_VK_ANSI_Q); +const MAC_VK_ANSI_W = 0x0d; +_defineConstant("MAC_VK_ANSI_W", MAC_VK_ANSI_W); +const MAC_VK_ANSI_E = 0x0e; +_defineConstant("MAC_VK_ANSI_E", MAC_VK_ANSI_E); +const MAC_VK_ANSI_R = 0x0f; +_defineConstant("MAC_VK_ANSI_R", MAC_VK_ANSI_R); +const MAC_VK_ANSI_Y = 0x10; +_defineConstant("MAC_VK_ANSI_Y", MAC_VK_ANSI_Y); +const MAC_VK_ANSI_T = 0x11; +_defineConstant("MAC_VK_ANSI_T", MAC_VK_ANSI_T); +const MAC_VK_ANSI_1 = 0x12; +_defineConstant("MAC_VK_ANSI_1", MAC_VK_ANSI_1); +const MAC_VK_ANSI_2 = 0x13; +_defineConstant("MAC_VK_ANSI_2", MAC_VK_ANSI_2); +const MAC_VK_ANSI_3 = 0x14; +_defineConstant("MAC_VK_ANSI_3", MAC_VK_ANSI_3); +const MAC_VK_ANSI_4 = 0x15; +_defineConstant("MAC_VK_ANSI_4", MAC_VK_ANSI_4); +const MAC_VK_ANSI_6 = 0x16; +_defineConstant("MAC_VK_ANSI_6", MAC_VK_ANSI_6); +const MAC_VK_ANSI_5 = 0x17; +_defineConstant("MAC_VK_ANSI_5", MAC_VK_ANSI_5); +const MAC_VK_ANSI_Equal = 0x18; +_defineConstant("MAC_VK_ANSI_Equal", MAC_VK_ANSI_Equal); +const MAC_VK_ANSI_9 = 0x19; +_defineConstant("MAC_VK_ANSI_9", MAC_VK_ANSI_9); +const MAC_VK_ANSI_7 = 0x1a; +_defineConstant("MAC_VK_ANSI_7", MAC_VK_ANSI_7); +const MAC_VK_ANSI_Minus = 0x1b; +_defineConstant("MAC_VK_ANSI_Minus", MAC_VK_ANSI_Minus); +const MAC_VK_ANSI_8 = 0x1c; +_defineConstant("MAC_VK_ANSI_8", MAC_VK_ANSI_8); +const MAC_VK_ANSI_0 = 0x1d; +_defineConstant("MAC_VK_ANSI_0", MAC_VK_ANSI_0); +const MAC_VK_ANSI_RightBracket = 0x1e; +_defineConstant("MAC_VK_ANSI_RightBracket", MAC_VK_ANSI_RightBracket); +const MAC_VK_ANSI_O = 0x1f; +_defineConstant("MAC_VK_ANSI_O", MAC_VK_ANSI_O); +const MAC_VK_ANSI_U = 0x20; +_defineConstant("MAC_VK_ANSI_U", MAC_VK_ANSI_U); +const MAC_VK_ANSI_LeftBracket = 0x21; +_defineConstant("MAC_VK_ANSI_LeftBracket", MAC_VK_ANSI_LeftBracket); +const MAC_VK_ANSI_I = 0x22; +_defineConstant("MAC_VK_ANSI_I", MAC_VK_ANSI_I); +const MAC_VK_ANSI_P = 0x23; +_defineConstant("MAC_VK_ANSI_P", MAC_VK_ANSI_P); +const MAC_VK_Return = 0x24; +_defineConstant("MAC_VK_Return", MAC_VK_Return); +const MAC_VK_ANSI_L = 0x25; +_defineConstant("MAC_VK_ANSI_L", MAC_VK_ANSI_L); +const MAC_VK_ANSI_J = 0x26; +_defineConstant("MAC_VK_ANSI_J", MAC_VK_ANSI_J); +const MAC_VK_ANSI_Quote = 0x27; +_defineConstant("MAC_VK_ANSI_Quote", MAC_VK_ANSI_Quote); +const MAC_VK_ANSI_K = 0x28; +_defineConstant("MAC_VK_ANSI_K", MAC_VK_ANSI_K); +const MAC_VK_ANSI_Semicolon = 0x29; +_defineConstant("MAC_VK_ANSI_Semicolon", MAC_VK_ANSI_Semicolon); +const MAC_VK_ANSI_Backslash = 0x2a; +_defineConstant("MAC_VK_ANSI_Backslash", MAC_VK_ANSI_Backslash); +const MAC_VK_ANSI_Comma = 0x2b; +_defineConstant("MAC_VK_ANSI_Comma", MAC_VK_ANSI_Comma); +const MAC_VK_ANSI_Slash = 0x2c; +_defineConstant("MAC_VK_ANSI_Slash", MAC_VK_ANSI_Slash); +const MAC_VK_ANSI_N = 0x2d; +_defineConstant("MAC_VK_ANSI_N", MAC_VK_ANSI_N); +const MAC_VK_ANSI_M = 0x2e; +_defineConstant("MAC_VK_ANSI_M", MAC_VK_ANSI_M); +const MAC_VK_ANSI_Period = 0x2f; +_defineConstant("MAC_VK_ANSI_Period", MAC_VK_ANSI_Period); +const MAC_VK_Tab = 0x30; +_defineConstant("MAC_VK_Tab", MAC_VK_Tab); +const MAC_VK_Space = 0x31; +_defineConstant("MAC_VK_Space", MAC_VK_Space); +const MAC_VK_ANSI_Grave = 0x32; +_defineConstant("MAC_VK_ANSI_Grave", MAC_VK_ANSI_Grave); +const MAC_VK_Delete = 0x33; +_defineConstant("MAC_VK_Delete", MAC_VK_Delete); +const MAC_VK_PC_Backspace = 0x33; +_defineConstant("MAC_VK_PC_Backspace", MAC_VK_PC_Backspace); +const MAC_VK_Powerbook_KeypadEnter = 0x34; +_defineConstant("MAC_VK_Powerbook_KeypadEnter", MAC_VK_Powerbook_KeypadEnter); +const MAC_VK_Escape = 0x35; +_defineConstant("MAC_VK_Escape", MAC_VK_Escape); +const MAC_VK_RightCommand = 0x36; +_defineConstant("MAC_VK_RightCommand", MAC_VK_RightCommand); +const MAC_VK_Command = 0x37; +_defineConstant("MAC_VK_Command", MAC_VK_Command); +const MAC_VK_Shift = 0x38; +_defineConstant("MAC_VK_Shift", MAC_VK_Shift); +const MAC_VK_CapsLock = 0x39; +_defineConstant("MAC_VK_CapsLock", MAC_VK_CapsLock); +const MAC_VK_Option = 0x3a; +_defineConstant("MAC_VK_Option", MAC_VK_Option); +const MAC_VK_Control = 0x3b; +_defineConstant("MAC_VK_Control", MAC_VK_Control); +const MAC_VK_RightShift = 0x3c; +_defineConstant("MAC_VK_RightShift", MAC_VK_RightShift); +const MAC_VK_RightOption = 0x3d; +_defineConstant("MAC_VK_RightOption", MAC_VK_RightOption); +const MAC_VK_RightControl = 0x3e; +_defineConstant("MAC_VK_RightControl", MAC_VK_RightControl); +const MAC_VK_Function = 0x3f; +_defineConstant("MAC_VK_Function", MAC_VK_Function); +const MAC_VK_F17 = 0x40; +_defineConstant("MAC_VK_F17", MAC_VK_F17); +const MAC_VK_ANSI_KeypadDecimal = 0x41; +_defineConstant("MAC_VK_ANSI_KeypadDecimal", MAC_VK_ANSI_KeypadDecimal); +const MAC_VK_ANSI_KeypadMultiply = 0x43; +_defineConstant("MAC_VK_ANSI_KeypadMultiply", MAC_VK_ANSI_KeypadMultiply); +const MAC_VK_ANSI_KeypadPlus = 0x45; +_defineConstant("MAC_VK_ANSI_KeypadPlus", MAC_VK_ANSI_KeypadPlus); +const MAC_VK_ANSI_KeypadClear = 0x47; +_defineConstant("MAC_VK_ANSI_KeypadClear", MAC_VK_ANSI_KeypadClear); +const MAC_VK_VolumeUp = 0x48; +_defineConstant("MAC_VK_VolumeUp", MAC_VK_VolumeUp); +const MAC_VK_VolumeDown = 0x49; +_defineConstant("MAC_VK_VolumeDown", MAC_VK_VolumeDown); +const MAC_VK_Mute = 0x4a; +_defineConstant("MAC_VK_Mute", MAC_VK_Mute); +const MAC_VK_ANSI_KeypadDivide = 0x4b; +_defineConstant("MAC_VK_ANSI_KeypadDivide", MAC_VK_ANSI_KeypadDivide); +const MAC_VK_ANSI_KeypadEnter = 0x4c; +_defineConstant("MAC_VK_ANSI_KeypadEnter", MAC_VK_ANSI_KeypadEnter); +const MAC_VK_ANSI_KeypadMinus = 0x4e; +_defineConstant("MAC_VK_ANSI_KeypadMinus", MAC_VK_ANSI_KeypadMinus); +const MAC_VK_F18 = 0x4f; +_defineConstant("MAC_VK_F18", MAC_VK_F18); +const MAC_VK_F19 = 0x50; +_defineConstant("MAC_VK_F19", MAC_VK_F19); +const MAC_VK_ANSI_KeypadEquals = 0x51; +_defineConstant("MAC_VK_ANSI_KeypadEquals", MAC_VK_ANSI_KeypadEquals); +const MAC_VK_ANSI_Keypad0 = 0x52; +_defineConstant("MAC_VK_ANSI_Keypad0", MAC_VK_ANSI_Keypad0); +const MAC_VK_ANSI_Keypad1 = 0x53; +_defineConstant("MAC_VK_ANSI_Keypad1", MAC_VK_ANSI_Keypad1); +const MAC_VK_ANSI_Keypad2 = 0x54; +_defineConstant("MAC_VK_ANSI_Keypad2", MAC_VK_ANSI_Keypad2); +const MAC_VK_ANSI_Keypad3 = 0x55; +_defineConstant("MAC_VK_ANSI_Keypad3", MAC_VK_ANSI_Keypad3); +const MAC_VK_ANSI_Keypad4 = 0x56; +_defineConstant("MAC_VK_ANSI_Keypad4", MAC_VK_ANSI_Keypad4); +const MAC_VK_ANSI_Keypad5 = 0x57; +_defineConstant("MAC_VK_ANSI_Keypad5", MAC_VK_ANSI_Keypad5); +const MAC_VK_ANSI_Keypad6 = 0x58; +_defineConstant("MAC_VK_ANSI_Keypad6", MAC_VK_ANSI_Keypad6); +const MAC_VK_ANSI_Keypad7 = 0x59; +_defineConstant("MAC_VK_ANSI_Keypad7", MAC_VK_ANSI_Keypad7); +const MAC_VK_F20 = 0x5a; +_defineConstant("MAC_VK_F20", MAC_VK_F20); +const MAC_VK_ANSI_Keypad8 = 0x5b; +_defineConstant("MAC_VK_ANSI_Keypad8", MAC_VK_ANSI_Keypad8); +const MAC_VK_ANSI_Keypad9 = 0x5c; +_defineConstant("MAC_VK_ANSI_Keypad9", MAC_VK_ANSI_Keypad9); +const MAC_VK_JIS_Yen = 0x5d; +_defineConstant("MAC_VK_JIS_Yen", MAC_VK_JIS_Yen); +const MAC_VK_JIS_Underscore = 0x5e; +_defineConstant("MAC_VK_JIS_Underscore", MAC_VK_JIS_Underscore); +const MAC_VK_JIS_KeypadComma = 0x5f; +_defineConstant("MAC_VK_JIS_KeypadComma", MAC_VK_JIS_KeypadComma); +const MAC_VK_F5 = 0x60; +_defineConstant("MAC_VK_F5", MAC_VK_F5); +const MAC_VK_F6 = 0x61; +_defineConstant("MAC_VK_F6", MAC_VK_F6); +const MAC_VK_F7 = 0x62; +_defineConstant("MAC_VK_F7", MAC_VK_F7); +const MAC_VK_F3 = 0x63; +_defineConstant("MAC_VK_F3", MAC_VK_F3); +const MAC_VK_F8 = 0x64; +_defineConstant("MAC_VK_F8", MAC_VK_F8); +const MAC_VK_F9 = 0x65; +_defineConstant("MAC_VK_F9", MAC_VK_F9); +const MAC_VK_JIS_Eisu = 0x66; +_defineConstant("MAC_VK_JIS_Eisu", MAC_VK_JIS_Eisu); +const MAC_VK_F11 = 0x67; +_defineConstant("MAC_VK_F11", MAC_VK_F11); +const MAC_VK_JIS_Kana = 0x68; +_defineConstant("MAC_VK_JIS_Kana", MAC_VK_JIS_Kana); +const MAC_VK_F13 = 0x69; +_defineConstant("MAC_VK_F13", MAC_VK_F13); +const MAC_VK_PC_PrintScreen = 0x69; +_defineConstant("MAC_VK_PC_PrintScreen", MAC_VK_PC_PrintScreen); +const MAC_VK_F16 = 0x6a; +_defineConstant("MAC_VK_F16", MAC_VK_F16); +const MAC_VK_F14 = 0x6b; +_defineConstant("MAC_VK_F14", MAC_VK_F14); +const MAC_VK_PC_ScrollLock = 0x6b; +_defineConstant("MAC_VK_PC_ScrollLock", MAC_VK_PC_ScrollLock); +const MAC_VK_F10 = 0x6d; +_defineConstant("MAC_VK_F10", MAC_VK_F10); +const MAC_VK_PC_ContextMenu = 0x6e; +_defineConstant("MAC_VK_PC_ContextMenu", MAC_VK_PC_ContextMenu); +const MAC_VK_F12 = 0x6f; +_defineConstant("MAC_VK_F12", MAC_VK_F12); +const MAC_VK_F15 = 0x71; +_defineConstant("MAC_VK_F15", MAC_VK_F15); +const MAC_VK_PC_Pause = 0x71; +_defineConstant("MAC_VK_PC_Pause", MAC_VK_PC_Pause); +const MAC_VK_Help = 0x72; +_defineConstant("MAC_VK_Help", MAC_VK_Help); +const MAC_VK_PC_Insert = 0x72; +_defineConstant("MAC_VK_PC_Insert", MAC_VK_PC_Insert); +const MAC_VK_Home = 0x73; +_defineConstant("MAC_VK_Home", MAC_VK_Home); +const MAC_VK_PageUp = 0x74; +_defineConstant("MAC_VK_PageUp", MAC_VK_PageUp); +const MAC_VK_ForwardDelete = 0x75; +_defineConstant("MAC_VK_ForwardDelete", MAC_VK_ForwardDelete); +const MAC_VK_PC_Delete = 0x75; +_defineConstant("MAC_VK_PC_Delete", MAC_VK_PC_Delete); +const MAC_VK_F4 = 0x76; +_defineConstant("MAC_VK_F4", MAC_VK_F4); +const MAC_VK_End = 0x77; +_defineConstant("MAC_VK_End", MAC_VK_End); +const MAC_VK_F2 = 0x78; +_defineConstant("MAC_VK_F2", MAC_VK_F2); +const MAC_VK_PageDown = 0x79; +_defineConstant("MAC_VK_PageDown", MAC_VK_PageDown); +const MAC_VK_F1 = 0x7a; +_defineConstant("MAC_VK_F1", MAC_VK_F1); +const MAC_VK_LeftArrow = 0x7b; +_defineConstant("MAC_VK_LeftArrow", MAC_VK_LeftArrow); +const MAC_VK_RightArrow = 0x7c; +_defineConstant("MAC_VK_RightArrow", MAC_VK_RightArrow); +const MAC_VK_DownArrow = 0x7d; +_defineConstant("MAC_VK_DownArrow", MAC_VK_DownArrow); +const MAC_VK_UpArrow = 0x7e; +_defineConstant("MAC_VK_UpArrow", MAC_VK_UpArrow); diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js new file mode 100644 index 0000000000..a237c25103 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -0,0 +1,2267 @@ +/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +// Generally gTestPath should be set by the harness. +/* global gTestPath */ + +/** + * SimpleTest framework object. + * @class + */ +var SimpleTest = {}; +var parentRunner = null; + +// Using a try/catch rather than SpecialPowers.Cu.isRemoteProxy() because +// it doesn't cover the case where an iframe is xorigin but fission is +// not enabled. +let isSameOrigin = function (w) { + try { + w.top.TestRunner; + } catch (e) { + if (e instanceof DOMException) { + return false; + } + } + return true; +}; +let isXOrigin = !isSameOrigin(window); + +// Note: duplicated in browser-test.js . See also bug 1820150. +function isErrorOrException(err) { + // It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)` + // but we don't, so do it ourselves: + if (!err) { + return false; + } + if (err instanceof SpecialPowers.Ci.nsIException) { + return true; + } + try { + let glob = SpecialPowers.Cu.getGlobalForObject(err); + return err instanceof glob.Error; + } catch { + // getGlobalForObject can be upset if it doesn't get passed an object. + // Just do a standard instanceof check using this global and cross fingers: + } + return err instanceof Error; +} + +// In normal test runs, the window that has a TestRunner in its parent is +// the primary window. In single test runs, if there is no parent and there +// is no opener then it is the primary window. +var isSingleTestRun = + parent == window && + !(opener || (window.arguments && window.arguments[0].SimpleTest)); +try { + var isPrimaryTestWindow = + (isXOrigin && parent != window && parent == top) || + (!isXOrigin && (!!parent.TestRunner || isSingleTestRun)); +} catch (e) { + dump( + "TEST-UNEXPECTED-FAIL, Exception caught: " + + e.message + + ", at: " + + e.fileName + + " (" + + e.lineNumber + + "), location: " + + window.location.href + + "\n" + ); +} + +let xOriginRunner = { + init(harnessWindow) { + this.harnessWindow = harnessWindow; + let url = new URL(document.URL); + this.testFile = url.pathname; + this.showTestReport = url.searchParams.get("showTestReport") == "true"; + this.expected = url.searchParams.get("expected"); + }, + callHarnessMethod(applyOn, command, ...params) { + // Message handled by xOriginTestRunnerHandler in TestRunner.js + this.harnessWindow.postMessage( + { + harnessType: "SimpleTest", + applyOn, + command, + params, + }, + "*" + ); + }, + getParameterInfo() { + let url = new URL(document.URL); + return { + currentTestURL: url.searchParams.get("currentTestURL"), + testRoot: url.searchParams.get("testRoot"), + }; + }, + addFailedTest(test) { + this.callHarnessMethod("runner", "addFailedTest", test); + }, + expectAssertions(min, max) { + this.callHarnessMethod("runner", "expectAssertions", min, max); + }, + expectChildProcessCrash() { + this.callHarnessMethod("runner", "expectChildProcessCrash"); + }, + requestLongerTimeout(factor) { + this.callHarnessMethod("runner", "requestLongerTimeout", factor); + }, + _lastAssertionCount: 0, + testFinished(tests) { + var newAssertionCount = SpecialPowers.assertionCount(); + var numAsserts = newAssertionCount - this._lastAssertionCount; + this._lastAssertionCount = newAssertionCount; + this.callHarnessMethod("runner", "addAssertionCount", numAsserts); + this.callHarnessMethod("runner", "testFinished", tests); + }, + structuredLogger: { + info(msg) { + xOriginRunner.callHarnessMethod("logger", "structuredLogger.info", msg); + }, + warning(msg) { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.warning", + msg + ); + }, + error(msg) { + xOriginRunner.callHarnessMethod("logger", "structuredLogger.error", msg); + }, + activateBuffering() { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.activateBuffering" + ); + }, + deactivateBuffering() { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.deactivateBuffering" + ); + }, + testStatus(url, subtest, status, expected, diagnostic, stack) { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.testStatus", + url, + subtest, + status, + expected, + diagnostic, + stack + ); + }, + }, +}; + +// Finds the TestRunner for this test run and the SpecialPowers object (in +// case it is not defined) from a parent/opener window. +// +// Finding the SpecialPowers object is needed when we have ChromePowers in +// harness.xhtml and we need SpecialPowers in the iframe, and also for tests +// like test_focus.xhtml where we open a window which opens another window which +// includes SimpleTest.js. +(function () { + function ancestor(w) { + return w.parent != w + ? w.parent + : w.opener || + (!isXOrigin && + w.arguments && + SpecialPowers.wrap(Window).isInstance(w.arguments[0]) && + w.arguments[0]); + } + + var w = ancestor(window); + while (w && !parentRunner) { + isXOrigin = !isSameOrigin(w); + + if (isXOrigin) { + if (w.parent != w) { + w = w.top; + } + xOriginRunner.init(w); + parentRunner = xOriginRunner; + } + + if (!parentRunner) { + parentRunner = w.TestRunner; + if (!parentRunner && w.wrappedJSObject) { + parentRunner = w.wrappedJSObject.TestRunner; + } + } + w = ancestor(w); + } + + if (parentRunner) { + SimpleTest.harnessParameters = parentRunner.getParameterInfo(); + } +})(); + +/* Helper functions pulled out of various MochiKit modules */ +if (typeof repr == "undefined") { + this.repr = function repr(o) { + if (typeof o == "undefined") { + return "undefined"; + } else if (o === null) { + return "null"; + } + try { + if (typeof o.__repr__ == "function") { + return o.__repr__(); + } else if (typeof o.repr == "function" && o.repr != repr) { + return o.repr(); + } + } catch (e) {} + try { + if ( + typeof o.NAME == "string" && + (o.toString == Function.prototype.toString || + o.toString == Object.prototype.toString) + ) { + return o.NAME; + } + } catch (e) {} + var ostring; + try { + if (o === 0) { + ostring = 1 / o > 0 ? "+0" : "-0"; + } else if (typeof o === "string") { + ostring = JSON.stringify(o); + } else if (Array.isArray(o)) { + ostring = "[" + o.map(val => repr(val)).join(", ") + "]"; + } else { + ostring = o + ""; + } + } catch (e) { + return "[" + typeof o + "]"; + } + if (typeof o == "function") { + o = ostring.replace(/^\s+/, ""); + var idx = o.indexOf("{"); + if (idx != -1) { + o = o.substr(0, idx) + "{...}"; + } + } + return ostring; + }; +} + +/* This returns a function that applies the previously given parameters. + * This is used by SimpleTest.showReport + */ +if (typeof partial == "undefined") { + this.partial = function (func) { + var args = []; + for (let i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + return function () { + if (arguments.length) { + for (let i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + } + func(args); + }; + }; +} + +if (typeof getElement == "undefined") { + this.getElement = function (id) { + return typeof id == "string" ? document.getElementById(id) : id; + }; + this.$ = this.getElement; +} + +SimpleTest._newCallStack = function (path) { + var rval = function callStackHandler() { + var callStack = callStackHandler.callStack; + for (var i = 0; i < callStack.length; i++) { + if (callStack[i].apply(this, arguments) === false) { + break; + } + } + try { + this[path] = null; + } catch (e) { + // pass + } + }; + rval.callStack = []; + return rval; +}; + +if (typeof addLoadEvent == "undefined") { + this.addLoadEvent = function (func) { + var existing = window.onload; + var regfunc = existing; + if ( + !( + typeof existing == "function" && + typeof existing.callStack == "object" && + existing.callStack !== null + ) + ) { + regfunc = SimpleTest._newCallStack("onload"); + if (typeof existing == "function") { + regfunc.callStack.push(existing); + } + window.onload = regfunc; + } + regfunc.callStack.push(func); + }; +} + +function createEl(type, attrs, html) { + //use createElementNS so the xul/xhtml tests have no issues + var el; + if (!document.body) { + el = document.createElementNS("http://www.w3.org/1999/xhtml", type); + } else { + el = document.createElement(type); + } + if (attrs !== null && attrs !== undefined) { + for (var k in attrs) { + el.setAttribute(k, attrs[k]); + } + } + if (html !== null && html !== undefined) { + el.appendChild(document.createTextNode(html)); + } + return el; +} + +/* lots of tests use this as a helper to get css properties */ +if (typeof computedStyle == "undefined") { + this.computedStyle = function (elem, cssProperty) { + elem = getElement(elem); + if (elem.currentStyle) { + return elem.currentStyle[cssProperty]; + } + if (typeof document.defaultView == "undefined" || document === null) { + return undefined; + } + var style = document.defaultView.getComputedStyle(elem); + if (typeof style == "undefined" || style === null) { + return undefined; + } + + var selectorCase = cssProperty.replace(/([A-Z])/g, "-$1").toLowerCase(); + + return style.getPropertyValue(selectorCase); + }; +} + +SimpleTest._tests = []; +SimpleTest._stopOnLoad = true; +SimpleTest._cleanupFunctions = []; +SimpleTest._taskCleanupFunctions = []; +SimpleTest._currentTask = null; +SimpleTest._timeoutFunctions = []; +SimpleTest._inChaosMode = false; +// When using failure pattern file to filter unexpected issues, +// SimpleTest.expected would be an array of [pattern, expected count], +// and SimpleTest.num_failed would be an array of actual counts which +// has the same length as SimpleTest.expected. +SimpleTest.expected = "pass"; +SimpleTest.num_failed = 0; + +SpecialPowers.setAsDefaultAssertHandler(); + +function usesFailurePatterns() { + return Array.isArray(SimpleTest.expected); +} + +/** + * Checks whether there is any failure pattern matches the given error + * message, and if found, bumps the counter of the failure pattern. + * + * @return {boolean} Whether a matched failure pattern is found. + */ +function recordIfMatchesFailurePattern(name, diag) { + let index = SimpleTest.expected.findIndex(([pat, count]) => { + return ( + pat == null || + (typeof name == "string" && name.includes(pat)) || + (typeof diag == "string" && diag.includes(pat)) + ); + }); + if (index >= 0) { + SimpleTest.num_failed[index]++; + return true; + } + return false; +} + +SimpleTest.setExpected = function () { + if (!parentRunner) { + return; + } + if (!Array.isArray(parentRunner.expected)) { + SimpleTest.expected = parentRunner.expected; + } else { + // Assertions are checked by the runner. + SimpleTest.expected = parentRunner.expected.filter( + ([pat]) => pat != "ASSERTION" + ); + SimpleTest.num_failed = new Array(SimpleTest.expected.length); + SimpleTest.num_failed.fill(0); + } +}; +SimpleTest.setExpected(); + +/** + * Something like assert. + **/ +SimpleTest.ok = function (condition, name) { + if (arguments.length > 2) { + const diag = "Too many arguments passed to `ok(condition, name)`"; + SimpleTest.record(false, name, diag); + } else { + SimpleTest.record(condition, name); + } +}; + +SimpleTest.record = function (condition, name, diag, stack, expected) { + var test = { result: !!condition, name, diag }; + let successInfo; + let failureInfo; + if (SimpleTest.expected == "fail") { + if (!test.result) { + SimpleTest.num_failed++; + test.result = true; + } + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else if (!test.result && usesFailurePatterns()) { + if (recordIfMatchesFailurePattern(name, diag)) { + test.result = true; + // Add a mark for unexpected failures suppressed by failure pattern. + name = "[suppressed] " + name; + } + successInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } else if (expected == "fail") { + successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else { + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } + + if (condition) { + stack = null; + } else if (!stack) { + stack = new Error().stack + .replace(/^(.*@)http:\/\/mochi.test:8888\/tests\//gm, " $1") + .split("\n"); + stack.splice(0, 1); + stack = stack.join("\n"); + } + SimpleTest._logResult(test, successInfo, failureInfo, stack); + SimpleTest._tests.push(test); +}; + +/** + * Roughly equivalent to ok(Object.is(a, b), name) + **/ +SimpleTest.is = function (a, b, name) { + // Be lazy and use Object.is til we want to test a browser without it. + var pass = Object.is(a, b); + var diag = pass ? "" : "got " + repr(a) + ", expected " + repr(b); + SimpleTest.record(pass, name, diag); +}; + +SimpleTest.isfuzzy = function (a, b, epsilon, name) { + var pass = a >= b - epsilon && a <= b + epsilon; + var diag = pass + ? "" + : "got " + + repr(a) + + ", expected " + + repr(b) + + " epsilon: +/- " + + repr(epsilon); + SimpleTest.record(pass, name, diag); +}; + +SimpleTest.isnot = function (a, b, name) { + var pass = !Object.is(a, b); + var diag = pass ? "" : "didn't expect " + repr(a) + ", but got it"; + SimpleTest.record(pass, name, diag); +}; + +/** + * Check that the function call throws an exception. + */ +SimpleTest.doesThrow = function (fn, name) { + var gotException = false; + try { + fn(); + } catch (ex) { + gotException = true; + } + ok(gotException, name); +}; + +// --------------- Test.Builder/Test.More todo() ----------------- + +SimpleTest.todo = function (condition, name, diag) { + var test = { result: !!condition, name, diag, todo: true }; + if ( + test.result && + usesFailurePatterns() && + recordIfMatchesFailurePattern(name, diag) + ) { + // Flipping the result to false so we don't get unexpected result. There + // is no perfect way here. A known failure can trigger unexpected pass, + // in which case, tagging it as KNOWN-FAIL probably makes more sense than + // marking it PASS. + test.result = false; + // Add a mark for unexpected failures suppressed by failure pattern. + name = "[suppressed] " + name; + } + var successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + var failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + SimpleTest._logResult(test, successInfo, failureInfo); + SimpleTest._tests.push(test); +}; + +/* + * Returns the absolute URL to a test data file from where tests + * are served. i.e. the file doesn't necessarely exists where tests + * are executed. + * + * (For android, mochitest are executed on the device, while + * all mochitest html (and others) files are served from the test runner + * slave) + */ +SimpleTest.getTestFileURL = function (path) { + var location = window.location; + // Remove mochitest html file name from the path + var remotePath = location.pathname.replace(/\/[^\/]+?$/, ""); + var url = location.origin + remotePath + "/" + path; + return url; +}; + +SimpleTest._getCurrentTestURL = function () { + return ( + (SimpleTest.harnessParameters && + SimpleTest.harnessParameters.currentTestURL) || + (parentRunner && parentRunner.currentTestURL) || + (typeof gTestPath == "string" && gTestPath) || + "unknown test url" + ); +}; + +SimpleTest._forceLogMessageOutput = false; + +/** + * Force all test messages to be displayed. Only applies for the current test. + */ +SimpleTest.requestCompleteLog = function () { + if (!parentRunner || SimpleTest._forceLogMessageOutput) { + return; + } + + parentRunner.structuredLogger.deactivateBuffering(); + SimpleTest._forceLogMessageOutput = true; + + SimpleTest.registerCleanupFunction(function () { + parentRunner.structuredLogger.activateBuffering(); + SimpleTest._forceLogMessageOutput = false; + }); +}; + +SimpleTest._logResult = function (test, passInfo, failInfo, stack) { + var url = SimpleTest._getCurrentTestURL(); + var result = test.result ? passInfo : failInfo; + var diagnostic = test.diag || null; + // BUGFIX : coercing test.name to a string, because some a11y tests pass an xpconnect object + var subtest = test.name ? String(test.name) : null; + var isError = !test.result == !test.todo; + + if (parentRunner) { + if (!result.status || !result.expected) { + if (diagnostic) { + parentRunner.structuredLogger.info(diagnostic); + } + return; + } + + if (isError) { + parentRunner.addFailedTest(url); + } + + parentRunner.structuredLogger.testStatus( + url, + subtest, + result.status, + result.expected, + diagnostic, + stack + ); + } else if (typeof dump === "function") { + var diagMessage = test.name + (test.diag ? " - " + test.diag : ""); + var debugMsg = [result.message, url, diagMessage].join(" | "); + dump(debugMsg + "\n"); + } else { + // Non-Mozilla browser? Just do nothing. + } +}; + +SimpleTest.info = function (name, message) { + var log = message ? name + " | " + message : name; + if (parentRunner) { + parentRunner.structuredLogger.info(log); + } else { + dump(log + "\n"); + } +}; + +/** + * Copies of is and isnot with the call to ok replaced by a call to todo. + **/ + +SimpleTest.todo_is = function (a, b, name) { + var pass = Object.is(a, b); + var diag = pass + ? repr(a) + " should equal " + repr(b) + : "got " + repr(a) + ", expected " + repr(b); + SimpleTest.todo(pass, name, diag); +}; + +SimpleTest.todo_isnot = function (a, b, name) { + var pass = !Object.is(a, b); + var diag = pass + ? repr(a) + " should not equal " + repr(b) + : "didn't expect " + repr(a) + ", but got it"; + SimpleTest.todo(pass, name, diag); +}; + +/** + * Makes a test report, returns it as a DIV element. + **/ +SimpleTest.report = function () { + var passed = 0; + var failed = 0; + var todo = 0; + + var tallyAndCreateDiv = function (test) { + var cls, msg, div; + var diag = test.diag ? " - " + test.diag : ""; + if (test.todo && !test.result) { + todo++; + cls = "test_todo"; + msg = "todo | " + test.name + diag; + } else if (test.result && !test.todo) { + passed++; + cls = "test_ok"; + msg = "passed | " + test.name + diag; + } else { + failed++; + cls = "test_not_ok"; + msg = "failed | " + test.name + diag; + } + div = createEl("div", { class: cls }, msg); + return div; + }; + var results = []; + for (var d = 0; d < SimpleTest._tests.length; d++) { + results.push(tallyAndCreateDiv(SimpleTest._tests[d])); + } + + var summary_class = + // eslint-disable-next-line no-nested-ternary + failed != 0 ? "some_fail" : passed == 0 ? "todo_only" : "all_pass"; + + var div1 = createEl("div", { class: "tests_report" }); + var div2 = createEl("div", { class: "tests_summary " + summary_class }); + var div3 = createEl("div", { class: "tests_passed" }, "Passed: " + passed); + var div4 = createEl("div", { class: "tests_failed" }, "Failed: " + failed); + var div5 = createEl("div", { class: "tests_todo" }, "Todo: " + todo); + div2.appendChild(div3); + div2.appendChild(div4); + div2.appendChild(div5); + div1.appendChild(div2); + for (var t = 0; t < results.length; t++) { + //iterate in order + div1.appendChild(results[t]); + } + return div1; +}; + +/** + * Toggle element visibility + **/ +SimpleTest.toggle = function (el) { + if (computedStyle(el, "display") == "block") { + el.style.display = "none"; + } else { + el.style.display = "block"; + } +}; + +/** + * Toggle visibility for divs with a specific class. + **/ +SimpleTest.toggleByClass = function (cls, evt) { + var children = document.getElementsByTagName("div"); + var elements = []; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var clsName = child.className; + if (!clsName) { + continue; + } + var classNames = clsName.split(" "); + for (var j = 0; j < classNames.length; j++) { + if (classNames[j] == cls) { + elements.push(child); + break; + } + } + } + for (var t = 0; t < elements.length; t++) { + //TODO: again, for-in loop over elems seems to break this + SimpleTest.toggle(elements[t]); + } + if (evt) { + evt.preventDefault(); + } +}; + +/** + * Shows the report in the browser + **/ +SimpleTest.showReport = function () { + var togglePassed = createEl("a", { href: "#" }, "Toggle passed checks"); + var toggleFailed = createEl("a", { href: "#" }, "Toggle failed checks"); + var toggleTodo = createEl("a", { href: "#" }, "Toggle todo checks"); + togglePassed.onclick = partial(SimpleTest.toggleByClass, "test_ok"); + toggleFailed.onclick = partial(SimpleTest.toggleByClass, "test_not_ok"); + toggleTodo.onclick = partial(SimpleTest.toggleByClass, "test_todo"); + var body = document.body; // Handles HTML documents + if (!body) { + // Do the XML thing. + body = document.getElementsByTagNameNS( + "http://www.w3.org/1999/xhtml", + "body" + )[0]; + } + var firstChild = body.childNodes[0]; + var addNode; + if (firstChild) { + addNode = function (el) { + body.insertBefore(el, firstChild); + }; + } else { + addNode = function (el) { + body.appendChild(el); + }; + } + addNode(togglePassed); + addNode(createEl("span", null, " ")); + addNode(toggleFailed); + addNode(createEl("span", null, " ")); + addNode(toggleTodo); + addNode(SimpleTest.report()); + // Add a separator from the test content. + addNode(createEl("hr")); +}; + +/** + * Tells SimpleTest to don't finish the test when the document is loaded, + * useful for asynchronous tests. + * + * When SimpleTest.waitForExplicitFinish is called, + * explicit SimpleTest.finish() is required. + **/ +SimpleTest.waitForExplicitFinish = function () { + SimpleTest._stopOnLoad = false; +}; + +/** + * Multiply the timeout the parent runner uses for this test by the + * given factor. + * + * For example, in a test that may take a long time to complete, using + * "SimpleTest.requestLongerTimeout(5)" will give it 5 times as long to + * finish. + * + * @param {Number} factor + * The multiplication factor to use on the timeout for this test. + */ +SimpleTest.requestLongerTimeout = function (factor) { + if (parentRunner) { + parentRunner.requestLongerTimeout(factor); + } else { + dump( + "[SimpleTest.requestLongerTimeout()] ignoring request, maybe you meant to call the global `requestLongerTimeout` instead?\n" + ); + } +}; + +/** + * Note that the given range of assertions is to be expected. When + * this function is not called, 0 assertions are expected. When only + * one argument is given, that number of assertions are expected. + * + * A test where we expect to have assertions (which should largely be a + * transitional mechanism to get assertion counts down from our current + * situation) can call the SimpleTest.expectAssertions() function, with + * either one or two arguments: one argument gives an exact number + * expected, and two arguments give a range. For example, a test might do + * one of the following: + * + * @example + * + * // Currently triggers two assertions (bug NNNNNN). + * SimpleTest.expectAssertions(2); + * + * // Currently triggers one assertion on Mac (bug NNNNNN). + * if (navigator.platform.indexOf("Mac") == 0) { + * SimpleTest.expectAssertions(1); + * } + * + * // Currently triggers two assertions on all platforms (bug NNNNNN), + * // but intermittently triggers two additional assertions (bug NNNNNN) + * // on Windows. + * if (navigator.platform.indexOf("Win") == 0) { + * SimpleTest.expectAssertions(2, 4); + * } else { + * SimpleTest.expectAssertions(2); + * } + * + * // Intermittently triggers up to three assertions (bug NNNNNN). + * SimpleTest.expectAssertions(0, 3); + */ +SimpleTest.expectAssertions = function (min, max) { + if (parentRunner) { + parentRunner.expectAssertions(min, max); + } +}; + +SimpleTest._flakyTimeoutIsOK = false; +SimpleTest._originalSetTimeout = window.setTimeout; +window.setTimeout = function SimpleTest_setTimeoutShim() { + // Don't break tests that are loaded without a parent runner. + if (parentRunner) { + // Right now, we only enable these checks for mochitest-plain. + switch (SimpleTest.harnessParameters.testRoot) { + case "browser": + case "chrome": + case "a11y": + break; + default: + if ( + !SimpleTest._alreadyFinished && + arguments.length > 1 && + arguments[1] > 0 + ) { + if (SimpleTest._flakyTimeoutIsOK) { + SimpleTest.todo( + false, + "The author of the test has indicated that flaky timeouts are expected. Reason: " + + SimpleTest._flakyTimeoutReason + ); + } else { + SimpleTest.ok( + false, + "Test attempted to use a flaky timeout value " + arguments[1] + ); + } + } + } + } + return SimpleTest._originalSetTimeout.apply(window, arguments); +}; + +/** + * Request the framework to allow usage of setTimeout(func, timeout) + * where ``timeout > 0``. This is required to note that the author of + * the test is aware of the inherent flakiness in the test caused by + * that, and asserts that there is no way around using the magic timeout + * value number for some reason. + * + * Use of this function is **STRONGLY** discouraged. Think twice before + * using it. Such magic timeout values could result in intermittent + * failures in your test, and are almost never necessary! + * + * @param {String} reason + * A string representation of the reason why the test needs timeouts. + * + */ +SimpleTest.requestFlakyTimeout = function (reason) { + SimpleTest.is(typeof reason, "string", "A valid string reason is expected"); + SimpleTest.isnot(reason, "", "Reason cannot be empty"); + SimpleTest._flakyTimeoutIsOK = true; + SimpleTest._flakyTimeoutReason = reason; +}; + +/** + * If the page is not yet loaded, waits for the load event. If the page is + * not yet focused, focuses and waits for the window to be focused. + * If the current page is 'about:blank', then the page is assumed to not + * yet be loaded. Pass true for expectBlankPage to not make this assumption + * if you expect a blank page to be present. + * + * The target object should be specified if it is different than 'window'. The + * actual focused window may be a descendant window of aObject. + * + * @param {Window|browser|BrowsingContext} [aObject] + * Optional object to be focused, and may be any of: + * window - a window object to focus + * browser - a /